Model mutable feature regions
This commit is contained in:
parent
ea66091f85
commit
1a921434d9
2 changed files with 232 additions and 0 deletions
148
internal/world/decoration_region.go
Normal file
148
internal/world/decoration_region.go
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"regionio/internal/worldgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
// decorationRegion is the mutable terrain view used while replaying source
|
||||||
|
// chunk feature passes. A target needs base terrain through radius two: its nine
|
||||||
|
// possible source chunks each inspect biomes in their own radius-one region.
|
||||||
|
type decorationRegion struct {
|
||||||
|
chunks map[[2]int32]*Chunk
|
||||||
|
sourceX int32
|
||||||
|
sourceZ int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDecorationRegion(chunks []*Chunk) (*decorationRegion, error) {
|
||||||
|
region := &decorationRegion{chunks: make(map[[2]int32]*Chunk, len(chunks))}
|
||||||
|
for _, chunk := range chunks {
|
||||||
|
if chunk == nil {
|
||||||
|
return nil, fmt.Errorf("world: nil chunk in decoration region")
|
||||||
|
}
|
||||||
|
key := [2]int32{chunk.X, chunk.Z}
|
||||||
|
if _, exists := region.chunks[key]; exists {
|
||||||
|
return nil, fmt.Errorf("world: duplicate decoration chunk (%d,%d)", chunk.X, chunk.Z)
|
||||||
|
}
|
||||||
|
region.chunks[key] = chunk
|
||||||
|
}
|
||||||
|
return region, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *decorationRegion) setSource(cx, cz int32) error {
|
||||||
|
if _, ok := r.chunks[[2]int32{cx, cz}]; !ok {
|
||||||
|
return fmt.Errorf("world: decoration source (%d,%d) unavailable", cx, cz)
|
||||||
|
}
|
||||||
|
r.sourceX, r.sourceZ = cx, cz
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *decorationRegion) chunkAtBlock(x, z int) *Chunk {
|
||||||
|
return r.chunks[[2]int32{int32(x >> 4), int32(z >> 4)}]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *decorationRegion) getBlock(x, y, z int) uint16 {
|
||||||
|
if y < MinY || y >= MinY+WorldHeight {
|
||||||
|
return StateAir
|
||||||
|
}
|
||||||
|
chunk := r.chunkAtBlock(x, z)
|
||||||
|
if chunk == nil {
|
||||||
|
return StateAir
|
||||||
|
}
|
||||||
|
return chunk.GetBlock(x&15, y, z&15)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *decorationRegion) setBlock(x, y, z int, state uint16) bool {
|
||||||
|
if y < MinY || y >= MinY+WorldHeight {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cx, cz := int32(x>>4), int32(z>>4)
|
||||||
|
if abs32(cx-r.sourceX) > 1 || abs32(cz-r.sourceZ) > 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
chunk := r.chunks[[2]int32{cx, cz}]
|
||||||
|
if chunk == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
chunk.SetBlock(x&15, y, z&15, state)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// heightAt mirrors WorldGenRegion.getHeight: one above the highest matching
|
||||||
|
// block, or MinY when the column has no match.
|
||||||
|
func (r *decorationRegion) heightAt(kind string, x, z int) int {
|
||||||
|
for y := MinY + WorldHeight - 1; y >= MinY; y-- {
|
||||||
|
state := r.getBlock(x, y, z)
|
||||||
|
match := false
|
||||||
|
switch kind {
|
||||||
|
case "WORLD_SURFACE", "WORLD_SURFACE_WG":
|
||||||
|
match = state != StateAir
|
||||||
|
case "OCEAN_FLOOR", "OCEAN_FLOOR_WG":
|
||||||
|
match = stateFlags(state)&flagBlocksMotion != 0
|
||||||
|
case "MOTION_BLOCKING":
|
||||||
|
match = blocksMotionOrFluid(state)
|
||||||
|
case "MOTION_BLOCKING_NO_LEAVES":
|
||||||
|
match = blocksMotionNoLeaves(state)
|
||||||
|
}
|
||||||
|
if match {
|
||||||
|
return y + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MinY
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *decorationRegion) getBiome(x, y, z int) (uint16, bool) {
|
||||||
|
chunk := r.chunkAtBlock(x, z)
|
||||||
|
if chunk == nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return chunk.GetBiome(x&15, y, z&15), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *decorationRegion) sourceBiomes() []string {
|
||||||
|
seen := make(map[uint16]bool)
|
||||||
|
var names []string
|
||||||
|
for cx := r.sourceX - 1; cx <= r.sourceX+1; cx++ {
|
||||||
|
for cz := r.sourceZ - 1; cz <= r.sourceZ+1; cz++ {
|
||||||
|
chunk := r.chunks[[2]int32{cx, cz}]
|
||||||
|
if chunk == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for si := 0; si < SectionCount; si++ {
|
||||||
|
for bx := 0; bx < biomeCellsXZ; bx++ {
|
||||||
|
for by := 0; by < biomeCellsXZ; by++ {
|
||||||
|
for bz := 0; bz < biomeCellsXZ; bz++ {
|
||||||
|
id := chunk.GetBiome(bx*biomeCellSize, MinY+si*16+by*biomeCellSize, bz*biomeCellSize)
|
||||||
|
if !seen[id] {
|
||||||
|
seen[id] = true
|
||||||
|
names = append(names, biomeNameByID(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *decorationRegion) placementContext(biomeAllows func(worldgen.FeaturePosition) bool) worldgen.PlacementContext {
|
||||||
|
return worldgen.PlacementContext{
|
||||||
|
MinY: MinY,
|
||||||
|
Height: WorldHeight,
|
||||||
|
BiomeAllows: biomeAllows,
|
||||||
|
HeightAt: r.heightAt,
|
||||||
|
BlockPredicate: func(predicate json.RawMessage, position worldgen.FeaturePosition) (bool, error) {
|
||||||
|
return false, fmt.Errorf("world: unsupported block predicate at (%d,%d,%d): %s", position.X, position.Y, position.Z, predicate)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func abs32(value int32) int32 {
|
||||||
|
if value < 0 {
|
||||||
|
return -value
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
84
internal/world/decoration_region_test.go
Normal file
84
internal/world/decoration_region_test.go
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecorationRegionUsesAbsoluteNegativeCoordinates(t *testing.T) {
|
||||||
|
chunk := NewChunk(-1, -1, BiomePlains)
|
||||||
|
chunk.SetBlock(15, 12, 15, StateStone)
|
||||||
|
region, err := newDecorationRegion([]*Chunk{chunk})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := region.getBlock(-1, 12, -1); got != StateStone {
|
||||||
|
t.Fatalf("block (-1,12,-1) = %d, want stone", got)
|
||||||
|
}
|
||||||
|
if biome, ok := region.getBiome(-1, 12, -1); !ok || biome != BiomePlains {
|
||||||
|
t.Fatalf("biome = %d, %v", biome, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecorationRegionEnforcesSourceWriteRadius(t *testing.T) {
|
||||||
|
var chunks []*Chunk
|
||||||
|
for cx := int32(-2); cx <= 2; cx++ {
|
||||||
|
chunks = append(chunks, NewChunk(cx, 0, BiomePlains))
|
||||||
|
}
|
||||||
|
region, err := newDecorationRegion(chunks)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := region.setSource(0, 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !region.setBlock(-1, 20, 0, StateStone) || !region.setBlock(31, 20, 0, StateDirt) {
|
||||||
|
t.Fatal("write inside source radius rejected")
|
||||||
|
}
|
||||||
|
if region.setBlock(-17, 20, 0, StateStone) || region.setBlock(32, 20, 0, StateStone) {
|
||||||
|
t.Fatal("write outside source radius accepted")
|
||||||
|
}
|
||||||
|
if got := region.getBlock(31, 20, 0); got != StateDirt {
|
||||||
|
t.Fatalf("shared region mutation = %d, want dirt", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecorationRegionHeightmapsMatchPlacementSemantics(t *testing.T) {
|
||||||
|
chunk := NewChunk(0, 0, BiomePlains)
|
||||||
|
chunk.SetBlock(2, 30, 3, StateStone)
|
||||||
|
chunk.SetBlock(2, 31, 3, StateWater)
|
||||||
|
region, err := newDecorationRegion([]*Chunk{chunk})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := region.heightAt("WORLD_SURFACE", 2, 3); got != 32 {
|
||||||
|
t.Fatalf("WORLD_SURFACE = %d, want 32", got)
|
||||||
|
}
|
||||||
|
if got := region.heightAt("OCEAN_FLOOR", 2, 3); got != 31 {
|
||||||
|
t.Fatalf("OCEAN_FLOOR = %d, want 31", got)
|
||||||
|
}
|
||||||
|
if got := region.heightAt("MOTION_BLOCKING", 2, 3); got != 32 {
|
||||||
|
t.Fatalf("MOTION_BLOCKING = %d, want 32", got)
|
||||||
|
}
|
||||||
|
if got := region.heightAt("WORLD_SURFACE", 4, 5); got != MinY {
|
||||||
|
t.Fatalf("empty height = %d, want %d", got, MinY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecorationRegionSourceBiomesUseThreeByThreeChunks(t *testing.T) {
|
||||||
|
center := NewChunk(0, 0, BiomePlains)
|
||||||
|
east := NewChunk(1, 0, biomeIDByName("minecraft:desert"))
|
||||||
|
outside := NewChunk(2, 0, biomeIDByName("minecraft:forest"))
|
||||||
|
region, err := newDecorationRegion([]*Chunk{center, east, outside})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := region.setSource(0, 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got := region.sourceBiomes()
|
||||||
|
want := []string{"minecraft:plains", "minecraft:desert"}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("source biomes = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue