From 3425d39f7d04d209a4fd97e441ecfe853a24b911 Mon Sep 17 00:00:00 2001 From: Tal Date: Sat, 9 Dec 2023 10:20:49 -0600 Subject: [PATCH] dragonfly: Implement support for custom blocks (#512) Co-authored-by: TwistedAsylumMC Co-authored-by: DaPigGuy Co-authored-by: JustTalDevelops --- .github/workflows/pr.yml | 4 +- .github/workflows/push.yml | 4 +- cmd/blockhash/main.go | 20 ++- go.mod | 3 +- go.sum | 5 + server/block/block.go | 14 +- server/block/customblock/material.go | 58 ++++++ server/block/customblock/permutations.go | 44 +++++ server/block/customblock/render_method.go | 58 ++++++ server/block/hash.go | 178 +++++++++++++++++++ server/conf.go | 2 +- server/entity/experience_orb.go | 2 +- server/internal/blockinternal/builder.go | 98 ++++++++++ server/internal/blockinternal/components.go | 118 ++++++++++++ server/internal/packbuilder/blocks.go | 80 +++++++++ server/internal/packbuilder/pack_icon.png | Bin 0 -> 5953 bytes server/internal/packbuilder/resource_pack.go | 11 ++ server/internal/sliceutil/sliceutil.go | 2 +- server/item/book_and_quill.go | 2 +- server/item/category/category.go | 2 +- server/item/inventory/inventory.go | 2 +- server/item/recipe/register.go | 2 +- server/item/stack.go | 2 +- server/player/scoreboard/scoreboard.go | 2 +- server/server.go | 25 ++- server/session/handler_crafting.go | 2 +- server/session/handler_enchanting.go | 2 +- server/world/block.go | 39 +++- server/world/block_state.go | 45 +++-- server/world/tick.go | 2 +- server/world/world.go | 2 +- 31 files changed, 793 insertions(+), 37 deletions(-) create mode 100644 server/block/customblock/material.go create mode 100644 server/block/customblock/permutations.go create mode 100644 server/block/customblock/render_method.go create mode 100644 server/internal/blockinternal/builder.go create mode 100644 server/internal/blockinternal/components.go create mode 100644 server/internal/packbuilder/blocks.go create mode 100644 server/internal/packbuilder/pack_icon.png diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 8f13a39ab..198c2874b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.19 + - name: Set up Go 1.21 uses: actions/setup-go@v1 with: - go-version: 1.19 + go-version: 1.21 id: go - name: Check out code into the Go module directory diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 60f1691ea..da7fbfe7e 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.19 + - name: Set up Go 1.21 uses: actions/setup-go@v1 with: - go-version: 1.19 + go-version: 1.21 id: go - name: Check out code into the Go module directory diff --git a/cmd/blockhash/main.go b/cmd/blockhash/main.go index a82126baf..adf052679 100644 --- a/cmd/blockhash/main.go +++ b/cmd/blockhash/main.go @@ -55,12 +55,13 @@ func procPackage(pkg *ast.Package, fs *token.FileSet, w io.Writer) { b.writePackage(w) i := b.writeConstants(w) + b.writeNextHash(w) b.writeMethods(w, i) } var ( packageFormat = "// Code generated by cmd/blockhash; DO NOT EDIT.\n\npackage %v\n\n" - methodFormat = "\nfunc (%v%v) Hash() uint64 {\n\treturn %v\n}\n" + methodFormat = "\n// Hash ...\nfunc (%v%v) Hash() uint64 {\n\treturn %v\n}\n" constFormat = "\thash%v" ) @@ -112,13 +113,28 @@ func (b *hashBuilder) writeConstants(w io.Writer) (bitSize int) { i++ } - if _, err := fmt.Fprintln(w, ")"); err != nil { + if _, err := fmt.Fprintln(w, "\thashCustomBlockBase\n)"); err != nil { log.Fatalln(err) } return bits.Len64(i) } +func (b *hashBuilder) writeNextHash(w io.Writer) { + if _, err := fmt.Fprintln(w, "\n// customBlockBase represents the base hash for all custom blocks."); err != nil { + log.Fatalln(err) + } + if _, err := fmt.Fprintln(w, "var customBlockBase = uint64(hashCustomBlockBase - 1)"); err != nil { + log.Fatalln(err) + } + if _, err := fmt.Fprintln(w, "\n// NextHash returns the next free hash for custom blocks."); err != nil { + log.Fatalln(err) + } + if _, err := fmt.Fprintln(w, "func NextHash() uint64 {\n\tcustomBlockBase++\n\treturn customBlockBase\n}"); err != nil { + log.Fatalln(err) + } +} + func (b *hashBuilder) writeMethods(w io.Writer, baseBits int) { for _, name := range b.names { fields := b.blockFields[name] diff --git a/go.mod b/go.mod index bc8057b5f..ae3ef8ec4 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/df-mc/dragonfly -go 1.19 +go 1.21 require ( github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9 @@ -13,6 +13,7 @@ require ( github.com/pelletier/go-toml v1.9.5 github.com/rogpeppe/go-internal v1.9.0 github.com/sandertv/gophertunnel v1.34.0 + github.com/segmentio/fasthash v1.0.3 github.com/sirupsen/logrus v1.9.0 go.uber.org/atomic v1.10.0 golang.org/x/exp v0.0.0-20230206171751-46f607a40771 diff --git a/go.sum b/go.sum index af2ce5406..64c2ed10d 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,7 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= @@ -50,6 +51,8 @@ github.com/sandertv/go-raknet v1.12.0 h1:olUzZlIJyX/pgj/mrsLCZYjKLNDsYiWdvQ4NIm3 github.com/sandertv/go-raknet v1.12.0/go.mod h1:Gx+WgZBMQ0V2UoouGoJ8Wj6CDrMBQ4SB2F/ggpl5/+Y= github.com/sandertv/gophertunnel v1.34.0 h1:fHXTPL4+hUJFF5xObM9T07GpXkTdSvKmYAvx65k0IAw= github.com/sandertv/gophertunnel v1.34.0/go.mod h1:+Dbhj3bs74gZoSkyab7kglx1Rbq8S5G7sJd/wr5Qm9g= +github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= +github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= @@ -57,6 +60,7 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -122,3 +126,4 @@ gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/block/block.go b/server/block/block.go index f1812b4d5..4fa16ba14 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" @@ -73,13 +74,24 @@ type EntityInsider interface { EntityInside(pos cube.Pos, w *world.World, e world.Entity) } -// Frictional represents a block that may have a custom friction value, friction is used for entity drag when the +// Frictional represents a block that may have a custom friction value. Friction is used for entity drag when the // entity is on ground. If a block does not implement this interface, it should be assumed that its friction is 0.6. type Frictional interface { // Friction returns the block's friction value. Friction() float64 } +// Permutable represents a custom block that can have more permutations than its default state. +type Permutable interface { + // States returns a map of all the different properties for the block. The key is the property name, and the value + // is a slice of all the possible values for that property. It is important that a block is registered in dragonfly + // for each of the possible combinations of properties and values. + States() map[string][]any + // Permutations returns a slice of all the different permutations for the block. Multiple permutations can be + // applied at once if their conditions are met. + Permutations() []customblock.Permutation +} + func calculateFace(user item.User, placePos cube.Pos) cube.Face { userPos := user.Position() pos := cube.PosFromVec3(userPos) diff --git a/server/block/customblock/material.go b/server/block/customblock/material.go new file mode 100644 index 000000000..8b018947a --- /dev/null +++ b/server/block/customblock/material.go @@ -0,0 +1,58 @@ +package customblock + +// Material represents a single material used for rendering part of a custom block. +type Material struct { + // texture is the name of the texture for the material. + texture string + // renderMethod is the method to use when rendering the material. + renderMethod Method + // faceDimming is if the material should be dimmed by the direction it's facing. + faceDimming bool + // ambientOcclusion is if the material should have ambient occlusion applied when lighting. + ambientOcclusion bool +} + +// NewMaterial returns a new Material with the provided information. It enables face dimming by default and ambient +// occlusion based on the render method given. +func NewMaterial(texture string, method Method) Material { + return Material{ + texture: texture, + renderMethod: method, + faceDimming: true, + ambientOcclusion: method.AmbientOcclusion(), + } +} + +// WithFaceDimming returns a copy of the Material with face dimming enabled. +func (m Material) WithFaceDimming() Material { + m.faceDimming = true + return m +} + +// WithoutFaceDimming returns a copy of the Material with face dimming disabled. +func (m Material) WithoutFaceDimming() Material { + m.faceDimming = false + return m +} + +// WithAmbientOcclusion returns a copy of the Material with ambient occlusion enabled. +func (m Material) WithAmbientOcclusion() Material { + m.ambientOcclusion = true + return m +} + +// WithoutAmbientOcclusion returns a copy of the Material with ambient occlusion disabled. +func (m Material) WithoutAmbientOcclusion() Material { + m.ambientOcclusion = false + return m +} + +// Encode returns the material encoded as a map that can be sent over the network to the client. +func (m Material) Encode() map[string]any { + return map[string]any{ + "texture": m.texture, + "render_method": m.renderMethod.String(), + "face_dimming": m.faceDimming, + "ambient_occlusion": m.ambientOcclusion, + } +} diff --git a/server/block/customblock/permutations.go b/server/block/customblock/permutations.go new file mode 100644 index 000000000..ae55bf620 --- /dev/null +++ b/server/block/customblock/permutations.go @@ -0,0 +1,44 @@ +package customblock + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl64" +) + +// Properties represents the different properties that can be applied to a block or a permutation. +type Properties struct { + // CollisionBox represents the bounding box of the block that the player can collide with. This cannot exceed the + // position of the current block in the world, otherwise it will be cut off at the edge. + CollisionBox cube.BBox + // Cube determines whether the block should inherit the default cube geometry. This will only be considered if the + // Geometry field is empty. + Cube bool + // Geometry represents the geometry identifier that should be used for the block. If you want to use the default + // cube geometry, leave this field empty and set Cube to true. + Geometry string + // MapColour represents the hex colour that should be used for the block on a map. + MapColour string + // Rotation represents the rotation of the block. Rotations are only applied in 90 degree increments, meaning + // 1 = 90 degrees, 2 = 180 degrees, 3 = 270 degrees and 4 = 360 degrees. + Rotation cube.Pos + // Scale is the scale of the block, with 1 being the default scale in all axes. When scaled, the block cannot + // exceed a 30x30x30 pixel area otherwise the client will not render the block. + Scale mgl64.Vec3 + // SelectionBox represents the bounding box of the block that the player can interact with. This cannot exceed the + // position of the current block in the world, otherwise it will be cut off at the edge. + SelectionBox cube.BBox + // Textures define the textures that should be used for the block. The key is the target of the texture, such as + // "*" for all sides, or one of "up", "down", "north", "south", "east", "west" for a specific side. + Textures map[string]Material + // Translation is the translation of the block within itself. When translated, the block cannot exceed a 30x30x30 + // pixel area otherwise the client will not render the block. + Translation mgl64.Vec3 +} + +// Permutation represents a specific permutation for a block that is only applied when the condition is met. +type Permutation struct { + Properties + // Condition is a molang query that is used to determine whether the permutation should be applied. + // Only the latest version of molang is supported. + Condition string +} diff --git a/server/block/customblock/render_method.go b/server/block/customblock/render_method.go new file mode 100644 index 000000000..e003f6666 --- /dev/null +++ b/server/block/customblock/render_method.go @@ -0,0 +1,58 @@ +package customblock + +// Method is the method to use when rendering a material for a custom block. +type Method struct { + renderMethod +} + +// OpaqueRenderMethod returns the opaque rendering method for a material. It does not render an alpha layer, meaning it +// does not support transparent or translucent textures, only textures that are fully opaque. +func OpaqueRenderMethod() Method { + return Method{0} +} + +// AlphaTestRenderMethod returns the alpha_test rendering method for a material. It does not allow for translucent +// textures, only textures that are fully opaque or fully transparent, used for blocks such as regular glass. It also +// disables ambient occlusion by default. +func AlphaTestRenderMethod() Method { + return Method{1} +} + +// BlendRenderMethod returns the blend rendering method for a material. It allows for transparent and translucent +// textures, used for blocks such as stained-glass. It also disables ambient occlusion by default. +func BlendRenderMethod() Method { + return Method{2} +} + +// DoubleSidedRenderMethod returns the double_sided rendering method for a material. It is used to completely disable +// backface culling, which would be used for flat faces visible from both sides. +func DoubleSidedRenderMethod() Method { + return Method{3} +} + +type renderMethod uint8 + +// Uint8 returns the render method as a uint8. +func (m renderMethod) Uint8() uint8 { + return uint8(m) +} + +// String ... +func (m renderMethod) String() string { + switch m { + case 0: + return "opaque" + case 1: + return "alpha_test" + case 2: + return "blend" + case 3: + return "double_sided" + } + panic("should never happen") +} + +// AmbientOcclusion returns if ambient occlusion should be enabled by default for a material using this rendering method. +func (m renderMethod) AmbientOcclusion() bool { + return m != 1 && m != 2 +} diff --git a/server/block/hash.go b/server/block/hash.go index 744da0ba0..66284c6bb 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -56,6 +56,7 @@ const ( hashDragonEgg hashDriedKelp hashDripstone + hashDropper hashEmerald hashEmeraldOre hashEnchantingTable @@ -170,672 +171,849 @@ const ( hashWoodFenceGate hashWoodTrapdoor hashWool + hashCustomBlockBase ) +// customBlockBase represents the base hash for all custom blocks. +var customBlockBase = uint64(hashCustomBlockBase - 1) + +// NextHash returns the next free hash for custom blocks. +func NextHash() uint64 { + customBlockBase++ + return customBlockBase +} + +// Hash ... func (Air) Hash() uint64 { return hashAir } +// Hash ... func (Amethyst) Hash() uint64 { return hashAmethyst } +// Hash ... func (AncientDebris) Hash() uint64 { return hashAncientDebris } +// Hash ... func (a Andesite) Hash() uint64 { return hashAndesite | uint64(boolByte(a.Polished))<<8 } +// Hash ... func (a Anvil) Hash() uint64 { return hashAnvil | uint64(a.Type.Uint8())<<8 | uint64(a.Facing)<<10 } +// Hash ... func (b Banner) Hash() uint64 { return hashBanner | uint64(b.Attach.Uint8())<<8 } +// Hash ... func (b Barrel) Hash() uint64 { return hashBarrel | uint64(b.Facing)<<8 | uint64(boolByte(b.Open))<<11 } +// Hash ... func (Barrier) Hash() uint64 { return hashBarrier } +// Hash ... func (b Basalt) Hash() uint64 { return hashBasalt | uint64(boolByte(b.Polished))<<8 | uint64(b.Axis)<<9 } +// Hash ... func (Beacon) Hash() uint64 { return hashBeacon } +// Hash ... func (b Bedrock) Hash() uint64 { return hashBedrock | uint64(boolByte(b.InfiniteBurning))<<8 } +// Hash ... func (b BeetrootSeeds) Hash() uint64 { return hashBeetrootSeeds | uint64(b.Growth)<<8 } +// Hash ... func (b Blackstone) Hash() uint64 { return hashBlackstone | uint64(b.Type.Uint8())<<8 } +// Hash ... func (b BlastFurnace) Hash() uint64 { return hashBlastFurnace | uint64(b.Facing)<<8 | uint64(boolByte(b.Lit))<<11 } +// Hash ... func (BlueIce) Hash() uint64 { return hashBlueIce } +// Hash ... func (b Bone) Hash() uint64 { return hashBone | uint64(b.Axis)<<8 } +// Hash ... func (Bookshelf) Hash() uint64 { return hashBookshelf } +// Hash ... func (Bricks) Hash() uint64 { return hashBricks } +// Hash ... func (c Cactus) Hash() uint64 { return hashCactus | uint64(c.Age)<<8 } +// Hash ... func (c Cake) Hash() uint64 { return hashCake | uint64(c.Bites)<<8 } +// Hash ... func (Calcite) Hash() uint64 { return hashCalcite } +// Hash ... func (c Carpet) Hash() uint64 { return hashCarpet | uint64(c.Colour.Uint8())<<8 } +// Hash ... func (c Carrot) Hash() uint64 { return hashCarrot | uint64(c.Growth)<<8 } +// Hash ... func (c Chain) Hash() uint64 { return hashChain | uint64(c.Axis)<<8 } +// Hash ... func (c Chest) Hash() uint64 { return hashChest | uint64(c.Facing)<<8 } +// Hash ... func (ChiseledQuartz) Hash() uint64 { return hashChiseledQuartz } +// Hash ... func (Clay) Hash() uint64 { return hashClay } +// Hash ... func (Coal) Hash() uint64 { return hashCoal } +// Hash ... func (c CoalOre) Hash() uint64 { return hashCoalOre | uint64(c.Type.Uint8())<<8 } +// Hash ... func (c Cobblestone) Hash() uint64 { return hashCobblestone | uint64(boolByte(c.Mossy))<<8 } +// Hash ... func (c CocoaBean) Hash() uint64 { return hashCocoaBean | uint64(c.Facing)<<8 | uint64(c.Age)<<10 } +// Hash ... func (c Composter) Hash() uint64 { return hashComposter | uint64(c.Level)<<8 } +// Hash ... func (c Concrete) Hash() uint64 { return hashConcrete | uint64(c.Colour.Uint8())<<8 } +// Hash ... func (c ConcretePowder) Hash() uint64 { return hashConcretePowder | uint64(c.Colour.Uint8())<<8 } +// Hash ... func (c CopperOre) Hash() uint64 { return hashCopperOre | uint64(c.Type.Uint8())<<8 } +// Hash ... func (c Coral) Hash() uint64 { return hashCoral | uint64(c.Type.Uint8())<<8 | uint64(boolByte(c.Dead))<<11 } +// Hash ... func (c CoralBlock) Hash() uint64 { return hashCoralBlock | uint64(c.Type.Uint8())<<8 | uint64(boolByte(c.Dead))<<11 } +// Hash ... func (CraftingTable) Hash() uint64 { return hashCraftingTable } +// Hash ... func (DeadBush) Hash() uint64 { return hashDeadBush } +// Hash ... func (p DecoratedPot) Hash() uint64 { return hashDecoratedPot | uint64(p.Facing)<<8 } +// Hash ... func (d Deepslate) Hash() uint64 { return hashDeepslate | uint64(d.Type.Uint8())<<8 | uint64(d.Axis)<<10 } +// Hash ... func (d DeepslateBricks) Hash() uint64 { return hashDeepslateBricks | uint64(boolByte(d.Cracked))<<8 } +// Hash ... func (d DeepslateTiles) Hash() uint64 { return hashDeepslateTiles | uint64(boolByte(d.Cracked))<<8 } +// Hash ... func (Diamond) Hash() uint64 { return hashDiamond } +// Hash ... func (d DiamondOre) Hash() uint64 { return hashDiamondOre | uint64(d.Type.Uint8())<<8 } +// Hash ... func (d Diorite) Hash() uint64 { return hashDiorite | uint64(boolByte(d.Polished))<<8 } +// Hash ... func (d Dirt) Hash() uint64 { return hashDirt | uint64(boolByte(d.Coarse))<<8 } +// Hash ... func (DirtPath) Hash() uint64 { return hashDirtPath } +// Hash ... func (d DoubleFlower) Hash() uint64 { return hashDoubleFlower | uint64(boolByte(d.UpperPart))<<8 | uint64(d.Type.Uint8())<<9 } +// Hash ... func (d DoubleTallGrass) Hash() uint64 { return hashDoubleTallGrass | uint64(boolByte(d.UpperPart))<<8 | uint64(d.Type.Uint8())<<9 } +// Hash ... func (DragonEgg) Hash() uint64 { return hashDragonEgg } +// Hash ... func (DriedKelp) Hash() uint64 { return hashDriedKelp } +// Hash ... func (Dripstone) Hash() uint64 { return hashDripstone } +// Hash ... func (Emerald) Hash() uint64 { return hashEmerald } +// Hash ... func (e EmeraldOre) Hash() uint64 { return hashEmeraldOre | uint64(e.Type.Uint8())<<8 } +// Hash ... func (EnchantingTable) Hash() uint64 { return hashEnchantingTable } +// Hash ... func (EndBricks) Hash() uint64 { return hashEndBricks } +// Hash ... func (EndStone) Hash() uint64 { return hashEndStone } +// Hash ... func (c EnderChest) Hash() uint64 { return hashEnderChest | uint64(c.Facing)<<8 } +// Hash ... func (f Farmland) Hash() uint64 { return hashFarmland | uint64(f.Hydration)<<8 } +// Hash ... func (f Fire) Hash() uint64 { return hashFire | uint64(f.Type.Uint8())<<8 | uint64(f.Age)<<9 } +// Hash ... func (FletchingTable) Hash() uint64 { return hashFletchingTable } +// Hash ... func (f Flower) Hash() uint64 { return hashFlower | uint64(f.Type.Uint8())<<8 } +// Hash ... func (f Froglight) Hash() uint64 { return hashFroglight | uint64(f.Type.Uint8())<<8 | uint64(f.Axis)<<10 } +// Hash ... func (f Furnace) Hash() uint64 { return hashFurnace | uint64(f.Facing)<<8 | uint64(boolByte(f.Lit))<<11 } +// Hash ... func (Glass) Hash() uint64 { return hashGlass } +// Hash ... func (GlassPane) Hash() uint64 { return hashGlassPane } +// Hash ... func (t GlazedTerracotta) Hash() uint64 { return hashGlazedTerracotta | uint64(t.Colour.Uint8())<<8 | uint64(t.Facing)<<12 } +// Hash ... func (Glowstone) Hash() uint64 { return hashGlowstone } +// Hash ... func (Gold) Hash() uint64 { return hashGold } +// Hash ... func (g GoldOre) Hash() uint64 { return hashGoldOre | uint64(g.Type.Uint8())<<8 } +// Hash ... func (g Granite) Hash() uint64 { return hashGranite | uint64(boolByte(g.Polished))<<8 } +// Hash ... func (Grass) Hash() uint64 { return hashGrass } +// Hash ... func (Gravel) Hash() uint64 { return hashGravel } +// Hash ... func (g Grindstone) Hash() uint64 { return hashGrindstone | uint64(g.Attach.Uint8())<<8 | uint64(g.Facing)<<10 } +// Hash ... func (h HayBale) Hash() uint64 { return hashHayBale | uint64(h.Axis)<<8 } +// Hash ... func (Honeycomb) Hash() uint64 { return hashHoneycomb } +// Hash ... func (InvisibleBedrock) Hash() uint64 { return hashInvisibleBedrock } +// Hash ... func (Iron) Hash() uint64 { return hashIron } +// Hash ... func (IronBars) Hash() uint64 { return hashIronBars } +// Hash ... func (i IronOre) Hash() uint64 { return hashIronOre | uint64(i.Type.Uint8())<<8 } +// Hash ... func (i ItemFrame) Hash() uint64 { return hashItemFrame | uint64(i.Facing)<<8 | uint64(boolByte(i.Glowing))<<11 } +// Hash ... func (Jukebox) Hash() uint64 { return hashJukebox } +// Hash ... func (k Kelp) Hash() uint64 { return hashKelp | uint64(k.Age)<<8 } +// Hash ... func (l Ladder) Hash() uint64 { return hashLadder | uint64(l.Facing)<<8 } +// Hash ... func (l Lantern) Hash() uint64 { return hashLantern | uint64(boolByte(l.Hanging))<<8 | uint64(l.Type.Uint8())<<9 } +// Hash ... func (Lapis) Hash() uint64 { return hashLapis } +// Hash ... func (l LapisOre) Hash() uint64 { return hashLapisOre | uint64(l.Type.Uint8())<<8 } +// Hash ... func (l Lava) Hash() uint64 { return hashLava | uint64(boolByte(l.Still))<<8 | uint64(l.Depth)<<9 | uint64(boolByte(l.Falling))<<17 } +// Hash ... func (l Leaves) Hash() uint64 { return hashLeaves | uint64(l.Wood.Uint8())<<8 | uint64(boolByte(l.Persistent))<<12 | uint64(boolByte(l.ShouldUpdate))<<13 } +// Hash ... func (l Lectern) Hash() uint64 { return hashLectern | uint64(l.Facing)<<8 } +// Hash ... func (l Light) Hash() uint64 { return hashLight | uint64(l.Level)<<8 } +// Hash ... func (l LitPumpkin) Hash() uint64 { return hashLitPumpkin | uint64(l.Facing)<<8 } +// Hash ... func (l Log) Hash() uint64 { return hashLog | uint64(l.Wood.Uint8())<<8 | uint64(boolByte(l.Stripped))<<12 | uint64(l.Axis)<<13 } +// Hash ... func (l Loom) Hash() uint64 { return hashLoom | uint64(l.Facing)<<8 } +// Hash ... func (Melon) Hash() uint64 { return hashMelon } +// Hash ... func (m MelonSeeds) Hash() uint64 { return hashMelonSeeds | uint64(m.Growth)<<8 | uint64(m.Direction)<<16 } +// Hash ... func (MossCarpet) Hash() uint64 { return hashMossCarpet } +// Hash ... func (Mud) Hash() uint64 { return hashMud } +// Hash ... func (MudBricks) Hash() uint64 { return hashMudBricks } +// Hash ... func (m MuddyMangroveRoots) Hash() uint64 { return hashMuddyMangroveRoots | uint64(m.Axis)<<8 } +// Hash ... func (NetherBrickFence) Hash() uint64 { return hashNetherBrickFence } +// Hash ... func (n NetherBricks) Hash() uint64 { return hashNetherBricks | uint64(n.Type.Uint8())<<8 } +// Hash ... func (NetherGoldOre) Hash() uint64 { return hashNetherGoldOre } +// Hash ... func (NetherQuartzOre) Hash() uint64 { return hashNetherQuartzOre } +// Hash ... func (NetherSprouts) Hash() uint64 { return hashNetherSprouts } +// Hash ... func (n NetherWart) Hash() uint64 { return hashNetherWart | uint64(n.Age)<<8 } +// Hash ... func (n NetherWartBlock) Hash() uint64 { return hashNetherWartBlock | uint64(boolByte(n.Warped))<<8 } +// Hash ... func (Netherite) Hash() uint64 { return hashNetherite } +// Hash ... func (Netherrack) Hash() uint64 { return hashNetherrack } +// Hash ... func (Note) Hash() uint64 { return hashNote } +// Hash ... func (o Obsidian) Hash() uint64 { return hashObsidian | uint64(boolByte(o.Crying))<<8 } +// Hash ... func (PackedIce) Hash() uint64 { return hashPackedIce } +// Hash ... func (PackedMud) Hash() uint64 { return hashPackedMud } +// Hash ... func (p Planks) Hash() uint64 { return hashPlanks | uint64(p.Wood.Uint8())<<8 } +// Hash ... func (Podzol) Hash() uint64 { return hashPodzol } +// Hash ... func (b PolishedBlackstoneBrick) Hash() uint64 { return hashPolishedBlackstoneBrick | uint64(boolByte(b.Cracked))<<8 } +// Hash ... func (p Potato) Hash() uint64 { return hashPotato | uint64(p.Growth)<<8 } +// Hash ... func (p Prismarine) Hash() uint64 { return hashPrismarine | uint64(p.Type.Uint8())<<8 } +// Hash ... func (p Pumpkin) Hash() uint64 { return hashPumpkin | uint64(boolByte(p.Carved))<<8 | uint64(p.Facing)<<9 } +// Hash ... func (p PumpkinSeeds) Hash() uint64 { return hashPumpkinSeeds | uint64(p.Growth)<<8 | uint64(p.Direction)<<16 } +// Hash ... func (Purpur) Hash() uint64 { return hashPurpur } +// Hash ... func (p PurpurPillar) Hash() uint64 { return hashPurpurPillar | uint64(p.Axis)<<8 } +// Hash ... func (q Quartz) Hash() uint64 { return hashQuartz | uint64(boolByte(q.Smooth))<<8 } +// Hash ... func (QuartzBricks) Hash() uint64 { return hashQuartzBricks } +// Hash ... func (q QuartzPillar) Hash() uint64 { return hashQuartzPillar | uint64(q.Axis)<<8 } +// Hash ... func (RawCopper) Hash() uint64 { return hashRawCopper } +// Hash ... func (RawGold) Hash() uint64 { return hashRawGold } +// Hash ... func (RawIron) Hash() uint64 { return hashRawIron } +// Hash ... func (ReinforcedDeepslate) Hash() uint64 { return hashReinforcedDeepslate } +// Hash ... func (s Sand) Hash() uint64 { return hashSand | uint64(boolByte(s.Red))<<8 } +// Hash ... func (s Sandstone) Hash() uint64 { return hashSandstone | uint64(s.Type.Uint8())<<8 | uint64(boolByte(s.Red))<<10 } +// Hash ... func (SeaLantern) Hash() uint64 { return hashSeaLantern } +// Hash ... func (s SeaPickle) Hash() uint64 { return hashSeaPickle | uint64(s.AdditionalCount)<<8 | uint64(boolByte(s.Dead))<<16 } +// Hash ... func (Shroomlight) Hash() uint64 { return hashShroomlight } +// Hash ... func (s Sign) Hash() uint64 { return hashSign | uint64(s.Wood.Uint8())<<8 | uint64(s.Attach.Uint8())<<12 } +// Hash ... func (s Skull) Hash() uint64 { return hashSkull | uint64(s.Attach.FaceUint8())<<8 } +// Hash ... func (s Slab) Hash() uint64 { return hashSlab | s.Block.Hash()<<8 | uint64(boolByte(s.Top))<<24 | uint64(boolByte(s.Double))<<25 } +// Hash ... func (SmithingTable) Hash() uint64 { return hashSmithingTable } +// Hash ... func (s Smoker) Hash() uint64 { return hashSmoker | uint64(s.Facing)<<8 | uint64(boolByte(s.Lit))<<11 } +// Hash ... func (Snow) Hash() uint64 { return hashSnow } +// Hash ... func (SoulSand) Hash() uint64 { return hashSoulSand } +// Hash ... func (SoulSoil) Hash() uint64 { return hashSoulSoil } +// Hash ... func (s Sponge) Hash() uint64 { return hashSponge | uint64(boolByte(s.Wet))<<8 } +// Hash ... func (SporeBlossom) Hash() uint64 { return hashSporeBlossom } +// Hash ... func (g StainedGlass) Hash() uint64 { return hashStainedGlass | uint64(g.Colour.Uint8())<<8 } +// Hash ... func (p StainedGlassPane) Hash() uint64 { return hashStainedGlassPane | uint64(p.Colour.Uint8())<<8 } +// Hash ... func (t StainedTerracotta) Hash() uint64 { return hashStainedTerracotta | uint64(t.Colour.Uint8())<<8 } +// Hash ... func (s Stairs) Hash() uint64 { return hashStairs | s.Block.Hash()<<8 | uint64(boolByte(s.UpsideDown))<<24 | uint64(s.Facing)<<25 } +// Hash ... func (s Stone) Hash() uint64 { return hashStone | uint64(boolByte(s.Smooth))<<8 } +// Hash ... func (s StoneBricks) Hash() uint64 { return hashStoneBricks | uint64(s.Type.Uint8())<<8 } +// Hash ... func (s Stonecutter) Hash() uint64 { return hashStonecutter | uint64(s.Facing)<<8 } +// Hash ... func (c SugarCane) Hash() uint64 { return hashSugarCane | uint64(c.Age)<<8 } +// Hash ... func (TNT) Hash() uint64 { return hashTNT } +// Hash ... func (g TallGrass) Hash() uint64 { return hashTallGrass | uint64(g.Type.Uint8())<<8 } +// Hash ... func (Terracotta) Hash() uint64 { return hashTerracotta } +// Hash ... func (t Torch) Hash() uint64 { return hashTorch | uint64(t.Facing)<<8 | uint64(t.Type.Uint8())<<11 } +// Hash ... func (Tuff) Hash() uint64 { return hashTuff } +// Hash ... func (w Wall) Hash() uint64 { return hashWall | w.Block.Hash()<<8 | uint64(w.NorthConnection.Uint8())<<24 | uint64(w.EastConnection.Uint8())<<26 | uint64(w.SouthConnection.Uint8())<<28 | uint64(w.WestConnection.Uint8())<<30 | uint64(boolByte(w.Post))<<32 } +// Hash ... func (w Water) Hash() uint64 { return hashWater | uint64(boolByte(w.Still))<<8 | uint64(w.Depth)<<9 | uint64(boolByte(w.Falling))<<17 } +// Hash ... func (s WheatSeeds) Hash() uint64 { return hashWheatSeeds | uint64(s.Growth)<<8 } +// Hash ... func (w Wood) Hash() uint64 { return hashWood | uint64(w.Wood.Uint8())<<8 | uint64(boolByte(w.Stripped))<<12 | uint64(w.Axis)<<13 } +// Hash ... func (d WoodDoor) Hash() uint64 { return hashWoodDoor | uint64(d.Wood.Uint8())<<8 | uint64(d.Facing)<<12 | uint64(boolByte(d.Open))<<14 | uint64(boolByte(d.Top))<<15 | uint64(boolByte(d.Right))<<16 } +// Hash ... func (w WoodFence) Hash() uint64 { return hashWoodFence | uint64(w.Wood.Uint8())<<8 } +// Hash ... func (f WoodFenceGate) Hash() uint64 { return hashWoodFenceGate | uint64(f.Wood.Uint8())<<8 | uint64(f.Facing)<<12 | uint64(boolByte(f.Open))<<14 | uint64(boolByte(f.Lowered))<<15 } +// Hash ... func (t WoodTrapdoor) Hash() uint64 { return hashWoodTrapdoor | uint64(t.Wood.Uint8())<<8 | uint64(t.Facing)<<12 | uint64(boolByte(t.Open))<<14 | uint64(boolByte(t.Top))<<15 } +// Hash ... func (w Wool) Hash() uint64 { return hashWool | uint64(w.Colour.Uint8())<<8 } diff --git a/server/conf.go b/server/conf.go index cb4c6340a..4891b06fd 100644 --- a/server/conf.go +++ b/server/conf.go @@ -15,9 +15,9 @@ import ( "github.com/google/uuid" "github.com/sandertv/gophertunnel/minecraft/resource" "github.com/sirupsen/logrus" - "golang.org/x/exp/slices" "os" "path/filepath" + "slices" ) // Config contains options for starting a Minecraft server. diff --git a/server/entity/experience_orb.go b/server/entity/experience_orb.go index 7a35aa9cd..fc24b4b69 100644 --- a/server/entity/experience_orb.go +++ b/server/entity/experience_orb.go @@ -5,7 +5,7 @@ import ( "github.com/df-mc/dragonfly/server/internal/nbtconv" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" - "golang.org/x/exp/slices" + "slices" "time" ) diff --git a/server/internal/blockinternal/builder.go b/server/internal/blockinternal/builder.go new file mode 100644 index 000000000..e320ab585 --- /dev/null +++ b/server/internal/blockinternal/builder.go @@ -0,0 +1,98 @@ +package blockinternal + +import ( + "github.com/df-mc/dragonfly/server/item/category" + "golang.org/x/exp/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 + + 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) *ComponentBuilder { + if components == nil { + components = map[string]any{} + } + return &ComponentBuilder{ + permutations: make(map[string]map[string]any), + components: components, + + 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(), + }, + } + 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 +} diff --git a/server/internal/blockinternal/components.go b/server/internal/blockinternal/components.go new file mode 100644 index 000000000..affec240e --- /dev/null +++ b/server/internal/blockinternal/components.go @@ -0,0 +1,118 @@ +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) 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), + }) + } + 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"] = bboxComponent(props.CollisionBox) + } + if props.SelectionBox != (cube.BBox{}) { + components["minecraft:selection_box"] = bboxComponent(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 +} + +// bboxComponent returns the component data for a bounding box. It translates the coordinates to the origin and size +// format that the client expects. +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)}, + } +} diff --git a/server/internal/packbuilder/blocks.go b/server/internal/packbuilder/blocks.go new file mode 100644 index 000000000..f2a281601 --- /dev/null +++ b/server/internal/packbuilder/blocks.go @@ -0,0 +1,80 @@ +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. +) + +// 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) { + 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 world.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) + } +} diff --git a/server/internal/packbuilder/pack_icon.png b/server/internal/packbuilder/pack_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a7723d9b11246054fa2f66d0cfdb2680b3a8a6d1 GIT binary patch literal 5953 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 literal 0 HcmV?d00001 diff --git a/server/internal/packbuilder/resource_pack.go b/server/internal/packbuilder/resource_pack.go index 8f2a32faa..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. @@ -23,8 +27,15 @@ func BuildResourcePack() (*resource.Pack, bool) { assets += itemCount lang = append(lang, itemLang...) + blockCount, blockLang := buildBlocks(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) diff --git a/server/internal/sliceutil/sliceutil.go b/server/internal/sliceutil/sliceutil.go index 739ec8d71..f7a9c41b5 100644 --- a/server/internal/sliceutil/sliceutil.go +++ b/server/internal/sliceutil/sliceutil.go @@ -1,6 +1,6 @@ package sliceutil -import "golang.org/x/exp/slices" +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. diff --git a/server/item/book_and_quill.go b/server/item/book_and_quill.go index a9437d5fa..76ef0d313 100644 --- a/server/item/book_and_quill.go +++ b/server/item/book_and_quill.go @@ -1,6 +1,6 @@ package item -import "golang.org/x/exp/slices" +import "slices" // BookAndQuill is an item used to write WrittenBook(s). type BookAndQuill struct { 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/item/inventory/inventory.go b/server/item/inventory/inventory.go index 6b4a00eae..3cf60757d 100644 --- a/server/item/inventory/inventory.go +++ b/server/item/inventory/inventory.go @@ -4,8 +4,8 @@ import ( "errors" "fmt" "github.com/df-mc/dragonfly/server/item" - "golang.org/x/exp/slices" "math" + "slices" "strings" "sync" ) diff --git a/server/item/recipe/register.go b/server/item/recipe/register.go index 0117da45c..7fa66069a 100644 --- a/server/item/recipe/register.go +++ b/server/item/recipe/register.go @@ -1,7 +1,7 @@ package recipe import ( - "golang.org/x/exp/slices" + "slices" ) // recipes is a list of each recipe. diff --git a/server/item/stack.go b/server/item/stack.go index ca3cbc554..92eb3a47d 100644 --- a/server/item/stack.go +++ b/server/item/stack.go @@ -3,8 +3,8 @@ package item import ( "fmt" "github.com/df-mc/dragonfly/server/world" - "golang.org/x/exp/slices" "reflect" + "slices" "sort" "strings" "sync/atomic" diff --git a/server/player/scoreboard/scoreboard.go b/server/player/scoreboard/scoreboard.go index 9047e00d2..1167d9847 100644 --- a/server/player/scoreboard/scoreboard.go +++ b/server/player/scoreboard/scoreboard.go @@ -2,7 +2,7 @@ package scoreboard import ( "fmt" - "golang.org/x/exp/slices" + "slices" "strings" ) diff --git a/server/server.go b/server/server.go index 5e6351b64..adf2700d9 100644 --- a/server/server.go +++ b/server/server.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/df-mc/atomic" "github.com/df-mc/dragonfly/server/cmd" + "github.com/df-mc/dragonfly/server/internal/blockinternal" "github.com/df-mc/dragonfly/server/internal/iteminternal" "github.com/df-mc/dragonfly/server/internal/sliceutil" _ "github.com/df-mc/dragonfly/server/item" // Imported for maintaining correct initialisation order. @@ -47,7 +48,8 @@ type Server struct { world, nether, end *world.World - customItems []protocol.ItemComponentEntry + customBlocks []protocol.BlockEntry + customItems []protocol.ItemComponentEntry listeners []Listener incoming chan *session.Session @@ -273,6 +275,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)) @@ -286,6 +289,21 @@ 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), + } + } +} + // makeItemComponents initializes the server's item components map using the // registered custom items. It allows item components to be created only once // at startup @@ -360,8 +378,9 @@ func (srv *Server) defaultGameData() minecraft.GameData { PlayerPermissions: packet.PermissionLevelMember, PlayerPosition: vec64To32(srv.world.Spawn().Vec3Centre().Add(mgl64.Vec3{0, 1.62})), - Items: srv.itemEntries(), - GameRules: []protocol.GameRule{{Name: "naturalregeneration", Value: false}}, + Items: srv.itemEntries(), + CustomBlocks: srv.customBlocks, + GameRules: []protocol.GameRule{{Name: "naturalregeneration", Value: false}}, ServerAuthoritativeInventory: true, PlayerMovementSettings: protocol.PlayerMovementSettings{ diff --git a/server/session/handler_crafting.go b/server/session/handler_crafting.go index 739368a4d..6501691bd 100644 --- a/server/session/handler_crafting.go +++ b/server/session/handler_crafting.go @@ -8,8 +8,8 @@ import ( "github.com/df-mc/dragonfly/server/item/recipe" "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol" - "golang.org/x/exp/slices" "math" + "slices" ) // handleCraft handles the CraftRecipe request action. diff --git a/server/session/handler_enchanting.go b/server/session/handler_enchanting.go index 235f851db..7c2166d74 100644 --- a/server/session/handler_enchanting.go +++ b/server/session/handler_enchanting.go @@ -9,9 +9,9 @@ import ( "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" - "golang.org/x/exp/slices" "math" "math/rand" + "slices" ) const ( diff --git a/server/world/block.go b/server/world/block.go index 375fb9b16..eba68c54e 100644 --- a/server/world/block.go +++ b/server/world/block.go @@ -4,7 +4,9 @@ import ( "fmt" "github.com/brentp/intintmap" "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/customblock" "github.com/df-mc/dragonfly/server/world/chunk" + "image" "math" "math/rand" ) @@ -25,6 +27,26 @@ type Block interface { Model() BlockModel } +// CustomBlock represents a block that is non-vanilla and requires a resource pack and extra steps to show it to the +// 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. + 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[string]image.Image +} + // Liquid represents a block that can be moved through and which can flow in the world after placement. There // are two liquids in vanilla, which are lava and water. type Liquid interface { @@ -57,9 +79,10 @@ var hashes = intintmap.New(7000, 0.999) // block passed. RegisterBlock panics if the block properties returned were not valid, existing properties. func RegisterBlock(b Block) { name, properties := b.EncodeBlock() - h := stateHash{name: name, properties: hashProperties(properties)} - - rid, ok := stateRuntimeIDs[h] + if _, ok := b.(CustomBlock); ok { + registerBlockState(blockState{Name: name, Properties: properties}, true) + } + rid, ok := stateRuntimeIDs[stateHash{name: name, properties: hashProperties(properties)}] if !ok { // We assume all blocks must have all their states registered beforehand. Vanilla blocks will have // this done through registering of all states present in the block_states.nbt file. @@ -93,6 +116,11 @@ func RegisterBlock(b Block) { if _, ok := b.(LiquidDisplacer); ok { liquidDisplacingBlocks[rid] = true } + if c, ok := b.(CustomBlock); ok { + if _, ok := customBlocks[name]; !ok { + customBlocks[name] = c + } + } } // BlockRuntimeID attempts to return a runtime ID of a block previously registered using RegisterBlock(). @@ -141,6 +169,11 @@ func BlockByName(name string, properties map[string]any) (Block, bool) { return blocks[rid], true } +// CustomBlocks returns a map of all custom blocks registered with their names as keys. +func CustomBlocks() map[string]CustomBlock { + return customBlocks +} + // air returns an air block. func air() Block { b, _ := BlockByRuntimeID(airRID) diff --git a/server/world/block_state.go b/server/world/block_state.go index c67df1a9f..4cb0a23d6 100644 --- a/server/world/block_state.go +++ b/server/world/block_state.go @@ -6,7 +6,9 @@ import ( "fmt" "github.com/df-mc/dragonfly/server/world/chunk" "github.com/sandertv/gophertunnel/minecraft/nbt" + "github.com/segmentio/fasthash/fnv1" "math" + "slices" "sort" "strings" "unsafe" @@ -20,6 +22,8 @@ var ( // blocks holds a list of all registered Blocks indexed by their runtime ID. Blocks that were not explicitly // 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{} // 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. @@ -48,7 +52,7 @@ func init() { if err := dec.Decode(&s); err != nil { break } - registerBlockState(s) + registerBlockState(s, false) } chunk.RuntimeIDToState = func(runtimeID uint32) (name string, properties map[string]any, found bool) { @@ -69,7 +73,7 @@ func init() { // registerBlockState registers a new blockState to the states slice. The function panics if the properties the // blockState hold are invalid or if the blockState was already registered. -func registerBlockState(s blockState) { +func registerBlockState(s blockState, order bool) { h := stateHash{name: s.Name, properties: hashProperties(s.Properties)} if _, ok := stateRuntimeIDs[h]; ok { panic(fmt.Sprintf("cannot register the same state twice (%+v)", s)) @@ -78,18 +82,39 @@ func registerBlockState(s blockState) { blockProperties[s.Name] = s.Properties } rid := uint32(len(blocks)) + blocks = append(blocks, unknownBlock{s}) + if order { + 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) + }) + + for id, b := range blocks { + name, properties := b.EncodeBlock() + i := stateHash{name: name, properties: hashProperties(properties)} + if name == "minecraft:air" { + airRID = uint32(id) + } + if i == h { + rid = uint32(id) + } + stateRuntimeIDs[i] = uint32(id) + hashes.Put(int64(b.Hash()), int64(id)) + } + } + if s.Name == "minecraft:air" { airRID = rid } - stateRuntimeIDs[h] = rid - blocks = append(blocks, unknownBlock{s}) - nbtBlocks = append(nbtBlocks, false) - randomTickBlocks = append(randomTickBlocks, false) - liquidBlocks = append(liquidBlocks, false) - liquidDisplacingBlocks = append(liquidDisplacingBlocks, false) - chunk.FilteringBlocks = append(chunk.FilteringBlocks, 15) - chunk.LightBlocks = append(chunk.LightBlocks, 0) + nbtBlocks = slices.Insert(nbtBlocks, int(rid), false) + randomTickBlocks = slices.Insert(randomTickBlocks, int(rid), false) + liquidBlocks = slices.Insert(liquidBlocks, int(rid), false) + liquidDisplacingBlocks = slices.Insert(liquidDisplacingBlocks, int(rid), false) + chunk.FilteringBlocks = slices.Insert(chunk.FilteringBlocks, int(rid), 15) + chunk.LightBlocks = slices.Insert(chunk.LightBlocks, int(rid), 0) + stateRuntimeIDs[h] = rid } // unknownBlock represents a block that has not yet been implemented. It is used for registering block diff --git a/server/world/tick.go b/server/world/tick.go index 005b4eb5a..b2b163aae 100644 --- a/server/world/tick.go +++ b/server/world/tick.go @@ -4,8 +4,8 @@ import ( "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/internal/sliceutil" "golang.org/x/exp/maps" - "golang.org/x/exp/slices" "math/rand" + "slices" "time" ) diff --git a/server/world/world.go b/server/world/world.go index f54151122..f8bb440b6 100644 --- a/server/world/world.go +++ b/server/world/world.go @@ -15,7 +15,7 @@ import ( "github.com/go-gl/mathgl/mgl64" "github.com/google/uuid" "golang.org/x/exp/maps" - "golang.org/x/exp/slices" + "slices" ) // World implements a Minecraft world. It manages all aspects of what players can see, such as blocks,