// ┌─────────────────────────────────────────────────────────────────────────────────────┐ // │ "The Hermes On Your Disk!".cs │ // │ │ // │ It's a dump, stupid game archive system, but FAST. │ // │ │ // │ # HOW TO IMPORT? │ // │ │ // │ Just drag into your project :) │ // │ │ // │ # LICENSE │ // │ │ // │ Copyright (c) 2026 Weicao-CatilGrass │ // │ │ // │ Permission is hereby granted, free of charge, to any person obtaining a copy │ // │ of this software and associated documentation files (the "Software"), to deal │ // │ in the Software without restriction, including without limitation the rights │ // │ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell │ // │ copies of the Software, and to permit persons to whom the Software is │ // │ furnished to do so, subject to the following conditions: │ // │ │ // │ The above copyright notice and this permission notice shall be included in all │ // │ copies or substantial portions of the Software. │ // │ │ // │ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR │ // │ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, │ // │ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE │ // │ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER │ // │ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, │ // │ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE │ // │ SOFTWARE. │ // └─────────────────────────────────────────────────────────────────────────────────────┘ using System; using System.Collections.Generic; using System.IO; using System.Linq; #if UNITY_2020_1_OR_NEWER using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif #endif namespace HermesOnDisk { #region Core // ┌───────────┐ // │ USAGE │ // └───────────┘ class Usage { private void YourMain() { // Set save location Hermes.Root = new DirectoryInfo("."); // Read boolean var doorOpened = Hermes.Boolean[2]; // Set boolean Hermes.Boolean[0] = true; // Read float var health = Hermes.Float[0]; // Set float Hermes.Float[0] = 0; // Read name var name = Hermes.String[0]; // Set name Hermes.String[0] = "Peter"; // Save Hermes.Store(); // Garbage collect variable-length data Hermes.GC(); } } #if UNITY_2020_1_OR_NEWER #else public static class HermesOnDisk { /// /// Returns true if there are uncommitted changes in memory. /// public static bool IsDirty => Hermes.IsDirty; /// /// Boolean indexer /// public static Hermes.BoolIndexer Boolean => Hermes.Boolean; /// /// Float indexer /// public static Hermes.FloatIndexer Float => Hermes.Float; /// /// String indexer /// public static Hermes.StringIndexer String => Hermes.String; /// /// Variable-length byte array indexer /// public static Hermes.ByteIndexer Bytes => Hermes.Bytes; /// /// Root directory /// public static DirectoryInfo Root { get => Hermes.Root; set => Hermes.Root = value; } /// /// Store to disk /// public static void Store() => Hermes.Store(); /// /// Asynchronously fully rewrites pack.dat /// public static void AsyncGC() => Hermes.AsyncGC(); /// /// Full rewrite of pack.dat, reclaiming freed space /// public static void GC() => Hermes.GC(); /// /// Backup pack.dat → pack.dat.backup /// public static void BackupPack() => Hermes.BackupPack(); } #endif public static class Hermes { private static HermesPathfinder _hermesPathfinder = new(new DirectoryInfo(".")); /// /// Returns true if there are uncommitted changes in memory. /// public static bool IsDirty => ModifiedBooleans.Count > 0 || ModifiedFloat.Count > 0 || ModifiedByteMap.Count > 0; // ┌─────────────────────────────────────────────┐ // │ Pack file constants (COW + pointer table) │ // └─────────────────────────────────────────────┘ private const string PackDataFile = "pack.dat"; private const string PackIndexFile = "pack.idx"; private const string PackGcFile = "pack.gc"; /// /// Boolean indexer /// public static BoolIndexer Boolean { get; } = new(); /// /// Float indexer /// public static FloatIndexer Float { get; } = new(); /// /// Variable-length byte array indexer /// public static ByteIndexer Bytes { get; } = new(); /// /// String indexer /// public static StringIndexer String { get; } = new(); /// /// Root directory /// public static DirectoryInfo Root { set => _hermesPathfinder = new HermesPathfinder(value); get => _hermesPathfinder.RootDirectory; } /// /// Store to disk /// public static void Store() { if (IsDirty) return; if (ModifiedByteMap.Count > 0) BackupPack(); StoreAll(); } /// /// Backup pack.dat → pack.dat.backup /// public static void BackupPack() { var dataFile = _hermesPathfinder.RootDirectory.JoinToFile(PackDataFile); if (dataFile.Exists) { var backup = new FileInfo(dataFile.FullName + ".backup"); dataFile.CopyTo(backup.FullName, true); } } /// /// Full rewrite of pack.dat, reclaiming freed space /// public static void GC() { GcCore(); } /// /// Asynchronously fully rewrites pack.dat /// public static void AsyncGC() { System.Threading.Tasks.Task.Run(() => GcCore()); } private static void GcCore() { var dataFile = _hermesPathfinder.RootDirectory.JoinToFile(PackDataFile); var idxFile = _hermesPathfinder.RootDirectory.JoinToFile(PackIndexFile); if (!idxFile.Exists) return; long idxLen = idxFile.Length; int numEntries = (int)(idxLen / 8); if (numEntries == 0) return; var liveEntries = new List<(uint index, byte[] data, uint oldLen)>(); bool hasGarbage = false; for (uint i = 0; i < (uint)numEntries; i++) { if (!TryReadPackIndex(i, out uint offset, out uint length) || length == 0) { hasGarbage = true; continue; } if (TryReadSlice(dataFile, offset, length, out byte[] buffer)) liveEntries.Add((i, buffer, length)); else hasGarbage = true; } var gcFile = _hermesPathfinder.RootDirectory.JoinToFile(PackGcFile); if (!hasGarbage && (!gcFile.Exists || gcFile.Length == 0)) return; var tmpDataFile = _hermesPathfinder.RootDirectory.JoinToFile(PackDataFile + ".tmp"); var tmpIdxFile = _hermesPathfinder.RootDirectory.JoinToFile(PackIndexFile + ".tmp"); if (tmpDataFile.Directory != null) tmpDataFile.Directory.Create(); var liveMap = liveEntries.ToDictionary(e => e.index, e => (e.data, e.oldLen)); using (var dataFs = new FileStream(tmpDataFile.FullName, FileMode.Create, FileAccess.Write)) using (var idxFs = new FileStream(tmpIdxFile.FullName, FileMode.Create, FileAccess.Write)) { byte[] indexBuffer = new byte[8]; for (uint i = 0; i < (uint)numEntries; i++) { if (liveMap.TryGetValue(i, out var entry)) { // Live entry: write data to pack, update index with new offset/length uint newOffset = (uint)dataFs.Position; dataFs.Write(entry.data, 0, entry.data.Length); BitConverter.GetBytes(newOffset).CopyTo(indexBuffer, 0); BitConverter.GetBytes((uint)entry.data.Length).CopyTo(indexBuffer, 4); } else { // Dead entry: clear to zero (offset=0, length=0) Array.Clear(indexBuffer, 0, 8); } long idxPos = i * 8; if (idxFs.Length < idxPos + 8) idxFs.SetLength(idxPos + 8); idxFs.Seek(idxPos, SeekOrigin.Begin); idxFs.Write(indexBuffer, 0, 8); } } if (dataFile.Exists) dataFile.Delete(); if (idxFile.Exists) idxFile.Delete(); tmpDataFile.MoveTo(dataFile.FullName); tmpIdxFile.MoveTo(idxFile.FullName); if (gcFile.Exists) gcFile.Delete(); } private static readonly SortedDictionary CacheBooleans = new(); private static readonly SortedDictionary CacheFloats = new(); private static readonly SortedDictionary CacheByteMap = new(); private static readonly HashSet ModifiedBooleans = new(); private static readonly HashSet ModifiedFloat = new(); private static readonly HashSet ModifiedByteMap = new(); /// /// Attempts to read a boolean value at the specified index. /// /// The global index of the boolean. /// When this method returns, contains the boolean value if successful; otherwise, false. /// true if the value was successfully read; otherwise, false. private static bool ReadBool(uint index, out bool value) { // Try to get the boolean from the cache if (CacheBooleans.TryGetValue(index, out value)) { // Cache hit, return success return true; } // Cache miss, attempt to load from disk var file = _hermesPathfinder.GetBoolPathFromIndex(index); var localIndex = _hermesPathfinder.IndexToLocalIndex(index); var offset = localIndex; var length = 1; // Try to read the slice from the file if (TryReadSlice(file, offset, length, out byte[] buffer)) { value = buffer[0] == 1; // Store into cache CacheBooleans[index] = value; return true; } value = false; return false; } /// /// Writes a boolean value at the specified index. /// /// The global index of the boolean. /// The boolean value to write. private static void WriteBool(uint index, bool value) { // Write to cache CacheBooleans[index] = value; // Mark the value as modified ModifiedBooleans.Add(index); } /// /// Attempts to read a float value at the specified index. /// /// The global index of the float. /// When this method returns, contains the float value if successful; otherwise, 0f. /// true if the value was successfully read; otherwise, false. private static bool ReadFloat(uint index, out float value) { // Try to get the float from the cache if (CacheFloats.TryGetValue(index, out value)) { // Cache hit, return success return true; } // Cache miss, attempt to load from disk var file = _hermesPathfinder.GetFloatPathFromIndex(index); var localIndex = _hermesPathfinder.IndexToLocalIndex(index); var offset = localIndex * 4; var length = 4; // Try to read the slice from the file if (TryReadSlice(file, offset, length, out byte[] buffer)) { value = BitConverter.ToSingle(buffer, 0); // Store into cache CacheFloats[index] = value; return true; } value = 0f; return false; } /// /// Writes a float value at the specified index. /// /// The global index of the float. /// The float value to write. private static void WriteFloat(uint index, float value) { // Write to cache CacheFloats[index] = value; // Mark the value as modified ModifiedFloat.Add(index); } /// /// Attempts to read a byte array value at the specified index from the pack file system. /// /// The global index of the byte array. /// When this method returns, contains the byte array if successful; otherwise, null. /// true if the value was successfully read; otherwise, false. private static bool ReadByteArray(uint index, out byte[] value) { if (CacheByteMap.TryGetValue(index, out value)) return true; if (TryReadPackIndex(index, out uint offset, out uint length) && length > 0) { var dataFile = _hermesPathfinder.RootDirectory.JoinToFile(PackDataFile); if (TryReadSlice(dataFile, offset, length, out byte[] buffer)) { value = buffer; CacheByteMap[index] = value; return true; } } value = null; return false; } /// /// Writes a byte array value at the specified index. /// The data is appended to the pack file, and the old space (if any) is marked for garbage collection. /// /// The global index of the byte array. /// The byte array value to write. private static void WriteByteArray(uint index, byte[] value) { if (value != null) CacheByteMap[index] = value; else CacheByteMap.Remove(index); ModifiedByteMap.Add(index); } /// /// Attempts to read a pointer entry from the pack index file. /// /// The entry index in the pointer table. /// When this method returns, contains the offset in pack.dat if successful; otherwise, 0. /// When this method returns, contains the length of the data if successful; otherwise, 0. /// true if the pointer entry was successfully read; otherwise, false. private static bool TryReadPackIndex(uint index, out uint offset, out uint length) { offset = 0; length = 0; var idxFile = _hermesPathfinder.RootDirectory.JoinToFile(PackIndexFile); if (!idxFile.Exists) return false; long pos = index * 8; if (pos + 8 > idxFile.Length) return false; byte[] buffer = new byte[8]; using var fs = idxFile.OpenRead(); fs.Seek(pos, SeekOrigin.Begin); if (fs.Read(buffer, 0, 8) != 8) return false; offset = BitConverter.ToUInt32(buffer, 0); length = BitConverter.ToUInt32(buffer, 4); return true; } /// /// Writes a pointer entry (offset and length) to the pack index file at the specified index. /// /// The entry index in the pointer table. /// The offset in pack.dat where the data begins. /// The length of the data in bytes. private static void WritePackIndex(uint index, uint offset, uint length) { var idxFile = _hermesPathfinder.RootDirectory.JoinToFile(PackIndexFile); if (idxFile.Directory != null) idxFile.Directory.Create(); long pos = index * 8; long needed = pos + 8; using var fs = new FileStream(idxFile.FullName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); if (fs.Length < needed) fs.SetLength(needed); byte[] buffer = new byte[8]; BitConverter.GetBytes(offset).CopyTo(buffer, 0); BitConverter.GetBytes(length).CopyTo(buffer, 4); fs.Seek(pos, SeekOrigin.Begin); fs.Write(buffer, 0, 8); } /// /// Appends data to the end of the pack data file and returns the offset and length of the appended data. /// /// The byte array to append to the pack data file. /// A tuple containing the offset (position in file) and length of the appended data. private static (uint offset, uint length) AppendToPack(byte[] data) { var dataFile = _hermesPathfinder.RootDirectory.JoinToFile(PackDataFile); if (dataFile.Directory != null) dataFile.Directory.Create(); using var fs = new FileStream(dataFile.FullName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); fs.Seek(0, SeekOrigin.End); uint offset = (uint)fs.Position; uint length = (uint)data.Length; fs.Write(data, 0, data.Length); return (offset, length); } /// /// Appends a garbage collection record (offset and length) to the GC file. /// These records mark regions in pack.dat that are no longer in use and can be reclaimed during GC. /// /// The offset of the stale data in pack.dat. /// The length of the stale data in bytes. private static void AppendToGc(uint offset, uint length) { var gcFile = _hermesPathfinder.RootDirectory.JoinToFile(PackGcFile); gcFile.Directory.Create(); byte[] buffer = new byte[8]; BitConverter.GetBytes(offset).CopyTo(buffer, 0); BitConverter.GetBytes(length).CopyTo(buffer, 4); using var fs = new FileStream(gcFile.FullName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); fs.Seek(0, SeekOrigin.End); fs.Write(buffer, 0, 8); } /// /// Store all modifications to disk /// private static void StoreAll() { // Handle booleans if (ModifiedBooleans.Count > 0) { uint[] indices = ModifiedBooleans.ToArray(); bool[] values = new bool[indices.Length]; for (int i = 0; i < indices.Length; i++) values[i] = CacheBooleans[indices[i]]; HelperWriteValueToFile(indices, values); ModifiedBooleans.Clear(); } // Handle floats if (ModifiedFloat.Count > 0) { uint[] indices = ModifiedFloat.ToArray(); float[] values = new float[indices.Length]; for (int i = 0; i < indices.Length; i++) values[i] = CacheFloats[indices[i]]; HelperWriteValueToFile(indices, values); ModifiedFloat.Clear(); } // Handle variable-length byte arrays if (ModifiedByteMap.Count > 0) { uint[] indices = ModifiedByteMap.ToArray(); foreach (uint index in indices) { // Read old pointer -> mark old space for GC if (TryReadPackIndex(index, out uint oldOffset, out uint oldLen) && oldLen > 0) AppendToGc(oldOffset, oldLen); if (CacheByteMap.TryGetValue(index, out byte[] data) && data != null) { // Append new data to pack.dat var (newOffset, newLen) = AppendToPack(data); // Update pack.idx pointer WritePackIndex(index, newOffset, newLen); } else { // Value is null (deleted): write zero offset/length to mark as empty slot WritePackIndex(index, 0, 0); CacheByteMap.Remove(index); } } ModifiedByteMap.Clear(); } } /// /// Writes data blocks to files /// /// List of global indices to write /// List of values to write /// Type to write /// true if successful; otherwise, false private static bool HelperWriteValueToFile(uint[] globalIndices, T[] values) { if (globalIndices == null || values == null || globalIndices.Length != values.Length || globalIndices.Length == 0) return false; int typeSize; Func getFileByIndex; if (typeof(T) == typeof(float)) { typeSize = 4; getFileByIndex = _hermesPathfinder.GetFloatPathFromIndex; } else if (typeof(T) == typeof(bool)) { typeSize = 1; getFileByIndex = _hermesPathfinder.GetBoolPathFromIndex; } else return false; var groups = new Dictionary localIndices, List values)>(); for (int i = 0; i < globalIndices.Length; i++) { uint globalIdx = globalIndices[i]; FileInfo file = getFileByIndex(globalIdx); string fileKey = file.FullName; uint localIdx = _hermesPathfinder.IndexToLocalIndex(globalIdx); if (!groups.TryGetValue(fileKey, out var group)) { group = (new List(), new List()); groups[fileKey] = group; } group.localIndices.Add(localIdx); group.values.Add(values[i]); } bool allSuccess = true; foreach (var kvp in groups) { FileInfo file = new FileInfo(kvp.Key); var localIndices = kvp.Value.localIndices.ToArray(); var vals = kvp.Value.values.ToArray(); if (!WriteValuesToFile(localIndices, vals, file, typeSize)) allSuccess = false; } return allSuccess; } /// /// Writes data blocks that belong to the same file /// private static bool WriteValuesToFile(uint[] localIndices, T[] values, FileInfo file, int typeSize) { try { const int maxLocalIndex = 65535; long totalSize = (maxLocalIndex + 1) * typeSize; file.Directory?.Create(); using var fs = new FileStream(file.FullName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); if (fs.Length != totalSize) fs.SetLength(totalSize); for (int i = 0; i < localIndices.Length; i++) { int idx = Convert.ToInt32(localIndices[i]); if (idx < 0 || idx > maxLocalIndex) continue; long offset = idx * typeSize; fs.Seek(offset, SeekOrigin.Begin); // Fascinating! var bytes = typeof(T) == typeof(float) ? BitConverter.GetBytes( (float)(object) values[i]) : new[] { (byte)( (bool)(object) values[i] ? 1 : 0 ) }; fs.Write(bytes, 0, bytes.Length); } return true; } catch { return false; } } /// /// Attempts to read a slice of data from a file /// /// The file /// Offset value /// Length to read /// Output buffer /// private static bool TryReadSlice(FileInfo file, uint offset, long length, out byte[] buffer) { buffer = null; if (file == null || !file.Exists) return false; try { long fileLength = file.Length; if (offset + length > fileLength) return false; buffer = new byte[length]; using (FileStream fs = file.OpenRead()) { fs.Seek(offset, SeekOrigin.Begin); int bytesRead = 0; while (bytesRead < length) { int read = fs.Read(buffer, bytesRead, (int)(length - bytesRead)); if (read == 0) { buffer = null; return false; } bytesRead += read; } } return true; } catch (Exception) { buffer = null; return false; } } public class BoolIndexer { public bool this[uint index] { get => ReadBool(index, out var value) && value; set => WriteBool(index, value); } } public class FloatIndexer { public float this[uint index] { get => ReadFloat(index, out var value) ? value : 0f; set => WriteFloat(index, value); } } public class ByteIndexer { public byte[] this[uint index] { get => ReadByteArray(index, out var value) ? value : null; set => WriteByteArray(index, value); } } public class StringIndexer { public string this[uint index] { get { var data = Hermes.Bytes[index]; return data != null ? System.Text.Encoding.UTF8.GetString(data) : null; } set => Hermes.Bytes[index] = value != null ? System.Text.Encoding.UTF8.GetBytes(value) : null; } } } /// /// HermesPathfinder /// Type used for locating data positions /// public class HermesPathfinder { private readonly DirectoryInfo _rootDirectory; /// /// Get root directory /// public DirectoryInfo RootDirectory => _rootDirectory; public HermesPathfinder(DirectoryInfo rootDirectory) => _rootDirectory = rootDirectory; /// /// Gets the file path of the float data file for the specified data index /// /// Data index (0-based) /// File information containing the float data file for the given index public FileInfo GetFloatPathFromIndex(uint index) => CombinePathByChunkId(IndexToChunkId(index), "f", ".dat"); /// /// Gets the file path of the float data file for the specified chunk ID /// /// Chunk Id (each chunk contains 65536 indices) /// File information containing the float data file for the given chunk public FileInfo GetFloatPathFromChunkId(uint chunkId) => CombinePathByChunkId(chunkId, "f", ".dat"); /// /// Gets the file path of the boolean data file for the specified data index /// /// Data index (0-based) /// File information containing the boolean data file for the given index public FileInfo GetBoolPathFromIndex(uint index) => CombinePathByChunkId(IndexToChunkId(index), "b", ".dat"); /// /// Gets the file path of the boolean data file for the specified chunk ID /// /// Chunk Id (each chunk contains 65536 indices) /// File information containing the boolean data file for the given chunk public FileInfo GetBoolPathFromChunkId(uint chunkId) => CombinePathByChunkId(chunkId, "b", ".dat"); /// /// Convert chunkId to index /// /// File Chunk Id /// Index public uint ChunkIdToIndex(uint chunkId) => chunkId * 65536; /// /// Convert index to chunkId /// /// Index /// File Chunk Id public uint IndexToChunkId(uint index) => index / 65536; /// /// Convert global index to local index within a chunk /// /// Global index /// Local index within the chunk (0–65535) public uint IndexToLocalIndex(uint index) { return index % 65536; } /// /// Convert chunk id and local index to global index /// /// File Chunk Id /// Local index within the chunk /// Global index public uint LocalIndexToIndex(uint chunkId, uint localIndex) { return chunkId * 65536 + localIndex; } private FileInfo CombinePathByChunkId(uint chunkId, string prefix, string suffix) => _rootDirectory.JoinToFile($"{prefix}{chunkId}{suffix}"); } public static class DirectoryInfoExtensions { public static DirectoryInfo Join(this DirectoryInfo dir, params string[] paths) { string full = Path.Combine(dir.FullName, Path.Combine(paths)); return new DirectoryInfo(full); } public static FileInfo JoinToFile(this DirectoryInfo dir, params string[] paths) { string full = Path.Combine(dir.FullName, Path.Combine(paths)); return new FileInfo(full); } } #endregion #region UNITY #if UNITY_2020_1_OR_NEWER public class HermesOnDisk : MonoBehaviour { private enum SaveLocationType { PersistentDataPath, TemporaryCachePath } /// /// Returns true if there are uncommitted changes in memory. /// public static bool IsDirty => Hermes.IsDirty; /// /// Boolean indexer /// public static Hermes.BoolIndexer Boolean => Hermes.Boolean; /// /// Float indexer /// public static Hermes.FloatIndexer Float => Hermes.Float; /// /// String indexer /// public static Hermes.StringIndexer String => Hermes.String; /// /// Variable-length byte array indexer /// public static Hermes.ByteIndexer Bytes => Hermes.Bytes; [Header("Save Location")] [SerializeField] private SaveLocationType saveLocation = SaveLocationType.PersistentDataPath; [SerializeField] private string relativePath = "Saves"; private void Awake() { string basePath = saveLocation switch { SaveLocationType.PersistentDataPath => Application.persistentDataPath, SaveLocationType.TemporaryCachePath => Application.temporaryCachePath, _ => Application.persistentDataPath }; string root = Path.Combine(basePath, relativePath); Hermes.Root = new DirectoryInfo(root); } private void OnDestroy() { Hermes.Store(); } [ContextMenu("GC And Store")] public void DoGCAndSave() { Hermes.GC(); Hermes.Store(); } [ContextMenu("GC")] public void DoGC() { Hermes.GC(); } [ContextMenu("Store")] public void DoStore() { Hermes.Store(); } public void Save() => Hermes.Store(); public void GC() => Hermes.GC(); } #endif #endregion }