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 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 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,