package world import ( "testing" "time" "github.com/df-mc/dragonfly/server/block/cube" "github.com/go-gl/mathgl/mgl64" ) // TestSynchronousWorldExec verifies that Exec on a synchronous World runs the // transaction on the calling goroutine, with the returned channel closed once // Exec returns. func TestSynchronousWorldExec(t *testing.T) { w := Config{Synchronous: true}.New() defer w.Close() var ran bool c := w.Exec(func(tx *Tx) { ran = true }) if !ran { t.Fatal("expected transaction to have run when Exec returned") } select { case <-c: default: t.Fatal("expected channel returned by Exec to be closed when Exec returned") } } // TestSynchronousWorldAdvanceTick verifies that a synchronous World does not // tick on its own and that AdvanceTick advances the current tick exactly once // per call, even without any viewers. func TestSynchronousWorldAdvanceTick(t *testing.T) { w := Config{Synchronous: true}.New() defer w.Close() current := func() int64 { w.set.Lock() defer w.set.Unlock() return w.set.CurrentTick } start := current() time.Sleep(time.Second / 10) if got := current(); got != start { t.Fatalf("expected no automatic ticking, tick advanced from %v to %v", start, got) } for range 5 { w.AdvanceTick() } if got := current(); got != start+5 { t.Fatalf("expected current tick %v after 5 AdvanceTick calls, got %v", start+5, got) } } func TestSynchronousExecWorldCanRemoveEntity(t *testing.T) { w := Config{Synchronous: true}.New() defer w.Close() h := EntitySpawnOpts{Position: mgl64.Vec3{0, 4, 0}}.New(testEntityType{}, testEntityConfig{}) <-w.Exec(func(tx *Tx) { tx.AddEntity(h) }) done := make(chan struct{}) go func() { h.ExecWorld(func(tx *Tx, e Entity) { tx.RemoveEntity(e) }) close(done) }() select { case <-done: case <-time.After(time.Second * 5): t.Fatal("ExecWorld self-removal did not complete") } } func TestSynchronousAdvanceTickTicksViewerlessEntities(t *testing.T) { w := Config{Synchronous: true}.New() defer w.Close() h := EntitySpawnOpts{Position: mgl64.Vec3{0, 4, 0}}.New(testEntityType{}, testEntityConfig{}) <-w.Exec(func(tx *Tx) { tx.AddEntity(h) }) start := h.data.Pos for range 3 { w.AdvanceTick() } if got := h.data.Pos; got == start { t.Fatalf("expected entity position to change after ticking, got %v", got) } } func TestSynchronousAdvanceTickTicksViewerlessBlockEntities(t *testing.T) { w := Config{Synchronous: true}.New() defer w.Close() pos := cube.Pos{0, 4, 0} tb := &testTickerBlock{} <-w.Exec(func(tx *Tx) { col := tx.World().chunk(chunkPosFromBlockPos(pos)) chest, ok := tx.World().conf.Blocks.BlockByName("minecraft:chest", map[string]any{"minecraft:cardinal_direction": "north"}) if !ok { t.Fatal("expected chest block to be registered") } col.SetBlock(uint8(pos[0]), int16(pos[1]), uint8(pos[2]), 0, tx.World().conf.Blocks.BlockRuntimeID(chest)) col.BlockEntities[pos] = tb }) w.AdvanceTick() if tb.ticks == 0 { t.Fatal("expected block entity to tick") } } type testEntityConfig struct{} func (testEntityConfig) Apply(*EntityData) {} type testEntityType struct{} func (testEntityType) Open(_ *Tx, handle *EntityHandle, data *EntityData) Entity { return &testEntity{handle: handle, data: data} } func (testEntityType) EncodeEntity() string { return "dragonfly:test_entity" } func (testEntityType) BBox(Entity) cube.BBox { return cube.Box(0, 0, 0, 1, 1, 1) } func (testEntityType) DecodeNBT(map[string]any, *EntityData) {} func (testEntityType) EncodeNBT(*EntityData) map[string]any { return nil } type testEntity struct { handle *EntityHandle data *EntityData } func (e *testEntity) Close() error { return nil } func (e *testEntity) H() *EntityHandle { return e.handle } func (e *testEntity) Position() mgl64.Vec3 { return e.data.Pos } func (e *testEntity) Rotation() cube.Rotation { return e.data.Rot } func (e *testEntity) Tick(*Tx, int64) { e.data.Pos = e.data.Pos.Add(mgl64.Vec3{0, -0.1, 0}) } type testTickerBlock struct { ticks int } func (*testTickerBlock) EncodeBlock() (string, map[string]any) { return "dragonfly:test_ticker", nil } func (*testTickerBlock) Hash() (uint64, uint64) { return 1<<32 - 1, 0 } func (*testTickerBlock) Model() BlockModel { return unknownModel{} } func (*testTickerBlock) DecodeNBT(map[string]any) any { return &testTickerBlock{} } func (*testTickerBlock) EncodeNBT() map[string]any { return nil } func (b *testTickerBlock) Tick(int64, cube.Pos, *Tx) { b.ticks++ }