up3
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled

This commit is contained in:
2026-07-09 08:33:57 +08:00
commit 26ed99fda6
845 changed files with 75419 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
package blockinternal
import (
"github.com/df-mc/dragonfly/server/item/category"
"maps"
"slices"
)
// ComponentBuilder represents a builder that can be used to construct a block components map to be sent to a client.
type ComponentBuilder struct {
permutations map[string]map[string]any
properties []map[string]any
components map[string]any
blockID int32
identifier string
menuCategory category.Category
}
// NewComponentBuilder returns a new component builder with the provided block data, using the provided components map
// as a base.
func NewComponentBuilder(identifier string, components map[string]any, blockID int32) *ComponentBuilder {
if components == nil {
components = map[string]any{}
}
return &ComponentBuilder{
permutations: make(map[string]map[string]any),
components: components,
blockID: blockID,
identifier: identifier,
menuCategory: category.Construction(),
}
}
// AddProperty adds the provided block property to the builder.
func (builder *ComponentBuilder) AddProperty(name string, values []any) {
builder.properties = append(builder.properties, map[string]any{
"name": name,
"enum": values,
})
}
// AddComponent adds the provided component to the builder. If the component already exists, it will be overwritten.
func (builder *ComponentBuilder) AddComponent(name string, value any) {
builder.components[name] = value
}
// AddPermutation adds a permutation to the builder. If there is already an existing permutation for the provided
// condition, the new components will be added to the existing permutation.
func (builder *ComponentBuilder) AddPermutation(condition string, components map[string]any) {
if len(builder.permutations) == 0 {
// This trigger really does not matter at all, the component just needs to be set for custom block placements to
// function as expected client-side, when permutations are applied.
builder.AddComponent("minecraft:on_player_placing", map[string]any{
"triggerType": "placement_trigger",
})
}
if builder.permutations[condition] == nil {
builder.permutations[condition] = map[string]any{}
}
for key, value := range components {
builder.permutations[condition][key] = value
}
}
// SetMenuCategory sets the creative category for the current block.
func (builder *ComponentBuilder) SetMenuCategory(category category.Category) {
builder.menuCategory = category
}
// Construct constructs the final block components map that is ready to be sent to the client.
func (builder *ComponentBuilder) Construct() map[string]any {
properties := slices.Clone(builder.properties)
components := maps.Clone(builder.components)
result := map[string]any{
"components": components,
"molangVersion": int32(10),
"menu_category": map[string]any{
"category": builder.menuCategory.String(),
"group": builder.menuCategory.Group(),
},
"vanilla_block_data": map[string]any{
"block_id": builder.blockID,
},
}
if len(properties) > 0 {
result["properties"] = properties
}
permutations := maps.Clone(builder.permutations)
if len(permutations) > 0 {
result["permutations"] = []map[string]any{}
for condition, values := range permutations {
result["permutations"] = append(result["permutations"].([]map[string]any), map[string]any{
"condition": condition,
"components": values,
})
}
}
return result
}
+136
View File
@@ -0,0 +1,136 @@
package blockinternal
import (
"github.com/df-mc/dragonfly/server/block"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/block/customblock"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
)
// Components returns all the components for the custom block, including permutations and properties.
func Components(identifier string, b world.CustomBlock, blockID int32) map[string]any {
components := componentsFromProperties(b.Properties())
builder := NewComponentBuilder(identifier, components, blockID)
if emitter, ok := b.(block.LightEmitter); ok {
builder.AddComponent("minecraft:block_light_emission", map[string]any{
"emission": float32(emitter.LightEmissionLevel() / 15),
})
}
if diffuser, ok := b.(block.LightDiffuser); ok {
builder.AddComponent("minecraft:block_light_filter", map[string]any{
"lightLevel": int32(diffuser.LightDiffusionLevel()),
})
}
if breakable, ok := b.(block.Breakable); ok {
info := breakable.BreakInfo()
builder.AddComponent("minecraft:destructible_by_mining", map[string]any{"value": float32(info.Hardness)})
}
if frictional, ok := b.(block.Frictional); ok {
builder.AddComponent("minecraft:friction", map[string]any{"value": float32(frictional.Friction())})
}
if flammable, ok := b.(block.Flammable); ok {
info := flammable.FlammabilityInfo()
builder.AddComponent("minecraft:flammable", map[string]any{
"flame_odds": int32(info.Encouragement),
"burn_odds": int32(info.Flammability),
})
}
if permutable, ok := b.(block.Permutable); ok {
for name, values := range permutable.States() {
builder.AddProperty(name, values)
}
for _, permutation := range permutable.Permutations() {
builder.AddPermutation(permutation.Condition, componentsFromProperties(permutation.Properties))
}
}
if item, ok := b.(world.CustomItem); ok {
builder.SetMenuCategory(item.Category())
}
return builder.Construct()
}
// componentsFromProperties builds a base components map that includes all the common data between a regular block and
// a custom permutation.
func componentsFromProperties(props customblock.Properties) map[string]any {
components := make(map[string]any)
if props.CollisionBox != (cube.BBox{}) {
components["minecraft:collision_box"] = collisionBoxComponent(props.CollisionBox)
}
if props.SelectionBox != (cube.BBox{}) {
components["minecraft:selection_box"] = selectionBoxComponent(props.SelectionBox)
}
if props.Geometry != "" {
components["minecraft:geometry"] = map[string]any{"identifier": props.Geometry}
} else if props.Cube {
components["minecraft:unit_cube"] = map[string]any{}
}
if props.MapColour != "" {
components["minecraft:map_color"] = map[string]any{"value": props.MapColour}
}
if props.Textures != nil {
materials := map[string]any{}
for target, material := range props.Textures {
materials[target] = material.Encode()
}
components["minecraft:material_instances"] = map[string]any{
"mappings": map[string]any{},
"materials": materials,
}
}
transformation := make(map[string]any)
if props.Rotation != (cube.Pos{}) {
transformation["RX"] = int32(props.Rotation.X())
transformation["RY"] = int32(props.Rotation.Y())
transformation["RZ"] = int32(props.Rotation.Z())
}
if props.Translation != (mgl64.Vec3{}) {
transformation["TX"] = float32(props.Translation.X())
transformation["TY"] = float32(props.Translation.Y())
transformation["TZ"] = float32(props.Translation.Z())
}
if props.Scale != (mgl64.Vec3{}) {
transformation["SX"] = float32(props.Scale.X())
transformation["SY"] = float32(props.Scale.Y())
transformation["SZ"] = float32(props.Scale.Z())
} else if len(transformation) > 0 {
transformation["SX"] = float32(1.0)
transformation["SY"] = float32(1.0)
transformation["SZ"] = float32(1.0)
}
if len(transformation) > 0 {
components["minecraft:transformation"] = transformation
}
return components
}
// collisionBoxComponent returns the component data for a collision box, using absolute min/max coordinates in pixels.
func collisionBoxComponent(box cube.BBox) map[string]any {
min, max := box.Min(), box.Max()
return map[string]any{
"enabled": true,
"boxes": []map[string]any{
{
"minX": float32(min.X() * 16),
"minY": float32(min.Y() * 16),
"minZ": float32(min.Z() * 16),
"maxX": float32(max.X() * 16),
"maxY": float32(max.Y() * 16),
"maxZ": float32(max.Z() * 16),
},
},
}
}
// selectionBoxComponent returns the component data for a selection box, translating coordinates to the origin/size
// format the client expects.
func selectionBoxComponent(box cube.BBox) map[string]any {
min, max := box.Min(), box.Max()
originX, originY, originZ := min.X()*16, min.Y()*16, min.Z()*16
sizeX, sizeY, sizeZ := (max.X()-min.X())*16, (max.Y()-min.Y())*16, (max.Z()-min.Z())*16
return map[string]any{
"enabled": true,
"origin": []float32{float32(originX) - 8, float32(originY), float32(originZ) - 8},
"size": []float32{float32(sizeX), float32(sizeY), float32(sizeZ)},
}
}
+72
View File
@@ -0,0 +1,72 @@
package iteminternal
import (
"github.com/df-mc/dragonfly/server/item/category"
"maps"
)
// ComponentBuilder represents a builder that can be used to construct an item components map to be sent to a client.
type ComponentBuilder struct {
name string
identifier string
category category.Category
properties map[string]any
components map[string]any
}
// NewComponentBuilder returns a new component builder with the provided item data.
func NewComponentBuilder(name, identifier string, category category.Category) *ComponentBuilder {
return &ComponentBuilder{
name: name,
identifier: identifier,
category: category,
properties: make(map[string]any),
components: make(map[string]any),
}
}
// AddProperty adds the provided property to the builder.
func (builder *ComponentBuilder) AddProperty(name string, value any) {
builder.properties[name] = value
}
// AddComponent adds the provided component to the builder.
func (builder *ComponentBuilder) AddComponent(name string, value any) {
builder.components[name] = value
}
// Construct constructs the final item components map and returns it. It also applies the default properties required
// for the item to work without modifying the original maps in the builder.
func (builder *ComponentBuilder) Construct() map[string]any {
properties := maps.Clone(builder.properties)
components := maps.Clone(builder.components)
builder.applyDefaultProperties(properties)
builder.applyDefaultComponents(components, properties)
return map[string]any{"components": components}
}
// applyDefaultProperties applies the default properties to the provided map. It is important that this method does
// not modify the builder's properties map directly otherwise Empty() will return false in future use of the builder.
func (builder *ComponentBuilder) applyDefaultProperties(x map[string]any) {
x["minecraft:icon"] = map[string]any{
"textures": map[string]any{
"default": builder.identifier,
},
}
x["creative_group"] = builder.category.Group()
x["creative_category"] = int32(builder.category.Uint8())
if _, ok := x["max_stack_size"]; !ok {
x["max_stack_size"] = int32(64)
}
}
// applyDefaultComponents applies the default components to the provided map. It is important that this method does not
// modify the builder's components map directly otherwise Empty() will return false in future use of the builder.
func (builder *ComponentBuilder) applyDefaultComponents(x, properties map[string]any) {
x["item_properties"] = properties
x["minecraft:display_name"] = map[string]any{
"value": builder.name,
}
}
@@ -0,0 +1,82 @@
package iteminternal
import (
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
"strings"
)
// Components returns all the components of the given custom item. If the item has no components, a nil map and false
// are returned.
func Components(it world.CustomItem) map[string]any {
category := it.Category()
identifier, _ := it.EncodeItem()
name := strings.Split(identifier, ":")[1]
builder := NewComponentBuilder(it.Name(), identifier, category)
if x, ok := it.(item.Armour); ok {
builder.AddComponent("minecraft:armor", map[string]any{
"protection": int32(x.DefencePoints()),
})
var slot string
switch it.(type) {
case item.HelmetType:
slot = "slot.armor.head"
case item.ChestplateType:
slot = "slot.armor.chest"
case item.LeggingsType:
slot = "slot.armor.legs"
case item.BootsType:
slot = "slot.armor.feet"
}
builder.AddComponent("minecraft:wearable", map[string]any{
"slot": slot,
})
}
if x, ok := it.(item.Consumable); ok {
builder.AddProperty("use_duration", int32(x.ConsumeDuration().Seconds()*20))
builder.AddComponent("minecraft:food", map[string]any{
"can_always_eat": x.AlwaysConsumable(),
})
if y, ok := it.(item.Drinkable); ok && y.Drinkable() {
builder.AddProperty("use_animation", int32(2))
} else {
builder.AddProperty("use_animation", int32(1))
}
}
if x, ok := it.(item.Cooldown); ok {
builder.AddComponent("minecraft:cooldown", map[string]any{
"category": name,
"duration": float32(x.Cooldown().Seconds()),
})
}
if x, ok := it.(item.Durable); ok {
builder.AddComponent("minecraft:durability", map[string]any{
"max_durability": int32(x.DurabilityInfo().MaxDurability),
})
}
if x, ok := it.(item.MaxCounter); ok {
builder.AddProperty("max_stack_size", int32(x.MaxCount()))
}
if x, ok := it.(item.OffHand); ok {
builder.AddProperty("allow_off_hand", x.OffHand())
}
if x, ok := it.(item.Throwable); ok {
// The data in minecraft:projectile is only used by vanilla server-side, but we must send at least an empty map
// so the client will play the throwing animation.
builder.AddComponent("minecraft:projectile", map[string]any{})
builder.AddComponent("minecraft:throwable", map[string]any{
"do_swing_animation": x.SwingAnimation(),
})
}
if x, ok := it.(item.Glinted); ok {
builder.AddProperty("foil", x.Glinted())
}
if x, ok := it.(item.HandEquipped); ok {
builder.AddProperty("hand_equipped", x.HandEquipped())
}
return builder.Construct()
}
+24
View File
@@ -0,0 +1,24 @@
package nbtconv
import (
"encoding/binary"
"image/color"
)
// Int32FromRGBA converts a color.RGBA into an int32. These int32s are present in, for example, signs.
func Int32FromRGBA(x color.RGBA) int32 {
if x.R == 0 && x.G == 0 && x.B == 0 {
// Default to black colour. The default (0x000000) is a transparent colour. Text with this colour will not show
// up on the sign.
return int32(-0x1000000)
}
return int32(binary.BigEndian.Uint32([]byte{x.A, x.R, x.G, x.B}))
}
// RGBAFromInt32 converts an int32 into a color.RGBA. These int32s are present in, for example, signs.
func RGBAFromInt32(x int32) color.RGBA {
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, uint32(x))
return color.RGBA{A: b[0], R: b[1], G: b[2], B: b[3]}
}
+31
View File
@@ -0,0 +1,31 @@
package nbtconv
import (
"github.com/df-mc/dragonfly/server/item/inventory"
)
// InvFromNBT decodes the data of an NBT slice into the inventory passed.
func InvFromNBT(inv *inventory.Inventory, items []any) {
for _, itemData := range items {
data, _ := itemData.(map[string]any)
it := Item(data, nil)
if it.Empty() {
continue
}
_ = inv.SetItem(int(Uint8(data, "Slot")), it)
}
}
// InvToNBT encodes an inventory to a data slice which may be encoded as NBT.
func InvToNBT(inv *inventory.Inventory) []map[string]any {
var items []map[string]any
for index, i := range inv.Slots() {
if i.Empty() {
continue
}
data := WriteItem(i, true)
data["Slot"] = byte(index)
items = append(items, data)
}
return items
}
+283
View File
@@ -0,0 +1,283 @@
package nbtconv
import (
"bytes"
"encoding/gob"
"time"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
"golang.org/x/exp/constraints"
)
// Bool reads a uint8 value from a map at key k and returns true if it equals 1.
func Bool(m map[string]any, k string) bool {
return Uint8(m, k) == 1
}
// Uint8 reads a uint8 value from a map at key k.
func Uint8(m map[string]any, k string) uint8 {
v, _ := m[k].(uint8)
return v
}
// String reads a string value from a map at key k.
func String(m map[string]any, k string) string {
v, _ := m[k].(string)
return v
}
// Int16 reads an int16 value from a map at key k.
func Int16(m map[string]any, k string) int16 {
v, _ := m[k].(int16)
return v
}
// Int32 reads an int32 value from a map at key k.
func Int32(m map[string]any, k string) int32 {
v, _ := m[k].(int32)
return v
}
// Int64 reads an int16 value from a map at key k.
func Int64(m map[string]any, k string) int64 {
v, _ := m[k].(int64)
return v
}
// TickDuration reads a uint8/int16/in32 value from a map at key k and converts
// it from ticks to a time.Duration.
func TickDuration[T constraints.Integer](m map[string]any, k string) time.Duration {
var v time.Duration
switch any(*new(T)).(type) {
case uint8:
v = time.Duration(Uint8(m, k))
case int16:
v = time.Duration(Int16(m, k))
case int32:
v = time.Duration(Int32(m, k))
default:
panic("invalid tick duration value type")
}
return v * time.Millisecond * 50
}
// Float32 reads a float32 value from a map at key k.
func Float32(m map[string]any, k string) float32 {
v, _ := m[k].(float32)
return v
}
// Rotation reads a cube.Rotation from the map passed.
func Rotation(m map[string]any) cube.Rotation {
return cube.Rotation{float64(Float32(m, "Yaw")), float64(Float32(m, "Pitch"))}
}
// Float64 reads a float64 value from a map at key k.
func Float64(m map[string]any, k string) float64 {
v, _ := m[k].(float64)
return v
}
// Slice reads a []any value from a map at key k.
func Slice(m map[string]any, k string) []any {
v, _ := m[k].([]any)
return v
}
// Vec3 converts x, y and z values in an NBT map to an mgl64.Vec3.
func Vec3(x map[string]any, k string) mgl64.Vec3 {
if i, ok := x[k].([]any); ok {
if len(i) != 3 {
return mgl64.Vec3{}
}
var v mgl64.Vec3
for index, f := range i {
f32, _ := f.(float32)
v[index] = float64(f32)
}
return v
} else if i, ok := x[k].([]float32); ok {
if len(i) != 3 {
return mgl64.Vec3{}
}
return mgl64.Vec3{float64(i[0]), float64(i[1]), float64(i[2])}
}
return mgl64.Vec3{}
}
// Vec3ToFloat32Slice converts an mgl64.Vec3 to a []float32 with 3 elements.
func Vec3ToFloat32Slice(x mgl64.Vec3) []float32 {
return []float32{float32(x[0]), float32(x[1]), float32(x[2])}
}
// Pos converts x, y and z values in an NBT map to a cube.Pos.
func Pos(x map[string]any, k string) cube.Pos {
if i, ok := x[k].([]any); ok {
if len(i) != 3 {
return cube.Pos{}
}
var v cube.Pos
for index, f := range i {
f32, _ := f.(int32)
v[index] = int(f32)
}
return v
} else if i, ok := x[k].([]int32); ok {
if len(i) != 3 {
return cube.Pos{}
}
return cube.Pos{int(i[0]), int(i[1]), int(i[2])}
}
return cube.Pos{}
}
// PosToInt32Slice converts a cube.Pos to a []int32 with 3 elements.
func PosToInt32Slice(x cube.Pos) []int32 {
return []int32{int32(x[0]), int32(x[1]), int32(x[2])}
}
// MapItem converts an item's name, count, damage (and properties when it is a block) in a map obtained by decoding NBT
// to a world.Item.
func MapItem(x map[string]any, k string) item.Stack {
if m, ok := x[k].(map[string]any); ok {
return Item(m, nil)
}
return item.Stack{}
}
// Item decodes the data of an item into an item stack.
func Item(data map[string]any, s *item.Stack) item.Stack {
disk, tag := s == nil, data
if disk {
t, ok := data["tag"].(map[string]any)
if !ok {
t = map[string]any{}
}
tag = t
a := readItemStack(data, tag)
s = &a
}
readAnvilCost(tag, s)
readDamage(tag, s, disk)
readDisplay(tag, s)
readDragonflyData(tag, s)
readEnchantments(tag, s)
readUnbreakable(tag, s)
return *s
}
// Block decodes the data of a block into a world.Block.
func Block(m map[string]any, k string) world.Block {
if mk, ok := m[k].(map[string]any); ok {
name, _ := mk["name"].(string)
properties, _ := mk["states"].(map[string]any)
b, _ := world.BlockByName(name, properties)
return b
}
return nil
}
// readItemStack reads an item.Stack from the NBT in the map passed.
func readItemStack(m, t map[string]any) item.Stack {
var it world.Item
if blockItem, ok := Block(m, "Block").(world.Item); ok {
it = blockItem
}
if v, ok := world.ItemByName(String(m, "Name"), Int16(m, "Damage")); ok {
it = v
}
if it == nil {
return item.Stack{}
}
if n, ok := it.(world.NBTer); ok {
it = n.DecodeNBT(t).(world.Item)
}
return item.NewStack(it, int(Uint8(m, "Count")))
}
// readDamage reads the damage value stored in the NBT with the Damage tag and saves it to the item.Stack passed.
func readDamage(m map[string]any, s *item.Stack, disk bool) {
if disk {
*s = s.Damage(int(Int16(m, "Damage")))
return
}
*s = s.Damage(int(Int32(m, "Damage")))
}
// readAnvilCost ...
func readAnvilCost(m map[string]any, s *item.Stack) {
*s = s.WithAnvilCost(int(Int32(m, "RepairCost")))
}
// readEnchantments reads the enchantments stored in the ench tag of the NBT passed and stores it into an item.Stack.
func readEnchantments(m map[string]any, s *item.Stack) {
enchantments, ok := m["ench"].([]map[string]any)
if !ok {
for _, e := range Slice(m, "ench") {
if v, ok := e.(map[string]any); ok {
enchantments = append(enchantments, v)
}
}
}
for _, ench := range enchantments {
if t, ok := item.EnchantmentByID(int(Int16(ench, "id"))); ok {
*s = s.WithForcedEnchantments(item.NewEnchantment(t, int(Int16(ench, "lvl"))))
}
}
}
// readDisplay reads the display data present in the display field in the NBT. It includes a custom name of the item
// and the lore.
func readDisplay(m map[string]any, s *item.Stack) {
if display, ok := m["display"].(map[string]any); ok {
if name, ok := display["Name"].(string); ok {
// Only add the custom name if actually set.
*s = s.WithCustomName(name)
}
if lore, ok := display["Lore"].([]string); ok {
*s = s.WithLore(lore...)
} else if lore, ok := display["Lore"].([]any); ok {
loreLines := make([]string, 0, len(lore))
for _, l := range lore {
loreLines = append(loreLines, l.(string))
}
*s = s.WithLore(loreLines...)
}
}
}
// readDragonflyData reads data written to the dragonflyData field in the NBT of an item and adds it to the item.Stack
// passed.
func readDragonflyData(m map[string]any, s *item.Stack) {
if customData, ok := m["dragonflyData"]; ok {
d, ok := customData.([]byte)
if !ok {
if itf, ok := customData.([]any); ok {
for _, v := range itf {
b, _ := v.(byte)
d = append(d, b)
}
}
}
var values []mapValue
if err := gob.NewDecoder(bytes.NewBuffer(d)).Decode(&values); err != nil {
panic("error decoding item user data: " + err.Error())
}
for _, val := range values {
*s = s.WithValue(val.K, val.V)
}
}
}
// readUnbreakable reads the unbreakable value stored in the NBT with the Unbreakable tag and saves it to the item.Stack
// passed.
func readUnbreakable(m map[string]any, s *item.Stack) {
if Bool(m, "Unbreakable") {
*s = s.AsUnbreakable()
}
}
+159
View File
@@ -0,0 +1,159 @@
package nbtconv
import (
"bytes"
"encoding/gob"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/chunk"
"sort"
)
// WriteItem encodes an item stack into a map that can be encoded using NBT.
func WriteItem(s item.Stack, disk bool) map[string]any {
tag := make(map[string]any)
if s.Empty() {
return tag
}
if nbt, ok := s.Item().(world.NBTer); ok {
for k, v := range nbt.EncodeNBT() {
tag[k] = v
}
}
writeAnvilCost(tag, s)
writeDamage(tag, s, disk)
writeDisplay(tag, s)
writeDragonflyData(tag, s)
writeEnchantments(tag, s)
writeUnbreakable(tag, s)
data := make(map[string]any)
if disk {
writeItemStack(data, tag, s)
} else {
for k, v := range tag {
data[k] = v
}
}
return data
}
// WriteBlock encodes a world.Block into a map that can be encoded using NBT.
func WriteBlock(b world.Block) map[string]any {
name, properties := b.EncodeBlock()
return map[string]any{
"name": name,
"states": properties,
"version": chunk.CurrentBlockVersion,
}
}
// writeItemStack writes the name, metadata value, count and NBT of an item to a map ready for NBT encoding.
func writeItemStack(m, t map[string]any, s item.Stack) {
m["Name"], m["Damage"] = s.Item().EncodeItem()
if b, ok := s.Item().(world.Block); ok {
v := map[string]any{}
writeBlock(v, b)
m["Block"] = v
}
m["Count"] = byte(s.Count())
if len(t) > 0 {
m["tag"] = t
}
}
// writeBlock writes the name, properties and version of a block to a map ready for NBT encoding.
func writeBlock(m map[string]any, b world.Block) {
m["name"], m["states"] = b.EncodeBlock()
m["version"] = chunk.CurrentBlockVersion
}
// writeDragonflyData writes additional data associated with an item.Stack to a map for NBT encoding.
func writeDragonflyData(m map[string]any, s item.Stack) {
if v := s.Values(); len(v) != 0 {
buf := new(bytes.Buffer)
if err := gob.NewEncoder(buf).Encode(mapToSlice(v)); err != nil {
panic("error encoding item user data: " + err.Error())
}
m["dragonflyData"] = buf.Bytes()
}
}
// mapToSlice converts a map to a slice of the type mapValue and orders the slice by the keys in the map to ensure a
// deterministic order.
func mapToSlice(m map[string]any) []mapValue {
values := make([]mapValue, 0, len(m))
for k, v := range m {
values = append(values, mapValue{K: k, V: v})
}
sort.Slice(values, func(i, j int) bool {
return values[i].K < values[j].K
})
return values
}
// mapValue represents a value in a map. It is used to convert maps to a slice and order the slice before encoding to
// NBT to ensure a deterministic output.
type mapValue struct {
K string
V any
}
// writeEnchantments writes the enchantments of an item to a map for NBT encoding.
func writeEnchantments(m map[string]any, s item.Stack) {
if len(s.Enchantments()) != 0 {
var enchantments []map[string]any
for _, e := range s.Enchantments() {
if eType, ok := item.EnchantmentID(e.Type()); ok {
enchantments = append(enchantments, map[string]any{
"id": int16(eType),
"lvl": int16(e.Level()),
})
}
}
m["ench"] = enchantments
}
}
// writeDisplay writes the display name and lore of an item to a map for NBT encoding.
func writeDisplay(m map[string]any, s item.Stack) {
name, lore := s.CustomName(), s.Lore()
v := map[string]any{}
if name != "" {
v["Name"] = name
}
if len(lore) != 0 {
v["Lore"] = lore
}
if len(v) != 0 {
m["display"] = v
}
}
// writeDamage writes the damage to an item.Stack (either an int16 for disk or int32 for network) to a map for NBT
// encoding.
func writeDamage(m map[string]any, s item.Stack, disk bool) {
if v, ok := m["Damage"]; !ok || v.(int16) == 0 {
if _, ok := s.Item().(item.Durable); ok {
if disk {
m["Damage"] = int16(s.MaxDurability() - s.Durability())
} else {
m["Damage"] = int32(s.MaxDurability() - s.Durability())
}
}
}
}
// writeAnvilCost ...
func writeAnvilCost(m map[string]any, s item.Stack) {
if cost := s.AnvilCost(); cost > 0 {
m["RepairCost"] = int32(cost)
}
}
// writeUnbreakable writes the unbreakable tag to an item stack if it is unbreakable.
func writeUnbreakable(m map[string]any, s item.Stack) {
if s.Unbreakable() {
m["Unbreakable"] = byte(1)
}
}
+81
View File
@@ -0,0 +1,81 @@
package packbuilder
import (
"encoding/json"
"fmt"
"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(reg world.BlockRegistry, dir string) (count int, lang []string) {
if err := os.MkdirAll(filepath.Join(dir, "models/blocks"), os.ModePerm); err != nil {
panic(err)
}
if err := os.MkdirAll(filepath.Join(dir, "textures/blocks"), os.ModePerm); err != nil {
panic(err)
}
textureData := make(map[string]any)
for identifier, blk := range reg.CustomBlocks() {
b, ok := blk.(world.CustomBlockBuildable)
if !ok {
continue
}
name := strings.Split(identifier, ":")[1]
lang = append(lang, fmt.Sprintf("tile.%s.name=%s", identifier, b.Name()))
for name, texture := range b.Textures() {
textureData[name] = map[string]string{"textures": "textures/blocks/" + name}
buildBlockTexture(dir, name, texture)
}
if b.Geometry() != nil {
if err := os.WriteFile(filepath.Join(dir, "models/blocks", fmt.Sprintf("%s.geo.json", name)), b.Geometry(), 0666); err != nil {
panic(err)
}
}
count++
}
buildBlockAtlas(dir, map[string]any{
"resource_pack_name": "vanilla",
"texture_name": "atlas.terrain",
"padding": 8,
"num_mip_levels": 4,
"texture_data": textureData,
})
return
}
// buildBlockTexture creates a PNG file for the block from the provided image and name and writes it to the pack.
func buildBlockTexture(dir, name string, img image.Image) {
texture, err := os.Create(filepath.Join(dir, fmt.Sprintf("textures/blocks/%s.png", name)))
if err != nil {
panic(err)
}
if err := png.Encode(texture, img); err != nil {
_ = texture.Close()
panic(err)
}
if err := texture.Close(); err != nil {
panic(err)
}
}
// buildBlockAtlas creates the identifier to texture mapping and writes it to the pack.
func buildBlockAtlas(dir string, atlas map[string]any) {
b, err := json.Marshal(atlas)
if err != nil {
panic(err)
}
if err := os.WriteFile(filepath.Join(dir, "textures/terrain_texture.json"), b, 0666); err != nil {
panic(err)
}
}
+70
View File
@@ -0,0 +1,70 @@
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.
)
// buildItems builds all the item-related files for the resource pack. This includes textures, language
// entries and item atlas.
func buildItems(dir string) (count int, lang []string) {
if err := os.Mkdir(filepath.Join(dir, "items"), os.ModePerm); err != nil {
panic(err)
}
if err := os.MkdirAll(filepath.Join(dir, "textures/items"), os.ModePerm); err != nil {
panic(err)
}
textureData := make(map[string]any)
for _, item := range world.CustomItems() {
identifier, _ := item.EncodeItem()
lang = append(lang, fmt.Sprintf("item.%s.name=%s", identifier, item.Name()))
name := strings.Split(identifier, ":")[1]
textureData[identifier] = map[string]string{"textures": fmt.Sprintf("textures/items/%s.png", name)}
buildItemTexture(dir, name, item.Texture())
count++
}
buildItemAtlas(dir, map[string]any{
"resource_pack_name": "vanilla",
"texture_name": "atlas.items",
"texture_data": textureData,
})
return
}
// buildItemTexture creates a PNG file for the item from the provided image and name and writes it to the pack.
func buildItemTexture(dir, name string, img image.Image) {
texture, err := os.Create(filepath.Join(dir, "textures/items", name+".png"))
if err != nil {
panic(err)
}
if err := png.Encode(texture, img); err != nil {
_ = texture.Close()
panic(err)
}
if err := texture.Close(); err != nil {
panic(err)
}
}
// buildItemAtlas creates the identifier to texture mapping and writes it to the pack.
func buildItemAtlas(dir string, atlas map[string]any) {
b, err := json.Marshal(atlas)
if err != nil {
panic(err)
}
if err := os.WriteFile(filepath.Join(dir, "textures/item_texture.json"), b, 0666); err != nil {
panic(err)
}
}
+17
View File
@@ -0,0 +1,17 @@
package packbuilder
import (
"os"
"path/filepath"
"strings"
)
// buildLanguageFile creates a lang file and writes all of the language entries to the pack.
func buildLanguageFile(dir string, lang []string) {
if err := os.Mkdir(filepath.Join(dir, "texts"), os.ModePerm); err != nil {
panic(err)
}
if err := os.WriteFile(filepath.Join(dir, "texts/en_US.lang"), []byte(strings.Join(lang, "\n")), 0666); err != nil {
panic(err)
}
}
+53
View File
@@ -0,0 +1,53 @@
package packbuilder
import (
"encoding/json"
"github.com/google/uuid"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/resource"
"os"
"path/filepath"
"strconv"
"strings"
)
// buildManifest creates a JSON manifest file for the client to be able to read the resource pack. It creates
// basic information and writes it to the pack.
func buildManifest(dir string, headerUUID, moduleUUID uuid.UUID) {
m, err := json.Marshal(resource.Manifest{
FormatVersion: 2,
Header: resource.Header{
Name: "dragonfly auto-generated resource pack",
Description: "This resource pack contains auto-generated content from dragonfly",
UUID: headerUUID,
Version: [3]int{0, 0, 1},
MinimumGameVersion: parseVersion(protocol.CurrentVersion),
},
Modules: []resource.Module{
{
UUID: moduleUUID.String(),
Description: "This resource pack contains auto-generated content from dragonfly",
Type: "resources",
Version: [3]int{0, 0, 1},
},
},
})
if err != nil {
panic(err)
}
if err := os.WriteFile(filepath.Join(dir, "manifest.json"), m, 0666); err != nil {
panic(err)
}
}
// parseVersion parses the version passed in the format of a.b.c as a [3]int.
func parseVersion(ver string) [3]int {
frag := strings.Split(ver, ".")
if len(frag) != 3 {
panic("invalid version number " + ver)
}
a, _ := strconv.ParseInt(frag[0], 10, 64)
b, _ := strconv.ParseInt(frag[1], 10, 64)
c, _ := strconv.ParseInt(frag[2], 10, 64)
return [3]int{int(a), int(b), int(c)}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

@@ -0,0 +1,52 @@
package packbuilder
import (
_ "embed"
"os"
"github.com/df-mc/dragonfly/server/world"
"github.com/sandertv/gophertunnel/minecraft/resource"
"golang.org/x/mod/sumdb/dirhash"
)
//go:embed pack_icon.png
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(reg world.BlockRegistry) (*resource.Pack, bool) {
dir, err := os.MkdirTemp("", "dragonfly_resource_pack-")
if err != nil {
panic(err)
}
defer os.RemoveAll(dir)
var assets int
var lang []string
itemCount, itemLang := buildItems(dir)
assets += itemCount
lang = append(lang, itemLang...)
blockCount, blockLang := buildBlocks(reg, dir)
assets += blockCount
lang = append(lang, blockLang...)
if assets > 0 {
buildLanguageFile(dir, lang)
if err := os.WriteFile(dir+"/pack_icon.png", packIcon, 0666); err != nil {
panic(err)
}
hash, err := dirhash.HashDir(dir, "", dirhash.Hash1)
if err != nil {
panic(err)
}
var header, module [16]byte
copy(header[:], hash)
copy(module[:], hash[16:])
buildManifest(dir, header, module)
return resource.MustReadPath(dir), true
}
return nil, false
}
+49
View File
@@ -0,0 +1,49 @@
package sliceutil
import "slices"
// Convert converts a slice of type B to a slice of type A. Convert panics if B
// cannot be type asserted to type A.
func Convert[A, B any, S ~[]B](v S) []A {
a := make([]A, len(v))
for i, b := range v {
a[i] = (any)(b).(A)
}
return a
}
// SearchValue iterates through slice v, calling function f for every element.
// If true is returned in this function, the respective element is returned and
// ok is true. If the function f does not return true for any element, false is
// returned.
func SearchValue[A any, S ~[]A](v S, f func(a A) bool) (a A, ok bool) {
for _, val := range v {
if f(val) {
return val, true
}
}
return
}
// Filter iterates over elements of collection, returning an array of all
// elements function c returns true for.
func Filter[E any](s []E, c func(E) bool) []E {
a := make([]E, 0, len(s))
for _, e := range s {
if c(e) {
a = append(a, e)
}
}
return a
}
// DeleteVal deletes the first occurrence of a value in a slice of the type E
// and returns a new slice without the value.
func DeleteVal[E any](s []E, v E) []E {
for i, vs := range s {
if (any)(v) == (any)(vs) {
return slices.Delete(s, i, i+1)
}
}
return s
}