Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8dd771d
fix: support block network hashes
HashimTheArab Jul 13, 2026
52e67d1
Update Dragonfly hash helpers
HashimTheArab Jul 13, 2026
6bade6f
Update Dragonfly hash helper tests
HashimTheArab Jul 13, 2026
56a8a82
feat: add block network codecs
HashimTheArab Jul 14, 2026
12d4287
Merge remote-tracking branch 'origin/feat/block-network-codec' into f…
HashimTheArab Jul 14, 2026
ec3e081
refactor: centralize block network modes
HashimTheArab Jul 14, 2026
972f5b8
fix: translate block IDs in events and entities
HashimTheArab Jul 14, 2026
5cc3a96
fix: validate runtime IDs through registry lookups
HashimTheArab Jul 14, 2026
6d9e99d
Merge remote-tracking branch 'origin/feat/block-network-codec' into f…
HashimTheArab Jul 14, 2026
b1ecd32
fix: queue canonical local block updates
HashimTheArab Jul 14, 2026
4b59091
fix: translate actor block metadata
HashimTheArab Jul 14, 2026
0b7130c
fix: preserve transfer-safe actor state
HashimTheArab Jul 14, 2026
1489ed0
Merge remote-tracking branch 'origin/stable' into fix/block-network-h…
HashimTheArab Jul 14, 2026
c802374
refactor: simplify block network rewrites
HashimTheArab Jul 14, 2026
8456520
test: remove block network regression coverage
HashimTheArab Jul 14, 2026
8fa0826
refactor: require consistent block network modes
HashimTheArab Jul 14, 2026
bbc4175
refactor: simplify block cache keys
HashimTheArab Jul 14, 2026
8279cca
fix: preserve items with unknown block hashes
HashimTheArab Jul 14, 2026
b0deafd
perf: use runtime cache chunk compaction
HashimTheArab Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
7 changes: 6 additions & 1 deletion integration/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion integration/proxy/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions player/component/acknowledgement/chunks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions player/component/world.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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))
}
}

Expand Down Expand Up @@ -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.
Expand Down
18 changes: 13 additions & 5 deletions player/items.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}

Expand Down
17 changes: 15 additions & 2 deletions player/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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.
Expand Down
28 changes: 5 additions & 23 deletions player/packet.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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()
Expand Down
7 changes: 7 additions & 0 deletions player/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down
36 changes: 32 additions & 4 deletions player/world.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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()
Expand All @@ -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)
}

Expand Down Expand Up @@ -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.
})
Expand Down
Loading