Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7d60b8f
refactor(world/chunk): make block registry instance-scoped (no global…
HashimTheArab Jan 14, 2026
0e871de
deps: go mod tidy
HashimTheArab Jan 14, 2026
78e910f
docs: blockregistry
HashimTheArab Jan 14, 2026
6abca88
docs
HashimTheArab Jan 14, 2026
2d0fefb
more fixes
HashimTheArab Jan 14, 2026
1747665
refactor: remove unused code
HashimTheArab Jan 14, 2026
03a76f2
refactor: remove unused code
HashimTheArab Jan 14, 2026
b2dce13
fix: add explicit panic for non matched
HashimTheArab Jan 14, 2026
8e8e75b
fix: add mutex to blockregistry for finalized check
HashimTheArab Jan 14, 2026
7da98e4
refactor: detailed panic
HashimTheArab Jan 14, 2026
4c8171f
fix: deadlock
HashimTheArab Jan 14, 2026
52dbe5b
fix: revert iterator to original logic
HashimTheArab Jan 14, 2026
9be1f43
fixes
HashimTheArab Jan 14, 2026
dbc9813
fix: possible panics due to default block registry not init
HashimTheArab Jan 14, 2026
50e7cd0
docs
HashimTheArab Jan 14, 2026
33422a1
docs: chunk.BlockRegistry
HashimTheArab Jan 14, 2026
ad1f06e
refactor: simplify changes to just use world.BlockByName instead of w…
HashimTheArab Jan 14, 2026
6d80bc5
docs
HashimTheArab Jan 14, 2026
e78b41f
doc
HashimTheArab Jan 15, 2026
6b3d0e0
format
HashimTheArab Jan 15, 2026
4c07b1c
docs
HashimTheArab Jan 15, 2026
a85b670
Merge branch 'master' into feat/block-registries
HashimTheArab Feb 28, 2026
7b15a55
Merge branch 'master' into feat/block-registries
HashimTheArab Mar 8, 2026
6c05283
fix mask issue
HashimTheArab Apr 7, 2026
dc3a5a0
Merge branch 'master' into feat/block-registries
HashimTheArab Apr 7, 2026
7f0ecaf
fix: Keep world persistence bound to the active block registry
HashimTheArab Apr 7, 2026
e0d2111
small optimization
HashimTheArab Apr 22, 2026
105814f
Merge branch 'master' into feat/block-registries
HashimTheArab Apr 22, 2026
53f9320
- remove redundant assignment in session.go
HashimTheArab Apr 23, 2026
6bf8bd1
remove internal intintmap and use dep
HashimTheArab Apr 23, 2026
25a1c3d
Merge branch 'master' into feat/block-registries
HashimTheArab Apr 26, 2026
e42f3c0
Merge branch 'master' into feat/block-registries
HashimTheArab Apr 27, 2026
4327396
fix review findings
HashimTheArab Apr 29, 2026
3e8e010
Merge branch 'master' into feat/block-registries
didntpot Apr 29, 2026
5665f85
refactor: subchunk doesnt need br
HashimTheArab Apr 29, 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 @@ -3,7 +3,7 @@ module github.com/df-mc/dragonfly
go 1.26.0

require (
github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9
github.com/brentp/intintmap v0.0.0-20251106190759-56907b1f8479
github.com/cespare/xxhash/v2 v2.3.0
github.com/df-mc/goleveldb v1.1.9
github.com/df-mc/worldupgrader v1.0.20
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9 h1:/G0ghZwrhou0Wq21qc1vXXMm/t/aKWkALWwITptKbE0=
github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9/go.mod h1:TOk10ahXejq9wkEaym3KPRNeuR/h5Jx+s8QRWIa2oTM=
github.com/brentp/intintmap v0.0.0-20251106190759-56907b1f8479 h1:UZbbt19ACBOFO+CiDQFjaEoPJkBhj7GNGtIq59WR6Os=
github.com/brentp/intintmap v0.0.0-20251106190759-56907b1f8479/go.mod h1:TOk10ahXejq9wkEaym3KPRNeuR/h5Jx+s8QRWIa2oTM=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
Expand Down
21 changes: 14 additions & 7 deletions server/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ type Config struct {
// may be added to the Server's worlds. If no entity types are registered,
// Entities will be set to entity.DefaultRegistry.
Entities world.EntityRegistry
// Blocks is the BlockRegistry template used for newly created worlds. If nil, world.DefaultBlockRegistry is used.
// For a non-default registry, set this to world.NewBlockRegistry(), register blocks on that instance, and ensure
// it is finalized before use.
Blocks world.BlockRegistry
}

// New creates a Server using fields of conf. The Server's worlds are created
Expand Down Expand Up @@ -156,8 +160,17 @@ func (conf Config) New() *Server {
if len(conf.Entities.Types()) == 0 {
conf.Entities = entity.DefaultRegistry
}
if conf.Blocks == nil {
conf.Blocks = world.DefaultBlockRegistry
}

// Initialize the passed block registry and also initialize the default block registry which
// is used in some vanilla paths.
conf.Blocks.Finalize()
world.DefaultBlockRegistry.Finalize()

if !conf.DisableResourceBuilding {
if pack, ok := packbuilder.BuildResourcePack(); ok {
if pack, ok := packbuilder.BuildResourcePack(conf.Blocks); ok {
conf.Resources = append(conf.Resources, pack)
}
}
Expand All @@ -179,7 +192,6 @@ func (conf Config) New() *Server {
}

creative_registerCreativeItems()
world_finaliseBlockRegistry()
recipe_registerVanilla()

srv.world = srv.createWorld(world.Overworld, &srv.nether, &srv.end)
Expand Down Expand Up @@ -351,8 +363,3 @@ func creative_registerCreativeItems()
//
//go:linkname recipe_registerVanilla github.com/df-mc/dragonfly/server/item/recipe.registerVanilla
func recipe_registerVanilla()

// noinspection ALL
//
//go:linkname world_finaliseBlockRegistry github.com/df-mc/dragonfly/server/world.finaliseBlockRegistry
func world_finaliseBlockRegistry()
7 changes: 4 additions & 3 deletions server/internal/packbuilder/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ package packbuilder
import (
"encoding/json"
"fmt"
"github.com/df-mc/dragonfly/server/world"
"image"
"image/png"
"os"
"path/filepath"
"strings"
_ "unsafe" // Imported for compiler directives.

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

// buildBlocks builds all the block-related files for the resource pack. This includes textures, geometries, language
// entries and terrain texture atlas.
func buildBlocks(dir string) (count int, lang []string) {
func buildBlocks(reg world.BlockRegistry, dir string) (count int, lang []string) {
if err := os.MkdirAll(filepath.Join(dir, "models/blocks"), os.ModePerm); err != nil {
panic(err)
}
Expand All @@ -23,7 +24,7 @@ func buildBlocks(dir string) (count int, lang []string) {
}

textureData := make(map[string]any)
for identifier, blk := range world.CustomBlocks() {
for identifier, blk := range reg.CustomBlocks() {
b, ok := blk.(world.CustomBlockBuildable)
if !ok {
continue
Expand Down
8 changes: 5 additions & 3 deletions server/internal/packbuilder/resource_pack.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package packbuilder

import (
_ "embed"
"os"

"github.com/df-mc/dragonfly/server/world"
"github.com/sandertv/gophertunnel/minecraft/resource"
"golang.org/x/mod/sumdb/dirhash"
"os"
)

//go:embed pack_icon.png
Expand All @@ -13,7 +15,7 @@ var packIcon []byte
// BuildResourcePack builds a resource pack based on custom features that have been registered to the server.
// It creates a UUID based on the hash of the directory so the client will only be prompted to download it
// once it is changed.
func BuildResourcePack() (*resource.Pack, bool) {
func BuildResourcePack(reg world.BlockRegistry) (*resource.Pack, bool) {
dir, err := os.MkdirTemp("", "dragonfly_resource_pack-")
if err != nil {
panic(err)
Expand All @@ -27,7 +29,7 @@ func BuildResourcePack() (*resource.Pack, bool) {
assets += itemCount
lang = append(lang, itemLang...)

blockCount, blockLang := buildBlocks(dir)
blockCount, blockLang := buildBlocks(reg, dir)
assets += blockCount
lang = append(lang, blockLang...)

Expand Down
4 changes: 3 additions & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ func (srv *Server) startListening() {
// registered custom blocks. It allows block components to be created only once
// at startup.
func (srv *Server) makeBlockEntries() {
custom := slices.Collect(maps.Values(world.CustomBlocks()))
custom := slices.Collect(maps.Values(srv.conf.Blocks.CustomBlocks()))
srv.customBlocks = make([]protocol.BlockEntry, len(custom))

for i, b := range custom {
Expand Down Expand Up @@ -532,6 +532,7 @@ func (srv *Server) createPlayer(id uuid.UUID, conn session.Conn, conf player.Con
JoinMessage: srv.conf.JoinMessage,
QuitMessage: srv.conf.QuitMessage,
HandleStop: srv.handleSessionClose,
BlockRegistry: w.BlockRegistry(),
}.New(conn)

conf.Name = conn.IdentityData().DisplayName
Expand Down Expand Up @@ -563,6 +564,7 @@ func (srv *Server) createWorld(dim world.Dimension, nether, end **world.World) *
SaveInterval: srv.conf.SaveInterval,
ChunkUnloadInterval: srv.conf.ChunkUnloadInterval,
Entities: srv.conf.Entities,
Blocks: srv.conf.Blocks,
PortalDestination: func(dim world.Dimension) *world.World {
switch dim {
case world.Nether:
Expand Down
10 changes: 5 additions & 5 deletions server/session/handler_inventory_transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,17 @@ func (h *InventoryTransactionHandler) Handle(p packet.Packet, s *Session, tx *wo
h.resendInventories(s)
return
case *protocol.UseItemOnEntityTransactionData:
if err = s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack), c); err != nil {
if err = s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(s.br, data.HeldItem.Stack), c); err != nil {
return
}
return h.handleUseItemOnEntityTransaction(data, s, tx, c)
case *protocol.UseItemTransactionData:
if err = s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack), c); err != nil {
if err = s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(s.br, data.HeldItem.Stack), c); err != nil {
return
}
return h.handleUseItemTransaction(data, s, c)
case *protocol.ReleaseItemTransactionData:
if err = s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack), c); err != nil {
if err = s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(s.br, data.HeldItem.Stack), c); err != nil {
return
}
return h.handleReleaseItemTransaction(c)
Expand Down Expand Up @@ -96,12 +96,12 @@ func (h *InventoryTransactionHandler) handleNormalTransaction(pk *packet.Invento
)
for _, action := range pk.Actions {
if action.SourceType == protocol.InventoryActionSourceWorld && action.InventorySlot == 0 {
if old := stackToItem(action.OldItem.Stack); !old.Empty() {
if old := stackToItem(s.br, action.OldItem.Stack); !old.Empty() {
return fmt.Errorf("unexpected non-empty old item in transaction action: %#v", action.OldItem)
}
count = int(action.NewItem.Stack.Count)
} else if action.SourceType == protocol.InventoryActionSourceContainer && action.WindowID == protocol.WindowIDInventory {
if expected = stackToItem(action.OldItem.Stack); expected.Empty() {
if expected = stackToItem(s.br, action.OldItem.Stack); expected.Empty() {
return fmt.Errorf("unexpected empty old item in transaction action: %#v", action.OldItem)
}
slot = int(action.InventorySlot)
Expand Down
2 changes: 1 addition & 1 deletion server/session/handler_mob_equipment.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (*MobEquipmentHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c
// This window ID is expected, but we don't handle it.
return nil
case protocol.WindowIDInventory:
return s.VerifyAndSetHeldSlot(int(pk.InventorySlot), stackToItem(pk.NewItem.Stack), c)
return s.VerifyAndSetHeldSlot(int(pk.InventorySlot), stackToItem(s.br, pk.NewItem.Stack), c)
default:
return fmt.Errorf("only main inventory should be involved in slot change, got window ID %v", pk.WindowID)
}
Expand Down
2 changes: 1 addition & 1 deletion server/session/handler_player_auth_input.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ func (h PlayerAuthInputHandler) handleUseItemData(data protocol.UseItemTransacti
defer s.swingingArm.Store(false)

held, _ := c.HeldItems()
if !held.Equal(stackToItem(data.HeldItem.Stack)) {
if !held.Equal(stackToItem(s.br, data.HeldItem.Stack)) {
s.conf.Log.Debug("process packet: PlayerAuthInput: UseItemTransaction: mismatch between actual held item and client held item")
return nil
}
Expand Down
48 changes: 24 additions & 24 deletions server/session/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ func (s *Session) sendRecipes() {
recipes = append(recipes, &protocol.ShapelessRecipe{
RecipeID: uuid.New().String(),
Priority: int32(i.Priority()),
Input: stacksToIngredientItems(i.Input()),
Output: stacksToRecipeStacks(i.Output()),
Input: stacksToIngredientItems(s.br, i.Input()),
Output: stacksToRecipeStacks(s.br, i.Output()),
Block: i.Block(),
RecipeNetworkID: networkID,
})
Expand All @@ -136,13 +136,13 @@ func (s *Session) sendRecipes() {
Priority: int32(i.Priority()),
Width: int32(i.Shape().Width()),
Height: int32(i.Shape().Height()),
Input: stacksToIngredientItems(i.Input()),
Output: stacksToRecipeStacks(i.Output()),
Input: stacksToIngredientItems(s.br, i.Input()),
Output: stacksToRecipeStacks(s.br, i.Output()),
Block: i.Block(),
RecipeNetworkID: networkID,
})
case recipe.SmithingTransform:
input, output := stacksToIngredientItems(i.Input()), stacksToRecipeStacks(i.Output())
input, output := stacksToIngredientItems(s.br, i.Input()), stacksToRecipeStacks(s.br, i.Output())
recipes = append(recipes, &protocol.SmithingTransformRecipe{
RecipeID: uuid.New().String(),
Base: input[0],
Expand All @@ -153,7 +153,7 @@ func (s *Session) sendRecipes() {
RecipeNetworkID: networkID,
})
case recipe.SmithingTrim:
input := stacksToIngredientItems(i.Input())
input := stacksToIngredientItems(s.br, i.Input())
recipes = append(recipes, &protocol.SmithingTrimRecipe{
RecipeID: uuid.New().String(),
Base: input[0],
Expand All @@ -164,8 +164,8 @@ func (s *Session) sendRecipes() {
})
case recipe.Furnace:
recipes = append(recipes, &protocol.FurnaceRecipe{
InputType: stackFromItem(i.Input()[0].(item.Stack)).ItemType,
Output: stackFromItem(i.Output()[0]),
InputType: stackFromItem(s.br, i.Input()[0].(item.Stack)).ItemType,
Output: stackFromItem(s.br, i.Output()[0]),
Block: i.Block(),
})
case recipe.Potion:
Expand Down Expand Up @@ -235,7 +235,7 @@ func (s *Session) sendInv(inv *inventory.Inventory, windowID uint32) {
Content: make([]protocol.ItemInstance, 0, inv.Size()),
}
for _, i := range inv.Slots() {
pk.Content = append(pk.Content, instanceFromItem(i))
pk.Content = append(pk.Content, instanceFromItem(s.br, i))
}
s.writePacket(pk)
}
Expand All @@ -245,7 +245,7 @@ func (s *Session) sendItem(item item.Stack, slot int, windowID uint32) {
s.writePacket(&packet.InventorySlot{
WindowID: windowID,
Slot: uint32(slot),
NewItem: instanceFromItem(item),
NewItem: instanceFromItem(s.br, item),
})
}

Expand Down Expand Up @@ -653,7 +653,7 @@ func (s *Session) broadcastOffHandFunc(tx *world.Tx, c Controllable) inventory.S
i, _ := s.offHand.Item(0)
s.writePacket(&packet.InventoryContent{
WindowID: protocol.WindowIDOffHand,
Content: []protocol.ItemInstance{instanceFromItem(i)},
Content: []protocol.ItemInstance{instanceFromItem(s.br, i)},
})
}
}
Expand Down Expand Up @@ -701,7 +701,7 @@ func (s *Session) SendHeldSlot(slot int, c Controllable, force bool) {
mainHand, _ := c.HeldItems()
s.writePacket(&packet.MobEquipment{
EntityRuntimeID: selfEntityRuntimeID,
NewItem: instanceFromItem(mainHand),
NewItem: instanceFromItem(s.br, mainHand),
InventorySlot: byte(slot),
HotBarSlot: byte(slot),
})
Expand Down Expand Up @@ -913,14 +913,14 @@ func valueOrDefault[T comparable](v, def T) T {
}

// stackFromItem converts an item.Stack to its network ItemStack representation.
func stackFromItem(it item.Stack) protocol.ItemStack {
func stackFromItem(br world.BlockRegistry, it item.Stack) protocol.ItemStack {
if it.Empty() {
return protocol.ItemStack{}
}

var blockRuntimeID uint32
if b, ok := it.Item().(world.Block); ok {
blockRuntimeID = world.BlockRuntimeID(b)
blockRuntimeID = br.BlockRuntimeID(b)
}

rid, meta, _ := world.ItemRuntimeID(it.Item())
Expand All @@ -938,7 +938,7 @@ func stackFromItem(it item.Stack) protocol.ItemStack {
}

// stackToItem converts a network ItemStack representation back to an item.Stack.
func stackToItem(it protocol.ItemStack) item.Stack {
func stackToItem(br world.BlockRegistry, it protocol.ItemStack) item.Stack {
t, ok := world.ItemByRuntimeID(it.NetworkID, int16(it.MetadataValue))
if !ok {
t = block.Air{}
Expand All @@ -947,7 +947,7 @@ func stackToItem(it protocol.ItemStack) item.Stack {
// It shouldn't matter if it (for whatever reason) wasn't able to get the block runtime ID,
// since on the next line, we assert that the block is an item. If it didn't succeed, it'll
// return air anyway.
b, _ := world.BlockByRuntimeID(uint32(it.BlockRuntimeID))
b, _ := br.BlockByRuntimeID(uint32(it.BlockRuntimeID))
if t, ok = b.(world.Item); !ok {
t = block.Air{}
}
Expand All @@ -961,24 +961,24 @@ func stackToItem(it protocol.ItemStack) item.Stack {
}

// instanceFromItem converts an item.Stack to its network ItemInstance representation.
func instanceFromItem(it item.Stack) protocol.ItemInstance {
func instanceFromItem(br world.BlockRegistry, it item.Stack) protocol.ItemInstance {
return protocol.ItemInstance{
StackNetworkID: item_id(it),
Stack: stackFromItem(it),
Stack: stackFromItem(br, it),
}
}

// stacksToRecipeStacks converts a list of item.Stacks to their protocol representation with damage stripped for recipes.
func stacksToRecipeStacks(inputs []item.Stack) []protocol.ItemStack {
func stacksToRecipeStacks(br world.BlockRegistry, inputs []item.Stack) []protocol.ItemStack {
items := make([]protocol.ItemStack, 0, len(inputs))
for _, i := range inputs {
items = append(items, deleteDamage(stackFromItem(i)))
items = append(items, deleteDamage(stackFromItem(br, i)))
}
return items
}

// stacksToIngredientItems converts a list of item.Stacks to recipe ingredient items used over the network.
func stacksToIngredientItems(inputs []recipe.Item) []protocol.ItemDescriptorCount {
func stacksToIngredientItems(_ world.BlockRegistry, inputs []recipe.Item) []protocol.ItemDescriptorCount {
items := make([]protocol.ItemDescriptorCount, 0, len(inputs))
for _, i := range inputs {
var d protocol.ItemDescriptor = &protocol.InvalidItemDescriptor{}
Expand Down Expand Up @@ -1011,13 +1011,13 @@ func stacksToIngredientItems(inputs []recipe.Item) []protocol.ItemDescriptorCoun
}

// creativeContent returns all creative groups, and creative inventory items as protocol item stacks.
func creativeContent() ([]protocol.CreativeGroup, []protocol.CreativeItem) {
func creativeContent(br world.BlockRegistry) ([]protocol.CreativeGroup, []protocol.CreativeItem) {
groups := make([]protocol.CreativeGroup, 0, len(creative.Groups()))
for _, group := range creative.Groups() {
groups = append(groups, protocol.CreativeGroup{
Category: int32(group.Category.Uint8()),
Name: group.Name,
Icon: deleteDamage(stackFromItem(group.Icon)),
Icon: deleteDamage(stackFromItem(br, group.Icon)),
})
}

Expand All @@ -1031,7 +1031,7 @@ func creativeContent() ([]protocol.CreativeGroup, []protocol.CreativeItem) {
}
it = append(it, protocol.CreativeItem{
CreativeItemNetworkID: uint32(index) + 1,
Item: deleteDamage(stackFromItem(i.Stack)),
Item: deleteDamage(stackFromItem(br, i.Stack)),
GroupIndex: uint32(group),
})
}
Expand Down
Loading