Core Save Engine
How gregCore persists mod data without ever endangering vanilla saves. Three layers: SaveGuard (backups + sidecars), mod save APIs (per-mod state), EntityInventory + gregID (stable device identity).
Design principles
Section titled “Design principles”- Vanilla saves stay vanilla. The game only lists
*.save. All mod data goes to sidecars the game ignores. - Back up before first touch. Every save gets a permanent
pre-gregbackup plus rolling timestamped backups (max 3 per save). - One stable identity. Every device gets exactly one invisible
gregID; anything else is converted exactly once. Mods address devices through the inventory, never by screen label.
Layer 1 — SaveGuard (GregSaveGuard)
Section titled “Layer 1 — SaveGuard (GregSaveGuard)”Patches installed via Harmony on SaveSystem.SaveGame / SerializeToBytes / SaveGameData (prefix) and SaveSystem.LoadGame / LoadFromBytes / LoadGameData + WaypointInitializationSystem.LoadNetworkState (postfix):
- On save prefix:
BackupVanillaSave(dir, name)→WriteSidecars(atomic.tmp+.bakswap) →SanitizeSaveData(data, phase)(e.g. clampsnetworkData.sfpModules[].prefabIDto a valid vanilla index or a registered map, so a modded save still loads vanilla). - On load postfix:
LoadSidecarsForCurrentSave()(idempotent per_loadedKey) → inventory rebuild → cable-endpoint healing. - Guarantee: no framework write without a fresh backup first.
SaveGamePrefixwrites sidecars only when the backup succeeded;SaveGameData/SerializeToBytesprefixes back up before the (lossy-for-mods) sanitize mutation. Backup failure → mod writes skipped, vanilla save proceeds.BackupEnabled=falseis an explicit opt-out that also disables mod writes. - Paths: sidecars at
<saveDir>/greg_<modId>.<sanitizedSave>.tsv; backups atDocuments/DatacenterBackups/(pre-greg/permanent +yyyy-MM-dd_HH-mm-ss/pruned, max 3). - Registration for C# mods:
RegisterSidecar(modId, saveFunc, loadFunc)andRegisterVanillaModuleMap(modId, toVanillaFunc, fallbackId).
Layer 2 — mod save APIs
Section titled “Layer 2 — mod save APIs”| API | Scope | Details |
|---|---|---|
greg.config.* (Lua, config.json) |
User-facing settings | get / get_or / set / delete / has / keys |
greg.save.* (Lua, save.json) |
Runtime state | Same methods + save_now() (force flush; set/delete already write through) |
greg.io.* (Lua, <modId>/data/) |
Files | read_file / write_file / append_file / delete_file / file_exists / list_files / read_json / write_json (+ read_text/write_text aliases, data_dir); traversal rejected |
GregConfigService (C#) |
Typed JSON configs | LoadConfig<T> / SaveConfig<T>, auto-creates directories |
GregPersistenceService (C#) |
Key/value JSON | Files in %AppData%/gregCore/Saves/<key>.json, filename-traversal guard |
GregModSave (C#) |
Vanilla-list items | ItemSave{ModFolderName, Position, Rotation, SaveValue, SaveIntArray, …} with Create / Fill / Read / ReadAll / Upsert / Remove on SaveData.modItemData, keyed by modFolderName |
GregModPack (C#) |
Pack snapshots | ShopItem / StaticItem / DllRef / Snapshot DTOs with builder (Create / EnsureLists / AddShopItem / AddStaticItem / AddDll) and reader, incl. managed↔Il2Cpp array bridges |
GregSaveEngine (C#, LiteDB) |
Grid/wall state | LiteDatabase at <saveDir>/gregSave_<guid>.greg.db, collections greg_meta + grid_state; writes gated by GregFeatureGuard.SaveEngine.Write; IsGregSave checks the .greg.db header |
LiteDB (5.0.21) is used only here (GregSaveEngine, WallSaveIntegration) — not as a general store.
Layer 3 — gregID + EntityInventory
Section titled “Layer 3 — gregID + EntityInventory”- Format: exactly one schema —
gregID:<Type>:<12HEX>(e.g.gregID:Switch:…). Assigned live atStart/Awake(GregSwitchIdAssignPatch,GregPatchPanelIdAssignPatch,GregServerIdAssignPatch);CleanIdstrips only numericGetInstanceIDsuffixes, so lettered user names (Core_Switch_A) survive. - Healing on load (
GregNetworkIdHealing, hooked onWaypointInitializationSystem.LoadNetworkState): rewritesswitchID / patchPanelID / serverIDplus cable endpoints, per-entry null-guarded (one bad entry never aborts healing), thenRequestRouteEvaluation(). Raw↔display mapping persists viaSaveSystem.displayToRawMap. - Display separation:
gameObject.namestays vanilla;Server.UpdateServerScreenUI/NetworkSwitch.UpdateScreenUIscrub anygregIDtoken from screens. IDs live in hidden fields + inventory. - EntityInventory (
GregEntityInventory): on load, everything in the save is inventoried — servers, switches, routers, firewalls, patch panels, cables, SFP modules, LACP groups. Each entry gets a stable, player-invisible UID; mods drive things directly (TryFindLive,TryGetUid,GetAll,Rebuiltevent). Servers/switches/patch panels reuse theirgregID; cables/LACP use deterministic UIDs from vanilla IDs; routers/firewalls/SFP persist via sidecar (greg_inventory.<save>.tsv, index-shift repair via hint). - Control: MelonPreferences
gregCore.EntityInventory(Enabled,VerboseLogging,DumpOnRebuild) +Dump()/Verify(). - Guard:
IncompatibleModGuardunpatches the old standalone 404-PersistentID mod (name/assembly match,UnpatchSelfat init + scene load) so gregID is the only ID system. Log + one toast per session.
What mod authors must (not) do
Section titled “What mod authors must (not) do”- Do: keep user settings in
greg.config, runtime state ingreg.save, bulk files ingreg.iosandbox; address devices via inventory UIDs; register sidecars for C# world edits. - Do not: write into vanilla save structures directly, reuse shop/item IDs or GUIDs (see Developer Shop Items), hand-edit sidecars, or cache Il2Cpp objects long-term (GC moves them — see Developer Harmony IL2CPP).
- Deep dives: Developer Data Storage, Developer Hardware IDs Inventory.