Fix shared static variables in structure generation - #14175
Draft
lisolaris wants to merge 1 commit into
Draft
Conversation
Structure starts can be generated in parallel (STRUCTURE_STARTS is marked parallel-capable), but NetherFortressPieces and StrongholdPieces keep their piece-weight state (placeCount, currentPieces, imposedPiece, totalWeight) in shared static fields. Two structures generating concurrently corrupt each other's state: nether fortresses get prematurely sealed off or abnormally extended, strongholds can lose their portal room. Move the state to per-StructureStart instances: the nether fortress copies its weight objects in the StartPiece constructor, and the stronghold tracks per-structure state in StartPiece fields (IdentityHashMap placeCounts, minDepth replaces the anonymous-subclass depth constraints).
lisolaris
force-pushed
the
fix/structure-generation-race
branch
from
August 10, 2026 04:12
214112e to
7394fee
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Fixes #14176: a parallel structure generation race in
NetherFortressPiecesandStrongholdPieces: their piece-weight selection state lives in shared static fields, which is safe only because vanilla structure generation assumes serialized execution.STRUCTURE_STARTSis executed in parallel, so two fortresses (or strongholds) generating on different worker threads can corrupt each other's state — nether fortresses get prematurely sealed off or abnormally extended, strongholds can lose their portal room.Changes
NetherFortressPieces: theStartPiececonstructor copies eachPieceWeightinstead of resetting and sharing the static array elements — the static arrays are never mutated after class loading.StrongholdPieces: the staticcurrentPieces/totalWeight/imposedPiece/PieceWeight.placeCountstate moves to per-StartPieceinstance fields;placeCountis tracked in anIdentityHashMapkeyed by the shared weight object. AminDepthfield is added toPieceWeightto replace thedoPlaceoverrides in theLibrary/PortalRoomanonymous subclasses, so the standard weight logic handles the depth constraints directly rather than copying the weights.StrongholdStructure: the now-redundantresetPieces()call is removed (the reset happens in theStartPiececonstructor).Why IdentityHashMap?
Counts are keyed by the shared weight object's identity — the same semantics as vanilla's
PieceWeight.placeCount, which lives on the object itself. It does not rely onequals/hashCode, so the counting is unaffected ifPieceWeightever overrides them.Why not ThreadLocal?
Paper can modify the vanilla classes directly, so ownership is expressed naturally as per-
StartPiecestate instead of redirecting accesses throughThreadLocal(which the Moonrise fix uses because its mixin handlers cannot reach theStartPiecelocal — see Tuinity/Moonrise#192). The state access points either already take aStartPieceparameter (generatePieceFromSmallDoor) or are parameterised trivially (updatePieceWeight), so no signature chains change.Semantics
Only the state access location changes — no
nextIntcall order or evaluation order is touched, so single-threaded generation stays bit-for-bit identical to vanilla.Verification
The same seed (
5535345) was tested on vanilla server, Paper before the fix, and Paper after the fix. The relevant comparison is Paper after vs vanilla (not Paper after vs Paper before).5535345: the control fortress[18,17](generated alone) is identical to vanilla piece-for-piece; the two concurrently generated fortresses[-6,-5],[-5,7]now match the vanilla tree (castle sections present, seal-off ratio back to vanilla levels).PortalRoomexactly once per structure, theimposedPiece→FiveCrossingchain intact. Note this is a regression check only: the stronghold race cannot be exercised in normal play (sparse start positions) and was not reproduced, so the stronghold fix is preventive — validated by the identical shared-state pattern rather than by observing the failure.Why a separate fix in Paper
Moonrise and Paper have different integration models:
Therefore this fix is implemented independently (a corresponding fix has been prepared in Moonrise) while preserving identical behaviour — both implementations remove cross-structure shared mutable state; the difference is only due to the integration mechanism.
About AI Assistance
The debug and fix process received help from an AI agent, including full code research for the structure generation race, debug log instrumentation, and specific code changes.
Besides, as I'm not a native English speaker, this PR and the companion issue #14176 were polished by LLM.