Skip to content

server/item: move the item NBT codec out of nbtconv - #1361

Open
HashimTheArab wants to merge 2 commits into
df-mc:masterfrom
HashimTheArab:item-nbt-codec
Open

server/item: move the item NBT codec out of nbtconv#1361
HashimTheArab wants to merge 2 commits into
df-mc:masterfrom
HashimTheArab:item-nbt-codec

Conversation

@HashimTheArab

@HashimTheArab HashimTheArab commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Crossbow reaches the item stack NBT codec through go:linkname rather than an import, because nbtconv imports item and the import cannot go back the other way.

A linkname pull creates no dependency edge. The compiler never looks for nbtconv, and the linker only resolves the symbols if something else in the binary happens to import it. Anything importing server — a real server — gets it via server/entity and server/block, which is presumably why this has gone unnoticed. A binary that imports server/item on its own does not:

package main

import (
	"github.com/df-mc/dragonfly/server/item"
	"github.com/df-mc/dragonfly/server/world"
)

func main() {
	var nbt world.NBTer = item.Crossbow{}
	_ = nbt.EncodeNBT()
}
github.com/df-mc/dragonfly/server/item.Crossbow.EncodeNBT: relocation target
github.com/df-mc/dragonfly/server/internal/nbtconv.WriteItem not defined

It bites tools and test binaries that pull in server/item without reaching entities or blocks. I hit it writing an item-persistence helper package.

The change

The stack codec moves into item, where the type it serialises lives and where Crossbow can call it directly. That is the only direction the dependency can point: anything encoding a Stack must import item, so a package below item cannot do it, and item cannot import upwards.

All existing codec callers now use item.WriteNBT, item.ReadNBT, and item.MapNBT directly. The item-specific forwarding wrappers have been removed from nbtconv; its inventory helpers remain because they also encode and decode inventory slots. Both linknames and the now-unnecessary blank unsafe import are gone.

Net: 16 files changed, +364 / -320, almost all of it code moving between packages and direct call-site updates.

Two things worth a reviewer's attention

Duplicated map readers. The codec needs six trivial readers (Bool, Uint8, String, Int16, Int32, Slice) plus a block decoder. They are duplicated privately in item as nbtBool, nbtUint8 and so on rather than imported, because nbtconv depends on item. Splitting the generic half of nbtconv into a leaf package both could import would avoid that, at the cost of touching every call site — happy to do it that way instead if you prefer.

item picks up world/chunk, for chunk.CurrentBlockVersion in writeBlock. chunk does not import item, so there is no cycle, but it does widen item's footprint for one constant. Moving that constant somewhere more neutral would avoid it.

Testing

  • go build ./...
  • go vet ./...
  • go test ./...
  • The reproducer above fails to link on master and runs on this branch.
  • mapValue keeps its name and field names. gob identifies the type by name, so worlds written before this still decode — the on-disk format is unchanged.

Crossbow reaches nbtconv.WriteItem and nbtconv.MapItem through go:linkname
rather than an import, because nbtconv imports item and the import cannot go
back the other way.

A linkname pull creates no dependency edge, so the compiler never looks for
nbtconv and the linker only resolves the symbols if something else in the
binary happens to import it. Anything importing server (a real server) gets
it via server/entity and server/block, which is why this has not come up.
A binary that imports server/item on its own does not:

	package main

	import (
		"github.com/df-mc/dragonfly/server/item"
		"github.com/df-mc/dragonfly/server/world"
	)

	func main() {
		var nbt world.NBTer = item.Crossbow{}
		_ = nbt.EncodeNBT()
	}

	github.com/df-mc/dragonfly/server/item.Crossbow.EncodeNBT: relocation
	target github.com/df-mc/dragonfly/server/internal/nbtconv.WriteItem
	not defined

The stack codec is moved into item, where the type it serialises lives and
where Crossbow can call it directly. nbtconv.WriteItem, Item and MapItem stay
as one-line wrappers, so none of the 22 call sites in entity, block and
item/creative change, and both linknames and the blank unsafe import are gone.

The six map readers the codec needs are duplicated privately in item rather
than imported: nbtconv depends on item, so the codec has to live here for the
dependency to point the right way, and these are the only helpers it uses.

mapValue keeps its name and field names. gob identifies the type by name, so
worlds written before this still decode.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant