diff --git a/go.mod b/go.mod index 1607c8b6..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-20260702190811-865056f9d47f +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 22df3593..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-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-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/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/component/acknowledgement/chunks.go b/player/component/acknowledgement/chunks.go index 850762fb..794c36c9 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.BlockNetwork()) if err != nil { ack.mPlayer.Disconnect(fmt.Sprintf(game.ErrorInternalDecodeChunk, err)) return @@ -93,7 +93,7 @@ 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.BlockNetwork()) if err != nil { ack.mPlayer.Disconnect(fmt.Sprintf(game.ErrorInternalDecodeChunk, err)) continue diff --git a/player/component/world.go b/player/component/world.go index e655f575..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, 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())}, 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())}, 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(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 e10007ab..4aa8a8aa 100644 --- a/player/items.go +++ b/player/items.go @@ -18,10 +18,11 @@ 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 t, ok = b.(world.Item); !ok { - t = block.Air{} + if it.BlockRuntimeID != 0 { + 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 { @@ -32,10 +33,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.EncodeBlockRuntimeID(uint32(instance.Stack.BlockRuntimeID))) + } + return instance } func (p *Player) StackToItem(it protocol.ItemStack) item.Stack { + if it.BlockRuntimeID != 0 { + 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 7e223432..f8692785 100755 --- a/player/network.go +++ b/player/network.go @@ -2,10 +2,12 @@ package player import ( "context" + "fmt" "net" "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,7 +43,9 @@ func (p *Player) SetServerConn(conn ServerConn) { return } + blockNetwork := blocknetwork.NewCodec(p.World().BlockRegistry(), blocknetwork.ModeFromHashes(conn.GameData().UseBlockNetworkIDHashes)) if p.serverConn == nil { + p.blockNetwork = blockNetwork for _, item := range conn.GameData().Items { if i, ok := world.ItemByName(item.Name, 0); ok { p.items[item.RuntimeID] = i @@ -63,6 +67,11 @@ func (p *Player) SetServerConn(conn ServerConn) { p.movement.ResetTransferState(p.GameDat.PlayerPosition) } +// 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 // a proxy switches this player to another backend. type BackendTransferState struct { @@ -72,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) } @@ -91,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 415095c4..0d758f0b 100644 --- a/player/packet.go +++ b/player/packet.go @@ -1,13 +1,10 @@ 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" @@ -404,30 +401,15 @@ 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) + fullChunk := !pk.CacheEnabled && pk.SubChunkCount != protocol.SubChunkRequestModeLimited && pk.SubChunkCount != protocol.SubChunkRequestModeLimitless + 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 { - 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() diff --git a/player/player.go b/player/player.go index 20ad9466..8124bbbf 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,6 +72,10 @@ type Player struct { GameDat minecraft.GameData Version int32 + // 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. RuntimeId uint64 @@ -249,6 +254,8 @@ func New(log *slog.Logger, mState MonitoringState, listener *minecraft.Listener) listener: listener, + blockNetwork: 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 44f633d6..bd224ccf 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" ) @@ -71,6 +72,32 @@ func (p *Player) World() *oworld.World { return p.world } +// 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) DecodeBlockRuntimeID(id uint32) uint32 { + return blockRuntimeIDFromNetwork(p.blockNetwork, id) +} + +// 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) EncodeBlockRuntimeID(id uint32) uint32 { + return blockRuntimeIDToNetwork(p.blockNetwork, id) +} + +func blockRuntimeIDFromNetwork(codec blocknetwork.Codec, id uint32) uint32 { + if runtimeID, ok := codec.ToRuntimeID(id); ok { + return runtimeID + } + return id +} + +func blockRuntimeIDToNetwork(codec blocknetwork.Codec, id uint32) uint32 { + if networkID, ok := codec.FromRuntimeID(id); ok { + return networkID + } + return id +} + // This function is deprecated and instead, the user should call p.World().PurgeChunks() directly. func (p *Player) RegenerateWorld() { p.world.PurgeChunks() @@ -93,17 +120,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: world.BlockRuntimeID(p.World().Block(pos)), + NewBlockRuntimeID: p.EncodeBlockRuntimeID(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) } @@ -166,11 +194,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.EncodeBlockRuntimeID(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/world/cache.go b/world/cache.go index 1db10088..150c869e 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" @@ -47,11 +48,11 @@ func unsubSC(hash xxh3.Uint128) { } } -func CacheSubChunk(payload *bytes.Buffer, c *chunk.Chunk, pos protocol.ChunkPos) (*CachedSubChunk, error) { +func CacheSubChunk(payload *bytes.Buffer, c *chunk.Chunk, pos protocol.ChunkPos, codec blocknetwork.Codec) (*CachedSubChunk, error) { scMu.Lock() defer scMu.Unlock() - hash := xxh3.Hash128(payload.Bytes()) + hash := xxh3.Hash128Seed(payload.Bytes(), uint64(codec.Mode())) if sc, ok := subChunkCache[hash]; ok { sc.subs.Add(1) //fmt.Println("returning cached subchunk", hash) @@ -63,6 +64,9 @@ func CacheSubChunk(payload *bytes.Buffer, c *chunk.Chunk, pos protocol.ChunkPos) if err != nil { return nil, err } + if codec.Mode() == blocknetwork.Hashes { + decodedSC.ConvertBlockNetworkHashesToRuntimeIDs(BlockRegistry) + } cachedSC := &CachedSubChunk{hash: hash, layer: index, sc: decodedSC} cachedSC.subs.Add(1) @@ -72,11 +76,11 @@ func CacheSubChunk(payload *bytes.Buffer, c *chunk.Chunk, pos protocol.ChunkPos) return cachedSC, nil } -func CacheChunk(input *packet.LevelChunk) (ChunkInfo, error) { +func CacheChunk(input *packet.LevelChunk, codec blocknetwork.Codec) (ChunkInfo, error) { cMu.Lock() defer cMu.Unlock() - hash := xxh3.Hash128(input.RawPayload) + hash := xxh3.Hash128Seed(input.RawPayload, uint64(codec.Mode())) if c, ok := chunkCache[hash]; ok { c.subs.Add(1) //fmt.Println("returning cached chunk", hash) @@ -97,7 +101,10 @@ func CacheChunk(input *packet.LevelChunk) (ChunkInfo, error) { if err != nil { return ChunkInfo{}, err } - decodedChunk.Compact() + if codec.Mode() == blocknetwork.Hashes { + decodedChunk.ConvertBlockNetworkHashesToRuntimeIDs() + } + decodedChunk.CompactForRuntimeCache() cachedChunk := &CachedChunk{hash: hash, chunk: decodedChunk} cachedChunk.subs.Add(1) @@ -105,6 +112,38 @@ func CacheChunk(input *packet.LevelChunk) (ChunkInfo, error) { 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 +// 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) + } + buf := bytes.NewBuffer(input.RawPayload) + decoded, _, err := chunk.NetworkDecodeBuffer(BlockRegistry, buf, int(input.SubChunkCount), dimension.Range()) + if err != nil { + return err + } + if codec.Mode() == blocknetwork.Hashes { + decoded.ConvertBlockNetworkHashesToRuntimeIDs() + } + var data chunk.SerialisedData + if codec.Mode() == blocknetwork.Hashes { + 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 +} + type CachedSubChunk struct { layer byte hash xxh3.Uint128