diff --git a/main.go b/main.go index 89c129769..91d101814 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,13 @@ package main import ( "fmt" "github.com/df-mc/dragonfly/server" + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/item/creative" + "github.com/df-mc/dragonfly/server/player" "github.com/df-mc/dragonfly/server/player/chat" + "github.com/df-mc/dragonfly/server/world" "github.com/pelletier/go-toml" "github.com/sirupsen/logrus" "os" @@ -21,11 +27,19 @@ func main() { log.Fatalln(err) } + for _, direction := range cube.Directions() { + world.RegisterBlock(block.Pig{Facing: direction}) + } + world.RegisterItem(block.Pig{}) + creative.RegisterItem(item.NewStack(block.Pig{}, 1)) + srv := conf.New() srv.CloseOnProgramEnd() srv.Listen() - for srv.Accept(nil) { + for srv.Accept(func(p *player.Player) { + p.Inventory().AddItem(item.NewStack(block.Pig{}, 64)) + }) { } } diff --git a/server/block/block.go b/server/block/block.go index 3ea09166e..3795330c3 100644 --- a/server/block/block.go +++ b/server/block/block.go @@ -2,6 +2,7 @@ package block import ( "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/customblock" "github.com/df-mc/dragonfly/server/block/model" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" @@ -42,13 +43,6 @@ type LightEmitter interface { LightEmissionLevel() uint8 } -// PermutableLightEmitter represents a permutable custom block that emits light when placed. -type PermutableLightEmitter interface { - // LightEmissionLevel returns the light emission level of the block, a number from 0-15 where 15 is the - // brightest and 0 means it doesn't emit light at all. - LightEmissionLevel() (uint8, bool, map[string]uint8) -} - // LightDiffuser represents a block that diffuses light. This means that a specific amount of light levels // will be subtracted when light passes through the block. // Blocks that do not implement LightDiffuser will be assumed to be solid: Light will not be able to pass @@ -60,14 +54,6 @@ type LightDiffuser interface { LightDiffusionLevel() uint8 } -// PermutableLightDiffuser represents a permutable custom block that diffuses light. -type PermutableLightDiffuser interface { - // LightDiffusionLevel returns the amount of light levels that is subtracted when light passes through - // this block. Some blocks, such as leaves, have this behaviour. A diffusion level of 15 means that all - // light will be completely blocked when it passes through the block. - LightDiffusionLevel() (uint8, bool, map[string]uint8) -} - // Replaceable represents a block that may be replaced by another block automatically. An example is grass, // which may be replaced by clicking it with another block. type Replaceable interface { @@ -95,22 +81,9 @@ type Frictional interface { Friction() float64 } -// PermutableFrictional represents a permutable custom block that may have a custom friction value. -type PermutableFrictional interface { - // Friction returns the block's friction value. - Friction() (float64, bool, map[string]float64) -} - -// Rotatable represents a custom block that may be rotated. -type Rotatable interface { - // Rotation returns the rotation of the block as an mgl64.Vec3. - Rotation() mgl64.Vec3 -} - -// PermutableRotatable represents a permutable custom block that may be rotated. -type PermutableRotatable interface { - // Rotation returns the rotation of the block as an mgl64.Vec3. - Rotation() (mgl64.Vec3, bool, map[string]mgl64.Vec3) +type Permutable interface { + States() map[string][]any + Permutations() []customblock.Permutation } func calculateFace(user item.User, placePos cube.Pos) cube.Face { @@ -247,12 +220,6 @@ type Flammable interface { FlammabilityInfo() FlammabilityInfo } -// PermutableFlammable is an interface for permutable custom blocks that can catch on fire. -type PermutableFlammable interface { - // FlammabilityInfo returns information about a block's behavior involving fire. - FlammabilityInfo() (FlammabilityInfo, bool, map[string]FlammabilityInfo) -} - // FlammabilityInfo contains values related to block behaviors involving fire. type FlammabilityInfo struct { // Encouragement is the chance a block will catch on fire during attempted fire spread. diff --git a/server/block/break_info.go b/server/block/break_info.go index fcaccd4d9..fceeb8160 100644 --- a/server/block/break_info.go +++ b/server/block/break_info.go @@ -17,12 +17,6 @@ type Breakable interface { BreakInfo() BreakInfo } -// PermutableBreakable represents a permutable custom block that may be broken by a player in survival mode. -type PermutableBreakable interface { - // BreakInfo returns information of the block related to the breaking of it. - BreakInfo() (BreakInfo, bool, map[string]BreakInfo) -} - // BreakDuration returns the base duration that breaking the block passed takes when being broken using the // item passed. func BreakDuration(b world.Block, i item.Stack) time.Duration { diff --git a/server/block/customblock/geometry.go b/server/block/customblock/geometry.go deleted file mode 100644 index 8bdea1417..000000000 --- a/server/block/customblock/geometry.go +++ /dev/null @@ -1,95 +0,0 @@ -package customblock - -import ( - "github.com/go-gl/mathgl/mgl32" - "github.com/go-gl/mathgl/mgl64" - "math" -) - -// Geometries represents the JSON structure of a vanilla geometry file. It contains a format version and a slice of -// unique geometries. -type Geometries struct { - FormatVersion string `json:"format_version"` - Geometry []Geometry `json:"minecraft:geometry"` -} - -// Geometry represents a single geometry that contains bones and other information. -type Geometry struct { - Description struct { - Identifier string `json:"identifier"` - TextureWidth int `json:"texture_width"` - TextureHeight int `json:"texture_height"` - VisibleBoundsWidth float64 `json:"visible_bounds_width"` - VisibleBoundsHeight float64 `json:"visible_bounds_height"` - VisibleBoundsOffset mgl64.Vec3 `json:"visible_bounds_offset"` - } `json:"description"` - Bones []struct { - Name string `json:"name"` - Pivot mgl64.Vec3 `json:"pivot,omitempty"` - Rotation mgl64.Vec3 `json:"rotation,omitempty"` - Cubes []struct { - Origin mgl64.Vec3 `json:"origin"` - Size mgl64.Vec3 `json:"size"` - UV any `json:"uv"` - Pivot mgl64.Vec3 `json:"pivot,omitempty"` - Rotation mgl64.Vec3 `json:"rotation,omitempty"` - Inflate float64 `json:"inflate,omitempty"` - } `json:"cubes"` - } `json:"bones"` -} - -// Encode encodes the geometry into a JSON component. -func (g Geometry) Encode() map[string]any { - origin, size := vec64To32(g.Origin()), vec64To32(g.Size()) - box := map[string]any{ - "enabled": byte(0x1), - "origin": []float32{ - origin.X(), - origin.Y(), - origin.Z(), - }, - "size": []float32{ - size.X(), - size.Y(), - size.Z(), - }, - } - return map[string]any{ - "minecraft:aim_collision": box, - "minecraft:collision_box": box, - "minecraft:pick_collision": map[string]any{ - "enabled": uint8(1), - "origin": origin[:], - "size": size[:], - }, - } -} - -// Origin returns the origin of the geometry. It is calculated by using the smallest origin points of all cubes. -func (g Geometry) Origin() (x mgl64.Vec3) { - for _, bone := range g.Bones { - for _, cube := range bone.Cubes { - x[0] = math.Min(x[0], cube.Origin.X()) - x[1] = math.Min(x[1], cube.Origin.Y()) - x[2] = math.Min(x[2], cube.Origin.Z()) - } - } - return -} - -// Size returns the size of the geometry. It is calculated by using the largest size of all cubes. -func (g Geometry) Size() (x mgl64.Vec3) { - for _, bone := range g.Bones { - for _, cube := range bone.Cubes { - x[0] = math.Max(x[0], math.Abs(cube.Size.X())) - x[1] = math.Max(x[1], math.Abs(cube.Size.Y())) - x[2] = math.Max(x[2], math.Abs(cube.Size.Z())) - } - } - return -} - -// vec64To32 converts a mgl64.Vec3 to a mgl32.Vec3. -func vec64To32(vec3 mgl64.Vec3) mgl32.Vec3 { - return mgl32.Vec3{float32(vec3[0]), float32(vec3[1]), float32(vec3[2])} -} diff --git a/server/block/customblock/material.go b/server/block/customblock/material.go index 031e34af0..1a7590574 100644 --- a/server/block/customblock/material.go +++ b/server/block/customblock/material.go @@ -16,9 +16,9 @@ type Material struct { // occlusion based on the render method given. func NewMaterial(texture string, method Method) Material { return Material{ - faceDimming: true, texture: texture, renderMethod: method, + faceDimming: true, ambientOcclusion: method.AmbientOcclusion(), } } @@ -52,15 +52,7 @@ func (m Material) Encode() map[string]any { return map[string]any{ "texture": m.texture, "render_method": m.renderMethod.String(), - "face_dimming": boolByte(m.faceDimming), - "ambient_occlusion": boolByte(m.ambientOcclusion), - } -} - -// boolByte returns 1 if the bool passed is true, or 0 if it is false. -func boolByte(b bool) uint8 { - if b { - return 1 + "face_dimming": m.faceDimming, + "ambient_occlusion": m.ambientOcclusion, } - return 0 } diff --git a/server/block/customblock/model.go b/server/block/customblock/model.go deleted file mode 100644 index 90bf1563b..000000000 --- a/server/block/customblock/model.go +++ /dev/null @@ -1,51 +0,0 @@ -package customblock - -import ( - "fmt" -) - -// Model represents the model of a custom block. It can contain multiple materials applied to different parts of the -// model, as well as a reference to its geometry. -type Model struct { - materials map[Target]Material - geometry Geometry -} - -// NewModel returns a new Model with the provided information. If the size is larger than 16x16x16, this method will -// panic since the client does not allow models larger than a single block. -func NewModel(geometry Geometry) Model { - if size := geometry.Size(); size.X() > 16 || size.Y() > 16 || size.Z() > 16 { - panic(fmt.Errorf("model size cannot exceed 16x16x16, got %v", size)) - } - return Model{ - materials: make(map[Target]Material), - geometry: geometry, - } -} - -// WithMaterial returns a copy of the Model with the provided material. -func (m Model) WithMaterial(target Target, material Material) Model { - m.materials[target] = material - return m -} - -// Encode returns the model encoded as a map[string]any. -func (m Model) Encode() map[string]any { - materials := map[string]any{} - for target, material := range m.materials { - materials[target.String()] = material.Encode() - } - model := map[string]any{"minecraft:material_instances": map[string]any{ - "mappings": map[string]any{}, - "materials": materials, - }} - if identifier := m.geometry.Description.Identifier; len(identifier) > 0 { - for k, v := range m.geometry.Encode() { - model[k] = v - } - model["minecraft:geometry"] = map[string]any{"value": identifier} - } else { - model["minecraft:unit_cube"] = map[string]any{} - } - return model -} diff --git a/server/block/customblock/permutations.go b/server/block/customblock/permutations.go new file mode 100644 index 000000000..24df9be93 --- /dev/null +++ b/server/block/customblock/permutations.go @@ -0,0 +1,23 @@ +package customblock + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl64" +) + +type Properties struct { + CollisionBox cube.BBox + Cube bool + Geometry string + MapColour string + Rotation cube.Pos + Scale mgl64.Vec3 + SelectionBox cube.BBox + Textures map[string]Material + Translation mgl64.Vec3 +} + +type Permutation struct { + Properties + Condition string +} diff --git a/server/block/pig.go b/server/block/pig.go index 0479c33d6..47f2a6c2e 100644 --- a/server/block/pig.go +++ b/server/block/pig.go @@ -1,7 +1,6 @@ package block import ( - "encoding/json" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/block/customblock" "github.com/df-mc/dragonfly/server/item" @@ -43,10 +42,10 @@ func (p Pig) BreakInfo() BreakInfo { } // Textures ... -func (p Pig) Textures() (map[customblock.Target]image.Image, map[string]map[customblock.Target]image.Image, customblock.Method) { - return map[customblock.Target]image.Image{ - customblock.MaterialTargetAll(): p.Texture(), - }, nil, customblock.AlphaTestRenderMethod() +func (p Pig) Textures() map[string]image.Image { + return map[string]image.Image{ + "pig": p.Texture(), + } } // Texture ... @@ -63,18 +62,13 @@ func (p Pig) Texture() image.Image { return img } -// Geometries ... -func (p Pig) Geometries() (customblock.Geometry, map[string]customblock.Geometry, bool) { - b, err := os.ReadFile("skull.geo.json") - if err != nil { - panic(err) - } - var geometry customblock.Geometries - err = json.Unmarshal(b, &geometry) +// Geometry ... +func (p Pig) Geometry() []byte { + data, err := os.ReadFile("skull.geo.json") if err != nil { panic(err) } - return geometry.Geometry[0], nil, true + return data } // UseOnBlock ... @@ -84,20 +78,11 @@ func (p Pig) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.Wor return } - p.Facing = user.Rotation().Direction().RotateRight() + p.Facing = user.Rotation().Direction() place(w, pos, p, user, ctx) return placed(ctx) } -// Rotation ... -func (p Pig) Rotation() (mgl64.Vec3, bool, map[string]mgl64.Vec3) { - return mgl64.Vec3{}, false, map[string]mgl64.Vec3{ - "query.block_property('direction') == 1": {0, 180, 0}, - "query.block_property('direction') == 2": {0, 90, 0}, - "query.block_property('direction') == 3": {0, 270, 0}, - } -} - // EncodeItem ... func (p Pig) EncodeItem() (name string, meta int16) { return "dragonfly:pig", 0 @@ -105,7 +90,7 @@ func (p Pig) EncodeItem() (name string, meta int16) { // EncodeBlock ... func (p Pig) EncodeBlock() (string, map[string]any) { - return "dragonfly:pig", map[string]any{"direction": int32(p.Facing)} + return "dragonfly:pig", map[string]any{"rotation": int32(p.Facing)} } // pigHash ... @@ -113,5 +98,45 @@ var pigHash = NextHash() // Hash ... func (p Pig) Hash() uint64 { - return pigHash | uint64(p.Facing)<<8 + return pigHash | (uint64(p.Facing) << 8) +} + +func (p Pig) Properties() customblock.Properties { + return customblock.Properties{ + CollisionBox: cube.Box(0.25, 0, 0.25, 0.75, 0.5, 0.75), + SelectionBox: cube.Box(0.25, 0, 0.25, 0.75, 0.5, 0.75), + Geometry: "geometry.skull", + Textures: map[string]customblock.Material{ + "*": customblock.NewMaterial("pig", customblock.OpaqueRenderMethod()), + }, + } +} + +func (p Pig) States() map[string][]any { + return map[string][]any{ + "rotation": {int32(0), int32(1), int32(2), int32(3)}, + } +} + +func (p Pig) Permutations() []customblock.Permutation { + return []customblock.Permutation{ + { + Condition: "query.block_state('rotation') == 1", + Properties: customblock.Properties{ + Rotation: cube.Pos{0, 3, 0}, + }, + }, + { + Condition: "query.block_state('rotation') == 2", + Properties: customblock.Properties{ + Rotation: cube.Pos{0, 2, 0}, + }, + }, + { + Condition: "query.block_state('rotation') == 3", + Properties: customblock.Properties{ + Rotation: cube.Pos{0, 1, 0}, + }, + }, + } } diff --git a/server/internal/blockinternal/builder.go b/server/internal/blockinternal/builder.go index 909fb45ae..bac24aa15 100644 --- a/server/internal/blockinternal/builder.go +++ b/server/internal/blockinternal/builder.go @@ -1,13 +1,9 @@ package blockinternal import ( - "fmt" - "github.com/df-mc/dragonfly/server/block/customblock" - "github.com/df-mc/dragonfly/server/world" - "github.com/segmentio/fasthash/fnv1" + "github.com/df-mc/dragonfly/server/item/category" "golang.org/x/exp/maps" "golang.org/x/exp/slices" - "strings" ) // ComponentBuilder represents a builder that can be used to construct a block components map to be sent to a client. @@ -16,24 +12,30 @@ type ComponentBuilder struct { properties []map[string]any components map[string]any - identifier string - group []world.CustomBlock + identifier string + menuCategory category.Category } // NewComponentBuilder returns a new component builder with the provided block data. -func NewComponentBuilder(identifier string, group []world.CustomBlock) *ComponentBuilder { +func NewComponentBuilder(identifier string, components map[string]any) *ComponentBuilder { + if components == nil { + components = map[string]any{} + } return &ComponentBuilder{ permutations: make(map[string]map[string]any), - components: make(map[string]any), + components: components, - identifier: identifier, - group: group, + identifier: identifier, + menuCategory: category.Construction(), } } // AddProperty adds the provided property to the builder. -func (builder *ComponentBuilder) AddProperty(value map[string]any) { - builder.properties = append(builder.properties, value) +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. @@ -58,22 +60,31 @@ func (builder *ComponentBuilder) AddPermutation(condition string, components map } } +// SetMenuCategory sets the menu category for the block. +func (builder *ComponentBuilder) SetMenuCategory(category category.Category) { + builder.menuCategory = category +} + // Construct constructs the final block components map and returns it. It also applies the default properties required // for the block to work without modifying the original maps in the builder. func (builder *ComponentBuilder) Construct() map[string]any { properties := slices.Clone(builder.properties) components := maps.Clone(builder.components) - builder.applyDefaultProperties(&properties) - builder.applyDefaultComponents(components) - result := map[string]any{"components": components} + result := map[string]any{ + "components": components, + "molangVersion": int32(10), + "menu_category": map[string]any{ + "category": builder.menuCategory.String(), + "group": builder.menuCategory.Group(), + }, + } if len(properties) > 0 { result["properties"] = properties } permutations := maps.Clone(builder.permutations) if len(permutations) > 0 { - result["molangVersion"] = int32(0) result["permutations"] = []map[string]any{} for condition, values := range permutations { result["permutations"] = append(result["permutations"].([]map[string]any), map[string]any{ @@ -84,66 +95,3 @@ func (builder *ComponentBuilder) Construct() map[string]any { } return result } - -// 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) { - var names []string - values := make(map[string][]any) - for _, b := range builder.group { - _, properties := b.EncodeBlock() - for name, value := range properties { - if _, ok := values[name]; !ok { - names = append(names, name) - values[name] = []any{} - } - if slices.IndexFunc(values[name], func(i any) bool { - return i == value - }) >= 0 { - // Already exists, skip. - continue - } - values[name] = append(values[name], value) - } - } - for _, name := range names { - *x = append(*x, map[string]any{"enum": values[name], "name": name}) - } -} - -// 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 map[string]any) { - base := builder.group[0] - name := strings.Split(builder.identifier, ":")[1] - - geometry, permutationGeometries, _ := base.Geometries() - generalModel := customblock.NewModel(geometry) - - textures, permutationTextures, method := base.Textures() - for target := range textures { - generalModel = generalModel.WithMaterial(target, customblock.NewMaterial(fmt.Sprintf("%v_%v", name, target.Name()), method)) - } - - permutationModels := make(map[string]customblock.Model) - for permutation, permutationSpecificGeometry := range permutationGeometries { - permutationModels[permutation] = customblock.NewModel(permutationSpecificGeometry) - } - for permutation, permutationSpecificTextures := range permutationTextures { - h := fnv1.HashString64(permutation) - for target := range permutationSpecificTextures { - if _, ok := permutationModels[permutation]; !ok { - // If we don't have a model for this permutation, re-use the base geometry and create a new model. - permutationModels[permutation] = customblock.NewModel(geometry) - } - permutationModel := permutationModels[permutation] - permutationModels[permutation] = permutationModel.WithMaterial(target, customblock.NewMaterial(fmt.Sprintf("%s_%s_%x", name, target.Name(), h), method)) - } - } - for permutation, model := range permutationModels { - builder.AddPermutation(permutation, model.Encode()) - } - for key, value := range generalModel.Encode() { - x[key] = value - } -} diff --git a/server/internal/blockinternal/components.go b/server/internal/blockinternal/components.go index 6dc2d3e48..cd832522e 100644 --- a/server/internal/blockinternal/components.go +++ b/server/internal/blockinternal/components.go @@ -2,137 +2,114 @@ 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 of the given custom block group. If the group has no components, a nil map // and false are returned. -func Components(identifier string, group []world.CustomBlock) (map[string]any, error) { - if len(group) == 0 { - // We don't have any blocks in the group so return false. - return nil, nil +func Components(identifier string, b world.CustomBlock) map[string]any { + components := componentsFromProperties(b.Properties()) + builder := NewComponentBuilder(identifier, components) + if emitter, ok := b.(block.LightEmitter); ok { + builder.AddComponent("minecraft:block_light_emission", map[string]any{ + "emission": float32(emitter.LightEmissionLevel() / 15), + }) } - - base := group[0] - builder := NewComponentBuilder(identifier, group) - if r, ok := base.(block.Rotatable); ok { - rotation := r.Rotation() - builder.AddComponent("minecraft:rotation", map[string]any{ - "x": float32(rotation.X()), - "y": float32(rotation.Y()), - "z": float32(rotation.Z()), + if diffuser, ok := b.(block.LightDiffuser); ok { + builder.AddComponent("minecraft:block_light_filter", map[string]any{ + "lightLevel": int32(diffuser.LightDiffusionLevel()), }) } - if r, ok := base.(block.PermutableRotatable); ok { - rotation, exists, rotations := r.Rotation() - if exists { - builder.AddComponent("minecraft:rotation", map[string]any{ - "x": float32(rotation.X()), - "y": float32(rotation.Y()), - "z": float32(rotation.Z()), - }) - } - for condition, value := range rotations { - builder.AddPermutation(condition, map[string]any{"minecraft:rotation": map[string]any{ - "x": float32(value.X()), - "y": float32(value.Y()), - "z": float32(value.Z()), - }}) - } + if breakable, ok := b.(block.Breakable); ok { + info := breakable.BreakInfo() + builder.AddComponent("minecraft:destructible_by_mining", map[string]any{"value": float32(info.Hardness)}) } - if l, ok := base.(block.LightEmitter); ok { - builder.AddComponent("minecraft:block_light_emission", map[string]any{ - "emission": float32(l.LightEmissionLevel() / 15), + 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 l, ok := base.(block.PermutableLightEmitter); ok { - level, exists, levels := l.LightEmissionLevel() - if exists { - builder.AddComponent("minecraft:block_light_emission", map[string]any{ - "emission": float32(level / 15), - }) + if permutable, ok := b.(block.Permutable); ok { + for name, values := range permutable.States() { + builder.AddProperty(name, values) } - for condition, value := range levels { - builder.AddPermutation(condition, map[string]any{"minecraft:block_light_emission": map[string]any{ - "emission": float32(value / 15), - }}) + for _, permutation := range permutable.Permutations() { + builder.AddPermutation(permutation.Condition, componentsFromProperties(permutation.Properties)) } } - if d, ok := base.(block.LightDiffuser); ok { - builder.AddComponent("minecraft:block_light_filter", map[string]any{ - "lightLevel": int32(d.LightDiffusionLevel()), - }) + if item, ok := b.(world.CustomItem); ok { + builder.SetMenuCategory(item.Category()) } - if d, ok := base.(block.PermutableLightDiffuser); ok { - level, exists, levels := d.LightDiffusionLevel() - if exists { - builder.AddComponent("minecraft:block_light_filter", map[string]any{"lightLevel": int32(level)}) - } - for condition, value := range levels { - builder.AddPermutation(condition, map[string]any{"minecraft:block_light_filter": map[string]any{ - "lightLevel": int32(value), - }}) - } + return builder.Construct() +} + +func componentsFromProperties(props customblock.Properties) map[string]any { + components := make(map[string]any) + if props.CollisionBox != (cube.BBox{}) { + components["minecraft:collision_box"] = bboxComponent(props.CollisionBox) } - if i, ok := base.(block.Breakable); ok { - info := i.BreakInfo() - builder.AddComponent("minecraft:destroy_time", map[string]any{"value": float32(info.Hardness)}) - // TODO: Explosion resistance. + if props.SelectionBox != (cube.BBox{}) { + components["minecraft:selection_box"] = bboxComponent(props.SelectionBox) } - if i, ok := base.(block.PermutableBreakable); ok { - info, exists, infos := i.BreakInfo() - if exists { - builder.AddComponent("minecraft:destroy_time", map[string]any{"value": float32(info.Hardness)}) - } - for condition, value := range infos { - builder.AddPermutation(condition, map[string]any{"minecraft:destroy_time": map[string]any{ - "value": float32(value.Hardness), - }}) - } - // TODO: Explosion resistance. + if props.Geometry != "" { + components["minecraft:geometry"] = map[string]any{"identifier": props.Geometry} + } else if props.Cube { + components["minecraft:unit_cube"] = map[string]any{} } - if f, ok := base.(block.Frictional); ok { - builder.AddComponent("minecraft:friction", map[string]any{"value": float32(f.Friction())}) + if props.MapColour != "" { + components["minecraft:map_color"] = map[string]any{"value": props.MapColour} } - if f, ok := base.(block.PermutableFrictional); ok { - friction, exists, frictions := f.Friction() - if exists { - builder.AddComponent("minecraft:friction", map[string]any{"value": float32(friction)}) + if props.Textures != nil { + materials := map[string]any{} + for target, material := range props.Textures { + materials[target] = material.Encode() } - for condition, value := range frictions { - builder.AddPermutation(condition, map[string]any{"minecraft:friction": map[string]any{ - "value": float32(value), - }}) + components["minecraft:material_instances"] = map[string]any{ + "mappings": map[string]any{}, + "materials": materials, } } - if f, ok := base.(block.Flammable); ok { - info := f.FlammabilityInfo() - builder.AddComponent("minecraft:flammable", map[string]any{ - "flame_odds": int32(info.Encouragement), - "burn_odds": int32(info.Flammability), - }) + 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 f, ok := base.(block.PermutableFlammable); ok { - info, exists, infos := f.FlammabilityInfo() - if exists { - builder.AddComponent("minecraft:flammable", map[string]any{ - "flame_odds": int32(info.Encouragement), - "burn_odds": int32(info.Flammability), - }) - } - for condition, value := range infos { - builder.AddPermutation(condition, map[string]any{"minecraft:flammable": map[string]any{ - "flame_odds": int32(value.Encouragement), - "burn_odds": int32(value.Flammability), - }}) - } + if props.Translation != (mgl64.Vec3{}) { + transformation["TX"] = float32(props.Translation.X()) + transformation["TY"] = float32(props.Translation.Y()) + transformation["TZ"] = float32(props.Translation.Z()) } - if c, ok := base.(world.CustomItem); ok { - category := c.Category() - builder.AddComponent("minecraft:creative_category", map[string]any{ - "category": category.String(), - "group": category.Group(), - }) + 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 +} + +func bboxComponent(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)}, } - return builder.Construct(), nil } diff --git a/server/internal/packbuilder/blocks.go b/server/internal/packbuilder/blocks.go index 1393d7554..051be3a43 100644 --- a/server/internal/packbuilder/blocks.go +++ b/server/internal/packbuilder/blocks.go @@ -3,24 +3,19 @@ package packbuilder import ( "encoding/json" "fmt" - "github.com/df-mc/dragonfly/server/block/customblock" "github.com/df-mc/dragonfly/server/world" - "github.com/segmentio/fasthash/fnv1" "image" "image/png" - "io/ioutil" "os" "path/filepath" "strings" _ "unsafe" // Imported for compiler directives. ) -var formatVersion = "1.12.0" // TODO: This should be set by the user - // buildBlocks builds all of 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) { - if err := os.MkdirAll(filepath.Join(dir, "models/entity"), os.ModePerm); err != nil { + 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 { @@ -28,30 +23,23 @@ func buildBlocks(dir string) (count int, lang []string) { } textureData := make(map[string]any) - for identifier, group := range world.CustomBlocks() { - if len(group) == 0 { - panic(fmt.Sprintf("no custom blocks found for identifier %v", identifier)) + for identifier, blk := range world.CustomBlocks() { + b, ok := blk.(world.CustomBlockBuildable) + if !ok { + continue } - base := group[0] name := strings.Split(identifier, ":")[1] - lang = append(lang, fmt.Sprintf("tile.%s.name=%s", identifier, base.Name())) - textures, permutationTextures, _ := base.Textures() - for target, texture := range textures { - textureName := fmt.Sprintf("%s_%s", name, target.Name()) - textureData[textureName] = map[string]string{"textures": "textures/blocks/" + textureName} - buildBlockTexture(dir, textureName, texture) + 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) } - for permutation, permutationSpecificTextures := range permutationTextures { - h := fnv1.HashString64(permutation) - for target, texture := range permutationSpecificTextures { - textureName := fmt.Sprintf("%s_%s_%x", name, target.Name(), h) - textureData[textureName] = map[string]string{"textures": "textures/blocks/" + textureName} - buildBlockTexture(dir, textureName, 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) } } - - buildBlockGeometry(dir, name, base) count++ } @@ -80,44 +68,13 @@ func buildBlockTexture(dir, name string, img image.Image) { } } -// buildBlockGeometry writes the JSON geometry file from the provided name and block and writes it to the pack. -func buildBlockGeometry(dir, name string, block world.CustomBlock) { - if geometry, permutationGeometries, ok := block.Geometries(); ok { - data, err := json.Marshal(customblock.Geometries{ - FormatVersion: formatVersion, - Geometry: []customblock.Geometry{geometry}, - }) - if err != nil { - panic(err) - } - if err := ioutil.WriteFile(filepath.Join(dir, "models/entity", fmt.Sprintf("%s.geo.json", name)), data, 0666); err != nil { - panic(err) - } - - for permutation, permutationSpecificGeometry := range permutationGeometries { - data, err = json.Marshal(customblock.Geometries{ - FormatVersion: formatVersion, - Geometry: []customblock.Geometry{permutationSpecificGeometry}, - }) - if err != nil { - panic(err) - } - - h := fnv1.HashString64(permutation) - if err := ioutil.WriteFile(filepath.Join(dir, "models/entity", fmt.Sprintf("%s_%x.geo.json", name, h)), data, 0666); 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 := ioutil.WriteFile(filepath.Join(dir, "textures/terrain_texture.json"), b, 0666); err != nil { + if err := os.WriteFile(filepath.Join(dir, "textures/terrain_texture.json"), b, 0666); err != nil { panic(err) } } diff --git a/server/internal/packbuilder/pack_icon.png b/server/internal/packbuilder/pack_icon.png new file mode 100644 index 000000000..a7723d9b1 Binary files /dev/null and b/server/internal/packbuilder/pack_icon.png differ diff --git a/server/internal/packbuilder/resource_pack.go b/server/internal/packbuilder/resource_pack.go index 6d7f1745c..d588109c5 100644 --- a/server/internal/packbuilder/resource_pack.go +++ b/server/internal/packbuilder/resource_pack.go @@ -1,11 +1,15 @@ package packbuilder import ( + _ "embed" "github.com/rogpeppe/go-internal/dirhash" "github.com/sandertv/gophertunnel/minecraft/resource" "os" ) +//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. @@ -29,6 +33,9 @@ func BuildResourcePack() (*resource.Pack, bool) { 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) diff --git a/server/item/category/category.go b/server/item/category/category.go index f9d3662d9..5dc77791e 100644 --- a/server/item/category/category.go +++ b/server/item/category/category.go @@ -61,5 +61,5 @@ func (c Category) Group() string { if len(c.group) > 0 { return "itemGroup.name." + c.group } - return "none" + return "" } diff --git a/server/server.go b/server/server.go index 6c29ab70a..5cb337555 100644 --- a/server/server.go +++ b/server/server.go @@ -5,6 +5,7 @@ import ( "context" _ "embed" "encoding/base64" + "encoding/json" "fmt" "github.com/df-mc/atomic" "github.com/df-mc/dragonfly/server/cmd" @@ -19,7 +20,6 @@ import ( "github.com/go-gl/mathgl/mgl32" "github.com/go-gl/mathgl/mgl64" "github.com/google/uuid" - "github.com/kr/pretty" "github.com/sandertv/gophertunnel/minecraft" "github.com/sandertv/gophertunnel/minecraft/nbt" "github.com/sandertv/gophertunnel/minecraft/protocol" @@ -49,9 +49,8 @@ type Server struct { world, nether, end *world.World - customItems []protocol.ItemComponentEntry - itemComponents map[string]map[string]any - blockComponents map[string]map[string]any + customBlocks []protocol.BlockEntry + customItems []protocol.ItemComponentEntry listeners []Listener incoming chan *session.Session @@ -277,6 +276,7 @@ func (srv *Server) listen(l Listener) { // startListening starts making the EncodeBlock listener listen, accepting new // connections from players. func (srv *Server) startListening() { + srv.makeBlockEntries() srv.makeItemComponents() srv.wg.Add(len(srv.conf.Listeners)) @@ -290,6 +290,24 @@ func (srv *Server) startListening() { } } +// makeBlockEntries initializes the server's block components map using the registered custom blocks. It allows block +// components to be created only once at startup. +func (srv *Server) makeBlockEntries() { + custom := maps.Values(world.CustomBlocks()) + srv.customBlocks = make([]protocol.BlockEntry, len(custom)) + + for i, b := range custom { + name, _ := b.EncodeBlock() + srv.customBlocks[i] = protocol.BlockEntry{ + Name: name, + Properties: blockinternal.Components(name, b), + } + } + + data, _ := json.Marshal(srv.customBlocks) + os.WriteFile("custom_blocks.json", data, 0644) +} + // makeItemComponents initializes the server's item components map using the // registered custom items. It allows item components to be created only once // at startup @@ -306,19 +324,6 @@ func (srv *Server) makeItemComponents() { } } -// makeBlockComponents initializes the server's block components map using the registered custom blocks. It allows block -// components to be created only once at startup. -func (srv *Server) makeBlockComponents() { - srv.blockComponents = make(map[string]map[string]any) - for identifier, group := range world.CustomBlocks() { - data, err := blockinternal.Components(identifier, group) - if err != nil { - srv.conf.Log.Fatalf("error creating block components: %v", err) - } - srv.blockComponents[identifier] = data - } -} - // wait awaits the closing of all Listeners added to the Server through a call // to listen and closed the players channel once that happens. func (srv *Server) wait() { @@ -378,7 +383,7 @@ func (srv *Server) defaultGameData() minecraft.GameData { PlayerPosition: vec64To32(srv.world.Spawn().Vec3Centre().Add(mgl64.Vec3{0, 1.62})), Items: srv.itemEntries(), - CustomBlocks: srv.blockEntries(), + CustomBlocks: srv.customBlocks, GameRules: []protocol.GameRule{{Name: "naturalregeneration", Value: false}}, ServerAuthoritativeInventory: true, @@ -561,37 +566,6 @@ func (srv *Server) itemEntries() []protocol.ItemEntry { return entries } -// itemComponentEntries returns a list of all custom item component entries of the server, ready to be sent in the -// ItemComponent packet. If the list does not exist, it will be created and stored for future use. -func (srv *Server) itemComponentEntries() (entries []protocol.ItemComponentEntry) { - if srv.itemComponents == nil { - srv.makeItemComponents() - } - for name, entry := range srv.itemComponents { - entries = append(entries, protocol.ItemComponentEntry{ - Name: name, - Data: entry, - }) - } - return -} - -// blockEntries loads a list of all custom block entries of the server, ready to be sent in the StartGame packet. If the -// list does not exist, it will be created and stored for future use. -func (srv *Server) blockEntries() (entries []protocol.BlockEntry) { - if srv.blockComponents == nil { - srv.makeBlockComponents() - } - for name, properties := range srv.blockComponents { - pretty.Println(properties) - entries = append(entries, protocol.BlockEntry{ - Name: name, - Properties: properties, - }) - } - return -} - // ashyBiome represents a biome that has any form of ash. type ashyBiome interface { // Ash returns the ash and white ash of the biome. diff --git a/server/world/block.go b/server/world/block.go index d4570b619..eba68c54e 100644 --- a/server/world/block.go +++ b/server/world/block.go @@ -31,15 +31,20 @@ type Block interface { // client. type CustomBlock interface { Block + Properties() customblock.Properties +} + +type CustomBlockBuildable interface { + CustomBlock // Name is the name displayed to clients using the block. Name() string // Geometries is the geometries for the block that define the shape of the block. If false is returned, no custom // geometry will be applied. Permutation-specific geometry can be defined by returning a map of permutations to // geometry. - Geometries() (customblock.Geometry, map[string]customblock.Geometry, bool) + Geometry() []byte // Textures is a map of images indexed by their target, used to map textures on to the block. Permutation-specific // textures can be defined by returning a map of permutations to textures. - Textures() (map[customblock.Target]image.Image, map[string]map[customblock.Target]image.Image, customblock.Method) + Textures() map[string]image.Image } // Liquid represents a block that can be moved through and which can flow in the world after placement. There @@ -113,9 +118,8 @@ func RegisterBlock(b Block) { } if c, ok := b.(CustomBlock); ok { if _, ok := customBlocks[name]; !ok { - customBlocks[name] = []CustomBlock{} + customBlocks[name] = c } - customBlocks[name] = append(customBlocks[name], c) } } @@ -166,7 +170,7 @@ func BlockByName(name string, properties map[string]any) (Block, bool) { } // CustomBlocks returns a map of all custom blocks registered with their names as keys. -func CustomBlocks() map[string][]CustomBlock { +func CustomBlocks() map[string]CustomBlock { return customBlocks } diff --git a/server/world/block_state.go b/server/world/block_state.go index 71a491efb..81199f37d 100644 --- a/server/world/block_state.go +++ b/server/world/block_state.go @@ -23,7 +23,7 @@ var ( // registered are of the type unknownBlock. blocks []Block // customBlocks maps a custom block's identifier to a slice of custom blocks. - customBlocks = map[string][]CustomBlock{} + customBlocks = map[string]CustomBlock{} // stateRuntimeIDs holds a map for looking up the runtime ID of a block by the stateHash it produces. stateRuntimeIDs = map[stateHash]uint32{} // nbtBlocks holds a list of NBTer implementations for blocks registered that implement the NBTer interface. @@ -87,7 +87,7 @@ func registerBlockState(s blockState, order bool) { sort.SliceStable(blocks, func(i, j int) bool { nameOne, _ := blocks[i].EncodeBlock() nameTwo, _ := blocks[j].EncodeBlock() - return nameOne == nameTwo && fnv1.HashString64(nameOne) < fnv1.HashString64(nameTwo) + return nameOne != nameTwo && fnv1.HashString64(nameOne) < fnv1.HashString64(nameTwo) }) for id, b := range blocks { diff --git a/skull.geo.json b/skull.geo.json index 4545c74b4..8a1d2b4a7 100644 --- a/skull.geo.json +++ b/skull.geo.json @@ -3,7 +3,7 @@ "minecraft:geometry": [ { "description": { - "identifier": "geometry.emperialspe.skull", + "identifier": "geometry.skull", "texture_width": 32, "texture_height": 16, "visible_bounds_width": 2, @@ -12,7 +12,7 @@ }, "bones": [ { - "name": "unknown_bone", + "name": "root", "pivot": [0, 0, 0], "rotation": [0, 180, 0], "cubes": [