From 8dd771dadce31323f3973a091a447683917e079d Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 13 Jul 2026 18:58:01 -0400 Subject: [PATCH 01/16] fix: support block network hashes --- go.mod | 2 +- go.sum | 4 +- player/component/acknowledgement/chunks.go | 6 +- player/component/world.go | 8 +- player/component/world_hash_test.go | 182 +++++++++++++++ player/items.go | 13 +- player/network.go | 1 + player/packet.go | 245 +++++++++++++++++++-- player/player.go | 4 + player/world.go | 58 ++++- player/world_hash_test.go | 243 ++++++++++++++++++++ world/cache.go | 111 ++++++++-- world/world.go | 23 +- 13 files changed, 830 insertions(+), 70 deletions(-) create mode 100644 player/component/world_hash_test.go create mode 100644 player/world_hash_test.go diff --git a/go.mod b/go.mod index 1607c8b6..d7ab4976 100755 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/oomph-ac/oomph go 1.26.0 -replace github.com/df-mc/dragonfly => github.com/oomph-ac/dragonfly v0.0.0-20260702190811-865056f9d47f +replace github.com/df-mc/dragonfly => github.com/oomph-ac/dragonfly v0.0.0-20260713224537-2de5b48545f8 replace github.com/sandertv/gophertunnel => github.com/hashimthearab/gophertunnel v1.25.3-0.20260713165240-828896b9c778 diff --git a/go.sum b/go.sum index 22df3593..5d8cdfd8 100755 --- a/go.sum +++ b/go.sum @@ -54,8 +54,8 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/oomph-ac/dragonfly v0.0.0-20260702190811-865056f9d47f h1:F1qyVUh1JMkYI1o6XzOQy1cFFdanPsOepv+jZSzZ3qo= -github.com/oomph-ac/dragonfly v0.0.0-20260702190811-865056f9d47f/go.mod h1:OFJjCz3xfDalVZEtV4lJdbenwKDEAczKHikHN7kVwMc= +github.com/oomph-ac/dragonfly v0.0.0-20260713224537-2de5b48545f8 h1:dQCMLVDOpW2ZmPNfOJk4YKxPnCvQmg9N+0K58PgO5cI= +github.com/oomph-ac/dragonfly v0.0.0-20260713224537-2de5b48545f8/go.mod h1:OFJjCz3xfDalVZEtV4lJdbenwKDEAczKHikHN7kVwMc= github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0= github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk= github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= diff --git a/player/component/acknowledgement/chunks.go b/player/component/acknowledgement/chunks.go index 850762fb..d07d22ac 100644 --- a/player/component/acknowledgement/chunks.go +++ b/player/component/acknowledgement/chunks.go @@ -29,7 +29,7 @@ func (ack *ChunkUpdate) Run() { ack.mPlayer.Disconnect(game.ErrorChunkCacheUnsupported) return } - cInfo, err := oworld.CacheChunk(ack.pk) + cInfo, err := oworld.CacheChunk(ack.pk, ack.mPlayer.GameDat.UseBlockNetworkIDHashes) if err != nil { ack.mPlayer.Disconnect(fmt.Sprintf(game.ErrorInternalDecodeChunk, err)) return @@ -93,13 +93,13 @@ func (ack *SubChunkUpdate) Run() { bufUsed = true buf.Write(entry.RawPayload) - cachedSub, err := oworld.CacheSubChunk(buf, ch, chunkPos) + cachedSub, err := oworld.CacheSubChunk(buf, ch, chunkPos, ack.mPlayer.GameDat.UseBlockNetworkIDHashes) if err != nil { ack.mPlayer.Disconnect(fmt.Sprintf(game.ErrorInternalDecodeChunk, err)) continue } ch.Sub()[cachedSub.Layer()] = cachedSub.SubChunk() - ack.mPlayer.World().AddSubChunk(chunkPos, cachedSub.Hash()) + ack.mPlayer.World().AddSubChunk(chunkPos, cachedSub.Hash(), ack.mPlayer.GameDat.UseBlockNetworkIDHashes) ack.mPlayer.Dbg.Notify(player.DebugModeChunks, true, "cached subchunk %d at %v", cachedSub.Layer(), chunkPos) case protocol.SubChunkResultSuccessAllAir: ack.mPlayer.Dbg.Notify(player.DebugModeChunks, true, "all-air chunk at %v", chunkPos) diff --git a/player/component/world.go b/player/component/world.go index e655f575..28a5fdba 100644 --- a/player/component/world.go +++ b/player/component/world.go @@ -77,7 +77,7 @@ func (c *WorldUpdaterComponent) HandleUpdateBlock(pk *packet.UpdateBlock) { c.mPlayer.Log().Debug("unsupported layer update block", "layer", pk.Layer, "block", pk.NewBlockRuntimeID, "pos", pos) return } - c.AddPendingUpdate(pos, pk.NewBlockRuntimeID) + c.AddPendingUpdate(pos, c.mPlayer.BlockRuntimeIDFromNetwork(pk.NewBlockRuntimeID)) } // HandleUpdateSubChunkBlocks handles an UpdateSubChunkBlocks packet from the server. @@ -86,10 +86,10 @@ func (c *WorldUpdaterComponent) HandleUpdateSubChunkBlocks(pk *packet.UpdateSubC c.mPlayer.ACKs().Add(acknowledgement.NewPlayerInitalizedACK(c.mPlayer)) } for _, entry := range pk.Blocks { - c.AddPendingUpdate(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, entry.BlockRuntimeID) + c.AddPendingUpdate(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, c.mPlayer.BlockRuntimeIDFromNetwork(entry.BlockRuntimeID)) } for _, entry := range pk.Extra { - c.AddPendingUpdate(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, entry.BlockRuntimeID) + c.AddPendingUpdate(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, c.mPlayer.BlockRuntimeIDFromNetwork(entry.BlockRuntimeID)) } } @@ -188,7 +188,7 @@ func (c *WorldUpdaterComponent) AttemptItemInteractionWithBlock(pk *packet.Inven case *block.Air: // This only happens when Dragonfly is unsure of what the item is (unregistered), so we use the client-authoritative block in hand. c.mPlayer.Dbg.Notify(player.DebugModeBlockPlacement, true, "called c.mPlayer.PlaceBlock: using client-authoritative block in hand") - if b, ok := df_world.BlockByRuntimeID(uint32(dat.HeldItem.Stack.BlockRuntimeID)); ok { + if b, ok := df_world.BlockByRuntimeID(c.mPlayer.BlockRuntimeIDFromClient(uint32(dat.HeldItem.Stack.BlockRuntimeID))); ok { c.mPlayer.Dbg.Notify(player.DebugModeBlockPlacement, true, "placing block with runtime ID: %d", dat.HeldItem.Stack.BlockRuntimeID) // If the block at the position is not replacable, we want to place the block on the side of the block. diff --git a/player/component/world_hash_test.go b/player/component/world_hash_test.go new file mode 100644 index 00000000..8bd47f62 --- /dev/null +++ b/player/component/world_hash_test.go @@ -0,0 +1,182 @@ +package component + +import ( + "bytes" + "io" + "log/slog" + "testing" + "time" + + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/block/cube" + dfworld "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/chunk" + "github.com/oomph-ac/oomph/player" + playercontext "github.com/oomph-ac/oomph/player/context" + oomphworld "github.com/oomph-ac/oomph/world" + "github.com/sandertv/gophertunnel/minecraft" + "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +func TestWorldUpdaterDecodesHashedLevelChunk(t *testing.T) { + oomphworld.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := oomphworld.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + + position := cube.Pos{1, 0, 1} + c := chunk.New(oomphworld.BlockRegistry, dfworld.Overworld.Range()) + c.SetBlock(uint8(position[0]), int16(position[1]), uint8(position[2]), 0, stoneRID) + data := chunk.Encode(c, chunk.NetworkEncoding) + stoneRIDBytes, stoneHashBytes := bytes.NewBuffer(nil), bytes.NewBuffer(nil) + if err := protocol.WriteVarint32(stoneRIDBytes, int32(stoneRID)); err != nil { + t.Fatal(err) + } + if err := protocol.WriteVarint32(stoneHashBytes, int32(stoneHash)); err != nil { + t.Fatal(err) + } + raw := bytes.NewBuffer(nil) + converted := false + for _, subChunk := range data.SubChunks { + hashed := bytes.ReplaceAll(subChunk, stoneRIDBytes.Bytes(), stoneHashBytes.Bytes()) + if !bytes.Equal(hashed, subChunk) { + converted = true + } + raw.Write(hashed) + } + if !converted { + t.Fatal("stone runtime ID not found in encoded chunk") + } + raw.Write(data.Biomes) + raw.WriteByte(0) + + p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{CurrentTime: time.Now()}, nil) + Register(p) + p.GameDat.UseBlockNetworkIDHashes = true + p.WorldUpdater().HandleLevelChunk(&packet.LevelChunk{ + Position: protocol.ChunkPos{0, 0}, + Dimension: packet.DimensionOverworld, + SubChunkCount: uint32(len(data.SubChunks)), + RawPayload: raw.Bytes(), + }) + + got := p.World().Block(position) + if _, ok := got.(block.Stone); !ok { + gotRID := dfworld.BlockRuntimeID(got) + t.Fatalf("block = %T (runtime ID %d), want stone (runtime ID %d, network hash %d)", got, gotRID, stoneRID, stoneHash) + } +} + +func TestWorldUpdaterConvertsHashedBlockUpdate(t *testing.T) { + oomphworld.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := oomphworld.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{CurrentTime: time.Now()}, nil) + Register(p) + p.GameDat.UseBlockNetworkIDHashes = true + updater := p.WorldUpdater().(*WorldUpdaterComponent) + position := cube.Pos{4, 5, 6} + + updater.HandleUpdateBlock(&packet.UpdateBlock{ + Position: protocol.BlockPos{4, 5, 6}, + NewBlockRuntimeID: stoneHash, + }) + + if got := updater.batchedBlockUpdates.Blocks()[position]; got != stoneRID { + t.Fatalf("pending block runtime ID = %d, want %d (network hash %d)", got, stoneRID, stoneHash) + } +} + +func TestClientBlockHashModeSurvivesBackendTransfer(t *testing.T) { + oomphworld.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := oomphworld.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{CurrentTime: time.Now()}, nil) + Register(p) + p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: true}}) + p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: false}}) + + if got := p.BlockRuntimeIDToNetwork(stoneRID); got != stoneHash { + t.Fatalf("client block ID after transfer = %d, want initial hash %d", got, stoneHash) + } +} + +func TestLevelChunkIsTranslatedToRetainedClientHashModeAfterTransfer(t *testing.T) { + oomphworld.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := oomphworld.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + position := cube.Pos{1, 0, 1} + c := chunk.New(oomphworld.BlockRegistry, dfworld.Overworld.Range()) + c.SetBlock(uint8(position[0]), int16(position[1]), uint8(position[2]), 0, stoneRID) + data := chunk.Encode(c, chunk.NetworkEncoding) + raw := bytes.NewBuffer(nil) + for _, sub := range data.SubChunks { + raw.Write(sub) + } + raw.Write(data.Biomes) + raw.WriteByte(0) + + p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{CurrentTime: time.Now()}, nil) + Register(p) + p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: true}}) + p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: false}}) + pk := packet.Packet(&packet.LevelChunk{ + Position: protocol.ChunkPos{0, 0}, + Dimension: packet.DimensionOverworld, + SubChunkCount: uint32(len(data.SubChunks)), + RawPayload: raw.Bytes(), + }) + ctx := playercontext.NewHandlePacketContext(&pk) + p.HandleServerPacket(ctx) + + forwarded := pk.(*packet.LevelChunk) + decoded, err := chunk.NetworkDecode(oomphworld.BlockRegistry, forwarded.RawPayload, int(forwarded.SubChunkCount), dfworld.Overworld.Range()) + if err != nil { + t.Fatal(err) + } + if got := decoded.Block(uint8(position[0]), int16(position[1]), uint8(position[2]), 0); got != stoneHash { + t.Fatalf("forwarded block ID = %d, want client hash %d", got, stoneHash) + } + if _, ok := p.World().Block(position).(block.Stone); !ok { + t.Fatalf("internal block = %T, want block.Stone", p.World().Block(position)) + } +} + +func TestUpdateBlockSyncedIsTranslatedToRetainedClientMode(t *testing.T) { + oomphworld.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := oomphworld.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{CurrentTime: time.Now()}, nil) + Register(p) + p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: false}}) + p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: true}}) + pk := packet.Packet(&packet.UpdateBlockSynced{NewBlockRuntimeID: stoneHash}) + ctx := playercontext.NewHandlePacketContext(&pk) + + p.HandleServerPacket(ctx) + + if got := pk.(*packet.UpdateBlockSynced).NewBlockRuntimeID; got != stoneRID { + t.Fatalf("forwarded block ID = %d, want client runtime ID %d", got, stoneRID) + } +} + +type hashModeServerConn struct{ data minecraft.GameData } + +func (hashModeServerConn) WritePacket(packet.Packet) error { return nil } +func (hashModeServerConn) Close() error { return nil } +func (c hashModeServerConn) GameData() minecraft.GameData { return c.data } diff --git a/player/items.go b/player/items.go index e10007ab..89345dd5 100644 --- a/player/items.go +++ b/player/items.go @@ -18,8 +18,8 @@ func (p *Player) ConvertToStack(it protocol.ItemStack) item.Stack { t = block.Air{} } } - if it.BlockRuntimeID > 0 { - b, _ := p.World().BlockRegistry().BlockByRuntimeID(uint32(it.BlockRuntimeID)) + if it.BlockRuntimeID != 0 { + b, _ := p.World().BlockRegistry().BlockByRuntimeID(p.BlockRuntimeIDFromNetwork(uint32(it.BlockRuntimeID))) if t, ok = b.(world.Item); !ok { t = block.Air{} } @@ -32,10 +32,17 @@ func (p *Player) ConvertToStack(it protocol.ItemStack) item.Stack { } func (p *Player) InstanceFromItem(it item.Stack) protocol.ItemInstance { - return utils.InstanceFromItem(p.World().BlockRegistry(), it) + instance := utils.InstanceFromItem(p.World().BlockRegistry(), it) + if instance.Stack.BlockRuntimeID != 0 { + instance.Stack.BlockRuntimeID = int32(p.BlockRuntimeIDToNetwork(uint32(instance.Stack.BlockRuntimeID))) + } + return instance } func (p *Player) StackToItem(it protocol.ItemStack) item.Stack { + if it.BlockRuntimeID != 0 { + it.BlockRuntimeID = int32(p.BlockRuntimeIDFromNetwork(uint32(it.BlockRuntimeID))) + } return utils.StackToItem(p.World().BlockRegistry(), it) } diff --git a/player/network.go b/player/network.go index 7e223432..fa5e2b12 100755 --- a/player/network.go +++ b/player/network.go @@ -42,6 +42,7 @@ func (p *Player) SetServerConn(conn ServerConn) { } if p.serverConn == nil { + p.clientUsesBlockNetworkIDHashes = conn.GameData().UseBlockNetworkIDHashes for _, item := range conn.GameData().Items { if i, ok := world.ItemByName(item.Name, 0); ok { p.items[item.RuntimeID] = i diff --git a/player/packet.go b/player/packet.go index 415095c4..83ec0cb8 100644 --- a/player/packet.go +++ b/player/packet.go @@ -1,13 +1,11 @@ package player import ( - "bytes" "strings" "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" - "github.com/df-mc/dragonfly/server/world/chunk" "github.com/oomph-ac/oomph/entity" "github.com/oomph-ac/oomph/game" "github.com/oomph-ac/oomph/oconfig" @@ -39,13 +37,17 @@ var ClientDecode = []uint32{ var ServerDecode = []uint32{ packet.IDAddActor, + packet.IDAddItemActor, packet.IDAddPlayer, packet.IDChunkRadiusUpdated, packet.IDInventorySlot, packet.IDInventoryContent, + packet.IDInventoryTransaction, packet.IDItemStackResponse, packet.IDLevelChunk, packet.IDMobEffect, + packet.IDMobEquipment, + packet.IDMobArmourEquipment, packet.IDMoveActorAbsolute, packet.IDMovePlayer, packet.IDRemoveActor, @@ -56,6 +58,7 @@ var ServerDecode = []uint32{ packet.IDUpdateAbilities, packet.IDUpdateAttributes, packet.IDUpdateBlock, + packet.IDUpdateBlockSynced, packet.IDUpdateSubChunkBlocks, packet.IDContainerOpen, packet.IDContainerClose, @@ -278,6 +281,67 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { } } p.RunDetections(pk) + if p.GameDat.UseBlockNetworkIDHashes != p.clientUsesBlockNetworkIDHashes && p.rewriteClientBlockNetworkIDs(pk) { + ctx.SetModified() + } +} + +func (p *Player) rewriteClientBlockNetworkIDs(pk packet.Packet) bool { + rewriteStack := func(stack *protocol.ItemStack) { + if stack.BlockRuntimeID != 0 { + stack.BlockRuntimeID = int32(p.BlockRuntimeIDFromClientToBackend(uint32(stack.BlockRuntimeID))) + } + } + rewriteCraftResults := func(actions []protocol.StackRequestAction) bool { + modified := false + for _, action := range actions { + if action, ok := action.(*protocol.CraftResultsDeprecatedStackRequestAction); ok { + for i := range action.ResultItems { + rewriteStack(&action.ResultItems[i]) + } + modified = true + } + } + return modified + } + switch pk := pk.(type) { + case *packet.PlayerAuthInput: + modified := false + if pk.InputData.Load(packet.InputFlagPerformItemInteraction) { + pk.ItemInteractionData.BlockRuntimeID = p.BlockRuntimeIDFromClientToBackend(pk.ItemInteractionData.BlockRuntimeID) + rewriteStack(&pk.ItemInteractionData.HeldItem.Stack) + modified = true + } + if pk.InputData.Load(packet.InputFlagPerformItemStackRequest) { + modified = rewriteCraftResults(pk.ItemStackRequest.Actions) || modified + } + return modified + case *packet.ItemStackRequest: + modified := false + for i := range pk.Requests { + modified = rewriteCraftResults(pk.Requests[i].Actions) || modified + } + return modified + case *packet.InventoryTransaction: + for i := range pk.Actions { + rewriteStack(&pk.Actions[i].OldItem.Stack) + rewriteStack(&pk.Actions[i].NewItem.Stack) + } + switch data := pk.TransactionData.(type) { + case *protocol.UseItemTransactionData: + data.BlockRuntimeID = p.BlockRuntimeIDFromClientToBackend(data.BlockRuntimeID) + rewriteStack(&data.HeldItem.Stack) + case *protocol.UseItemOnEntityTransactionData: + rewriteStack(&data.HeldItem.Stack) + case *protocol.ReleaseItemTransactionData: + rewriteStack(&data.HeldItem.Stack) + } + return true + case *packet.MobEquipment: + rewriteStack(&pk.NewItem.Stack) + return true + } + return false } // splitCommandLine splits a command line into arguments, preserving quoted substrings @@ -404,30 +468,16 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { case *packet.ItemStackResponse: p.inventory.HandleItemStackResponse(pk) case *packet.LevelChunk: - // HACK: For some reason, some chunks forwarded through gophertunnel will spawn invisible blocks? Lunar had this issue as well - // and seemed to have fixed it by fully re-encoding the chunk. - if p.opts.Network.AttemptFixChunks && !pk.CacheEnabled && !(pk.SubChunkCount == protocol.SubChunkRequestModeLimited || pk.SubChunkCount == protocol.SubChunkRequestModeLimitless) { - dim, ok := world.DimensionByID(int(pk.Dimension)) - if !ok { - dim = world.Overworld - } - if c, err := chunk.NetworkDecode(oworld.BlockRegistry, pk.RawPayload, int(pk.SubChunkCount), dim.Range()); err != nil { - p.Log().Warn("unable to decode chunk", "error", err) + p.worldUpdater.HandleLevelChunk(pk) + backendHashes, clientHashes := p.GameDat.UseBlockNetworkIDHashes, p.clientUsesBlockNetworkIDHashes + fullChunk := !pk.CacheEnabled && pk.SubChunkCount != protocol.SubChunkRequestModeLimited && pk.SubChunkCount != protocol.SubChunkRequestModeLimitless + if fullChunk && (p.opts.Network.AttemptFixChunks || backendHashes != clientHashes) { + if err := oworld.ReencodeLevelChunk(pk, backendHashes, clientHashes); err != nil { + p.Log().Warn("unable to re-encode chunk", "error", err) } else { - data := chunk.Encode(c, chunk.NetworkEncoding) - chunkBuf := bytes.NewBuffer(nil) - for _, sub := range data.SubChunks { - chunkBuf.Write(sub) - } - chunkBuf.Write(data.Biomes) - chunkBuf.WriteByte(0) - pk.RawPayload = append([]byte(nil), chunkBuf.Bytes()...) - pk.SubChunkCount = uint32(len(data.SubChunks)) ctx.SetModified() } } - - p.worldUpdater.HandleLevelChunk(pk) case *packet.MobEffect: pk.Tick = 0 ctx.SetModified() @@ -479,6 +529,26 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { p.gamemodeHandle.Handle(pk) case *packet.SubChunk: p.worldUpdater.HandleSubChunk(pk) + backendHashes, clientHashes := p.GameDat.UseBlockNetworkIDHashes, p.clientUsesBlockNetworkIDHashes + if !pk.CacheEnabled && backendHashes != clientHashes { + dimension, ok := world.DimensionByID(int(pk.Dimension)) + if !ok { + dimension = world.Overworld + } + for i := range pk.SubChunkEntries { + entry := &pk.SubChunkEntries[i] + if entry.Result != protocol.SubChunkResultSuccess { + continue + } + payload, err := oworld.ReencodeSubChunk(entry.RawPayload, dimension, backendHashes, clientHashes) + if err != nil { + p.Log().Warn("unable to re-encode subchunk", "error", err) + continue + } + entry.RawPayload = payload + ctx.SetModified() + } + } case *packet.UpdateAbilities: if pk.AbilityData.EntityUniqueID == p.UniqueId { p.movement.ServerUpdate(pk) @@ -492,8 +562,26 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { } case *packet.UpdateBlock: p.worldUpdater.HandleUpdateBlock(pk) + if p.GameDat.UseBlockNetworkIDHashes != p.clientUsesBlockNetworkIDHashes { + pk.NewBlockRuntimeID = p.BlockRuntimeIDFromBackendToClient(pk.NewBlockRuntimeID) + ctx.SetModified() + } + case *packet.UpdateBlockSynced: + if p.GameDat.UseBlockNetworkIDHashes != p.clientUsesBlockNetworkIDHashes { + pk.NewBlockRuntimeID = p.BlockRuntimeIDFromBackendToClient(pk.NewBlockRuntimeID) + ctx.SetModified() + } case *packet.UpdateSubChunkBlocks: p.worldUpdater.HandleUpdateSubChunkBlocks(pk) + if p.GameDat.UseBlockNetworkIDHashes != p.clientUsesBlockNetworkIDHashes { + for i := range pk.Blocks { + pk.Blocks[i].BlockRuntimeID = p.BlockRuntimeIDFromBackendToClient(pk.Blocks[i].BlockRuntimeID) + } + for i := range pk.Extra { + pk.Extra[i].BlockRuntimeID = p.BlockRuntimeIDFromBackendToClient(pk.Extra[i].BlockRuntimeID) + } + ctx.SetModified() + } case *packet.ContainerOpen: p.inventory.CreateWindow(pk.WindowID, pk.ContainerType) case *packet.ContainerClose: @@ -522,4 +610,117 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { p.CreativeItems[item.CreativeItemNetworkID] = item } } + if p.GameDat.UseBlockNetworkIDHashes != p.clientUsesBlockNetworkIDHashes && p.rewriteServerItemBlockNetworkIDs(pk) { + ctx.SetModified() + } +} + +func (p *Player) rewriteServerItemBlockNetworkIDs(pk packet.Packet) bool { + rewriteStack := func(stack *protocol.ItemStack) { + if stack.BlockRuntimeID != 0 { + stack.BlockRuntimeID = int32(p.BlockRuntimeIDFromBackendToClient(uint32(stack.BlockRuntimeID))) + } + } + switch pk := pk.(type) { + case *packet.InventorySlot: + rewriteStack(&pk.NewItem.Stack) + if storageItem, ok := pk.StorageItem.Value(); ok { + rewriteStack(&storageItem.Stack) + pk.StorageItem = protocol.Option(storageItem) + } + return true + case *packet.InventoryContent: + // The inventory ACK retains the original slice for later processing in the backend's ID mode. + pk.Content = append([]protocol.ItemInstance(nil), pk.Content...) + for i := range pk.Content { + rewriteStack(&pk.Content[i].Stack) + } + rewriteStack(&pk.StorageItem.Stack) + return true + case *packet.MobEquipment: + rewriteStack(&pk.NewItem.Stack) + return true + case *packet.MobArmourEquipment: + rewriteStack(&pk.Helmet.Stack) + rewriteStack(&pk.Chestplate.Stack) + rewriteStack(&pk.Leggings.Stack) + rewriteStack(&pk.Boots.Stack) + rewriteStack(&pk.Body.Stack) + return true + case *packet.AddPlayer: + rewriteStack(&pk.HeldItem.Stack) + return true + case *packet.AddItemActor: + rewriteStack(&pk.Item.Stack) + return true + case *packet.CreativeContent: + for i := range pk.Groups { + rewriteStack(&pk.Groups[i].Icon) + } + for i := range pk.Items { + rewriteStack(&pk.Items[i].Item) + } + return true + case *packet.InventoryTransaction: + for i := range pk.Actions { + rewriteStack(&pk.Actions[i].OldItem.Stack) + rewriteStack(&pk.Actions[i].NewItem.Stack) + } + switch data := pk.TransactionData.(type) { + case *protocol.UseItemTransactionData: + data.BlockRuntimeID = p.BlockRuntimeIDFromBackendToClient(data.BlockRuntimeID) + rewriteStack(&data.HeldItem.Stack) + case *protocol.UseItemOnEntityTransactionData: + rewriteStack(&data.HeldItem.Stack) + case *protocol.ReleaseItemTransactionData: + rewriteStack(&data.HeldItem.Stack) + } + return true + case *packet.CraftingData: + for i, recipe := range pk.Recipes { + switch recipe := recipe.(type) { + case *protocol.ShapedRecipe: + clientRecipe := *recipe + clientRecipe.Output = append([]protocol.ItemStack(nil), recipe.Output...) + for outputIndex := range clientRecipe.Output { + rewriteStack(&clientRecipe.Output[outputIndex]) + } + pk.Recipes[i] = &clientRecipe + case *protocol.ShulkerBoxRecipe: + clientRecipe := *recipe + clientRecipe.Output = append([]protocol.ItemStack(nil), recipe.Output...) + for outputIndex := range clientRecipe.Output { + rewriteStack(&clientRecipe.Output[outputIndex]) + } + pk.Recipes[i] = &clientRecipe + case *protocol.ShapelessChemistryRecipe: + clientRecipe := *recipe + clientRecipe.Output = append([]protocol.ItemStack(nil), recipe.Output...) + for outputIndex := range clientRecipe.Output { + rewriteStack(&clientRecipe.Output[outputIndex]) + } + pk.Recipes[i] = &clientRecipe + case *protocol.ShapedChemistryRecipe: + clientRecipe := *recipe + clientRecipe.Output = append([]protocol.ItemStack(nil), recipe.Output...) + for outputIndex := range clientRecipe.Output { + rewriteStack(&clientRecipe.Output[outputIndex]) + } + pk.Recipes[i] = &clientRecipe + case *protocol.ShapelessRecipe: + clientRecipe := *recipe + clientRecipe.Output = append([]protocol.ItemStack(nil), recipe.Output...) + for outputIndex := range clientRecipe.Output { + rewriteStack(&clientRecipe.Output[outputIndex]) + } + pk.Recipes[i] = &clientRecipe + case *protocol.SmithingTransformRecipe: + clientRecipe := *recipe + rewriteStack(&clientRecipe.Result) + pk.Recipes[i] = &clientRecipe + } + } + return true + } + return false } diff --git a/player/player.go b/player/player.go index 20ad9466..d52af80a 100755 --- a/player/player.go +++ b/player/player.go @@ -71,6 +71,10 @@ type Player struct { GameDat minecraft.GameData Version int32 + // clientUsesBlockNetworkIDHashes is fixed by the initial backend's StartGame. It remains unchanged across fast + // backend transfers because the connected client does not receive another StartGame packet. + clientUsesBlockNetworkIDHashes bool + // With fast transfers, the client will still retain it's original runtime and unique IDs, so // we must translate them to new ones, while still retaining the old ones for the client to use. RuntimeId uint64 diff --git a/player/world.go b/player/world.go index 44f633d6..76429a7d 100644 --- a/player/world.go +++ b/player/world.go @@ -71,6 +71,58 @@ func (p *Player) World() *oworld.World { return p.world } +// BlockRuntimeIDFromNetwork converts a network block hash to the runtime ID used by Oomph's block registry when the +// backend enabled hashed block network IDs. Unknown values are preserved so callers can retain their existing fallback. +func (p *Player) BlockRuntimeIDFromNetwork(id uint32) uint32 { + return p.convertBlockRuntimeID(id, p.GameDat.UseBlockNetworkIDHashes, false) +} + +// BlockRuntimeIDToNetwork converts an Oomph block runtime ID to the hash expected by clients when the backend enabled +// hashed block network IDs. Unknown values are preserved so custom block fallbacks remain intact. +func (p *Player) BlockRuntimeIDToNetwork(id uint32) uint32 { + return p.convertBlockRuntimeID(id, false, p.clientUsesBlockNetworkIDHashes) +} + +// BlockRuntimeIDFromClient converts a client-visible block ID to Oomph's registry runtime ID. +func (p *Player) BlockRuntimeIDFromClient(id uint32) uint32 { + return p.convertBlockRuntimeID(id, p.clientUsesBlockNetworkIDHashes, false) +} + +// BlockRuntimeIDToBackend converts an Oomph registry runtime ID to the current backend's network representation. +func (p *Player) BlockRuntimeIDToBackend(id uint32) uint32 { + return p.convertBlockRuntimeID(id, false, p.GameDat.UseBlockNetworkIDHashes) +} + +// BlockRuntimeIDFromBackendToClient translates a block ID from the current backend's representation to the one fixed +// by the client's initial StartGame packet. +func (p *Player) BlockRuntimeIDFromBackendToClient(id uint32) uint32 { + return p.convertBlockRuntimeID(id, p.GameDat.UseBlockNetworkIDHashes, p.clientUsesBlockNetworkIDHashes) +} + +// BlockRuntimeIDFromClientToBackend translates a client-visible block ID to the current backend's representation. +func (p *Player) BlockRuntimeIDFromClientToBackend(id uint32) uint32 { + return p.convertBlockRuntimeID(id, p.clientUsesBlockNetworkIDHashes, p.GameDat.UseBlockNetworkIDHashes) +} + +func (p *Player) convertBlockRuntimeID(id uint32, fromHashes, toHashes bool) uint32 { + if fromHashes == toHashes { + return id + } + if fromHashes { + converted, ok := p.World().BlockRegistry().HashToRuntimeID(id) + if !ok { + return id + } + id = converted + } + if toHashes { + if converted, ok := p.World().BlockRegistry().RuntimeIDToHash(id); ok { + return converted + } + } + return id +} + // This function is deprecated and instead, the user should call p.World().PurgeChunks() directly. func (p *Player) RegenerateWorld() { p.world.PurgeChunks() @@ -99,7 +151,7 @@ func (p *Player) SyncBlock(pos df_cube.Pos) { int32(pos[1]), int32(pos[2]), }, - NewBlockRuntimeID: world.BlockRuntimeID(p.World().Block(pos)), + NewBlockRuntimeID: p.BlockRuntimeIDToNetwork(world.BlockRuntimeID(p.World().Block(pos))), Flags: packet.BlockUpdateNetwork, Layer: 0, // TODO: Implement and account for multi-layer blocks. } @@ -166,11 +218,11 @@ func (p *Player) SendBlockUpdates(positions []protocol.BlockPos) { for _, pos := range positions { p.SendPacketToClient(&packet.UpdateBlock{ Position: pos, - NewBlockRuntimeID: world.BlockRuntimeID(p.World().Block(df_cube.Pos{ + NewBlockRuntimeID: p.BlockRuntimeIDToNetwork(world.BlockRuntimeID(p.World().Block(df_cube.Pos{ int(pos.X()), int(pos.Y()), int(pos.Z()), - })), + }))), Flags: packet.BlockUpdateNeighbours, Layer: 0, // TODO: Implement and account for multi-layer blocks. }) diff --git a/player/world_hash_test.go b/player/world_hash_test.go new file mode 100644 index 00000000..1c0cfdf3 --- /dev/null +++ b/player/world_hash_test.go @@ -0,0 +1,243 @@ +package player + +import ( + "fmt" + "io" + "log/slog" + "testing" + "time" + + "github.com/df-mc/dragonfly/server/block" + dfworld "github.com/df-mc/dragonfly/server/world" + "github.com/oomph-ac/oomph/world" + "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +func TestBlockRuntimeIDToNetworkUsesHashMode(t *testing.T) { + world.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) + p.clientUsesBlockNetworkIDHashes = true + + if got := p.BlockRuntimeIDToNetwork(stoneRID); got != stoneHash { + t.Fatalf("network block ID = %d, want hash %d", got, stoneHash) + } +} + +func TestReportedBlockNetworkHashesResolve(t *testing.T) { + world.FinalizeBlockRegistry() + tests := []struct { + hash uint32 + want any + }{ + {hash: 1741778478, want: block.Cobblestone{}}, + {hash: 2150698529, want: block.Stone{}}, + } + for _, test := range tests { + runtimeID, ok := world.BlockRegistry.HashToRuntimeID(test.hash) + if !ok { + t.Fatalf("reported hash %d did not resolve", test.hash) + } + got, ok := world.BlockRegistry.BlockByRuntimeID(runtimeID) + if !ok { + t.Fatalf("runtime ID %d for hash %d did not resolve", runtimeID, test.hash) + } + if fmt.Sprintf("%T", got) != fmt.Sprintf("%T", test.want) { + t.Fatalf("hash %d resolved to %T, want %T", test.hash, got, test.want) + } + } +} + +func TestConvertToStackAcceptsSignedBlockNetworkHash(t *testing.T) { + world.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok || int32(stoneHash) >= 0 { + t.Fatalf("stone network hash = %d, want a signed-negative hash", stoneHash) + } + p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) + p.GameDat.UseBlockNetworkIDHashes = true + + stack := p.ConvertToStack(protocol.ItemStack{BlockRuntimeID: int32(stoneHash), Count: 1}) + if _, ok := stack.Item().(block.Stone); !ok { + t.Fatalf("converted item = %T, want block.Stone", stack.Item()) + } +} + +func TestBlockRuntimeIDTranslationSeparatesClientAndBackendModes(t *testing.T) { + world.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) + p.clientUsesBlockNetworkIDHashes = true + p.GameDat.UseBlockNetworkIDHashes = false + + if got := p.BlockRuntimeIDFromClient(stoneHash); got != stoneRID { + t.Fatalf("client hash translated to %d, want runtime ID %d", got, stoneRID) + } + if got := p.BlockRuntimeIDFromClientToBackend(stoneHash); got != stoneRID { + t.Fatalf("client hash translated for backend to %d, want runtime ID %d", got, stoneRID) + } +} + +func TestRewriteClientBlockNetworkIDsCoversAuthInputInteraction(t *testing.T) { + world.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) + p.clientUsesBlockNetworkIDHashes = true + p.GameDat.UseBlockNetworkIDHashes = false + inputData := protocol.NewBitset(200) + inputData.Set(packet.InputFlagPerformItemInteraction) + pk := &packet.PlayerAuthInput{ + InputData: inputData, + ItemInteractionData: protocol.UseItemTransactionData{ + BlockRuntimeID: stoneHash, + HeldItem: protocol.ItemInstance{Stack: protocol.ItemStack{BlockRuntimeID: int32(stoneHash)}}, + }, + } + + if !p.rewriteClientBlockNetworkIDs(pk) { + t.Fatal("auth input interaction was not rewritten") + } + if got := pk.ItemInteractionData.BlockRuntimeID; got != stoneRID { + t.Fatalf("interaction block ID = %d, want runtime ID %d", got, stoneRID) + } + if got := uint32(pk.ItemInteractionData.HeldItem.Stack.BlockRuntimeID); got != stoneRID { + t.Fatalf("held item block ID = %d, want runtime ID %d", got, stoneRID) + } +} + +func TestRewriteClientBlockNetworkIDsCoversAuthInputCraftResults(t *testing.T) { + world.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) + p.clientUsesBlockNetworkIDHashes = true + p.GameDat.UseBlockNetworkIDHashes = false + inputData := protocol.NewBitset(200) + inputData.Set(packet.InputFlagPerformItemStackRequest) + action := &protocol.CraftResultsDeprecatedStackRequestAction{ + ResultItems: []protocol.ItemStack{{BlockRuntimeID: int32(stoneHash)}}, + } + pk := &packet.PlayerAuthInput{ + InputData: inputData, + ItemStackRequest: protocol.ItemStackRequest{ + Actions: []protocol.StackRequestAction{action}, + }, + } + + if !p.rewriteClientBlockNetworkIDs(pk) { + t.Fatal("auth input craft results were not rewritten") + } + if got := uint32(action.ResultItems[0].BlockRuntimeID); got != stoneRID { + t.Fatalf("craft result block ID = %d, want runtime ID %d", got, stoneRID) + } +} + +func TestRewriteClientBlockNetworkIDsCoversStandaloneCraftResults(t *testing.T) { + world.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) + p.clientUsesBlockNetworkIDHashes = true + p.GameDat.UseBlockNetworkIDHashes = false + action := &protocol.CraftResultsDeprecatedStackRequestAction{ + ResultItems: []protocol.ItemStack{{BlockRuntimeID: int32(stoneHash)}}, + } + pk := &packet.ItemStackRequest{Requests: []protocol.ItemStackRequest{{ + Actions: []protocol.StackRequestAction{action}, + }}} + + if !p.rewriteClientBlockNetworkIDs(pk) { + t.Fatal("standalone craft results were not rewritten") + } + if got := uint32(action.ResultItems[0].BlockRuntimeID); got != stoneRID { + t.Fatalf("craft result block ID = %d, want runtime ID %d", got, stoneRID) + } +} + +func TestRewriteServerInventoryStackUsesRetainedClientMode(t *testing.T) { + world.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) + p.clientUsesBlockNetworkIDHashes = false + p.GameDat.UseBlockNetworkIDHashes = true + pk := &packet.InventorySlot{ + NewItem: protocol.ItemInstance{Stack: protocol.ItemStack{BlockRuntimeID: int32(stoneHash)}}, + StorageItem: protocol.Option(protocol.ItemInstance{Stack: protocol.ItemStack{BlockRuntimeID: int32(stoneHash)}}), + } + + if !p.rewriteServerItemBlockNetworkIDs(pk) { + t.Fatal("inventory slot was not rewritten") + } + if got := uint32(pk.NewItem.Stack.BlockRuntimeID); got != stoneRID { + t.Fatalf("inventory block ID = %d, want runtime ID %d", got, stoneRID) + } + storageItem, ok := pk.StorageItem.Value() + if !ok { + t.Fatal("inventory storage item was removed") + } + if got := uint32(storageItem.Stack.BlockRuntimeID); got != stoneRID { + t.Fatalf("inventory storage block ID = %d, want runtime ID %d", got, stoneRID) + } + original := []protocol.ItemInstance{{Stack: protocol.ItemStack{BlockRuntimeID: int32(stoneHash)}}} + content := &packet.InventoryContent{Content: original} + if !p.rewriteServerItemBlockNetworkIDs(content) { + t.Fatal("inventory content was not rewritten") + } + if got := uint32(content.Content[0].Stack.BlockRuntimeID); got != stoneRID { + t.Fatalf("inventory content block ID = %d, want runtime ID %d", got, stoneRID) + } + if got := uint32(original[0].Stack.BlockRuntimeID); got != stoneHash { + t.Fatalf("retained backend inventory block ID = %d, want hash %d", got, stoneHash) + } +} + +func TestRewriteServerRecipesPreservesBackendOutputs(t *testing.T) { + world.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) + p.clientUsesBlockNetworkIDHashes = true + p.GameDat.UseBlockNetworkIDHashes = false + backendRecipe := &protocol.ShapedChemistryRecipe{ShapedRecipe: protocol.ShapedRecipe{ + Output: []protocol.ItemStack{{BlockRuntimeID: int32(stoneRID)}}, + }} + pk := &packet.CraftingData{Recipes: []protocol.Recipe{backendRecipe}} + + if !p.rewriteServerItemBlockNetworkIDs(pk) { + t.Fatal("crafting data was not rewritten") + } + clientRecipe := pk.Recipes[0].(*protocol.ShapedChemistryRecipe) + if got := uint32(clientRecipe.Output[0].BlockRuntimeID); got != stoneHash { + t.Fatalf("client recipe block ID = %d, want hash %d", got, stoneHash) + } + if got := uint32(backendRecipe.Output[0].BlockRuntimeID); got != stoneRID { + t.Fatalf("retained backend recipe block ID = %d, want runtime ID %d", got, stoneRID) + } +} diff --git a/world/cache.go b/world/cache.go index 1db10088..999dbf6a 100644 --- a/world/cache.go +++ b/world/cache.go @@ -14,45 +14,50 @@ import ( ) var ( - chunkCache = make(map[xxh3.Uint128]*CachedChunk) + chunkCache = make(map[blockCacheKey]*CachedChunk) cMu sync.Mutex - subChunkCache = make(map[xxh3.Uint128]*CachedSubChunk) + subChunkCache = make(map[blockCacheKey]*CachedSubChunk) scMu sync.Mutex ) -func unsubC(hash xxh3.Uint128) { +type blockCacheKey struct { + hash xxh3.Uint128 + networkHashes bool +} + +func unsubC(key blockCacheKey) { cMu.Lock() defer cMu.Unlock() - if c, ok := chunkCache[hash]; ok { + if c, ok := chunkCache[key]; ok { c.subs.Add(-1) if c.subs.Load() <= 0 { - delete(chunkCache, hash) + delete(chunkCache, key) } } } -func unsubSC(hash xxh3.Uint128) { +func unsubSC(key blockCacheKey) { scMu.Lock() defer scMu.Unlock() - if c, ok := subChunkCache[hash]; ok { + if c, ok := subChunkCache[key]; ok { //fmt.Println("unsubscribing from subchunk", hash, c.subs.Load()) c.subs.Add(-1) if c.subs.Load() <= 0 { //fmt.Println("deleting subchunk from cache", hash) - delete(subChunkCache, hash) + delete(subChunkCache, key) } } } -func CacheSubChunk(payload *bytes.Buffer, c *chunk.Chunk, pos protocol.ChunkPos) (*CachedSubChunk, error) { +func CacheSubChunk(payload *bytes.Buffer, c *chunk.Chunk, pos protocol.ChunkPos, networkHashes bool) (*CachedSubChunk, error) { scMu.Lock() defer scMu.Unlock() - hash := xxh3.Hash128(payload.Bytes()) - if sc, ok := subChunkCache[hash]; ok { + key := blockCacheKey{hash: xxh3.Hash128(payload.Bytes()), networkHashes: networkHashes} + if sc, ok := subChunkCache[key]; ok { sc.subs.Add(1) //fmt.Println("returning cached subchunk", hash) return sc, nil @@ -63,24 +68,27 @@ func CacheSubChunk(payload *bytes.Buffer, c *chunk.Chunk, pos protocol.ChunkPos) if err != nil { return nil, err } + if networkHashes { + decodedSC.ConvertBlockNetworkHashesToRuntimeIDs(BlockRegistry) + } - cachedSC := &CachedSubChunk{hash: hash, layer: index, sc: decodedSC} + cachedSC := &CachedSubChunk{hash: key.hash, layer: index, sc: decodedSC} cachedSC.subs.Add(1) - subChunkCache[hash] = cachedSC + subChunkCache[key] = cachedSC //fmt.Println("newly cached subchunk", hash) return cachedSC, nil } -func CacheChunk(input *packet.LevelChunk) (ChunkInfo, error) { +func CacheChunk(input *packet.LevelChunk, networkHashes bool) (ChunkInfo, error) { cMu.Lock() defer cMu.Unlock() - hash := xxh3.Hash128(input.RawPayload) - if c, ok := chunkCache[hash]; ok { + key := blockCacheKey{hash: xxh3.Hash128(input.RawPayload), networkHashes: networkHashes} + if c, ok := chunkCache[key]; ok { c.subs.Add(1) - //fmt.Println("returning cached chunk", hash) - return ChunkInfo{Hash: hash, Chunk: c.chunk, Cached: true}, nil + //fmt.Println("returning cached chunk", key.hash) + return ChunkInfo{Hash: key.hash, networkHashes: networkHashes, Chunk: c.chunk, Cached: true}, nil } dimension, ok := world.DimensionByID(int(input.Dimension)) @@ -97,12 +105,73 @@ func CacheChunk(input *packet.LevelChunk) (ChunkInfo, error) { if err != nil { return ChunkInfo{}, err } + if networkHashes { + decodedChunk.ConvertBlockNetworkHashesToRuntimeIDs() + } decodedChunk.Compact() - cachedChunk := &CachedChunk{hash: hash, chunk: decodedChunk} + cachedChunk := &CachedChunk{hash: key.hash, chunk: decodedChunk} cachedChunk.subs.Add(1) - chunkCache[hash] = cachedChunk - return ChunkInfo{Hash: hash, Chunk: cachedChunk.chunk, Cached: true}, nil + chunkCache[key] = cachedChunk + return ChunkInfo{Hash: key.hash, networkHashes: networkHashes, Chunk: cachedChunk.chunk, Cached: true}, nil +} + +// ReencodeLevelChunk converts the block palettes in input between network ID representations while preserving trailing +// block entity data. +func ReencodeLevelChunk(input *packet.LevelChunk, sourceHashes, targetHashes bool) error { + dimension, ok := world.DimensionByID(int(input.Dimension)) + if !ok { + return fmt.Errorf("unknown dimension %v", input.Dimension) + } + buf := bytes.NewBuffer(input.RawPayload) + decoded, _, err := chunk.NetworkDecodeBuffer(BlockRegistry, buf, int(input.SubChunkCount), dimension.Range()) + if err != nil { + return err + } + if sourceHashes { + decoded.ConvertBlockNetworkHashesToRuntimeIDs() + } + var data chunk.SerialisedData + if targetHashes { + data = chunk.EncodeWithBlockNetworkHashes(decoded) + } else { + data = chunk.Encode(decoded, chunk.NetworkEncoding) + } + out := bytes.NewBuffer(make([]byte, 0, len(input.RawPayload))) + for _, sub := range data.SubChunks { + out.Write(sub) + } + out.Write(data.Biomes) + out.Write(buf.Bytes()) + input.RawPayload = out.Bytes() + input.SubChunkCount = uint32(len(data.SubChunks)) + return nil +} + +// ReencodeSubChunk converts one successful SubChunk entry between network ID representations while preserving trailing +// block entity data. +func ReencodeSubChunk(payload []byte, dimension world.Dimension, sourceHashes, targetHashes bool) ([]byte, error) { + buf := bytes.NewBuffer(payload) + decodedChunk := chunk.New(BlockRegistry, dimension.Range()) + var index byte + decoded, err := decodeSubChunk(buf, decodedChunk, &index, chunk.NetworkEncoding) + if err != nil { + return nil, err + } + if sourceHashes { + decoded.ConvertBlockNetworkHashesToRuntimeIDs(BlockRegistry) + } + if int(index) >= len(decodedChunk.Sub()) { + return nil, fmt.Errorf("subchunk index %d out of range", index) + } + decodedChunk.Sub()[index] = decoded + var encoded []byte + if targetHashes { + encoded = chunk.EncodeSubChunkWithBlockNetworkHashes(decodedChunk, int(index)) + } else { + encoded = chunk.EncodeSubChunk(decodedChunk, chunk.NetworkEncoding, int(index)) + } + return append(encoded, buf.Bytes()...), nil } type CachedSubChunk struct { diff --git a/world/world.go b/world/world.go index de84fa66..97c39f5b 100644 --- a/world/world.go +++ b/world/world.go @@ -16,16 +16,17 @@ import ( ) type ChunkInfo struct { - Cached bool - Hash xxh3.Uint128 - Chunk *chunk.Chunk + Cached bool + Hash xxh3.Uint128 + networkHashes bool + Chunk *chunk.Chunk } type World struct { lastCleanPos protocol.ChunkPos chunks map[protocol.ChunkPos]ChunkInfo - subChunks map[protocol.ChunkPos][]xxh3.Uint128 + subChunks map[protocol.ChunkPos][]blockCacheKey exemptedChunks map[protocol.ChunkPos]struct{} blockUpdates map[protocol.ChunkPos]map[df_cube.Pos]world.Block @@ -38,7 +39,7 @@ type World struct { func New(debugFn func(string, ...any)) *World { return &World{ chunks: make(map[protocol.ChunkPos]ChunkInfo), - subChunks: make(map[protocol.ChunkPos][]xxh3.Uint128), + subChunks: make(map[protocol.ChunkPos][]blockCacheKey), exemptedChunks: make(map[protocol.ChunkPos]struct{}), blockUpdates: make(map[protocol.ChunkPos]map[df_cube.Pos]world.Block), @@ -66,11 +67,11 @@ func (w *World) AddChunk(chunkPos protocol.ChunkPos, c ChunkInfo) { } // AddSubChunk adds a subchunk to the world. -func (w *World) AddSubChunk(chunkPos protocol.ChunkPos, hash xxh3.Uint128) { +func (w *World) AddSubChunk(chunkPos protocol.ChunkPos, hash xxh3.Uint128, networkHashes bool) { if _, ok := w.subChunks[chunkPos]; !ok { - w.subChunks[chunkPos] = make([]xxh3.Uint128, 0, 16) + w.subChunks[chunkPos] = make([]blockCacheKey, 0, 16) } - w.subChunks[chunkPos] = append(w.subChunks[chunkPos], hash) + w.subChunks[chunkPos] = append(w.subChunks[chunkPos], blockCacheKey{hash: hash, networkHashes: networkHashes}) } // Chunk returns a cached chunk at the position passed. The mutex is @@ -168,11 +169,11 @@ func (w *World) PurgeChunks() { func (w *World) removeChunk(info ChunkInfo, chunkPos protocol.ChunkPos) { if info.Cached { - unsubC(info.Hash) + unsubC(blockCacheKey{hash: info.Hash, networkHashes: info.networkHashes}) } if subChunks, ok := w.subChunks[chunkPos]; ok { - for _, subChunkHash := range subChunks { - unsubSC(subChunkHash) + for _, subChunkKey := range subChunks { + unsubSC(subChunkKey) } } delete(w.subChunks, chunkPos) From 52e67d1ecf673d1372bdd4ac979fe74ec36cb1a8 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 13 Jul 2026 18:59:24 -0400 Subject: [PATCH 02/16] Update Dragonfly hash helpers --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d7ab4976..434e860e 100755 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/oomph-ac/oomph go 1.26.0 -replace github.com/df-mc/dragonfly => github.com/oomph-ac/dragonfly v0.0.0-20260713224537-2de5b48545f8 +replace github.com/df-mc/dragonfly => github.com/oomph-ac/dragonfly v0.0.0-20260713225844-d3a42efadf41 replace github.com/sandertv/gophertunnel => github.com/hashimthearab/gophertunnel v1.25.3-0.20260713165240-828896b9c778 diff --git a/go.sum b/go.sum index 5d8cdfd8..cd5b6bed 100755 --- a/go.sum +++ b/go.sum @@ -54,8 +54,8 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/oomph-ac/dragonfly v0.0.0-20260713224537-2de5b48545f8 h1:dQCMLVDOpW2ZmPNfOJk4YKxPnCvQmg9N+0K58PgO5cI= -github.com/oomph-ac/dragonfly v0.0.0-20260713224537-2de5b48545f8/go.mod h1:OFJjCz3xfDalVZEtV4lJdbenwKDEAczKHikHN7kVwMc= +github.com/oomph-ac/dragonfly v0.0.0-20260713225844-d3a42efadf41 h1:6ZEfitk1lxsoMjiFZZhtDFthFqrJCDwosvqt5ZeMWHc= +github.com/oomph-ac/dragonfly v0.0.0-20260713225844-d3a42efadf41/go.mod h1:OFJjCz3xfDalVZEtV4lJdbenwKDEAczKHikHN7kVwMc= github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0= github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk= github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= From 6bade6fbbe80f11e6b27fed92eb701bb752907aa Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 13 Jul 2026 19:05:14 -0400 Subject: [PATCH 03/16] Update Dragonfly hash helper tests --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 434e860e..d794ec03 100755 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/oomph-ac/oomph go 1.26.0 -replace github.com/df-mc/dragonfly => github.com/oomph-ac/dragonfly v0.0.0-20260713225844-d3a42efadf41 +replace github.com/df-mc/dragonfly => github.com/oomph-ac/dragonfly v0.0.0-20260713230435-a683785ee72e replace github.com/sandertv/gophertunnel => github.com/hashimthearab/gophertunnel v1.25.3-0.20260713165240-828896b9c778 diff --git a/go.sum b/go.sum index cd5b6bed..a103df9b 100755 --- a/go.sum +++ b/go.sum @@ -54,8 +54,8 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/oomph-ac/dragonfly v0.0.0-20260713225844-d3a42efadf41 h1:6ZEfitk1lxsoMjiFZZhtDFthFqrJCDwosvqt5ZeMWHc= -github.com/oomph-ac/dragonfly v0.0.0-20260713225844-d3a42efadf41/go.mod h1:OFJjCz3xfDalVZEtV4lJdbenwKDEAczKHikHN7kVwMc= +github.com/oomph-ac/dragonfly v0.0.0-20260713230435-a683785ee72e h1:b0O+I3X11M91SlUlvDEZ7Nihohb0SoUnOiUCZufXyfE= +github.com/oomph-ac/dragonfly v0.0.0-20260713230435-a683785ee72e/go.mod h1:OFJjCz3xfDalVZEtV4lJdbenwKDEAczKHikHN7kVwMc= github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0= github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk= github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= From 56a8a8255c829cf4cd43d16198ca7501a37afbc5 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 13 Jul 2026 20:25:49 -0400 Subject: [PATCH 04/16] feat: add block network codecs --- world/blocknetwork/codec.go | 70 +++++++++++++ world/blocknetwork/codec_test.go | 173 +++++++++++++++++++++++++++++++ world/blocknetwork/mode.go | 20 ++++ 3 files changed, 263 insertions(+) create mode 100644 world/blocknetwork/codec.go create mode 100644 world/blocknetwork/codec_test.go create mode 100644 world/blocknetwork/mode.go diff --git a/world/blocknetwork/codec.go b/world/blocknetwork/codec.go new file mode 100644 index 00000000..a0ad171a --- /dev/null +++ b/world/blocknetwork/codec.go @@ -0,0 +1,70 @@ +package blocknetwork + +import "github.com/df-mc/dragonfly/server/world/chunk" + +// Codec converts IDs used by one network endpoint to and from canonical block-registry runtime IDs. +type Codec struct { + registry chunk.BlockRegistry + mode Mode +} + +// NewCodec returns a Codec backed by registry using mode. It panics if registry is nil. +func NewCodec(registry chunk.BlockRegistry, mode Mode) Codec { + if registry == nil { + panic("blocknetwork: nil block registry") + } + return Codec{registry: registry, mode: mode} +} + +// Mode returns the network representation used by the codec. +func (c Codec) Mode() Mode { + return c.mode +} + +// ToRuntimeID converts a network ID to a canonical block-registry runtime ID. +func (c Codec) ToRuntimeID(networkID uint32) (uint32, bool) { + if c.mode == Hashes { + return c.registry.HashToRuntimeID(networkID) + } + return networkID, networkID < uint32(c.registry.BlockCount()) +} + +// FromRuntimeID converts a canonical block-registry runtime ID to the codec's network representation. +func (c Codec) FromRuntimeID(runtimeID uint32) (uint32, bool) { + if c.mode == Hashes { + return c.registry.RuntimeIDToHash(runtimeID) + } + return runtimeID, runtimeID < uint32(c.registry.BlockCount()) +} + +// Translator converts block IDs from one endpoint codec to another. +type Translator struct { + source Codec + target Codec +} + +// NewTranslator returns a Translator that converts IDs from source to target. +func NewTranslator(source, target Codec) Translator { + return Translator{source: source, target: target} +} + +// Required reports whether source and target use different block ID representations. +func (t Translator) Required() bool { + return t.source.Mode() != t.target.Mode() +} + +// Translate converts a source network ID to the target representation. Unknown IDs are preserved. +func (t Translator) Translate(networkID uint32) uint32 { + if !t.Required() { + return networkID + } + runtimeID, ok := t.source.ToRuntimeID(networkID) + if !ok { + return networkID + } + translated, ok := t.target.FromRuntimeID(runtimeID) + if !ok { + return networkID + } + return translated +} diff --git a/world/blocknetwork/codec_test.go b/world/blocknetwork/codec_test.go new file mode 100644 index 00000000..088b847e --- /dev/null +++ b/world/blocknetwork/codec_test.go @@ -0,0 +1,173 @@ +package blocknetwork_test + +import ( + "math" + "os" + "testing" + + "github.com/df-mc/dragonfly/server/block" + "github.com/oomph-ac/oomph/world" + "github.com/oomph-ac/oomph/world/blocknetwork" +) + +func TestMain(m *testing.M) { + world.FinalizeBlockRegistry() + os.Exit(m.Run()) +} + +func TestModeFromHashes(t *testing.T) { + t.Parallel() + + if got := blocknetwork.ModeFromHashes(false); got != blocknetwork.RuntimeIDs { + t.Fatalf("ModeFromHashes(false) = %v, want RuntimeIDs", got) + } + if got := blocknetwork.ModeFromHashes(true); got != blocknetwork.Hashes { + t.Fatalf("ModeFromHashes(true) = %v, want Hashes", got) + } +} + +func TestCodecConvertsNetworkIDs(t *testing.T) { + t.Parallel() + + runtimeID := world.BlockRegistry.BlockRuntimeID(block.Stone{}) + networkHash, ok := world.BlockRegistry.RuntimeIDToHash(runtimeID) + if !ok { + t.Fatal("stone runtime ID has no network hash") + } + + tests := []struct { + name string + codec blocknetwork.Codec + networkID uint32 + }{ + {name: "runtime IDs", codec: blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.RuntimeIDs), networkID: runtimeID}, + {name: "hashes", codec: blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.Hashes), networkID: networkHash}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got, found := test.codec.ToRuntimeID(test.networkID); !found || got != runtimeID { + t.Fatalf("ToRuntimeID(%d) = (%d, %t), want (%d, true)", test.networkID, got, found, runtimeID) + } + if got, found := test.codec.FromRuntimeID(runtimeID); !found || got != test.networkID { + t.Fatalf("FromRuntimeID(%d) = (%d, %t), want (%d, true)", runtimeID, got, found, test.networkID) + } + if got := test.codec.Mode(); got != blocknetwork.ModeFromHashes(test.name == "hashes") { + t.Fatalf("Mode() = %v, unexpected for %s", got, test.name) + } + }) + } +} + +func TestCodecRejectsUnknownIDs(t *testing.T) { + t.Parallel() + + runtimeCodec := blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.RuntimeIDs) + hashCodec := blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.Hashes) + unknownRuntimeID := uint32(world.BlockRegistry.BlockCount() + 100) + unknownHash := unknownNetworkHash(t) + + if _, ok := runtimeCodec.ToRuntimeID(unknownRuntimeID); ok { + t.Fatalf("runtime codec accepted unknown runtime ID %d", unknownRuntimeID) + } + if _, ok := runtimeCodec.FromRuntimeID(unknownRuntimeID); ok { + t.Fatalf("runtime codec encoded unknown runtime ID %d", unknownRuntimeID) + } + if _, ok := hashCodec.ToRuntimeID(unknownHash); ok { + t.Fatalf("hash codec accepted unknown hash %d", unknownHash) + } + if _, ok := hashCodec.FromRuntimeID(unknownRuntimeID); ok { + t.Fatalf("hash codec encoded unknown runtime ID %d", unknownRuntimeID) + } +} + +func TestTranslatorConvertsBetweenModes(t *testing.T) { + t.Parallel() + + runtimeID := world.BlockRegistry.BlockRuntimeID(block.Stone{}) + networkHash, ok := world.BlockRegistry.RuntimeIDToHash(runtimeID) + if !ok { + t.Fatal("stone runtime ID has no network hash") + } + runtimeCodec := blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.RuntimeIDs) + hashCodec := blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.Hashes) + + tests := []struct { + name string + source blocknetwork.Codec + target blocknetwork.Codec + input uint32 + want uint32 + needed bool + }{ + {name: "runtime to runtime", source: runtimeCodec, target: runtimeCodec, input: runtimeID, want: runtimeID}, + {name: "runtime to hash", source: runtimeCodec, target: hashCodec, input: runtimeID, want: networkHash, needed: true}, + {name: "hash to runtime", source: hashCodec, target: runtimeCodec, input: networkHash, want: runtimeID, needed: true}, + {name: "hash to hash", source: hashCodec, target: hashCodec, input: networkHash, want: networkHash}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + translator := blocknetwork.NewTranslator(test.source, test.target) + if got := translator.Required(); got != test.needed { + t.Fatalf("Required() = %t, want %t", got, test.needed) + } + if got := translator.Translate(test.input); got != test.want { + t.Fatalf("Translate(%d) = %d, want %d", test.input, got, test.want) + } + }) + } +} + +func TestTranslatorPreservesUnknownAndHighBitIDs(t *testing.T) { + t.Parallel() + + runtimeCodec := blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.RuntimeIDs) + hashCodec := blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.Hashes) + unknownHash := unknownNetworkHash(t) + if got := blocknetwork.NewTranslator(hashCodec, runtimeCodec).Translate(unknownHash); got != unknownHash { + t.Fatalf("unknown hash translated to %d, want original %d", got, unknownHash) + } + unknownRuntimeID := uint32(world.BlockRegistry.BlockCount() + 100) + if got := blocknetwork.NewTranslator(runtimeCodec, hashCodec).Translate(unknownRuntimeID); got != unknownRuntimeID { + t.Fatalf("unknown runtime ID translated to %d, want original %d", got, unknownRuntimeID) + } + + highBitHash, runtimeID := highBitNetworkHash(t) + if got := blocknetwork.NewTranslator(hashCodec, runtimeCodec).Translate(highBitHash); got != runtimeID { + t.Fatalf("high-bit hash %#x translated to %d, want runtime ID %d", highBitHash, got, runtimeID) + } + if got := blocknetwork.NewTranslator(runtimeCodec, hashCodec).Translate(runtimeID); got != highBitHash { + t.Fatalf("runtime ID %d translated to %#x, want high-bit hash %#x", runtimeID, got, highBitHash) + } +} + +func TestNewCodecRejectsNilRegistry(t *testing.T) { + t.Parallel() + + defer func() { + if recover() == nil { + t.Fatal("NewCodec(nil, RuntimeIDs) did not panic") + } + }() + blocknetwork.NewCodec(nil, blocknetwork.RuntimeIDs) +} + +func unknownNetworkHash(t *testing.T) uint32 { + t.Helper() + for hash := uint32(math.MaxUint32); ; hash-- { + if _, ok := world.BlockRegistry.HashToRuntimeID(hash); !ok { + return hash + } + } +} + +func highBitNetworkHash(t *testing.T) (uint32, uint32) { + t.Helper() + for runtimeID := range uint32(world.BlockRegistry.BlockCount()) { + hash, ok := world.BlockRegistry.RuntimeIDToHash(runtimeID) + if ok && hash > math.MaxInt32 { + return hash, runtimeID + } + } + t.Fatal("block registry contains no high-bit network hash") + return 0, 0 +} diff --git a/world/blocknetwork/mode.go b/world/blocknetwork/mode.go new file mode 100644 index 00000000..e71a8677 --- /dev/null +++ b/world/blocknetwork/mode.go @@ -0,0 +1,20 @@ +// Package blocknetwork converts block IDs between Bedrock's runtime-ID and network-hash representations. +package blocknetwork + +// Mode identifies the block ID representation used by a network endpoint. +type Mode uint8 + +const ( + // RuntimeIDs identifies ordinary block-registry runtime IDs. + RuntimeIDs Mode = iota + // Hashes identifies block network hashes. + Hashes +) + +// ModeFromHashes returns Hashes when enabled is true and RuntimeIDs otherwise. +func ModeFromHashes(enabled bool) Mode { + if enabled { + return Hashes + } + return RuntimeIDs +} From ec3e081a2238739fbab15c20cd9f5ed1075714c1 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 13 Jul 2026 20:29:45 -0400 Subject: [PATCH 05/16] refactor: centralize block network modes --- player/component/acknowledgement/chunks.go | 6 ++-- player/component/world_hash_test.go | 23 ++++++++++++-- player/network.go | 25 ++++++++++++++- player/packet.go | 20 ++++++------ player/player.go | 11 +++++-- player/world.go | 36 ++++++++++------------ player/world_hash_test.go | 28 ++++++++--------- world/cache.go | 33 ++++++++++---------- world/world.go | 15 ++++----- 9 files changed, 120 insertions(+), 77 deletions(-) diff --git a/player/component/acknowledgement/chunks.go b/player/component/acknowledgement/chunks.go index d07d22ac..e790ac83 100644 --- a/player/component/acknowledgement/chunks.go +++ b/player/component/acknowledgement/chunks.go @@ -29,7 +29,7 @@ func (ack *ChunkUpdate) Run() { ack.mPlayer.Disconnect(game.ErrorChunkCacheUnsupported) return } - cInfo, err := oworld.CacheChunk(ack.pk, ack.mPlayer.GameDat.UseBlockNetworkIDHashes) + cInfo, err := oworld.CacheChunk(ack.pk, ack.mPlayer.BackendBlockNetwork()) if err != nil { ack.mPlayer.Disconnect(fmt.Sprintf(game.ErrorInternalDecodeChunk, err)) return @@ -93,13 +93,13 @@ func (ack *SubChunkUpdate) Run() { bufUsed = true buf.Write(entry.RawPayload) - cachedSub, err := oworld.CacheSubChunk(buf, ch, chunkPos, ack.mPlayer.GameDat.UseBlockNetworkIDHashes) + cachedSub, err := oworld.CacheSubChunk(buf, ch, chunkPos, ack.mPlayer.BackendBlockNetwork()) if err != nil { ack.mPlayer.Disconnect(fmt.Sprintf(game.ErrorInternalDecodeChunk, err)) continue } ch.Sub()[cachedSub.Layer()] = cachedSub.SubChunk() - ack.mPlayer.World().AddSubChunk(chunkPos, cachedSub.Hash(), ack.mPlayer.GameDat.UseBlockNetworkIDHashes) + ack.mPlayer.World().AddSubChunk(chunkPos, cachedSub.Hash(), ack.mPlayer.BackendBlockNetwork()) ack.mPlayer.Dbg.Notify(player.DebugModeChunks, true, "cached subchunk %d at %v", cachedSub.Layer(), chunkPos) case protocol.SubChunkResultSuccessAllAir: ack.mPlayer.Dbg.Notify(player.DebugModeChunks, true, "all-air chunk at %v", chunkPos) diff --git a/player/component/world_hash_test.go b/player/component/world_hash_test.go index 8bd47f62..b667a76e 100644 --- a/player/component/world_hash_test.go +++ b/player/component/world_hash_test.go @@ -14,6 +14,7 @@ import ( "github.com/oomph-ac/oomph/player" playercontext "github.com/oomph-ac/oomph/player/context" oomphworld "github.com/oomph-ac/oomph/world" + "github.com/oomph-ac/oomph/world/blocknetwork" "github.com/sandertv/gophertunnel/minecraft" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" @@ -55,7 +56,7 @@ func TestWorldUpdaterDecodesHashedLevelChunk(t *testing.T) { p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{CurrentTime: time.Now()}, nil) Register(p) - p.GameDat.UseBlockNetworkIDHashes = true + p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: true}}) p.WorldUpdater().HandleLevelChunk(&packet.LevelChunk{ Position: protocol.ChunkPos{0, 0}, Dimension: packet.DimensionOverworld, @@ -79,7 +80,7 @@ func TestWorldUpdaterConvertsHashedBlockUpdate(t *testing.T) { } p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{CurrentTime: time.Now()}, nil) Register(p) - p.GameDat.UseBlockNetworkIDHashes = true + p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: true}}) updater := p.WorldUpdater().(*WorldUpdaterComponent) position := cube.Pos{4, 5, 6} @@ -103,7 +104,25 @@ func TestClientBlockHashModeSurvivesBackendTransfer(t *testing.T) { p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{CurrentTime: time.Now()}, nil) Register(p) p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: true}}) + if got := p.ClientBlockNetwork().Mode(); got != blocknetwork.Hashes { + t.Fatalf("initial client mode = %v, want Hashes", got) + } + if got := p.BackendBlockNetwork().Mode(); got != blocknetwork.Hashes { + t.Fatalf("initial backend mode = %v, want Hashes", got) + } + if p.ClientToBackendBlockNetwork().Required() || p.BackendToClientBlockNetwork().Required() { + t.Fatal("matching initial endpoint modes require translation") + } p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: false}}) + if got := p.ClientBlockNetwork().Mode(); got != blocknetwork.Hashes { + t.Fatalf("client mode after transfer = %v, want retained Hashes", got) + } + if got := p.BackendBlockNetwork().Mode(); got != blocknetwork.RuntimeIDs { + t.Fatalf("backend mode after transfer = %v, want RuntimeIDs", got) + } + if !p.ClientToBackendBlockNetwork().Required() || !p.BackendToClientBlockNetwork().Required() { + t.Fatal("different endpoint modes do not require translation") + } if got := p.BlockRuntimeIDToNetwork(stoneRID); got != stoneHash { t.Fatalf("client block ID after transfer = %d, want initial hash %d", got, stoneHash) diff --git a/player/network.go b/player/network.go index fa5e2b12..0ebe1bf2 100755 --- a/player/network.go +++ b/player/network.go @@ -6,6 +6,7 @@ import ( "time" "github.com/df-mc/dragonfly/server/world" + "github.com/oomph-ac/oomph/world/blocknetwork" "github.com/sandertv/gophertunnel/minecraft" "github.com/sandertv/gophertunnel/minecraft/protocol/login" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" @@ -41,14 +42,16 @@ func (p *Player) SetServerConn(conn ServerConn) { return } + backendBlockNetwork := blocknetwork.NewCodec(p.World().BlockRegistry(), blocknetwork.ModeFromHashes(conn.GameData().UseBlockNetworkIDHashes)) if p.serverConn == nil { - p.clientUsesBlockNetworkIDHashes = conn.GameData().UseBlockNetworkIDHashes + p.clientBlockNetwork = backendBlockNetwork for _, item := range conn.GameData().Items { if i, ok := world.ItemByName(item.Name, 0); ok { p.items[item.RuntimeID] = i } } } + p.backendBlockNetwork = backendBlockNetwork p.GameDat = conn.GameData() p.serverConn = conn @@ -64,6 +67,26 @@ func (p *Player) SetServerConn(conn ServerConn) { p.movement.ResetTransferState(p.GameDat.PlayerPosition) } +// ClientBlockNetwork returns the codec for block IDs visible to the client. +func (p *Player) ClientBlockNetwork() blocknetwork.Codec { + return p.clientBlockNetwork +} + +// BackendBlockNetwork returns the codec for block IDs used by the current backend. +func (p *Player) BackendBlockNetwork() blocknetwork.Codec { + return p.backendBlockNetwork +} + +// ClientToBackendBlockNetwork returns a translator for client-originated block IDs. +func (p *Player) ClientToBackendBlockNetwork() blocknetwork.Translator { + return blocknetwork.NewTranslator(p.clientBlockNetwork, p.backendBlockNetwork) +} + +// BackendToClientBlockNetwork returns a translator for backend-originated block IDs. +func (p *Player) BackendToClientBlockNetwork() blocknetwork.Translator { + return blocknetwork.NewTranslator(p.backendBlockNetwork, p.clientBlockNetwork) +} + // BackendTransferState contains client-visible state that must be cleared when // a proxy switches this player to another backend. type BackendTransferState struct { diff --git a/player/packet.go b/player/packet.go index 83ec0cb8..92f9ac50 100644 --- a/player/packet.go +++ b/player/packet.go @@ -281,7 +281,7 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { } } p.RunDetections(pk) - if p.GameDat.UseBlockNetworkIDHashes != p.clientUsesBlockNetworkIDHashes && p.rewriteClientBlockNetworkIDs(pk) { + if p.ClientToBackendBlockNetwork().Required() && p.rewriteClientBlockNetworkIDs(pk) { ctx.SetModified() } } @@ -469,10 +469,9 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { p.inventory.HandleItemStackResponse(pk) case *packet.LevelChunk: p.worldUpdater.HandleLevelChunk(pk) - backendHashes, clientHashes := p.GameDat.UseBlockNetworkIDHashes, p.clientUsesBlockNetworkIDHashes fullChunk := !pk.CacheEnabled && pk.SubChunkCount != protocol.SubChunkRequestModeLimited && pk.SubChunkCount != protocol.SubChunkRequestModeLimitless - if fullChunk && (p.opts.Network.AttemptFixChunks || backendHashes != clientHashes) { - if err := oworld.ReencodeLevelChunk(pk, backendHashes, clientHashes); err != nil { + if fullChunk && (p.opts.Network.AttemptFixChunks || p.BackendToClientBlockNetwork().Required()) { + if err := oworld.ReencodeLevelChunk(pk, p.BackendBlockNetwork(), p.ClientBlockNetwork()); err != nil { p.Log().Warn("unable to re-encode chunk", "error", err) } else { ctx.SetModified() @@ -529,8 +528,7 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { p.gamemodeHandle.Handle(pk) case *packet.SubChunk: p.worldUpdater.HandleSubChunk(pk) - backendHashes, clientHashes := p.GameDat.UseBlockNetworkIDHashes, p.clientUsesBlockNetworkIDHashes - if !pk.CacheEnabled && backendHashes != clientHashes { + if !pk.CacheEnabled && p.BackendToClientBlockNetwork().Required() { dimension, ok := world.DimensionByID(int(pk.Dimension)) if !ok { dimension = world.Overworld @@ -540,7 +538,7 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { if entry.Result != protocol.SubChunkResultSuccess { continue } - payload, err := oworld.ReencodeSubChunk(entry.RawPayload, dimension, backendHashes, clientHashes) + payload, err := oworld.ReencodeSubChunk(entry.RawPayload, dimension, p.BackendBlockNetwork(), p.ClientBlockNetwork()) if err != nil { p.Log().Warn("unable to re-encode subchunk", "error", err) continue @@ -562,18 +560,18 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { } case *packet.UpdateBlock: p.worldUpdater.HandleUpdateBlock(pk) - if p.GameDat.UseBlockNetworkIDHashes != p.clientUsesBlockNetworkIDHashes { + if p.BackendToClientBlockNetwork().Required() { pk.NewBlockRuntimeID = p.BlockRuntimeIDFromBackendToClient(pk.NewBlockRuntimeID) ctx.SetModified() } case *packet.UpdateBlockSynced: - if p.GameDat.UseBlockNetworkIDHashes != p.clientUsesBlockNetworkIDHashes { + if p.BackendToClientBlockNetwork().Required() { pk.NewBlockRuntimeID = p.BlockRuntimeIDFromBackendToClient(pk.NewBlockRuntimeID) ctx.SetModified() } case *packet.UpdateSubChunkBlocks: p.worldUpdater.HandleUpdateSubChunkBlocks(pk) - if p.GameDat.UseBlockNetworkIDHashes != p.clientUsesBlockNetworkIDHashes { + if p.BackendToClientBlockNetwork().Required() { for i := range pk.Blocks { pk.Blocks[i].BlockRuntimeID = p.BlockRuntimeIDFromBackendToClient(pk.Blocks[i].BlockRuntimeID) } @@ -610,7 +608,7 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { p.CreativeItems[item.CreativeItemNetworkID] = item } } - if p.GameDat.UseBlockNetworkIDHashes != p.clientUsesBlockNetworkIDHashes && p.rewriteServerItemBlockNetworkIDs(pk) { + if p.BackendToClientBlockNetwork().Required() && p.rewriteServerItemBlockNetworkIDs(pk) { ctx.SetModified() } } diff --git a/player/player.go b/player/player.go index d52af80a..ebff3fa2 100755 --- a/player/player.go +++ b/player/player.go @@ -17,6 +17,7 @@ import ( "github.com/oomph-ac/oomph/player/context" "github.com/oomph-ac/oomph/utils" "github.com/oomph-ac/oomph/world" + "github.com/oomph-ac/oomph/world/blocknetwork" "github.com/sandertv/gophertunnel/minecraft" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/login" @@ -71,9 +72,10 @@ type Player struct { GameDat minecraft.GameData Version int32 - // clientUsesBlockNetworkIDHashes is fixed by the initial backend's StartGame. It remains unchanged across fast - // backend transfers because the connected client does not receive another StartGame packet. - clientUsesBlockNetworkIDHashes bool + // clientBlockNetwork is fixed by the initial backend's StartGame. It remains unchanged across fast backend + // transfers because the connected client does not receive another StartGame packet. + clientBlockNetwork blocknetwork.Codec + backendBlockNetwork blocknetwork.Codec // With fast transfers, the client will still retain it's original runtime and unique IDs, so // we must translate them to new ones, while still retaining the old ones for the client to use. @@ -253,6 +255,9 @@ func New(log *slog.Logger, mState MonitoringState, listener *minecraft.Listener) listener: listener, + clientBlockNetwork: blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.RuntimeIDs), + backendBlockNetwork: blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.RuntimeIDs), + remoteEventFunc: func(e RemoteEvent, p *Player) { enc, _ := json.Marshal(e) p.SendPacketToServer(&packet.ScriptMessage{ diff --git a/player/world.go b/player/world.go index 76429a7d..408fe239 100644 --- a/player/world.go +++ b/player/world.go @@ -12,6 +12,7 @@ import ( "github.com/oomph-ac/oomph/game" "github.com/oomph-ac/oomph/utils" oworld "github.com/oomph-ac/oomph/world" + "github.com/oomph-ac/oomph/world/blocknetwork" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -74,51 +75,46 @@ func (p *Player) World() *oworld.World { // BlockRuntimeIDFromNetwork converts a network block hash to the runtime ID used by Oomph's block registry when the // backend enabled hashed block network IDs. Unknown values are preserved so callers can retain their existing fallback. func (p *Player) BlockRuntimeIDFromNetwork(id uint32) uint32 { - return p.convertBlockRuntimeID(id, p.GameDat.UseBlockNetworkIDHashes, false) + return blockRuntimeIDFromNetwork(p.backendBlockNetwork, id) } // BlockRuntimeIDToNetwork converts an Oomph block runtime ID to the hash expected by clients when the backend enabled // hashed block network IDs. Unknown values are preserved so custom block fallbacks remain intact. func (p *Player) BlockRuntimeIDToNetwork(id uint32) uint32 { - return p.convertBlockRuntimeID(id, false, p.clientUsesBlockNetworkIDHashes) + return blockRuntimeIDToNetwork(p.clientBlockNetwork, id) } // BlockRuntimeIDFromClient converts a client-visible block ID to Oomph's registry runtime ID. func (p *Player) BlockRuntimeIDFromClient(id uint32) uint32 { - return p.convertBlockRuntimeID(id, p.clientUsesBlockNetworkIDHashes, false) + return blockRuntimeIDFromNetwork(p.clientBlockNetwork, id) } // BlockRuntimeIDToBackend converts an Oomph registry runtime ID to the current backend's network representation. func (p *Player) BlockRuntimeIDToBackend(id uint32) uint32 { - return p.convertBlockRuntimeID(id, false, p.GameDat.UseBlockNetworkIDHashes) + return blockRuntimeIDToNetwork(p.backendBlockNetwork, id) } // BlockRuntimeIDFromBackendToClient translates a block ID from the current backend's representation to the one fixed // by the client's initial StartGame packet. func (p *Player) BlockRuntimeIDFromBackendToClient(id uint32) uint32 { - return p.convertBlockRuntimeID(id, p.GameDat.UseBlockNetworkIDHashes, p.clientUsesBlockNetworkIDHashes) + return p.BackendToClientBlockNetwork().Translate(id) } // BlockRuntimeIDFromClientToBackend translates a client-visible block ID to the current backend's representation. func (p *Player) BlockRuntimeIDFromClientToBackend(id uint32) uint32 { - return p.convertBlockRuntimeID(id, p.clientUsesBlockNetworkIDHashes, p.GameDat.UseBlockNetworkIDHashes) + return p.ClientToBackendBlockNetwork().Translate(id) } -func (p *Player) convertBlockRuntimeID(id uint32, fromHashes, toHashes bool) uint32 { - if fromHashes == toHashes { - return id +func blockRuntimeIDFromNetwork(codec blocknetwork.Codec, id uint32) uint32 { + if runtimeID, ok := codec.ToRuntimeID(id); ok { + return runtimeID } - if fromHashes { - converted, ok := p.World().BlockRegistry().HashToRuntimeID(id) - if !ok { - return id - } - id = converted - } - if toHashes { - if converted, ok := p.World().BlockRegistry().RuntimeIDToHash(id); ok { - return converted - } + return id +} + +func blockRuntimeIDToNetwork(codec blocknetwork.Codec, id uint32) uint32 { + if networkID, ok := codec.FromRuntimeID(id); ok { + return networkID } return id } diff --git a/player/world_hash_test.go b/player/world_hash_test.go index 1c0cfdf3..9b267278 100644 --- a/player/world_hash_test.go +++ b/player/world_hash_test.go @@ -10,10 +10,16 @@ import ( "github.com/df-mc/dragonfly/server/block" dfworld "github.com/df-mc/dragonfly/server/world" "github.com/oomph-ac/oomph/world" + "github.com/oomph-ac/oomph/world/blocknetwork" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) +func setBlockNetworkModes(p *Player, client, backend blocknetwork.Mode) { + p.clientBlockNetwork = blocknetwork.NewCodec(world.BlockRegistry, client) + p.backendBlockNetwork = blocknetwork.NewCodec(world.BlockRegistry, backend) +} + func TestBlockRuntimeIDToNetworkUsesHashMode(t *testing.T) { world.FinalizeBlockRegistry() stoneRID := dfworld.BlockRuntimeID(block.Stone{}) @@ -22,7 +28,7 @@ func TestBlockRuntimeIDToNetworkUsesHashMode(t *testing.T) { t.Fatal("stone has no network hash") } p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - p.clientUsesBlockNetworkIDHashes = true + p.clientBlockNetwork = blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.Hashes) if got := p.BlockRuntimeIDToNetwork(stoneRID); got != stoneHash { t.Fatalf("network block ID = %d, want hash %d", got, stoneHash) @@ -61,7 +67,7 @@ func TestConvertToStackAcceptsSignedBlockNetworkHash(t *testing.T) { t.Fatalf("stone network hash = %d, want a signed-negative hash", stoneHash) } p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - p.GameDat.UseBlockNetworkIDHashes = true + p.backendBlockNetwork = blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.Hashes) stack := p.ConvertToStack(protocol.ItemStack{BlockRuntimeID: int32(stoneHash), Count: 1}) if _, ok := stack.Item().(block.Stone); !ok { @@ -77,8 +83,7 @@ func TestBlockRuntimeIDTranslationSeparatesClientAndBackendModes(t *testing.T) { t.Fatal("stone has no network hash") } p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - p.clientUsesBlockNetworkIDHashes = true - p.GameDat.UseBlockNetworkIDHashes = false + setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) if got := p.BlockRuntimeIDFromClient(stoneHash); got != stoneRID { t.Fatalf("client hash translated to %d, want runtime ID %d", got, stoneRID) @@ -96,8 +101,7 @@ func TestRewriteClientBlockNetworkIDsCoversAuthInputInteraction(t *testing.T) { t.Fatal("stone has no network hash") } p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - p.clientUsesBlockNetworkIDHashes = true - p.GameDat.UseBlockNetworkIDHashes = false + setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) inputData := protocol.NewBitset(200) inputData.Set(packet.InputFlagPerformItemInteraction) pk := &packet.PlayerAuthInput{ @@ -127,8 +131,7 @@ func TestRewriteClientBlockNetworkIDsCoversAuthInputCraftResults(t *testing.T) { t.Fatal("stone has no network hash") } p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - p.clientUsesBlockNetworkIDHashes = true - p.GameDat.UseBlockNetworkIDHashes = false + setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) inputData := protocol.NewBitset(200) inputData.Set(packet.InputFlagPerformItemStackRequest) action := &protocol.CraftResultsDeprecatedStackRequestAction{ @@ -157,8 +160,7 @@ func TestRewriteClientBlockNetworkIDsCoversStandaloneCraftResults(t *testing.T) t.Fatal("stone has no network hash") } p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - p.clientUsesBlockNetworkIDHashes = true - p.GameDat.UseBlockNetworkIDHashes = false + setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) action := &protocol.CraftResultsDeprecatedStackRequestAction{ ResultItems: []protocol.ItemStack{{BlockRuntimeID: int32(stoneHash)}}, } @@ -182,8 +184,7 @@ func TestRewriteServerInventoryStackUsesRetainedClientMode(t *testing.T) { t.Fatal("stone has no network hash") } p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - p.clientUsesBlockNetworkIDHashes = false - p.GameDat.UseBlockNetworkIDHashes = true + setBlockNetworkModes(p, blocknetwork.RuntimeIDs, blocknetwork.Hashes) pk := &packet.InventorySlot{ NewItem: protocol.ItemInstance{Stack: protocol.ItemStack{BlockRuntimeID: int32(stoneHash)}}, StorageItem: protocol.Option(protocol.ItemInstance{Stack: protocol.ItemStack{BlockRuntimeID: int32(stoneHash)}}), @@ -223,8 +224,7 @@ func TestRewriteServerRecipesPreservesBackendOutputs(t *testing.T) { t.Fatal("stone has no network hash") } p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - p.clientUsesBlockNetworkIDHashes = true - p.GameDat.UseBlockNetworkIDHashes = false + setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) backendRecipe := &protocol.ShapedChemistryRecipe{ShapedRecipe: protocol.ShapedRecipe{ Output: []protocol.ItemStack{{BlockRuntimeID: int32(stoneRID)}}, }} diff --git a/world/cache.go b/world/cache.go index 999dbf6a..8a8221a4 100644 --- a/world/cache.go +++ b/world/cache.go @@ -8,6 +8,7 @@ import ( "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/chunk" + "github.com/oomph-ac/oomph/world/blocknetwork" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "github.com/zeebo/xxh3" @@ -22,8 +23,8 @@ var ( ) type blockCacheKey struct { - hash xxh3.Uint128 - networkHashes bool + hash xxh3.Uint128 + mode blocknetwork.Mode } func unsubC(key blockCacheKey) { @@ -52,11 +53,11 @@ func unsubSC(key blockCacheKey) { } } -func CacheSubChunk(payload *bytes.Buffer, c *chunk.Chunk, pos protocol.ChunkPos, networkHashes bool) (*CachedSubChunk, error) { +func CacheSubChunk(payload *bytes.Buffer, c *chunk.Chunk, pos protocol.ChunkPos, codec blocknetwork.Codec) (*CachedSubChunk, error) { scMu.Lock() defer scMu.Unlock() - key := blockCacheKey{hash: xxh3.Hash128(payload.Bytes()), networkHashes: networkHashes} + key := blockCacheKey{hash: xxh3.Hash128(payload.Bytes()), mode: codec.Mode()} if sc, ok := subChunkCache[key]; ok { sc.subs.Add(1) //fmt.Println("returning cached subchunk", hash) @@ -68,7 +69,7 @@ func CacheSubChunk(payload *bytes.Buffer, c *chunk.Chunk, pos protocol.ChunkPos, if err != nil { return nil, err } - if networkHashes { + if codec.Mode() == blocknetwork.Hashes { decodedSC.ConvertBlockNetworkHashesToRuntimeIDs(BlockRegistry) } @@ -80,15 +81,15 @@ func CacheSubChunk(payload *bytes.Buffer, c *chunk.Chunk, pos protocol.ChunkPos, return cachedSC, nil } -func CacheChunk(input *packet.LevelChunk, networkHashes bool) (ChunkInfo, error) { +func CacheChunk(input *packet.LevelChunk, codec blocknetwork.Codec) (ChunkInfo, error) { cMu.Lock() defer cMu.Unlock() - key := blockCacheKey{hash: xxh3.Hash128(input.RawPayload), networkHashes: networkHashes} + key := blockCacheKey{hash: xxh3.Hash128(input.RawPayload), mode: codec.Mode()} if c, ok := chunkCache[key]; ok { c.subs.Add(1) //fmt.Println("returning cached chunk", key.hash) - return ChunkInfo{Hash: key.hash, networkHashes: networkHashes, Chunk: c.chunk, Cached: true}, nil + return ChunkInfo{Hash: key.hash, blockNetworkMode: codec.Mode(), Chunk: c.chunk, Cached: true}, nil } dimension, ok := world.DimensionByID(int(input.Dimension)) @@ -105,7 +106,7 @@ func CacheChunk(input *packet.LevelChunk, networkHashes bool) (ChunkInfo, error) if err != nil { return ChunkInfo{}, err } - if networkHashes { + if codec.Mode() == blocknetwork.Hashes { decodedChunk.ConvertBlockNetworkHashesToRuntimeIDs() } decodedChunk.Compact() @@ -113,12 +114,12 @@ func CacheChunk(input *packet.LevelChunk, networkHashes bool) (ChunkInfo, error) cachedChunk := &CachedChunk{hash: key.hash, chunk: decodedChunk} cachedChunk.subs.Add(1) chunkCache[key] = cachedChunk - return ChunkInfo{Hash: key.hash, networkHashes: networkHashes, Chunk: cachedChunk.chunk, Cached: true}, nil + return ChunkInfo{Hash: key.hash, blockNetworkMode: codec.Mode(), Chunk: cachedChunk.chunk, Cached: true}, nil } // ReencodeLevelChunk converts the block palettes in input between network ID representations while preserving trailing // block entity data. -func ReencodeLevelChunk(input *packet.LevelChunk, sourceHashes, targetHashes bool) error { +func ReencodeLevelChunk(input *packet.LevelChunk, source, target blocknetwork.Codec) error { dimension, ok := world.DimensionByID(int(input.Dimension)) if !ok { return fmt.Errorf("unknown dimension %v", input.Dimension) @@ -128,11 +129,11 @@ func ReencodeLevelChunk(input *packet.LevelChunk, sourceHashes, targetHashes boo if err != nil { return err } - if sourceHashes { + if source.Mode() == blocknetwork.Hashes { decoded.ConvertBlockNetworkHashesToRuntimeIDs() } var data chunk.SerialisedData - if targetHashes { + if target.Mode() == blocknetwork.Hashes { data = chunk.EncodeWithBlockNetworkHashes(decoded) } else { data = chunk.Encode(decoded, chunk.NetworkEncoding) @@ -150,7 +151,7 @@ func ReencodeLevelChunk(input *packet.LevelChunk, sourceHashes, targetHashes boo // ReencodeSubChunk converts one successful SubChunk entry between network ID representations while preserving trailing // block entity data. -func ReencodeSubChunk(payload []byte, dimension world.Dimension, sourceHashes, targetHashes bool) ([]byte, error) { +func ReencodeSubChunk(payload []byte, dimension world.Dimension, source, target blocknetwork.Codec) ([]byte, error) { buf := bytes.NewBuffer(payload) decodedChunk := chunk.New(BlockRegistry, dimension.Range()) var index byte @@ -158,7 +159,7 @@ func ReencodeSubChunk(payload []byte, dimension world.Dimension, sourceHashes, t if err != nil { return nil, err } - if sourceHashes { + if source.Mode() == blocknetwork.Hashes { decoded.ConvertBlockNetworkHashesToRuntimeIDs(BlockRegistry) } if int(index) >= len(decodedChunk.Sub()) { @@ -166,7 +167,7 @@ func ReencodeSubChunk(payload []byte, dimension world.Dimension, sourceHashes, t } decodedChunk.Sub()[index] = decoded var encoded []byte - if targetHashes { + if target.Mode() == blocknetwork.Hashes { encoded = chunk.EncodeSubChunkWithBlockNetworkHashes(decodedChunk, int(index)) } else { encoded = chunk.EncodeSubChunk(decodedChunk, chunk.NetworkEncoding, int(index)) diff --git a/world/world.go b/world/world.go index 97c39f5b..d208b7e6 100644 --- a/world/world.go +++ b/world/world.go @@ -7,6 +7,7 @@ import ( "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/chunk" "github.com/ethaniccc/float32-cube/cube" + "github.com/oomph-ac/oomph/world/blocknetwork" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/zeebo/xxh3" @@ -16,10 +17,10 @@ import ( ) type ChunkInfo struct { - Cached bool - Hash xxh3.Uint128 - networkHashes bool - Chunk *chunk.Chunk + Cached bool + Hash xxh3.Uint128 + blockNetworkMode blocknetwork.Mode + Chunk *chunk.Chunk } type World struct { @@ -67,11 +68,11 @@ func (w *World) AddChunk(chunkPos protocol.ChunkPos, c ChunkInfo) { } // AddSubChunk adds a subchunk to the world. -func (w *World) AddSubChunk(chunkPos protocol.ChunkPos, hash xxh3.Uint128, networkHashes bool) { +func (w *World) AddSubChunk(chunkPos protocol.ChunkPos, hash xxh3.Uint128, codec blocknetwork.Codec) { if _, ok := w.subChunks[chunkPos]; !ok { w.subChunks[chunkPos] = make([]blockCacheKey, 0, 16) } - w.subChunks[chunkPos] = append(w.subChunks[chunkPos], blockCacheKey{hash: hash, networkHashes: networkHashes}) + w.subChunks[chunkPos] = append(w.subChunks[chunkPos], blockCacheKey{hash: hash, mode: codec.Mode()}) } // Chunk returns a cached chunk at the position passed. The mutex is @@ -169,7 +170,7 @@ func (w *World) PurgeChunks() { func (w *World) removeChunk(info ChunkInfo, chunkPos protocol.ChunkPos) { if info.Cached { - unsubC(blockCacheKey{hash: info.Hash, networkHashes: info.networkHashes}) + unsubC(blockCacheKey{hash: info.Hash, mode: info.blockNetworkMode}) } if subChunks, ok := w.subChunks[chunkPos]; ok { for _, subChunkKey := range subChunks { From 972f5b8b9d5a6e658c6f59c9b72846d2cb3c5ec7 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 13 Jul 2026 20:38:56 -0400 Subject: [PATCH 06/16] fix: translate block IDs in events and entities --- player/block_network_packet.go | 62 +++++++++++++++++++++++++ player/packet.go | 19 +++++++- player/world_hash_test.go | 83 ++++++++++++++++++++++++++++++++-- 3 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 player/block_network_packet.go diff --git a/player/block_network_packet.go b/player/block_network_packet.go new file mode 100644 index 00000000..04562154 --- /dev/null +++ b/player/block_network_packet.go @@ -0,0 +1,62 @@ +package player + +import ( + "github.com/oomph-ac/oomph/world/blocknetwork" + "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +const fallingBlockEntityType = "minecraft:falling_block" + +func rewriteLevelEventBlockNetworkID(pk *packet.LevelEvent, translator blocknetwork.Translator) bool { + switch pk.EventType { + case packet.LevelEventParticlesDestroyBlock, packet.LevelEventParticlesDestroyBlockNoSound: + pk.EventData = int32(translator.Translate(uint32(pk.EventData))) + return true + default: + return false + } +} + +func rewriteLevelSoundBlockNetworkID(pk *packet.LevelSoundEvent, translator blocknetwork.Translator) bool { + if !levelSoundUsesBlockNetworkID(pk.SoundType) { + return false + } + pk.ExtraData = int32(translator.Translate(uint32(pk.ExtraData))) + return true +} + +func levelSoundUsesBlockNetworkID(soundType string) bool { + switch soundType { + case packet.SoundEventItemUseOn, + packet.SoundEventHit, + packet.SoundEventStep, + packet.SoundEventBreak, + packet.SoundEventPlace, + packet.SoundEventLand, + packet.SoundEventPressurePlateClickOff, + packet.SoundEventPressurePlateClickOn, + packet.SoundEventDoorOpen, + packet.SoundEventDoorClose, + packet.SoundEventTrapdoorOpen, + packet.SoundEventTrapdoorClose, + packet.SoundEventFenceGateOpen, + packet.SoundEventFenceGateClose: + return true + default: + return false + } +} + +func rewriteFallingBlockMetadata(metadata map[uint32]any, translator blocknetwork.Translator) (map[uint32]any, bool) { + value, ok := metadata[protocol.EntityDataKeyVariant].(int32) + if !ok { + return metadata, false + } + clientMetadata := make(map[uint32]any, len(metadata)) + for key, value := range metadata { + clientMetadata[key] = value + } + clientMetadata[protocol.EntityDataKeyVariant] = int32(translator.Translate(uint32(value))) + return clientMetadata, true +} diff --git a/player/packet.go b/player/packet.go index 92f9ac50..60e8d27b 100644 --- a/player/packet.go +++ b/player/packet.go @@ -44,7 +44,9 @@ var ServerDecode = []uint32{ packet.IDInventoryContent, packet.IDInventoryTransaction, packet.IDItemStackResponse, + packet.IDLevelEvent, packet.IDLevelChunk, + packet.IDLevelSoundEvent, packet.IDMobEffect, packet.IDMobEquipment, packet.IDMobArmourEquipment, @@ -340,6 +342,8 @@ func (p *Player) rewriteClientBlockNetworkIDs(pk packet.Packet) bool { case *packet.MobEquipment: rewriteStack(&pk.NewItem.Stack) return true + case *packet.LevelSoundEvent: + return rewriteLevelSoundBlockNetworkID(pk, p.ClientToBackendBlockNetwork()) } return false } @@ -608,12 +612,12 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { p.CreativeItems[item.CreativeItemNetworkID] = item } } - if p.BackendToClientBlockNetwork().Required() && p.rewriteServerItemBlockNetworkIDs(pk) { + if p.BackendToClientBlockNetwork().Required() && p.rewriteServerBlockNetworkIDs(pk) { ctx.SetModified() } } -func (p *Player) rewriteServerItemBlockNetworkIDs(pk packet.Packet) bool { +func (p *Player) rewriteServerBlockNetworkIDs(pk packet.Packet) bool { rewriteStack := func(stack *protocol.ItemStack) { if stack.BlockRuntimeID != 0 { stack.BlockRuntimeID = int32(p.BlockRuntimeIDFromBackendToClient(uint32(stack.BlockRuntimeID))) @@ -648,9 +652,20 @@ func (p *Player) rewriteServerItemBlockNetworkIDs(pk packet.Packet) bool { case *packet.AddPlayer: rewriteStack(&pk.HeldItem.Stack) return true + case *packet.AddActor: + if pk.EntityType != fallingBlockEntityType { + return false + } + metadata, modified := rewriteFallingBlockMetadata(pk.EntityMetadata, p.BackendToClientBlockNetwork()) + pk.EntityMetadata = metadata + return modified case *packet.AddItemActor: rewriteStack(&pk.Item.Stack) return true + case *packet.LevelEvent: + return rewriteLevelEventBlockNetworkID(pk, p.BackendToClientBlockNetwork()) + case *packet.LevelSoundEvent: + return rewriteLevelSoundBlockNetworkID(pk, p.BackendToClientBlockNetwork()) case *packet.CreativeContent: for i := range pk.Groups { rewriteStack(&pk.Groups[i].Icon) diff --git a/player/world_hash_test.go b/player/world_hash_test.go index 9b267278..b0428499 100644 --- a/player/world_hash_test.go +++ b/player/world_hash_test.go @@ -176,6 +176,25 @@ func TestRewriteClientBlockNetworkIDsCoversStandaloneCraftResults(t *testing.T) } } +func TestRewriteClientBlockNetworkIDsCoversBlockSound(t *testing.T) { + world.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) + setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) + pk := &packet.LevelSoundEvent{SoundType: packet.SoundEventPlace, ExtraData: int32(stoneHash)} + + if !p.rewriteClientBlockNetworkIDs(pk) { + t.Fatal("block sound was not rewritten") + } + if got := uint32(pk.ExtraData); got != stoneRID { + t.Fatalf("block sound ID = %d, want runtime ID %d", got, stoneRID) + } +} + func TestRewriteServerInventoryStackUsesRetainedClientMode(t *testing.T) { world.FinalizeBlockRegistry() stoneRID := dfworld.BlockRuntimeID(block.Stone{}) @@ -190,7 +209,7 @@ func TestRewriteServerInventoryStackUsesRetainedClientMode(t *testing.T) { StorageItem: protocol.Option(protocol.ItemInstance{Stack: protocol.ItemStack{BlockRuntimeID: int32(stoneHash)}}), } - if !p.rewriteServerItemBlockNetworkIDs(pk) { + if !p.rewriteServerBlockNetworkIDs(pk) { t.Fatal("inventory slot was not rewritten") } if got := uint32(pk.NewItem.Stack.BlockRuntimeID); got != stoneRID { @@ -205,7 +224,7 @@ func TestRewriteServerInventoryStackUsesRetainedClientMode(t *testing.T) { } original := []protocol.ItemInstance{{Stack: protocol.ItemStack{BlockRuntimeID: int32(stoneHash)}}} content := &packet.InventoryContent{Content: original} - if !p.rewriteServerItemBlockNetworkIDs(content) { + if !p.rewriteServerBlockNetworkIDs(content) { t.Fatal("inventory content was not rewritten") } if got := uint32(content.Content[0].Stack.BlockRuntimeID); got != stoneRID { @@ -216,6 +235,64 @@ func TestRewriteServerInventoryStackUsesRetainedClientMode(t *testing.T) { } } +func TestRewriteServerBlockNetworkIDsCoversBlockEvents(t *testing.T) { + world.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) + setBlockNetworkModes(p, blocknetwork.RuntimeIDs, blocknetwork.Hashes) + + event := &packet.LevelEvent{EventType: packet.LevelEventParticlesDestroyBlock, EventData: int32(stoneHash)} + if !p.rewriteServerBlockNetworkIDs(event) { + t.Fatal("block event was not rewritten") + } + if got := uint32(event.EventData); got != stoneRID { + t.Fatalf("block event ID = %d, want runtime ID %d", got, stoneRID) + } + + sound := &packet.LevelSoundEvent{SoundType: packet.SoundEventHit, ExtraData: int32(stoneHash)} + if !p.rewriteServerBlockNetworkIDs(sound) { + t.Fatal("block sound was not rewritten") + } + if got := uint32(sound.ExtraData); got != stoneRID { + t.Fatalf("block sound ID = %d, want runtime ID %d", got, stoneRID) + } +} + +func TestRewriteServerBlockNetworkIDsCoversFallingBlockMetadata(t *testing.T) { + world.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) + setBlockNetworkModes(p, blocknetwork.RuntimeIDs, blocknetwork.Hashes) + backendMetadata := map[uint32]any{protocol.EntityDataKeyVariant: int32(stoneHash)} + pk := &packet.AddActor{EntityType: "minecraft:falling_block", EntityMetadata: backendMetadata} + + if !p.rewriteServerBlockNetworkIDs(pk) { + t.Fatal("falling-block metadata was not rewritten") + } + if got := uint32(pk.EntityMetadata[protocol.EntityDataKeyVariant].(int32)); got != stoneRID { + t.Fatalf("falling-block metadata ID = %d, want runtime ID %d", got, stoneRID) + } + if got := uint32(backendMetadata[protocol.EntityDataKeyVariant].(int32)); got != stoneHash { + t.Fatalf("retained backend metadata ID = %d, want hash %d", got, stoneHash) + } + + unrelated := &packet.AddActor{ + EntityType: "minecraft:tropicalfish", + EntityMetadata: map[uint32]any{protocol.EntityDataKeyVariant: int32(stoneHash)}, + } + if p.rewriteServerBlockNetworkIDs(unrelated) { + t.Fatal("unrelated entity variant was rewritten") + } +} + func TestRewriteServerRecipesPreservesBackendOutputs(t *testing.T) { world.FinalizeBlockRegistry() stoneRID := dfworld.BlockRuntimeID(block.Stone{}) @@ -230,7 +307,7 @@ func TestRewriteServerRecipesPreservesBackendOutputs(t *testing.T) { }} pk := &packet.CraftingData{Recipes: []protocol.Recipe{backendRecipe}} - if !p.rewriteServerItemBlockNetworkIDs(pk) { + if !p.rewriteServerBlockNetworkIDs(pk) { t.Fatal("crafting data was not rewritten") } clientRecipe := pk.Recipes[0].(*protocol.ShapedChemistryRecipe) From 5cc3a96b22087882483dce05bc704b467afe6abd Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 13 Jul 2026 20:44:23 -0400 Subject: [PATCH 07/16] fix: validate runtime IDs through registry lookups --- world/blocknetwork/codec.go | 6 ++++-- world/blocknetwork/codec_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/world/blocknetwork/codec.go b/world/blocknetwork/codec.go index a0ad171a..07427a6c 100644 --- a/world/blocknetwork/codec.go +++ b/world/blocknetwork/codec.go @@ -26,7 +26,8 @@ func (c Codec) ToRuntimeID(networkID uint32) (uint32, bool) { if c.mode == Hashes { return c.registry.HashToRuntimeID(networkID) } - return networkID, networkID < uint32(c.registry.BlockCount()) + _, _, ok := c.registry.RuntimeIDToState(networkID) + return networkID, ok } // FromRuntimeID converts a canonical block-registry runtime ID to the codec's network representation. @@ -34,7 +35,8 @@ func (c Codec) FromRuntimeID(runtimeID uint32) (uint32, bool) { if c.mode == Hashes { return c.registry.RuntimeIDToHash(runtimeID) } - return runtimeID, runtimeID < uint32(c.registry.BlockCount()) + _, _, ok := c.registry.RuntimeIDToState(runtimeID) + return runtimeID, ok } // Translator converts block IDs from one endpoint codec to another. diff --git a/world/blocknetwork/codec_test.go b/world/blocknetwork/codec_test.go index 088b847e..bf6b0bde 100644 --- a/world/blocknetwork/codec_test.go +++ b/world/blocknetwork/codec_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/world/chunk" "github.com/oomph-ac/oomph/world" "github.com/oomph-ac/oomph/world/blocknetwork" ) @@ -80,6 +81,21 @@ func TestCodecRejectsUnknownIDs(t *testing.T) { } } +func TestRuntimeCodecUsesRegistryLookupInsteadOfAssumingDenseIDs(t *testing.T) { + t.Parallel() + + runtimeID := world.BlockRegistry.BlockRuntimeID(block.Stone{}) + registry := sparseBlockRegistry{BlockRegistry: world.BlockRegistry, missing: runtimeID} + codec := blocknetwork.NewCodec(registry, blocknetwork.RuntimeIDs) + + if _, ok := codec.ToRuntimeID(runtimeID); ok { + t.Fatalf("runtime codec accepted missing runtime ID %d", runtimeID) + } + if _, ok := codec.FromRuntimeID(runtimeID); ok { + t.Fatalf("runtime codec encoded missing runtime ID %d", runtimeID) + } +} + func TestTranslatorConvertsBetweenModes(t *testing.T) { t.Parallel() @@ -171,3 +187,15 @@ func highBitNetworkHash(t *testing.T) (uint32, uint32) { t.Fatal("block registry contains no high-bit network hash") return 0, 0 } + +type sparseBlockRegistry struct { + chunk.BlockRegistry + missing uint32 +} + +func (r sparseBlockRegistry) RuntimeIDToState(runtimeID uint32) (string, map[string]any, bool) { + if runtimeID == r.missing { + return "", nil, false + } + return r.BlockRegistry.RuntimeIDToState(runtimeID) +} From b1ecd327c630fa4a49790d8b9d6e738e53ec5a31 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 13 Jul 2026 20:52:26 -0400 Subject: [PATCH 08/16] fix: queue canonical local block updates --- player/component/world_hash_test.go | 25 +++++++++++++++++++++++++ player/world.go | 5 +++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/player/component/world_hash_test.go b/player/component/world_hash_test.go index b667a76e..b177fcf7 100644 --- a/player/component/world_hash_test.go +++ b/player/component/world_hash_test.go @@ -129,6 +129,31 @@ func TestClientBlockHashModeSurvivesBackendTransfer(t *testing.T) { } } +func TestSyncBlockQueuesCanonicalRuntimeIDAcrossModes(t *testing.T) { + oomphworld.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := oomphworld.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{ + CurrentTime: time.Now(), + IsReplay: true, + }, nil) + Register(p) + p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: true}}) + p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: false}}) + position := cube.Pos{4, 5, 6} + p.World().SetBlock(position, block.Stone{}, nil) + + p.SyncBlock(position) + + updater := p.WorldUpdater().(*WorldUpdaterComponent) + if got := updater.batchedBlockUpdates.Blocks()[position]; got != stoneRID { + t.Fatalf("pending block runtime ID = %d, want canonical runtime ID %d (client hash %d)", got, stoneRID, stoneHash) + } +} + func TestLevelChunkIsTranslatedToRetainedClientHashModeAfterTransfer(t *testing.T) { oomphworld.FinalizeBlockRegistry() stoneRID := dfworld.BlockRuntimeID(block.Stone{}) diff --git a/player/world.go b/player/world.go index 408fe239..8d926027 100644 --- a/player/world.go +++ b/player/world.go @@ -141,17 +141,18 @@ func (p *Player) SyncBlock(pos df_cube.Pos) { if p.WorldUpdater().HasPendingUpdate(pos) { return } + blockRuntimeID := world.BlockRuntimeID(p.World().Block(pos)) pk := &packet.UpdateBlock{ Position: protocol.BlockPos{ int32(pos[0]), int32(pos[1]), int32(pos[2]), }, - NewBlockRuntimeID: p.BlockRuntimeIDToNetwork(world.BlockRuntimeID(p.World().Block(pos))), + NewBlockRuntimeID: p.BlockRuntimeIDToNetwork(blockRuntimeID), Flags: packet.BlockUpdateNetwork, Layer: 0, // TODO: Implement and account for multi-layer blocks. } - p.WorldUpdater().HandleUpdateBlock(pk) + p.WorldUpdater().AddPendingUpdate(pos, blockRuntimeID) _ = p.SendPacketToClient(pk) } From 4b59091b06f15d7df83a0b799352fb5e5a12a780 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 13 Jul 2026 20:57:22 -0400 Subject: [PATCH 09/16] fix: translate actor block metadata --- player/block_network_packet.go | 32 ++++++++++++++++++++------- player/packet.go | 11 ++++++---- player/world_hash_test.go | 40 ++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/player/block_network_packet.go b/player/block_network_packet.go index 04562154..565f9a08 100644 --- a/player/block_network_packet.go +++ b/player/block_network_packet.go @@ -48,15 +48,31 @@ func levelSoundUsesBlockNetworkID(soundType string) bool { } } -func rewriteFallingBlockMetadata(metadata map[uint32]any, translator blocknetwork.Translator) (map[uint32]any, bool) { - value, ok := metadata[protocol.EntityDataKeyVariant].(int32) - if !ok { - return metadata, false +func rewriteActorBlockMetadata(metadata map[uint32]any, translator blocknetwork.Translator, fallingBlock bool) (map[uint32]any, bool) { + keys := []uint32{protocol.EntityDataKeyDisplayTileRuntimeID, protocol.EntityDataKeyCarryBlockRuntimeID} + if fallingBlock { + keys = append(keys, protocol.EntityDataKeyVariant) + } + var clientMetadata map[uint32]any + for _, key := range keys { + value, ok := metadata[key].(int32) + if !ok { + continue + } + translated := int32(translator.Translate(uint32(value))) + if translated == value { + continue + } + if clientMetadata == nil { + clientMetadata = make(map[uint32]any, len(metadata)) + for key, value := range metadata { + clientMetadata[key] = value + } + } + clientMetadata[key] = translated } - clientMetadata := make(map[uint32]any, len(metadata)) - for key, value := range metadata { - clientMetadata[key] = value + if clientMetadata == nil { + return metadata, false } - clientMetadata[protocol.EntityDataKeyVariant] = int32(translator.Translate(uint32(value))) return clientMetadata, true } diff --git a/player/packet.go b/player/packet.go index 60e8d27b..d78f2090 100644 --- a/player/packet.go +++ b/player/packet.go @@ -651,12 +651,15 @@ func (p *Player) rewriteServerBlockNetworkIDs(pk packet.Packet) bool { return true case *packet.AddPlayer: rewriteStack(&pk.HeldItem.Stack) + metadata, _ := rewriteActorBlockMetadata(pk.EntityMetadata, p.BackendToClientBlockNetwork(), false) + pk.EntityMetadata = metadata return true case *packet.AddActor: - if pk.EntityType != fallingBlockEntityType { - return false - } - metadata, modified := rewriteFallingBlockMetadata(pk.EntityMetadata, p.BackendToClientBlockNetwork()) + metadata, modified := rewriteActorBlockMetadata(pk.EntityMetadata, p.BackendToClientBlockNetwork(), pk.EntityType == fallingBlockEntityType) + pk.EntityMetadata = metadata + return modified + case *packet.SetActorData: + metadata, modified := rewriteActorBlockMetadata(pk.EntityMetadata, p.BackendToClientBlockNetwork(), false) pk.EntityMetadata = metadata return modified case *packet.AddItemActor: diff --git a/player/world_hash_test.go b/player/world_hash_test.go index b0428499..e05fa943 100644 --- a/player/world_hash_test.go +++ b/player/world_hash_test.go @@ -293,6 +293,46 @@ func TestRewriteServerBlockNetworkIDsCoversFallingBlockMetadata(t *testing.T) { } } +func TestRewriteServerBlockNetworkIDsCoversBlockValuedActorMetadata(t *testing.T) { + world.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) + setBlockNetworkModes(p, blocknetwork.RuntimeIDs, blocknetwork.Hashes) + + spawnMetadata := map[uint32]any{protocol.EntityDataKeyCarryBlockRuntimeID: int32(stoneHash)} + spawn := &packet.AddActor{EntityType: "minecraft:enderman", EntityMetadata: spawnMetadata} + if !p.rewriteServerBlockNetworkIDs(spawn) { + t.Fatal("carried-block spawn metadata was not rewritten") + } + if got := uint32(spawn.EntityMetadata[protocol.EntityDataKeyCarryBlockRuntimeID].(int32)); got != stoneRID { + t.Fatalf("carried-block spawn metadata ID = %d, want runtime ID %d", got, stoneRID) + } + if got := uint32(spawnMetadata[protocol.EntityDataKeyCarryBlockRuntimeID].(int32)); got != stoneHash { + t.Fatalf("retained backend spawn metadata ID = %d, want hash %d", got, stoneHash) + } + + updateMetadata := map[uint32]any{ + protocol.EntityDataKeyCarryBlockRuntimeID: int32(stoneHash), + protocol.EntityDataKeyDisplayTileRuntimeID: int32(stoneHash), + } + update := &packet.SetActorData{EntityMetadata: updateMetadata} + if !p.rewriteServerBlockNetworkIDs(update) { + t.Fatal("block-valued actor metadata update was not rewritten") + } + for _, key := range []uint32{protocol.EntityDataKeyCarryBlockRuntimeID, protocol.EntityDataKeyDisplayTileRuntimeID} { + if got := uint32(update.EntityMetadata[key].(int32)); got != stoneRID { + t.Fatalf("actor metadata key %d ID = %d, want runtime ID %d", key, got, stoneRID) + } + if got := uint32(updateMetadata[key].(int32)); got != stoneHash { + t.Fatalf("retained backend metadata key %d ID = %d, want hash %d", key, got, stoneHash) + } + } +} + func TestRewriteServerRecipesPreservesBackendOutputs(t *testing.T) { world.FinalizeBlockRegistry() stoneRID := dfworld.BlockRuntimeID(block.Stone{}) From 0b7130c8bfbd100bdb0435c7a18786030afe8c71 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 13 Jul 2026 21:02:20 -0400 Subject: [PATCH 10/16] fix: preserve transfer-safe actor state --- player/component/combat.go | 2 +- player/packet.go | 23 ++++++++++++++++-- player/world_hash_test.go | 48 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/player/component/combat.go b/player/component/combat.go index 8cc5d989..067bab3f 100644 --- a/player/component/combat.go +++ b/player/component/combat.go @@ -517,7 +517,7 @@ func (c *AuthoritativeCombatComponent) checkForMispredictedEntity() bool { var newItem protocol.ItemInstance if c.mPlayer.LastEquipmentData != nil { - newItem = c.mPlayer.LastEquipmentData.NewItem + newItem = c.mPlayer.ClientItemForBackend(c.mPlayer.LastEquipmentData.NewItem) } c.attackInput = &packet.InventoryTransaction{ diff --git a/player/packet.go b/player/packet.go index d78f2090..8c91e7b8 100644 --- a/player/packet.go +++ b/player/packet.go @@ -262,7 +262,7 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { ctx.Cancel() } case *packet.MobEquipment: - p.LastEquipmentData = pk + p.retainClientEquipment(pk) if pk.WindowID == protocol.WindowIDInventory { p.inventory.SetHeldSlot(int32(pk.HotBarSlot)) } @@ -348,6 +348,19 @@ func (p *Player) rewriteClientBlockNetworkIDs(pk packet.Packet) bool { return false } +func (p *Player) retainClientEquipment(pk *packet.MobEquipment) { + retained := *pk + p.LastEquipmentData = &retained +} + +// ClientItemForBackend translates an item retained in the client's block network mode to the current backend mode. +func (p *Player) ClientItemForBackend(instance protocol.ItemInstance) protocol.ItemInstance { + if instance.Stack.BlockRuntimeID != 0 { + instance.Stack.BlockRuntimeID = int32(p.BlockRuntimeIDFromClientToBackend(uint32(instance.Stack.BlockRuntimeID))) + } + return instance +} + // splitCommandLine splits a command line into arguments, preserving quoted substrings // as single arguments. Supports both single (”) and double ("") quotes, and escaping // characters using backslashes within or outside quotes. @@ -659,7 +672,13 @@ func (p *Player) rewriteServerBlockNetworkIDs(pk packet.Packet) bool { pk.EntityMetadata = metadata return modified case *packet.SetActorData: - metadata, modified := rewriteActorBlockMetadata(pk.EntityMetadata, p.BackendToClientBlockNetwork(), false) + fallingBlock := false + if p.entTracker != nil { + if actor := p.entTracker.FindEntity(pk.EntityRuntimeID); actor != nil { + fallingBlock = actor.Type == fallingBlockEntityType + } + } + metadata, modified := rewriteActorBlockMetadata(pk.EntityMetadata, p.BackendToClientBlockNetwork(), fallingBlock) pk.EntityMetadata = metadata return modified case *packet.AddItemActor: diff --git a/player/world_hash_test.go b/player/world_hash_test.go index e05fa943..c63c15a4 100644 --- a/player/world_hash_test.go +++ b/player/world_hash_test.go @@ -9,6 +9,8 @@ import ( "github.com/df-mc/dragonfly/server/block" dfworld "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl32" + "github.com/oomph-ac/oomph/entity" "github.com/oomph-ac/oomph/world" "github.com/oomph-ac/oomph/world/blocknetwork" "github.com/sandertv/gophertunnel/minecraft/protocol" @@ -318,12 +320,14 @@ func TestRewriteServerBlockNetworkIDsCoversBlockValuedActorMetadata(t *testing.T updateMetadata := map[uint32]any{ protocol.EntityDataKeyCarryBlockRuntimeID: int32(stoneHash), protocol.EntityDataKeyDisplayTileRuntimeID: int32(stoneHash), + protocol.EntityDataKeyVariant: int32(stoneHash), } - update := &packet.SetActorData{EntityMetadata: updateMetadata} + p.entTracker = actorTypeTracker{entity: &entity.Entity{Type: fallingBlockEntityType}} + update := &packet.SetActorData{EntityRuntimeID: 42, EntityMetadata: updateMetadata} if !p.rewriteServerBlockNetworkIDs(update) { t.Fatal("block-valued actor metadata update was not rewritten") } - for _, key := range []uint32{protocol.EntityDataKeyCarryBlockRuntimeID, protocol.EntityDataKeyDisplayTileRuntimeID} { + for _, key := range []uint32{protocol.EntityDataKeyCarryBlockRuntimeID, protocol.EntityDataKeyDisplayTileRuntimeID, protocol.EntityDataKeyVariant} { if got := uint32(update.EntityMetadata[key].(int32)); got != stoneRID { t.Fatalf("actor metadata key %d ID = %d, want runtime ID %d", key, got, stoneRID) } @@ -333,6 +337,46 @@ func TestRewriteServerBlockNetworkIDsCoversBlockValuedActorMetadata(t *testing.T } } +func TestRetainedClientEquipmentIsIndependentOfBackendTranslation(t *testing.T) { + world.FinalizeBlockRegistry() + stoneRID := dfworld.BlockRuntimeID(block.Stone{}) + stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) + if !ok { + t.Fatal("stone has no network hash") + } + p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) + setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) + pk := &packet.MobEquipment{NewItem: protocol.ItemInstance{Stack: protocol.ItemStack{BlockRuntimeID: int32(stoneHash)}}} + + p.retainClientEquipment(pk) + p.rewriteClientBlockNetworkIDs(pk) + + if got := uint32(pk.NewItem.Stack.BlockRuntimeID); got != stoneRID { + t.Fatalf("backend equipment block ID = %d, want runtime ID %d", got, stoneRID) + } + if got := uint32(p.LastEquipmentData.NewItem.Stack.BlockRuntimeID); got != stoneHash { + t.Fatalf("retained client equipment block ID = %d, want hash %d", got, stoneHash) + } + generated := p.ClientItemForBackend(p.LastEquipmentData.NewItem) + if got := uint32(generated.Stack.BlockRuntimeID); got != stoneRID { + t.Fatalf("generated backend equipment block ID = %d, want runtime ID %d", got, stoneRID) + } +} + +type actorTypeTracker struct { + entity *entity.Entity +} + +func (actorTypeTracker) AddEntity(uint64, *entity.Entity) {} +func (actorTypeTracker) RemoveEntity(uint64) {} +func (t actorTypeTracker) FindEntity(uint64) *entity.Entity { return t.entity } +func (actorTypeTracker) All() map[uint64]*entity.Entity { return nil } +func (actorTypeTracker) MoveEntity(uint64, int64, mgl32.Vec3, bool) {} +func (actorTypeTracker) HandleMovePlayer(*packet.MovePlayer) {} +func (actorTypeTracker) HandleMoveActorAbsolute(*packet.MoveActorAbsolute) {} +func (actorTypeTracker) HandleSetActorData(*packet.SetActorData) {} +func (actorTypeTracker) Tick(int64) {} + func TestRewriteServerRecipesPreservesBackendOutputs(t *testing.T) { world.FinalizeBlockRegistry() stoneRID := dfworld.BlockRuntimeID(block.Stone{}) From c802374de49b699cdbb4f3818b4a62543b087b50 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 14 Jul 2026 00:34:46 -0400 Subject: [PATCH 11/16] refactor: simplify block network rewrites --- player/block_network_packet.go | 111 +++++++++++++++- player/component/world.go | 6 +- player/component/world_hash_test.go | 22 +++- player/items.go | 6 +- player/packet.go | 192 ++++++++++++---------------- player/world.go | 32 ++--- player/world_hash_test.go | 45 ++++++- 7 files changed, 265 insertions(+), 149 deletions(-) diff --git a/player/block_network_packet.go b/player/block_network_packet.go index 565f9a08..337e9e9b 100644 --- a/player/block_network_packet.go +++ b/player/block_network_packet.go @@ -8,11 +8,111 @@ import ( const fallingBlockEntityType = "minecraft:falling_block" +func translateBlockNetworkID(id uint32, translator blocknetwork.Translator) (uint32, bool) { + translated := translator.Translate(id) + return translated, translated != id +} + +func rewriteBlockNetworkID(id *uint32, translator blocknetwork.Translator) bool { + translated, modified := translateBlockNetworkID(*id, translator) + if modified { + *id = translated + } + return modified +} + +func rewriteStackBlockNetworkID(stack *protocol.ItemStack, translator blocknetwork.Translator) bool { + if stack.BlockRuntimeID == 0 { + return false + } + translated, modified := translateBlockNetworkID(uint32(stack.BlockRuntimeID), translator) + if modified { + stack.BlockRuntimeID = int32(translated) + } + return modified +} + +func rewriteStacksBlockNetworkIDs(stacks []protocol.ItemStack, translator blocknetwork.Translator) ([]protocol.ItemStack, bool) { + var rewritten []protocol.ItemStack + for i := range stacks { + stack := stacks[i] + if !rewriteStackBlockNetworkID(&stack, translator) { + continue + } + if rewritten == nil { + rewritten = append([]protocol.ItemStack(nil), stacks...) + } + rewritten[i] = stack + } + return rewritten, rewritten != nil +} + +func rewriteShapedRecipeBlockNetworkIDs(recipe protocol.ShapedRecipe, translator blocknetwork.Translator) (protocol.ShapedRecipe, bool) { + output, modified := rewriteStacksBlockNetworkIDs(recipe.Output, translator) + if modified { + recipe.Output = output + } + return recipe, modified +} + +func rewriteShapelessRecipeBlockNetworkIDs(recipe protocol.ShapelessRecipe, translator blocknetwork.Translator) (protocol.ShapelessRecipe, bool) { + output, modified := rewriteStacksBlockNetworkIDs(recipe.Output, translator) + if modified { + recipe.Output = output + } + return recipe, modified +} + +func rewriteRecipeBlockNetworkIDs(recipe protocol.Recipe, translator blocknetwork.Translator) (protocol.Recipe, bool) { + switch recipe := recipe.(type) { + case *protocol.ShapedRecipe: + clientRecipe, modified := rewriteShapedRecipeBlockNetworkIDs(*recipe, translator) + if modified { + return &clientRecipe, true + } + case *protocol.ShulkerBoxRecipe: + output, modified := rewriteShapelessRecipeBlockNetworkIDs(recipe.ShapelessRecipe, translator) + if modified { + clientRecipe := *recipe + clientRecipe.ShapelessRecipe = output + return &clientRecipe, true + } + case *protocol.ShapelessChemistryRecipe: + output, modified := rewriteShapelessRecipeBlockNetworkIDs(recipe.ShapelessRecipe, translator) + if modified { + clientRecipe := *recipe + clientRecipe.ShapelessRecipe = output + return &clientRecipe, true + } + case *protocol.ShapedChemistryRecipe: + output, modified := rewriteShapedRecipeBlockNetworkIDs(recipe.ShapedRecipe, translator) + if modified { + clientRecipe := *recipe + clientRecipe.ShapedRecipe = output + return &clientRecipe, true + } + case *protocol.ShapelessRecipe: + clientRecipe, modified := rewriteShapelessRecipeBlockNetworkIDs(*recipe, translator) + if modified { + return &clientRecipe, true + } + case *protocol.SmithingTransformRecipe: + clientRecipe := *recipe + if rewriteStackBlockNetworkID(&clientRecipe.Result, translator) { + return &clientRecipe, true + } + } + return recipe, false +} + func rewriteLevelEventBlockNetworkID(pk *packet.LevelEvent, translator blocknetwork.Translator) bool { switch pk.EventType { case packet.LevelEventParticlesDestroyBlock, packet.LevelEventParticlesDestroyBlockNoSound: - pk.EventData = int32(translator.Translate(uint32(pk.EventData))) - return true + translated, modified := translateBlockNetworkID(uint32(pk.EventData), translator) + if modified { + pk.EventData = int32(translated) + } + return modified default: return false } @@ -22,8 +122,11 @@ func rewriteLevelSoundBlockNetworkID(pk *packet.LevelSoundEvent, translator bloc if !levelSoundUsesBlockNetworkID(pk.SoundType) { return false } - pk.ExtraData = int32(translator.Translate(uint32(pk.ExtraData))) - return true + translated, modified := translateBlockNetworkID(uint32(pk.ExtraData), translator) + if modified { + pk.ExtraData = int32(translated) + } + return modified } func levelSoundUsesBlockNetworkID(soundType string) bool { diff --git a/player/component/world.go b/player/component/world.go index 28a5fdba..51f5781d 100644 --- a/player/component/world.go +++ b/player/component/world.go @@ -77,7 +77,7 @@ func (c *WorldUpdaterComponent) HandleUpdateBlock(pk *packet.UpdateBlock) { c.mPlayer.Log().Debug("unsupported layer update block", "layer", pk.Layer, "block", pk.NewBlockRuntimeID, "pos", pos) return } - c.AddPendingUpdate(pos, c.mPlayer.BlockRuntimeIDFromNetwork(pk.NewBlockRuntimeID)) + c.AddPendingUpdate(pos, c.mPlayer.BlockRuntimeIDFromBackend(pk.NewBlockRuntimeID)) } // HandleUpdateSubChunkBlocks handles an UpdateSubChunkBlocks packet from the server. @@ -86,10 +86,10 @@ func (c *WorldUpdaterComponent) HandleUpdateSubChunkBlocks(pk *packet.UpdateSubC c.mPlayer.ACKs().Add(acknowledgement.NewPlayerInitalizedACK(c.mPlayer)) } for _, entry := range pk.Blocks { - c.AddPendingUpdate(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, c.mPlayer.BlockRuntimeIDFromNetwork(entry.BlockRuntimeID)) + c.AddPendingUpdate(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, c.mPlayer.BlockRuntimeIDFromBackend(entry.BlockRuntimeID)) } for _, entry := range pk.Extra { - c.AddPendingUpdate(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, c.mPlayer.BlockRuntimeIDFromNetwork(entry.BlockRuntimeID)) + c.AddPendingUpdate(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, c.mPlayer.BlockRuntimeIDFromBackend(entry.BlockRuntimeID)) } } diff --git a/player/component/world_hash_test.go b/player/component/world_hash_test.go index b177fcf7..a768228b 100644 --- a/player/component/world_hash_test.go +++ b/player/component/world_hash_test.go @@ -124,7 +124,7 @@ func TestClientBlockHashModeSurvivesBackendTransfer(t *testing.T) { t.Fatal("different endpoint modes do not require translation") } - if got := p.BlockRuntimeIDToNetwork(stoneRID); got != stoneHash { + if got := p.BlockRuntimeIDToClient(stoneRID); got != stoneHash { t.Fatalf("client block ID after transfer = %d, want initial hash %d", got, stoneHash) } } @@ -219,6 +219,26 @@ func TestUpdateBlockSyncedIsTranslatedToRetainedClientMode(t *testing.T) { } } +func TestUnknownUpdateBlockSyncedDoesNotMarkPacketModified(t *testing.T) { + oomphworld.FinalizeBlockRegistry() + p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{CurrentTime: time.Now()}, nil) + Register(p) + p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: false}}) + p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: true}}) + const unknownHash = uint32(1_000_000_000) + pk := packet.Packet(&packet.UpdateBlockSynced{NewBlockRuntimeID: unknownHash}) + ctx := playercontext.NewHandlePacketContext(&pk) + + p.HandleServerPacket(ctx) + + if ctx.Modified() { + t.Fatal("unknown preserved block ID marked packet modified") + } + if got := pk.(*packet.UpdateBlockSynced).NewBlockRuntimeID; got != unknownHash { + t.Fatalf("unknown block ID = %d, want preserved value %d", got, unknownHash) + } +} + type hashModeServerConn struct{ data minecraft.GameData } func (hashModeServerConn) WritePacket(packet.Packet) error { return nil } diff --git a/player/items.go b/player/items.go index 89345dd5..2223f27f 100644 --- a/player/items.go +++ b/player/items.go @@ -19,7 +19,7 @@ func (p *Player) ConvertToStack(it protocol.ItemStack) item.Stack { } } if it.BlockRuntimeID != 0 { - b, _ := p.World().BlockRegistry().BlockByRuntimeID(p.BlockRuntimeIDFromNetwork(uint32(it.BlockRuntimeID))) + b, _ := p.World().BlockRegistry().BlockByRuntimeID(p.BlockRuntimeIDFromBackend(uint32(it.BlockRuntimeID))) if t, ok = b.(world.Item); !ok { t = block.Air{} } @@ -34,14 +34,14 @@ func (p *Player) ConvertToStack(it protocol.ItemStack) item.Stack { func (p *Player) InstanceFromItem(it item.Stack) protocol.ItemInstance { instance := utils.InstanceFromItem(p.World().BlockRegistry(), it) if instance.Stack.BlockRuntimeID != 0 { - instance.Stack.BlockRuntimeID = int32(p.BlockRuntimeIDToNetwork(uint32(instance.Stack.BlockRuntimeID))) + instance.Stack.BlockRuntimeID = int32(p.BlockRuntimeIDToClient(uint32(instance.Stack.BlockRuntimeID))) } return instance } func (p *Player) StackToItem(it protocol.ItemStack) item.Stack { if it.BlockRuntimeID != 0 { - it.BlockRuntimeID = int32(p.BlockRuntimeIDFromNetwork(uint32(it.BlockRuntimeID))) + it.BlockRuntimeID = int32(p.BlockRuntimeIDFromBackend(uint32(it.BlockRuntimeID))) } return utils.StackToItem(p.World().BlockRegistry(), it) } diff --git a/player/packet.go b/player/packet.go index 8c91e7b8..e9e085ef 100644 --- a/player/packet.go +++ b/player/packet.go @@ -289,19 +289,14 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { } func (p *Player) rewriteClientBlockNetworkIDs(pk packet.Packet) bool { - rewriteStack := func(stack *protocol.ItemStack) { - if stack.BlockRuntimeID != 0 { - stack.BlockRuntimeID = int32(p.BlockRuntimeIDFromClientToBackend(uint32(stack.BlockRuntimeID))) - } - } + translator := p.ClientToBackendBlockNetwork() rewriteCraftResults := func(actions []protocol.StackRequestAction) bool { modified := false for _, action := range actions { if action, ok := action.(*protocol.CraftResultsDeprecatedStackRequestAction); ok { for i := range action.ResultItems { - rewriteStack(&action.ResultItems[i]) + modified = rewriteStackBlockNetworkID(&action.ResultItems[i], translator) || modified } - modified = true } } return modified @@ -310,9 +305,8 @@ func (p *Player) rewriteClientBlockNetworkIDs(pk packet.Packet) bool { case *packet.PlayerAuthInput: modified := false if pk.InputData.Load(packet.InputFlagPerformItemInteraction) { - pk.ItemInteractionData.BlockRuntimeID = p.BlockRuntimeIDFromClientToBackend(pk.ItemInteractionData.BlockRuntimeID) - rewriteStack(&pk.ItemInteractionData.HeldItem.Stack) - modified = true + changed := rewriteBlockNetworkID(&pk.ItemInteractionData.BlockRuntimeID, translator) + modified = rewriteStackBlockNetworkID(&pk.ItemInteractionData.HeldItem.Stack, translator) || changed } if pk.InputData.Load(packet.InputFlagPerformItemStackRequest) { modified = rewriteCraftResults(pk.ItemStackRequest.Actions) || modified @@ -325,25 +319,25 @@ func (p *Player) rewriteClientBlockNetworkIDs(pk packet.Packet) bool { } return modified case *packet.InventoryTransaction: + modified := false for i := range pk.Actions { - rewriteStack(&pk.Actions[i].OldItem.Stack) - rewriteStack(&pk.Actions[i].NewItem.Stack) + modified = rewriteStackBlockNetworkID(&pk.Actions[i].OldItem.Stack, translator) || modified + modified = rewriteStackBlockNetworkID(&pk.Actions[i].NewItem.Stack, translator) || modified } switch data := pk.TransactionData.(type) { case *protocol.UseItemTransactionData: - data.BlockRuntimeID = p.BlockRuntimeIDFromClientToBackend(data.BlockRuntimeID) - rewriteStack(&data.HeldItem.Stack) + changed := rewriteBlockNetworkID(&data.BlockRuntimeID, translator) + modified = rewriteStackBlockNetworkID(&data.HeldItem.Stack, translator) || modified || changed case *protocol.UseItemOnEntityTransactionData: - rewriteStack(&data.HeldItem.Stack) + modified = rewriteStackBlockNetworkID(&data.HeldItem.Stack, translator) || modified case *protocol.ReleaseItemTransactionData: - rewriteStack(&data.HeldItem.Stack) + modified = rewriteStackBlockNetworkID(&data.HeldItem.Stack, translator) || modified } - return true + return modified case *packet.MobEquipment: - rewriteStack(&pk.NewItem.Stack) - return true + return rewriteStackBlockNetworkID(&pk.NewItem.Stack, translator) case *packet.LevelSoundEvent: - return rewriteLevelSoundBlockNetworkID(pk, p.ClientToBackendBlockNetwork()) + return rewriteLevelSoundBlockNetworkID(pk, translator) } return false } @@ -355,9 +349,7 @@ func (p *Player) retainClientEquipment(pk *packet.MobEquipment) { // ClientItemForBackend translates an item retained in the client's block network mode to the current backend mode. func (p *Player) ClientItemForBackend(instance protocol.ItemInstance) protocol.ItemInstance { - if instance.Stack.BlockRuntimeID != 0 { - instance.Stack.BlockRuntimeID = int32(p.BlockRuntimeIDFromClientToBackend(uint32(instance.Stack.BlockRuntimeID))) - } + rewriteStackBlockNetworkID(&instance.Stack, p.ClientToBackendBlockNetwork()) return instance } @@ -578,24 +570,34 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { case *packet.UpdateBlock: p.worldUpdater.HandleUpdateBlock(pk) if p.BackendToClientBlockNetwork().Required() { - pk.NewBlockRuntimeID = p.BlockRuntimeIDFromBackendToClient(pk.NewBlockRuntimeID) - ctx.SetModified() + if rewriteBlockNetworkID(&pk.NewBlockRuntimeID, p.BackendToClientBlockNetwork()) { + ctx.SetModified() + } } case *packet.UpdateBlockSynced: if p.BackendToClientBlockNetwork().Required() { - pk.NewBlockRuntimeID = p.BlockRuntimeIDFromBackendToClient(pk.NewBlockRuntimeID) - ctx.SetModified() + if rewriteBlockNetworkID(&pk.NewBlockRuntimeID, p.BackendToClientBlockNetwork()) { + ctx.SetModified() + } } case *packet.UpdateSubChunkBlocks: p.worldUpdater.HandleUpdateSubChunkBlocks(pk) if p.BackendToClientBlockNetwork().Required() { + translator := p.BackendToClientBlockNetwork() + modified := false for i := range pk.Blocks { - pk.Blocks[i].BlockRuntimeID = p.BlockRuntimeIDFromBackendToClient(pk.Blocks[i].BlockRuntimeID) + if rewriteBlockNetworkID(&pk.Blocks[i].BlockRuntimeID, translator) { + modified = true + } } for i := range pk.Extra { - pk.Extra[i].BlockRuntimeID = p.BlockRuntimeIDFromBackendToClient(pk.Extra[i].BlockRuntimeID) + if rewriteBlockNetworkID(&pk.Extra[i].BlockRuntimeID, translator) { + modified = true + } + } + if modified { + ctx.SetModified() } - ctx.SetModified() } case *packet.ContainerOpen: p.inventory.CreateWindow(pk.WindowID, pk.ContainerType) @@ -631,44 +633,47 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { } func (p *Player) rewriteServerBlockNetworkIDs(pk packet.Packet) bool { - rewriteStack := func(stack *protocol.ItemStack) { - if stack.BlockRuntimeID != 0 { - stack.BlockRuntimeID = int32(p.BlockRuntimeIDFromBackendToClient(uint32(stack.BlockRuntimeID))) - } - } + translator := p.BackendToClientBlockNetwork() switch pk := pk.(type) { case *packet.InventorySlot: - rewriteStack(&pk.NewItem.Stack) + modified := rewriteStackBlockNetworkID(&pk.NewItem.Stack, translator) if storageItem, ok := pk.StorageItem.Value(); ok { - rewriteStack(&storageItem.Stack) - pk.StorageItem = protocol.Option(storageItem) + if rewriteStackBlockNetworkID(&storageItem.Stack, translator) { + pk.StorageItem = protocol.Option(storageItem) + modified = true + } } - return true + return modified case *packet.InventoryContent: // The inventory ACK retains the original slice for later processing in the backend's ID mode. - pk.Content = append([]protocol.ItemInstance(nil), pk.Content...) + modified := false for i := range pk.Content { - rewriteStack(&pk.Content[i].Stack) + stack := pk.Content[i].Stack + if !rewriteStackBlockNetworkID(&stack, translator) { + continue + } + if !modified { + pk.Content = append([]protocol.ItemInstance(nil), pk.Content...) + } + pk.Content[i].Stack = stack + modified = true } - rewriteStack(&pk.StorageItem.Stack) - return true + return rewriteStackBlockNetworkID(&pk.StorageItem.Stack, translator) || modified case *packet.MobEquipment: - rewriteStack(&pk.NewItem.Stack) - return true + return rewriteStackBlockNetworkID(&pk.NewItem.Stack, translator) case *packet.MobArmourEquipment: - rewriteStack(&pk.Helmet.Stack) - rewriteStack(&pk.Chestplate.Stack) - rewriteStack(&pk.Leggings.Stack) - rewriteStack(&pk.Boots.Stack) - rewriteStack(&pk.Body.Stack) - return true + modified := rewriteStackBlockNetworkID(&pk.Helmet.Stack, translator) + modified = rewriteStackBlockNetworkID(&pk.Chestplate.Stack, translator) || modified + modified = rewriteStackBlockNetworkID(&pk.Leggings.Stack, translator) || modified + modified = rewriteStackBlockNetworkID(&pk.Boots.Stack, translator) || modified + return rewriteStackBlockNetworkID(&pk.Body.Stack, translator) || modified case *packet.AddPlayer: - rewriteStack(&pk.HeldItem.Stack) - metadata, _ := rewriteActorBlockMetadata(pk.EntityMetadata, p.BackendToClientBlockNetwork(), false) + modified := rewriteStackBlockNetworkID(&pk.HeldItem.Stack, translator) + metadata, metadataModified := rewriteActorBlockMetadata(pk.EntityMetadata, translator, false) pk.EntityMetadata = metadata - return true + return modified || metadataModified case *packet.AddActor: - metadata, modified := rewriteActorBlockMetadata(pk.EntityMetadata, p.BackendToClientBlockNetwork(), pk.EntityType == fallingBlockEntityType) + metadata, modified := rewriteActorBlockMetadata(pk.EntityMetadata, translator, pk.EntityType == fallingBlockEntityType) pk.EntityMetadata = metadata return modified case *packet.SetActorData: @@ -678,84 +683,49 @@ func (p *Player) rewriteServerBlockNetworkIDs(pk packet.Packet) bool { fallingBlock = actor.Type == fallingBlockEntityType } } - metadata, modified := rewriteActorBlockMetadata(pk.EntityMetadata, p.BackendToClientBlockNetwork(), fallingBlock) + metadata, modified := rewriteActorBlockMetadata(pk.EntityMetadata, translator, fallingBlock) pk.EntityMetadata = metadata return modified case *packet.AddItemActor: - rewriteStack(&pk.Item.Stack) - return true + return rewriteStackBlockNetworkID(&pk.Item.Stack, translator) case *packet.LevelEvent: - return rewriteLevelEventBlockNetworkID(pk, p.BackendToClientBlockNetwork()) + return rewriteLevelEventBlockNetworkID(pk, translator) case *packet.LevelSoundEvent: - return rewriteLevelSoundBlockNetworkID(pk, p.BackendToClientBlockNetwork()) + return rewriteLevelSoundBlockNetworkID(pk, translator) case *packet.CreativeContent: + modified := false for i := range pk.Groups { - rewriteStack(&pk.Groups[i].Icon) + modified = rewriteStackBlockNetworkID(&pk.Groups[i].Icon, translator) || modified } for i := range pk.Items { - rewriteStack(&pk.Items[i].Item) + modified = rewriteStackBlockNetworkID(&pk.Items[i].Item, translator) || modified } - return true + return modified case *packet.InventoryTransaction: + modified := false for i := range pk.Actions { - rewriteStack(&pk.Actions[i].OldItem.Stack) - rewriteStack(&pk.Actions[i].NewItem.Stack) + modified = rewriteStackBlockNetworkID(&pk.Actions[i].OldItem.Stack, translator) || modified + modified = rewriteStackBlockNetworkID(&pk.Actions[i].NewItem.Stack, translator) || modified } switch data := pk.TransactionData.(type) { case *protocol.UseItemTransactionData: - data.BlockRuntimeID = p.BlockRuntimeIDFromBackendToClient(data.BlockRuntimeID) - rewriteStack(&data.HeldItem.Stack) + changed := rewriteBlockNetworkID(&data.BlockRuntimeID, translator) + modified = rewriteStackBlockNetworkID(&data.HeldItem.Stack, translator) || modified || changed case *protocol.UseItemOnEntityTransactionData: - rewriteStack(&data.HeldItem.Stack) + modified = rewriteStackBlockNetworkID(&data.HeldItem.Stack, translator) || modified case *protocol.ReleaseItemTransactionData: - rewriteStack(&data.HeldItem.Stack) + modified = rewriteStackBlockNetworkID(&data.HeldItem.Stack, translator) || modified } - return true + return modified case *packet.CraftingData: + modified := false for i, recipe := range pk.Recipes { - switch recipe := recipe.(type) { - case *protocol.ShapedRecipe: - clientRecipe := *recipe - clientRecipe.Output = append([]protocol.ItemStack(nil), recipe.Output...) - for outputIndex := range clientRecipe.Output { - rewriteStack(&clientRecipe.Output[outputIndex]) - } - pk.Recipes[i] = &clientRecipe - case *protocol.ShulkerBoxRecipe: - clientRecipe := *recipe - clientRecipe.Output = append([]protocol.ItemStack(nil), recipe.Output...) - for outputIndex := range clientRecipe.Output { - rewriteStack(&clientRecipe.Output[outputIndex]) - } - pk.Recipes[i] = &clientRecipe - case *protocol.ShapelessChemistryRecipe: - clientRecipe := *recipe - clientRecipe.Output = append([]protocol.ItemStack(nil), recipe.Output...) - for outputIndex := range clientRecipe.Output { - rewriteStack(&clientRecipe.Output[outputIndex]) - } - pk.Recipes[i] = &clientRecipe - case *protocol.ShapedChemistryRecipe: - clientRecipe := *recipe - clientRecipe.Output = append([]protocol.ItemStack(nil), recipe.Output...) - for outputIndex := range clientRecipe.Output { - rewriteStack(&clientRecipe.Output[outputIndex]) - } - pk.Recipes[i] = &clientRecipe - case *protocol.ShapelessRecipe: - clientRecipe := *recipe - clientRecipe.Output = append([]protocol.ItemStack(nil), recipe.Output...) - for outputIndex := range clientRecipe.Output { - rewriteStack(&clientRecipe.Output[outputIndex]) - } - pk.Recipes[i] = &clientRecipe - case *protocol.SmithingTransformRecipe: - clientRecipe := *recipe - rewriteStack(&clientRecipe.Result) - pk.Recipes[i] = &clientRecipe + if clientRecipe, changed := rewriteRecipeBlockNetworkIDs(recipe, translator); changed { + pk.Recipes[i] = clientRecipe + modified = true } } - return true + return modified } return false } diff --git a/player/world.go b/player/world.go index 8d926027..d7284684 100644 --- a/player/world.go +++ b/player/world.go @@ -72,15 +72,15 @@ func (p *Player) World() *oworld.World { return p.world } -// BlockRuntimeIDFromNetwork converts a network block hash to the runtime ID used by Oomph's block registry when the -// backend enabled hashed block network IDs. Unknown values are preserved so callers can retain their existing fallback. -func (p *Player) BlockRuntimeIDFromNetwork(id uint32) uint32 { +// BlockRuntimeIDFromBackend converts a backend block ID to Oomph's canonical registry runtime ID. +// Unknown values are preserved so callers can retain their existing fallback. +func (p *Player) BlockRuntimeIDFromBackend(id uint32) uint32 { return blockRuntimeIDFromNetwork(p.backendBlockNetwork, id) } -// BlockRuntimeIDToNetwork converts an Oomph block runtime ID to the hash expected by clients when the backend enabled -// hashed block network IDs. Unknown values are preserved so custom block fallbacks remain intact. -func (p *Player) BlockRuntimeIDToNetwork(id uint32) uint32 { +// BlockRuntimeIDToClient converts a canonical registry runtime ID to the client's block ID representation. +// Unknown values are preserved so custom block fallbacks remain intact. +func (p *Player) BlockRuntimeIDToClient(id uint32) uint32 { return blockRuntimeIDToNetwork(p.clientBlockNetwork, id) } @@ -89,22 +89,6 @@ func (p *Player) BlockRuntimeIDFromClient(id uint32) uint32 { return blockRuntimeIDFromNetwork(p.clientBlockNetwork, id) } -// BlockRuntimeIDToBackend converts an Oomph registry runtime ID to the current backend's network representation. -func (p *Player) BlockRuntimeIDToBackend(id uint32) uint32 { - return blockRuntimeIDToNetwork(p.backendBlockNetwork, id) -} - -// BlockRuntimeIDFromBackendToClient translates a block ID from the current backend's representation to the one fixed -// by the client's initial StartGame packet. -func (p *Player) BlockRuntimeIDFromBackendToClient(id uint32) uint32 { - return p.BackendToClientBlockNetwork().Translate(id) -} - -// BlockRuntimeIDFromClientToBackend translates a client-visible block ID to the current backend's representation. -func (p *Player) BlockRuntimeIDFromClientToBackend(id uint32) uint32 { - return p.ClientToBackendBlockNetwork().Translate(id) -} - func blockRuntimeIDFromNetwork(codec blocknetwork.Codec, id uint32) uint32 { if runtimeID, ok := codec.ToRuntimeID(id); ok { return runtimeID @@ -148,7 +132,7 @@ func (p *Player) SyncBlock(pos df_cube.Pos) { int32(pos[1]), int32(pos[2]), }, - NewBlockRuntimeID: p.BlockRuntimeIDToNetwork(blockRuntimeID), + NewBlockRuntimeID: p.BlockRuntimeIDToClient(blockRuntimeID), Flags: packet.BlockUpdateNetwork, Layer: 0, // TODO: Implement and account for multi-layer blocks. } @@ -215,7 +199,7 @@ func (p *Player) SendBlockUpdates(positions []protocol.BlockPos) { for _, pos := range positions { p.SendPacketToClient(&packet.UpdateBlock{ Position: pos, - NewBlockRuntimeID: p.BlockRuntimeIDToNetwork(world.BlockRuntimeID(p.World().Block(df_cube.Pos{ + NewBlockRuntimeID: p.BlockRuntimeIDToClient(world.BlockRuntimeID(p.World().Block(df_cube.Pos{ int(pos.X()), int(pos.Y()), int(pos.Z()), diff --git a/player/world_hash_test.go b/player/world_hash_test.go index c63c15a4..851a40eb 100644 --- a/player/world_hash_test.go +++ b/player/world_hash_test.go @@ -22,7 +22,7 @@ func setBlockNetworkModes(p *Player, client, backend blocknetwork.Mode) { p.backendBlockNetwork = blocknetwork.NewCodec(world.BlockRegistry, backend) } -func TestBlockRuntimeIDToNetworkUsesHashMode(t *testing.T) { +func TestBlockRuntimeIDToClientUsesHashMode(t *testing.T) { world.FinalizeBlockRegistry() stoneRID := dfworld.BlockRuntimeID(block.Stone{}) stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) @@ -32,7 +32,7 @@ func TestBlockRuntimeIDToNetworkUsesHashMode(t *testing.T) { p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) p.clientBlockNetwork = blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.Hashes) - if got := p.BlockRuntimeIDToNetwork(stoneRID); got != stoneHash { + if got := p.BlockRuntimeIDToClient(stoneRID); got != stoneHash { t.Fatalf("network block ID = %d, want hash %d", got, stoneHash) } } @@ -90,7 +90,7 @@ func TestBlockRuntimeIDTranslationSeparatesClientAndBackendModes(t *testing.T) { if got := p.BlockRuntimeIDFromClient(stoneHash); got != stoneRID { t.Fatalf("client hash translated to %d, want runtime ID %d", got, stoneRID) } - if got := p.BlockRuntimeIDFromClientToBackend(stoneHash); got != stoneRID { + if got := p.ClientToBackendBlockNetwork().Translate(stoneHash); got != stoneRID { t.Fatalf("client hash translated for backend to %d, want runtime ID %d", got, stoneRID) } } @@ -237,6 +237,45 @@ func TestRewriteServerInventoryStackUsesRetainedClientMode(t *testing.T) { } } +func TestRewriteBlockNetworkIDsReportsOnlyActualChanges(t *testing.T) { + world.FinalizeBlockRegistry() + p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) + setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) + + clientPackets := map[string]packet.Packet{ + "empty inventory transaction": &packet.InventoryTransaction{}, + "empty equipment": &packet.MobEquipment{}, + "unrelated sound": &packet.LevelSoundEvent{SoundType: packet.SoundEventAttackNoDamage}, + } + for name, pk := range clientPackets { + t.Run("client/"+name, func(t *testing.T) { + if p.rewriteClientBlockNetworkIDs(pk) { + t.Fatal("packet reported a block network ID rewrite without changing an ID") + } + }) + } + + setBlockNetworkModes(p, blocknetwork.RuntimeIDs, blocknetwork.Hashes) + serverPackets := map[string]packet.Packet{ + "empty inventory slot": &packet.InventorySlot{}, + "empty inventory content": &packet.InventoryContent{}, + "empty equipment": &packet.MobEquipment{}, + "empty player spawn": &packet.AddPlayer{}, + "empty item actor": &packet.AddItemActor{}, + "empty creative content": &packet.CreativeContent{}, + "empty transaction": &packet.InventoryTransaction{}, + "empty crafting data": &packet.CraftingData{}, + "unrelated sound": &packet.LevelSoundEvent{SoundType: packet.SoundEventAttackNoDamage}, + } + for name, pk := range serverPackets { + t.Run("server/"+name, func(t *testing.T) { + if p.rewriteServerBlockNetworkIDs(pk) { + t.Fatal("packet reported a block network ID rewrite without changing an ID") + } + }) + } +} + func TestRewriteServerBlockNetworkIDsCoversBlockEvents(t *testing.T) { world.FinalizeBlockRegistry() stoneRID := dfworld.BlockRuntimeID(block.Stone{}) From 84565205b6dc12024025ede19f3df284a97adfae Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 14 Jul 2026 00:38:21 -0400 Subject: [PATCH 12/16] test: remove block network regression coverage --- player/component/world_hash_test.go | 246 --------------- player/world_hash_test.go | 443 ---------------------------- 2 files changed, 689 deletions(-) delete mode 100644 player/component/world_hash_test.go delete mode 100644 player/world_hash_test.go diff --git a/player/component/world_hash_test.go b/player/component/world_hash_test.go deleted file mode 100644 index a768228b..00000000 --- a/player/component/world_hash_test.go +++ /dev/null @@ -1,246 +0,0 @@ -package component - -import ( - "bytes" - "io" - "log/slog" - "testing" - "time" - - "github.com/df-mc/dragonfly/server/block" - "github.com/df-mc/dragonfly/server/block/cube" - dfworld "github.com/df-mc/dragonfly/server/world" - "github.com/df-mc/dragonfly/server/world/chunk" - "github.com/oomph-ac/oomph/player" - playercontext "github.com/oomph-ac/oomph/player/context" - oomphworld "github.com/oomph-ac/oomph/world" - "github.com/oomph-ac/oomph/world/blocknetwork" - "github.com/sandertv/gophertunnel/minecraft" - "github.com/sandertv/gophertunnel/minecraft/protocol" - "github.com/sandertv/gophertunnel/minecraft/protocol/packet" -) - -func TestWorldUpdaterDecodesHashedLevelChunk(t *testing.T) { - oomphworld.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := oomphworld.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - - position := cube.Pos{1, 0, 1} - c := chunk.New(oomphworld.BlockRegistry, dfworld.Overworld.Range()) - c.SetBlock(uint8(position[0]), int16(position[1]), uint8(position[2]), 0, stoneRID) - data := chunk.Encode(c, chunk.NetworkEncoding) - stoneRIDBytes, stoneHashBytes := bytes.NewBuffer(nil), bytes.NewBuffer(nil) - if err := protocol.WriteVarint32(stoneRIDBytes, int32(stoneRID)); err != nil { - t.Fatal(err) - } - if err := protocol.WriteVarint32(stoneHashBytes, int32(stoneHash)); err != nil { - t.Fatal(err) - } - raw := bytes.NewBuffer(nil) - converted := false - for _, subChunk := range data.SubChunks { - hashed := bytes.ReplaceAll(subChunk, stoneRIDBytes.Bytes(), stoneHashBytes.Bytes()) - if !bytes.Equal(hashed, subChunk) { - converted = true - } - raw.Write(hashed) - } - if !converted { - t.Fatal("stone runtime ID not found in encoded chunk") - } - raw.Write(data.Biomes) - raw.WriteByte(0) - - p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{CurrentTime: time.Now()}, nil) - Register(p) - p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: true}}) - p.WorldUpdater().HandleLevelChunk(&packet.LevelChunk{ - Position: protocol.ChunkPos{0, 0}, - Dimension: packet.DimensionOverworld, - SubChunkCount: uint32(len(data.SubChunks)), - RawPayload: raw.Bytes(), - }) - - got := p.World().Block(position) - if _, ok := got.(block.Stone); !ok { - gotRID := dfworld.BlockRuntimeID(got) - t.Fatalf("block = %T (runtime ID %d), want stone (runtime ID %d, network hash %d)", got, gotRID, stoneRID, stoneHash) - } -} - -func TestWorldUpdaterConvertsHashedBlockUpdate(t *testing.T) { - oomphworld.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := oomphworld.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{CurrentTime: time.Now()}, nil) - Register(p) - p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: true}}) - updater := p.WorldUpdater().(*WorldUpdaterComponent) - position := cube.Pos{4, 5, 6} - - updater.HandleUpdateBlock(&packet.UpdateBlock{ - Position: protocol.BlockPos{4, 5, 6}, - NewBlockRuntimeID: stoneHash, - }) - - if got := updater.batchedBlockUpdates.Blocks()[position]; got != stoneRID { - t.Fatalf("pending block runtime ID = %d, want %d (network hash %d)", got, stoneRID, stoneHash) - } -} - -func TestClientBlockHashModeSurvivesBackendTransfer(t *testing.T) { - oomphworld.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := oomphworld.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{CurrentTime: time.Now()}, nil) - Register(p) - p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: true}}) - if got := p.ClientBlockNetwork().Mode(); got != blocknetwork.Hashes { - t.Fatalf("initial client mode = %v, want Hashes", got) - } - if got := p.BackendBlockNetwork().Mode(); got != blocknetwork.Hashes { - t.Fatalf("initial backend mode = %v, want Hashes", got) - } - if p.ClientToBackendBlockNetwork().Required() || p.BackendToClientBlockNetwork().Required() { - t.Fatal("matching initial endpoint modes require translation") - } - p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: false}}) - if got := p.ClientBlockNetwork().Mode(); got != blocknetwork.Hashes { - t.Fatalf("client mode after transfer = %v, want retained Hashes", got) - } - if got := p.BackendBlockNetwork().Mode(); got != blocknetwork.RuntimeIDs { - t.Fatalf("backend mode after transfer = %v, want RuntimeIDs", got) - } - if !p.ClientToBackendBlockNetwork().Required() || !p.BackendToClientBlockNetwork().Required() { - t.Fatal("different endpoint modes do not require translation") - } - - if got := p.BlockRuntimeIDToClient(stoneRID); got != stoneHash { - t.Fatalf("client block ID after transfer = %d, want initial hash %d", got, stoneHash) - } -} - -func TestSyncBlockQueuesCanonicalRuntimeIDAcrossModes(t *testing.T) { - oomphworld.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := oomphworld.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{ - CurrentTime: time.Now(), - IsReplay: true, - }, nil) - Register(p) - p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: true}}) - p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: false}}) - position := cube.Pos{4, 5, 6} - p.World().SetBlock(position, block.Stone{}, nil) - - p.SyncBlock(position) - - updater := p.WorldUpdater().(*WorldUpdaterComponent) - if got := updater.batchedBlockUpdates.Blocks()[position]; got != stoneRID { - t.Fatalf("pending block runtime ID = %d, want canonical runtime ID %d (client hash %d)", got, stoneRID, stoneHash) - } -} - -func TestLevelChunkIsTranslatedToRetainedClientHashModeAfterTransfer(t *testing.T) { - oomphworld.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := oomphworld.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - position := cube.Pos{1, 0, 1} - c := chunk.New(oomphworld.BlockRegistry, dfworld.Overworld.Range()) - c.SetBlock(uint8(position[0]), int16(position[1]), uint8(position[2]), 0, stoneRID) - data := chunk.Encode(c, chunk.NetworkEncoding) - raw := bytes.NewBuffer(nil) - for _, sub := range data.SubChunks { - raw.Write(sub) - } - raw.Write(data.Biomes) - raw.WriteByte(0) - - p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{CurrentTime: time.Now()}, nil) - Register(p) - p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: true}}) - p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: false}}) - pk := packet.Packet(&packet.LevelChunk{ - Position: protocol.ChunkPos{0, 0}, - Dimension: packet.DimensionOverworld, - SubChunkCount: uint32(len(data.SubChunks)), - RawPayload: raw.Bytes(), - }) - ctx := playercontext.NewHandlePacketContext(&pk) - p.HandleServerPacket(ctx) - - forwarded := pk.(*packet.LevelChunk) - decoded, err := chunk.NetworkDecode(oomphworld.BlockRegistry, forwarded.RawPayload, int(forwarded.SubChunkCount), dfworld.Overworld.Range()) - if err != nil { - t.Fatal(err) - } - if got := decoded.Block(uint8(position[0]), int16(position[1]), uint8(position[2]), 0); got != stoneHash { - t.Fatalf("forwarded block ID = %d, want client hash %d", got, stoneHash) - } - if _, ok := p.World().Block(position).(block.Stone); !ok { - t.Fatalf("internal block = %T, want block.Stone", p.World().Block(position)) - } -} - -func TestUpdateBlockSyncedIsTranslatedToRetainedClientMode(t *testing.T) { - oomphworld.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := oomphworld.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{CurrentTime: time.Now()}, nil) - Register(p) - p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: false}}) - p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: true}}) - pk := packet.Packet(&packet.UpdateBlockSynced{NewBlockRuntimeID: stoneHash}) - ctx := playercontext.NewHandlePacketContext(&pk) - - p.HandleServerPacket(ctx) - - if got := pk.(*packet.UpdateBlockSynced).NewBlockRuntimeID; got != stoneRID { - t.Fatalf("forwarded block ID = %d, want client runtime ID %d", got, stoneRID) - } -} - -func TestUnknownUpdateBlockSyncedDoesNotMarkPacketModified(t *testing.T) { - oomphworld.FinalizeBlockRegistry() - p := player.New(slog.New(slog.NewTextHandler(io.Discard, nil)), player.MonitoringState{CurrentTime: time.Now()}, nil) - Register(p) - p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: false}}) - p.SetServerConn(hashModeServerConn{data: minecraft.GameData{UseBlockNetworkIDHashes: true}}) - const unknownHash = uint32(1_000_000_000) - pk := packet.Packet(&packet.UpdateBlockSynced{NewBlockRuntimeID: unknownHash}) - ctx := playercontext.NewHandlePacketContext(&pk) - - p.HandleServerPacket(ctx) - - if ctx.Modified() { - t.Fatal("unknown preserved block ID marked packet modified") - } - if got := pk.(*packet.UpdateBlockSynced).NewBlockRuntimeID; got != unknownHash { - t.Fatalf("unknown block ID = %d, want preserved value %d", got, unknownHash) - } -} - -type hashModeServerConn struct{ data minecraft.GameData } - -func (hashModeServerConn) WritePacket(packet.Packet) error { return nil } -func (hashModeServerConn) Close() error { return nil } -func (c hashModeServerConn) GameData() minecraft.GameData { return c.data } diff --git a/player/world_hash_test.go b/player/world_hash_test.go deleted file mode 100644 index 851a40eb..00000000 --- a/player/world_hash_test.go +++ /dev/null @@ -1,443 +0,0 @@ -package player - -import ( - "fmt" - "io" - "log/slog" - "testing" - "time" - - "github.com/df-mc/dragonfly/server/block" - dfworld "github.com/df-mc/dragonfly/server/world" - "github.com/go-gl/mathgl/mgl32" - "github.com/oomph-ac/oomph/entity" - "github.com/oomph-ac/oomph/world" - "github.com/oomph-ac/oomph/world/blocknetwork" - "github.com/sandertv/gophertunnel/minecraft/protocol" - "github.com/sandertv/gophertunnel/minecraft/protocol/packet" -) - -func setBlockNetworkModes(p *Player, client, backend blocknetwork.Mode) { - p.clientBlockNetwork = blocknetwork.NewCodec(world.BlockRegistry, client) - p.backendBlockNetwork = blocknetwork.NewCodec(world.BlockRegistry, backend) -} - -func TestBlockRuntimeIDToClientUsesHashMode(t *testing.T) { - world.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - p.clientBlockNetwork = blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.Hashes) - - if got := p.BlockRuntimeIDToClient(stoneRID); got != stoneHash { - t.Fatalf("network block ID = %d, want hash %d", got, stoneHash) - } -} - -func TestReportedBlockNetworkHashesResolve(t *testing.T) { - world.FinalizeBlockRegistry() - tests := []struct { - hash uint32 - want any - }{ - {hash: 1741778478, want: block.Cobblestone{}}, - {hash: 2150698529, want: block.Stone{}}, - } - for _, test := range tests { - runtimeID, ok := world.BlockRegistry.HashToRuntimeID(test.hash) - if !ok { - t.Fatalf("reported hash %d did not resolve", test.hash) - } - got, ok := world.BlockRegistry.BlockByRuntimeID(runtimeID) - if !ok { - t.Fatalf("runtime ID %d for hash %d did not resolve", runtimeID, test.hash) - } - if fmt.Sprintf("%T", got) != fmt.Sprintf("%T", test.want) { - t.Fatalf("hash %d resolved to %T, want %T", test.hash, got, test.want) - } - } -} - -func TestConvertToStackAcceptsSignedBlockNetworkHash(t *testing.T) { - world.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok || int32(stoneHash) >= 0 { - t.Fatalf("stone network hash = %d, want a signed-negative hash", stoneHash) - } - p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - p.backendBlockNetwork = blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.Hashes) - - stack := p.ConvertToStack(protocol.ItemStack{BlockRuntimeID: int32(stoneHash), Count: 1}) - if _, ok := stack.Item().(block.Stone); !ok { - t.Fatalf("converted item = %T, want block.Stone", stack.Item()) - } -} - -func TestBlockRuntimeIDTranslationSeparatesClientAndBackendModes(t *testing.T) { - world.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) - - if got := p.BlockRuntimeIDFromClient(stoneHash); got != stoneRID { - t.Fatalf("client hash translated to %d, want runtime ID %d", got, stoneRID) - } - if got := p.ClientToBackendBlockNetwork().Translate(stoneHash); got != stoneRID { - t.Fatalf("client hash translated for backend to %d, want runtime ID %d", got, stoneRID) - } -} - -func TestRewriteClientBlockNetworkIDsCoversAuthInputInteraction(t *testing.T) { - world.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) - inputData := protocol.NewBitset(200) - inputData.Set(packet.InputFlagPerformItemInteraction) - pk := &packet.PlayerAuthInput{ - InputData: inputData, - ItemInteractionData: protocol.UseItemTransactionData{ - BlockRuntimeID: stoneHash, - HeldItem: protocol.ItemInstance{Stack: protocol.ItemStack{BlockRuntimeID: int32(stoneHash)}}, - }, - } - - if !p.rewriteClientBlockNetworkIDs(pk) { - t.Fatal("auth input interaction was not rewritten") - } - if got := pk.ItemInteractionData.BlockRuntimeID; got != stoneRID { - t.Fatalf("interaction block ID = %d, want runtime ID %d", got, stoneRID) - } - if got := uint32(pk.ItemInteractionData.HeldItem.Stack.BlockRuntimeID); got != stoneRID { - t.Fatalf("held item block ID = %d, want runtime ID %d", got, stoneRID) - } -} - -func TestRewriteClientBlockNetworkIDsCoversAuthInputCraftResults(t *testing.T) { - world.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) - inputData := protocol.NewBitset(200) - inputData.Set(packet.InputFlagPerformItemStackRequest) - action := &protocol.CraftResultsDeprecatedStackRequestAction{ - ResultItems: []protocol.ItemStack{{BlockRuntimeID: int32(stoneHash)}}, - } - pk := &packet.PlayerAuthInput{ - InputData: inputData, - ItemStackRequest: protocol.ItemStackRequest{ - Actions: []protocol.StackRequestAction{action}, - }, - } - - if !p.rewriteClientBlockNetworkIDs(pk) { - t.Fatal("auth input craft results were not rewritten") - } - if got := uint32(action.ResultItems[0].BlockRuntimeID); got != stoneRID { - t.Fatalf("craft result block ID = %d, want runtime ID %d", got, stoneRID) - } -} - -func TestRewriteClientBlockNetworkIDsCoversStandaloneCraftResults(t *testing.T) { - world.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) - action := &protocol.CraftResultsDeprecatedStackRequestAction{ - ResultItems: []protocol.ItemStack{{BlockRuntimeID: int32(stoneHash)}}, - } - pk := &packet.ItemStackRequest{Requests: []protocol.ItemStackRequest{{ - Actions: []protocol.StackRequestAction{action}, - }}} - - if !p.rewriteClientBlockNetworkIDs(pk) { - t.Fatal("standalone craft results were not rewritten") - } - if got := uint32(action.ResultItems[0].BlockRuntimeID); got != stoneRID { - t.Fatalf("craft result block ID = %d, want runtime ID %d", got, stoneRID) - } -} - -func TestRewriteClientBlockNetworkIDsCoversBlockSound(t *testing.T) { - world.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) - pk := &packet.LevelSoundEvent{SoundType: packet.SoundEventPlace, ExtraData: int32(stoneHash)} - - if !p.rewriteClientBlockNetworkIDs(pk) { - t.Fatal("block sound was not rewritten") - } - if got := uint32(pk.ExtraData); got != stoneRID { - t.Fatalf("block sound ID = %d, want runtime ID %d", got, stoneRID) - } -} - -func TestRewriteServerInventoryStackUsesRetainedClientMode(t *testing.T) { - world.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - setBlockNetworkModes(p, blocknetwork.RuntimeIDs, blocknetwork.Hashes) - pk := &packet.InventorySlot{ - NewItem: protocol.ItemInstance{Stack: protocol.ItemStack{BlockRuntimeID: int32(stoneHash)}}, - StorageItem: protocol.Option(protocol.ItemInstance{Stack: protocol.ItemStack{BlockRuntimeID: int32(stoneHash)}}), - } - - if !p.rewriteServerBlockNetworkIDs(pk) { - t.Fatal("inventory slot was not rewritten") - } - if got := uint32(pk.NewItem.Stack.BlockRuntimeID); got != stoneRID { - t.Fatalf("inventory block ID = %d, want runtime ID %d", got, stoneRID) - } - storageItem, ok := pk.StorageItem.Value() - if !ok { - t.Fatal("inventory storage item was removed") - } - if got := uint32(storageItem.Stack.BlockRuntimeID); got != stoneRID { - t.Fatalf("inventory storage block ID = %d, want runtime ID %d", got, stoneRID) - } - original := []protocol.ItemInstance{{Stack: protocol.ItemStack{BlockRuntimeID: int32(stoneHash)}}} - content := &packet.InventoryContent{Content: original} - if !p.rewriteServerBlockNetworkIDs(content) { - t.Fatal("inventory content was not rewritten") - } - if got := uint32(content.Content[0].Stack.BlockRuntimeID); got != stoneRID { - t.Fatalf("inventory content block ID = %d, want runtime ID %d", got, stoneRID) - } - if got := uint32(original[0].Stack.BlockRuntimeID); got != stoneHash { - t.Fatalf("retained backend inventory block ID = %d, want hash %d", got, stoneHash) - } -} - -func TestRewriteBlockNetworkIDsReportsOnlyActualChanges(t *testing.T) { - world.FinalizeBlockRegistry() - p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) - - clientPackets := map[string]packet.Packet{ - "empty inventory transaction": &packet.InventoryTransaction{}, - "empty equipment": &packet.MobEquipment{}, - "unrelated sound": &packet.LevelSoundEvent{SoundType: packet.SoundEventAttackNoDamage}, - } - for name, pk := range clientPackets { - t.Run("client/"+name, func(t *testing.T) { - if p.rewriteClientBlockNetworkIDs(pk) { - t.Fatal("packet reported a block network ID rewrite without changing an ID") - } - }) - } - - setBlockNetworkModes(p, blocknetwork.RuntimeIDs, blocknetwork.Hashes) - serverPackets := map[string]packet.Packet{ - "empty inventory slot": &packet.InventorySlot{}, - "empty inventory content": &packet.InventoryContent{}, - "empty equipment": &packet.MobEquipment{}, - "empty player spawn": &packet.AddPlayer{}, - "empty item actor": &packet.AddItemActor{}, - "empty creative content": &packet.CreativeContent{}, - "empty transaction": &packet.InventoryTransaction{}, - "empty crafting data": &packet.CraftingData{}, - "unrelated sound": &packet.LevelSoundEvent{SoundType: packet.SoundEventAttackNoDamage}, - } - for name, pk := range serverPackets { - t.Run("server/"+name, func(t *testing.T) { - if p.rewriteServerBlockNetworkIDs(pk) { - t.Fatal("packet reported a block network ID rewrite without changing an ID") - } - }) - } -} - -func TestRewriteServerBlockNetworkIDsCoversBlockEvents(t *testing.T) { - world.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - setBlockNetworkModes(p, blocknetwork.RuntimeIDs, blocknetwork.Hashes) - - event := &packet.LevelEvent{EventType: packet.LevelEventParticlesDestroyBlock, EventData: int32(stoneHash)} - if !p.rewriteServerBlockNetworkIDs(event) { - t.Fatal("block event was not rewritten") - } - if got := uint32(event.EventData); got != stoneRID { - t.Fatalf("block event ID = %d, want runtime ID %d", got, stoneRID) - } - - sound := &packet.LevelSoundEvent{SoundType: packet.SoundEventHit, ExtraData: int32(stoneHash)} - if !p.rewriteServerBlockNetworkIDs(sound) { - t.Fatal("block sound was not rewritten") - } - if got := uint32(sound.ExtraData); got != stoneRID { - t.Fatalf("block sound ID = %d, want runtime ID %d", got, stoneRID) - } -} - -func TestRewriteServerBlockNetworkIDsCoversFallingBlockMetadata(t *testing.T) { - world.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - setBlockNetworkModes(p, blocknetwork.RuntimeIDs, blocknetwork.Hashes) - backendMetadata := map[uint32]any{protocol.EntityDataKeyVariant: int32(stoneHash)} - pk := &packet.AddActor{EntityType: "minecraft:falling_block", EntityMetadata: backendMetadata} - - if !p.rewriteServerBlockNetworkIDs(pk) { - t.Fatal("falling-block metadata was not rewritten") - } - if got := uint32(pk.EntityMetadata[protocol.EntityDataKeyVariant].(int32)); got != stoneRID { - t.Fatalf("falling-block metadata ID = %d, want runtime ID %d", got, stoneRID) - } - if got := uint32(backendMetadata[protocol.EntityDataKeyVariant].(int32)); got != stoneHash { - t.Fatalf("retained backend metadata ID = %d, want hash %d", got, stoneHash) - } - - unrelated := &packet.AddActor{ - EntityType: "minecraft:tropicalfish", - EntityMetadata: map[uint32]any{protocol.EntityDataKeyVariant: int32(stoneHash)}, - } - if p.rewriteServerBlockNetworkIDs(unrelated) { - t.Fatal("unrelated entity variant was rewritten") - } -} - -func TestRewriteServerBlockNetworkIDsCoversBlockValuedActorMetadata(t *testing.T) { - world.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - setBlockNetworkModes(p, blocknetwork.RuntimeIDs, blocknetwork.Hashes) - - spawnMetadata := map[uint32]any{protocol.EntityDataKeyCarryBlockRuntimeID: int32(stoneHash)} - spawn := &packet.AddActor{EntityType: "minecraft:enderman", EntityMetadata: spawnMetadata} - if !p.rewriteServerBlockNetworkIDs(spawn) { - t.Fatal("carried-block spawn metadata was not rewritten") - } - if got := uint32(spawn.EntityMetadata[protocol.EntityDataKeyCarryBlockRuntimeID].(int32)); got != stoneRID { - t.Fatalf("carried-block spawn metadata ID = %d, want runtime ID %d", got, stoneRID) - } - if got := uint32(spawnMetadata[protocol.EntityDataKeyCarryBlockRuntimeID].(int32)); got != stoneHash { - t.Fatalf("retained backend spawn metadata ID = %d, want hash %d", got, stoneHash) - } - - updateMetadata := map[uint32]any{ - protocol.EntityDataKeyCarryBlockRuntimeID: int32(stoneHash), - protocol.EntityDataKeyDisplayTileRuntimeID: int32(stoneHash), - protocol.EntityDataKeyVariant: int32(stoneHash), - } - p.entTracker = actorTypeTracker{entity: &entity.Entity{Type: fallingBlockEntityType}} - update := &packet.SetActorData{EntityRuntimeID: 42, EntityMetadata: updateMetadata} - if !p.rewriteServerBlockNetworkIDs(update) { - t.Fatal("block-valued actor metadata update was not rewritten") - } - for _, key := range []uint32{protocol.EntityDataKeyCarryBlockRuntimeID, protocol.EntityDataKeyDisplayTileRuntimeID, protocol.EntityDataKeyVariant} { - if got := uint32(update.EntityMetadata[key].(int32)); got != stoneRID { - t.Fatalf("actor metadata key %d ID = %d, want runtime ID %d", key, got, stoneRID) - } - if got := uint32(updateMetadata[key].(int32)); got != stoneHash { - t.Fatalf("retained backend metadata key %d ID = %d, want hash %d", key, got, stoneHash) - } - } -} - -func TestRetainedClientEquipmentIsIndependentOfBackendTranslation(t *testing.T) { - world.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) - pk := &packet.MobEquipment{NewItem: protocol.ItemInstance{Stack: protocol.ItemStack{BlockRuntimeID: int32(stoneHash)}}} - - p.retainClientEquipment(pk) - p.rewriteClientBlockNetworkIDs(pk) - - if got := uint32(pk.NewItem.Stack.BlockRuntimeID); got != stoneRID { - t.Fatalf("backend equipment block ID = %d, want runtime ID %d", got, stoneRID) - } - if got := uint32(p.LastEquipmentData.NewItem.Stack.BlockRuntimeID); got != stoneHash { - t.Fatalf("retained client equipment block ID = %d, want hash %d", got, stoneHash) - } - generated := p.ClientItemForBackend(p.LastEquipmentData.NewItem) - if got := uint32(generated.Stack.BlockRuntimeID); got != stoneRID { - t.Fatalf("generated backend equipment block ID = %d, want runtime ID %d", got, stoneRID) - } -} - -type actorTypeTracker struct { - entity *entity.Entity -} - -func (actorTypeTracker) AddEntity(uint64, *entity.Entity) {} -func (actorTypeTracker) RemoveEntity(uint64) {} -func (t actorTypeTracker) FindEntity(uint64) *entity.Entity { return t.entity } -func (actorTypeTracker) All() map[uint64]*entity.Entity { return nil } -func (actorTypeTracker) MoveEntity(uint64, int64, mgl32.Vec3, bool) {} -func (actorTypeTracker) HandleMovePlayer(*packet.MovePlayer) {} -func (actorTypeTracker) HandleMoveActorAbsolute(*packet.MoveActorAbsolute) {} -func (actorTypeTracker) HandleSetActorData(*packet.SetActorData) {} -func (actorTypeTracker) Tick(int64) {} - -func TestRewriteServerRecipesPreservesBackendOutputs(t *testing.T) { - world.FinalizeBlockRegistry() - stoneRID := dfworld.BlockRuntimeID(block.Stone{}) - stoneHash, ok := world.BlockRegistry.RuntimeIDToHash(stoneRID) - if !ok { - t.Fatal("stone has no network hash") - } - p := New(slog.New(slog.NewTextHandler(io.Discard, nil)), MonitoringState{CurrentTime: time.Now()}, nil) - setBlockNetworkModes(p, blocknetwork.Hashes, blocknetwork.RuntimeIDs) - backendRecipe := &protocol.ShapedChemistryRecipe{ShapedRecipe: protocol.ShapedRecipe{ - Output: []protocol.ItemStack{{BlockRuntimeID: int32(stoneRID)}}, - }} - pk := &packet.CraftingData{Recipes: []protocol.Recipe{backendRecipe}} - - if !p.rewriteServerBlockNetworkIDs(pk) { - t.Fatal("crafting data was not rewritten") - } - clientRecipe := pk.Recipes[0].(*protocol.ShapedChemistryRecipe) - if got := uint32(clientRecipe.Output[0].BlockRuntimeID); got != stoneHash { - t.Fatalf("client recipe block ID = %d, want hash %d", got, stoneHash) - } - if got := uint32(backendRecipe.Output[0].BlockRuntimeID); got != stoneRID { - t.Fatalf("retained backend recipe block ID = %d, want runtime ID %d", got, stoneRID) - } -} From 8fa0826bacba87e21ffd5798693630da61719a81 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 14 Jul 2026 00:51:30 -0400 Subject: [PATCH 13/16] refactor: require consistent block network modes --- integration/proxy/proxy.go | 7 +- integration/proxy/proxy_test.go | 2 +- player/block_network_packet.go | 181 ---------------- player/component/acknowledgement/chunks.go | 6 +- player/component/combat.go | 2 +- player/component/world.go | 8 +- player/items.go | 6 +- player/network.go | 35 ++-- player/packet.go | 230 +-------------------- player/player.go | 10 +- player/world.go | 21 +- world/cache.go | 36 +--- 12 files changed, 50 insertions(+), 494 deletions(-) delete mode 100644 player/block_network_packet.go diff --git a/integration/proxy/proxy.go b/integration/proxy/proxy.go index 2bc55d89..9851e527 100644 --- a/integration/proxy/proxy.go +++ b/integration/proxy/proxy.go @@ -285,8 +285,13 @@ func (s *session) transfer(ctx context.Context, address string) (bool, error) { } s.routeMu.Lock() + state, err := s.player.TransferServerConn(backend) + if err != nil { + s.routeMu.Unlock() + _ = backend.Close() + return false, err + } old := s.swapBackend(backend) - state := s.player.TransferServerConn(backend) err = s.resetTransferState(state) s.routeMu.Unlock() _ = old.Close() diff --git a/integration/proxy/proxy_test.go b/integration/proxy/proxy_test.go index 0e939a7c..b0278954 100644 --- a/integration/proxy/proxy_test.go +++ b/integration/proxy/proxy_test.go @@ -207,7 +207,7 @@ func TestTransferResetSynchronizesWithPlayerTick(t *testing.T) { p.SetServerConn(backend) go p.StartTicking() for i := 0; i < 100; i++ { - p.TransferServerConn(&fakeBackend{data: minecraft.GameData{EntityRuntimeID: uint64(i + 2)}}) + _, _ = p.TransferServerConn(&fakeBackend{data: minecraft.GameData{EntityRuntimeID: uint64(i + 2)}}) } _ = p.Close() select { diff --git a/player/block_network_packet.go b/player/block_network_packet.go deleted file mode 100644 index 337e9e9b..00000000 --- a/player/block_network_packet.go +++ /dev/null @@ -1,181 +0,0 @@ -package player - -import ( - "github.com/oomph-ac/oomph/world/blocknetwork" - "github.com/sandertv/gophertunnel/minecraft/protocol" - "github.com/sandertv/gophertunnel/minecraft/protocol/packet" -) - -const fallingBlockEntityType = "minecraft:falling_block" - -func translateBlockNetworkID(id uint32, translator blocknetwork.Translator) (uint32, bool) { - translated := translator.Translate(id) - return translated, translated != id -} - -func rewriteBlockNetworkID(id *uint32, translator blocknetwork.Translator) bool { - translated, modified := translateBlockNetworkID(*id, translator) - if modified { - *id = translated - } - return modified -} - -func rewriteStackBlockNetworkID(stack *protocol.ItemStack, translator blocknetwork.Translator) bool { - if stack.BlockRuntimeID == 0 { - return false - } - translated, modified := translateBlockNetworkID(uint32(stack.BlockRuntimeID), translator) - if modified { - stack.BlockRuntimeID = int32(translated) - } - return modified -} - -func rewriteStacksBlockNetworkIDs(stacks []protocol.ItemStack, translator blocknetwork.Translator) ([]protocol.ItemStack, bool) { - var rewritten []protocol.ItemStack - for i := range stacks { - stack := stacks[i] - if !rewriteStackBlockNetworkID(&stack, translator) { - continue - } - if rewritten == nil { - rewritten = append([]protocol.ItemStack(nil), stacks...) - } - rewritten[i] = stack - } - return rewritten, rewritten != nil -} - -func rewriteShapedRecipeBlockNetworkIDs(recipe protocol.ShapedRecipe, translator blocknetwork.Translator) (protocol.ShapedRecipe, bool) { - output, modified := rewriteStacksBlockNetworkIDs(recipe.Output, translator) - if modified { - recipe.Output = output - } - return recipe, modified -} - -func rewriteShapelessRecipeBlockNetworkIDs(recipe protocol.ShapelessRecipe, translator blocknetwork.Translator) (protocol.ShapelessRecipe, bool) { - output, modified := rewriteStacksBlockNetworkIDs(recipe.Output, translator) - if modified { - recipe.Output = output - } - return recipe, modified -} - -func rewriteRecipeBlockNetworkIDs(recipe protocol.Recipe, translator blocknetwork.Translator) (protocol.Recipe, bool) { - switch recipe := recipe.(type) { - case *protocol.ShapedRecipe: - clientRecipe, modified := rewriteShapedRecipeBlockNetworkIDs(*recipe, translator) - if modified { - return &clientRecipe, true - } - case *protocol.ShulkerBoxRecipe: - output, modified := rewriteShapelessRecipeBlockNetworkIDs(recipe.ShapelessRecipe, translator) - if modified { - clientRecipe := *recipe - clientRecipe.ShapelessRecipe = output - return &clientRecipe, true - } - case *protocol.ShapelessChemistryRecipe: - output, modified := rewriteShapelessRecipeBlockNetworkIDs(recipe.ShapelessRecipe, translator) - if modified { - clientRecipe := *recipe - clientRecipe.ShapelessRecipe = output - return &clientRecipe, true - } - case *protocol.ShapedChemistryRecipe: - output, modified := rewriteShapedRecipeBlockNetworkIDs(recipe.ShapedRecipe, translator) - if modified { - clientRecipe := *recipe - clientRecipe.ShapedRecipe = output - return &clientRecipe, true - } - case *protocol.ShapelessRecipe: - clientRecipe, modified := rewriteShapelessRecipeBlockNetworkIDs(*recipe, translator) - if modified { - return &clientRecipe, true - } - case *protocol.SmithingTransformRecipe: - clientRecipe := *recipe - if rewriteStackBlockNetworkID(&clientRecipe.Result, translator) { - return &clientRecipe, true - } - } - return recipe, false -} - -func rewriteLevelEventBlockNetworkID(pk *packet.LevelEvent, translator blocknetwork.Translator) bool { - switch pk.EventType { - case packet.LevelEventParticlesDestroyBlock, packet.LevelEventParticlesDestroyBlockNoSound: - translated, modified := translateBlockNetworkID(uint32(pk.EventData), translator) - if modified { - pk.EventData = int32(translated) - } - return modified - default: - return false - } -} - -func rewriteLevelSoundBlockNetworkID(pk *packet.LevelSoundEvent, translator blocknetwork.Translator) bool { - if !levelSoundUsesBlockNetworkID(pk.SoundType) { - return false - } - translated, modified := translateBlockNetworkID(uint32(pk.ExtraData), translator) - if modified { - pk.ExtraData = int32(translated) - } - return modified -} - -func levelSoundUsesBlockNetworkID(soundType string) bool { - switch soundType { - case packet.SoundEventItemUseOn, - packet.SoundEventHit, - packet.SoundEventStep, - packet.SoundEventBreak, - packet.SoundEventPlace, - packet.SoundEventLand, - packet.SoundEventPressurePlateClickOff, - packet.SoundEventPressurePlateClickOn, - packet.SoundEventDoorOpen, - packet.SoundEventDoorClose, - packet.SoundEventTrapdoorOpen, - packet.SoundEventTrapdoorClose, - packet.SoundEventFenceGateOpen, - packet.SoundEventFenceGateClose: - return true - default: - return false - } -} - -func rewriteActorBlockMetadata(metadata map[uint32]any, translator blocknetwork.Translator, fallingBlock bool) (map[uint32]any, bool) { - keys := []uint32{protocol.EntityDataKeyDisplayTileRuntimeID, protocol.EntityDataKeyCarryBlockRuntimeID} - if fallingBlock { - keys = append(keys, protocol.EntityDataKeyVariant) - } - var clientMetadata map[uint32]any - for _, key := range keys { - value, ok := metadata[key].(int32) - if !ok { - continue - } - translated := int32(translator.Translate(uint32(value))) - if translated == value { - continue - } - if clientMetadata == nil { - clientMetadata = make(map[uint32]any, len(metadata)) - for key, value := range metadata { - clientMetadata[key] = value - } - } - clientMetadata[key] = translated - } - if clientMetadata == nil { - return metadata, false - } - return clientMetadata, true -} diff --git a/player/component/acknowledgement/chunks.go b/player/component/acknowledgement/chunks.go index e790ac83..e2b01284 100644 --- a/player/component/acknowledgement/chunks.go +++ b/player/component/acknowledgement/chunks.go @@ -29,7 +29,7 @@ func (ack *ChunkUpdate) Run() { ack.mPlayer.Disconnect(game.ErrorChunkCacheUnsupported) return } - cInfo, err := oworld.CacheChunk(ack.pk, ack.mPlayer.BackendBlockNetwork()) + cInfo, err := oworld.CacheChunk(ack.pk, ack.mPlayer.BlockNetwork()) if err != nil { ack.mPlayer.Disconnect(fmt.Sprintf(game.ErrorInternalDecodeChunk, err)) return @@ -93,13 +93,13 @@ func (ack *SubChunkUpdate) Run() { bufUsed = true buf.Write(entry.RawPayload) - cachedSub, err := oworld.CacheSubChunk(buf, ch, chunkPos, ack.mPlayer.BackendBlockNetwork()) + cachedSub, err := oworld.CacheSubChunk(buf, ch, chunkPos, ack.mPlayer.BlockNetwork()) if err != nil { ack.mPlayer.Disconnect(fmt.Sprintf(game.ErrorInternalDecodeChunk, err)) continue } ch.Sub()[cachedSub.Layer()] = cachedSub.SubChunk() - ack.mPlayer.World().AddSubChunk(chunkPos, cachedSub.Hash(), ack.mPlayer.BackendBlockNetwork()) + ack.mPlayer.World().AddSubChunk(chunkPos, cachedSub.Hash(), ack.mPlayer.BlockNetwork()) ack.mPlayer.Dbg.Notify(player.DebugModeChunks, true, "cached subchunk %d at %v", cachedSub.Layer(), chunkPos) case protocol.SubChunkResultSuccessAllAir: ack.mPlayer.Dbg.Notify(player.DebugModeChunks, true, "all-air chunk at %v", chunkPos) diff --git a/player/component/combat.go b/player/component/combat.go index 067bab3f..8cc5d989 100644 --- a/player/component/combat.go +++ b/player/component/combat.go @@ -517,7 +517,7 @@ func (c *AuthoritativeCombatComponent) checkForMispredictedEntity() bool { var newItem protocol.ItemInstance if c.mPlayer.LastEquipmentData != nil { - newItem = c.mPlayer.ClientItemForBackend(c.mPlayer.LastEquipmentData.NewItem) + newItem = c.mPlayer.LastEquipmentData.NewItem } c.attackInput = &packet.InventoryTransaction{ diff --git a/player/component/world.go b/player/component/world.go index 51f5781d..5622f20c 100644 --- a/player/component/world.go +++ b/player/component/world.go @@ -77,7 +77,7 @@ func (c *WorldUpdaterComponent) HandleUpdateBlock(pk *packet.UpdateBlock) { c.mPlayer.Log().Debug("unsupported layer update block", "layer", pk.Layer, "block", pk.NewBlockRuntimeID, "pos", pos) return } - c.AddPendingUpdate(pos, c.mPlayer.BlockRuntimeIDFromBackend(pk.NewBlockRuntimeID)) + c.AddPendingUpdate(pos, c.mPlayer.DecodeBlockRuntimeID(pk.NewBlockRuntimeID)) } // HandleUpdateSubChunkBlocks handles an UpdateSubChunkBlocks packet from the server. @@ -86,10 +86,10 @@ func (c *WorldUpdaterComponent) HandleUpdateSubChunkBlocks(pk *packet.UpdateSubC c.mPlayer.ACKs().Add(acknowledgement.NewPlayerInitalizedACK(c.mPlayer)) } for _, entry := range pk.Blocks { - c.AddPendingUpdate(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, c.mPlayer.BlockRuntimeIDFromBackend(entry.BlockRuntimeID)) + c.AddPendingUpdate(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, c.mPlayer.DecodeBlockRuntimeID(entry.BlockRuntimeID)) } for _, entry := range pk.Extra { - c.AddPendingUpdate(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, c.mPlayer.BlockRuntimeIDFromBackend(entry.BlockRuntimeID)) + c.AddPendingUpdate(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, c.mPlayer.DecodeBlockRuntimeID(entry.BlockRuntimeID)) } } @@ -188,7 +188,7 @@ func (c *WorldUpdaterComponent) AttemptItemInteractionWithBlock(pk *packet.Inven case *block.Air: // This only happens when Dragonfly is unsure of what the item is (unregistered), so we use the client-authoritative block in hand. c.mPlayer.Dbg.Notify(player.DebugModeBlockPlacement, true, "called c.mPlayer.PlaceBlock: using client-authoritative block in hand") - if b, ok := df_world.BlockByRuntimeID(c.mPlayer.BlockRuntimeIDFromClient(uint32(dat.HeldItem.Stack.BlockRuntimeID))); ok { + if b, ok := df_world.BlockByRuntimeID(c.mPlayer.DecodeBlockRuntimeID(uint32(dat.HeldItem.Stack.BlockRuntimeID))); ok { c.mPlayer.Dbg.Notify(player.DebugModeBlockPlacement, true, "placing block with runtime ID: %d", dat.HeldItem.Stack.BlockRuntimeID) // If the block at the position is not replacable, we want to place the block on the side of the block. diff --git a/player/items.go b/player/items.go index 2223f27f..96e70acb 100644 --- a/player/items.go +++ b/player/items.go @@ -19,7 +19,7 @@ func (p *Player) ConvertToStack(it protocol.ItemStack) item.Stack { } } if it.BlockRuntimeID != 0 { - b, _ := p.World().BlockRegistry().BlockByRuntimeID(p.BlockRuntimeIDFromBackend(uint32(it.BlockRuntimeID))) + b, _ := p.World().BlockRegistry().BlockByRuntimeID(p.DecodeBlockRuntimeID(uint32(it.BlockRuntimeID))) if t, ok = b.(world.Item); !ok { t = block.Air{} } @@ -34,14 +34,14 @@ func (p *Player) ConvertToStack(it protocol.ItemStack) item.Stack { func (p *Player) InstanceFromItem(it item.Stack) protocol.ItemInstance { instance := utils.InstanceFromItem(p.World().BlockRegistry(), it) if instance.Stack.BlockRuntimeID != 0 { - instance.Stack.BlockRuntimeID = int32(p.BlockRuntimeIDToClient(uint32(instance.Stack.BlockRuntimeID))) + instance.Stack.BlockRuntimeID = int32(p.EncodeBlockRuntimeID(uint32(instance.Stack.BlockRuntimeID))) } return instance } func (p *Player) StackToItem(it protocol.ItemStack) item.Stack { if it.BlockRuntimeID != 0 { - it.BlockRuntimeID = int32(p.BlockRuntimeIDFromBackend(uint32(it.BlockRuntimeID))) + it.BlockRuntimeID = int32(p.DecodeBlockRuntimeID(uint32(it.BlockRuntimeID))) } return utils.StackToItem(p.World().BlockRegistry(), it) } diff --git a/player/network.go b/player/network.go index 0ebe1bf2..f8692785 100755 --- a/player/network.go +++ b/player/network.go @@ -2,6 +2,7 @@ package player import ( "context" + "fmt" "net" "time" @@ -42,16 +43,15 @@ func (p *Player) SetServerConn(conn ServerConn) { return } - backendBlockNetwork := blocknetwork.NewCodec(p.World().BlockRegistry(), blocknetwork.ModeFromHashes(conn.GameData().UseBlockNetworkIDHashes)) + blockNetwork := blocknetwork.NewCodec(p.World().BlockRegistry(), blocknetwork.ModeFromHashes(conn.GameData().UseBlockNetworkIDHashes)) if p.serverConn == nil { - p.clientBlockNetwork = backendBlockNetwork + p.blockNetwork = blockNetwork for _, item := range conn.GameData().Items { if i, ok := world.ItemByName(item.Name, 0); ok { p.items[item.RuntimeID] = i } } } - p.backendBlockNetwork = backendBlockNetwork p.GameDat = conn.GameData() p.serverConn = conn @@ -67,24 +67,9 @@ func (p *Player) SetServerConn(conn ServerConn) { p.movement.ResetTransferState(p.GameDat.PlayerPosition) } -// ClientBlockNetwork returns the codec for block IDs visible to the client. -func (p *Player) ClientBlockNetwork() blocknetwork.Codec { - return p.clientBlockNetwork -} - -// BackendBlockNetwork returns the codec for block IDs used by the current backend. -func (p *Player) BackendBlockNetwork() blocknetwork.Codec { - return p.backendBlockNetwork -} - -// ClientToBackendBlockNetwork returns a translator for client-originated block IDs. -func (p *Player) ClientToBackendBlockNetwork() blocknetwork.Translator { - return blocknetwork.NewTranslator(p.clientBlockNetwork, p.backendBlockNetwork) -} - -// BackendToClientBlockNetwork returns a translator for backend-originated block IDs. -func (p *Player) BackendToClientBlockNetwork() blocknetwork.Translator { - return blocknetwork.NewTranslator(p.backendBlockNetwork, p.clientBlockNetwork) +// BlockNetwork returns the codec shared by the client and every backend in this session. +func (p *Player) BlockNetwork() blocknetwork.Codec { + return p.blockNetwork } // BackendTransferState contains client-visible state that must be cleared when @@ -96,11 +81,15 @@ type BackendTransferState struct { // TransferServerConn atomically installs a backend and clears state owned by // the previous backend. It uses the same processing lock as packet handling and // Tick, so no component can observe a partially reset transfer. -func (p *Player) TransferServerConn(conn ServerConn) BackendTransferState { +func (p *Player) TransferServerConn(conn ServerConn) (BackendTransferState, error) { p.procMu.Lock() defer p.procMu.Unlock() state := BackendTransferState{EffectIDs: make([]int32, 0, len(p.effects.All()))} + targetMode := blocknetwork.ModeFromHashes(conn.GameData().UseBlockNetworkIDHashes) + if targetMode != p.blockNetwork.Mode() { + return state, fmt.Errorf("backend block-hash setting %t does not match session setting %t", targetMode == blocknetwork.Hashes, p.blockNetwork.Mode() == blocknetwork.Hashes) + } for effectID := range p.effects.All() { state.EffectIDs = append(state.EffectIDs, effectID) } @@ -115,7 +104,7 @@ func (p *Player) TransferServerConn(conn ServerConn) BackendTransferState { p.effects.RemoveAll() p.combat.Reset() p.clientCombat.Reset() - return state + return state, nil } // ChunkRadius returns the chunk radius as requested by the client at the other end of the conn. diff --git a/player/packet.go b/player/packet.go index e9e085ef..0d758f0b 100644 --- a/player/packet.go +++ b/player/packet.go @@ -5,7 +5,6 @@ import ( "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/item" - "github.com/df-mc/dragonfly/server/world" "github.com/oomph-ac/oomph/entity" "github.com/oomph-ac/oomph/game" "github.com/oomph-ac/oomph/oconfig" @@ -37,19 +36,13 @@ var ClientDecode = []uint32{ var ServerDecode = []uint32{ packet.IDAddActor, - packet.IDAddItemActor, packet.IDAddPlayer, packet.IDChunkRadiusUpdated, packet.IDInventorySlot, packet.IDInventoryContent, - packet.IDInventoryTransaction, packet.IDItemStackResponse, - packet.IDLevelEvent, packet.IDLevelChunk, - packet.IDLevelSoundEvent, packet.IDMobEffect, - packet.IDMobEquipment, - packet.IDMobArmourEquipment, packet.IDMoveActorAbsolute, packet.IDMovePlayer, packet.IDRemoveActor, @@ -60,7 +53,6 @@ var ServerDecode = []uint32{ packet.IDUpdateAbilities, packet.IDUpdateAttributes, packet.IDUpdateBlock, - packet.IDUpdateBlockSynced, packet.IDUpdateSubChunkBlocks, packet.IDContainerOpen, packet.IDContainerClose, @@ -262,7 +254,7 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { ctx.Cancel() } case *packet.MobEquipment: - p.retainClientEquipment(pk) + p.LastEquipmentData = pk if pk.WindowID == protocol.WindowIDInventory { p.inventory.SetHeldSlot(int32(pk.HotBarSlot)) } @@ -283,74 +275,6 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { } } p.RunDetections(pk) - if p.ClientToBackendBlockNetwork().Required() && p.rewriteClientBlockNetworkIDs(pk) { - ctx.SetModified() - } -} - -func (p *Player) rewriteClientBlockNetworkIDs(pk packet.Packet) bool { - translator := p.ClientToBackendBlockNetwork() - rewriteCraftResults := func(actions []protocol.StackRequestAction) bool { - modified := false - for _, action := range actions { - if action, ok := action.(*protocol.CraftResultsDeprecatedStackRequestAction); ok { - for i := range action.ResultItems { - modified = rewriteStackBlockNetworkID(&action.ResultItems[i], translator) || modified - } - } - } - return modified - } - switch pk := pk.(type) { - case *packet.PlayerAuthInput: - modified := false - if pk.InputData.Load(packet.InputFlagPerformItemInteraction) { - changed := rewriteBlockNetworkID(&pk.ItemInteractionData.BlockRuntimeID, translator) - modified = rewriteStackBlockNetworkID(&pk.ItemInteractionData.HeldItem.Stack, translator) || changed - } - if pk.InputData.Load(packet.InputFlagPerformItemStackRequest) { - modified = rewriteCraftResults(pk.ItemStackRequest.Actions) || modified - } - return modified - case *packet.ItemStackRequest: - modified := false - for i := range pk.Requests { - modified = rewriteCraftResults(pk.Requests[i].Actions) || modified - } - return modified - case *packet.InventoryTransaction: - modified := false - for i := range pk.Actions { - modified = rewriteStackBlockNetworkID(&pk.Actions[i].OldItem.Stack, translator) || modified - modified = rewriteStackBlockNetworkID(&pk.Actions[i].NewItem.Stack, translator) || modified - } - switch data := pk.TransactionData.(type) { - case *protocol.UseItemTransactionData: - changed := rewriteBlockNetworkID(&data.BlockRuntimeID, translator) - modified = rewriteStackBlockNetworkID(&data.HeldItem.Stack, translator) || modified || changed - case *protocol.UseItemOnEntityTransactionData: - modified = rewriteStackBlockNetworkID(&data.HeldItem.Stack, translator) || modified - case *protocol.ReleaseItemTransactionData: - modified = rewriteStackBlockNetworkID(&data.HeldItem.Stack, translator) || modified - } - return modified - case *packet.MobEquipment: - return rewriteStackBlockNetworkID(&pk.NewItem.Stack, translator) - case *packet.LevelSoundEvent: - return rewriteLevelSoundBlockNetworkID(pk, translator) - } - return false -} - -func (p *Player) retainClientEquipment(pk *packet.MobEquipment) { - retained := *pk - p.LastEquipmentData = &retained -} - -// ClientItemForBackend translates an item retained in the client's block network mode to the current backend mode. -func (p *Player) ClientItemForBackend(instance protocol.ItemInstance) protocol.ItemInstance { - rewriteStackBlockNetworkID(&instance.Stack, p.ClientToBackendBlockNetwork()) - return instance } // splitCommandLine splits a command line into arguments, preserving quoted substrings @@ -479,8 +403,8 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { case *packet.LevelChunk: p.worldUpdater.HandleLevelChunk(pk) fullChunk := !pk.CacheEnabled && pk.SubChunkCount != protocol.SubChunkRequestModeLimited && pk.SubChunkCount != protocol.SubChunkRequestModeLimitless - if fullChunk && (p.opts.Network.AttemptFixChunks || p.BackendToClientBlockNetwork().Required()) { - if err := oworld.ReencodeLevelChunk(pk, p.BackendBlockNetwork(), p.ClientBlockNetwork()); err != nil { + if fullChunk && p.opts.Network.AttemptFixChunks { + if err := oworld.ReencodeLevelChunk(pk, p.BlockNetwork()); err != nil { p.Log().Warn("unable to re-encode chunk", "error", err) } else { ctx.SetModified() @@ -537,25 +461,6 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { p.gamemodeHandle.Handle(pk) case *packet.SubChunk: p.worldUpdater.HandleSubChunk(pk) - if !pk.CacheEnabled && p.BackendToClientBlockNetwork().Required() { - dimension, ok := world.DimensionByID(int(pk.Dimension)) - if !ok { - dimension = world.Overworld - } - for i := range pk.SubChunkEntries { - entry := &pk.SubChunkEntries[i] - if entry.Result != protocol.SubChunkResultSuccess { - continue - } - payload, err := oworld.ReencodeSubChunk(entry.RawPayload, dimension, p.BackendBlockNetwork(), p.ClientBlockNetwork()) - if err != nil { - p.Log().Warn("unable to re-encode subchunk", "error", err) - continue - } - entry.RawPayload = payload - ctx.SetModified() - } - } case *packet.UpdateAbilities: if pk.AbilityData.EntityUniqueID == p.UniqueId { p.movement.ServerUpdate(pk) @@ -569,36 +474,8 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { } case *packet.UpdateBlock: p.worldUpdater.HandleUpdateBlock(pk) - if p.BackendToClientBlockNetwork().Required() { - if rewriteBlockNetworkID(&pk.NewBlockRuntimeID, p.BackendToClientBlockNetwork()) { - ctx.SetModified() - } - } - case *packet.UpdateBlockSynced: - if p.BackendToClientBlockNetwork().Required() { - if rewriteBlockNetworkID(&pk.NewBlockRuntimeID, p.BackendToClientBlockNetwork()) { - ctx.SetModified() - } - } case *packet.UpdateSubChunkBlocks: p.worldUpdater.HandleUpdateSubChunkBlocks(pk) - if p.BackendToClientBlockNetwork().Required() { - translator := p.BackendToClientBlockNetwork() - modified := false - for i := range pk.Blocks { - if rewriteBlockNetworkID(&pk.Blocks[i].BlockRuntimeID, translator) { - modified = true - } - } - for i := range pk.Extra { - if rewriteBlockNetworkID(&pk.Extra[i].BlockRuntimeID, translator) { - modified = true - } - } - if modified { - ctx.SetModified() - } - } case *packet.ContainerOpen: p.inventory.CreateWindow(pk.WindowID, pk.ContainerType) case *packet.ContainerClose: @@ -627,105 +504,4 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { p.CreativeItems[item.CreativeItemNetworkID] = item } } - if p.BackendToClientBlockNetwork().Required() && p.rewriteServerBlockNetworkIDs(pk) { - ctx.SetModified() - } -} - -func (p *Player) rewriteServerBlockNetworkIDs(pk packet.Packet) bool { - translator := p.BackendToClientBlockNetwork() - switch pk := pk.(type) { - case *packet.InventorySlot: - modified := rewriteStackBlockNetworkID(&pk.NewItem.Stack, translator) - if storageItem, ok := pk.StorageItem.Value(); ok { - if rewriteStackBlockNetworkID(&storageItem.Stack, translator) { - pk.StorageItem = protocol.Option(storageItem) - modified = true - } - } - return modified - case *packet.InventoryContent: - // The inventory ACK retains the original slice for later processing in the backend's ID mode. - modified := false - for i := range pk.Content { - stack := pk.Content[i].Stack - if !rewriteStackBlockNetworkID(&stack, translator) { - continue - } - if !modified { - pk.Content = append([]protocol.ItemInstance(nil), pk.Content...) - } - pk.Content[i].Stack = stack - modified = true - } - return rewriteStackBlockNetworkID(&pk.StorageItem.Stack, translator) || modified - case *packet.MobEquipment: - return rewriteStackBlockNetworkID(&pk.NewItem.Stack, translator) - case *packet.MobArmourEquipment: - modified := rewriteStackBlockNetworkID(&pk.Helmet.Stack, translator) - modified = rewriteStackBlockNetworkID(&pk.Chestplate.Stack, translator) || modified - modified = rewriteStackBlockNetworkID(&pk.Leggings.Stack, translator) || modified - modified = rewriteStackBlockNetworkID(&pk.Boots.Stack, translator) || modified - return rewriteStackBlockNetworkID(&pk.Body.Stack, translator) || modified - case *packet.AddPlayer: - modified := rewriteStackBlockNetworkID(&pk.HeldItem.Stack, translator) - metadata, metadataModified := rewriteActorBlockMetadata(pk.EntityMetadata, translator, false) - pk.EntityMetadata = metadata - return modified || metadataModified - case *packet.AddActor: - metadata, modified := rewriteActorBlockMetadata(pk.EntityMetadata, translator, pk.EntityType == fallingBlockEntityType) - pk.EntityMetadata = metadata - return modified - case *packet.SetActorData: - fallingBlock := false - if p.entTracker != nil { - if actor := p.entTracker.FindEntity(pk.EntityRuntimeID); actor != nil { - fallingBlock = actor.Type == fallingBlockEntityType - } - } - metadata, modified := rewriteActorBlockMetadata(pk.EntityMetadata, translator, fallingBlock) - pk.EntityMetadata = metadata - return modified - case *packet.AddItemActor: - return rewriteStackBlockNetworkID(&pk.Item.Stack, translator) - case *packet.LevelEvent: - return rewriteLevelEventBlockNetworkID(pk, translator) - case *packet.LevelSoundEvent: - return rewriteLevelSoundBlockNetworkID(pk, translator) - case *packet.CreativeContent: - modified := false - for i := range pk.Groups { - modified = rewriteStackBlockNetworkID(&pk.Groups[i].Icon, translator) || modified - } - for i := range pk.Items { - modified = rewriteStackBlockNetworkID(&pk.Items[i].Item, translator) || modified - } - return modified - case *packet.InventoryTransaction: - modified := false - for i := range pk.Actions { - modified = rewriteStackBlockNetworkID(&pk.Actions[i].OldItem.Stack, translator) || modified - modified = rewriteStackBlockNetworkID(&pk.Actions[i].NewItem.Stack, translator) || modified - } - switch data := pk.TransactionData.(type) { - case *protocol.UseItemTransactionData: - changed := rewriteBlockNetworkID(&data.BlockRuntimeID, translator) - modified = rewriteStackBlockNetworkID(&data.HeldItem.Stack, translator) || modified || changed - case *protocol.UseItemOnEntityTransactionData: - modified = rewriteStackBlockNetworkID(&data.HeldItem.Stack, translator) || modified - case *protocol.ReleaseItemTransactionData: - modified = rewriteStackBlockNetworkID(&data.HeldItem.Stack, translator) || modified - } - return modified - case *packet.CraftingData: - modified := false - for i, recipe := range pk.Recipes { - if clientRecipe, changed := rewriteRecipeBlockNetworkIDs(recipe, translator); changed { - pk.Recipes[i] = clientRecipe - modified = true - } - } - return modified - } - return false } diff --git a/player/player.go b/player/player.go index ebff3fa2..8124bbbf 100755 --- a/player/player.go +++ b/player/player.go @@ -72,10 +72,9 @@ type Player struct { GameDat minecraft.GameData Version int32 - // clientBlockNetwork is fixed by the initial backend's StartGame. It remains unchanged across fast backend - // transfers because the connected client does not receive another StartGame packet. - clientBlockNetwork blocknetwork.Codec - backendBlockNetwork blocknetwork.Codec + // blockNetwork is fixed by the initial backend's StartGame. All backends reachable through an instant transfer + // must use the same block-network representation because the client does not receive another StartGame packet. + blockNetwork blocknetwork.Codec // With fast transfers, the client will still retain it's original runtime and unique IDs, so // we must translate them to new ones, while still retaining the old ones for the client to use. @@ -255,8 +254,7 @@ func New(log *slog.Logger, mState MonitoringState, listener *minecraft.Listener) listener: listener, - clientBlockNetwork: blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.RuntimeIDs), - backendBlockNetwork: blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.RuntimeIDs), + blockNetwork: blocknetwork.NewCodec(world.BlockRegistry, blocknetwork.RuntimeIDs), remoteEventFunc: func(e RemoteEvent, p *Player) { enc, _ := json.Marshal(e) diff --git a/player/world.go b/player/world.go index d7284684..bd224ccf 100644 --- a/player/world.go +++ b/player/world.go @@ -72,21 +72,16 @@ func (p *Player) World() *oworld.World { return p.world } -// BlockRuntimeIDFromBackend converts a backend block ID to Oomph's canonical registry runtime ID. +// DecodeBlockRuntimeID converts a network block ID to Oomph's canonical registry runtime ID. // Unknown values are preserved so callers can retain their existing fallback. -func (p *Player) BlockRuntimeIDFromBackend(id uint32) uint32 { - return blockRuntimeIDFromNetwork(p.backendBlockNetwork, id) +func (p *Player) DecodeBlockRuntimeID(id uint32) uint32 { + return blockRuntimeIDFromNetwork(p.blockNetwork, id) } -// BlockRuntimeIDToClient converts a canonical registry runtime ID to the client's block ID representation. +// EncodeBlockRuntimeID converts a canonical registry runtime ID to the session's network representation. // Unknown values are preserved so custom block fallbacks remain intact. -func (p *Player) BlockRuntimeIDToClient(id uint32) uint32 { - return blockRuntimeIDToNetwork(p.clientBlockNetwork, id) -} - -// BlockRuntimeIDFromClient converts a client-visible block ID to Oomph's registry runtime ID. -func (p *Player) BlockRuntimeIDFromClient(id uint32) uint32 { - return blockRuntimeIDFromNetwork(p.clientBlockNetwork, id) +func (p *Player) EncodeBlockRuntimeID(id uint32) uint32 { + return blockRuntimeIDToNetwork(p.blockNetwork, id) } func blockRuntimeIDFromNetwork(codec blocknetwork.Codec, id uint32) uint32 { @@ -132,7 +127,7 @@ func (p *Player) SyncBlock(pos df_cube.Pos) { int32(pos[1]), int32(pos[2]), }, - NewBlockRuntimeID: p.BlockRuntimeIDToClient(blockRuntimeID), + NewBlockRuntimeID: p.EncodeBlockRuntimeID(blockRuntimeID), Flags: packet.BlockUpdateNetwork, Layer: 0, // TODO: Implement and account for multi-layer blocks. } @@ -199,7 +194,7 @@ func (p *Player) SendBlockUpdates(positions []protocol.BlockPos) { for _, pos := range positions { p.SendPacketToClient(&packet.UpdateBlock{ Position: pos, - NewBlockRuntimeID: p.BlockRuntimeIDToClient(world.BlockRuntimeID(p.World().Block(df_cube.Pos{ + NewBlockRuntimeID: p.EncodeBlockRuntimeID(world.BlockRuntimeID(p.World().Block(df_cube.Pos{ int(pos.X()), int(pos.Y()), int(pos.Z()), diff --git a/world/cache.go b/world/cache.go index 8a8221a4..18135674 100644 --- a/world/cache.go +++ b/world/cache.go @@ -117,9 +117,9 @@ func CacheChunk(input *packet.LevelChunk, codec blocknetwork.Codec) (ChunkInfo, return ChunkInfo{Hash: key.hash, blockNetworkMode: codec.Mode(), Chunk: cachedChunk.chunk, Cached: true}, nil } -// ReencodeLevelChunk converts the block palettes in input between network ID representations while preserving trailing -// block entity data. -func ReencodeLevelChunk(input *packet.LevelChunk, source, target blocknetwork.Codec) error { +// ReencodeLevelChunk fully re-encodes the block palettes in input while preserving the session's block-network +// representation and trailing block entity data. +func ReencodeLevelChunk(input *packet.LevelChunk, codec blocknetwork.Codec) error { dimension, ok := world.DimensionByID(int(input.Dimension)) if !ok { return fmt.Errorf("unknown dimension %v", input.Dimension) @@ -129,11 +129,11 @@ func ReencodeLevelChunk(input *packet.LevelChunk, source, target blocknetwork.Co if err != nil { return err } - if source.Mode() == blocknetwork.Hashes { + if codec.Mode() == blocknetwork.Hashes { decoded.ConvertBlockNetworkHashesToRuntimeIDs() } var data chunk.SerialisedData - if target.Mode() == blocknetwork.Hashes { + if codec.Mode() == blocknetwork.Hashes { data = chunk.EncodeWithBlockNetworkHashes(decoded) } else { data = chunk.Encode(decoded, chunk.NetworkEncoding) @@ -149,32 +149,6 @@ func ReencodeLevelChunk(input *packet.LevelChunk, source, target blocknetwork.Co return nil } -// ReencodeSubChunk converts one successful SubChunk entry between network ID representations while preserving trailing -// block entity data. -func ReencodeSubChunk(payload []byte, dimension world.Dimension, source, target blocknetwork.Codec) ([]byte, error) { - buf := bytes.NewBuffer(payload) - decodedChunk := chunk.New(BlockRegistry, dimension.Range()) - var index byte - decoded, err := decodeSubChunk(buf, decodedChunk, &index, chunk.NetworkEncoding) - if err != nil { - return nil, err - } - if source.Mode() == blocknetwork.Hashes { - decoded.ConvertBlockNetworkHashesToRuntimeIDs(BlockRegistry) - } - if int(index) >= len(decodedChunk.Sub()) { - return nil, fmt.Errorf("subchunk index %d out of range", index) - } - decodedChunk.Sub()[index] = decoded - var encoded []byte - if target.Mode() == blocknetwork.Hashes { - encoded = chunk.EncodeSubChunkWithBlockNetworkHashes(decodedChunk, int(index)) - } else { - encoded = chunk.EncodeSubChunk(decodedChunk, chunk.NetworkEncoding, int(index)) - } - return append(encoded, buf.Bytes()...), nil -} - type CachedSubChunk struct { layer byte hash xxh3.Uint128 From bbc4175ebb375223ce368db0abb6e23dc3694fed Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 14 Jul 2026 00:57:35 -0400 Subject: [PATCH 14/16] refactor: simplify block cache keys --- player/component/acknowledgement/chunks.go | 2 +- world/cache.go | 43 ++++++++++------------ world/world.go | 24 ++++++------ 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/player/component/acknowledgement/chunks.go b/player/component/acknowledgement/chunks.go index e2b01284..794c36c9 100644 --- a/player/component/acknowledgement/chunks.go +++ b/player/component/acknowledgement/chunks.go @@ -99,7 +99,7 @@ func (ack *SubChunkUpdate) Run() { continue } ch.Sub()[cachedSub.Layer()] = cachedSub.SubChunk() - ack.mPlayer.World().AddSubChunk(chunkPos, cachedSub.Hash(), ack.mPlayer.BlockNetwork()) + ack.mPlayer.World().AddSubChunk(chunkPos, cachedSub.Hash()) ack.mPlayer.Dbg.Notify(player.DebugModeChunks, true, "cached subchunk %d at %v", cachedSub.Layer(), chunkPos) case protocol.SubChunkResultSuccessAllAir: ack.mPlayer.Dbg.Notify(player.DebugModeChunks, true, "all-air chunk at %v", chunkPos) diff --git a/world/cache.go b/world/cache.go index 18135674..471e5358 100644 --- a/world/cache.go +++ b/world/cache.go @@ -15,40 +15,35 @@ import ( ) var ( - chunkCache = make(map[blockCacheKey]*CachedChunk) + chunkCache = make(map[xxh3.Uint128]*CachedChunk) cMu sync.Mutex - subChunkCache = make(map[blockCacheKey]*CachedSubChunk) + subChunkCache = make(map[xxh3.Uint128]*CachedSubChunk) scMu sync.Mutex ) -type blockCacheKey struct { - hash xxh3.Uint128 - mode blocknetwork.Mode -} - -func unsubC(key blockCacheKey) { +func unsubC(hash xxh3.Uint128) { cMu.Lock() defer cMu.Unlock() - if c, ok := chunkCache[key]; ok { + if c, ok := chunkCache[hash]; ok { c.subs.Add(-1) if c.subs.Load() <= 0 { - delete(chunkCache, key) + delete(chunkCache, hash) } } } -func unsubSC(key blockCacheKey) { +func unsubSC(hash xxh3.Uint128) { scMu.Lock() defer scMu.Unlock() - if c, ok := subChunkCache[key]; ok { + if c, ok := subChunkCache[hash]; ok { //fmt.Println("unsubscribing from subchunk", hash, c.subs.Load()) c.subs.Add(-1) if c.subs.Load() <= 0 { //fmt.Println("deleting subchunk from cache", hash) - delete(subChunkCache, key) + delete(subChunkCache, hash) } } } @@ -57,8 +52,8 @@ func CacheSubChunk(payload *bytes.Buffer, c *chunk.Chunk, pos protocol.ChunkPos, scMu.Lock() defer scMu.Unlock() - key := blockCacheKey{hash: xxh3.Hash128(payload.Bytes()), mode: codec.Mode()} - if sc, ok := subChunkCache[key]; ok { + hash := xxh3.Hash128Seed(payload.Bytes(), uint64(codec.Mode())) + if sc, ok := subChunkCache[hash]; ok { sc.subs.Add(1) //fmt.Println("returning cached subchunk", hash) return sc, nil @@ -73,9 +68,9 @@ func CacheSubChunk(payload *bytes.Buffer, c *chunk.Chunk, pos protocol.ChunkPos, decodedSC.ConvertBlockNetworkHashesToRuntimeIDs(BlockRegistry) } - cachedSC := &CachedSubChunk{hash: key.hash, layer: index, sc: decodedSC} + cachedSC := &CachedSubChunk{hash: hash, layer: index, sc: decodedSC} cachedSC.subs.Add(1) - subChunkCache[key] = cachedSC + subChunkCache[hash] = cachedSC //fmt.Println("newly cached subchunk", hash) return cachedSC, nil @@ -85,11 +80,11 @@ func CacheChunk(input *packet.LevelChunk, codec blocknetwork.Codec) (ChunkInfo, cMu.Lock() defer cMu.Unlock() - key := blockCacheKey{hash: xxh3.Hash128(input.RawPayload), mode: codec.Mode()} - if c, ok := chunkCache[key]; ok { + hash := xxh3.Hash128Seed(input.RawPayload, uint64(codec.Mode())) + if c, ok := chunkCache[hash]; ok { c.subs.Add(1) - //fmt.Println("returning cached chunk", key.hash) - return ChunkInfo{Hash: key.hash, blockNetworkMode: codec.Mode(), Chunk: c.chunk, Cached: true}, nil + //fmt.Println("returning cached chunk", hash) + return ChunkInfo{Hash: hash, Chunk: c.chunk, Cached: true}, nil } dimension, ok := world.DimensionByID(int(input.Dimension)) @@ -111,10 +106,10 @@ func CacheChunk(input *packet.LevelChunk, codec blocknetwork.Codec) (ChunkInfo, } decodedChunk.Compact() - cachedChunk := &CachedChunk{hash: key.hash, chunk: decodedChunk} + cachedChunk := &CachedChunk{hash: hash, chunk: decodedChunk} cachedChunk.subs.Add(1) - chunkCache[key] = cachedChunk - return ChunkInfo{Hash: key.hash, blockNetworkMode: codec.Mode(), Chunk: cachedChunk.chunk, Cached: true}, nil + chunkCache[hash] = cachedChunk + return ChunkInfo{Hash: hash, Chunk: cachedChunk.chunk, Cached: true}, nil } // ReencodeLevelChunk fully re-encodes the block palettes in input while preserving the session's block-network diff --git a/world/world.go b/world/world.go index d208b7e6..de84fa66 100644 --- a/world/world.go +++ b/world/world.go @@ -7,7 +7,6 @@ import ( "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/chunk" "github.com/ethaniccc/float32-cube/cube" - "github.com/oomph-ac/oomph/world/blocknetwork" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/zeebo/xxh3" @@ -17,17 +16,16 @@ import ( ) type ChunkInfo struct { - Cached bool - Hash xxh3.Uint128 - blockNetworkMode blocknetwork.Mode - Chunk *chunk.Chunk + Cached bool + Hash xxh3.Uint128 + Chunk *chunk.Chunk } type World struct { lastCleanPos protocol.ChunkPos chunks map[protocol.ChunkPos]ChunkInfo - subChunks map[protocol.ChunkPos][]blockCacheKey + subChunks map[protocol.ChunkPos][]xxh3.Uint128 exemptedChunks map[protocol.ChunkPos]struct{} blockUpdates map[protocol.ChunkPos]map[df_cube.Pos]world.Block @@ -40,7 +38,7 @@ type World struct { func New(debugFn func(string, ...any)) *World { return &World{ chunks: make(map[protocol.ChunkPos]ChunkInfo), - subChunks: make(map[protocol.ChunkPos][]blockCacheKey), + subChunks: make(map[protocol.ChunkPos][]xxh3.Uint128), exemptedChunks: make(map[protocol.ChunkPos]struct{}), blockUpdates: make(map[protocol.ChunkPos]map[df_cube.Pos]world.Block), @@ -68,11 +66,11 @@ func (w *World) AddChunk(chunkPos protocol.ChunkPos, c ChunkInfo) { } // AddSubChunk adds a subchunk to the world. -func (w *World) AddSubChunk(chunkPos protocol.ChunkPos, hash xxh3.Uint128, codec blocknetwork.Codec) { +func (w *World) AddSubChunk(chunkPos protocol.ChunkPos, hash xxh3.Uint128) { if _, ok := w.subChunks[chunkPos]; !ok { - w.subChunks[chunkPos] = make([]blockCacheKey, 0, 16) + w.subChunks[chunkPos] = make([]xxh3.Uint128, 0, 16) } - w.subChunks[chunkPos] = append(w.subChunks[chunkPos], blockCacheKey{hash: hash, mode: codec.Mode()}) + w.subChunks[chunkPos] = append(w.subChunks[chunkPos], hash) } // Chunk returns a cached chunk at the position passed. The mutex is @@ -170,11 +168,11 @@ func (w *World) PurgeChunks() { func (w *World) removeChunk(info ChunkInfo, chunkPos protocol.ChunkPos) { if info.Cached { - unsubC(blockCacheKey{hash: info.Hash, mode: info.blockNetworkMode}) + unsubC(info.Hash) } if subChunks, ok := w.subChunks[chunkPos]; ok { - for _, subChunkKey := range subChunks { - unsubSC(subChunkKey) + for _, subChunkHash := range subChunks { + unsubSC(subChunkHash) } } delete(w.subChunks, chunkPos) From 8279cca345bf96964faefc3cf8814d0cd68c11ad Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 14 Jul 2026 01:04:39 -0400 Subject: [PATCH 15/16] fix: preserve items with unknown block hashes --- player/items.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/player/items.go b/player/items.go index 96e70acb..4aa8a8aa 100644 --- a/player/items.go +++ b/player/items.go @@ -19,9 +19,10 @@ func (p *Player) ConvertToStack(it protocol.ItemStack) item.Stack { } } if it.BlockRuntimeID != 0 { - b, _ := p.World().BlockRegistry().BlockByRuntimeID(p.DecodeBlockRuntimeID(uint32(it.BlockRuntimeID))) - if t, ok = b.(world.Item); !ok { - t = block.Air{} + if b, found := p.World().BlockRegistry().BlockByRuntimeID(p.DecodeBlockRuntimeID(uint32(it.BlockRuntimeID))); found { + if t, ok = b.(world.Item); !ok { + t = block.Air{} + } } } if nbter, ok := t.(world.NBTer); ok && len(it.NBTData) != 0 { From b0deafdd8c4e5c64e7a83bf8281906a2b8f0073b Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 14 Jul 2026 01:21:54 -0400 Subject: [PATCH 16/16] perf: use runtime cache chunk compaction --- go.mod | 2 +- go.sum | 4 ++-- world/cache.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index d794ec03..b1bc3c24 100755 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/oomph-ac/oomph go 1.26.0 -replace github.com/df-mc/dragonfly => github.com/oomph-ac/dragonfly v0.0.0-20260713230435-a683785ee72e +replace github.com/df-mc/dragonfly => github.com/oomph-ac/dragonfly v0.0.0-20260714052058-083ef3b8b766 replace github.com/sandertv/gophertunnel => github.com/hashimthearab/gophertunnel v1.25.3-0.20260713165240-828896b9c778 diff --git a/go.sum b/go.sum index a103df9b..de688188 100755 --- a/go.sum +++ b/go.sum @@ -54,8 +54,8 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/oomph-ac/dragonfly v0.0.0-20260713230435-a683785ee72e h1:b0O+I3X11M91SlUlvDEZ7Nihohb0SoUnOiUCZufXyfE= -github.com/oomph-ac/dragonfly v0.0.0-20260713230435-a683785ee72e/go.mod h1:OFJjCz3xfDalVZEtV4lJdbenwKDEAczKHikHN7kVwMc= +github.com/oomph-ac/dragonfly v0.0.0-20260714052058-083ef3b8b766 h1:dRWNQjPINjG24MGUzJdTSwHV/bMI/WJYORT13uMPZBc= +github.com/oomph-ac/dragonfly v0.0.0-20260714052058-083ef3b8b766/go.mod h1:OFJjCz3xfDalVZEtV4lJdbenwKDEAczKHikHN7kVwMc= github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0= github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk= github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= diff --git a/world/cache.go b/world/cache.go index 471e5358..150c869e 100644 --- a/world/cache.go +++ b/world/cache.go @@ -104,7 +104,7 @@ func CacheChunk(input *packet.LevelChunk, codec blocknetwork.Codec) (ChunkInfo, if codec.Mode() == blocknetwork.Hashes { decodedChunk.ConvertBlockNetworkHashesToRuntimeIDs() } - decodedChunk.Compact() + decodedChunk.CompactForRuntimeCache() cachedChunk := &CachedChunk{hash: hash, chunk: decodedChunk} cachedChunk.subs.Add(1)