HUNGRY GHOST / PRODUCT DOCUMENTATION

Save Compatibility Lab

Version 1.0.0 · Hungry Ghost · Unreal Engine 5.8 · Windows

Protect the progress your players earned. Keep historical save fixtures, load them with current code, and compare the resulting state with an explicitly approved baseline.

What is included

The tool tests the state your adapter returns. It does not automatically discover a game's save format or migrations, repair broken saves, or restore a world.

Install and try the example

  1. Install the plugin for UE 5.8. For a source installation, put the SaveCompatibilityLab folder under your project's Plugins directory, regenerate project files, and build the Development Editor target with the UE 5.8 supported C++ toolchain.
  2. Enable Save Compatibility Lab in the Plugins window and restart the editor if requested.
  3. Open Tools → Save Compatibility Lab and choose Create examples.
  4. Expect three passing cases and one failing case. The deliberate failure removes a potion from the inventory. Select that row to inspect the changed array count and missing item.
  5. Choose Open report to inspect the same evidence in a browser.

Example creation always makes a new folder below Saved/SaveCompatibilityLab. It never replaces an earlier fixture set. The sample adapter migrates version 1 JSON's coins into current Gold; version 2 uses gold. Currency is serialized as a string to retain 64-bit precision. A separate native fixture uses an actual Unreal GVAS .sav file.

Bundled fixture pack

The plugin includes ready-to-import synthetic test data in Content/SaveCompatibilityLab/Fixtures/ beneath its installed plugin directory. Choose Import suite in the workbench, select suite.json from that folder, then Run suite. Expect three passes and one deliberate missing-potion failure. Use passing-suite.json for the three passing cases only.

These JSON manifests, JSON fixtures, native Unreal .sav fixture and expected snapshot are functional test data, not Content Browser assets. Each manifest resolves its inputs relative to its own folder, so the pack works from an engine-plugin or project-plugin installation. Keep the files together. The runner copies inputs into isolated run folders.

The separate example project contains the same pack at Content/SaveCompatibilityLabExample/Fixtures/. Install the plugin separately, open the example project and import a manifest from that folder. Reports are generated on demand; no placeholder folders or generated reports are shipped.

Author a regression suite

Keep representative saves from your released builds in version control or a private fixture store. Include different player progression stages, inventory contents, quest states and schema versions. Use synthetic or scrubbed player data.

Add save fixture adds a case to the current suite. In the right-hand panel, set its name, save path, adapter class, baseline path and rules. Use Run suite to refresh cases and results after edits.

For a persistent editor asset, create Miscellaneous → Data Asset → SaveLabSuite in the Content Browser. Select it and choose Load selected asset in the workbench. Save changes using the editor's Save All command. For a portable file, use Export suite. Import suite reads the file into an editable in-memory suite; export it again to persist changes.

Asset fixture roots are relative to the project directory. JSON manifest fixture roots are relative to the manifest directory. Absolute paths are also supported. Individual save and baseline paths can be absolute or relative to the fixture root. Export attempts to make the root relative to the exported manifest.

Approve a baseline deliberately

A baseline represents the expected post-load, post-migration state. Capture it with a known-good build and loader, review it, then commit the file. Do not regenerate it automatically when a test fails.

Select a fixture row and choose Capture baseline for selected case. Choose a new .json filename. This command copies the save into a fresh capture folder, invokes the case's adapter, writes the snapshot and assigns the new path to the case. It refuses to overwrite an existing baseline. Review the result before treating it as approved, then save the suite asset or export its manifest.

The lower-level Blueprint/C++ Write Snapshot API writes its specified destination and can overwrite a file; callers are responsible for approval and file management.

Connect your actual game loader

Each case creates a fresh adapter object. Load And Capture receives the absolute path of an isolated copy of the historical save. It returns success, a typed snapshot and an error message.

Implement a child of SaveLabAdapter. Your implementation should:

  1. Read the supplied file rather than a live player save slot.
  2. Invoke your project's real deserialization and migration code.
  3. Validate the migrated state.
  4. Return a snapshot of the state that matters to compatibility.

The function is synchronous. Latent Blueprint operations, asynchronous world restoration and operations that require a running game world need a project-specific harness. The commandlet does not boot gameplay or call your GameInstance save subsystem automatically.

Blueprint integration

Create a Blueprint class derived from SaveLabAdapter, override Load And Capture, and call your project's synchronous load/migrate function with Isolated Save File. Feed the resulting state UObject into Capture Object, or construct SaveLabSnapshot.Fields explicitly. Return false with an actionable error if loading or migration fails. Assign this Blueprint class in each suite case.

Blueprint-only projects can use an installed, compiled plugin. A source-only installation requires the C++ toolchain.

C++ integration

Add SaveCompatibilityLab to your module dependencies. Derive a UCLASS from USaveLabAdapter, declare the override below in its header, and call your own loader in the implementation:

bool UMySaveAdapter::LoadAndCapture_Implementation(
    const FString& IsolatedSaveFile,
    FSaveLabSnapshot& Snapshot,
    FString& Error)
{
    // Consume the supplied copy and run the real migration path.
    UMyCurrentSaveState* State = MyLoadAndMigrate(IsolatedSaveFile, Error);
    if (!State) return false;
    return USaveLabLibrary::CaptureObject(State, true, Snapshot, Error);
}

MyLoadAndMigrate and UMyCurrentSaveState are placeholders for your project's code. The included USaveLabExampleAdapter is a complete executable reference.

Native adapter scope

SaveLabNativeAdapter supports Unreal GVAS SaveGame files through UGameplayStatics::LoadGameFromMemory. The original SaveGame class must be available in the current project, with appropriate redirects when classes move or are renamed.

It captures current reflected properties on the loaded SaveGame object, excluding transient and deprecated fields. It does not automatically invoke migrations stored in a GameInstance, subsystem or separate load manager. Use your own adapter for those, encrypted/compressed/custom formats, third-party save plugins, or world restoration.

Fixtures and adapter code are trusted test inputs. File copies isolate the input files; adapter execution runs inside the Unreal process and is not a security sandbox or crash-isolated worker. A crashing game loader can terminate a suite. A custom adapter that ignores the supplied path can still write to live slots.

Snapshot paths and types

Snapshots use format version 1. A fields map contains typed values encoded as strings:

{
  "formatVersion": 1,
  "fields": {
    "/Gold": {"type": "integer:int64", "value": "9007199254740993"},
    "/Health": {"type": "float", "value": "87.5"},
    "/Inventory": {"type": "array", "value": "2"},
    "/Inventory/0": {"type": "string", "value": "IronSword"},
    "/Inventory/1": {"type": "string", "value": "HealthPotion"}
  }
}

Reflection captures structs recursively, arrays by index, maps by exported key and sets by exported element. Container fields retain their size; structs retain their type. Paths escape / as ~1 and ~ as ~0. Custom map keys should have unique, stable text exports; collisions are errors. Object references use their exported reference representation, not recursive object/world traversal. Use a custom snapshot for stable IDs or normalized unordered collections.

Capture Object can restrict top-level properties to those marked SaveGame. Nested reflected members of an included struct are traversed. Built-in native SaveGame capture includes nontransient reflected properties because Unreal's default SaveGame serialization is not restricted to the SaveGame flag.

Limits: 32 MiB per input JSON/save file, 100,000 snapshot fields, nesting depth 32 for reflection, 4,096 characters per path, and 1,048,576 characters per value. Empty snapshots are rejected. Nonfinite reflected floats are rejected.

Comparison rules and assertions

By default, missing fields, added fields, changed values and changed types fail.

Rule Behavior
Ignore Paths An exact path, or descendants of a path ending in /*. To ignore an array's count and contents, add both /Inventory and /Inventory/*. Differences remain visible as allowed.
Renamed Paths An exact old baseline path mapped to a current path. Both must exist; colliding targets are errors. For a renamed struct, map its container and child paths explicitly.
Float Tolerance Absolute tolerance for float values only. Integer values remain exact strings regardless of tolerance.
Allow Added Fields Permit fields absent from the baseline. Additions remain in the report.
Exists / Absent Require a current path to exist or be absent.
Equals Compare the current value string exactly. Use this for precise 64-bit currency and identifiers.
Number Range Inclusive minimum/maximum for numeric types, using double precision. Use Equals for exact integers beyond double's precision.

Assertions evaluate current state independently of ignored paths. An ignored inventory difference does not suppress a failed inventory assertion. An empty suite, an unsupported suite version, or a suite with every case disabled cannot pass.

Continuous integration

Run the commandlet from your project using its current build:

UnrealEditor-Cmd.exe MyGame.uproject -run=SaveCompatibilityLab -Manifest=C:/Fixtures/suite.json -Output=C:/Reports/SaveLab -unattended -NullRHI -nop4

Or load a suite asset:

UnrealEditor-Cmd.exe MyGame.uproject -run=SaveCompatibilityLab -Suite=/Game/Tests/DA_SaveSuite.DA_SaveSuite -Output=C:/Reports/SaveLab -unattended -NullRHI -nop4

Generate the teaching fixture set:

UnrealEditor-Cmd.exe MyGame.uproject -run=SaveCompatibilityLab -CreateExamples -Output=C:/Fixtures/Examples -unattended -NullRHI -nop4

Exit codes: 0 = suite passed (or example generation succeeded); 1 = failed/invalid suite execution; 2 = configuration, manifest, asset-loading or report-write error. Treat every nonzero code as a failed CI job. An Unreal process crash may return a different nonzero code.

Archive the unique run directory. It includes report.json, report.html, and one UUID folder per executed fixture containing its copy and captured.json when capture succeeds. Reports include engine version, UTC creation time, duration, source SHA-1 fingerprint, differences and assertion failures. SHA-1 is a reproducibility fingerprint, not a security signature.

The tool does not automatically delete reports or fixture copies. Apply your own retention policy to Saved/SaveCompatibilityLab and CI artifacts. These files can contain the saved state; share only intended data.

Performance and supported scope

There is no runtime tick, background scanner, network service or telemetry in the plugin. Capture and suite runs are explicit synchronous operations on the calling thread. Large suites can block the editor; run batch validation in CI. Custom loader costs are project-dependent.

Supported target: UE 5.8, Win64. The runtime library compiles for game targets; the workbench and commandlet require the editor module. Other engines/platforms are not claimed. No third-party libraries, paid dependencies, Blueprint assets, mesh assets, audio, or game-specific content are required.

Troubleshooting

Support: contact Hungry Ghost through the publisher contact information on Fab. Include plugin/engine version, a scrubbed reproduction suite, the relevant error and expected behavior.