0.4.0 update, 2026-09-01 
LocalVault Changelog
Engines UE 5.5 · 5.6 · 5.7 · 5.8 Platform Win64 Tests 21/21 on every engine
At a glance
| Area |
What changed |
Project content detection |
Improved; phantom library rows eliminated. |
Custody correctness |
Directory backups verify. Fixed; manifests bumped to v3. |
Project detection |
Installed content now read from Fab’s own install record. |
Phantom rows |
BuildPatch staging directories are no longer counted as downloads. |
Shipped content |
17 .uasset demo files are moved out of the content root. |
Engine support |
5.5 → 5.8, one package each. |
Platforms |
macOS and Linux are still shelved due to extra work needed. |
Responsiveness |
Session waits, metrics and thumbnails no longer gate the UI. |
Project content detection and phantom library rows
Two independent defects, one shared cause: LocalVault was inferring facts that Fab already writes
down.
① Content added to a project was not always detected
ResolveProjectInstalledAssetPath scored the Fab listing title against the names of top-level
Content/ folders, accepting a match at 65 points. Measured against a real 416-asset library:
| Folder naming |
Detected |
First 1–3 words of the title (WildWestTown) |
89–99% |
A later word in the title (Village, Trooper) |
0% |
An abbreviation (WWT, MVM, UDS) |
0% |
Creator-prefixed (Nimikko_WildWest) |
0% |
Worse, the candidate sweep only looked one level below Content/, while Fab’s non-pack workflows
import into /Game/Fab/Megascans/…, /Game/Fab/<folder> and /Game/Fab/MetaHuman. For all of those
the only name available to match was Fab : so every Megascans, generic 3D and MetaHuman asset was
structurally undetectable, however many times it was re-added.
② Assets listed as local with a GUID for a name
Asset packs are never downloaded into the Fab cache. Fab installs them straight into the project and
points BuildPatchServices’ staging directory at the cache:
Fab "Add to Project" (an asset pack, or a plugin on 5.8)
│
├── installs content ─────────────→ <Project>/Content/<PackFolder>/
│
└── BuildPatch staging ───────────→ <FabCache>/<listing id>/
├── PatchData/
├── Install/
└── Meta/
deleted ONLY on a successful install
↳ cancelled · failed · interrupted ⇒ left behind
↳ LocalVault counted it as a completed download
↳ new database row, titled with the raw GUID ✗
Those rows could never merge with the real library record, because the two ids live in different
namespaces:
owned-library API browser · cache · install record
assetId listing id
5fd9e41aca4a478ca4093d06b37b768d c857a3c1-5be5-4d37-a34f-2158e7da6763
└───────── same asset, different spelling ─────────┘
the only link in the payload:
fabUrl = https://www.fab.com/listings/<listing id>
Compared raw, an asset never matched its own download. Nothing pruned the results, so they persisted
in Database.json across restarts: which is why re-adding never helped.
Fixed
- Project content is now read from Fab’s own install record. Fab writes
UFabLocalAssets::PathsListingID (/Game/<Folder> → listing id) into
EditorPerProjectUserSettings.ini at install time. It is exact, and it covers the nested locations
the non-pack workflows use. Reached through UObject reflection, like FabSettings and
FabBrowserApi already were, because UFabLocalAssets is MinimalAPI and exports no linkable
symbols.
- BuildPatch staging directories are no longer counted as downloads. Recognised by their
PatchData/, Install/, Meta/ and $resumeData contents, and skipped.
- Cached downloads are matched to the library record they belong to. Identifiers are compared in
a normalised form, and the listing id is recovered from the listing URL — the only place the
library payload carries it.
- Records left behind by the above are pruned. A cache-discovered record with no payload, no
backup and no ownership is dropped on the next refresh. Records with a backup are custody and are
always kept.
- A cache entry that genuinely matches nothing in the library is labelled
Unrecognized Fab download (<id>) rather than presenting a raw id as if it were a title.
Changed
- The name-similarity pass remains as a fallback, and now also considers the immediate children of
Content/Fab — the only way to find imports Fab does not register at all, MetaHuman among them.
Notes
Fab’s own install record is not always right. It derives the registered folder from the first
entry of the pack manifest’s file list, so it occasionally files a pack under an
editor-infrastructure folder instead of its content root — observed on this machine as “Core
Systems” registered against /Game/__ExternalActors__ while its content sits in
/Game/CoreSystems. Install records pointing at folders that cannot be a content root are
rejected, and the name matcher is used for those assets instead. Taking the record at face value
would have regressed an asset the old matcher got right.
Tests
LocalVault.Fab.IdentifierBridge — listing-id/asset-id normalization, listing-id recovery from a
URL, /Game/… → disk paths, rejection of implausible install roots.
LocalVault.Fab.StagingDirectoryIsNotADownload — staging areas excluded, real payloads still
included.
LocalVault.Fab.StaleCacheRecordsAreDropped — empty cache records pruned, vaulted and
still-present ones kept.
Upgrade note
The first refresh after upgrading removes cache-discovered records that hold no payload and no
backup. On a machine that has added packs to projects there may be a number of these — every one of
them a row that showed a GUID where a title belongs. Nothing that was ever vaulted is removed.
The plugin shipped 17 “mock” demo packages that proved problematic
Found while validating that shipped assets were compatible with the 5.5 floor. Pre-existing, and
version-independent : reproduced identically on 5.5 and 5.8.
Content/MockData held 17 files named .uasset/.umap that are not Unreal packages. Each is a
9–10 byte text file containing MOCK DATA, magic 0x4B434F4D ("MOCK") rather than 0x9E2A83C1.
Content/MockData/…/SM_Crate_A.uasset ← 10 bytes of "MOCK DATA"
│
│ CanContainContent: true ⇒ mounted as /LocalVault/…
▼
LogUObjectGlobals: Warning: Failed to load '/LocalVault/MockData/…/SM_Crate_A'
…
Failure - 17 error(s)
The data itself was fine; LocalVault only ever treats these as bytes to copy, checksum, verify and
restore for the first-run demo, and never loads them as assets. Only the location was wrong.
Changed
- The sample catalog moved from
Content/MockData/ to Resources/MockData/, which ships but is
never mounted. The realistic .uasset filenames are kept, so the demo still looks like real Fab
content.
CanContainContent is now false. LocalVault ships no engine content. Content/ is gone; it
held nothing else.
Config/FilterPlugin.ini no longer ships /Content/....
Test-PluginDescriptor now requires CanContainContent to be false, inverting the old check.
- The zip audit’s
.uproject exemption moved from Content/ to Resources/.
Tests
LocalVault.Content.MockCatalogIsNotMountedContent : the catalog resolves, sits outside the
mounted content directory, still holds sample directories, and no package file ships inside
Content/.
A new packaging audit fails the build on any .uasset/.umap under the plugin’s Content/.
A filesystem assertion in RunLocalVaultTests.ps1 fails before the editor is even launched.
All three were verified by planting a 10-byte Content/RegressionProbe/Bad.uasset and confirming
each one fails on it.
Upgrade note
5.5 is the compatibility floor. Any real engine content must be authored and saved in 5.5 to
load on all four engines, because Unreal packages are forward-compatible only. Nothing shipped is
affected today — LocalVault ships no engine content at all, and the descriptor now says so.
Engine support: UE 5.8 added
Added
- UE 5.8.2.
Scripts/PackageForFab.ps1 now defaults to four versions and produces four packages
and four submission zips.
- A CI runner preflight that reads
Engine/Build/Build.version at UE_ROOT and fails
immediately if it does not match the matrix row, catching a mislabelled runner in seconds rather
than as a confusing build error minutes in.
Changed
- The CI matrix now covers all four engine versions.
Scripts/RunLocalVaultTests.ps1 reads the engine version from the engine’s own
Engine/Build/Build.version and stamps it into the temporary host project’s EngineAssociation,
instead of hardcoding 5.7. A hardcoded value silently associates the host project with the wrong
engine the moment a second version is supported.
LocalVault.Build.cs comments updated for four versions and for FabLocalAssets.
Notes
- No toolchain logic change was needed.
- 5.8’s Fab plugin does not move anything LocalVault depends on.
- 5.8 adds
FPluginInstallWorkflow, which installs Fab plugins into
Plugins/Marketplace/<PluginName> via a second downloader entry point. Two consequences: it stages
through the Fab cache exactly as pack installs do, so it is a fresh source of the phantom rows
fixed above (the staging exclusion covers it); and Fab does not register plugin installs in
UFabLocalAssets, so those are found by the existing plugin-descriptor sweep, which already walks
Plugins/ and one level below it.
All four engines are fully validated
The full automation suite has now been run on each.
| Check |
5.5 |
5.6 |
5.7 |
5.8 |
| Automation suite (21 tests) |
21/21 |
21/21 |
21/21 |
21/21 |
| Fab package + submission zip |
 |
 |
 |
 |
| Strict-includes build, isolated |
 |
 |
 |
 |
| Compiler warnings |
0 |
0 |
0 |
0 |
| Preferred MSVC toolchain |
14.38.33130 |
14.38.33130 |
14.44.35207 |
14.44.35207 |
Three known gaps closed
Each was closed by making something fail first, then confirming it passes.
The UFabLocalAssets reflection read is now exercised
The install-record lookup above was tested only around its edges — URL parsing, normalization,
install-root filtering. The FMapProperty read itself never ran, and that is exactly the class of
code that compiles identically and behaves differently per engine version.
LocalVault.Fab.InstallRecordReflection substitutes known pairs into the live UFabLocalAssets
CDO through reflection, reads them back through the production code path, and restores the originals.
It asserts:
- the round-trip survives intact
- dashed and 32-hex spellings resolve to the same asset
- nested import paths are preserved
/Game/__ExternalActors__ is rejected
- a library record reaches its install record via its listing URL alone
- an unregistered asset resolves to nothing, not to someone else’s row
The CDO is global and its map is a config property, so the substitution is scoped and restored,
and nothing calls SaveConfig — a test that leaked entries would corrupt the editor’s real record of
installed Fab content.
Confirmed running, not skipping, on all four engines. Fab is EnabledByDefault and the
automation host project mounts it.
The shipped package is now checked for unloadable content
RunLocalVaultTests.ps1 asserts the packaged plugin ships no .uasset/.umap under Content/, and
runs an editor load of the plugin’s mount point when the descriptor declares content.
The editor scan is currently inert, and the script says so. With CanContainContent: false
nothing is mounted, so the scan finds nothing and would pass whatever shipped — proven by planting a
decoy, where it reported Success - 0 error(s).