Skip to content

Add UE 5.8 support: custom versions and cooked FField flags removal - #156

Open
stevenlafl wants to merge 1 commit into
atenfyr:masterfrom
stevenlafl:ue5.8-cooked-support
Open

stevenlafl wants to merge 1 commit into
atenfyr:masterfrom
stevenlafl:ue5.8-cooked-support

Conversation

@stevenlafl

@stevenlafl stevenlafl commented Sep 1, 2026

Copy link
Copy Markdown

First off, feel free to close it upon reading the disclosure section. I have a feeling what's done here will be done by someone else in the future anyways; in any case, to get this working fast for anyone comes along and wants it (and the work still isn't done), here it is for those users. I used it when patching DML and Dev Menu for The Last Caretaker and it works in-game after the latest 5.8 update.

Add UE 5.8 support: custom versions and cooked FField flags removal

This PR makes UAssetAPI able to correctly parse UE 5.8-cooked packages, which currently misparse in three related ways.

1. VER_UE5_8 custom version data (CustomVersions.cs)

UE 5.8 did not add any new EUnrealEngineObjectUE5Version entries, so 5.7 and 5.8 share the same ObjectVersionUE5 and were previously indistinguishable: SetEngineVersion(VER_UE5_8) followed by GetEngineVersion() returned VER_UE5_7. 5.8 did, however, bump several custom version enums. This adds the 53 new entries (43x FFortniteMainBranchObjectVersion, 7x FUE5ReleaseStreamObjectVersion, 3x FFortniteReleaseBranchCustomObjectVersion) annotated [Introduced(EngineVersion.VER_UE5_8)], mirrored from the corresponding *ObjectVersions.inl headers in the 5.8 release branch. With these, engine-version round-trips resolve correctly and versioned 5.8 assets can be auto-detected.

2. FField flags are no longer serialized in cooked packages (FField.cs)

As of UE 5.8, FField::Serialize only serializes FlagsPrivate when !Ar.IsFilterEditorOnly() (on load it reads into a discarded dummy under the same condition). Cooked packages are filter-editor-only, so every FField in a 5.8-cooked package is 4 bytes shorter than before. Reading such a package with the old layout shifts every subsequent field in each Function/Class/Struct export's property chain. In-game this class of mismatch manifests as ObjectSerializationError ... Bad name index when older cooked data meets a newer runtime, and in UAssetAPI as garbage SerializedType names.

Added FField.SerializesFieldFlags(UAsset): flags are serialized unless the asset is filter-editor-only and GetEngineVersion() >= VER_UE5_8. Read and Write are gated symmetrically, so 5.7-and-earlier assets and uncooked 5.8 assets are byte-identical to before.

3. UEnum underlying type (EnumExport.cs)

As of UE 5.8, UEnum::Serialize reads/writes a 1-byte underlying type when FFortniteMainBranchObjectVersion >= EnumUnderlyingType (one of the entries added in section 1). Mirrored in UEnum with an EEnumUnderlyingType enum, gated on the same custom version, so Enum/UserDefinedEnum exports in 5.8 assets parse and round-trip correctly.

4. Version resolution fixes (UAsset.cs)

Two pre-existing issues surfaced while making VER_UE5_8 resolvable:

Sentinel alias in GetIntroducedFromCustomVersionValue:

Enum.GetName on a custom version value can resolve to the VersionPlusOne/LatestVersion aliases instead of the real entry sharing that value (which one wins is unspecified for duplicated enum values). Their VER_UE4_AUTOMATIC_VERSION(_PLUS_ONE) attributes then poison the min/max introduction range in GetEngineVersion, forcing the object-version-only fallback. This became visible with this PR (VER_UE5_8 resolution depends on the custom-version range), but is a pre-existing issue; e.g. FInstancedStructCustomVersion resolves its container value to VersionPlusOne today. Now the real entry is preferred, and a bare sentinel match contributes no constraint.

Index-vs-value in GuessCustomVersionFromTypeAndEngineVersion: it returned the index into its name array rather than the enum value, and walked names in Enum.GetNames order (unsigned value order, putting negative sentinels like BeforeCustomVersionWasAdded = -1 last). For FInstancedStructCustomVersion this produced a default container value of 1, which does not exist in that enum. It now walks entries in numeric value order and returns the actual value; containers get 0 for engines 5.3 and newer, and omit the version before it existed.

Testing

  • All 27 existing tests pass unchanged.
  • SetEngineVersion/GetEngineVersion round-trips verified for VER_UE4_27 and VER_UE5_3 through VER_UE5_8.
  • Default custom version containers spot-checked across those versions (e.g. FortniteMain 47/111/134/170/207/225/268; InstancedStruct absent pre-5.3, 0 after).
  • The EnumUnderlyingType gate verified to be inactive at VER_UE5_7 and active at VER_UE5_8.
  • Flags gating verified: 5.7-cooked serializes flags, 5.8-cooked does not, 5.8-uncooked still does.
  • Field-chain layout verified against a commercial UE 5.8.1 title's cooked blueprints (function/class exports parse correctly with the gate; each FField is exactly 4 bytes shorter than its 5.7 counterpart, confirmed against the same title's previous 5.7.4 build).

Disclosure: human/AI split

This work was done in a Claude Code session driven by me. To be transparent about what that means here:

Done by me (human): initiating and directing the investigation (the motivating case was a UE 5.7.4 -> 5.8.1 game update breaking cooked pak mods), providing licensed UE source access for verification, running every in-game test iteration against the 5.8.1 runtime that the failure analysis and final validation rest on, reviewing the full diff and this description, and deciding what to upstream.

Done by the AI assistant: the binary format analysis (diffing cooked exports between the 5.7 and 5.8 cooks of the same title, correlating with the corresponding serializer changes in UE source), authoring the code changes in this PR, and running the test suite and the version-resolution/flags-gating checks described above.

Everything in this PR was verified against real cooked data and the engine source rather than taken on the model's word; I'm happy to answer questions or rework any of it.

…lying type

- Add the 53 custom version entries introduced in UE 5.8 (43x
  FFortniteMainBranchObjectVersion, 7x FUE5ReleaseStreamObjectVersion,
  3x FFortniteReleaseBranchCustomObjectVersion), annotated
  [Introduced(EngineVersion.VER_UE5_8)]. UE 5.8 added no new
  EUnrealEngineObjectUE5Version entries, so these custom versions are
  what makes VER_UE5_8 distinguishable from VER_UE5_7.

- As of UE 5.8, FField::Serialize no longer serializes FlagsPrivate
  into filter-editor-only (cooked) archives; reading a 5.8-cooked
  package with the old layout misparses every Function/Class/Struct
  export's property chain. Add FField.SerializesFieldFlags and gate
  read/write symmetrically.

- As of UE 5.8, UEnum::Serialize reads/writes a 1-byte underlying type
  when FFortniteMainBranchObjectVersion >= EnumUnderlyingType. Mirror
  in UEnum with an EEnumUnderlyingType enum under the same gate.

- Fix GetIntroducedFromCustomVersionValue resolving a value to the
  VersionPlusOne/LatestVersion sentinel aliases; prefer the real entry
  sharing the value, and contribute no version constraint when only a
  sentinel matches. Previously a sentinel's automatic-version attribute
  could poison the introduction range and force fallback resolution.

- Fix GuessCustomVersionFromTypeAndEngineVersion returning an index
  into its name array instead of the enum value, walking names in
  unsigned order (negative sentinels last). FInstancedStructCustomVersion
  previously guessed a container value of 1, which does not exist in
  that enum; it now guesses 0 for engines >= 5.3 and omits the version
  before it existed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant