From 3eaa9d834e41797221ccf92cf13b5f0d6da0127d Mon Sep 17 00:00:00 2001 From: Daniar Mannanov Date: Wed, 26 Aug 2026 10:39:28 +0300 Subject: [PATCH] docs: structure port notes from the jar bytecode Captures the decoded placement grid, the four legacy frequency reducers, the full ruined-portal findGenerationPoint and findSuitableY algorithms with their exact draw order, the template name arrays, and the fixture marker coordinates that pin the grid to vanilla. This is the working map for the piece-placement ports that follow. --- internal/worldgen/STRUCTURE_NOTES.md | 119 +++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 internal/worldgen/STRUCTURE_NOTES.md diff --git a/internal/worldgen/STRUCTURE_NOTES.md b/internal/worldgen/STRUCTURE_NOTES.md new file mode 100644 index 0000000..c7a7b9d --- /dev/null +++ b/internal/worldgen/STRUCTURE_NOTES.md @@ -0,0 +1,119 @@ +# Structure port notes — ground truth from the 26.1.2 jar + +Everything below was read out of `versions/26.1.2/server-26.1.2.jar` with +javap. It is the working map for porting structure piece generation; treat it +as reference, not as finished code. + +## Placement grid (implemented, see worldgen/structures.go) + +RandomSpreadStructurePlacement.getPotentialStructureChunk: + + regionX = floorDiv(chunkX, spacing) + regionZ = floorDiv(chunkZ, spacing) + r = WorldgenRandom(LegacyRandomSource(0)) + r.setLargeFeatureWithSalt(seed, regionX, regionZ, salt) + span = spacing - separation + offsetX = spread.evaluate(r, span) + offsetZ = spread.evaluate(r, span) + chunk = (regionX*spacing + offsetX, regionZ*spacing + offsetZ) + +RandomSpreadType.evaluate: linear -> nextInt(span); triangular -> +(nextInt(span)+nextInt(span))/2. + +setLargeFeatureWithSalt(seed, x, z, salt): +seed' = x*341873128712 + z*132897987541 + seed + salt, setSeed, zero draws. + +applyAdditionalChunkRestrictions runs only when frequency < 1.0f and calls the +configured reducer shouldGenerate(seed, salt, x, z, frequency): + +- default / probabilityReducer: setLargeFeatureWithSalt(seed, x, z, salt); + nextFloat() < frequency. +- legacy_type_1 / legacyPillagerOutpostReducer: regionX=x>>4, regionZ=z>>4; + setSeed((long)(regionX ^ (regionZ<<4)) ^ seed); nextInt() discarded once; + return nextInt((int)(1.0f/frequency)) == 0. For 0.2f the bound computes to 4. +- legacy_type_2 / legacyArbitrarySaltProbabilityReducer: + setLargeFeatureWithSalt(seed, x, z, **10387320**) — the set's own salt is + ignored; nextFloat() < frequency. Buried treasure uses this with 0.01. +- legacy_type_3 / legacyProbabilityReducerWithDouble: setLargeFeatureSeed(seed, + x, z) (the two-long XOR mix, no salt); nextDouble() < (double)frequency. + Mineshafts use this with 0.004. + +Verified against the fixture on seed 12345: ruined_portals claims exactly +chunk (1,0), where the capture carries the portal's obsidian/gold/crying +obsidian; mineshaft starts land at (-4,1), (-3,10), (4,-1), consistent with +the deep oak-plank cells crossing chunks (0,0)/(1,0). + +Not wired yet: concentric_rings (strongholds only). + +## Ruined portal (decoded, not yet ported) + +RuinedPortalStructure.findGenerationPoint, in draw order: + +1. Setup selection — skipped entirely (zero draws) when setups.size() <= 1; + otherwise sum weights, one nextFloat(), walk setups subtracting + weight/total, take the first that leaves the accumulator negative. +2. properties.airPocket = sample(nextFloat vs setup.airPocketProbability), + where sample returns false at p==0, true at p==1, else nextFloat() < p. +3. Template: nextFloat() < 0.05 picks giant_portal_{1..3}, otherwise + portal_{1..10} (both arrays exactly these names, one nextInt(len)). +4. Rotation = Util.getRandom(Rotation.values(), random) -> one nextInt(4) + over [none, clockwise_90, clockwise_180, counterclockwise_90]. +5. Mirror: nextFloat() < 0.5 -> none, else front_back. +6. pivot = (size.x/2, 0, size.z/2) (Java truncating division). +7. box = template.getBoundingBox(chunkOriginBlockPos, rotation, pivot, mirror). +8. surfaceY = getBaseHeight(box.center.x, box.center.z, + getHeightMapType(setup.placement)) - 1, where the type is OCEAN_FLOOR_WG + for on_ocean_floor and WORLD_SURFACE_WG otherwise. +9. y = findSuitableY(...) below. +10. Stub position = (chunkOriginX, y, chunkOriginZ). The piece consumer sets + properties.cold = setup.canBeCold && + biomeAt(quart pos).coldEnoughToSnow(pos, seaLevel), then adds one + RuinedPortalPiece(templateManager, pos, placement, properties, templateId, + template, rotation, mirror, pivot). + +findSuitableY(random, generator, placement, airPocket, surfaceY, ySpan, box, +accessor, randomState): + + minCut = accessor.minY + 15 + switch placement: + in_nether: + airPocket ? y = randomBetweenInclusive(32, 100) + : nextFloat() < 0.5 ? randomBetweenInclusive(27, 29) + : randomBetweenInclusive(29, 100) + in_mountain: y = getRandomWithinInterval(70, surfaceY - ySpan) + underground: y = getRandomWithinInterval(minCut, surfaceY - ySpan) + partly_buried: y = surfaceY + randomBetweenInclusive(2, 8) + default: y = surfaceY (on_land_surface, on_ocean_floor) + // settle: sample the four CORNER columns of the box as raw base columns + corners = [(minX,minZ),(maxX,minZ),(minX,maxZ),(maxX,maxZ)] as NoiseColumn + type = on_ocean_floor ? OCEAN_FLOOR_WG : WORLD_SURFACE_WG (isOpaque test) + while y > minCut: + opaque = 0 + for column in corners: + if type.isOpaque(column.getBlock(y)): + opaque++ + if opaque == 3: return y // stops inside the scan + y-- + return y + +getRandomWithinInterval(r, a, b): a >= b ? b : randomBetweenInclusive(a, b). +randomBetweenInclusive(r, lo, hi) = lo + nextInt(hi - lo + 1). + +Remaining to read before coding the piece: RuinedPortalPiece.postProcess +(mossiness block swaps, air-pocket carving bounds, lava under portal blocks, +vine/overgrown decoration, cold cracked-cobble substitution), and +StructureTemplate's bounding-box/palette transform math (rotation + front_back +mirror around the pivot). The 13 ruined_portal templates are extracted under +internal/worldgen/data/structure_template/ruined_portal/. + +## Fixture markers worth remembering + +Seed 12345, committed fixture chunks (0,0) (1,0) (0,1) (-1,-1): + +- (1,0): ruined portal — obsidian (17,13,3), crying obsidian (21,13,3), + gold block (19,18,3), chest (17,13,2). +- (0,0)/(1,0): mineshaft planks/fences near y=-41..-42. +- (-1,-1): unexplained cobblestone+mossy floor at y=35 under gravel/water with + two chests nearby — attributed to no replayed feature yet. Candidate leads: + a dungeon whose placement stream we mismatch, or an ocean ruin whose pieces + reach across the chunk border.