diff --git a/Assets/Scripts/Gameplay/LegacyCatalogBoundary.cs b/Assets/Scripts/Gameplay/LegacyCatalogBoundary.cs index 38677039c..ee93de72c 100644 --- a/Assets/Scripts/Gameplay/LegacyCatalogBoundary.cs +++ b/Assets/Scripts/Gameplay/LegacyCatalogBoundary.cs @@ -18,7 +18,7 @@ public static class LegacyCatalogBoundary { if (_catalogCache != null) return _catalogCache; - _catalogCache = new CultCache(); + _catalogCache = new CultCache(readOnly: true); _catalogCache.AddBackingStore(new SingleFileMessagePackBackingStore(GetLegacyCatalogPath(gameDataDirectory))); _catalogCache.AddBackingStore(new MultiFileMessagePackBackingStore(gameDataDirectory.FullName), typeof(NameFile)); _catalogCache.PullAllBackingStores(); diff --git a/Assets/Scripts/ServerShared/CultCache/CultCache.cs b/Assets/Scripts/ServerShared/CultCache/CultCache.cs index da264e5a6..581a41855 100644 --- a/Assets/Scripts/ServerShared/CultCache/CultCache.cs +++ b/Assets/Scripts/ServerShared/CultCache/CultCache.cs @@ -49,9 +49,11 @@ public class CultCache // private readonly Dictionary _externalEntries = new Dictionary(); public IEnumerable AllEntries => _entries.Values; + public bool ReadOnly { get; } - public CultCache() + public CultCache(bool readOnly = false) { + ReadOnly = readOnly; DatabaseLinkBase.Cache = this; foreach (var type in typeof(DatabaseEntry).GetAllChildClasses()) @@ -125,13 +127,19 @@ public class CultCache { var typeStore = _typeStores[type]; if(typeStore != source) + { + ThrowIfReadOnlyWrite(); typeStore.Push(entry); + } } else { var masterStore = _backingStores.FirstOrDefault(); if(masterStore != null && masterStore!=source) + { + ThrowIfReadOnlyWrite(); masterStore.Push(entry); + } } _entries[entry.ID] = entry; @@ -195,11 +203,21 @@ public class CultCache _entries.Remove(entry.ID); var type = entry.GetType(); if(_typeStores.ContainsKey(type)) - _typeStores[type].Delete(entry); + { + var typeStore = _typeStores[type]; + if(typeStore != source) + { + ThrowIfReadOnlyWrite(); + typeStore.Delete(entry); + } + } else foreach(var store in _backingStores) { if(store != source) + { + ThrowIfReadOnlyWrite(); store.Delete(entry); + } } foreach (var parentType in type.GetParentTypes()) { @@ -207,6 +225,12 @@ public class CultCache _types[parentType].Remove(entry); } } + + private void ThrowIfReadOnlyWrite() + { + if (ReadOnly) + throw new InvalidOperationException("Legacy CultCache is read-only. Durable state belongs to the Verse state spine."); + } } public interface RealtimeBackingStore @@ -256,7 +280,7 @@ public abstract class MultiFileBackingStore : CacheBackingStore, RealtimeBacking DirectoryInfo = new DirectoryInfo(path); foreach (var type in typeof(DatabaseEntry).GetAllChildClasses()) { - _entryTypeDirectories[type] = DirectoryInfo.CreateSubdirectory(type.Name); + _entryTypeDirectories[type] = new DirectoryInfo(Path.Combine(DirectoryInfo.FullName, type.Name)); } } @@ -283,6 +307,7 @@ public abstract class MultiFileBackingStore : CacheBackingStore, RealtimeBacking var type = entry.GetType(); Entries[entry.ID] = entry; var directory = _entryTypeDirectories[type]; + directory.Create(); File.WriteAllBytes(Path.Combine(directory.FullName, GetFileName(entry)), Serialize(entry)); } @@ -308,6 +333,7 @@ public abstract class MultiFileBackingStore : CacheBackingStore, RealtimeBacking { foreach (var directory in _entryTypeDirectories.Values) { + if (!directory.Exists) continue; var watcher = new FileSystemWatcher(directory.FullName); watcher.Changed += (sender, args) => { diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 93a47bdbd..f37cb189f 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -32,6 +32,9 @@ local CultCache, and legacy UI paths should be migration-only or deleted. `GameData/KeyboardLayouts/*.msgpack` authority paths are disabled or deleted. The old `SavedGame`/`SavedZone` DTOs and `Galaxy` save-loader constructor are deleted. +- `LegacyCatalogBoundary` opens the legacy `CultCache` in read-only mode. Old + MessagePack backing stores may hydrate in-memory domain objects for the + current Unity runtime, but this path cannot push or delete legacy files. - `Economy.Server` now starts the modern `Aetheria.State` CultMesh node and no longer owns RethinkDB/LiteNetLib state. - `Aetheria.State.Import` writes a typed quarantine manifest and migration