Skip to content

Component deserialization can't restore final fields on JDK 26 without --enable-final-field-mutation #5345

Description

@soloturn

AI-assisted issue. Filed by agent driven by @soloturn via GDD.

Context

Found while running the game locally under JDK 26 (the project targets 17). Movement appeared broken on load - not an input-binding issue, but the player entity's saved LocationComponent failing to restore.

Problem / Current State

ObjectFieldMapTypeHandler.deserialize() (subsystems/TypeHandlerLibrary/src/main/java/org/terasology/persistence/typeHandling/coreTypes/ObjectFieldMapTypeHandler.java:132-134) reflectively reassigns every non-private field it deserializes, including final ones:

} else {
    field.set(result, fieldValue.get());
}

For a final field like LocationComponent.position (public final Vector3f position = new Vector3f();), this requires the JVM to allow reflection to override final-field write protection. JDK 17-25 allow it with a warning ("Mutating final fields will be blocked in a future release unless final field mutation is enabled"); JDK 26 hard-blocks it:

java.lang.IllegalAccessException: Can not set final org.joml.Vector3f field
org.terasology.engine.logic.location.LocationComponent.position to org.joml.Vector3f
  at ObjectFieldMapTypeHandler.deserialize(ObjectFieldMapTypeHandler.java:133)
  at EntityRestorer.restore(EntityRestorer.java:35)
  at LocalChunkProvider.processReadyChunk(LocalChunkProvider.java:183)

The entity fails to restore its saved position; the game otherwise starts normally, so this is easy to miss and shows up as "movement doesn't work" rather than an obvious crash.

Currently worked around at the JVM launch level (--enable-final-field-mutation=ALL-UNNAMED, added to RunTerasology in build-logic/src/main/kotlin/org/terasology/gradology/exec.kt), but that's a flag, not a fix - it papers over the reflection call rather than avoiding it, and will presumably need re-litigating again whenever the JDK tightens this further.

Acceptance Criteria

  • ObjectFieldMapTypeHandler.deserialize() no longer needs --enable-final-field-mutation (or equivalent) to correctly restore final Component fields on JDK 17+.
  • LocationComponent (and other components with final mutable-value fields) round-trip correctly through save/load without the JVM flag.
  • Existing component serialization tests still pass.

Technical Notes

  • The fields this affects are the "mutable value object held in a final field, mutated via its own methods rather than reassignment" pattern - e.g. every JOML type (Vector3f, Quaternionf, etc.) follows this convention and exposes an in-place set(...) mutator returning this (already used throughout LocationComponent.java itself: position.set(pos), position.set(x, y, z), this.position.set(other.position)).
  • Suggested fix shape: when the target field is final, don't reassign it via field.set(result, newValue). Instead read the existing instance (Object existing = field.get(result) - reading a final field reflectively is unaffected by this JDK restriction, only writing is), and if it's non-null, look for a set(...) method on it accepting the deserialized value's type (or a compatible interface) and invoke that instead. Fall back to the current reflective field.set(...) only if no such in-place setter is found (preserves existing behavior for any field type that doesn't follow this convention, keeping the change additive/low-risk rather than a full replacement).
  • Worth checking whether the same generic-deserialization path is exercised anywhere else in the persistence stack (prefabs, network replication) with the same final-field assumption, since a fix here should probably be consistent across all of them rather than just the save/restore path this was found in.

Related

None.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type: BugIssues reporting and PRs fixing problems

    Type

    No type

    Projects

    Status
    No status
    Status
    ✅ Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions