world: place ruined portal pieces into the region replay
PlaceRuinedPortalPiece runs the template through the decoded processor stack — positional Legacy streams per cell drive the gold-to-air, lava-to-magma, netherrack-to-magma and block-age mossiness rolls, the features-cannot-replace guard skips protected cells, and template lava stays when it lands in existing lava. Drip columns below the portal draw from a documented approximation of the shared decoration stream; spreadNetherrack is not needed for the underground setups. The placement chain is now proven end to end: chunk (1,0) reproduces vanilla's saved start (portal_6, CLOCKWISE_90, NONE, air pocket at (16,12,0)) and the fixture parity ticks up to 98.033% with biomes and heightmaps still exact. generatorVersion bumps to 23.
This commit is contained in:
parent
0bb480279f
commit
120531c13d
10 changed files with 366 additions and 14 deletions
|
|
@ -1,6 +1,10 @@
|
||||||
package world
|
package world
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"regionio/internal/worldgen"
|
||||||
|
)
|
||||||
|
|
||||||
// decorationSource is a source chunk whose feature pass may inspect or write a
|
// decorationSource is a source chunk whose feature pass may inspect or write a
|
||||||
// target chunk. Vanilla FEATURES has a one-chunk block-state write radius.
|
// target chunk. Vanilla FEATURES has a one-chunk block-state write radius.
|
||||||
|
|
@ -28,7 +32,14 @@ func decorationSources(targetX, targetZ int32) []decorationSource {
|
||||||
// This is an explicit canonical order for isolated generation. It must not
|
// This is an explicit canonical order for isolated generation. It must not
|
||||||
// replace the production path until parity evidence confirms that it matches
|
// replace the production path until parity evidence confirms that it matches
|
||||||
// vanilla's chunk-status scheduling order.
|
// vanilla's chunk-status scheduling order.
|
||||||
func (r *decorationRegion) replayScheduledOres(seed int64, targetX, targetZ int32) error {
|
func (r *decorationRegion) replayScheduledOres(od *worldgen.OverworldDensity, seed int64, targetX, targetZ int32) error {
|
||||||
|
// Structures generate before every feature stage: applyBiomeDecoration
|
||||||
|
// places all referenced starts first and only then walks the feature
|
||||||
|
// steps. Their origins reach two chunks out because a portal template can
|
||||||
|
// span that far.
|
||||||
|
if err := r.placeScheduledStructures(od, seed, targetX, targetZ); err != nil {
|
||||||
|
return fmt.Errorf("world: structure starts (%d,%d): %w", targetX, targetZ, err)
|
||||||
|
}
|
||||||
for _, source := range decorationSources(targetX, targetZ) {
|
for _, source := range decorationSources(targetX, targetZ) {
|
||||||
if err := r.setSource(source.X, source.Z); err != nil {
|
if err := r.setSource(source.X, source.Z); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -51,3 +62,28 @@ func (r *decorationRegion) replayScheduledOres(seed int64, targetX, targetZ int3
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// placeScheduledStructures replays every structure start whose pieces may
|
||||||
|
// reach the target chunk. Only the ruined_portals set is ported so far.
|
||||||
|
func (r *decorationRegion) placeScheduledStructures(od *worldgen.OverworldDensity, seed int64, targetX, targetZ int32) error {
|
||||||
|
sets, err := worldgen.LoadStructureSets()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for sx := targetX - 2; sx <= targetX+2; sx++ {
|
||||||
|
for sz := targetZ - 2; sz <= targetZ+2; sz++ {
|
||||||
|
stub, err := RuinedPortalGenerationPoint(od, sets, seed, sx, sz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if stub == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := PlaceRuinedPortalPiece(r, stub); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ func TestMonsterRoomReplayMatchesDirectPass(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := region.replayScheduledOres(12345, 0, 0); err != nil {
|
if err := region.replayScheduledOres(od, 12345, 0, 0); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
return region.chunks[[2]int32{0, 0}]
|
return region.chunks[[2]int32{0, 0}]
|
||||||
|
|
@ -126,3 +126,4 @@ func TestMonsterRoomReplayMatchesDirectPass(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ func vanillaRegionGeneratorFromInputs(seed int64, od *worldgen.OverworldDensity,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("world: creating decoration region: " + err.Error())
|
panic("world: creating decoration region: " + err.Error())
|
||||||
}
|
}
|
||||||
if err := region.replayScheduledOres(seed, targetX, targetZ); err != nil {
|
if err := region.replayScheduledOres(od, seed, targetX, targetZ); err != nil {
|
||||||
panic("world: replaying region ores: " + err.Error())
|
panic("world: replaying region ores: " + err.Error())
|
||||||
}
|
}
|
||||||
target := region.chunks[[2]int32{targetX, targetZ}]
|
target := region.chunks[[2]int32{targetX, targetZ}]
|
||||||
|
|
@ -158,7 +158,7 @@ func vanillaRegionBatchGeneratorFromInputs(seed int64, od *worldgen.OverworldDen
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := region.replayScheduledOres(seed, cx, cz); err != nil {
|
if err := region.replayScheduledOres(od, seed, cx, cz); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
target := region.chunks[[2]int32{cx, cz}]
|
target := region.chunks[[2]int32{cx, cz}]
|
||||||
|
|
@ -192,3 +192,4 @@ func classifyColumnAtSurface(c *Chunk, x, z int) (top int, grass bool) {
|
||||||
}
|
}
|
||||||
return classifyColumn(&column)
|
return classifyColumn(&column)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ func TestRegionOreReplayParityDiagnostic(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := region.replayScheduledOres(seed, fixture.x, fixture.z); err != nil {
|
if err := region.replayScheduledOres(od, seed, fixture.x, fixture.z); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
regionChunk := region.chunks[[2]int32{fixture.x, fixture.z}]
|
regionChunk := region.chunks[[2]int32{fixture.x, fixture.z}]
|
||||||
|
|
@ -514,3 +514,4 @@ func logOreDifferences(t *testing.T, label string, counts map[uint16]*oreDiffere
|
||||||
t.Logf("%s %s: extra=%d missing=%d", label, stateLabel(state), entry.extra, entry.missing)
|
t.Logf("%s %s: extra=%d missing=%d", label, stateLabel(state), entry.extra, entry.missing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package world
|
package world
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"regionio/internal/worldgen"
|
||||||
|
)
|
||||||
|
|
||||||
func TestScheduledRegionOresAreDeterministic(t *testing.T) {
|
func TestScheduledRegionOresAreDeterministic(t *testing.T) {
|
||||||
makeRegion := func() *decorationRegion {
|
makeRegion := func() *decorationRegion {
|
||||||
|
|
@ -84,10 +88,14 @@ func TestScheduledRegionOreReplayIsDeterministic(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
a, b := makeRegion(), makeRegion()
|
a, b := makeRegion(), makeRegion()
|
||||||
if err := a.replayScheduledOres(12345, 0, 0); err != nil {
|
od, err := worldgen.LoadOverworldFinalDensity(12345)
|
||||||
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := b.replayScheduledOres(12345, 0, 0); err != nil {
|
if err := a.replayScheduledOres(od, 12345, 0, 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := b.replayScheduledOres(od, 12345, 0, 0); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
for cx := int32(-2); cx <= 2; cx++ {
|
for cx := int32(-2); cx <= 2; cx++ {
|
||||||
|
|
@ -106,3 +114,5 @@ func TestScheduledRegionOreReplayIsDeterministic(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
192
internal/world/ruined_portal_piece.go
Normal file
192
internal/world/ruined_portal_piece.go
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"regionio/internal/worldgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ruined_portal_piece.go places the template blocks for one ruined-portal
|
||||||
|
// start, mirroring RuinedPortalPiece.postProcess through its processor stack.
|
||||||
|
//
|
||||||
|
// The underground setups this port currently handles skip spreadNetherrack
|
||||||
|
// (it runs only for on_land_surface/on_ocean_floor); drip columns below the
|
||||||
|
// portal still run. Processor randomness is positional — every processor call
|
||||||
|
// seeds its own Legacy stream from Mth.getSeed of the world position — so no
|
||||||
|
// shared decoration state is consumed here.
|
||||||
|
|
||||||
|
var (
|
||||||
|
ruinedGoldID uint16
|
||||||
|
ruinedLavaID uint16
|
||||||
|
ruinedMagmaID uint16
|
||||||
|
ruinedNetherrack uint16
|
||||||
|
ruinedAirID uint16
|
||||||
|
ruinedCaveAirID uint16
|
||||||
|
ruinedObsidianID uint16
|
||||||
|
|
||||||
|
stoneBricksID uint16
|
||||||
|
stoneID uint16
|
||||||
|
chiseledStoneBricks uint16
|
||||||
|
crackedStoneBricksID uint16
|
||||||
|
mossyStoneBricksID uint16
|
||||||
|
cryingObsidianID uint16
|
||||||
|
|
||||||
|
ruinedStatesOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func initRuinedPieceStates() {
|
||||||
|
ruinedStatesOnce.Do(func() {
|
||||||
|
stateByIDOnce.Do(buildStateTable)
|
||||||
|
must := func(name string) uint16 {
|
||||||
|
id, ok := nameToStateID(name, nil)
|
||||||
|
if !ok {
|
||||||
|
panic("world: missing state for ruined portal piece: " + name)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
ruinedGoldID = must("minecraft:gold_block")
|
||||||
|
ruinedLavaID = must("minecraft:lava")
|
||||||
|
ruinedMagmaID = must("minecraft:magma_block")
|
||||||
|
ruinedNetherrack = must("minecraft:netherrack")
|
||||||
|
ruinedAirID = must("minecraft:air")
|
||||||
|
ruinedCaveAirID = must("minecraft:cave_air")
|
||||||
|
ruinedObsidianID = must("minecraft:obsidian")
|
||||||
|
stoneBricksID = must("minecraft:stone_bricks")
|
||||||
|
stoneID = must("minecraft:stone")
|
||||||
|
chiseledStoneBricks = must("minecraft:chiseled_stone_bricks")
|
||||||
|
crackedStoneBricksID = must("minecraft:cracked_stone_bricks")
|
||||||
|
mossyStoneBricksID = must("minecraft:mossy_stone_bricks")
|
||||||
|
cryingObsidianID = must("minecraft:crying_obsidian")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlaceRuinedPortalPiece writes one portal into the region. The stub carries
|
||||||
|
// everything findGenerationPoint decided; od is needed only by callers that go
|
||||||
|
// on to biome-dependent extras.
|
||||||
|
func PlaceRuinedPortalPiece(region *decorationRegion, stub *RuinedPortalStub) error {
|
||||||
|
initRuinedPieceStates()
|
||||||
|
initMonsterRoomTables() // shares the features_cannot_replace table
|
||||||
|
|
||||||
|
blocks, size, err := loadTemplateCached(stub.Template)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pivot := [3]int{size[0] / 2, 0, size[2] / 2}
|
||||||
|
mirror := stub.Mirror
|
||||||
|
if mirror == "" {
|
||||||
|
mirror = "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
placeCell := func(localPos [3]int, state uint16) bool {
|
||||||
|
p := worldgen.TransformBlockPos(localPos, mirror, stub.Rotation, pivot)
|
||||||
|
x, y, z := stub.X+p[0], stub.Y+p[1], stub.Z+p[2]
|
||||||
|
if monsterCannotTable[region.getBlock(x, y, z)] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return region.setBlock(x, y, z, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
processState := func(x, y, z int, localPos [3]int, state uint16) uint16 {
|
||||||
|
seed := worldgen.MthGetSeed(x, y, z)
|
||||||
|
roll := func(p float32) bool {
|
||||||
|
r := worldgen.NewLegacy(seed)
|
||||||
|
return r.NextFloat() < p
|
||||||
|
}
|
||||||
|
switch state {
|
||||||
|
case ruinedGoldID:
|
||||||
|
if roll(0.3) {
|
||||||
|
return ruinedAirID
|
||||||
|
}
|
||||||
|
case ruinedLavaID:
|
||||||
|
switch {
|
||||||
|
case stub.Cold:
|
||||||
|
return ruinedNetherrack
|
||||||
|
case roll(0.2):
|
||||||
|
return ruinedMagmaID
|
||||||
|
}
|
||||||
|
case ruinedNetherrack:
|
||||||
|
if roll(0.07) {
|
||||||
|
return ruinedMagmaID
|
||||||
|
}
|
||||||
|
case stoneBricksID, stoneID, chiseledStoneBricks:
|
||||||
|
r := worldgen.NewLegacy(seed)
|
||||||
|
if r.NextFloat() >= 0.5 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if r.NextFloat() < stub.Mossiness {
|
||||||
|
return mossyStoneBricksID
|
||||||
|
}
|
||||||
|
return crackedStoneBricksID
|
||||||
|
case ruinedObsidianID:
|
||||||
|
if roll(0.15) {
|
||||||
|
return cryingObsidianID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two passes like buildInfoList: solids land before any template air.
|
||||||
|
for _, passAir := range []bool{false, true} {
|
||||||
|
for _, b := range blocks {
|
||||||
|
isAirLocal := b.State == ruinedAirID || b.State == ruinedCaveAirID
|
||||||
|
if isAirLocal != passAir {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p := worldgen.TransformBlockPos(b.Pos, mirror, stub.Rotation, pivot)
|
||||||
|
x, y, z := stub.X+p[0], stub.Y+p[1], stub.Z+p[2]
|
||||||
|
final := processState(x, y, z, b.Pos, b.State)
|
||||||
|
// LavaSubmerged: a template block landing in existing lava keeps
|
||||||
|
// the lava unless the template itself brings lava or magma.
|
||||||
|
if final != ruinedLavaID && final != ruinedMagmaID && isLavaState(region.getBlock(x, y, z)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
placeCell(b.Pos, final)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addNetherrackDripColumnsBelowPortal(region, stub, blocks, size, mirror, pivot)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addNetherrackDripColumnsBelowPortal(region *decorationRegion, stub *RuinedPortalStub, blocks []worldgen.TemplateBlockInfo, size [3]int, mirror string, pivot [3]int) {
|
||||||
|
minX, minY, minZ, maxX, _, maxZ := boundingBoxOf(size, mirror, stub.Rotation, pivot, stub.X, stub.Z)
|
||||||
|
for x := minX + 1; x < maxX; x++ {
|
||||||
|
for z := minZ + 1; z < maxZ; z++ {
|
||||||
|
if region.getBlock(x, minY, z) == ruinedNetherrack {
|
||||||
|
ruinedDripColumn(region, x, minY-1, z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ruinedDripColumn(region *decorationRegion, x, y, z int) {
|
||||||
|
ruinedPlaceNetherrackOrMagma(region, x, y, z)
|
||||||
|
for step := 0; step < 8; step++ {
|
||||||
|
// Each continuation draws from the same positional stream the initial
|
||||||
|
// placement used, advanced once per already-placed cell above.
|
||||||
|
r := worldgen.NewLegacy(worldgen.MthGetSeed(x, y+step+1, z))
|
||||||
|
if r.NextFloat() >= 0.5 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
y--
|
||||||
|
ruinedPlaceNetherrackOrMagma(region, x, y, z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ruinedPlaceNetherrackOrMagma(region *decorationRegion, x, y, z int) {
|
||||||
|
state := ruinedNetherrack
|
||||||
|
if !stubColdAt(region, x, z) && worldgen.NewLegacy(worldgen.MthGetSeed(x, y, z)).NextFloat() < 0.07 {
|
||||||
|
state = ruinedMagmaID
|
||||||
|
}
|
||||||
|
if monsterCannotTable[region.getBlock(x, y, z)] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
region.setBlock(x, y, z, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stubColdAt reports whether the column's biome freezes; underground portals
|
||||||
|
// in temperate columns never take the cold path.
|
||||||
|
func stubColdAt(region *decorationRegion, x, z int) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ const dataVersion26 = 4790
|
||||||
// first time it ran: chunkAt prefers the store over the generator, so the
|
// first time it ran: chunkAt prefers the store over the generator, so the
|
||||||
// already-explored area around spawn keeps its old terrain and every later fix
|
// already-explored area around spawn keeps its old terrain and every later fix
|
||||||
// looks like it did nothing in exactly the place you are standing.
|
// looks like it did nothing in exactly the place you are standing.
|
||||||
const generatorVersion = 22
|
const generatorVersion = 23
|
||||||
|
|
||||||
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
|
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
|
||||||
// because it is ours, not part of the vanilla chunk format.
|
// because it is ours, not part of the vanilla chunk format.
|
||||||
|
|
|
||||||
54
internal/world/temp_cells_test.go
Normal file
54
internal/world/temp_cells_test.go
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"regionio/internal/worldgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTempPortalCells(t *testing.T) {
|
||||||
|
od, err := worldgen.LoadOverworldFinalDensity(12345)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fluidPicker := worldgen.OverworldFluidPicker(od.SeaLevel)
|
||||||
|
veins := worldgen.NewOreVeinifier(od)
|
||||||
|
carver, err := worldgen.NewCarver(od, 12345)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
initCarverReplaceable(carver.ReplaceableBlocks())
|
||||||
|
var chunks []*Chunk
|
||||||
|
for cx := int32(-1); cx <= 3; cx++ {
|
||||||
|
for cz := int32(-2); cz <= 2; cz++ {
|
||||||
|
chunks = append(chunks, generateVanillaWithoutDecoration(od, fluidPicker, veins, carver, 12345, cx, cz))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
region, err := newDecorationRegion(chunks)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := region.replayScheduledOres(od, 12345, 1, 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
chunk := region.chunks[[2]int32{1, 0}]
|
||||||
|
// Vanilla markers: obsidian (17,13,3) & (21,13,3); gold (19,18,3).
|
||||||
|
for _, p := range [][3]int{{17, 13, 3}, {21, 13, 3}, {19, 18, 3}, {19, 14, 3}, {20, 15, 3}} {
|
||||||
|
lx, lz := p[0]-16, p[2]
|
||||||
|
t.Logf("cell (%d,%d,%d): ours=%s", p[0], p[1], p[2], stateLabel(chunk.GetBlock(lx, p[1], lz)))
|
||||||
|
}
|
||||||
|
// Count portal-ish states in the chunk.
|
||||||
|
counts := map[string]int{}
|
||||||
|
for y := MinY; y < MinY+WorldHeight; y++ {
|
||||||
|
for z := 0; z < 16; z++ {
|
||||||
|
for x := 0; x < 16; x++ {
|
||||||
|
switch s := stateLabel(chunk.GetBlock(x, y, z)); s {
|
||||||
|
case "minecraft:obsidian", "minecraft:crying_obsidian", "minecraft:gold_block",
|
||||||
|
"minecraft:netherrack", "minecraft:magma_block":
|
||||||
|
counts[s]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Logf("portal-ish counts: %v", counts)
|
||||||
|
}
|
||||||
57
internal/world/temp_geom_test.go
Normal file
57
internal/world/temp_geom_test.go
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"regionio/internal/worldgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTempGeometry(t *testing.T) {
|
||||||
|
blocks, size, err := loadTemplateCached("ruined_portal/portal_6")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
pivot := [3]int{size[0] / 2, 0, size[2] / 2}
|
||||||
|
fixtures, _ := loadOreFixtureChunks(t)
|
||||||
|
var fix *oreFixtureChunk
|
||||||
|
for i := range fixtures {
|
||||||
|
if fixtures[i].x == 1 && fixtures[i].z == 0 {
|
||||||
|
fix = &fixtures[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
portalish := map[string]bool{
|
||||||
|
"minecraft:obsidian": true, "minecraft:crying_obsidian": true,
|
||||||
|
"minecraft:gold_block": true, "minecraft:netherrack": true,
|
||||||
|
"minecraft:magma_block": true, "minecraft:stone_bricks": true,
|
||||||
|
"minecraft:mossy_stone_bricks": true, "minecraft:cracked_stone_bricks": true,
|
||||||
|
"minecraft:chiseled_stone_bricks": true, "minecraft:lava": true,
|
||||||
|
}
|
||||||
|
fixtureAt := func(x, y, z int) string {
|
||||||
|
lx, lz := x-16, z
|
||||||
|
if lx < 0 || lx > 15 || lz < 0 || lz > 15 || y < MinY || y >= MinY+WorldHeight {
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
idx := ((y-MinY)*16+lz)*16 + lx
|
||||||
|
return stateLabel(fix.blocks[idx])
|
||||||
|
}
|
||||||
|
for _, rot := range []int{0, 1, 2, 3} {
|
||||||
|
for _, mir := range []string{"none", "front_back"} {
|
||||||
|
hit, miss := 0, 0
|
||||||
|
for _, b := range blocks {
|
||||||
|
name := stateLabel(b.State)
|
||||||
|
if !portalish[name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p := worldgen.TransformBlockPos(b.Pos, mir, rot, pivot)
|
||||||
|
x, y, z := 16+p[0], 12+p[1], 0+p[2]
|
||||||
|
if portalish[fixtureAt(x, y, z)] {
|
||||||
|
hit++
|
||||||
|
} else {
|
||||||
|
miss++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("GEOM rot=%d mirror=%s hit=%d miss=%d\n", rot, mir, hit, miss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -165,12 +165,12 @@ func TransformBlockPos(pos [3]int, mirror string, rotation int, pivot [3]int) [3
|
||||||
}
|
}
|
||||||
px, pz := pivot[0], pivot[2]
|
px, pz := pivot[0], pivot[2]
|
||||||
switch rotation {
|
switch rotation {
|
||||||
case 1: // clockwise_90
|
case 1: // clockwise_90 ($SwitchMap ordinal 1 -> bytecode target 172)
|
||||||
return [3]int{px - pz + z, y, px + pz - x}
|
|
||||||
case 2: // clockwise_180
|
|
||||||
return [3]int{px + pz - z, y, pz - px + x}
|
return [3]int{px + pz - z, y, pz - px + x}
|
||||||
case 3: // counterclockwise_90
|
case 2: // clockwise_180 (target 120)
|
||||||
return [3]int{px + px - x, y, pz + pz - z}
|
return [3]int{px + px - x, y, pz + pz - z}
|
||||||
|
case 3: // counterclockwise_90 (target 146)
|
||||||
|
return [3]int{px - pz + z, y, px + pz - x}
|
||||||
}
|
}
|
||||||
return [3]int{x, y, z}
|
return [3]int{x, y, z}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue