Merged File Cache Requirements
This page specifies the requirements and goals for the Merged File Cache library.
Standard requirements, e.g. Minimal Code Size apply too.
Automatic Expiration and Cleanup
The cache should automatically handle expiration and cleanup of stale entries.
Entries should be considered if the file has not been accessed within a given period of time.
This should be configurable.
Removal of stale entries should be done after all the mods are loaded
Sometimes the user might not boot the game, say, for 30 days when cache is set to 28 days.
In order to avoid clearing merged files that may be otherwise valid, the cache should clear stale entries only after mod loads have been completed to avoid false hits.
Handle Different Versions of Mods
Cache should be resilient to mod updates.
If a mod is updated to a new version, the cache should not use any entries associated with the previous version of the mod.
The invalidation of old entries can simply be done based on expiry date
No complex logic is needed to detect upgrades/downgrades.
Caching Based on Loadout
Each Cache directory should be specific to the current loadout used in the mod loader and manager.
This is simply achieved by using the native _loader.GetCacheFolder()
method.
TODO: Link Pending
When the user changes the loadout, the loader will provide a different cache directory specific to the loadout. Nothing special to do here.
The loader can provide a cache folder for the mod+loadout
or mod+version+loadout
combination.
In our case, we will use the former and try to handle backwards compatibility ourselves. If it happens that we made a breaking change, we will discard the whole cache folder and start fresh.
TODO: Link Pending
Caches are Scoped Per Mod
Caches must be scoped per mod.
The cache must be scoped per mod. In addition to loadout scoping.
Each mod gets its own cache. This works by having the mods create an instance of the cache with the ModId and version as the constructor parameters.
The caching library then uses an existing cache associated with that ModId+Version, or creates a new one.
Efficient Serialization
Serialization scheme should minimize deserialization time and code size.
Using rkyv for Rust is recommended.
TODO: Use bincode
here, because what we load should be mutable.
In the event a game with many merged files comes along, adding ZStandard compression will also be beneficial. For now however, pure serialization should be sufficient.
Thread Safety
The cache should be thread safe for reading.
Any number of threads should be able to read at the same time, and only one thread should be able to
write. Basically a RwLock
in Rust.
Value Storage
Stored values must adhere to the following rules:
- Values MUST be available on Disk in exact form submitted to cache.
- Not a byte should be modified.
- The values (files) must not move for the lifetime of the cache.
Do not make assumptions about the data. The data may be used as e.g. input to file emulators, or just kept in memory. We don't know.
FileSystem Optimization
Because the files are stored on FileSystem, the files should be laid out in a format that's quick to access.
Avoid putting too many files (> 255) in same folder, as that's slow to access. Instead, group the cached files into directories.
An acceptable strategy here is simply following the directory structure of the original file paths/keys, or creating a new folder every 255 files, whatever works.
We don't expect a single mod to produce >4000 cached files.
It's unlikely any mod will even hit 100.
Error Handling and Logging
The cache must not throw errors under any circumstance.
If the cache metadata/format is invalid or corrupt, simply discard the whole cache and start fresh. Then log the event. Provide any important information for debugging purposes.
Versioning
The cache must not fail after an upgrade.
If the cache format changes, the cache should be able to detect that and either migrate the cache or discard the original cache, starting fresh.