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
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.
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
LocationComponentfailing 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, includingfinalones:For a
finalfield likeLocationComponent.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: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 toRunTerasologyinbuild-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 restorefinalComponent fields on JDK 17+.LocationComponent(and other components withfinalmutable-value fields) round-trip correctly through save/load without the JVM flag.Technical Notes
finalfield, mutated via its own methods rather than reassignment" pattern - e.g. every JOML type (Vector3f,Quaternionf, etc.) follows this convention and exposes an in-placeset(...)mutator returningthis(already used throughoutLocationComponent.javaitself:position.set(pos),position.set(x, y, z),this.position.set(other.position)).final, don't reassign it viafield.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 aset(...)method on it accepting the deserialized value's type (or a compatible interface) and invoke that instead. Fall back to the current reflectivefield.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).Related
None.