From 70d2769b8071cadb88e9f9f0575567f2f1b43353 Mon Sep 17 00:00:00 2001 From: Zephyr-OP Date: Tue, 18 Aug 2026 23:18:17 -0600 Subject: [PATCH 1/2] Updated to support 1.26.40 Major update bringing dragonfly up to date with olebeck/dragonfly upstream: - Added bamboo blocks, mosaics, saplings, and models - Added cinnabar and sulfur block types - Added candle block with full registration - Added shulker box with optional colours - Added custom block material system - Added end portal and end portal frame blocks - Added RuntimeIDToHash to BlockRegistry interface - Refactored biome system with unknownBiome fallback - Refactored world biome/highestBlock methods to Tx receivers - Added Synchronous mode for deterministic World testing - Added sub-chunk count validation in NetworkDecodeBuffer - Added redstone system (wire, torch, lever, glowstone, TNT, etc.) - Updated SoilFor methods across dirt/grass/mud/podzol for bamboo - Added end crystal entity with explosion behavior - Added Hopper block - Added portal system for end/nether - Updated hunger, poison, and effect systems - Many block break info and item registration updates - Enchantment text improvements - Session handler updates for crafting, loom, smithing - TNT entity improvements - Area effect cloud improvements - Sign and lectern updates - Various entity movement and metadata fixes > Note: go.mod/go.sum dependency bumps were not included --- cmd/blockaccessor/main.go | 331 ++++ cmd/blockhash/main.go | 4 + server/block/accessor.go | 632 ++++++++ server/block/air.go | 7 + server/block/ancient_debris.go | 2 +- server/block/anvil.go | 2 +- server/block/bamboo.go | 202 +++ server/block/bamboo_block.go | 78 + server/block/bamboo_leaf_size.go | 59 + server/block/bamboo_mosaic.go | 42 + server/block/bamboo_sapling.go | 85 + server/block/banner_pattern_layer.go | 9 +- server/block/banner_pattern_type_register.go | 10 +- server/block/basalt.go | 2 +- server/block/bed.go | 7 +- server/block/bedrock.go | 5 + server/block/blackstone.go | 2 +- server/block/blast_furnace.go | 5 +- server/block/block.go | 8 + server/block/block_behaviour_test.go | 37 + server/block/break_info.go | 115 +- server/block/break_info_test.go | 134 ++ server/block/bricks.go | 2 +- server/block/cake.go | 97 +- server/block/campfire.go | 8 +- server/block/cinnabar.go | 31 + server/block/cinnabar_bricks.go | 22 + server/block/coal.go | 2 +- server/block/coal_ore.go | 2 +- server/block/cobblestone.go | 2 +- server/block/cobweb.go | 2 +- server/block/cocoa_bean.go | 2 +- server/block/composter.go | 4 +- server/block/copper.go | 2 +- server/block/copper_bars.go | 2 +- server/block/copper_chain.go | 2 +- server/block/copper_door.go | 2 +- server/block/copper_golem_statue.go | 2 +- server/block/copper_grate.go | 2 +- server/block/copper_ore.go | 2 +- server/block/copper_trapdoor.go | 2 +- server/block/coral_block.go | 2 +- server/block/cube/axis.go | 14 + server/block/cube/bbox.go | 154 +- server/block/cube/pos.go | 36 +- server/block/customblock/material.go | 16 +- server/block/decorated_pot.go | 9 +- server/block/deepslate.go | 4 +- server/block/deepslate_bricks.go | 2 +- server/block/deepslate_tiles.go | 2 +- server/block/diamond.go | 2 +- server/block/diamond_ore.go | 2 +- server/block/dirt.go | 2 +- server/block/dragon_egg.go | 2 +- server/block/dried_kelp.go | 2 +- server/block/dripstone.go | 2 +- server/block/emerald.go | 2 +- server/block/emerald_ore.go | 2 +- server/block/enchanting_table.go | 2 +- server/block/end_bricks.go | 2 +- server/block/end_portal.go | 65 + server/block/end_portal_frame.go | 89 ++ server/block/end_stone.go | 2 +- server/block/ender_chest.go | 2 +- server/block/explosion.go | 129 +- server/block/farmland.go | 3 +- server/block/fire.go | 22 +- server/block/furnace.go | 5 +- server/block/glowstone.go | 12 +- server/block/gold.go | 2 +- server/block/gold_ore.go | 2 +- server/block/grass.go | 2 +- server/block/gravel.go | 9 + server/block/grindstone.go | 2 +- server/block/hash.go | 77 +- server/block/hopper.go | 24 +- server/block/iron.go | 2 +- server/block/iron_bars.go | 2 +- server/block/iron_chain.go | 2 +- server/block/iron_ore.go | 2 +- server/block/item_frame.go | 4 +- server/block/jukebox.go | 7 +- server/block/lapis_ore.go | 2 +- server/block/lava.go | 9 +- server/block/leaves.go | 3 +- server/block/lectern.go | 7 +- server/block/lever.go | 26 +- server/block/liquid.go | 7 +- server/block/log.go | 10 +- server/block/magma.go | 2 +- server/block/model/bamboo.go | 27 + server/block/model/end_portal_frame.go | 19 + server/block/model/offset.go | 51 + server/block/model/portal.go | 22 + server/block/model/shulker.go | 35 + server/block/mud.go | 2 +- server/block/mud_bricks.go | 2 +- server/block/muddy_mangrove_roots.go | 2 +- server/block/nether_brick_fence.go | 2 +- server/block/nether_bricks.go | 2 +- server/block/netherite.go | 2 +- server/block/note.go | 45 +- server/block/obsidian.go | 14 +- server/block/packed_mud.go | 2 +- server/block/pink_petals.go | 2 +- server/block/planks.go | 2 +- server/block/podzol.go | 2 +- server/block/polished_blackstone_brick.go | 2 +- server/block/polished_cinnabar.go | 22 + server/block/polished_sulfur.go | 22 + server/block/polished_tuff.go | 2 +- server/block/portal.go | 69 + server/block/prismarine.go | 2 +- server/block/purpur.go | 4 +- server/block/quartz.go | 2 +- server/block/raw_copper.go | 2 +- server/block/raw_gold.go | 2 +- server/block/raw_iron.go | 2 +- server/block/redstone_block.go | 29 +- server/block/redstone_ore.go | 2 +- server/block/redstone_test.go | 1363 +++++++++++++++++ server/block/redstone_torch.go | 329 ++-- server/block/redstone_wire.go | 393 ++--- server/block/register.go | 57 +- server/block/reinforced_deepslate.go | 2 +- server/block/resin_bricks.go | 2 +- server/block/sand.go | 2 +- server/block/sandstone.go | 2 +- server/block/sea_pickle.go | 2 +- server/block/shulker_box.go | 300 ++++ server/block/sign.go | 14 +- server/block/slab.go | 12 +- server/block/slab_type.go | 25 + server/block/smoker.go | 5 +- server/block/smooth_basalt.go | 2 +- server/block/stained_terracotta.go | 2 +- server/block/stairs.go | 3 + server/block/stairs_type.go | 25 + server/block/stone.go | 10 +- server/block/stone_bricks.go | 2 +- server/block/sulfur.go | 31 + server/block/sulfur_bricks.go | 22 + server/block/terracotta.go | 2 +- server/block/tinted_glass.go | 29 + server/block/tnt.go | 20 +- server/block/tuff.go | 2 +- server/block/tuff_bricks.go | 2 +- server/block/wall_type.go | 22 + server/block/water.go | 11 +- server/block/wood.go | 6 +- server/block/wood_fence.go | 2 +- server/block/wood_fence_gate.go | 2 +- server/block/wood_type.go | 11 +- server/cmd/argument.go | 3 + server/cmd/command.go | 6 +- server/cmd/target.go | 3 + server/conf.go | 4 + server/entity/area_effect_cloud.go | 10 +- server/entity/area_effect_cloud_behaviour.go | 12 +- server/entity/base_behaviour.go | 20 + server/entity/damage.go | 5 +- server/entity/damageable.go | 38 + server/entity/effect/effect.go | 5 +- server/entity/effect/fatal_poison.go | 2 +- server/entity/effect/poison.go | 2 +- server/entity/end_crystal.go | 85 + server/entity/end_crystal_behaviour.go | 85 + server/entity/ender_pearl.go | 6 +- server/entity/ent.go | 127 +- server/entity/experience_orb_behaviour.go | 13 +- server/entity/firework.go | 5 +- server/entity/firework_behaviour.go | 5 + server/entity/healing.go | 2 +- server/entity/item.go | 6 +- server/entity/item_behaviour.go | 11 +- server/entity/living.go | 5 +- server/entity/movement.go | 24 +- server/entity/passive.go | 28 +- server/entity/projectile.go | 71 +- server/entity/register.go | 2 + server/entity/splashable.go | 6 +- server/entity/stationary.go | 4 +- server/entity/tnt.go | 9 +- server/entity/travel.go | 323 ++++ server/entity/travel_test.go | 594 +++++++ server/internal/blockinternal/components.go | 18 +- server/internal/mcrandom/mix_stafford13.go | 9 + .../internal/mcrandom/xoroshiro128plusplus.go | 25 + server/internal/nbtconv/item.go | 9 +- server/internal/nbtconv/read.go | 135 -- server/internal/nbtconv/write.go | 143 -- server/item/armour.go | 4 +- server/item/creative/creative_items.nbt | Bin 144033 -> 144033 bytes server/item/crossbow.go | 19 +- server/item/enchantment/respiration.go | 2 +- server/item/end_crystal.go | 53 + server/item/ender_eye.go | 47 + server/item/fire_charge.go | 4 + server/item/flint_and_steel.go | 11 +- server/item/goat_horn.go | 4 +- server/item/leggings.go | 2 +- server/item/nbt.go | 337 ++++ server/item/optional_colour.go | 47 + server/item/recipe/crafting_data.nbt | Bin 881633 -> 900455 bytes server/item/recipe/potion_data.nbt | Bin 33442 -> 33442 bytes server/item/register.go | 2 + server/listener.go | 1 + server/player/conf.go | 30 +- server/player/context.go | 52 + server/player/handler.go | 8 +- server/player/input/lock.go | 79 + server/player/input/restricter.go | 13 + server/player/player.go | 379 +++-- server/player/playerdb/inventory.go | 9 +- server/player/playerdb/json.go | 4 +- server/player/ref.go | 49 + server/server.go | 89 +- server/session/chunk.go | 42 +- server/session/command.go | 2 +- server/session/controllable.go | 2 + server/session/enchantment_texts.go | 2 +- server/session/entity_metadata.go | 69 +- server/session/handler_anvil.go | 2 +- server/session/handler_crafting.go | 4 +- .../session/handler_inventory_transaction.go | 10 +- server/session/handler_item_stack_request.go | 2 +- server/session/handler_loom.go | 9 +- server/session/handler_npc_request.go | 4 + server/session/handler_player_auth_input.go | 54 +- server/session/handler_smithing.go | 7 +- server/session/player.go | 103 +- server/session/session.go | 91 +- server/session/session_list.go | 15 +- server/session/text.go | 5 +- server/session/view_layer.go | 16 + server/session/world.go | 93 +- server/world/biome.go | 22 +- server/world/block.go | 50 - server/world/block_search.go | 103 ++ server/world/block_source.go | 6 +- server/world/block_states.nbt | Bin 2365370 -> 2436125 bytes server/world/chunk/block_registry.go | 2 + server/world/chunk/decode.go | 15 +- server/world/chunk/paletted_storage.go | 6 + server/world/chunk_request.go | 143 ++ server/world/conf.go | 52 +- server/world/dimension.go | 59 + server/world/entity.go | 208 ++- server/world/entity_ref.go | 77 + server/world/entity_schedule.go | 179 +++ server/world/explosion.go | 62 + server/world/game_mode.go | 63 +- server/world/generator.go | 21 +- server/world/handler.go | 50 +- server/world/loader.go | 123 +- server/world/portal/end.go | 185 +++ server/world/portal/end_test.go | 225 +++ server/world/portal/nether.go | 335 ++++ server/world/portal/nether_test.go | 245 +++ server/world/portal/scan.go | 192 +++ server/world/provider.go | 51 + server/world/redstone.go | 1214 +++++++++++++++ server/world/redstone_bench_test.go | 43 + server/world/redstone_test.go | 1115 ++++++++++++++ server/world/settings.go | 3 + server/world/sound/block.go | 12 + server/world/sound/custom.go | 14 + server/world/task.go | 419 +++++ server/world/task_test.go | 394 +++++ server/world/tick.go | 56 +- server/world/tx.go | 217 ++- server/world/tx_redstone.go | 97 ++ server/world/vanilla_items.nbt | Bin 192121 -> 195071 bytes server/world/view_layer.go | 37 +- server/world/viewer.go | 4 + server/world/weather.go | 48 +- server/world/world.go | 409 +++-- server/world/world_test.go | 259 ++++ 278 files changed, 14487 insertions(+), 1833 deletions(-) create mode 100644 cmd/blockaccessor/main.go create mode 100644 server/block/accessor.go create mode 100644 server/block/bamboo.go create mode 100644 server/block/bamboo_block.go create mode 100644 server/block/bamboo_leaf_size.go create mode 100644 server/block/bamboo_mosaic.go create mode 100644 server/block/bamboo_sapling.go create mode 100644 server/block/block_behaviour_test.go create mode 100644 server/block/break_info_test.go create mode 100644 server/block/cinnabar.go create mode 100644 server/block/cinnabar_bricks.go create mode 100644 server/block/end_portal.go create mode 100644 server/block/end_portal_frame.go create mode 100644 server/block/model/bamboo.go create mode 100644 server/block/model/end_portal_frame.go create mode 100644 server/block/model/offset.go create mode 100644 server/block/model/portal.go create mode 100644 server/block/model/shulker.go create mode 100644 server/block/polished_cinnabar.go create mode 100644 server/block/polished_sulfur.go create mode 100644 server/block/portal.go create mode 100644 server/block/redstone_test.go create mode 100644 server/block/shulker_box.go create mode 100644 server/block/sulfur.go create mode 100644 server/block/sulfur_bricks.go create mode 100644 server/block/tinted_glass.go create mode 100644 server/entity/base_behaviour.go create mode 100644 server/entity/damageable.go create mode 100644 server/entity/end_crystal.go create mode 100644 server/entity/end_crystal_behaviour.go create mode 100644 server/entity/travel.go create mode 100644 server/entity/travel_test.go create mode 100644 server/internal/mcrandom/mix_stafford13.go create mode 100644 server/internal/mcrandom/xoroshiro128plusplus.go create mode 100644 server/item/end_crystal.go create mode 100644 server/item/ender_eye.go create mode 100644 server/item/nbt.go create mode 100644 server/item/optional_colour.go create mode 100644 server/player/context.go create mode 100644 server/player/input/lock.go create mode 100644 server/player/input/restricter.go create mode 100644 server/player/ref.go create mode 100644 server/world/block_search.go create mode 100644 server/world/chunk_request.go create mode 100644 server/world/entity_ref.go create mode 100644 server/world/entity_schedule.go create mode 100644 server/world/explosion.go create mode 100644 server/world/portal/end.go create mode 100644 server/world/portal/end_test.go create mode 100644 server/world/portal/nether.go create mode 100644 server/world/portal/nether_test.go create mode 100644 server/world/portal/scan.go create mode 100644 server/world/redstone.go create mode 100644 server/world/redstone_bench_test.go create mode 100644 server/world/redstone_test.go create mode 100644 server/world/sound/custom.go create mode 100644 server/world/task.go create mode 100644 server/world/task_test.go create mode 100644 server/world/tx_redstone.go create mode 100644 server/world/world_test.go diff --git a/cmd/blockaccessor/main.go b/cmd/blockaccessor/main.go new file mode 100644 index 0000000000..36767af290 --- /dev/null +++ b/cmd/blockaccessor/main.go @@ -0,0 +1,331 @@ +package main + +import ( + "bytes" + "flag" + "go/ast" + "go/format" + "log" + "os" + "path/filepath" + "sort" + "text/template" + + "golang.org/x/tools/go/packages" +) + +// accessorSpec describes a generated getter/setter method pair. A block qualifies for a spec when it +// implements EncodeBlock and has a field named Field of the type Pkg.FieldType. +type accessorSpec struct { + // Iface is the name of the interface generated for the accessor pair, such as 'HasFacing'. + Iface string + // IfaceDoc is the doc comment written above the generated interface. + IfaceDoc string + // Method is the name of the generated setter method, such as 'WithFacing'. + Method string + // MethodDoc is the doc comment written above each generated setter method. + MethodDoc string + // Getter is the name of the generated getter method, such as 'FacingDirection'. It must differ from + // Field: a method cannot share its name with a struct field. + Getter string + // GetterDoc is the doc comment written above each generated getter method. + GetterDoc string + // Param is the name of the setter's parameter. + Param string + // ParamType is the qualified type of the setter's parameter, such as 'cube.Direction'. + ParamType string + // Field is the name of the struct field set by the setter. + Field string + // Pkg and FieldType are the package identifier and type name the field must have to qualify. + Pkg, FieldType string +} + +var specs = []accessorSpec{ + { + Iface: "HasFacing", + IfaceDoc: `HasFacing represents a block with a horizontal facing direction. Blocks that face any of the six +// block faces (such as torches, levers and hoppers) or that use an attachment do not implement it.`, + Method: "WithFacing", + MethodDoc: `WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door.`, + Getter: "FacingDirection", + GetterDoc: `FacingDirection returns the horizontal direction the block faces.`, + Param: "facing", ParamType: "cube.Direction", + Field: "Facing", Pkg: "cube", FieldType: "Direction", + }, + { + Iface: "HasAxis", + IfaceDoc: `HasAxis represents a block oriented along one of the three axes, such as logs and pillars.`, + Method: "WithAxis", + MethodDoc: `WithAxis returns a copy of the block with its axis set to axis.`, + Getter: "PillarAxis", + GetterDoc: `PillarAxis returns the axis the block is oriented along.`, + Param: "axis", ParamType: "cube.Axis", + Field: "Axis", Pkg: "cube", FieldType: "Axis", + }, + { + Iface: "HasColour", + IfaceDoc: `HasColour represents a block that comes in the sixteen dye colours, such as wool and concrete. On +// some blocks, such as beds and banners, the colour is stored as block entity data rather than in the +// encoded block state.`, + Method: "WithColour", + MethodDoc: `WithColour returns a copy of the block with its colour set to colour.`, + Getter: "DyeColour", + GetterDoc: `DyeColour returns the dye colour of the block.`, + Param: "colour", ParamType: "item.Colour", + Field: "Colour", Pkg: "item", FieldType: "Colour", + }, +} + +var fileTemplate = template.Must(template.New("accessor").Parse(`// Code generated by cmd/blockaccessor; DO NOT EDIT. + +package {{.PkgName}} + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" +) +{{range .Specs}} +// {{.IfaceDoc}} +type {{.Iface}} interface { + world.Block + {{.Getter}}() {{.ParamType}} + {{.Method}}({{.Param}} {{.ParamType}}) world.Block +} +{{end}}{{range .Methods}} +// {{.Spec.GetterDoc}} +func (b {{.Struct}}) {{.Spec.Getter}}() {{.Spec.ParamType}} { + return b.{{.Spec.Field}} +} + +// {{.Spec.MethodDoc}} +func (b {{.Struct}}) {{.Spec.Method}}({{.Spec.Param}} {{.Spec.ParamType}}) world.Block { + b.{{.Spec.Field}} = {{.Spec.Param}} + return b +} +{{end}}`)) + +func main() { + out := flag.String("o", "", "output file for accessor interfaces and methods") + flag.Parse() + + if len(flag.Args()) != 1 { + log.Fatalln("Must pass one package to produce accessor methods for.") + } + cfg := &packages.Config{ + Mode: packages.NeedName | packages.NeedSyntax | packages.NeedFiles, + } + pkgs, err := packages.Load(cfg, flag.Args()[0]) + if err != nil { + log.Fatalln(err) + } + if len(pkgs) != 1 { + log.Fatalln("Expected to load exactly one package.") + } + procPackage(pkgs[0], *out) +} + +func procPackage(pkg *packages.Package, out string) { + b := &accessorBuilder{ + pkg: pkg, + out: filepath.Base(out), + fields: make(map[string][]*ast.Field), + aliases: make(map[string]string), + handled: map[string]struct{}{}, + blocks: map[string]struct{}{}, + methods: map[string]struct{}{}, + } + b.readStructFields() + b.readFuncs() + buf := &bytes.Buffer{} + if err := fileTemplate.Execute(buf, struct { + PkgName string + Specs []accessorSpec + Methods []methodData + }{PkgName: pkg.Name, Specs: specs, Methods: b.resolveMethods()}); err != nil { + log.Fatalln(err) + } + src, err := format.Source(buf.Bytes()) + if err != nil { + log.Fatalln(err) + } + if err := os.WriteFile(out, src, 0644); err != nil { + log.Fatalln(err) + } +} + +// methodData holds the data needed to generate the accessor methods of one spec for one block struct. +type methodData struct { + Struct string + Spec accessorSpec +} + +type accessorBuilder struct { + pkg *packages.Package + // out is the base name of the generated file, excluded when reading the package so that previously + // generated methods are not treated as hand-written ones. + out string + fields map[string][]*ast.Field + aliases map[string]string + handled map[string]struct{} + // blocks holds the names of all structs that implement EncodeBlock with a value receiver. + blocks map[string]struct{} + // methods holds 'Struct.Method' keys for every hand-written method in the package, used to avoid + // generating an accessor that would collide with an existing method. + methods map[string]struct{} +} + +// resolveMethods returns the sorted list of accessor methods to generate, matching each spec against the +// fields of every block struct. +func (b *accessorBuilder) resolveMethods() []methodData { + var methods []methodData + for _, spec := range specs { + var names []string + for name := range b.blocks { + if !b.hasField(name, spec) { + continue + } + b.checkCollision(name, spec.Method) + b.checkCollision(name, spec.Getter) + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + methods = append(methods, methodData{Struct: name, Spec: spec}) + } + } + return methods +} + +// checkCollision exits with an error if the struct passed has a hand-written method with the name +// passed: these methods are generated exclusively, so that their behaviour cannot diverge per block. +func (b *accessorBuilder) checkCollision(structName, method string) { + if _, ok := b.methods[structName+"."+method]; ok { + log.Fatalln(structName + "." + method + " is generated by cmd/blockaccessor and must not be written by hand.") + } +} + +// hasField reports whether the struct passed has a field with the name and type required by spec. +func (b *accessorBuilder) hasField(structName string, spec accessorSpec) bool { + for _, field := range b.fields[structName] { + for _, name := range field.Names { + if name.Name != spec.Field { + continue + } + if s, ok := field.Type.(*ast.SelectorExpr); ok { + if x, ok := s.X.(*ast.Ident); ok && x.Name == spec.Pkg && s.Sel.Name == spec.FieldType { + return true + } + } + } + } + return false +} + +// syntax returns the syntax trees of the package, excluding the previously generated output file. +func (b *accessorBuilder) syntax() []*ast.File { + files := make([]*ast.File, 0, len(b.pkg.Syntax)) + for _, f := range b.pkg.Syntax { + if filepath.Base(b.pkg.Fset.Position(f.Pos()).Filename) == b.out { + continue + } + files = append(files, f) + } + return files +} + +func (b *accessorBuilder) readFuncs() { + for _, f := range b.syntax() { + ast.Inspect(f, b.readFuncDecls) + } +} + +func (b *accessorBuilder) readFuncDecls(node ast.Node) bool { + fun, ok := node.(*ast.FuncDecl) + if !ok || fun.Recv == nil { + return true + } + // Only value receivers qualify: the generated accessors copy the receiver. + ident, ok := fun.Recv.List[0].Type.(*ast.Ident) + if !ok { + return true + } + b.methods[ident.Name+"."+fun.Name.Name] = struct{}{} + if fun.Name.Name == "EncodeBlock" { + b.blocks[ident.Name] = struct{}{} + } + return true +} + +func (b *accessorBuilder) readStructFields() { + for _, f := range b.syntax() { + ast.Inspect(f, b.readStructs) + } + b.resolveEmbedded() + b.resolveAliases() +} + +func (b *accessorBuilder) resolveAliases() { + for name, alias := range b.aliases { + b.fields[name] = b.findFields(alias) + } +} + +func (b *accessorBuilder) findFields(structName string) []*ast.Field { + for { + if fields, ok := b.fields[structName]; ok { + // Alias found in the fields map, so it referred to a struct directly. + return fields + } + if nested, ok := b.aliases[structName]; ok { + // The alias itself was an alias, so continue with the next. + structName = nested + continue + } + // Neither an alias nor a struct: Break as this isn't going to go anywhere. + return nil + } +} + +func (b *accessorBuilder) resolveEmbedded() { + for name, fields := range b.fields { + if _, ok := b.handled[name]; ok { + continue + } + newFields := make([]*ast.Field, 0, len(fields)) + for _, f := range fields { + if len(f.Names) == 0 { + if ident, ok := f.Type.(*ast.Ident); ok { + for _, af := range b.findFields(ident.Name) { + if len(af.Names) == 0 { + // The struct embedded has an embedded struct of its own: Resolve that one + // first by restarting the process. + b.resolveEmbedded() + return + } + } + newFields = append(newFields, b.findFields(ident.Name)...) + } + } else { + newFields = append(newFields, f) + } + } + b.handled[name] = struct{}{} + b.fields[name] = newFields + } +} + +func (b *accessorBuilder) readStructs(node ast.Node) bool { + s, ok := node.(*ast.TypeSpec) + if !ok { + return true + } + switch t := s.Type.(type) { + case *ast.StructType: + b.fields[s.Name.Name] = t.Fields.List + case *ast.Ident: + b.aliases[s.Name.Name] = t.Name + } + return true +} diff --git a/cmd/blockhash/main.go b/cmd/blockhash/main.go index 4f8910fc95..94a40cbf5a 100644 --- a/cmd/blockhash/main.go +++ b/cmd/blockhash/main.go @@ -244,6 +244,8 @@ func (b *hashBuilder) ftype(structName, s string, expr ast.Expr, directives map[ return "uint64(" + s + ".FaceUint8())", 3 } return "uint64(" + s + ".Uint8())", 5 + case "OptionalColour": + return "uint64(" + s + ".Uint8())", 5 case "GrindstoneAttachment": return "uint64(" + s + ".Uint8())", 2 case "WoodType", "LeavesType", "FlowerType", "DoubleFlowerType", "Colour", "MushroomType", "SeagrassType": @@ -256,6 +258,8 @@ func (b *hashBuilder) ftype(structName, s string, expr ast.Expr, directives map[ return "uint64(" + s + ".Uint8())", 2 case "OreType", "FireType", "DoubleTallGrassType": return "uint64(" + s + ".Uint8())", 1 + case "BambooLeafSize": + return "uint64(" + s + ".Uint8())", 2 case "Direction", "Axis": return "uint64(" + s + ")", 2 case "Face": diff --git a/server/block/accessor.go b/server/block/accessor.go new file mode 100644 index 0000000000..10065e51ff --- /dev/null +++ b/server/block/accessor.go @@ -0,0 +1,632 @@ +// Code generated by cmd/blockaccessor; DO NOT EDIT. + +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" +) + +// HasFacing represents a block with a horizontal facing direction. Blocks that face any of the six +// block faces (such as torches, levers and hoppers) or that use an attachment do not implement it. +type HasFacing interface { + world.Block + FacingDirection() cube.Direction + WithFacing(facing cube.Direction) world.Block +} + +// HasAxis represents a block oriented along one of the three axes, such as logs and pillars. +type HasAxis interface { + world.Block + PillarAxis() cube.Axis + WithAxis(axis cube.Axis) world.Block +} + +// HasColour represents a block that comes in the sixteen dye colours, such as wool and concrete. On +// some blocks, such as beds and banners, the colour is stored as block entity data rather than in the +// encoded block state. +type HasColour interface { + world.Block + DyeColour() item.Colour + WithColour(colour item.Colour) world.Block +} + +// FacingDirection returns the horizontal direction the block faces. +func (b Anvil) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b Anvil) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b Bed) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b Bed) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b BlastFurnace) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b BlastFurnace) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b Campfire) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b Campfire) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b Chest) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b Chest) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b CocoaBean) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b CocoaBean) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b CopperDoor) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b CopperDoor) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b CopperGolemStatue) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b CopperGolemStatue) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b CopperTrapdoor) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b CopperTrapdoor) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b DecoratedPot) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b DecoratedPot) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b EndPortalFrame) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b EndPortalFrame) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b EnderChest) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b EnderChest) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b Furnace) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b Furnace) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b GlazedTerracotta) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b GlazedTerracotta) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b Grindstone) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b Grindstone) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b Ladder) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b Ladder) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b Lectern) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b Lectern) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b LitPumpkin) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b LitPumpkin) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b Loom) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b Loom) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b PinkPetals) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b PinkPetals) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b Pumpkin) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b Pumpkin) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b Smoker) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b Smoker) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b Stairs) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b Stairs) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b Stonecutter) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b Stonecutter) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b WoodDoor) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b WoodDoor) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b WoodFenceGate) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b WoodFenceGate) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// FacingDirection returns the horizontal direction the block faces. +func (b WoodTrapdoor) FacingDirection() cube.Direction { + return b.Facing +} + +// WithFacing returns a copy of the block with its facing set to facing. It does not update any +// other blocks that the block may be part of, such as the second half of a bed or door. +func (b WoodTrapdoor) WithFacing(facing cube.Direction) world.Block { + b.Facing = facing + return b +} + +// PillarAxis returns the axis the block is oriented along. +func (b BambooBlock) PillarAxis() cube.Axis { + return b.Axis +} + +// WithAxis returns a copy of the block with its axis set to axis. +func (b BambooBlock) WithAxis(axis cube.Axis) world.Block { + b.Axis = axis + return b +} + +// PillarAxis returns the axis the block is oriented along. +func (b Basalt) PillarAxis() cube.Axis { + return b.Axis +} + +// WithAxis returns a copy of the block with its axis set to axis. +func (b Basalt) WithAxis(axis cube.Axis) world.Block { + b.Axis = axis + return b +} + +// PillarAxis returns the axis the block is oriented along. +func (b Bone) PillarAxis() cube.Axis { + return b.Axis +} + +// WithAxis returns a copy of the block with its axis set to axis. +func (b Bone) WithAxis(axis cube.Axis) world.Block { + b.Axis = axis + return b +} + +// PillarAxis returns the axis the block is oriented along. +func (b CopperChain) PillarAxis() cube.Axis { + return b.Axis +} + +// WithAxis returns a copy of the block with its axis set to axis. +func (b CopperChain) WithAxis(axis cube.Axis) world.Block { + b.Axis = axis + return b +} + +// PillarAxis returns the axis the block is oriented along. +func (b Deepslate) PillarAxis() cube.Axis { + return b.Axis +} + +// WithAxis returns a copy of the block with its axis set to axis. +func (b Deepslate) WithAxis(axis cube.Axis) world.Block { + b.Axis = axis + return b +} + +// PillarAxis returns the axis the block is oriented along. +func (b Froglight) PillarAxis() cube.Axis { + return b.Axis +} + +// WithAxis returns a copy of the block with its axis set to axis. +func (b Froglight) WithAxis(axis cube.Axis) world.Block { + b.Axis = axis + return b +} + +// PillarAxis returns the axis the block is oriented along. +func (b HayBale) PillarAxis() cube.Axis { + return b.Axis +} + +// WithAxis returns a copy of the block with its axis set to axis. +func (b HayBale) WithAxis(axis cube.Axis) world.Block { + b.Axis = axis + return b +} + +// PillarAxis returns the axis the block is oriented along. +func (b InfestedDeepslate) PillarAxis() cube.Axis { + return b.Axis +} + +// WithAxis returns a copy of the block with its axis set to axis. +func (b InfestedDeepslate) WithAxis(axis cube.Axis) world.Block { + b.Axis = axis + return b +} + +// PillarAxis returns the axis the block is oriented along. +func (b IronChain) PillarAxis() cube.Axis { + return b.Axis +} + +// WithAxis returns a copy of the block with its axis set to axis. +func (b IronChain) WithAxis(axis cube.Axis) world.Block { + b.Axis = axis + return b +} + +// PillarAxis returns the axis the block is oriented along. +func (b Log) PillarAxis() cube.Axis { + return b.Axis +} + +// WithAxis returns a copy of the block with its axis set to axis. +func (b Log) WithAxis(axis cube.Axis) world.Block { + b.Axis = axis + return b +} + +// PillarAxis returns the axis the block is oriented along. +func (b MuddyMangroveRoots) PillarAxis() cube.Axis { + return b.Axis +} + +// WithAxis returns a copy of the block with its axis set to axis. +func (b MuddyMangroveRoots) WithAxis(axis cube.Axis) world.Block { + b.Axis = axis + return b +} + +// PillarAxis returns the axis the block is oriented along. +func (b Portal) PillarAxis() cube.Axis { + return b.Axis +} + +// WithAxis returns a copy of the block with its axis set to axis. +func (b Portal) WithAxis(axis cube.Axis) world.Block { + b.Axis = axis + return b +} + +// PillarAxis returns the axis the block is oriented along. +func (b PurpurPillar) PillarAxis() cube.Axis { + return b.Axis +} + +// WithAxis returns a copy of the block with its axis set to axis. +func (b PurpurPillar) WithAxis(axis cube.Axis) world.Block { + b.Axis = axis + return b +} + +// PillarAxis returns the axis the block is oriented along. +func (b QuartzPillar) PillarAxis() cube.Axis { + return b.Axis +} + +// WithAxis returns a copy of the block with its axis set to axis. +func (b QuartzPillar) WithAxis(axis cube.Axis) world.Block { + b.Axis = axis + return b +} + +// PillarAxis returns the axis the block is oriented along. +func (b Wood) PillarAxis() cube.Axis { + return b.Axis +} + +// WithAxis returns a copy of the block with its axis set to axis. +func (b Wood) WithAxis(axis cube.Axis) world.Block { + b.Axis = axis + return b +} + +// DyeColour returns the dye colour of the block. +func (b Banner) DyeColour() item.Colour { + return b.Colour +} + +// WithColour returns a copy of the block with its colour set to colour. +func (b Banner) WithColour(colour item.Colour) world.Block { + b.Colour = colour + return b +} + +// DyeColour returns the dye colour of the block. +func (b Bed) DyeColour() item.Colour { + return b.Colour +} + +// WithColour returns a copy of the block with its colour set to colour. +func (b Bed) WithColour(colour item.Colour) world.Block { + b.Colour = colour + return b +} + +// DyeColour returns the dye colour of the block. +func (b Carpet) DyeColour() item.Colour { + return b.Colour +} + +// WithColour returns a copy of the block with its colour set to colour. +func (b Carpet) WithColour(colour item.Colour) world.Block { + b.Colour = colour + return b +} + +// DyeColour returns the dye colour of the block. +func (b Concrete) DyeColour() item.Colour { + return b.Colour +} + +// WithColour returns a copy of the block with its colour set to colour. +func (b Concrete) WithColour(colour item.Colour) world.Block { + b.Colour = colour + return b +} + +// DyeColour returns the dye colour of the block. +func (b ConcretePowder) DyeColour() item.Colour { + return b.Colour +} + +// WithColour returns a copy of the block with its colour set to colour. +func (b ConcretePowder) WithColour(colour item.Colour) world.Block { + b.Colour = colour + return b +} + +// DyeColour returns the dye colour of the block. +func (b GlazedTerracotta) DyeColour() item.Colour { + return b.Colour +} + +// WithColour returns a copy of the block with its colour set to colour. +func (b GlazedTerracotta) WithColour(colour item.Colour) world.Block { + b.Colour = colour + return b +} + +// DyeColour returns the dye colour of the block. +func (b StainedGlass) DyeColour() item.Colour { + return b.Colour +} + +// WithColour returns a copy of the block with its colour set to colour. +func (b StainedGlass) WithColour(colour item.Colour) world.Block { + b.Colour = colour + return b +} + +// DyeColour returns the dye colour of the block. +func (b StainedGlassPane) DyeColour() item.Colour { + return b.Colour +} + +// WithColour returns a copy of the block with its colour set to colour. +func (b StainedGlassPane) WithColour(colour item.Colour) world.Block { + b.Colour = colour + return b +} + +// DyeColour returns the dye colour of the block. +func (b StainedTerracotta) DyeColour() item.Colour { + return b.Colour +} + +// WithColour returns a copy of the block with its colour set to colour. +func (b StainedTerracotta) WithColour(colour item.Colour) world.Block { + b.Colour = colour + return b +} + +// DyeColour returns the dye colour of the block. +func (b Wool) DyeColour() item.Colour { + return b.Colour +} + +// WithColour returns a copy of the block with its colour set to colour. +func (b Wool) WithColour(colour item.Colour) world.Block { + b.Colour = colour + return b +} diff --git a/server/block/air.go b/server/block/air.go index 895946b6d5..e1840a2942 100644 --- a/server/block/air.go +++ b/server/block/air.go @@ -1,5 +1,7 @@ package block +import "github.com/df-mc/dragonfly/server/world" + // Air is the block present in otherwise empty space. type Air struct { empty @@ -12,6 +14,11 @@ func (Air) HasLiquidDrops() bool { return false } +// PortalInterior returns true if air may occupy the inside of a portal frame before activation for the target dimension. +func (Air) PortalInterior(target world.Dimension) bool { + return target == world.Nether +} + // EncodeItem ... func (Air) EncodeItem() (name string, meta int16) { return "minecraft:air", 0 diff --git a/server/block/ancient_debris.go b/server/block/ancient_debris.go index 129c239334..0bd305d346 100644 --- a/server/block/ancient_debris.go +++ b/server/block/ancient_debris.go @@ -13,7 +13,7 @@ type AncientDebris struct { func (a AncientDebris) BreakInfo() BreakInfo { return newBreakInfo(30, func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierDiamond.HarvestLevel - }, pickaxeEffective, oneOf(a)).withBlastResistance(6000) + }, pickaxeEffective, oneOf(a)).withBlastResistance(1200) } // SmeltInfo ... diff --git a/server/block/anvil.go b/server/block/anvil.go index a4e5f043a9..3650ee9d9d 100644 --- a/server/block/anvil.go +++ b/server/block/anvil.go @@ -27,7 +27,7 @@ func (a Anvil) Model() world.BlockModel { // BreakInfo ... func (a Anvil) BreakInfo() BreakInfo { - return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(a)).withBlastResistance(6000) + return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(a)).withBlastResistance(1200) } // Activate ... diff --git a/server/block/bamboo.go b/server/block/bamboo.go new file mode 100644 index 0000000000..c3090e88fb --- /dev/null +++ b/server/block/bamboo.go @@ -0,0 +1,202 @@ +package block + +import ( + "math" + "math/rand/v2" + "time" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" +) + +// Bamboo is a versatile, fast-growing plant found primarily in jungles. +type Bamboo struct { + transparent + bass + + Ready bool + Thick bool + LeafSize BambooLeafSize +} + +var ( + _ item.BoneMealAffected = Bamboo{} + _ Flammable = Bamboo{} +) + +// FuelInfo ... +func (b Bamboo) FuelInfo() item.FuelInfo { + return newFuelInfo(time.Millisecond * 2500) +} + +// EncodeItem ... +func (b Bamboo) EncodeItem() (name string, meta int16) { + return "minecraft:bamboo", 0 +} + +// BoneMeal ... +func (b Bamboo) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult { + top := b.top(pos, tx) + if tx.Block(top).(Bamboo).grow(top, rand.IntN(2)+1, b.maxHeight(top), tx) { + return item.BoneMealResultSmall + } + return item.BoneMealResultNone +} + +// FlammabilityInfo ... +func (b Bamboo) FlammabilityInfo() FlammabilityInfo { + return newFlammabilityInfo(60, 60, true) +} + +// BreakInfo ... +func (b Bamboo) BreakInfo() BreakInfo { + return newBreakInfo(1, alwaysHarvestable, axeEffective, oneOf(b)) +} + +// EncodeBlock ... +func (b Bamboo) EncodeBlock() (string, map[string]any) { + thickness := "thin" + if b.Thick { + thickness = "thick" + } + return "minecraft:bamboo", map[string]any{ + "age_bit": boolByte(b.Ready), + "bamboo_leaf_size": b.LeafSize.String(), + "bamboo_stalk_thickness": thickness, + } +} + +// Model ... +func (b Bamboo) Model() world.BlockModel { + return model.Bamboo{Thick: b.Thick} +} + +// RandomTick ... +func (b Bamboo) RandomTick(pos cube.Pos, tx *world.Tx, r *rand.Rand) { + if tx.Light(pos) >= 9 && r.IntN(3) == 0 { + b.grow(pos, 1, b.maxHeight(pos), tx) + } +} + +// NeighbourUpdateTick ... +func (b Bamboo) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + down := tx.Block(pos.Side(cube.FaceDown)) + switch down.(type) { + case BambooSapling, Bamboo: + return + } + if supportsVegetation(b, down) { + return + } + breakBlock(b, pos, tx) +} + +// UseOnBlock ... +func (b Bamboo) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { + if face == cube.FaceUp { + switch x := tx.Block(pos).(type) { + case Bamboo: + if x.top(pos, tx) != pos { + return false + } + return b.grow(pos, 1, math.MaxInt, tx) + case BambooSapling: + return x.grow(pos, tx) + default: + } + } + + pos, _, used := firstReplaceable(tx, pos, face, b) + if !used { + return false + } + s := BambooSapling{} + if !supportsVegetation(s, tx.Block(pos.Sub(cube.Pos{0, 1}))) { + return false + } + place(tx, pos, s, user, ctx) + return placed(ctx) +} + +// maxHeight returns the maximum height, between 12 and 16, of the bamboo +// stalk at the position passed. +func (b Bamboo) maxHeight(pos cube.Pos) int { + seed := 3129871*uint32(pos.X()) ^ 116129781*uint32(pos.Z()) + seed *= 42317861*seed + 11 + return 12 + int(seed>>24)%5 +} + +// top ... +func (b Bamboo) top(pos cube.Pos, tx *world.Tx) (top cube.Pos) { + top = pos + for { + up := top.Side(cube.FaceUp) + if _, ok := tx.Block(up).(Bamboo); !ok { + return top + } + top = up + } +} + +// grow ... +func (b Bamboo) grow(pos cube.Pos, amount int, maxHeight int, tx *world.Tx) bool { + if !replaceableWith(tx, pos.Side(cube.FaceUp), b) { + return false + } + + height := 1 + for { + if _, ok := tx.Block(pos.Sub(cube.Pos{0, height})).(Bamboo); !ok { + break + } + height++ + if height >= maxHeight { + return false + } + } + + for i, block := range b.growthLayout(height+amount, amount) { + tx.SetBlock(pos.Sub(cube.Pos{0, i - amount}), block, nil) + } + + return true +} + +// growthLayout returns the new top blocks of a stalk grown by amount blocks +// to newHeight, ordered top to bottom. +func (b Bamboo) growthLayout(newHeight, amount int) []world.Block { + stemBlock := Bamboo{Thick: b.Thick || newHeight >= 4} + smallLeavesBlock := Bamboo{Thick: stemBlock.Thick, LeafSize: BambooSizeSmallLeaves()} + bigLeavesBlock := Bamboo{Thick: stemBlock.Thick, LeafSize: BambooSizeLargeLeaves()} + + switch { + case newHeight == 2: + return []world.Block{smallLeavesBlock} + case newHeight == 3: + return []world.Block{smallLeavesBlock, smallLeavesBlock} + case newHeight == 4: + return []world.Block{bigLeavesBlock, smallLeavesBlock, stemBlock, stemBlock} + case newHeight > 4: + newBlocks := []world.Block{bigLeavesBlock, bigLeavesBlock, smallLeavesBlock} + for i, mx := 0, min(amount, newHeight-len(newBlocks)); i < mx; i++ { + newBlocks = append(newBlocks, stemBlock) + } + return newBlocks + } + return nil +} + +// allBamboos ... +func allBamboos() (bamboos []world.Block) { + for _, thick := range []bool{false, true} { + for _, ready := range []bool{false, true} { + for _, leafSize := range BambooLeafSizes() { + bamboos = append(bamboos, Bamboo{Thick: thick, Ready: ready, LeafSize: leafSize}) + } + } + } + return +} diff --git a/server/block/bamboo_block.go b/server/block/bamboo_block.go new file mode 100644 index 0000000000..c03a1a03ff --- /dev/null +++ b/server/block/bamboo_block.go @@ -0,0 +1,78 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" + "time" +) + +// BambooBlock is a rotatable flammable block made from bamboo. +type BambooBlock struct { + solid + bass + + // Axis is the axis which the bamboo block faces. + Axis cube.Axis + // Stripped specifies if the bamboo block is stripped. + Stripped bool +} + +// FlammabilityInfo ... +func (BambooBlock) FlammabilityInfo() FlammabilityInfo { + return newFlammabilityInfo(5, 5, true) +} + +// BreakInfo ... +func (b BambooBlock) BreakInfo() BreakInfo { + return newBreakInfo(2.0, alwaysHarvestable, axeEffective, oneOf(b)) +} + +// FuelInfo ... +func (BambooBlock) FuelInfo() item.FuelInfo { + return newFuelInfo(time.Second * 15) +} + +// UseOnBlock ... +func (b BambooBlock) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) (used bool) { + pos, face, used = firstReplaceable(tx, pos, face, b) + if !used { + return + } + b.Axis = face.Axis() + + place(tx, pos, b, user, ctx) + return placed(ctx) +} + +// Strip ... +func (b BambooBlock) Strip() (world.Block, world.Sound, bool) { + return BambooBlock{Axis: b.Axis, Stripped: true}, nil, !b.Stripped +} + +// EncodeItem ... +func (b BambooBlock) EncodeItem() (name string, meta int16) { + if b.Stripped { + return "minecraft:stripped_bamboo_block", 0 + } + return "minecraft:bamboo_block", 0 +} + +// EncodeBlock ... +func (b BambooBlock) EncodeBlock() (name string, properties map[string]any) { + meta := map[string]any{"pillar_axis": b.Axis.String()} + if b.Stripped { + return "minecraft:stripped_bamboo_block", meta + } + return "minecraft:bamboo_block", meta +} + +// allBambooBlocks ... +func allBambooBlocks() (blocks []world.Block) { + for _, axis := range cube.Axes() { + blocks = append(blocks, BambooBlock{Axis: axis}) + blocks = append(blocks, BambooBlock{Axis: axis, Stripped: true}) + } + return +} diff --git a/server/block/bamboo_leaf_size.go b/server/block/bamboo_leaf_size.go new file mode 100644 index 0000000000..1796e4961c --- /dev/null +++ b/server/block/bamboo_leaf_size.go @@ -0,0 +1,59 @@ +package block + +// BambooLeafSize represents the size of bamboo leaves. +type BambooLeafSize struct { + bamboo +} + +type bamboo uint8 + +// BambooSizeNoLeaves ... +func BambooSizeNoLeaves() BambooLeafSize { + return BambooLeafSize{0} +} + +// BambooSizeSmallLeaves ... +func BambooSizeSmallLeaves() BambooLeafSize { + return BambooLeafSize{1} +} + +// BambooSizeLargeLeaves ... +func BambooSizeLargeLeaves() BambooLeafSize { + return BambooLeafSize{2} +} + +// Uint8 ... +func (b bamboo) Uint8() uint8 { + return uint8(b) +} + +// String ... +func (b bamboo) String() string { + switch b { + case 0: + return "no_leaves" + case 1: + return "small_leaves" + case 2: + return "large_leaves" + } + panic("unknown bamboo leaf size") +} + +// Name ... +func (b bamboo) Name() string { + switch b { + case 0: + return "No Leaves" + case 1: + return "Small Leaves" + case 2: + return "Large Leaves" + } + panic("unknown bamboo leaf size") +} + +// BambooLeafSizes returns all possible bamboo leaf sizes. +func BambooLeafSizes() []BambooLeafSize { + return []BambooLeafSize{BambooSizeNoLeaves(), BambooSizeSmallLeaves(), BambooSizeLargeLeaves()} +} diff --git a/server/block/bamboo_mosaic.go b/server/block/bamboo_mosaic.go new file mode 100644 index 0000000000..7cc9caa5a4 --- /dev/null +++ b/server/block/bamboo_mosaic.go @@ -0,0 +1,42 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/item" + "time" +) + +// BambooMosaic is a decorative bamboo plank variant. +type BambooMosaic struct { + solid + bass +} + +// FlammabilityInfo ... +func (BambooMosaic) FlammabilityInfo() FlammabilityInfo { + return newFlammabilityInfo(5, 20, true) +} + +// BreakInfo ... +func (b BambooMosaic) BreakInfo() BreakInfo { + return newBreakInfo(2, alwaysHarvestable, axeEffective, oneOf(b)).withBlastResistance(3) +} + +// RepairsWoodTools ... +func (BambooMosaic) RepairsWoodTools() bool { + return true +} + +// FuelInfo ... +func (BambooMosaic) FuelInfo() item.FuelInfo { + return newFuelInfo(time.Second * 15) +} + +// EncodeItem ... +func (BambooMosaic) EncodeItem() (name string, meta int16) { + return "minecraft:bamboo_mosaic", 0 +} + +// EncodeBlock ... +func (BambooMosaic) EncodeBlock() (string, map[string]any) { + return "minecraft:bamboo_mosaic", nil +} diff --git a/server/block/bamboo_sapling.go b/server/block/bamboo_sapling.go new file mode 100644 index 0000000000..5687206f34 --- /dev/null +++ b/server/block/bamboo_sapling.go @@ -0,0 +1,85 @@ +package block + +import ( + "math/rand/v2" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" +) + +// BambooSapling ... +type BambooSapling struct { + empty + transparent + bass + + Ready bool +} + +var ( + _ item.BoneMealAffected = BambooSapling{} + _ Flammable = BambooSapling{} +) + +// BoneMeal ... +func (b BambooSapling) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult { + if b.grow(pos, tx) { + return item.BoneMealResultSmall + } + return item.BoneMealResultNone +} + +// FlammabilityInfo ... +func (b BambooSapling) FlammabilityInfo() FlammabilityInfo { + return newFlammabilityInfo(60, 60, true) +} + +// NeighbourUpdateTick ... +func (b BambooSapling) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + down := tx.Block(pos.Side(cube.FaceDown)) + if supportsVegetation(b, down) { + return + } + breakBlock(b, pos, tx) +} + +// RandomTick ... +func (b BambooSapling) RandomTick(pos cube.Pos, tx *world.Tx, r *rand.Rand) { + if tx.Light(pos) >= 9 && r.IntN(3) == 0 { + b.grow(pos, tx) + } +} + +// BreakInfo ... +func (b BambooSapling) BreakInfo() BreakInfo { + return newBreakInfo(0, alwaysHarvestable, axeEffective, oneOf(Bamboo{})).withBlastResistance(1) +} + +// HasLiquidDrops ... +func (b BambooSapling) HasLiquidDrops() bool { + return true +} + +// EncodeBlock ... +func (b BambooSapling) EncodeBlock() (string, map[string]any) { + return "minecraft:bamboo_sapling", map[string]any{"age_bit": boolByte(b.Ready)} +} + +// grow ... +func (b BambooSapling) grow(pos cube.Pos, tx *world.Tx) bool { + if !replaceableWith(tx, pos.Side(cube.FaceUp), b) { + return false + } + + tx.SetBlock(pos, Bamboo{}, nil) + tx.SetBlock(pos.Side(cube.FaceUp), Bamboo{LeafSize: BambooSizeSmallLeaves()}, nil) + return true +} + +// allBambooSaplings ... +func allBambooSaplings() (saplings []world.Block) { + saplings = append(saplings, BambooSapling{Ready: false}) + saplings = append(saplings, BambooSapling{Ready: true}) + return +} diff --git a/server/block/banner_pattern_layer.go b/server/block/banner_pattern_layer.go index 3fef11f136..c47d509d51 100644 --- a/server/block/banner_pattern_layer.go +++ b/server/block/banner_pattern_layer.go @@ -1,6 +1,8 @@ package block import ( + "fmt" + "github.com/df-mc/dragonfly/server/internal/nbtconv" "github.com/df-mc/dragonfly/server/item" ) @@ -23,7 +25,12 @@ func (b BannerPatternLayer) EncodeNBT() map[string]any { // DecodeNBT decodes the given NBT map into a BannerPatternLayer and returns it. func (b BannerPatternLayer) DecodeNBT(data map[string]any) any { - b.Type = BannerPatternByID(nbtconv.String(data, "Pattern")) + id := nbtconv.String(data, "Pattern") + pattern, exists := BannerPatternByID(id) + if !exists { + panic(fmt.Errorf("unknown banner pattern id %q", id)) + } + b.Type = pattern b.Colour = invertColourID(int16(nbtconv.Int32(data, "Color"))) return b } diff --git a/server/block/banner_pattern_type_register.go b/server/block/banner_pattern_type_register.go index f6a6d90140..b906b81edc 100644 --- a/server/block/banner_pattern_type_register.go +++ b/server/block/banner_pattern_type_register.go @@ -57,13 +57,11 @@ func registerBannerPattern(id string, pattern BannerPatternType) { bannerPatternIDs[pattern] = id } -// BannerPatternByID returns a banner pattern by the ID it was registered with. -func BannerPatternByID(id string) BannerPatternType { +// BannerPatternByID returns a banner pattern by the ID it was registered with. Second return value describes whether +// a banner pattern with the ID was found. +func BannerPatternByID(id string) (BannerPatternType, bool) { b, ok := bannerPatternsMap[id] - if !ok { - panic("should never happen") - } - return b + return b, ok } // bannerPatternID returns the ID a banner pattern was registered with. diff --git a/server/block/basalt.go b/server/block/basalt.go index eb66d5080d..0abdfcea23 100644 --- a/server/block/basalt.go +++ b/server/block/basalt.go @@ -32,7 +32,7 @@ func (b Basalt) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world // BreakInfo ... func (b Basalt) BreakInfo() BreakInfo { - return newBreakInfo(1.25, pickaxeHarvestable, pickaxeEffective, oneOf(b)).withBlastResistance(21) + return newBreakInfo(1.25, pickaxeHarvestable, pickaxeEffective, oneOf(b)).withBlastResistance(4.2) } // EncodeItem ... diff --git a/server/block/bed.go b/server/block/bed.go index b02f0299a2..916e18aa5a 100644 --- a/server/block/bed.go +++ b/server/block/bed.go @@ -106,9 +106,12 @@ func (b Bed) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, _ *i if w.Dimension() != world.Overworld { tx.SetBlock(pos, nil, nil) ExplosionConfig{ - Size: 5, SpawnFire: true, - }.Explode(tx, pos.Vec3Centre()) + }.Explode(tx, world.BlockExplosionSource{ + Block: b, + Pos: pos, + ExplosionSize: 5, + }) return true } diff --git a/server/block/bedrock.go b/server/block/bedrock.go index a152bcf66c..dde5dd8ad7 100644 --- a/server/block/bedrock.go +++ b/server/block/bedrock.go @@ -20,3 +20,8 @@ func (b Bedrock) EncodeBlock() (name string, properties map[string]any) { //noinspection SpellCheckingInspection return "minecraft:bedrock", map[string]any{"infiniburn_bit": b.InfiniteBurning} } + +// SupportsEndCrystal always returns true. +func (Bedrock) SupportsEndCrystal() bool { + return true +} diff --git a/server/block/blackstone.go b/server/block/blackstone.go index f8426047c8..d3b7d3b4d1 100644 --- a/server/block/blackstone.go +++ b/server/block/blackstone.go @@ -38,7 +38,7 @@ func (b Blackstone) BreakInfo() BreakInfo { hardness = 2 } - return newBreakInfo(hardness, pickaxeHarvestable, pickaxeEffective, drops).withBlastResistance(30) + return newBreakInfo(hardness, pickaxeHarvestable, pickaxeEffective, drops).withBlastResistance(6) } // EncodeItem ... diff --git a/server/block/blast_furnace.go b/server/block/blast_furnace.go index e63cfedc93..5eee346eba 100644 --- a/server/block/blast_furnace.go +++ b/server/block/blast_furnace.go @@ -81,7 +81,10 @@ func (b BlastFurnace) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx // BreakInfo ... func (b BlastFurnace) BreakInfo() BreakInfo { - xp := b.Experience() + xp := 0 + if b.smelter != nil { + xp = b.Experience() + } return newBreakInfo(3.5, alwaysHarvestable, pickaxeEffective, oneOf(BlastFurnace{})).withXPDropRange(xp, xp).withBreakHandler(func(pos cube.Pos, tx *world.Tx, u item.User) { for _, i := range b.Inventory(tx, pos).Clear() { dropItem(tx, i, pos.Vec3()) diff --git a/server/block/block.go b/server/block/block.go index 044f65a81b..9707d7e5bd 100644 --- a/server/block/block.go +++ b/server/block/block.go @@ -54,6 +54,14 @@ type LightDiffuser interface { LightDiffusionLevel() uint8 } +// NonSuffocating represents a block that, despite being fully solid, never suffocates an entity standing +// inside it. This is distinct from LightDiffuser: a block may block all light while still being safe to +// stand inside, such as tinted glass. +type NonSuffocating interface { + // PreventsSuffocation returns true if the block never causes suffocation damage to entities inside it. + PreventsSuffocation() bool +} + // RedstoneWireStepDowner represents a block with custom behaviour for redstone wire providing power when travelling // down it. type RedstoneWireStepDowner interface { diff --git a/server/block/block_behaviour_test.go b/server/block/block_behaviour_test.go new file mode 100644 index 0000000000..e4be048d76 --- /dev/null +++ b/server/block/block_behaviour_test.go @@ -0,0 +1,37 @@ +package block_test + +import ( + "context" + "testing" + + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/entity" + "github.com/df-mc/dragonfly/server/world" +) + +// TestTorchBreaksWithoutSupport verifies that a torch is broken by a neighbour +// update on the tick after its supporting block is removed, using a +// synchronous World to make the tick deterministic. +func TestTorchBreaksWithoutSupport(t *testing.T) { + w := world.Config{Synchronous: true, Entities: entity.DefaultRegistry}.New() + defer w.Close() + + support, torch := cube.Pos{0, 0, 0}, cube.Pos{0, 1, 0} + w.Do(func(tx *world.Tx) { + tx.SetBlock(support, block.Stone{}, nil) + tx.SetBlock(torch, block.Torch{Facing: cube.FaceDown}, nil) + tx.SetBlock(support, block.Air{}, nil) + }) + w.AdvanceTick() + + b, err := world.Call(context.Background(), w, func(tx *world.Tx) (world.Block, error) { + return tx.Block(torch), nil + }) + if err != nil { + t.Fatalf("read torch block: %v", err) + } + if b != (block.Air{}) { + t.Errorf("expected torch to break after removing its support, got %v", b) + } +} diff --git a/server/block/break_info.go b/server/block/break_info.go index cc6004a1ae..a6745d1ba8 100644 --- a/server/block/break_info.go +++ b/server/block/break_info.go @@ -22,59 +22,100 @@ type Breakable interface { BreakInfo() BreakInfo } -// BreakDuration returns the base duration that breaking the block passed takes when being broken using the -// item passed. -func BreakDuration(b world.Block, i item.Stack) time.Duration { +// BreakContext carries the environmental and status-effect state that influences how quickly a block is +// broken. The zero value represents a player standing on the ground, out of water, without any relevant +// status effects or enchantments. +type BreakContext struct { + // HasteLevel is the level of the Haste effect (0 if absent). Level 1 corresponds to Haste I. + HasteLevel int + // ConduitPowerLevel is the level of the Conduit Power effect (0 if absent). It grants a mining speed + // boost equivalent to Haste; the two do not stack (the higher of the two is used). + ConduitPowerLevel int + // MiningFatigueLevel is the level of the Mining Fatigue effect (0 if absent). + MiningFatigueLevel int + // Underwater is true if the player's head is submerged in water, which slows mining by 5x unless + // negated by AquaAffinity. + Underwater bool + // AquaAffinity is true if the player wears a helmet enchanted with Aqua Affinity, negating the + // underwater mining penalty. + AquaAffinity bool + // Flying is true if the player is flying, which suppresses the airborne mining speed penalty. + Flying bool + // Airborne is true if the player is not on the ground, which slows mining by 5x. + Airborne bool +} + +// BreakDuration returns the duration that breaking the block passed takes when being broken using the item +// passed, accounting for the status effects and environment described by ctx. +// See https://minecraft.wiki/w/Breaking#Calculation. +func BreakDuration(b world.Block, i item.Stack, ctx BreakContext) time.Duration { breakable, ok := b.(Breakable) if !ok { return math.MaxInt64 } + info := breakable.BreakInfo() + if info.Hardness <= 0 { + return 0 + } t, ok := i.Item().(item.Tool) if !ok { t = item.ToolNone{} } - info := breakable.BreakInfo() - breakTime := info.Hardness * 5 - if info.Harvestable(t) { - breakTime = info.Hardness * 1.5 - } + canHarvest := info.Harvestable(t) + speed := 1.0 if info.Effective(t) { - eff := t.BaseMiningEfficiency(b) - if e, ok := i.Enchantment(enchantment.Efficiency); ok { - eff += enchantment.Efficiency.Addend(e.Level()) + speed = t.BaseMiningEfficiency(b) + if !canHarvest { + // A tool of the correct type but wrong tier (e.g. a wooden pickaxe on diamond ore) grants no + // speed bonus in Bedrock Edition. + speed = 1 + } else if e, ok := i.Enchantment(enchantment.Efficiency); ok { + speed += enchantment.Efficiency.Addend(e.Level()) } - breakTime /= eff } - // TODO: Account for haste etc here. - timeInTicksAccurate := math.Round(breakTime/0.05) * 0.05 - return (time.Duration(math.Round(timeInTicksAccurate*20)) * time.Second) / 20 -} - -// BreaksInstantly checks if the block passed can be broken instantly using the item stack passed to break -// it. -func BreaksInstantly(b world.Block, i item.Stack) bool { - breakable, ok := b.(Breakable) - if !ok { - return false + // Haste and Conduit Power do not stack; the higher of the two is used. They boost both the mining speed + // and the final destroy progress per tick. + positive := max(ctx.HasteLevel, ctx.ConduitPowerLevel) + if positive > 0 { + speed *= 0.2*float64(positive) + 1 } - hardness := breakable.BreakInfo().Hardness - if hardness == 0 { - return true + if ctx.MiningFatigueLevel > 0 { + speed *= math.Pow(0.3, float64(ctx.MiningFatigueLevel)) } - t, ok := i.Item().(item.Tool) - if !ok || !breakable.BreakInfo().Effective(t) { - return false + if ctx.Underwater && !ctx.AquaAffinity { + speed /= 5 + } + if ctx.Airborne && !ctx.Flying { + speed /= 5 } - // TODO: Account for haste etc here. - efficiencyVal := 0.0 - if e, ok := i.Enchantment(enchantment.Efficiency); ok { - efficiencyVal += enchantment.Efficiency.Addend(e.Level()) + damage := speed / info.Hardness + if canHarvest { + damage /= 30 + } else { + damage /= 100 + } + if positive > 0 { + damage *= math.Pow(1.2, float64(positive)) } - hasteVal := 0.0 - return (t.BaseMiningEfficiency(b)+efficiencyVal)*hasteVal >= hardness*30 + if ctx.MiningFatigueLevel > 0 { + damage *= math.Pow(0.7, float64(ctx.MiningFatigueLevel)) + } + if damage >= 1 { + // The block breaks within a single tick. + return 0 + } + return time.Duration(math.Ceil(1/damage)) * time.Second / 20 +} + +// BreaksInstantly checks if the block passed breaks instantly, that is, it has zero hardness and so is +// mined in a single tick with any item and without consuming tool durability. This is distinct from a +// block that only breaks within one tick because of a high-speed tool or status effects. +func BreaksInstantly(b world.Block) bool { + breakable, ok := b.(Breakable) + return ok && breakable.BreakInfo().Hardness <= 0 } // BreakInfo is a struct returned by every block. It holds information on block breaking related data, such as @@ -100,11 +141,11 @@ type BreakInfo struct { } // newBreakInfo creates a BreakInfo struct with the properties passed. The XPDrops field is 0 by default. The blast -// resistance is set to the block's hardness*5 by default. +// resistance is set to the block's hardness by default. func newBreakInfo(hardness float64, harvestable func(item.Tool) bool, effective func(item.Tool) bool, drops func(item.Tool, []item.Enchantment) []item.Stack) BreakInfo { return BreakInfo{ Hardness: hardness, - BlastResistance: hardness * 5, + BlastResistance: hardness, Harvestable: harvestable, Effective: effective, Drops: drops, diff --git a/server/block/break_info_test.go b/server/block/break_info_test.go new file mode 100644 index 0000000000..86f51643ba --- /dev/null +++ b/server/block/break_info_test.go @@ -0,0 +1,134 @@ +package block_test + +import ( + "testing" + + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/item/enchantment" + "github.com/df-mc/dragonfly/server/world" +) + +// TestBreakDuration verifies the Bedrock Edition breaking calculation against the reference values +// documented at https://minecraft.wiki/w/Breaking#Calculation. +func TestBreakDuration(t *testing.T) { + diamondPick := item.NewStack(item.Pickaxe{Tier: item.ToolTierDiamond}, 1) + efficiencyDiamondPick := diamondPick.WithEnchantments(item.NewEnchantment(enchantment.Efficiency, 3)) + woodPick := item.NewStack(item.Pickaxe{Tier: item.ToolTierWood}, 1) + + tests := []struct { + name string + block world.Block + stack item.Stack + ctx block.BreakContext + wantTicks int64 + }{ + { + name: "efficiency adds to best harvestable tool speed", + block: block.Stone{}, + stack: efficiencyDiamondPick, + wantTicks: 3, + }, + { + name: "haste applies speed and damage multipliers", + block: block.Stone{}, + stack: diamondPick, + ctx: block.BreakContext{HasteLevel: 1}, + wantTicks: 4, + }, + { + name: "grounded best tool", + block: block.Stone{}, + stack: diamondPick, + wantTicks: 6, + }, + { + name: "aqua affinity removes water penalty", + block: block.Stone{}, + stack: diamondPick, + ctx: block.BreakContext{Underwater: true, AquaAffinity: true}, + wantTicks: 6, + }, + { + name: "mining fatigue applies speed and damage multipliers", + block: block.Stone{}, + stack: diamondPick, + ctx: block.BreakContext{MiningFatigueLevel: 1}, + wantTicks: 27, + }, + { + name: "airborne penalty before rounding", + block: block.Stone{}, + stack: diamondPick, + ctx: block.BreakContext{Airborne: true}, + wantTicks: 29, + }, + { + name: "efficiency instant-mines a soft block on land", + block: block.Netherrack{}, + stack: efficiencyDiamondPick, + wantTicks: 0, + }, + { + name: "airborne prevents instant-mining the same soft block", + block: block.Netherrack{}, + stack: efficiencyDiamondPick, + ctx: block.BreakContext{Airborne: true}, + wantTicks: 4, + }, + { + name: "water without aqua affinity slows mining", + block: block.Stone{}, + stack: diamondPick, + ctx: block.BreakContext{Underwater: true}, + wantTicks: 29, + }, + { + name: "wrong tier tool cannot harvest", + block: block.DiamondOre{Type: block.StoneOre()}, + stack: woodPick, + wantTicks: 300, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := block.BreakDuration(tt.block, tt.stack, tt.ctx).Milliseconds() / 50; got != tt.wantTicks { + t.Errorf("got %d ticks, want %d", got, tt.wantTicks) + } + }) + } +} + +// TestBreaksInstantly verifies that BreaksInstantly reports an instant break only for zero-hardness blocks, +// not for positive-hardness blocks that merely break within one tick due to a fast tool. +func TestBreaksInstantly(t *testing.T) { + tests := []struct { + name string + block world.Block + want bool + }{ + { + name: "zero-hardness block breaks instantly", + block: block.ShortGrass{}, + want: true, + }, + { + name: "positive-hardness block does not break instantly", + block: block.Netherrack{}, + want: false, + }, + { + name: "stone does not break instantly", + block: block.Stone{}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := block.BreaksInstantly(tt.block); got != tt.want { + t.Errorf("got %v, want %v", got, tt.want) + } + }) + } +} diff --git a/server/block/bricks.go b/server/block/bricks.go index 3266513d74..536f03eb38 100644 --- a/server/block/bricks.go +++ b/server/block/bricks.go @@ -8,7 +8,7 @@ type Bricks struct { // BreakInfo ... func (b Bricks) BreakInfo() BreakInfo { - return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(b)).withBlastResistance(30) + return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(b)).withBlastResistance(6) } // EncodeItem ... diff --git a/server/block/cake.go b/server/block/cake.go index 2122c94b17..3f3ab130a0 100644 --- a/server/block/cake.go +++ b/server/block/cake.go @@ -4,6 +4,7 @@ import ( "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/block/model" "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/item/enchantment" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/sound" "github.com/go-gl/mathgl/mgl64" @@ -16,6 +17,20 @@ type Cake struct { // Bites is the amount of bites taken out of the cake. Bites int + // Candle is true if the cake has a candle on top. + Candle bool + // CandleColour is the colour of the candle. + CandleColour item.OptionalColour + // CandleLit is whether the candle is lit. + CandleLit bool +} + +// LightEmissionLevel ... +func (c Cake) LightEmissionLevel() uint8 { + if c.Candle && c.CandleLit { + return 3 + } + return 0 } // SideClosed ... @@ -42,16 +57,59 @@ func (c Cake) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.T func (c Cake) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { if _, air := tx.Block(pos.Side(cube.FaceDown)).(Air); air { breakBlock(c, pos, tx) + return + } + liquid, _ := tx.Liquid(pos) + if _, ok := liquid.(Water); ok && c.Candle && c.CandleLit { + c.CandleLit = false + tx.SetBlock(pos, c, nil) + tx.PlaySound(pos.Vec3Centre(), sound.FireExtinguish{}) } } // Activate ... -func (c Cake) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, _ *item.UseContext) bool { +func (c Cake) Activate(pos cube.Pos, face cube.Face, tx *world.Tx, u item.User, ctx *item.UseContext) bool { + held, _ := u.HeldItems() + if c.Bites == 0 && !c.Candle { + if candle, ok := held.Item().(Candle); ok { + c.Candle = true + c.CandleColour = candle.Colour + tx.SetBlock(pos, c, nil) + tx.PlaySound(pos.Vec3Centre(), sound.ItemUseOn{Block: c}) + ctx.SubtractFromCount(1) + return true + } + } + + if _, ok := held.Enchantment(enchantment.FireAspect); ok { + c.Ignite(pos, tx, nil) + ctx.DamageItem(1) + return true + } + + if _, ok := held.Item().(item.FlintAndSteel); ok { + return false + } + + if c.Candle && c.CandleLit && face == cube.FaceUp && held.Empty() { + c.CandleLit = false + tx.SetBlock(pos, c, nil) + return true + } + if i, ok := u.(interface { Saturate(food int, saturation float64) }); ok { + if c.Candle { + dropItem(tx, item.NewStack(Candle{Colour: c.CandleColour}, 1), pos.Vec3Centre()) + + c.Candle, c.CandleLit = false, false + c.CandleColour = item.OptionalColour(0) + } + i.Saturate(2, 0.4) tx.PlaySound(u.Position().Add(mgl64.Vec3{0, 1.5}), sound.Burp{}) + c.Bites++ if c.Bites > 6 { tx.SetBlock(pos, nil, nil) @@ -63,18 +121,51 @@ func (c Cake) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, _ * return false } +// Ignite ... +func (c Cake) Ignite(pos cube.Pos, tx *world.Tx, _ world.Entity) bool { + if !c.Candle || c.CandleLit { + return false + } + if _, ok := tx.Liquid(pos); ok { + return false + } + + c.CandleLit = true + tx.SetBlock(pos, c, nil) + tx.PlaySound(pos.Vec3(), sound.Ignite{}) + return true +} + +// EntityInside ... +func (c Cake) EntityInside(pos cube.Pos, tx *world.Tx, e world.Entity) { + if flammable, ok := e.(flammableEntity); ok { + if flammable.OnFireDuration() > 0 { + c.Ignite(pos, tx, e) + } + } +} + // BreakInfo ... func (c Cake) BreakInfo() BreakInfo { + if c.Candle { + return newBreakInfo(0.5, alwaysHarvestable, nothingEffective, oneOf(Candle{Colour: c.CandleColour})) + } return newBreakInfo(0.5, neverHarvestable, nothingEffective, simpleDrops()) } // EncodeItem ... func (c Cake) EncodeItem() (name string, meta int16) { + if c.Candle { + return "minecraft:" + c.CandleColour.Prepend("candle_cake"), 0 + } return "minecraft:cake", 0 } // EncodeBlock ... func (c Cake) EncodeBlock() (name string, properties map[string]any) { + if c.Candle { + return "minecraft:" + c.CandleColour.Prepend("candle_cake"), map[string]any{"lit": c.CandleLit} + } return "minecraft:cake", map[string]any{"bite_counter": int32(c.Bites)} } @@ -88,5 +179,9 @@ func allCake() (cake []world.Block) { for bites := 0; bites < 7; bites++ { cake = append(cake, Cake{Bites: bites}) } + for _, c := range item.OptionalColours() { + cake = append(cake, Cake{CandleColour: c, Candle: true}) + cake = append(cake, Cake{CandleColour: c, Candle: true, CandleLit: true}) + } return } diff --git a/server/block/campfire.go b/server/block/campfire.go index 88a143925b..3c6e7c3f7f 100644 --- a/server/block/campfire.go +++ b/server/block/campfire.go @@ -243,8 +243,8 @@ func (c Campfire) EncodeNBT() map[string]any { for i, v := range c.Items { id := strconv.Itoa(i + 1) if !v.Item.Empty() { - m["Item"+id] = nbtconv.WriteItem(v.Item, true) - m["ItemTime"+id] = uint8(v.Time.Milliseconds() / 50) + m["Item"+id] = item.WriteNBT(v.Item, true) + m["ItemTime"+id] = int32(v.Time.Milliseconds() / 50) } } return m @@ -255,8 +255,8 @@ func (c Campfire) DecodeNBT(data map[string]any) any { for i := 0; i < 4; i++ { id := strconv.Itoa(i + 1) c.Items[i] = CampfireItem{ - Item: nbtconv.MapItem(data, "Item"+id), - Time: time.Duration(nbtconv.Int16(data, "ItemTime"+id)) * time.Millisecond * 50, + Item: item.MapNBT(data, "Item"+id), + Time: time.Duration(nbtconv.Int32(data, "ItemTime"+id)) * time.Millisecond * 50, } } return c diff --git a/server/block/cinnabar.go b/server/block/cinnabar.go new file mode 100644 index 0000000000..cd2b8224dc --- /dev/null +++ b/server/block/cinnabar.go @@ -0,0 +1,31 @@ +package block + +// Cinnabar is a decorative rock that generates throughout sulfur caves and as part of sulfur springs. +type Cinnabar struct { + solid + bassDrum + + // Chiseled specifies if the cinnabar is chiseled. + Chiseled bool +} + +// BreakInfo ... +func (c Cinnabar) BreakInfo() BreakInfo { + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(c)).withBlastResistance(6) +} + +// EncodeItem ... +func (c Cinnabar) EncodeItem() (name string, meta int16) { + if c.Chiseled { + return "minecraft:chiseled_cinnabar", 0 + } + return "minecraft:cinnabar", 0 +} + +// EncodeBlock ... +func (c Cinnabar) EncodeBlock() (string, map[string]any) { + if c.Chiseled { + return "minecraft:chiseled_cinnabar", nil + } + return "minecraft:cinnabar", nil +} diff --git a/server/block/cinnabar_bricks.go b/server/block/cinnabar_bricks.go new file mode 100644 index 0000000000..06cd7ff554 --- /dev/null +++ b/server/block/cinnabar_bricks.go @@ -0,0 +1,22 @@ +package block + +// CinnabarBricks is a decorative variant of Cinnabar. +type CinnabarBricks struct { + solid + bassDrum +} + +// BreakInfo ... +func (c CinnabarBricks) BreakInfo() BreakInfo { + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(c)).withBlastResistance(6) +} + +// EncodeItem ... +func (CinnabarBricks) EncodeItem() (name string, meta int16) { + return "minecraft:cinnabar_bricks", 0 +} + +// EncodeBlock ... +func (CinnabarBricks) EncodeBlock() (string, map[string]any) { + return "minecraft:cinnabar_bricks", nil +} diff --git a/server/block/coal.go b/server/block/coal.go index 870e0a178a..aa8c432c89 100644 --- a/server/block/coal.go +++ b/server/block/coal.go @@ -13,7 +13,7 @@ type Coal struct { // BreakInfo ... func (c Coal) BreakInfo() BreakInfo { - return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(c)).withBlastResistance(30) + return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(c)).withBlastResistance(6) } // FlammabilityInfo ... diff --git a/server/block/coal_ore.go b/server/block/coal_ore.go index 13d3d6273f..7e53b39d11 100644 --- a/server/block/coal_ore.go +++ b/server/block/coal_ore.go @@ -13,7 +13,7 @@ type CoalOre struct { // BreakInfo ... func (c CoalOre) BreakInfo() BreakInfo { - return newBreakInfo(c.Type.Hardness(), pickaxeHarvestable, pickaxeEffective, oreDrops(item.Coal{}, c)).withXPDropRange(0, 2).withBlastResistance(15) + return newBreakInfo(c.Type.Hardness(), pickaxeHarvestable, pickaxeEffective, oreDrops(item.Coal{}, c)).withXPDropRange(0, 2).withBlastResistance(3) } // SmeltInfo ... diff --git a/server/block/cobblestone.go b/server/block/cobblestone.go index 3abff59b5c..bfe848dce7 100644 --- a/server/block/cobblestone.go +++ b/server/block/cobblestone.go @@ -14,7 +14,7 @@ type Cobblestone struct { // BreakInfo ... func (c Cobblestone) BreakInfo() BreakInfo { - return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(c)).withBlastResistance(30) + return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(c)).withBlastResistance(6) } // SmeltInfo ... diff --git a/server/block/cobweb.go b/server/block/cobweb.go index b18ac0df86..94f8df4a69 100644 --- a/server/block/cobweb.go +++ b/server/block/cobweb.go @@ -47,7 +47,7 @@ func (c Cobweb) BreakInfo() BreakInfo { } return nil }, - ).withBlastResistance(4) + ) } // HasLiquidDrops ... diff --git a/server/block/cocoa_bean.go b/server/block/cocoa_bean.go index bf19b07166..b2ad28e65d 100644 --- a/server/block/cocoa_bean.go +++ b/server/block/cocoa_bean.go @@ -93,7 +93,7 @@ func (c CocoaBean) BreakInfo() BreakInfo { return []item.Stack{item.NewStack(c, rand.IntN(2)+2)} } return []item.Stack{item.NewStack(c, 1)} - }).withBlastResistance(15) + }).withBlastResistance(3) } // CompostChance ... diff --git a/server/block/composter.go b/server/block/composter.go index e6c020061c..7b3072e839 100644 --- a/server/block/composter.go +++ b/server/block/composter.go @@ -82,7 +82,7 @@ func (c Composter) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { // BreakInfo ... func (c Composter) BreakInfo() BreakInfo { - return newBreakInfo(0.6, alwaysHarvestable, axeEffective, oneOf(c)).withBreakHandler(func(pos cube.Pos, tx *world.Tx, u item.User) { + return newBreakInfo(0.6, alwaysHarvestable, axeEffective, oneOf(Composter{})).withBreakHandler(func(pos cube.Pos, tx *world.Tx, u item.User) { if c.Level == 8 { dropItem(tx, item.NewStack(item.BoneMeal{}, 1), pos.Side(cube.FaceUp).Vec3Middle()) } @@ -97,6 +97,8 @@ func (c Composter) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User tx.SetBlock(pos, c, nil) dropItem(tx, item.NewStack(item.BoneMeal{}, 1), pos.Side(cube.FaceUp).Vec3Middle()) tx.PlaySound(pos.Vec3(), sound.ComposterEmpty{}) + // The bone meal was collected, so the item held must not also be used on the composter. + return true } return false } diff --git a/server/block/copper.go b/server/block/copper.go index fd814b2ae4..eecd8461ae 100644 --- a/server/block/copper.go +++ b/server/block/copper.go @@ -38,7 +38,7 @@ func (c Copper) Strip() (world.Block, world.Sound, bool) { func (c Copper) BreakInfo() BreakInfo { return newBreakInfo(3, func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierStone.HarvestLevel - }, pickaxeEffective, oneOf(c)).withBlastResistance(30) + }, pickaxeEffective, oneOf(c)).withBlastResistance(6) } // Wax waxes the copper block to stop it from oxidising further. diff --git a/server/block/copper_bars.go b/server/block/copper_bars.go index 87539cf42e..c66fea39e3 100644 --- a/server/block/copper_bars.go +++ b/server/block/copper_bars.go @@ -23,7 +23,7 @@ type CopperBars struct { // BreakInfo ... func (c CopperBars) BreakInfo() BreakInfo { - return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(c)).withBlastResistance(30) + return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(c)).withBlastResistance(6) } // SideClosed ... diff --git a/server/block/copper_chain.go b/server/block/copper_chain.go index 5f553ce457..c97b8d4261 100644 --- a/server/block/copper_chain.go +++ b/server/block/copper_chain.go @@ -43,7 +43,7 @@ func (c CopperChain) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx * // BreakInfo ... func (c CopperChain) BreakInfo() BreakInfo { - return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(c)).withBlastResistance(30) + return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(c)).withBlastResistance(6) } // Wax waxes the copper chain to stop it from oxidising further. diff --git a/server/block/copper_door.go b/server/block/copper_door.go index c73f3f3126..c46b05a897 100644 --- a/server/block/copper_door.go +++ b/server/block/copper_door.go @@ -158,7 +158,7 @@ func (d CopperDoor) RandomTick(pos cube.Pos, tx *world.Tx, r *rand.Rand) { func (d CopperDoor) BreakInfo() BreakInfo { return newBreakInfo(3, func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierStone.HarvestLevel - }, pickaxeEffective, oneOf(d)) + }, pickaxeEffective, oneOf(d)).withBlastResistance(6) } // SideClosed ... diff --git a/server/block/copper_golem_statue.go b/server/block/copper_golem_statue.go index 7198a9b34c..a6bf742e26 100644 --- a/server/block/copper_golem_statue.go +++ b/server/block/copper_golem_statue.go @@ -30,7 +30,7 @@ type CopperGolemStatue struct { // BreakInfo ... func (c CopperGolemStatue) BreakInfo() BreakInfo { - return newBreakInfo(3, alwaysHarvestable, pickaxeEffective, oneOf(c)).withBlastResistance(30) + return newBreakInfo(3, alwaysHarvestable, pickaxeEffective, oneOf(c)).withBlastResistance(6) } // Activate ... diff --git a/server/block/copper_grate.go b/server/block/copper_grate.go index cfdaea578a..5a05a44d41 100644 --- a/server/block/copper_grate.go +++ b/server/block/copper_grate.go @@ -27,7 +27,7 @@ type CopperGrate struct { func (c CopperGrate) BreakInfo() BreakInfo { return newBreakInfo(3, func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierStone.HarvestLevel - }, pickaxeEffective, oneOf(c)).withBlastResistance(30) + }, pickaxeEffective, oneOf(c)).withBlastResistance(6) } // Wax waxes the copper grate to stop it from oxidising further. diff --git a/server/block/copper_ore.go b/server/block/copper_ore.go index e851ad19d3..c2449791e4 100644 --- a/server/block/copper_ore.go +++ b/server/block/copper_ore.go @@ -17,7 +17,7 @@ type CopperOre struct { func (c CopperOre) BreakInfo() BreakInfo { return newBreakInfo(c.Type.Hardness(), func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierStone.HarvestLevel - }, pickaxeEffective, multiOreDrops(item.RawCopper{}, c, 2, 5)).withBlastResistance(15) + }, pickaxeEffective, multiOreDrops(item.RawCopper{}, c, 2, 5)).withBlastResistance(3) } // SmeltInfo ... diff --git a/server/block/copper_trapdoor.go b/server/block/copper_trapdoor.go index 919e1a0916..9a2316f601 100644 --- a/server/block/copper_trapdoor.go +++ b/server/block/copper_trapdoor.go @@ -101,7 +101,7 @@ func (t CopperTrapdoor) RandomTick(pos cube.Pos, tx *world.Tx, r *rand.Rand) { func (t CopperTrapdoor) BreakInfo() BreakInfo { return newBreakInfo(3, func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierStone.HarvestLevel - }, pickaxeEffective, oneOf(t)) + }, pickaxeEffective, oneOf(t)).withBlastResistance(6) } // SideClosed ... diff --git a/server/block/coral_block.go b/server/block/coral_block.go index 5de221afc6..aa21052af0 100644 --- a/server/block/coral_block.go +++ b/server/block/coral_block.go @@ -44,7 +44,7 @@ func (c CoralBlock) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { // BreakInfo ... func (c CoralBlock) BreakInfo() BreakInfo { - return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, silkTouchOneOf(CoralBlock{Type: c.Type, Dead: true}, c)).withBlastResistance(30) + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, silkTouchOneOf(CoralBlock{Type: c.Type, Dead: true}, c)).withBlastResistance(6) } // EncodeBlock ... diff --git a/server/block/cube/axis.go b/server/block/cube/axis.go index dd94d46ded..fa39ca40d8 100644 --- a/server/block/cube/axis.go +++ b/server/block/cube/axis.go @@ -44,6 +44,20 @@ func (a Axis) RotateRight() Axis { return a.RotateLeft() } +// Faces returns the negative and positive Face along the Axis. For X it +// returns FaceWest, FaceEast; for Y, FaceDown, FaceUp; for Z, FaceNorth, +// FaceSouth. +func (a Axis) Faces() (negative, positive Face) { + switch a { + case X: + return FaceWest, FaceEast + case Y: + return FaceDown, FaceUp + default: + return FaceNorth, FaceSouth + } +} + // Vec3 returns a unit Vec3 of either (1, 0, 0), (0, 1, 0) or (0, 0, 1), // depending on the Axis. func (a Axis) Vec3() mgl64.Vec3 { diff --git a/server/block/cube/bbox.go b/server/block/cube/bbox.go index b0a8131afd..76ca9e287b 100644 --- a/server/block/cube/bbox.go +++ b/server/block/cube/bbox.go @@ -1,19 +1,48 @@ package cube import ( + "github.com/go-gl/mathgl/mgl32" "github.com/go-gl/mathgl/mgl64" ) +// number represents a floating-point number supported by a bounding box. +type number interface { + ~float32 | ~float64 +} + +// vec3 represents a three-dimensional vector of floating-point numbers. +type vec3[T number] interface { + ~[3]T +} + +// boundingBox represents an axis-aligned bounding box backed by a vector type. +type boundingBox[T number, V vec3[T]] struct { + min, max V +} + // BBox represents an Axis Aligned Bounding Box in a 3D space. It is defined as // two Vec3s, of which one is the minimum and one is the maximum. -type BBox struct { - min, max mgl64.Vec3 -} +type BBox = boundingBox[float64, mgl64.Vec3] + +// BBox32 represents a float32 Axis Aligned Bounding Box in a 3D space. It is +// defined as two Vec3s, of which one is the minimum and one is the maximum. +type BBox32 = boundingBox[float32, mgl32.Vec3] // Box creates a new axis aligned bounding box with the minimum and maximum // coordinates provided. The returned box has minimum and maximum coordinates // swapped if necessary so that it is well-formed. func Box(x0, y0, z0, x1, y1, z1 float64) BBox { + return newBox[float64, mgl64.Vec3](x0, y0, z0, x1, y1, z1) +} + +// Box32 creates a new float32 axis aligned bounding box with the minimum and +// maximum coordinates provided. The returned box has minimum and maximum +// coordinates swapped if necessary so that it is well-formed. +func Box32(x0, y0, z0, x1, y1, z1 float32) BBox32 { + return newBox[float32, mgl32.Vec3](x0, y0, z0, x1, y1, z1) +} + +func newBox[T number, V vec3[T]](x0, y0, z0, x1, y1, z1 T) boundingBox[T, V] { if x0 > x1 { x0, x1 = x1, x0 } @@ -23,72 +52,67 @@ func Box(x0, y0, z0, x1, y1, z1 float64) BBox { if z0 > z1 { z0, z1 = z1, z0 } - return BBox{min: mgl64.Vec3{x0, y0, z0}, max: mgl64.Vec3{x1, y1, z1}} + return boundingBox[T, V]{min: V{x0, y0, z0}, max: V{x1, y1, z1}} } // Grow grows the bounding box in all directions by x and returns the new // bounding box. -func (box BBox) Grow(x float64) BBox { - add := mgl64.Vec3{x, x, x} - return BBox{min: box.min.Sub(add), max: box.max.Add(add)} +func (box boundingBox[T, V]) Grow(x T) boundingBox[T, V] { + return box.GrowVec3(V{x, x, x}) } // GrowVec3 grows the BBox on all axes as represented by the Vec3 passed. The // vec values are subtracted from the minimum values of the BBox and added to // the maximum values of the BBox. -func (box BBox) GrowVec3(vec mgl64.Vec3) BBox { - return BBox{min: box.min.Sub(vec), max: box.max.Add(vec)} +func (box boundingBox[T, V]) GrowVec3(vec V) boundingBox[T, V] { + for i := range 3 { + box.min[i] -= vec[i] + box.max[i] += vec[i] + } + return box } // Min returns the minimum coordinate of the bounding box. -func (box BBox) Min() mgl64.Vec3 { +func (box boundingBox[T, V]) Min() V { return box.min } // Max returns the maximum coordinate of the bounding box. -func (box BBox) Max() mgl64.Vec3 { +func (box boundingBox[T, V]) Max() V { return box.max } // Width returns the width of the BBox. -func (box BBox) Width() float64 { +func (box boundingBox[T, V]) Width() T { return box.max[0] - box.min[0] } // Length returns the length of the BBox. -func (box BBox) Length() float64 { +func (box boundingBox[T, V]) Length() T { return box.max[2] - box.min[2] } // Height returns the height of the BBox. -func (box BBox) Height() float64 { +func (box boundingBox[T, V]) Height() T { return box.max[1] - box.min[1] } // Extend expands the BBox on all axes as represented by the Vec3 passed. // Negative coordinates result in an expansion towards the negative axis, and // vice versa for positive coordinates. -func (box BBox) Extend(vec mgl64.Vec3) BBox { - if vec[0] < 0 { - box.min[0] += vec[0] - } else if vec[0] > 0 { - box.max[0] += vec[0] - } - if vec[1] < 0 { - box.min[1] += vec[1] - } else if vec[1] > 0 { - box.max[1] += vec[1] - } - if vec[2] < 0 { - box.min[2] += vec[2] - } else if vec[2] > 0 { - box.max[2] += vec[2] +func (box boundingBox[T, V]) Extend(vec V) boundingBox[T, V] { + for i := range 3 { + if vec[i] < 0 { + box.min[i] += vec[i] + } else if vec[i] > 0 { + box.max[i] += vec[i] + } } return box } // ExtendTowards extends the bounding box by x in a given direction. -func (box BBox) ExtendTowards(f Face, x float64) BBox { +func (box boundingBox[T, V]) ExtendTowards(f Face, x T) boundingBox[T, V] { switch f { case FaceDown: box.min[1] -= x @@ -107,7 +131,7 @@ func (box BBox) ExtendTowards(f Face, x float64) BBox { } // Stretch stretches the bounding box by x in a given axis. -func (box BBox) Stretch(a Axis, x float64) BBox { +func (box boundingBox[T, V]) Stretch(a Axis, x T) boundingBox[T, V] { switch a { case Y: box.min[1] -= x @@ -124,37 +148,44 @@ func (box BBox) Stretch(a Axis, x float64) BBox { // Translate moves the entire BBox with the Vec3 given. The (minimum and // maximum) x, y and z coordinates are moved by those in the Vec3 passed. -func (box BBox) Translate(vec mgl64.Vec3) BBox { - return BBox{min: box.min.Add(vec), max: box.max.Add(vec)} +func (box boundingBox[T, V]) Translate(vec V) boundingBox[T, V] { + for i := range 3 { + box.min[i] += vec[i] + box.max[i] += vec[i] + } + return box } // TranslateTowards moves the entire BBox by x in the direction of a Face f. -func (box BBox) TranslateTowards(f Face, x float64) BBox { +func (box boundingBox[T, V]) TranslateTowards(f Face, x T) boundingBox[T, V] { + var vec V switch f { case FaceDown: - return box.Translate(mgl64.Vec3{0, -x, 0}) + vec[1] = -x case FaceUp: - return box.Translate(mgl64.Vec3{0, x, 0}) + vec[1] = x case FaceNorth: - return box.Translate(mgl64.Vec3{0, 0, -x}) + vec[2] = -x case FaceSouth: - return box.Translate(mgl64.Vec3{0, 0, x}) + vec[2] = x case FaceWest: - return box.Translate(mgl64.Vec3{-x, 0, 0}) + vec[0] = -x case FaceEast: - return box.Translate(mgl64.Vec3{x, 0, 0}) + vec[0] = x + default: + return box } - return box + return box.Translate(vec) } // IntersectsWith checks if the BBox intersects with another BBox. -func (box BBox) IntersectsWith(other BBox) bool { +func (box boundingBox[T, V]) IntersectsWith(other boundingBox[T, V]) bool { return box.intersectsWith(other, 1e-5) } // intersectsWith checks if the BBox intersects with another BBox using a // specific epsilon. -func (box BBox) intersectsWith(other BBox, epsilon float64) bool { +func (box boundingBox[T, V]) intersectsWith(other boundingBox[T, V], epsilon T) bool { if other.max[0]-box.min[0] > epsilon && box.max[0]-other.min[0] > epsilon { if other.max[1]-box.min[1] > epsilon && box.max[1]-other.min[1] > epsilon { return other.max[2]-box.min[2] > epsilon && box.max[2]-other.min[2] > epsilon @@ -165,6 +196,15 @@ func (box BBox) intersectsWith(other BBox, epsilon float64) bool { // AnyIntersections checks if any of boxes intersect with search. func AnyIntersections(boxes []BBox, search BBox) bool { + return anyIntersections(boxes, search) +} + +// AnyIntersections32 checks if any of the float32 boxes intersect with search. +func AnyIntersections32(boxes []BBox32, search BBox32) bool { + return anyIntersections(boxes, search) +} + +func anyIntersections[T number, V vec3[T]](boxes []boundingBox[T, V], search boundingBox[T, V]) bool { for _, box := range boxes { if box.intersectsWith(search, 0) { return true @@ -174,7 +214,7 @@ func AnyIntersections(boxes []BBox, search BBox) bool { } // Vec3Within checks if a BBox has vec within it. -func (box BBox) Vec3Within(vec mgl64.Vec3) bool { +func (box boundingBox[T, V]) Vec3Within(vec V) bool { if vec[0] <= box.min[0] || vec[0] >= box.max[0] { return false } @@ -185,7 +225,7 @@ func (box BBox) Vec3Within(vec mgl64.Vec3) bool { } // Vec3WithinYZ checks if a BBox has vec within its Y and Z bounds. -func (box BBox) Vec3WithinYZ(vec mgl64.Vec3) bool { +func (box boundingBox[T, V]) Vec3WithinYZ(vec V) bool { if vec[2] < box.min[2] || vec[2] > box.max[2] { return false } @@ -193,7 +233,7 @@ func (box BBox) Vec3WithinYZ(vec mgl64.Vec3) bool { } // Vec3WithinXZ checks if a BBox has vec within its X and Z bounds. -func (box BBox) Vec3WithinXZ(vec mgl64.Vec3) bool { +func (box boundingBox[T, V]) Vec3WithinXZ(vec V) bool { if vec[0] < box.min[0] || vec[0] > box.max[0] { return false } @@ -201,7 +241,7 @@ func (box BBox) Vec3WithinXZ(vec mgl64.Vec3) bool { } // Vec3WithinXY checks if a BBox has vec within its X and Y bounds. -func (box BBox) Vec3WithinXY(vec mgl64.Vec3) bool { +func (box boundingBox[T, V]) Vec3WithinXY(vec V) bool { if vec[0] < box.min[0] || vec[0] > box.max[0] { return false } @@ -211,7 +251,7 @@ func (box BBox) Vec3WithinXY(vec mgl64.Vec3) bool { // XOffset calculates the offset on the X axis between two bounding boxes, // returning a delta always smaller than or equal to deltaX if deltaX is bigger // than 0, or always bigger than or equal to deltaX if it is smaller than 0. -func (box BBox) XOffset(nearby BBox, deltaX float64) float64 { +func (box boundingBox[T, V]) XOffset(nearby boundingBox[T, V], deltaX T) T { if box.max[1] <= nearby.min[1] || box.min[1] >= nearby.max[1] || box.max[2] <= nearby.min[2] || box.min[2] >= nearby.max[2] { // Not in the same Y/Z plane. return deltaX @@ -227,7 +267,7 @@ func (box BBox) XOffset(nearby BBox, deltaX float64) float64 { // YOffset calculates the offset on the Y axis between two bounding boxes, // returning a delta always smaller than or equal to deltaY if deltaY is bigger // than 0, or always bigger than or equal to deltaY if it is smaller than 0. -func (box BBox) YOffset(nearby BBox, deltaY float64) float64 { +func (box boundingBox[T, V]) YOffset(nearby boundingBox[T, V], deltaY T) T { if box.max[0] <= nearby.min[0] || box.min[0] >= nearby.max[0] || box.max[2] <= nearby.min[2] || box.min[2] >= nearby.max[2] { // Not the same X/Z plane. return deltaY @@ -244,7 +284,7 @@ func (box BBox) YOffset(nearby BBox, deltaY float64) float64 { // ZOffset calculates the offset on the Z axis between two bounding boxes, // returning a delta always smaller than or equal to deltaZ if deltaZ is bigger // than 0, or always bigger than or equal to deltaZ if it is smaller than 0. -func (box BBox) ZOffset(nearby BBox, deltaZ float64) float64 { +func (box boundingBox[T, V]) ZOffset(nearby boundingBox[T, V], deltaZ T) T { if box.max[0] <= nearby.min[0] || box.min[0] >= nearby.max[0] || box.max[1] <= nearby.min[1] || box.min[1] >= nearby.max[1] { // Not the same X/Y plane. return deltaZ @@ -259,9 +299,9 @@ func (box BBox) ZOffset(nearby BBox, deltaZ float64) float64 { } // Corners returns the positions of all corners of a BBox. -func (box BBox) Corners() []mgl64.Vec3 { +func (box boundingBox[T, V]) Corners() []V { bbmin, bbmax := box.min, box.max - return []mgl64.Vec3{ + return []V{ box.min, box.max, {bbmin[0], bbmin[1], bbmax[2]}, @@ -274,11 +314,15 @@ func (box BBox) Corners() []mgl64.Vec3 { } // Mul performs a scalar multiplication of the min and max points of a BBox. -func (box BBox) Mul(val float64) BBox { - return BBox{min: box.min.Mul(val), max: box.max.Mul(val)} +func (box boundingBox[T, V]) Mul(val T) boundingBox[T, V] { + for i := range 3 { + box.min[i] *= val + box.max[i] *= val + } + return box } // Volume calculates the volume of a BBox. -func (box BBox) Volume() float64 { +func (box boundingBox[T, V]) Volume() T { return box.Height() * box.Length() * box.Width() } diff --git a/server/block/cube/pos.go b/server/block/cube/pos.go index 2c87d8659a..99bb72000d 100644 --- a/server/block/cube/pos.go +++ b/server/block/cube/pos.go @@ -96,21 +96,29 @@ func (p Pos) Side(face Face) Pos { // Face returns the face that the other Pos was on compared to the current Pos. // The other Pos is assumed to be a direct neighbour of the current Pos. func (p Pos) Face(other Pos) Face { - switch other { - case p.Add(Pos{0, 1}): - return FaceUp - case p.Add(Pos{0, -1}): - return FaceDown - case p.Add(Pos{0, 0, -1}): - return FaceNorth - case p.Add(Pos{0, 0, 1}): - return FaceSouth - case p.Add(Pos{-1, 0, 0}): - return FaceWest - case p.Add(Pos{1, 0, 0}): - return FaceEast + face, _ := p.NeighbourFace(other) + return face +} + +// NeighbourFace returns the face that the other Pos was on compared to the +// current Pos, if the other Pos is a direct neighbour of the current Pos. +// Example: Pos{0, 0, 0}.NeighbourFace(Pos{0, 1, 0}) returns FaceUp, true. +func (p Pos) NeighbourFace(other Pos) (Face, bool) { + switch other.Sub(p) { + case Pos{0, 1, 0}: + return FaceUp, true + case Pos{0, -1, 0}: + return FaceDown, true + case Pos{0, 0, -1}: + return FaceNorth, true + case Pos{0, 0, 1}: + return FaceSouth, true + case Pos{-1, 0, 0}: + return FaceWest, true + case Pos{1, 0, 0}: + return FaceEast, true } - return FaceUp + return FaceUp, false } // Neighbours calls the function passed for each of the block position's diff --git a/server/block/customblock/material.go b/server/block/customblock/material.go index 8b018947ae..a6164e6524 100644 --- a/server/block/customblock/material.go +++ b/server/block/customblock/material.go @@ -8,19 +8,23 @@ type Material struct { 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 + // ambientOcclusion controls how much ambient occlusion is applied when lighting. + ambientOcclusion float32 } // 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{ + m := Material{ texture: texture, renderMethod: method, faceDimming: true, - ambientOcclusion: method.AmbientOcclusion(), + ambientOcclusion: 1, } + if !method.AmbientOcclusion() { + m.ambientOcclusion = 0 + } + return m } // WithFaceDimming returns a copy of the Material with face dimming enabled. @@ -37,13 +41,13 @@ func (m Material) WithoutFaceDimming() Material { // WithAmbientOcclusion returns a copy of the Material with ambient occlusion enabled. func (m Material) WithAmbientOcclusion() Material { - m.ambientOcclusion = true + m.ambientOcclusion = 1 return m } // WithoutAmbientOcclusion returns a copy of the Material with ambient occlusion disabled. func (m Material) WithoutAmbientOcclusion() Material { - m.ambientOcclusion = false + m.ambientOcclusion = 0 return m } diff --git a/server/block/decorated_pot.go b/server/block/decorated_pot.go index adaac17230..68dbee37be 100644 --- a/server/block/decorated_pot.go +++ b/server/block/decorated_pot.go @@ -72,7 +72,7 @@ func (p DecoratedPot) ExtractItem(h Hopper, pos cube.Pos, tx *world.Tx) bool { // InsertItem ... func (p DecoratedPot) InsertItem(h Hopper, pos cube.Pos, tx *world.Tx) bool { for sourceSlot, sourceStack := range h.inventory.Slots() { - if !sourceStack.Empty() && sourceStack.Comparable(p.Item) { + if !sourceStack.Empty() && sourceStack.Comparable(p.Item) && p.Item.Count() < p.Item.MaxCount() { if p.Item.Empty() { p.Item = sourceStack.Grow(-sourceStack.Count() + 1) } else { @@ -172,17 +172,20 @@ func (p DecoratedPot) EncodeNBT() map[string]any { "sherds": sherds, } if !p.Item.Empty() { - m["item"] = nbtconv.WriteItem(p.Item, true) + m["item"] = item.WriteNBT(p.Item, true) } return m } // DecodeNBT ... func (p DecoratedPot) DecodeNBT(data map[string]any) any { - p.Item = nbtconv.MapItem(data, "item") + p.Item = item.MapNBT(data, "item") p.Decorations = [4]PotDecoration{} if sherds := nbtconv.Slice(data, "sherds"); sherds != nil { for i, name := range sherds { + if i >= len(p.Decorations) { + break + } it, ok := world.ItemByName(name.(string), 0) if !ok { panic(fmt.Errorf("unknown item %s", name)) diff --git a/server/block/deepslate.go b/server/block/deepslate.go index 70ef4ac66f..36a5450fe4 100644 --- a/server/block/deepslate.go +++ b/server/block/deepslate.go @@ -21,9 +21,9 @@ type Deepslate struct { // BreakInfo ... func (d Deepslate) BreakInfo() BreakInfo { if d.Type == NormalDeepslate() { - return newBreakInfo(3, pickaxeHarvestable, pickaxeEffective, silkTouchOneOf(Deepslate{Type: CobbledDeepslate()}, d)).withBlastResistance(30) + return newBreakInfo(3, pickaxeHarvestable, pickaxeEffective, silkTouchOneOf(Deepslate{Type: CobbledDeepslate()}, d)).withBlastResistance(6) } - return newBreakInfo(3.5, pickaxeHarvestable, pickaxeEffective, oneOf(d)).withBlastResistance(30) + return newBreakInfo(3.5, pickaxeHarvestable, pickaxeEffective, oneOf(d)).withBlastResistance(6) } // SmeltInfo ... diff --git a/server/block/deepslate_bricks.go b/server/block/deepslate_bricks.go index 08342a2173..9676811743 100644 --- a/server/block/deepslate_bricks.go +++ b/server/block/deepslate_bricks.go @@ -13,7 +13,7 @@ type DeepslateBricks struct { // BreakInfo ... func (d DeepslateBricks) BreakInfo() BreakInfo { - return newBreakInfo(3.5, pickaxeHarvestable, pickaxeEffective, oneOf(d)).withBlastResistance(30) + return newBreakInfo(3.5, pickaxeHarvestable, pickaxeEffective, oneOf(d)).withBlastResistance(6) } // SmeltInfo ... diff --git a/server/block/deepslate_tiles.go b/server/block/deepslate_tiles.go index ed271d974f..1896320ecc 100644 --- a/server/block/deepslate_tiles.go +++ b/server/block/deepslate_tiles.go @@ -13,7 +13,7 @@ type DeepslateTiles struct { // BreakInfo ... func (d DeepslateTiles) BreakInfo() BreakInfo { - return newBreakInfo(3.5, pickaxeHarvestable, pickaxeEffective, oneOf(d)).withBlastResistance(30) + return newBreakInfo(3.5, pickaxeHarvestable, pickaxeEffective, oneOf(d)).withBlastResistance(6) } // SmeltInfo ... diff --git a/server/block/diamond.go b/server/block/diamond.go index cd89c09f9f..287a1bac87 100644 --- a/server/block/diamond.go +++ b/server/block/diamond.go @@ -13,7 +13,7 @@ type Diamond struct { func (d Diamond) BreakInfo() BreakInfo { return newBreakInfo(5, func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierIron.HarvestLevel - }, pickaxeEffective, oneOf(d)).withBlastResistance(30) + }, pickaxeEffective, oneOf(d)).withBlastResistance(6) } // PowersBeacon ... diff --git a/server/block/diamond_ore.go b/server/block/diamond_ore.go index f48a46e0e6..770ce353fe 100644 --- a/server/block/diamond_ore.go +++ b/server/block/diamond_ore.go @@ -17,7 +17,7 @@ type DiamondOre struct { func (d DiamondOre) BreakInfo() BreakInfo { return newBreakInfo(d.Type.Hardness(), func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierIron.HarvestLevel - }, pickaxeEffective, oreDrops(item.Diamond{}, d)).withXPDropRange(3, 7).withBlastResistance(15) + }, pickaxeEffective, oreDrops(item.Diamond{}, d)).withXPDropRange(3, 7).withBlastResistance(3) } // SmeltInfo ... diff --git a/server/block/dirt.go b/server/block/dirt.go index c2c916c501..ed84ddfd2e 100644 --- a/server/block/dirt.go +++ b/server/block/dirt.go @@ -20,7 +20,7 @@ func (d Dirt) SoilFor(block world.Block) bool { switch block.(type) { case ShortGrass, Fern, DoubleTallGrass, DeadBush: return !d.Coarse - case Flower, DoubleFlower, NetherSprouts, PinkPetals, SugarCane, Azalea, Sapling: + case Flower, DoubleFlower, NetherSprouts, PinkPetals, SugarCane, Azalea, Sapling, BambooSapling, Bamboo: return true } return false diff --git a/server/block/dragon_egg.go b/server/block/dragon_egg.go index 71af284869..f8bf298696 100644 --- a/server/block/dragon_egg.go +++ b/server/block/dragon_egg.go @@ -62,7 +62,7 @@ func (d DragonEgg) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User // BreakInfo ... func (d DragonEgg) BreakInfo() BreakInfo { - return newBreakInfo(3, pickaxeHarvestable, pickaxeEffective, oneOf(d)).withBlastResistance(45) + return newBreakInfo(3, pickaxeHarvestable, pickaxeEffective, oneOf(d)).withBlastResistance(9) } // EncodeItem ... diff --git a/server/block/dried_kelp.go b/server/block/dried_kelp.go index b47c2b1232..e53b2a46c6 100644 --- a/server/block/dried_kelp.go +++ b/server/block/dried_kelp.go @@ -12,7 +12,7 @@ type DriedKelp struct { // BreakInfo ... func (d DriedKelp) BreakInfo() BreakInfo { - return newBreakInfo(0.5, alwaysHarvestable, hoeEffective, oneOf(d)).withBlastResistance(12.5) + return newBreakInfo(0.5, alwaysHarvestable, hoeEffective, oneOf(d)).withBlastResistance(2.5) } // FlammabilityInfo ... diff --git a/server/block/dripstone.go b/server/block/dripstone.go index b3bbcb2e79..bebb305d5d 100644 --- a/server/block/dripstone.go +++ b/server/block/dripstone.go @@ -8,7 +8,7 @@ type Dripstone struct { // BreakInfo ... func (d Dripstone) BreakInfo() BreakInfo { - return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(d)).withBlastResistance(5) + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(d)).withBlastResistance(1) } // EncodeItem ... diff --git a/server/block/emerald.go b/server/block/emerald.go index bebdcb9dbc..520062aa9e 100644 --- a/server/block/emerald.go +++ b/server/block/emerald.go @@ -19,7 +19,7 @@ func (e Emerald) Instrument() sound.Instrument { func (e Emerald) BreakInfo() BreakInfo { return newBreakInfo(5, func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierIron.HarvestLevel - }, pickaxeEffective, oneOf(e)).withBlastResistance(30) + }, pickaxeEffective, oneOf(e)).withBlastResistance(6) } // PowersBeacon ... diff --git a/server/block/emerald_ore.go b/server/block/emerald_ore.go index dea7f38a93..caf1497618 100644 --- a/server/block/emerald_ore.go +++ b/server/block/emerald_ore.go @@ -19,7 +19,7 @@ func (e EmeraldOre) BreakInfo() BreakInfo { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierIron.HarvestLevel }, pickaxeEffective, oreDrops(item.Emerald{}, e)).withXPDropRange(3, 7) if e.Type == DeepslateOre() { - i = i.withBlastResistance(15) + i = i.withBlastResistance(3) } return i } diff --git a/server/block/enchanting_table.go b/server/block/enchanting_table.go index dbba12fc46..47ca65095e 100644 --- a/server/block/enchanting_table.go +++ b/server/block/enchanting_table.go @@ -22,7 +22,7 @@ func (e EnchantingTable) Model() world.BlockModel { // BreakInfo ... func (e EnchantingTable) BreakInfo() BreakInfo { - return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(e)).withBlastResistance(6000) + return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(e)).withBlastResistance(1200) } // SideClosed ... diff --git a/server/block/end_bricks.go b/server/block/end_bricks.go index c5b8474950..ec70a6d37d 100644 --- a/server/block/end_bricks.go +++ b/server/block/end_bricks.go @@ -8,7 +8,7 @@ type EndBricks struct { // BreakInfo ... func (e EndBricks) BreakInfo() BreakInfo { - return newBreakInfo(3, pickaxeHarvestable, pickaxeEffective, oneOf(e)).withBlastResistance(45) + return newBreakInfo(3, pickaxeHarvestable, pickaxeEffective, oneOf(e)).withBlastResistance(9) } // EncodeItem ... diff --git a/server/block/end_portal.go b/server/block/end_portal.go new file mode 100644 index 0000000000..56fcda2c37 --- /dev/null +++ b/server/block/end_portal.go @@ -0,0 +1,65 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/portal" +) + +// EndPortal is the translucent block that teleports the player to and from the End. It is created by inserting an Eye +// of Ender into all twelve frame blocks of an End portal frame ring. +type EndPortal struct { + transparent +} + +// Model ... +func (EndPortal) Model() world.BlockModel { + return model.Empty{} +} + +// LightEmissionLevel returns 15. +func (EndPortal) LightEmissionLevel() uint8 { + return 15 +} + +// HasLiquidDrops ... +func (EndPortal) HasLiquidDrops() bool { + return false +} + +// Portal returns the End dimension. The same block leads back to the Overworld when entered from the End. +func (EndPortal) Portal() world.Dimension { + return world.End +} + +// EncodeNBT encodes the End portal block actor. +func (EndPortal) EncodeNBT() map[string]any { + return map[string]any{"id": "EndPortal"} +} + +// DecodeNBT decodes the End portal block actor. +func (e EndPortal) DecodeNBT(map[string]any) any { + return e +} + +// NeighbourUpdateTick removes the connected portal blocks if the surrounding frame ring is no longer complete, +// like breaking the frame of a nether portal. +func (EndPortal) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + if portal.EndPortalRingIntact(tx, pos) { + return + } + portal.DeactivateEndPortal(tx, pos) +} + +// EntityInside ... +func (EndPortal) EntityInside(_ cube.Pos, tx *world.Tx, e world.Entity) { + if t, ok := e.(portalTraveller); ok { + t.TravelThroughPortal(tx, world.End) + } +} + +// EncodeBlock ... +func (EndPortal) EncodeBlock() (string, map[string]any) { + return "minecraft:end_portal", nil +} diff --git a/server/block/end_portal_frame.go b/server/block/end_portal_frame.go new file mode 100644 index 0000000000..765b8b5b59 --- /dev/null +++ b/server/block/end_portal_frame.go @@ -0,0 +1,89 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" +) + +// EndPortalFrame is the indestructible block that forms the twelve-block ring of an End portal. +type EndPortalFrame struct { + bassDrum + + // Eye is true if an Eye of Ender has been inserted into the frame. + Eye bool + // Facing is the direction the frame faces. Each frame in a valid ring faces the centre of the 3x3 interior. + Facing cube.Direction +} + +// Model ... +func (f EndPortalFrame) Model() world.BlockModel { + return model.EndPortalFrame{} +} + +// LightEmissionLevel returns 1. +func (EndPortalFrame) LightEmissionLevel() uint8 { + return 1 +} + +// EncodeItem ... +func (EndPortalFrame) EncodeItem() (name string, meta int16) { + return "minecraft:end_portal_frame", 0 +} + +// EncodeBlock ... +func (f EndPortalFrame) EncodeBlock() (string, map[string]any) { + return "minecraft:end_portal_frame", map[string]any{ + "end_portal_eye_bit": f.Eye, + "minecraft:cardinal_direction": f.Facing.String(), + } +} + +// UseOnBlock ... +func (f EndPortalFrame) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { + pos, _, used := firstReplaceable(tx, pos, face, f) + if !used { + return false + } + f.Facing = user.Rotation().Direction().Opposite() + f.Eye = false + place(tx, pos, f, user, ctx) + return placed(ctx) +} + +// EndPortalFrameState returns the frame's eye and facing state. +func (f EndPortalFrame) EndPortalFrameState() (eye bool, facing cube.Direction) { + return f.Eye, f.Facing +} + +// EncodeNBT encodes the End portal block actor stored with the frame. +func (EndPortalFrame) EncodeNBT() map[string]any { + return map[string]any{"id": "EndPortal"} +} + +// DecodeNBT decodes the End portal block actor. Eye and facing are stored in the block state. +func (f EndPortalFrame) DecodeNBT(map[string]any) any { + return f +} + +// InsertEndPortalEye returns a copy of the frame with an eye inserted, or false if it already held one. +func (f EndPortalFrame) InsertEndPortalEye() (world.Block, bool) { + if f.Eye { + return f, false + } + f.Eye = true + return f, true +} + +// allEndPortalFrames returns every state combination of EndPortalFrame for registration. +func allEndPortalFrames() []world.Block { + frames := make([]world.Block, 0, len(cube.Directions())*2) + for _, dir := range cube.Directions() { + for _, eye := range []bool{false, true} { + frames = append(frames, EndPortalFrame{Facing: dir, Eye: eye}) + } + } + return frames +} diff --git a/server/block/end_stone.go b/server/block/end_stone.go index 98224ed1c2..d4293b314e 100644 --- a/server/block/end_stone.go +++ b/server/block/end_stone.go @@ -8,7 +8,7 @@ type EndStone struct { // BreakInfo ... func (e EndStone) BreakInfo() BreakInfo { - return newBreakInfo(3, pickaxeHarvestable, pickaxeEffective, oneOf(e)).withBlastResistance(45) + return newBreakInfo(3, pickaxeHarvestable, pickaxeEffective, oneOf(e)).withBlastResistance(9) } // EncodeItem ... diff --git a/server/block/ender_chest.go b/server/block/ender_chest.go index 054b075ca5..8ded0bd781 100644 --- a/server/block/ender_chest.go +++ b/server/block/ender_chest.go @@ -37,7 +37,7 @@ func NewEnderChest() EnderChest { // BreakInfo ... func (c EnderChest) BreakInfo() BreakInfo { - return newBreakInfo(22.5, pickaxeHarvestable, pickaxeEffective, silkTouchDrop(item.NewStack(Obsidian{}, 8), item.NewStack(NewEnderChest(), 1))).withBlastResistance(3000) + return newBreakInfo(22.5, pickaxeHarvestable, pickaxeEffective, silkTouchDrop(item.NewStack(Obsidian{}, 8), item.NewStack(NewEnderChest(), 1))).withBlastResistance(600) } // LightEmissionLevel ... diff --git a/server/block/explosion.go b/server/block/explosion.go index 660549f583..7b519bc9c5 100644 --- a/server/block/explosion.go +++ b/server/block/explosion.go @@ -7,7 +7,6 @@ import ( "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/block/cube/trace" - "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/particle" @@ -15,11 +14,9 @@ import ( "github.com/go-gl/mathgl/mgl64" ) -// ExplosionConfig is the configuration for an explosion. The world, position, size, sound, particle, and more can all -// be configured through this configuration. +// ExplosionConfig is the configuration for an explosion. The sound, particle, item drop chance and more can all be +// configured through this configuration. The position and size come from the world.ExplosionSource passed to Explode. type ExplosionConfig struct { - // Size is the size of the explosion, it is effectively the radius which entities/blocks will be affected within. - Size float64 // RandSource is the source to use for the explosion "randomness". If set // to nil, RandSource defaults to a `rand.PCG`source seeded with // `time.Now().UnixNano()`. @@ -27,6 +24,9 @@ type ExplosionConfig struct { // SpawnFire will cause the explosion to randomly start fires in 1/3 of all destroyed air blocks that are // above opaque blocks. SpawnFire bool + // SuppressUnderwaterImpact prevents the explosion from affecting entities through liquid layers. Bedrock Edition + // applies this to every explosion. + SuppressUnderwaterImpact bool // ItemDropChance specifies how item drops should be handled. By default, // the item drop chance is 1/Size. If negative, no items will be dropped by // the explosion. If set to 1 or higher, all items are dropped. @@ -42,17 +42,27 @@ type ExplosionConfig struct { // ExplodableEntity represents an entity that can be exploded. type ExplodableEntity interface { - // Explode is called when an explosion occurs. The entity can then react to the explosion using the configuration - // and impact provided. - Explode(explosionPos mgl64.Vec3, impact float64, c ExplosionConfig) + // Explode is called when an explosion occurs. The entity can react using the source and impact provided. + Explode(src world.ExplosionSource, impact float64) } // Explodable represents a block that can be exploded. type Explodable interface { - // Explode is called when an explosion occurs. The block can react to the explosion using the configuration passed. - Explode(explosionPos mgl64.Vec3, pos cube.Pos, tx *world.Tx, c ExplosionConfig) + // Explode is called when an explosion occurs. The block can react using the source passed. + Explode(src world.ExplosionSource, pos cube.Pos, tx *world.Tx) } +type explosionBlockInfo struct { + resistance float64 + flags uint8 +} + +const ( + explosionBlockResists uint8 = 1 << iota + explosionBlockStopsRay + explosionBlockAffected +) + // rays ... var rays = make([]mgl64.Vec3, 0, 1352) @@ -71,7 +81,7 @@ func init() { } // Explode performs the explosion as specified by the configuration. -func (c ExplosionConfig) Explode(tx *world.Tx, explosionPos mgl64.Vec3) { +func (c ExplosionConfig) Explode(tx *world.Tx, src world.ExplosionSource) { if c.Sound == nil { c.Sound = sound.Explosion{} } @@ -82,14 +92,12 @@ func (c ExplosionConfig) Explode(tx *world.Tx, explosionPos mgl64.Vec3) { t := uint64(time.Now().UnixNano()) c.RandSource = rand.NewPCG(t, t) } - if c.Size == 0 { - c.Size = 4 - } + size, explosionPos := src.Size(), src.Position() if c.ItemDropChance == 0 { - c.ItemDropChance = 1.0 / c.Size + c.ItemDropChance = 1.0 / size } - r, d := rand.New(c.RandSource), c.Size*2 + r, d := rand.New(c.RandSource), size*2 box := cube.Box( math.Floor(explosionPos[0]-d-1), math.Floor(explosionPos[1]-d-1), @@ -110,48 +118,78 @@ func (c ExplosionConfig) Explode(tx *world.Tx, explosionPos mgl64.Vec3) { affectedEntities = append(affectedEntities, e) } - affectedBlocks := make([]cube.Pos, 0, 32) + estimatedBlocks := max(32, min(4096, int(size*size*size*16))) + if _, ok := tx.Liquid(cube.PosFromVec3(explosionPos)); ok { + // Liquids such as water stop a regular TNT blast at the source, so avoid reserving space for a full blast. + estimatedBlocks = 32 + } + affectedBlocks := make([]cube.Pos, 0, estimatedBlocks) + blockCache := make(map[cube.Pos]explosionBlockInfo, estimatedBlocks) for _, ray := range rays { pos := explosionPos - for blastForce := c.Size * (0.7 + r.Float64()*0.6); blastForce > 0.0; blastForce -= 0.225 { + for blastForce := size * (0.7 + r.Float64()*0.6); blastForce > 0.0; blastForce -= 0.225 { current := cube.PosFromVec3(pos) - currentBlock := tx.Block(current) - - resistance := 0.0 - if l, ok := tx.Liquid(current); ok { - resistance = l.BlastResistance() - } else if i, ok := currentBlock.(Breakable); ok { - resistance = i.BreakInfo().BlastResistance - } else if _, ok = currentBlock.(Air); !ok { + info, ok := blockCache[current] + if !ok { + currentBlock := tx.Block(current) + if l, ok := tx.Liquid(current); ok { + info.resistance = l.BlastResistance() + info.flags = explosionBlockResists + } else if i, ok := currentBlock.(Breakable); ok { + info.resistance = i.BreakInfo().BlastResistance + info.flags = explosionBlockResists + } else if _, ok = currentBlock.(Air); !ok { + info.flags = explosionBlockStopsRay + } + blockCache[current] = info + } + if info.flags&explosionBlockStopsRay != 0 { // Completely stop the ray if the current block is not air and unbreakable. break } pos = pos.Add(ray) - if blastForce -= (resistance/5 + 0.3) * 0.3; blastForce > 0 { + // Air offers no resistance to the ray, only blocks and liquids reduce its force beyond the step decay. + if info.flags&explosionBlockResists != 0 { + blastForce -= (info.resistance + 0.3) * 0.3 + } + if blastForce > 0 && info.flags&explosionBlockAffected == 0 { + info.flags |= explosionBlockAffected + blockCache[current] = info affectedBlocks = append(affectedBlocks, current) } } } - ctx := event.C(tx) + ctx := tx.Event() spawnFire := c.SpawnFire itemDropChance := c.ItemDropChance - if tx.World().Handler().HandleExplosion(ctx, explosionPos, &affectedEntities, &affectedBlocks, &itemDropChance, &spawnFire); ctx.Cancelled() { + if tx.World().Handler().HandleExplosion(ctx, src, &affectedEntities, &affectedBlocks, &itemDropChance, &spawnFire); ctx.Cancelled() { return } for _, e := range affectedEntities { - if explodable, ok := e.(ExplodableEntity); ok { - impact := (1 - e.Position().Sub(explosionPos).Len()/d) * exposure(tx, explosionPos, e) - explodable.Explode(explosionPos, impact, c) + explodable, ok := e.(ExplodableEntity) + if !ok { + continue + } + impact := (1 - e.Position().Sub(explosionPos).Len()/d) * c.exposure(tx, explosionPos, e) + if c.SuppressUnderwaterImpact && impact <= 0 { + // The blast never reached the entity. Skip the call entirely, as entities with a constant damage term, + // such as players, would otherwise still be hurt through the liquid that blocked it. + continue } + explodable.Explode(src, impact) } + blast := make(map[cube.Pos]struct{}, len(affectedBlocks)) + for _, pos := range affectedBlocks { + blast[pos] = struct{}{} + } for _, pos := range affectedBlocks { bl := tx.Block(pos) if explodable, ok := bl.(Explodable); ok { - explodable.Explode(explosionPos, pos, tx, c) + explodable.Explode(src, pos, tx) } else if breakable, ok := bl.(Breakable); ok { // Clear the block first so break handlers see the post-break world, this is required by things such as redstone updates. tx.SetBlock(pos, nil, nil) @@ -164,6 +202,7 @@ func (c ExplosionConfig) Explode(tx *world.Tx, explosionPos mgl64.Vec3) { dropItem(tx, drop, pos.Vec3Centre()) } } + removeDependents(pos, tx, blast) } } @@ -182,7 +221,7 @@ func (c ExplosionConfig) Explode(tx *world.Tx, explosionPos mgl64.Vec3) { } // exposure returns the exposure of an explosion to an entity, used to calculate the impact of an explosion. -func exposure(tx *world.Tx, origin mgl64.Vec3, e world.Entity) float64 { +func (c ExplosionConfig) exposure(tx *world.Tx, origin mgl64.Vec3, e world.Entity) float64 { pos := e.Position() box := e.H().Type().BBox(e).Translate(pos) @@ -208,6 +247,12 @@ func exposure(tx *world.Tx, origin mgl64.Vec3, e world.Entity) float64 { } var collided bool trace.TraverseBlocks(origin, point, func(pos cube.Pos) (cont bool) { + if c.SuppressUnderwaterImpact { + if _, liquid := tx.Liquid(pos); liquid { + collided = true + return false + } + } _, collided = trace.BlockIntercept(pos, tx, tx.Block(pos), origin, point) return !collided }) @@ -226,3 +271,19 @@ func exposure(tx *world.Tx, origin mgl64.Vec3, e world.Entity) float64 { func lerp(a, b, t float64) float64 { return b + a*(t-b) } + +// removeDependents removes the blocks in the blast of an explosion that depended on the block at the position passed. +// Neighbour updates only run at the end of a tick, so the other half of a block such as a door or a bed would still be +// standing when the explosion reaches it and would drop a second item of its own. +func removeDependents(pos cube.Pos, tx *world.Tx, blast map[cube.Pos]struct{}) { + pos.Neighbours(func(neighbour cube.Pos) { + if _, ok := blast[neighbour]; !ok { + // Blocks outside the blast are removed by the neighbour updates at the end of the tick, which drop no + // items of their own. + return + } + if ticker, ok := tx.Block(neighbour).(world.NeighbourUpdateTicker); ok { + ticker.NeighbourUpdateTick(neighbour, pos, tx) + } + }, tx.Range()) +} diff --git a/server/block/farmland.go b/server/block/farmland.go index 8f0f71a48f..f752ade410 100644 --- a/server/block/farmland.go +++ b/server/block/farmland.go @@ -4,7 +4,6 @@ import ( "math/rand/v2" "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/world" ) @@ -74,7 +73,7 @@ func (f Farmland) hydrated(pos cube.Pos, tx *world.Tx) bool { func (f Farmland) EntityLand(pos cube.Pos, tx *world.Tx, e world.Entity, _ *float64) { if living, ok := e.(livingEntity); ok { if fall, ok := living.(fallDistanceEntity); ok && rand.Float64() < fall.FallDistance()-0.5 { - ctx := event.C(tx) + ctx := tx.Event() if tx.World().Handler().HandleCropTrample(ctx, pos); !ctx.Cancelled() { tx.SetBlock(pos, Dirt{}, nil) } diff --git a/server/block/fire.go b/server/block/fire.go index 59a6052bed..e81330dd7f 100644 --- a/server/block/fire.go +++ b/server/block/fire.go @@ -3,13 +3,14 @@ package block //lint:file-ignore ST1022 Exported variables in this package have compiler directives. These variables are not otherwise exposed to users. import ( + "math/rand/v2" + "time" + "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/item/enchantment" "github.com/df-mc/dragonfly/server/world" - "math/rand/v2" - "time" + "github.com/df-mc/dragonfly/server/world/portal" ) // Fire is a non-solid block that can spread to nearby flammable blocks. @@ -175,16 +176,19 @@ func (f Fire) tick(pos cube.Pos, tx *world.Tx, r *rand.Rand) { // this might end up not happening. func (f Fire) spread(from, to cube.Pos, tx *world.Tx, r *rand.Rand) { if _, air := tx.Block(to).(Air); !air { - ctx := event.C(tx) + ctx := tx.Event() if tx.World().Handler().HandleBlockBurn(ctx, to); ctx.Cancelled() { return } } - ctx := event.C(tx) + ctx := tx.Event() if tx.World().Handler().HandleFireSpread(ctx, from, to); ctx.Cancelled() { return } spread := Fire{Type: f.Type, Age: min(15, f.Age+r.IntN(5)/4)} + if spread.Type == NormalFire() && portal.ActivateNetherPortal(tx, to) { + return + } tx.SetBlock(to, spread, nil) tx.ScheduleBlockUpdate(to, spread, time.Duration(30+r.IntN(10))*time.Second/20) } @@ -239,6 +243,11 @@ func (f Fire) HasLiquidDrops() bool { return false } +// PortalInterior returns true if fire may occupy the inside of a portal frame before activation for the target dimension. +func (f Fire) PortalInterior(target world.Dimension) bool { + return target == world.Nether && f.Type == NormalFire() +} + // LightEmissionLevel ... func (f Fire) LightEmissionLevel() uint8 { return f.Type.LightLevel() @@ -265,6 +274,9 @@ func (f Fire) Start(tx *world.Tx, pos cube.Pos) { if air || shortGrass || fern { below := tx.Block(pos.Side(cube.FaceDown)) if below.Model().FaceSolid(pos, cube.FaceUp, tx) || neighboursFlammable(pos, tx) { + if portal.ActivateNetherPortal(tx, pos) { + return + } f := Fire{} tx.SetBlock(pos, f, nil) tx.ScheduleBlockUpdate(pos, f, time.Duration(30+rand.IntN(10))*time.Second/20) diff --git a/server/block/furnace.go b/server/block/furnace.go index b7ab603a22..e763cce7fb 100644 --- a/server/block/furnace.go +++ b/server/block/furnace.go @@ -80,7 +80,10 @@ func (f Furnace) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *worl // BreakInfo ... func (f Furnace) BreakInfo() BreakInfo { - xp := f.Experience() + xp := 0 + if f.smelter != nil { + xp = f.Experience() + } return newBreakInfo(3.5, alwaysHarvestable, pickaxeEffective, oneOf(Furnace{})).withXPDropRange(xp, xp).withBreakHandler(func(pos cube.Pos, tx *world.Tx, u item.User) { for _, i := range f.Inventory(tx, pos).Clear() { dropItem(tx, i, pos.Vec3()) diff --git a/server/block/glowstone.go b/server/block/glowstone.go index 97539d7a51..e55d5171da 100644 --- a/server/block/glowstone.go +++ b/server/block/glowstone.go @@ -12,37 +12,29 @@ type Glowstone struct { solid } -// Instrument ... func (g Glowstone) Instrument() sound.Instrument { return sound.Pling() } -// CanRedstoneWireStepDown ... +// CanRedstoneWireStepDown keeps dust from stepping down over glowstone despite its solid top face. func (Glowstone) CanRedstoneWireStepDown(cube.Pos, cube.Pos, *world.Tx) bool { return false } -// RelaysRedstonePowerThrough returns false. -func (Glowstone) RelaysRedstonePowerThrough() bool { - return false -} +func (Glowstone) RedstoneNonConductive() {} -// BreakInfo ... func (g Glowstone) BreakInfo() BreakInfo { return newBreakInfo(0.3, alwaysHarvestable, nothingEffective, discreteDrops(item.GlowstoneDust{}, g, 2, 4, 4)) } -// EncodeItem ... func (Glowstone) EncodeItem() (name string, meta int16) { return "minecraft:glowstone", 0 } -// EncodeBlock ... func (Glowstone) EncodeBlock() (string, map[string]any) { return "minecraft:glowstone", nil } -// LightEmissionLevel returns 15. func (Glowstone) LightEmissionLevel() uint8 { return 15 } diff --git a/server/block/gold.go b/server/block/gold.go index 2a08ad2187..9b66cddbdf 100644 --- a/server/block/gold.go +++ b/server/block/gold.go @@ -19,7 +19,7 @@ func (g Gold) Instrument() sound.Instrument { func (g Gold) BreakInfo() BreakInfo { return newBreakInfo(3, func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierIron.HarvestLevel - }, pickaxeEffective, oneOf(g)).withBlastResistance(30) + }, pickaxeEffective, oneOf(g)).withBlastResistance(6) } // PowersBeacon ... diff --git a/server/block/gold_ore.go b/server/block/gold_ore.go index 698ac4fb94..b3db4a52b5 100644 --- a/server/block/gold_ore.go +++ b/server/block/gold_ore.go @@ -17,7 +17,7 @@ type GoldOre struct { func (g GoldOre) BreakInfo() BreakInfo { return newBreakInfo(g.Type.Hardness(), func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierIron.HarvestLevel - }, pickaxeEffective, oreDrops(item.RawGold{}, g)).withBlastResistance(15) + }, pickaxeEffective, oreDrops(item.RawGold{}, g)).withBlastResistance(3) } // SmeltInfo ... diff --git a/server/block/grass.go b/server/block/grass.go index 238af3e1ee..77279d8091 100644 --- a/server/block/grass.go +++ b/server/block/grass.go @@ -39,7 +39,7 @@ func init() { // SoilFor ... func (g Grass) SoilFor(block world.Block) bool { switch block.(type) { - case ShortGrass, Fern, DoubleTallGrass, Flower, DoubleFlower, NetherSprouts, PinkPetals, SugarCane, DeadBush, Azalea, Sapling: + case ShortGrass, Fern, DoubleTallGrass, Flower, DoubleFlower, NetherSprouts, PinkPetals, SugarCane, DeadBush, Azalea, Sapling, BambooSapling, Bamboo: return true } return false diff --git a/server/block/gravel.go b/server/block/gravel.go index 4326d59880..38e103a816 100644 --- a/server/block/gravel.go +++ b/server/block/gravel.go @@ -15,6 +15,15 @@ type Gravel struct { snare } +// SoilFor ... +func (g Gravel) SoilFor(block world.Block) bool { + switch block.(type) { + case BambooSapling, Bamboo: + return true + } + return false +} + // NeighbourUpdateTick ... func (g Gravel) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { g.fall(g, pos, tx) diff --git a/server/block/grindstone.go b/server/block/grindstone.go index e321f2e999..18fd2b4792 100644 --- a/server/block/grindstone.go +++ b/server/block/grindstone.go @@ -21,7 +21,7 @@ type Grindstone struct { // BreakInfo ... func (g Grindstone) BreakInfo() BreakInfo { - return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(g)).withBlastResistance(30) + return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(g)).withBlastResistance(6) } // Activate ... diff --git a/server/block/hash.go b/server/block/hash.go index d6790e02cd..f706ca0967 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -10,6 +10,10 @@ const ( hashAncientDebris hashAndesite hashAnvil + hashBamboo + hashBambooBlock + hashBambooMosaic + hashBambooSapling hashAzalea hashAzaleaLeaves hashBanner @@ -36,6 +40,8 @@ const ( hashCarrot hashChest hashChiseledQuartz + hashCinnabar + hashCinnabarBricks hashClay hashCoal hashCoalOre @@ -78,6 +84,8 @@ const ( hashEmeraldOre hashEnchantingTable hashEndBricks + hashEndPortal + hashEndPortalFrame hashEndRod hashEndStone hashEnderChest @@ -154,7 +162,10 @@ const ( hashPlanks hashPodzol hashPolishedBlackstoneBrick + hashPolishedCinnabar + hashPolishedSulfur hashPolishedTuff + hashPortal hashPotato hashPrismarine hashPumpkin @@ -183,6 +194,7 @@ const ( hashSeagrass hashShortGrass hashShroomlight + hashShulkerBox hashSign hashSkull hashSlab @@ -205,8 +217,11 @@ const ( hashStonecutter hashString hashSugarCane + hashSulfur + hashSulfurBricks hashTNT hashTerracotta + hashTintedGlass hashTorch hashTuff hashTuffBricks @@ -252,6 +267,22 @@ func (a Anvil) Hash() (uint64, uint64) { return hashAnvil, uint64(a.Type.Uint8()) | uint64(a.Facing)<<2 } +func (b Bamboo) Hash() (uint64, uint64) { + return hashBamboo, uint64(boolByte(b.Ready)) | uint64(boolByte(b.Thick))<<1 | uint64(b.LeafSize.Uint8())<<2 +} + +func (b BambooBlock) Hash() (uint64, uint64) { + return hashBambooBlock, uint64(b.Axis) | uint64(boolByte(b.Stripped))<<2 +} + +func (BambooMosaic) Hash() (uint64, uint64) { + return hashBambooMosaic, 0 +} + +func (b BambooSapling) Hash() (uint64, uint64) { + return hashBambooSapling, uint64(boolByte(b.Ready)) +} + func (a Azalea) Hash() (uint64, uint64) { return hashAzalea, uint64(boolByte(a.Flowering)) } @@ -321,7 +352,7 @@ func (c Cactus) Hash() (uint64, uint64) { } func (c Cake) Hash() (uint64, uint64) { - return hashCake, uint64(c.Bites) + return hashCake, uint64(c.Bites) | uint64(boolByte(c.Candle))<<8 | uint64(c.CandleColour.Uint8())<<9 | uint64(boolByte(c.CandleLit))<<14 } func (Calcite) Hash() (uint64, uint64) { @@ -352,6 +383,14 @@ func (ChiseledQuartz) Hash() (uint64, uint64) { return hashChiseledQuartz, 0 } +func (c Cinnabar) Hash() (uint64, uint64) { + return hashCinnabar, uint64(boolByte(c.Chiseled)) +} + +func (CinnabarBricks) Hash() (uint64, uint64) { + return hashCinnabarBricks, 0 +} + func (Clay) Hash() (uint64, uint64) { return hashClay, 0 } @@ -520,6 +559,14 @@ func (EndBricks) Hash() (uint64, uint64) { return hashEndBricks, 0 } +func (EndPortal) Hash() (uint64, uint64) { + return hashEndPortal, 0 +} + +func (f EndPortalFrame) Hash() (uint64, uint64) { + return hashEndPortalFrame, uint64(boolByte(f.Eye)) | uint64(f.Facing)<<1 +} + func (e EndRod) Hash() (uint64, uint64) { return hashEndRod, uint64(e.Facing) } @@ -824,10 +871,22 @@ func (b PolishedBlackstoneBrick) Hash() (uint64, uint64) { return hashPolishedBlackstoneBrick, uint64(boolByte(b.Cracked)) } +func (PolishedCinnabar) Hash() (uint64, uint64) { + return hashPolishedCinnabar, 0 +} + +func (PolishedSulfur) Hash() (uint64, uint64) { + return hashPolishedSulfur, 0 +} + func (PolishedTuff) Hash() (uint64, uint64) { return hashPolishedTuff, 0 } +func (p Portal) Hash() (uint64, uint64) { + return hashPortal, uint64(p.Axis) +} + func (p Potato) Hash() (uint64, uint64) { return hashPotato, uint64(p.Growth) } @@ -940,6 +999,10 @@ func (Shroomlight) Hash() (uint64, uint64) { return hashShroomlight, 0 } +func (s ShulkerBox) Hash() (uint64, uint64) { + return hashShulkerBox, uint64(s.Colour.Uint8()) +} + func (s Sign) Hash() (uint64, uint64) { return hashSign, uint64(s.Wood.Uint8()) | uint64(s.Attach.Uint8())<<4 } @@ -1028,6 +1091,14 @@ func (c SugarCane) Hash() (uint64, uint64) { return hashSugarCane, uint64(c.Age) } +func (s Sulfur) Hash() (uint64, uint64) { + return hashSulfur, uint64(boolByte(s.Chiseled)) +} + +func (SulfurBricks) Hash() (uint64, uint64) { + return hashSulfurBricks, 0 +} + func (TNT) Hash() (uint64, uint64) { return hashTNT, 0 } @@ -1036,6 +1107,10 @@ func (Terracotta) Hash() (uint64, uint64) { return hashTerracotta, 0 } +func (TintedGlass) Hash() (uint64, uint64) { + return hashTintedGlass, 0 +} + func (t Torch) Hash() (uint64, uint64) { return hashTorch, uint64(t.Facing) | uint64(t.Type.Uint8())<<3 } diff --git a/server/block/hopper.go b/server/block/hopper.go index 14a86a637b..d6f455dc06 100644 --- a/server/block/hopper.go +++ b/server/block/hopper.go @@ -28,8 +28,6 @@ type Hopper struct { // colour codes. CustomName string - // LastTick is the last world tick that the hopper was ticked. - LastTick int64 // TransferCooldown is the duration in ticks until the hopper can transfer items again. TransferCooldown int64 // CollectCooldown is the duration in ticks until the hopper can collect items again. @@ -74,7 +72,7 @@ func (Hopper) CanRedstoneWireStepDown(cube.Pos, cube.Pos, *world.Tx) bool { // BreakInfo ... func (h Hopper) BreakInfo() BreakInfo { - return newBreakInfo(3, pickaxeHarvestable, pickaxeEffective, oneOf(Hopper{})).withBlastResistance(24).withBreakHandler(func(pos cube.Pos, tx *world.Tx, u item.User) { + return newBreakInfo(3, pickaxeHarvestable, pickaxeEffective, oneOf(Hopper{})).withBlastResistance(4.8).withBreakHandler(func(pos cube.Pos, tx *world.Tx, u item.User) { for _, i := range h.Inventory(tx, pos).Clear() { dropItem(tx, i, pos.Vec3()) } @@ -134,20 +132,28 @@ func (h Hopper) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world } // Tick ... -func (h Hopper) Tick(currentTick int64, pos cube.Pos, tx *world.Tx) { - h.TransferCooldown-- - h.CollectCooldown-- - h.LastTick = currentTick +func (h Hopper) Tick(_ int64, pos cube.Pos, tx *world.Tx) { + cooldownChanged := h.TransferCooldown > 0 || h.CollectCooldown > 0 + if h.TransferCooldown > 0 { + h.TransferCooldown-- + } + if h.CollectCooldown > 0 { + h.CollectCooldown-- + } if !h.Powered && h.TransferCooldown <= 0 { inserted := h.insertItem(pos, tx) extracted := h.extractItem(pos, tx) if inserted || extracted { h.TransferCooldown = 8 + tx.SetBlock(pos, h, nil) + return } } - tx.SetBlock(pos, h, nil) + if cooldownChanged { + tx.SetBlockEntity(pos, h) + } } // HopperInsertable represents a block that can have its contents inserted into by a hopper. @@ -171,7 +177,7 @@ func (h Hopper) insertItem(pos cube.Pos, tx *world.Tx) bool { continue } - _, err := container.Inventory(tx, pos).AddItem(sourceStack.Grow(-sourceStack.Count() + 1)) + _, err := container.Inventory(tx, destPos).AddItem(sourceStack.Grow(-sourceStack.Count() + 1)) if err != nil { // The destination is full. return false diff --git a/server/block/iron.go b/server/block/iron.go index 87903c9957..f2aa9f05c1 100644 --- a/server/block/iron.go +++ b/server/block/iron.go @@ -19,7 +19,7 @@ func (i Iron) Instrument() sound.Instrument { func (i Iron) BreakInfo() BreakInfo { return newBreakInfo(5, func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierStone.HarvestLevel - }, pickaxeEffective, oneOf(i)).withBlastResistance(30) + }, pickaxeEffective, oneOf(i)).withBlastResistance(6) } // PowersBeacon ... diff --git a/server/block/iron_bars.go b/server/block/iron_bars.go index 24ecdceb32..45e11de9a4 100644 --- a/server/block/iron_bars.go +++ b/server/block/iron_bars.go @@ -14,7 +14,7 @@ type IronBars struct { // BreakInfo ... func (i IronBars) BreakInfo() BreakInfo { - return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(i)).withBlastResistance(30) + return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(i)).withBlastResistance(6) } // SideClosed ... diff --git a/server/block/iron_chain.go b/server/block/iron_chain.go index a5c50a5387..a266b34a58 100644 --- a/server/block/iron_chain.go +++ b/server/block/iron_chain.go @@ -36,7 +36,7 @@ func (c IronChain) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *wo // BreakInfo ... func (c IronChain) BreakInfo() BreakInfo { - return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(c)).withBlastResistance(30) + return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(c)).withBlastResistance(6) } // EncodeItem ... diff --git a/server/block/iron_ore.go b/server/block/iron_ore.go index 6a78ef68d1..cc9609a2f9 100644 --- a/server/block/iron_ore.go +++ b/server/block/iron_ore.go @@ -17,7 +17,7 @@ type IronOre struct { func (i IronOre) BreakInfo() BreakInfo { return newBreakInfo(i.Type.Hardness(), func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierStone.HarvestLevel - }, pickaxeEffective, oreDrops(item.RawIron{}, i)).withBlastResistance(15) + }, pickaxeEffective, oreDrops(item.RawIron{}, i)).withBlastResistance(3) } // SmeltInfo ... diff --git a/server/block/item_frame.go b/server/block/item_frame.go index 2eb96e210e..d5b6311bc7 100644 --- a/server/block/item_frame.go +++ b/server/block/item_frame.go @@ -119,7 +119,7 @@ func (i ItemFrame) EncodeBlock() (name string, properties map[string]any) { func (i ItemFrame) DecodeNBT(data map[string]any) any { i.DropChance = float64(nbtconv.Float32(data, "ItemDropChance")) i.Rotations = int(nbtconv.Uint8(data, "ItemRotation")) - i.Item = nbtconv.MapItem(data, "Item") + i.Item = item.MapNBT(data, "Item") return i } @@ -134,7 +134,7 @@ func (i ItemFrame) EncodeNBT() map[string]any { m["id"] = "GlowItemFrame" } if !i.Item.Empty() { - m["Item"] = nbtconv.WriteItem(i.Item, true) + m["Item"] = item.WriteNBT(i.Item, true) } return m } diff --git a/server/block/jukebox.go b/server/block/jukebox.go index c56b9a8688..bd2898387d 100644 --- a/server/block/jukebox.go +++ b/server/block/jukebox.go @@ -5,7 +5,6 @@ import ( "time" "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/internal/nbtconv" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/sound" @@ -56,7 +55,7 @@ func (j Jukebox) FuelInfo() item.FuelInfo { // BreakInfo ... func (j Jukebox) BreakInfo() BreakInfo { - return newBreakInfo(2, alwaysHarvestable, axeEffective, oneOf(Jukebox{})).withBlastResistance(30).withBreakHandler(func(pos cube.Pos, tx *world.Tx, u item.User) { + return newBreakInfo(2, alwaysHarvestable, axeEffective, oneOf(Jukebox{})).withBlastResistance(6).withBreakHandler(func(pos cube.Pos, tx *world.Tx, u item.User) { if _, hasDisc := j.Disc(); hasDisc { dropItem(tx, j.Item, pos.Vec3()) tx.PlaySound(pos.Vec3Centre(), sound.MusicDiscEnd{}) @@ -110,14 +109,14 @@ func (j Jukebox) Disc() (sound.DiscType, bool) { func (j Jukebox) EncodeNBT() map[string]any { m := map[string]any{"id": "Jukebox"} if _, hasDisc := j.Disc(); hasDisc { - m["RecordItem"] = nbtconv.WriteItem(j.Item, true) + m["RecordItem"] = item.WriteNBT(j.Item, true) } return m } // DecodeNBT ... func (j Jukebox) DecodeNBT(data map[string]any) any { - s := nbtconv.MapItem(data, "RecordItem") + s := item.MapNBT(data, "RecordItem") if _, ok := s.Item().(item.MusicDisc); ok { j.Item = s } diff --git a/server/block/lapis_ore.go b/server/block/lapis_ore.go index 56b12ab468..8d7ff6e397 100644 --- a/server/block/lapis_ore.go +++ b/server/block/lapis_ore.go @@ -17,7 +17,7 @@ type LapisOre struct { func (l LapisOre) BreakInfo() BreakInfo { return newBreakInfo(l.Type.Hardness(), func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierStone.HarvestLevel - }, pickaxeEffective, multiOreDrops(item.LapisLazuli{}, l, 4, 9)).withXPDropRange(2, 5).withBlastResistance(15) + }, pickaxeEffective, multiOreDrops(item.LapisLazuli{}, l, 4, 9)).withXPDropRange(2, 5).withBlastResistance(3) } // SmeltInfo ... diff --git a/server/block/lava.go b/server/block/lava.go index 9899d7b93b..67bf804727 100644 --- a/server/block/lava.go +++ b/server/block/lava.go @@ -5,7 +5,6 @@ import ( "time" "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/sound" ) @@ -139,9 +138,9 @@ func (l Lava) LiquidFalling() bool { return l.Falling } -// BlastResistance always returns 500. +// BlastResistance ... func (Lava) BlastResistance() float64 { - return 500 + return 100 } // LiquidType returns 10 as a unique identifier for the lava liquid. @@ -177,7 +176,7 @@ func (l Lava) Harden(pos cube.Pos, tx *world.Tx, flownIntoBy *cube.Pos) bool { } }, tx.Range()) if b != nil { - ctx := event.C(tx) + ctx := tx.Event() if tx.World().Handler().HandleLiquidHarden(ctx, pos, l, water, b); ctx.Cancelled() { return false } @@ -197,7 +196,7 @@ func (l Lava) Harden(pos cube.Pos, tx *world.Tx, flownIntoBy *cube.Pos) bool { } else { b = Cobblestone{} } - ctx := event.C(tx) + ctx := tx.Event() if tx.World().Handler().HandleLiquidHarden(ctx, pos, l, water, b); ctx.Cancelled() { return false } diff --git a/server/block/leaves.go b/server/block/leaves.go index 8ca2b33ab5..b7f17bd396 100644 --- a/server/block/leaves.go +++ b/server/block/leaves.go @@ -4,7 +4,6 @@ import ( "math/rand/v2" "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" @@ -68,7 +67,7 @@ func (l Leaves) RandomTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { tx.SetBlock(pos, l, nil) return } - ctx := event.C(tx) + ctx := tx.Event() if tx.World().Handler().HandleLeavesDecay(ctx, pos); ctx.Cancelled() { // Prevent immediate re-updating. l.ShouldUpdate = false diff --git a/server/block/lectern.go b/server/block/lectern.go index fee6c6fe19..2af563e72f 100644 --- a/server/block/lectern.go +++ b/server/block/lectern.go @@ -88,7 +88,8 @@ func (l Lectern) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, return false } - l.Book, l.Page = held, 0 + // Only one book is taken from the stack held, so only one may be put on the lectern. + l.Book, l.Page = held.Grow(-held.Count()+1), 0 tx.SetBlock(pos, l, nil) tx.PlaySound(pos.Vec3Centre(), sound.LecternBookPlace{}) @@ -135,7 +136,7 @@ func (l Lectern) EncodeNBT() map[string]any { "id": "Lectern", } if r, ok := l.Book.Item().(readableBook); ok { - m["book"] = nbtconv.WriteItem(l.Book, true) + m["book"] = item.WriteNBT(l.Book, true) m["totalPages"] = int32(r.TotalPages()) } return m @@ -144,7 +145,7 @@ func (l Lectern) EncodeNBT() map[string]any { // DecodeNBT ... func (l Lectern) DecodeNBT(m map[string]any) any { l.Page = int(nbtconv.Int32(m, "page")) - l.Book = nbtconv.MapItem(m, "book") + l.Book = item.MapNBT(m, "book") return l } diff --git a/server/block/lever.go b/server/block/lever.go index 10a2cacbea..3dcf03505b 100644 --- a/server/block/lever.go +++ b/server/block/lever.go @@ -23,33 +23,24 @@ type Lever struct { Direction cube.Direction } -// RedstoneSource ... -func (l Lever) RedstoneSource() bool { - return true -} - -// WeakPower ... -func (l Lever) WeakPower(cube.Pos, cube.Face, *world.Tx, bool) int { +func (l Lever) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { if l.Powered { return 15 } return 0 } -// StrongPower ... -func (l Lever) StrongPower(_ cube.Pos, face cube.Face, _ *world.Tx, _ bool) int { - if l.Powered && l.Facing == face { +func (l Lever) RedstoneStrongPower(_ cube.Pos, _ *world.Tx, face cube.Face) int { + if l.Powered && l.Facing.Opposite() == face { return 15 } return 0 } -// SideClosed ... func (l Lever) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { return false } -// NeighbourUpdateTick ... func (l Lever) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { supportPos := pos.Side(l.Facing.Opposite()) if !tx.Block(supportPos).Model().FaceSolid(supportPos, l.Facing, tx) { @@ -57,7 +48,6 @@ func (l Lever) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { } } -// UseOnBlock ... func (l Lever) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { pos, face, used := firstReplaceable(tx, pos, face, l) if !used { @@ -78,7 +68,6 @@ func (l Lever) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world. return placed(ctx) } -// Activate ... func (l Lever) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ *item.UseContext) bool { l.Powered = !l.Powered tx.SetBlock(pos, l, nil) @@ -87,23 +76,17 @@ func (l Lever) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ } else { tx.PlaySound(pos.Vec3Centre(), sound.PowerOff{}) } - updateDirectionalRedstone(pos, tx, l.Facing.Opposite()) return true } -// BreakInfo ... func (l Lever) BreakInfo() BreakInfo { - return newBreakInfo(0.5, alwaysHarvestable, nothingEffective, oneOf(Lever{})).withBreakHandler(func(pos cube.Pos, tx *world.Tx, _ item.User) { - updateDirectionalRedstone(pos, tx, l.Facing.Opposite()) - }) + return newBreakInfo(0.5, alwaysHarvestable, nothingEffective, oneOf(Lever{})) } -// EncodeItem ... func (l Lever) EncodeItem() (name string, meta int16) { return "minecraft:lever", 0 } -// EncodeBlock ... func (l Lever) EncodeBlock() (string, map[string]any) { direction := l.Facing.String() if l.Facing == cube.FaceDown || l.Facing == cube.FaceUp { @@ -116,7 +99,6 @@ func (l Lever) EncodeBlock() (string, map[string]any) { return "minecraft:lever", map[string]any{"open_bit": l.Powered, "lever_direction": direction} } -// allLevers ... func allLevers() (all []world.Block) { f := func(facing cube.Face, direction cube.Direction) { all = append(all, Lever{Facing: facing, Direction: direction}) diff --git a/server/block/liquid.go b/server/block/liquid.go index df72fa2890..9954ca3693 100644 --- a/server/block/liquid.go +++ b/server/block/liquid.go @@ -5,7 +5,6 @@ import ( "sync" "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/world" ) @@ -42,7 +41,7 @@ func tickLiquid(b world.Liquid, pos cube.Pos, tx *world.Tx) { if b.LiquidDepth()-4 > 0 { res = b.WithDepth(b.LiquidDepth()-2*b.SpreadDecay(), false) } - ctx := event.C(tx) + ctx := tx.Event() if tx.World().Handler().HandleLiquidDecay(ctx, pos, b, res); ctx.Cancelled() { return } @@ -138,7 +137,7 @@ func flowInto(b world.Liquid, src, pos cube.Pos, tx *world.Tx, falling bool) boo // (basically considered full depth), so no need to continue. return true } - ctx := event.C(tx) + ctx := tx.Event() if tx.World().Handler().HandleLiquidFlow(ctx, src, pos, b.WithDepth(newDepth, falling), existing); ctx.Cancelled() { return false } @@ -160,7 +159,7 @@ func flowInto(b world.Liquid, src, pos cube.Pos, tx *world.Tx, falling bool) boo // Can't flow into this block. return false } - ctx := event.C(tx) + ctx := tx.Event() if tx.World().Handler().HandleLiquidFlow(ctx, src, pos, b.WithDepth(newDepth, falling), existing); ctx.Cancelled() { return false } diff --git a/server/block/log.go b/server/block/log.go index 9303dd9f9e..de85e18a06 100644 --- a/server/block/log.go +++ b/server/block/log.go @@ -34,7 +34,12 @@ func (l Log) FlammabilityInfo() FlammabilityInfo { // BreakInfo ... func (l Log) BreakInfo() BreakInfo { - return newBreakInfo(2, alwaysHarvestable, axeEffective, oneOf(l)) + breakInfo := newBreakInfo(2, alwaysHarvestable, axeEffective, oneOf(l)) + if l.Wood == MangroveWood() && !l.Stripped { + // Mangrove logs have a much lower blast resistance than all other logs. + return breakInfo.withBlastResistance(0.4) + } + return breakInfo } // SmeltInfo ... @@ -106,6 +111,9 @@ func (l Log) EncodeBlock() (name string, properties map[string]any) { // allLogs returns a list of all possible log states. func allLogs() (logs []world.Block) { for _, w := range WoodTypes() { + if w == BambooWood() { + continue + } for axis := cube.Axis(0); axis < 3; axis++ { logs = append(logs, Log{Axis: axis, Stripped: true, Wood: w}) logs = append(logs, Log{Axis: axis, Stripped: false, Wood: w}) diff --git a/server/block/magma.go b/server/block/magma.go index 351e4fd5e6..9adff8d6e4 100644 --- a/server/block/magma.go +++ b/server/block/magma.go @@ -34,7 +34,7 @@ func (Magma) EntityStepOn(_ cube.Pos, _ *world.Tx, e world.Entity) { // BreakInfo ... func (m Magma) BreakInfo() BreakInfo { - return newBreakInfo(0.5, pickaxeHarvestable, pickaxeEffective, oneOf(m)).withBlastResistance(30) + return newBreakInfo(0.5, pickaxeHarvestable, pickaxeEffective, oneOf(m)) } // EncodeItem ... diff --git a/server/block/model/bamboo.go b/server/block/model/bamboo.go new file mode 100644 index 0000000000..073f50f82f --- /dev/null +++ b/server/block/model/bamboo.go @@ -0,0 +1,27 @@ +package model + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +// Bamboo is a model used by bamboo. +type Bamboo struct { + Thick bool +} + +// BBox ... +func (b Bamboo) BBox(pos cube.Pos, s world.BlockSource) []cube.BBox { + // The stalk's box extends from the block's centre towards positive X and Z. + size := 0.5 + 2.0/16.0 + if b.Thick { + size = 0.5 + 3.0/16.0 + } + offset := randomOffset(pos, -0.25, 0.25, 16) + return []cube.BBox{cube.Box(0.5, 0, 0.5, size, 1, size).Translate(offset)} +} + +// FaceSolid ... +func (b Bamboo) FaceSolid(pos cube.Pos, face cube.Face, s world.BlockSource) bool { + return false +} diff --git a/server/block/model/end_portal_frame.go b/server/block/model/end_portal_frame.go new file mode 100644 index 0000000000..c6e4c160a6 --- /dev/null +++ b/server/block/model/end_portal_frame.go @@ -0,0 +1,19 @@ +package model + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +// EndPortalFrame is the model of a 13/16 tall, full-width end portal frame. +type EndPortalFrame struct{} + +// BBox ... +func (EndPortalFrame) BBox(cube.Pos, world.BlockSource) []cube.BBox { + return []cube.BBox{cube.Box(0, 0, 0, 1, 0.8125, 1)} +} + +// FaceSolid returns true only for the down face. +func (EndPortalFrame) FaceSolid(_ cube.Pos, face cube.Face, _ world.BlockSource) bool { + return face == cube.FaceDown +} diff --git a/server/block/model/offset.go b/server/block/model/offset.go new file mode 100644 index 0000000000..75f998d150 --- /dev/null +++ b/server/block/model/offset.go @@ -0,0 +1,51 @@ +package model + +import ( + "math" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/internal/mcrandom" + "github.com/go-gl/mathgl/mgl64" +) + +// randomOffset returns the deterministic horizontal offset vanilla applies to +// the models of certain blocks, such as bamboo, at the position passed. +func randomOffset(pos cube.Pos, mn, mx float32, steps int) mgl64.Vec3 { + seed := offsetSeed(int32(pos.X()), int32(pos.Z())) + s0 := mcrandom.MixStafford13(seed) + s1 := mcrandom.MixStafford13(seed + 0x9E3779B97F4A7C15) + prng := mcrandom.NewXoroshiro128PlusPlus(s0, s1) + x := offsetValue(mn, mx, steps, randomFloat32(prng.Next())) + prng.Next() // The Y offset is always zero, but its draw is still consumed. + z := offsetValue(mn, mx, steps, randomFloat32(prng.Next())) + return mgl64.Vec3{float64(x), 0, float64(z)} +} + +// offsetSeed returns the seed vanilla uses for random model offsets. Both coordinate +// products use signed 64-bit arithmetic. The signed 32-bit extraction after the +// nonlinear mix is intentional. +func offsetSeed(x, z int32) uint64 { + v := int64(z)*116129781 ^ int64(x)*0x2fc20f + v = int64(int32(uint64(v*(v*42317861+11)) >> 16)) + return uint64(v) ^ 0x6A09E667F3BCC909 +} + +// randomFloat32 converts a random uint64 to a float in [0, 1). +func randomFloat32(random uint64) float32 { + return float32(random>>40) * (1.0 / 16777216.0) +} + +// offsetValue quantizes a random float to one of steps values in [mn, mx]. +func offsetValue(mn, mx float32, steps int, random float32) float32 { + if mn >= mx { + return mn + } + if steps == 1 { + return (mn + mx) * 0.5 + } + if steps > 1 { + index := float32(math.Floor(float64(float32(steps) * random))) + return mn + index*(mx-mn)/float32(steps-1) + } + return mn + (mx-mn)*random +} diff --git a/server/block/model/portal.go b/server/block/model/portal.go new file mode 100644 index 0000000000..a0007aba43 --- /dev/null +++ b/server/block/model/portal.go @@ -0,0 +1,22 @@ +package model + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +// Portal is a model used by portal blocks. +type Portal struct { + // Axis is the axis that the portal faces. + Axis cube.Axis +} + +// BBox ... +func (Portal) BBox(cube.Pos, world.BlockSource) []cube.BBox { + return nil +} + +// FaceSolid ... +func (Portal) FaceSolid(cube.Pos, cube.Face, world.BlockSource) bool { + return false +} diff --git a/server/block/model/shulker.go b/server/block/model/shulker.go new file mode 100644 index 0000000000..428f4b1674 --- /dev/null +++ b/server/block/model/shulker.go @@ -0,0 +1,35 @@ +package model + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +// Shulker is the model of a shulker box. The bounding box grows along the +// facing axis as the lid opens. +type Shulker struct { + // Facing is the direction that the lid opens towards. + Facing cube.Face + // Progress is the lid animation progress, ranging from 0 (closed) to 10 (fully open). + Progress int32 +} + +// BBox returns a single bounding box that extends outward along Facing as the +// lid opens. +func (s Shulker) BBox(cube.Pos, world.BlockSource) []cube.BBox { + peak := ShulkerPhysicalPeak(s.Progress) + return []cube.BBox{full.ExtendTowards(s.Facing, peak)} +} + +// ShulkerPhysicalPeak returns the lid extension along the facing axis for a +// given Progress in [0, 10]. The curve eases out cubically so the lid moves +// quickly and settles. +func ShulkerPhysicalPeak(progress int32) float64 { + t := float64(progress) / 10.0 + return (1.0 - (1.0-t)*(1.0-t)*(1.0-t)) * 0.5 +} + +// FaceSolid always returns false. +func (Shulker) FaceSolid(cube.Pos, cube.Face, world.BlockSource) bool { + return false +} diff --git a/server/block/mud.go b/server/block/mud.go index d37034f8c1..7d8463f9ea 100644 --- a/server/block/mud.go +++ b/server/block/mud.go @@ -10,7 +10,7 @@ type Mud struct { // SoilFor ... func (Mud) SoilFor(block world.Block) bool { switch block.(type) { - case ShortGrass, Fern, DoubleTallGrass, Flower, DoubleFlower, NetherSprouts, PinkPetals, DeadBush, Azalea: + case ShortGrass, Fern, DoubleTallGrass, Flower, DoubleFlower, NetherSprouts, PinkPetals, DeadBush, Azalea, BambooSapling, Bamboo: return true } return false diff --git a/server/block/mud_bricks.go b/server/block/mud_bricks.go index 1acf700c71..9e172c35aa 100644 --- a/server/block/mud_bricks.go +++ b/server/block/mud_bricks.go @@ -8,7 +8,7 @@ type MudBricks struct { // BreakInfo ... func (m MudBricks) BreakInfo() BreakInfo { - return newBreakInfo(1.5, alwaysHarvestable, nothingEffective, oneOf(m)).withBlastResistance(15) + return newBreakInfo(1.5, alwaysHarvestable, nothingEffective, oneOf(m)).withBlastResistance(3) } // EncodeItem ... diff --git a/server/block/muddy_mangrove_roots.go b/server/block/muddy_mangrove_roots.go index 462e65d8c2..19d180a2ee 100644 --- a/server/block/muddy_mangrove_roots.go +++ b/server/block/muddy_mangrove_roots.go @@ -23,7 +23,7 @@ func (m MuddyMangroveRoots) BreakInfo() BreakInfo { // SoilFor ... func (MuddyMangroveRoots) SoilFor(block world.Block) bool { switch block.(type) { - case ShortGrass, Fern, DoubleTallGrass, Flower, DoubleFlower, NetherSprouts, PinkPetals, Azalea: + case ShortGrass, Fern, DoubleTallGrass, Flower, DoubleFlower, NetherSprouts, PinkPetals, Azalea, BambooSapling, Bamboo: return true } return false diff --git a/server/block/nether_brick_fence.go b/server/block/nether_brick_fence.go index 841b1cb396..8761a66535 100644 --- a/server/block/nether_brick_fence.go +++ b/server/block/nether_brick_fence.go @@ -14,7 +14,7 @@ type NetherBrickFence struct { // BreakInfo ... func (n NetherBrickFence) BreakInfo() BreakInfo { - return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(n)).withBlastResistance(30) + return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(n)).withBlastResistance(6) } // SideClosed ... diff --git a/server/block/nether_bricks.go b/server/block/nether_bricks.go index 3b12f64238..f81c48f10e 100644 --- a/server/block/nether_bricks.go +++ b/server/block/nether_bricks.go @@ -17,7 +17,7 @@ type NetherBricks struct { // BreakInfo ... func (n NetherBricks) BreakInfo() BreakInfo { - return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(n)).withBlastResistance(30) + return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(n)).withBlastResistance(6) } // SmeltInfo ... diff --git a/server/block/netherite.go b/server/block/netherite.go index 307bc2263f..f10c723a35 100644 --- a/server/block/netherite.go +++ b/server/block/netherite.go @@ -14,7 +14,7 @@ type Netherite struct { func (n Netherite) BreakInfo() BreakInfo { return newBreakInfo(50, func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierDiamond.HarvestLevel - }, pickaxeEffective, oneOf(n)).withBlastResistance(6000) + }, pickaxeEffective, oneOf(n)).withBlastResistance(1200) } // PowersBeacon ... diff --git a/server/block/note.go b/server/block/note.go index 719d9cd52d..a4095c42b4 100644 --- a/server/block/note.go +++ b/server/block/note.go @@ -22,13 +22,13 @@ type Note struct { Powered bool } -// playNote ... +// playNote emits the configured note sound and particle at pos. func (n Note) playNote(pos cube.Pos, tx *world.Tx) { tx.PlaySound(pos.Vec3(), sound.Note{Instrument: n.instrument(pos, tx), Pitch: n.Pitch}) tx.AddParticle(pos.Vec3(), particle.Note{Instrument: n.Instrument(), Pitch: n.Pitch}) } -// updateInstrument ... +// instrument returns the note block instrument selected by the block below it. func (n Note) instrument(pos cube.Pos, tx *world.Tx) sound.Instrument { if instrumentBlock, ok := tx.Block(pos.Side(cube.FaceDown)).(interface { Instrument() sound.Instrument @@ -38,19 +38,16 @@ func (n Note) instrument(pos cube.Pos, tx *world.Tx) sound.Instrument { return sound.Piano() } -// DecodeNBT ... func (n Note) DecodeNBT(data map[string]any) any { n.Pitch = int(nbtconv.Uint8(data, "note")) n.Powered = nbtconv.Bool(data, "powered") return n } -// EncodeNBT ... func (n Note) EncodeNBT() map[string]any { return map[string]any{"note": byte(n.Pitch), "powered": boolByte(n.Powered)} } -// Activate tunes and plays the note block when there is room above it. func (n Note) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ *item.UseContext) bool { if !n.canPlay(pos, tx) { return false @@ -61,19 +58,24 @@ func (n Note) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ * return true } -// RedstoneUpdate updates the note block's powered state and plays the note when it first becomes powered. -func (n Note) RedstoneUpdate(pos cube.Pos, tx *world.Tx) { - poweredFaces := n.poweredFaces(pos, tx) - powered := len(poweredFaces) > 0 +// RedstonePowerUpdate records power changes; sound is deferred to post-update so cancellation can suppress it. +func (n Note) RedstonePowerUpdate(pos cube.Pos, tx *world.Tx, power int) (world.Block, bool) { + powered := power > 0 if powered == n.Powered { - return + return n, false } n.Powered = powered - if powered && n.canPlay(pos, tx) { - n.playNote(pos, tx) + return n, true +} + +// RedstonePowerPostUpdate plays the note after an uncancelled rising redstone edge. +func (n Note) RedstonePowerPostUpdate(pos cube.Pos, tx *world.Tx, before, after world.Block, _, _ int) { + beforeNote, beforeOK := before.(Note) + afterNote, afterOK := after.(Note) + if !beforeOK || !afterOK || beforeNote.Powered || !afterNote.Powered || !afterNote.canPlay(pos, tx) { + return } - tx.SetBlock(pos, n, &world.SetOpts{DisableBlockUpdates: true}) - updateAroundRedstone(pos, tx, poweredFaces...) + afterNote.playNote(pos, tx) } // canPlay reports whether the block above the note block is air. @@ -82,33 +84,18 @@ func (n Note) canPlay(pos cube.Pos, tx *world.Tx) bool { return ok } -func (n Note) poweredFaces(pos cube.Pos, tx *world.Tx) []cube.Face { - var faces []cube.Face - for _, face := range cube.Faces() { - adjacentPos := pos.Side(face) - if power := tx.RedstonePower(adjacentPos, face, true); power > 0 { - faces = append(faces, face) - } - } - return faces -} - -// BreakInfo ... func (n Note) BreakInfo() BreakInfo { return newBreakInfo(0.8, alwaysHarvestable, axeEffective, oneOf(Note{})) } -// FuelInfo ... func (Note) FuelInfo() item.FuelInfo { return newFuelInfo(time.Second * 15) } -// EncodeItem ... func (n Note) EncodeItem() (name string, meta int16) { return "minecraft:noteblock", 0 } -// EncodeBlock ... func (n Note) EncodeBlock() (name string, properties map[string]any) { return "minecraft:noteblock", nil } diff --git a/server/block/obsidian.go b/server/block/obsidian.go index 5d99bd4a0d..39ad975d83 100644 --- a/server/block/obsidian.go +++ b/server/block/obsidian.go @@ -2,6 +2,7 @@ package block import ( "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" ) // Obsidian is a dark purple block known for its high blast resistance and strength, most commonly found when @@ -37,9 +38,20 @@ func (o Obsidian) EncodeBlock() (string, map[string]any) { return "minecraft:obsidian", nil } +// Frame returns true if the block can form part of a nether portal frame in the dimension passed. Crying obsidian +// cannot be used as a frame block. +func (o Obsidian) Frame(dimension world.Dimension) bool { + return dimension == world.Nether && !o.Crying +} + +// SupportsEndCrystal returns whether an End crystal may be placed on the obsidian. +func (o Obsidian) SupportsEndCrystal() bool { + return !o.Crying +} + // BreakInfo ... func (o Obsidian) BreakInfo() BreakInfo { return newBreakInfo(35, func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierDiamond.HarvestLevel - }, pickaxeEffective, oneOf(o)).withBlastResistance(6000) + }, pickaxeEffective, oneOf(o)).withBlastResistance(1200) } diff --git a/server/block/packed_mud.go b/server/block/packed_mud.go index e3f388763a..acff516eb8 100644 --- a/server/block/packed_mud.go +++ b/server/block/packed_mud.go @@ -7,7 +7,7 @@ type PackedMud struct { // BreakInfo ... func (p PackedMud) BreakInfo() BreakInfo { - return newBreakInfo(1, alwaysHarvestable, nothingEffective, oneOf(p)).withBlastResistance(15) + return newBreakInfo(1, alwaysHarvestable, nothingEffective, oneOf(p)).withBlastResistance(3) } // EncodeItem ... diff --git a/server/block/pink_petals.go b/server/block/pink_petals.go index 3093836c08..f7ae78f56b 100644 --- a/server/block/pink_petals.go +++ b/server/block/pink_petals.go @@ -70,7 +70,7 @@ func (PinkPetals) HasLiquidDrops() bool { // BreakInfo ... func (p PinkPetals) BreakInfo() BreakInfo { - return newBreakInfo(0, alwaysHarvestable, nothingEffective, simpleDrops(item.NewStack(p, p.AdditionalCount+1))) + return newBreakInfo(0, alwaysHarvestable, nothingEffective, simpleDrops(item.NewStack(PinkPetals{}, p.AdditionalCount+1))) } // FlammabilityInfo ... diff --git a/server/block/planks.go b/server/block/planks.go index 8e08284c25..6afe7dfa26 100644 --- a/server/block/planks.go +++ b/server/block/planks.go @@ -26,7 +26,7 @@ func (p Planks) FlammabilityInfo() FlammabilityInfo { // BreakInfo ... func (p Planks) BreakInfo() BreakInfo { - return newBreakInfo(2, alwaysHarvestable, axeEffective, oneOf(p)).withBlastResistance(15) + return newBreakInfo(2, alwaysHarvestable, axeEffective, oneOf(p)).withBlastResistance(3) } // RepairsWoodTools ... diff --git a/server/block/podzol.go b/server/block/podzol.go index 0060733203..31975feb87 100644 --- a/server/block/podzol.go +++ b/server/block/podzol.go @@ -11,7 +11,7 @@ type Podzol struct { // SoilFor ... func (p Podzol) SoilFor(block world.Block) bool { switch block.(type) { - case ShortGrass, Fern, DoubleTallGrass, Flower, DoubleFlower, NetherSprouts, DeadBush, SugarCane, Azalea, Sapling: + case ShortGrass, Fern, DoubleTallGrass, Flower, DoubleFlower, NetherSprouts, DeadBush, SugarCane, Azalea, Sapling, BambooSapling, Bamboo: return true } return false diff --git a/server/block/polished_blackstone_brick.go b/server/block/polished_blackstone_brick.go index d75e67e414..dcbf90c9e4 100644 --- a/server/block/polished_blackstone_brick.go +++ b/server/block/polished_blackstone_brick.go @@ -13,7 +13,7 @@ type PolishedBlackstoneBrick struct { // BreakInfo ... func (b PolishedBlackstoneBrick) BreakInfo() BreakInfo { - return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(b)).withBlastResistance(30) + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(b)).withBlastResistance(6) } // SmeltInfo ... diff --git a/server/block/polished_cinnabar.go b/server/block/polished_cinnabar.go new file mode 100644 index 0000000000..a6cad03f54 --- /dev/null +++ b/server/block/polished_cinnabar.go @@ -0,0 +1,22 @@ +package block + +// PolishedCinnabar is a decorative variant of Cinnabar. +type PolishedCinnabar struct { + solid + bassDrum +} + +// BreakInfo ... +func (c PolishedCinnabar) BreakInfo() BreakInfo { + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(c)).withBlastResistance(6) +} + +// EncodeItem ... +func (PolishedCinnabar) EncodeItem() (name string, meta int16) { + return "minecraft:polished_cinnabar", 0 +} + +// EncodeBlock ... +func (PolishedCinnabar) EncodeBlock() (string, map[string]any) { + return "minecraft:polished_cinnabar", nil +} diff --git a/server/block/polished_sulfur.go b/server/block/polished_sulfur.go new file mode 100644 index 0000000000..5d83f2655c --- /dev/null +++ b/server/block/polished_sulfur.go @@ -0,0 +1,22 @@ +package block + +// PolishedSulfur is a decorative variant of Sulfur. +type PolishedSulfur struct { + solid + bassDrum +} + +// BreakInfo ... +func (s PolishedSulfur) BreakInfo() BreakInfo { + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(s)).withBlastResistance(6) +} + +// EncodeItem ... +func (PolishedSulfur) EncodeItem() (name string, meta int16) { + return "minecraft:polished_sulfur", 0 +} + +// EncodeBlock ... +func (PolishedSulfur) EncodeBlock() (string, map[string]any) { + return "minecraft:polished_sulfur", nil +} diff --git a/server/block/polished_tuff.go b/server/block/polished_tuff.go index 43480856e5..0a2243cfe6 100644 --- a/server/block/polished_tuff.go +++ b/server/block/polished_tuff.go @@ -10,7 +10,7 @@ type PolishedTuff struct { // BreakInfo ... func (t PolishedTuff) BreakInfo() BreakInfo { - return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(t)).withBlastResistance(30) + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(t)).withBlastResistance(6) } // EncodeItem ... diff --git a/server/block/portal.go b/server/block/portal.go new file mode 100644 index 0000000000..94aabbf899 --- /dev/null +++ b/server/block/portal.go @@ -0,0 +1,69 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/portal" +) + +// Portal is the translucent part of the nether portal that teleports the player to and from the Nether. +type Portal struct { + transparent + + // Axis is the axis which the portal faces. + Axis cube.Axis +} + +// portalTraveller represents an entity that can handle touching a portal block. +type portalTraveller interface { + TravelThroughPortal(tx *world.Tx, target world.Dimension) +} + +// Model ... +func (p Portal) Model() world.BlockModel { + return model.Portal{Axis: p.Axis} +} + +// Portal ... +func (Portal) Portal() world.Dimension { + return world.Nether +} + +// LightEmissionLevel returns 11. +func (Portal) LightEmissionLevel() uint8 { + return 11 +} + +// HasLiquidDrops ... +func (p Portal) HasLiquidDrops() bool { + return false +} + +// EncodeBlock ... +func (p Portal) EncodeBlock() (string, map[string]any) { + return "minecraft:portal", map[string]any{"portal_axis": p.Axis.String()} +} + +// NeighbourUpdateTick ... +func (p Portal) NeighbourUpdateTick(pos, neighbour cube.Pos, tx *world.Tx) { + face, ok := pos.NeighbourFace(neighbour) + if !ok { + return + } + axis := face.Axis() + if axis != cube.Y && axis != p.Axis { + return + } + if n, ok := portal.NetherPortalFromPos(tx, pos); ok && n.Framed() && n.Activated() { + return + } + portal.DeactivateNetherPortal(tx, pos) +} + +// EntityInside ... +func (p Portal) EntityInside(_ cube.Pos, tx *world.Tx, e world.Entity) { + if t, ok := e.(portalTraveller); ok { + t.TravelThroughPortal(tx, p.Portal()) + } +} diff --git a/server/block/prismarine.go b/server/block/prismarine.go index ea6da7a054..ceff7ae6db 100644 --- a/server/block/prismarine.go +++ b/server/block/prismarine.go @@ -15,7 +15,7 @@ type Prismarine struct { // BreakInfo ... func (p Prismarine) BreakInfo() BreakInfo { - return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(p)).withBlastResistance(30) + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(p)).withBlastResistance(6) } // EncodeItem ... diff --git a/server/block/purpur.go b/server/block/purpur.go index 0d1dafee61..c3139bb3db 100644 --- a/server/block/purpur.go +++ b/server/block/purpur.go @@ -25,7 +25,7 @@ type ( // BreakInfo ... func (p Purpur) BreakInfo() BreakInfo { - return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(p)).withBlastResistance(30) + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(p)).withBlastResistance(6) } // EncodeItem ... @@ -52,7 +52,7 @@ func (p PurpurPillar) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx // BreakInfo ... func (p PurpurPillar) BreakInfo() BreakInfo { - return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(p)).withBlastResistance(30) + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(p)).withBlastResistance(6) } // EncodeItem ... diff --git a/server/block/quartz.go b/server/block/quartz.go index 639db14cae..a560cb845c 100644 --- a/server/block/quartz.go +++ b/server/block/quartz.go @@ -45,7 +45,7 @@ func (q QuartzPillar) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx // BreakInfo ... func (q Quartz) BreakInfo() BreakInfo { if q.Smooth { - return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(q)).withBlastResistance(30) + return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(q)).withBlastResistance(6) } return newBreakInfo(0.8, pickaxeHarvestable, pickaxeEffective, oneOf(q)) } diff --git a/server/block/raw_copper.go b/server/block/raw_copper.go index 97fdb1c2df..e0fac2181c 100644 --- a/server/block/raw_copper.go +++ b/server/block/raw_copper.go @@ -14,7 +14,7 @@ type RawCopper struct { func (r RawCopper) BreakInfo() BreakInfo { return newBreakInfo(5, func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierStone.HarvestLevel - }, pickaxeEffective, oneOf(r)).withBlastResistance(30) + }, pickaxeEffective, oneOf(r)).withBlastResistance(6) } // EncodeItem ... diff --git a/server/block/raw_gold.go b/server/block/raw_gold.go index c47082bbde..e9c998b8e1 100644 --- a/server/block/raw_gold.go +++ b/server/block/raw_gold.go @@ -14,7 +14,7 @@ type RawGold struct { func (g RawGold) BreakInfo() BreakInfo { return newBreakInfo(5, func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierIron.HarvestLevel - }, pickaxeEffective, oneOf(g)).withBlastResistance(30) + }, pickaxeEffective, oneOf(g)).withBlastResistance(6) } // EncodeItem ... diff --git a/server/block/raw_iron.go b/server/block/raw_iron.go index 100919ee83..67a753773b 100644 --- a/server/block/raw_iron.go +++ b/server/block/raw_iron.go @@ -14,7 +14,7 @@ type RawIron struct { func (r RawIron) BreakInfo() BreakInfo { return newBreakInfo(5, func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierStone.HarvestLevel - }, pickaxeEffective, oneOf(r)).withBlastResistance(30) + }, pickaxeEffective, oneOf(r)).withBlastResistance(6) } // EncodeItem ... diff --git a/server/block/redstone_block.go b/server/block/redstone_block.go index 2841228426..6af40f0fd0 100644 --- a/server/block/redstone_block.go +++ b/server/block/redstone_block.go @@ -13,48 +13,35 @@ type RedstoneBlock struct { solid } -// BreakInfo ... func (r RedstoneBlock) BreakInfo() BreakInfo { - return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(r)).withBlastResistance(30).withBreakHandler(func(pos cube.Pos, tx *world.Tx, _ item.User) { - updateAroundRedstone(pos, tx) - }) + return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(r)).withBlastResistance(6) } -// EncodeItem ... func (r RedstoneBlock) EncodeItem() (name string, meta int16) { return "minecraft:redstone_block", 0 } -// EncodeBlock ... func (r RedstoneBlock) EncodeBlock() (string, map[string]any) { return "minecraft:redstone_block", nil } -// UseOnBlock ... func (r RedstoneBlock) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { pos, _, used := firstReplaceable(tx, pos, face, r) if !used { return false } place(tx, pos, r, user, ctx) - if placed(ctx) { - updateAroundRedstone(pos, tx) - return true - } - return false -} - -// RedstoneSource ... -func (r RedstoneBlock) RedstoneSource() bool { - return true + return placed(ctx) } -// WeakPower ... -func (r RedstoneBlock) WeakPower(_ cube.Pos, _ cube.Face, _ *world.Tx, _ bool) int { +func (RedstoneBlock) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { return 15 } -// StrongPower ... -func (r RedstoneBlock) StrongPower(_ cube.Pos, _ cube.Face, _ *world.Tx, _ bool) int { +// RedstoneStrongPower returns no strong power. Redstone blocks power adjacent components directly, but do not strongly +// power adjacent opaque blocks. +func (RedstoneBlock) RedstoneStrongPower(cube.Pos, *world.Tx, cube.Face) int { return 0 } + +func (RedstoneBlock) RedstoneNonConductive() {} diff --git a/server/block/redstone_ore.go b/server/block/redstone_ore.go index d027ae5703..fdaba140dd 100644 --- a/server/block/redstone_ore.go +++ b/server/block/redstone_ore.go @@ -111,7 +111,7 @@ func (r RedstoneOre) LightEmissionLevel() uint8 { func (r RedstoneOre) BreakInfo() BreakInfo { return newBreakInfo(r.Type.Hardness(), func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierIron.HarvestLevel - }, pickaxeEffective, discreteDrops(RedstoneWire{}, RedstoneOre{Type: r.Type}, 4, 5, 8)).withXPDropRange(1, 5).withBlastResistance(15) + }, pickaxeEffective, discreteDrops(RedstoneWire{}, RedstoneOre{Type: r.Type}, 4, 5, 8)).withXPDropRange(1, 5).withBlastResistance(3) } // SmeltInfo ... diff --git a/server/block/redstone_test.go b/server/block/redstone_test.go new file mode 100644 index 0000000000..8b5ebbe2fa --- /dev/null +++ b/server/block/redstone_test.go @@ -0,0 +1,1363 @@ +package block + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" +) + +func runWorld(w *world.World, f func(*world.Tx)) { + w.Do(f).Wait(context.Background()) +} + +func TestRedstoneWirePowersBlockBelowButNotAbove(t *testing.T) { + wire := RedstoneWire{Power: 15} + pos := cube.Pos{0, 64, 0} + + tests := []struct { + name string + face cube.Face + want int + }{ + {name: "top", face: cube.FaceUp}, + {name: "bottom", face: cube.FaceDown, want: 15}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if power := wire.RedstonePower(pos, nil, test.face); power != test.want { + t.Fatalf("power from %s face = %d, want %d", test.face, power, test.want) + } + }) + } +} + +func TestRedstoneWireVerticalTravel(t *testing.T) { + tests := []struct { + name string + upperSupport world.Block + fromHigh bool + want bool + }{ + {name: "up glowstone", upperSupport: Glowstone{}, want: true}, + {name: "down glowstone", upperSupport: Glowstone{}, fromHigh: true}, + {name: "down glass", upperSupport: Glass{}, fromHigh: true, want: true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + + low, high := cube.Pos{1, 64, 0}, cube.Pos{0, 65, 0} + var neighbours []cube.Pos + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(low.Side(cube.FaceDown), Stone{}, nil) + tx.SetBlock(high.Side(cube.FaceDown), test.upperSupport, nil) + tx.SetBlock(low, RedstoneWire{}, nil) + tx.SetBlock(high, RedstoneWire{}, nil) + + from := low + if test.fromHigh { + from = high + } + neighbours = RedstoneWire{}.RedstoneRelayerNeighbours(from, tx) + }) + + to := high + if test.fromHigh { + to = low + } + if got := redstoneWireTestContains(neighbours, to); got != test.want { + t.Fatalf("neighbours = %v, contains %v = %t, want %t", neighbours, to, got, test.want) + } + }) + } +} + +func TestRedstoneWireBreaksWhenSupportRemoved(t *testing.T) { + w := world.Config{Synchronous: true, Entities: redstoneBreakDropTestEntityRegistry()}.New() + defer w.Close() + + wirePos := cube.Pos{0, 64, 0} + supportPos := wirePos.Side(cube.FaceDown) + var blockAfter world.Block + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(supportPos, Stone{}, nil) + tx.SetBlock(wirePos, RedstoneWire{}, nil) + tx.SetBlock(supportPos, nil, nil) + }) + w.AdvanceTick() + runWorld(w, func(tx *world.Tx) { + blockAfter = tx.Block(wirePos) + }) + + if _, ok := blockAfter.(Air); !ok { + t.Fatalf("redstone wire after support removal = %T, want Air", blockAfter) + } +} + +func TestRedstoneWireGlowstoneLadderDoesNotOscillateAfterNeighbourBlockUpdate(t *testing.T) { + for _, test := range []struct { + name string + updatePos cube.Pos + breaking bool + }{ + {name: "place adjacent top dust", updatePos: cube.Pos{0, 67, -1}}, + {name: "place adjacent support", updatePos: cube.Pos{0, 66, -1}}, + {name: "place diagonal top dust", updatePos: cube.Pos{1, 67, -1}}, + {name: "break adjacent top dust", updatePos: cube.Pos{0, 67, -1}, breaking: true}, + {name: "break adjacent support", updatePos: cube.Pos{0, 66, -1}, breaking: true}, + {name: "break diagonal top dust", updatePos: cube.Pos{1, 67, -1}, breaking: true}, + } { + t.Run(test.name, func(t *testing.T) { + w := world.Config{Dim: world.End, Synchronous: true}.New() + defer w.Close() + + viewer := &redstoneWireTestBlockUpdateViewer{} + loader := world.NewLoader(2, w, viewer) + defer func() { + runWorld(w, func(tx *world.Tx) { + loader.Close(tx) + }) + }() + + sourcePos := cube.Pos{2, 64, 0} + dustPositions := []cube.Pos{ + {1, 64, 0}, + {0, 65, 0}, + {1, 66, 0}, + {0, 67, 0}, + } + supportPositions := []cube.Pos{ + dustPositions[0].Side(cube.FaceDown), + {0, 64, 0}, + {1, 65, 0}, + {0, 66, 0}, + } + topDustPos := dustPositions[len(dustPositions)-1] + runWorld(w, func(tx *world.Tx) { + loader.Move(tx, mgl64.Vec3{0, 64, 0}) + loader.Load(tx, 16) + }) + redstoneWireTestSetBlockAndWait(t, w, sourcePos, RedstoneBlock{}) + for i, supportPos := range supportPositions { + if i == 0 { + redstoneWireTestSetBlockAndWait(t, w, supportPos, Stone{}) + } else { + redstoneWireTestSetBlockAndWait(t, w, supportPos, Glowstone{}) + } + } + for _, dustPos := range dustPositions { + redstoneWireTestSetBlockAndWait(t, w, dustPos, RedstoneWire{}) + } + if test.breaking { + redstoneWireTestSetBlockAndWait(t, w, test.updatePos, Stone{}) + } + + redstoneWireTestWaitFor(t, w, func(tx *world.Tx) bool { + wire, ok := tx.Block(topDustPos).(RedstoneWire) + return ok && wire.Power > 0 + }) + viewer.reset() + + var initialPower int + runWorld(w, func(tx *world.Tx) { + initialPower = tx.Block(topDustPos).(RedstoneWire).Power + if test.breaking { + tx.SetBlock(test.updatePos, nil, nil) + } else { + tx.SetBlock(test.updatePos, Stone{}, nil) + } + }) + + lastPower := initialPower + lastTick := int64(-1) + powerChanges := 0 + for range 12 { + lastTick = redstoneWireTestWaitNextTick(t, w, lastTick) + var power int + runWorld(w, func(tx *world.Tx) { + power = tx.Block(topDustPos).(RedstoneWire).Power + }) + if power != lastPower { + powerChanges++ + lastPower = power + } + } + if powerChanges != 0 { + t.Fatalf("top glowstone ladder dust power changed %d times after neighbour update; initial=%d final=%d updatePos=%v breaking=%t", powerChanges, initialPower, lastPower, test.updatePos, test.breaking) + } + if updates := viewer.blockUpdateCount(topDustPos); updates != 0 { + t.Fatalf("top glowstone ladder dust received %d block updates after neighbour update; initial=%d final=%d updatePos=%v breaking=%t", updates, initialPower, lastPower, test.updatePos, test.breaking) + } + }) + } +} + +func TestRedstoneTorchAttachmentPower(t *testing.T) { + tests := []struct { + name string + setup func(tx *world.Tx, attachmentPos cube.Pos) + want bool + }{ + { + name: "ignores non-conductive attachment", + setup: func(tx *world.Tx, attachmentPos cube.Pos) { + tx.SetBlock(attachmentPos, Glass{}, nil) + tx.SetBlock(attachmentPos.Side(cube.FaceNorth), RedstoneBlock{}, nil) + }, + }, + { + name: "powered conductive attachment", + setup: func(tx *world.Tx, attachmentPos cube.Pos) { + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(attachmentPos.Side(cube.FaceNorth), RedstoneWire{Power: 15}, nil) + tx.SetBlock(attachmentPos.Side(cube.FaceNorth).Side(cube.FaceDown), Stone{}, nil) + }, + want: true, + }, + { + name: "redstone block attachment", + setup: func(tx *world.Tx, attachmentPos cube.Pos) { + tx.SetBlock(attachmentPos, RedstoneBlock{}, nil) + }, + want: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + + torchPos := cube.Pos{1, 64, 0} + attachmentPos := torchPos.Side(cube.FaceWest) + var powered bool + runWorld(w, func(tx *world.Tx) { + test.setup(tx, attachmentPos) + + torch := RedstoneTorch{Facing: cube.FaceWest, Lit: true} + powered = torch.attachmentPowered(torchPos, tx) + }) + + if powered != test.want { + t.Fatalf("attachment powered = %t, want %t", powered, test.want) + } + }) + } +} + +func TestRedstoneTorchUnknownFacingDoesNotPowerAttachmentFace(t *testing.T) { + torch := RedstoneTorch{Facing: unknownFace, Lit: true} + pos := cube.Pos{1, 64, 0} + + tests := []struct { + name string + face cube.Face + want int + }{ + {name: "attachment", face: cube.FaceDown}, + {name: "top", face: cube.FaceUp, want: 15}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if power := torch.RedstonePower(pos, nil, test.face); power != test.want { + t.Fatalf("unknown-facing torch power from %s face = %d, want %d", test.face, power, test.want) + } + }) + } +} + +func TestRedstoneTorchUnknownFacingUsesBlockBelowAsAttachment(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + + torchPos := cube.Pos{1, 64, 0} + attachmentPos := torchPos.Side(cube.FaceDown) + inputPos := attachmentPos.Side(cube.FaceNorth) + var unpoweredAttachment, poweredAttachment, supported bool + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(torchPos, RedstoneTorch{Facing: unknownFace, Lit: true}, nil) + + torch := tx.Block(torchPos).(RedstoneTorch) + unpoweredAttachment = torch.attachmentPowered(torchPos, tx) + + tx.SetBlock(inputPos, RedstoneWire{Power: 15}, nil) + tx.SetBlock(inputPos.Side(cube.FaceDown), Stone{}, nil) + poweredAttachment = torch.attachmentPowered(torchPos, tx) + + torch.NeighbourUpdateTick(torchPos, attachmentPos, tx) + _, supported = tx.Block(torchPos).(RedstoneTorch) + }) + + if unpoweredAttachment { + t.Fatal("unknown-facing torch treated itself as a powered attachment") + } + if !poweredAttachment { + t.Fatal("unknown-facing torch did not use the block below as its powered attachment") + } + if !supported { + t.Fatal("unknown-facing torch broke instead of using the block below as support") + } +} + +func TestRedstoneBlockPowersAdjacentComponentsButNotThroughStone(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + + sourcePos := cube.Pos{0, 64, 0} + adjacentDustPos := sourcePos.Side(cube.FaceEast) + stonePos := sourcePos.Side(cube.FaceWest) + farDustPos := stonePos.Side(cube.FaceWest) + var adjacentPower, farPower int + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(sourcePos, RedstoneBlock{}, nil) + tx.SetBlock(adjacentDustPos, RedstoneWire{}, nil) + tx.SetBlock(adjacentDustPos.Side(cube.FaceDown), Stone{}, nil) + tx.SetBlock(stonePos, Stone{}, nil) + tx.SetBlock(farDustPos, RedstoneWire{}, nil) + tx.SetBlock(farDustPos.Side(cube.FaceDown), Stone{}, nil) + + adjacentPower = tx.RedstonePower(adjacentDustPos) + farPower = tx.RedstonePower(farDustPos) + }) + + if adjacentPower == 0 { + t.Fatal("redstone block did not power adjacent dust") + } + if farPower != 0 { + t.Fatalf("redstone block powered dust through stone with %d, want 0", farPower) + } +} + +func TestRedstoneBlockDoesNotPowerTorchThroughStone(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + + torchPos := cube.Pos{1, 64, 0} + attachmentPos := torchPos.Side(cube.FaceWest) + var powered bool + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(attachmentPos.Side(cube.FaceNorth), RedstoneBlock{}, nil) + + torch := RedstoneTorch{Facing: cube.FaceWest, Lit: true} + powered = torch.attachmentPowered(torchPos, tx) + }) + + if powered { + t.Fatal("torch attachment was powered through stone by redstone block") + } +} + +func TestLeverStrongPowersAttachedBlockFace(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + + leverPos := cube.Pos{1, 64, 0} + attachedPos := leverPos.Side(cube.FaceWest) + unattachedPos := leverPos.Side(cube.FaceEast) + var attachedPower, unattachedPower int + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(attachedPos, Stone{}, nil) + tx.SetBlock(unattachedPos, Stone{}, nil) + tx.SetBlock(leverPos, Lever{Powered: true, Facing: cube.FaceEast}, nil) + + attachedPower = tx.RedstoneStrongPower(attachedPos) + unattachedPower = tx.RedstoneStrongPower(unattachedPos) + }) + + if attachedPower != 15 { + t.Fatalf("attached block strong power = %d, want 15", attachedPower) + } + if unattachedPower != 0 { + t.Fatalf("unattached block strong power = %d, want 0", unattachedPower) + } +} + +func TestLeverBreaksWhenSupportRemoved(t *testing.T) { + w := world.Config{Synchronous: true, Entities: redstoneBreakDropTestEntityRegistry()}.New() + defer w.Close() + + leverPos := cube.Pos{1, 64, 0} + supportPos := leverPos.Side(cube.FaceWest) + var blockAfter world.Block + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(supportPos, Stone{}, nil) + tx.SetBlock(leverPos, Lever{Facing: cube.FaceEast}, nil) + tx.SetBlock(supportPos, nil, nil) + }) + w.AdvanceTick() + runWorld(w, func(tx *world.Tx) { + blockAfter = tx.Block(leverPos) + }) + + if _, ok := blockAfter.(Air); !ok { + t.Fatalf("lever after support removal = %T, want Air", blockAfter) + } +} + +func TestLeverUpdatesConsumerBehindAttachedBlock(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + loader := world.NewLoader(1, w, world.NopViewer{}) + runWorld(w, func(tx *world.Tx) { + loader.Load(tx, 1) + }) + defer func() { + runWorld(w, func(tx *world.Tx) { + loader.Close(tx) + }) + }() + + leverPos := cube.Pos{0, 64, 0} + attachmentPos := leverPos.Side(cube.FaceWest) + notePos := attachmentPos.Side(cube.FaceWest) + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(notePos, Note{}, nil) + tx.SetBlock(leverPos, Lever{Facing: cube.FaceEast}, nil) + }) + + redstoneWireTestSetBlockAndWait(t, w, leverPos, Lever{Powered: true, Facing: cube.FaceEast}) + + var powered bool + runWorld(w, func(tx *world.Tx) { + powered = tx.Block(notePos).(Note).Powered + }) + if !powered { + t.Fatal("lever did not update consumer behind its attached block") + } +} + +func TestNoteBlockPlaysOnRedstoneRisingEdgeOnly(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + + handler := &redstoneSoundTestHandler{} + w.Handle(handler) + + leverPos := cube.Pos{0, 64, 0} + attachmentPos := leverPos.Side(cube.FaceWest) + notePos := attachmentPos.Side(cube.FaceWest) + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(notePos, Note{}, nil) + tx.SetBlock(leverPos, Lever{Facing: cube.FaceEast}, nil) + }) + + redstoneWireTestSetBlockAndWait(t, w, leverPos, Lever{Powered: true, Facing: cube.FaceEast}) + redstoneWireTestSetBlockAndWait(t, w, leverPos, Lever{Powered: false, Facing: cube.FaceEast}) + + var powered bool + runWorld(w, func(tx *world.Tx) { + powered = tx.Block(notePos).(Note).Powered + }) + if handler.noteSounds != 1 { + t.Fatalf("note sounds after rising and falling edge = %d, want 1", handler.noteSounds) + } + if powered { + t.Fatal("note block stayed powered after falling edge") + } +} + +func TestTNTDoesNotConductRedstonePower(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + + sourcePos := cube.Pos{0, 64, 0} + tntPos := sourcePos.Side(cube.FaceEast) + dustPos := tntPos.Side(cube.FaceEast) + var power int + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(sourcePos, RedstoneWire{Power: 15}, nil) + tx.SetBlock(sourcePos.Side(cube.FaceDown), Stone{}, nil) + tx.SetBlock(tntPos, TNT{}, nil) + tx.SetBlock(dustPos, RedstoneWire{}, nil) + tx.SetBlock(dustPos.Side(cube.FaceDown), Stone{}, nil) + + power = tx.RedstonePower(dustPos) + }) + + if power != 0 { + t.Fatalf("redstone power conducted through TNT = %d, want 0", power) + } +} + +func TestTNTRedstoneEngineRisingEdgePrimes(t *testing.T) { + w := world.Config{Synchronous: true, Entities: redstoneTNTTestEntityRegistry()}.New() + defer w.Close() + + sourcePos := cube.Pos{0, 64, 0} + tntPos := sourcePos.Side(cube.FaceEast) + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(tntPos, TNT{}, nil) + tx.SetBlock(sourcePos, RedstoneBlock{}, nil) + }) + w.AdvanceTick() + + var blockAfter world.Block + entities := 0 + runWorld(w, func(tx *world.Tx) { + blockAfter = tx.Block(tntPos) + for range tx.Entities() { + entities++ + } + }) + if _, ok := blockAfter.(Air); !ok { + t.Fatalf("TNT after engine redstone update = %T, want Air", blockAfter) + } + if entities != 1 { + t.Fatalf("entities after engine redstone update = %d, want 1 primed TNT", entities) + } +} + +func TestTNTRedstonePowerAction(t *testing.T) { + tests := []struct { + name string + oldPower int + newPower int + wantAir bool + wantEntities int + }{ + {name: "rising edge primes", newPower: 15, wantAir: true, wantEntities: 1}, + {name: "falling edge ignored", oldPower: 15}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + w := world.Config{Synchronous: true, Entities: redstoneTNTTestEntityRegistry()}.New() + defer w.Close() + + pos := cube.Pos{1, 64, 0} + var blockAfter world.Block + entities := 0 + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(pos, TNT{}, nil) + + (TNT{}).RedstonePowerAction(pos, tx, test.oldPower, test.newPower) + blockAfter = tx.Block(pos) + for range tx.Entities() { + entities++ + } + }) + + _, air := blockAfter.(Air) + if air != test.wantAir { + t.Fatalf("block after TNT action = %T, air=%t, want air=%t", blockAfter, air, test.wantAir) + } + if entities != test.wantEntities { + t.Fatalf("entities after TNT action = %d, want %d", entities, test.wantEntities) + } + }) + } +} + +func TestRedstoneTorchBurnsOutAfterRapidSelfTriggeredTurnOffs(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + + torchPos := cube.Pos{1, 64, 0} + attachmentPos := torchPos.Side(cube.FaceWest) + inputPos := attachmentPos.Side(cube.FaceNorth) + var lit bool + var burnedOut, recoverable bool + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(torchPos, RedstoneTorch{Facing: cube.FaceWest, Lit: true}, nil) + + redstoneTorchBurnoutTestToggle(tx, torchPos, inputPos, nil, true) + + lit = tx.Block(torchPos).(RedstoneTorch).Lit + burnedOut, recoverable = tx.Redstone().Torch(torchPos).BurnoutStatus() + }) + + if lit { + t.Fatalf("redstone torch stayed lit after rapid turn-offs; burnedOut=%t recoverable=%t", burnedOut, recoverable) + } + if !burnedOut { + t.Fatal("redstone torch turned off without recording burnout") + } +} + +func TestRedstoneTorchExternalTurnOffsDoNotBurnOut(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + + torchPos := cube.Pos{1, 64, 0} + attachmentPos := torchPos.Side(cube.FaceWest) + inputPos := attachmentPos.Side(cube.FaceNorth) + var lit bool + var burnedOut bool + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(torchPos, RedstoneTorch{Facing: cube.FaceWest, Lit: true}, nil) + + redstoneTorchBurnoutTestToggle(tx, torchPos, inputPos, nil, false) + + lit = tx.Block(torchPos).(RedstoneTorch).Lit + burnedOut, _ = tx.Redstone().Torch(torchPos).BurnoutStatus() + }) + + if lit { + t.Fatal("redstone torch stayed lit after external power was applied") + } + if burnedOut { + t.Fatal("externally toggled redstone torch recorded burnout") + } +} + +func TestRedstoneTorchScheduledTickReloadsLiveState(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + + torchPos := cube.Pos{1, 64, 0} + attachmentPos := torchPos.Side(cube.FaceWest) + var lit bool + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(torchPos, RedstoneTorch{Facing: cube.FaceWest}, &world.SetOpts{DisableRedstoneUpdates: true}) + + stale := RedstoneTorch{Facing: cube.FaceWest, Lit: true} + stale.ScheduledTick(torchPos, tx, nil) + lit = tx.Block(torchPos).(RedstoneTorch).Lit + }) + + if !lit { + t.Fatal("redstone torch scheduled tick used stale receiver state instead of live block state") + } +} + +func TestBurnedOutRedstoneTorchRelightsWhenInputIsRemoved(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + loader := world.NewLoader(1, w, world.NopViewer{}) + runWorld(w, func(tx *world.Tx) { + loader.Load(tx, 1) + }) + defer func() { + runWorld(w, func(tx *world.Tx) { + loader.Close(tx) + }) + }() + + torchPos := cube.Pos{1, 64, 0} + attachmentPos := torchPos.Side(cube.FaceWest) + inputPos := attachmentPos.Side(cube.FaceNorth) + var lit bool + var burnedOutTick int64 + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(torchPos, RedstoneTorch{Facing: cube.FaceWest, Lit: true}, nil) + + redstoneTorchBurnoutTestToggle(tx, torchPos, inputPos, nil, true) + + burnedOutTick = tx.CurrentTick() + }) + redstoneWireTestWaitTick(t, w, burnedOutTick) + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(inputPos, nil, nil) + torch := tx.Block(torchPos).(RedstoneTorch) + torch.RedstonePowerActionUpdate(torchPos, tx, world.RedstoneUpdate{ChangedNeighbour: inputPos, HasChangedNeighbour: true, ChangedRedstoneRelevant: true}) + tx.Block(torchPos).(RedstoneTorch).ScheduledTick(torchPos, tx, nil) + lit = tx.Block(torchPos).(RedstoneTorch).Lit + }) + + if !lit { + t.Fatal("burned-out redstone torch did not relight after its input was removed") + } +} + +func TestBurnedOutRedstoneTorchRecoversFromExternalScheduledUpdate(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + loader := world.NewLoader(1, w, world.NopViewer{}) + runWorld(w, func(tx *world.Tx) { + loader.Load(tx, 1) + }) + defer func() { + runWorld(w, func(tx *world.Tx) { + loader.Close(tx) + }) + }() + + torchPos := cube.Pos{1, 64, 0} + attachmentPos := torchPos.Side(cube.FaceWest) + inputPos := attachmentPos.Side(cube.FaceNorth) + sourcePos := inputPos.Side(cube.FaceNorth) + var lit bool + var burnedOutTick int64 + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(torchPos, RedstoneTorch{Facing: cube.FaceWest, Lit: true}, nil) + + redstoneTorchBurnoutTestToggle(tx, torchPos, inputPos, nil, true) + + burnedOutTick = tx.CurrentTick() + }) + redstoneWireTestWaitTick(t, w, burnedOutTick) + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(inputPos, nil, nil) + torch := tx.Block(torchPos).(RedstoneTorch) + torch.RedstonePowerActionUpdate(torchPos, tx, world.RedstoneUpdate{ + ChangedNeighbour: inputPos, + HasChangedNeighbour: true, + ChangedRedstoneRelevant: true, + Source: sourcePos, + HasSource: true, + Cause: world.RedstoneUpdateCauseScheduledTick, + }) + tx.Block(torchPos).(RedstoneTorch).ScheduledTick(torchPos, tx, nil) + lit = tx.Block(torchPos).(RedstoneTorch).Lit + }) + + if !lit { + t.Fatal("burned-out redstone torch did not recover from an external scheduled update") + } +} + +func TestBurnedOutRedstoneTorchDoesNotRecoverFromInputWirePowerDrop(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + + torchPos := cube.Pos{1, 64, 0} + attachmentPos := torchPos.Side(cube.FaceWest) + inputPos := attachmentPos.Side(cube.FaceNorth) + var lit bool + var burnedOut bool + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(torchPos, RedstoneTorch{Facing: cube.FaceWest, Lit: true}, nil) + + redstoneTorchBurnoutTestToggle(tx, torchPos, inputPos, RedstoneWire{}, true) + + tx.SetBlock(inputPos, RedstoneWire{}, nil) + torch := tx.Block(torchPos).(RedstoneTorch) + torch.RedstonePowerActionUpdate(torchPos, tx, world.RedstoneUpdate{ChangedNeighbour: inputPos, HasChangedNeighbour: true}) + tx.Block(torchPos).(RedstoneTorch).ScheduledTick(torchPos, tx, nil) + lit = tx.Block(torchPos).(RedstoneTorch).Lit + burnedOut, _ = tx.Redstone().Torch(torchPos).BurnoutStatus() + }) + + if lit || !burnedOut { + t.Fatalf("burned-out redstone torch recovered from wire power drop; lit=%t burnedOut=%t", lit, burnedOut) + } +} + +func TestBurnedOutRedstoneTorchRecoversFromZeroPositionInputUpdate(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + + torchPos := cube.Pos{1, 0, 0} + attachmentPos := cube.Pos{0, 0, 0} + inputPos := attachmentPos.Side(cube.FaceNorth) + var lit bool + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(torchPos, RedstoneTorch{Facing: cube.FaceWest, Lit: true}, nil) + + redstoneTorchBurnoutTestToggle(tx, torchPos, inputPos, nil, true) + + tx.SetBlock(inputPos, nil, nil) + torch := tx.Block(torchPos).(RedstoneTorch) + torch.RedstonePowerActionUpdate(torchPos, tx, world.RedstoneUpdate{ChangedNeighbour: attachmentPos, HasChangedNeighbour: true}) + tx.Block(torchPos).(RedstoneTorch).ScheduledTick(torchPos, tx, nil) + lit = tx.Block(torchPos).(RedstoneTorch).Lit + }) + + if !lit { + t.Fatal("burned-out redstone torch did not recover from a valid zero-position input update") + } +} + +func TestBurnedOutRedstoneTorchDoesNotRelightFromDisconnectedUpdate(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + loader := world.NewLoader(1, w, world.NopViewer{}) + runWorld(w, func(tx *world.Tx) { + loader.Load(tx, 1) + }) + defer func() { + runWorld(w, func(tx *world.Tx) { + loader.Close(tx) + }) + }() + + torchPos := cube.Pos{1, 64, 0} + attachmentPos := torchPos.Side(cube.FaceWest) + inputPos := attachmentPos.Side(cube.FaceNorth) + var lit, recoverable bool + var burnedOutTick int64 + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(torchPos, RedstoneTorch{Facing: cube.FaceWest, Lit: true}, nil) + + redstoneTorchBurnoutTestToggle(tx, torchPos, inputPos, nil, true) + + burnedOutTick = tx.CurrentTick() + }) + redstoneWireTestWaitTick(t, w, burnedOutTick) + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(inputPos, nil, &world.SetOpts{DisableBlockUpdates: true, DisableRedstoneUpdates: true}) + torch := tx.Block(torchPos).(RedstoneTorch) + torch.RedstonePowerActionUpdate(torchPos, tx, world.RedstoneUpdate{ChangedNeighbour: inputPos.Side(cube.FaceNorth).Side(cube.FaceNorth), HasChangedNeighbour: true}) + tx.Block(torchPos).(RedstoneTorch).ScheduledTick(torchPos, tx, nil) + lit = tx.Block(torchPos).(RedstoneTorch).Lit + _, recoverable = tx.Redstone().Torch(torchPos).BurnoutStatus() + }) + + if lit || recoverable { + t.Fatalf("burned-out redstone torch recovered from a disconnected update; lit=%t recoverable=%t", lit, recoverable) + } +} + +func TestBurnedOutRedstoneTorchDoesNotSelfRecoverWhenLoopUnpowersInput(t *testing.T) { + w := world.Config{Synchronous: true}.New() + defer w.Close() + + torchPos := cube.Pos{1, 64, 0} + attachmentPos := torchPos.Side(cube.FaceWest) + inputPos := attachmentPos.Side(cube.FaceNorth) + var lit bool + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(torchPos, RedstoneTorch{Facing: cube.FaceWest, Lit: true}, nil) + + redstoneTorchBurnoutTestToggle(tx, torchPos, inputPos, nil, true) + + tx.SetBlock(inputPos, nil, nil) + torch := tx.Block(torchPos).(RedstoneTorch) + torch.RedstonePowerActionUpdate(torchPos, tx, world.RedstoneUpdate{ChangedNeighbour: torchPos, HasChangedNeighbour: true, Cause: world.RedstoneUpdateCauseScheduledTick}) + tx.Block(torchPos).(RedstoneTorch).ScheduledTick(torchPos, tx, nil) + lit = tx.Block(torchPos).(RedstoneTorch).Lit + }) + + if lit { + t.Fatal("burned-out redstone torch self-recovered after its own loop unpowered the input") + } +} + +func redstoneWireTestWaitTick(t *testing.T, w *world.World, tick int64) { + t.Helper() + for range 200 { + w.AdvanceTick() + var current int64 + runWorld(w, func(tx *world.Tx) { + current = tx.CurrentTick() + }) + if current > tick { + return + } + } + t.Fatalf("world tick did not advance past %d", tick) +} + +func redstoneWireTestWaitNextTick(t *testing.T, w *world.World, tick int64) int64 { + t.Helper() + for range 200 { + w.AdvanceTick() + var current int64 + runWorld(w, func(tx *world.Tx) { + current = tx.CurrentTick() + }) + if current > tick { + return current + } + } + t.Fatalf("world tick did not advance past %d", tick) + return tick +} + +func redstoneWireTestWaitFor(t *testing.T, w *world.World, ready func(tx *world.Tx) bool) { + t.Helper() + for range 200 { + w.AdvanceTick() + done := false + runWorld(w, func(tx *world.Tx) { + done = ready(tx) + }) + if done { + return + } + } + t.Fatal("condition was not reached") +} + +func redstoneWireTestSetBlockAndWait(t *testing.T, w *world.World, pos cube.Pos, b world.Block) { + t.Helper() + var tick int64 + runWorld(w, func(tx *world.Tx) { + tick = tx.CurrentTick() + tx.SetBlock(pos, b, nil) + }) + redstoneWireTestWaitTick(t, w, tick) +} + +func redstoneWireTestContains(positions []cube.Pos, pos cube.Pos) bool { + for _, p := range positions { + if p == pos { + return true + } + } + return false +} + +type redstoneWireTestBlockUpdateViewer struct { + world.NopViewer + + mu sync.Mutex + updates map[cube.Pos]int +} + +func (v *redstoneWireTestBlockUpdateViewer) ViewBlockUpdate(pos cube.Pos, _ world.Block, _ int) { + v.mu.Lock() + defer v.mu.Unlock() + if v.updates == nil { + v.updates = make(map[cube.Pos]int) + } + v.updates[pos]++ +} + +func (v *redstoneWireTestBlockUpdateViewer) reset() { + v.mu.Lock() + defer v.mu.Unlock() + clear(v.updates) +} + +func (v *redstoneWireTestBlockUpdateViewer) blockUpdateCount(pos cube.Pos) int { + v.mu.Lock() + defer v.mu.Unlock() + return v.updates[pos] +} + +type redstoneSoundTestHandler struct { + world.NopHandler + + noteSounds int +} + +func (h *redstoneSoundTestHandler) HandleSound(_ *world.Context, s world.Sound, _ mgl64.Vec3) { + if _, ok := s.(sound.Note); ok { + h.noteSounds++ + } +} + +func redstoneTNTTestEntityRegistry() world.EntityRegistry { + return world.EntityRegistryConfig{ + TNT: func(opts world.EntitySpawnOpts, _ time.Duration) *world.EntityHandle { + return opts.New(redstoneTNTTestEntityType{}, redstoneTNTTestEntityConfig{}) + }, + }.New([]world.EntityType{redstoneTNTTestEntityType{}}) +} + +func redstoneBreakDropTestEntityRegistry() world.EntityRegistry { + return world.EntityRegistryConfig{ + Item: func(opts world.EntitySpawnOpts, _ any) *world.EntityHandle { + return opts.New(redstoneTNTTestEntityType{}, redstoneTNTTestEntityConfig{}) + }, + }.New([]world.EntityType{redstoneTNTTestEntityType{}}) +} + +type redstoneTNTTestEntityConfig struct{} + +func (redstoneTNTTestEntityConfig) Apply(*world.EntityData) {} + +type redstoneTNTTestEntityType struct{} + +func (redstoneTNTTestEntityType) Open(_ *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity { + return redstoneTNTTestEntity{handle: handle, data: data} +} + +func (redstoneTNTTestEntityType) EncodeEntity() string { return "test:tnt" } +func (redstoneTNTTestEntityType) BBox(world.Entity) cube.BBox { + return cube.Box(-0.49, 0, -0.49, 0.49, 0.98, 0.49) +} +func (redstoneTNTTestEntityType) DecodeNBT(map[string]any, *world.EntityData) {} +func (redstoneTNTTestEntityType) EncodeNBT(*world.EntityData) map[string]any { + return nil +} + +type redstoneTNTTestEntity struct { + handle *world.EntityHandle + data *world.EntityData +} + +func (e redstoneTNTTestEntity) Close() error { + return nil +} + +func (e redstoneTNTTestEntity) H() *world.EntityHandle { + return e.handle +} + +func (e redstoneTNTTestEntity) Position() mgl64.Vec3 { + return e.data.Pos +} + +func (e redstoneTNTTestEntity) Rotation() cube.Rotation { + return e.data.Rot +} +func TestRedstoneTorchLoopBurnsOutThroughWorldScheduler(t *testing.T) { + w := world.Config{Dim: world.End, Synchronous: true}.New() + defer w.Close() + + loader := world.NewLoader(2, w, world.NopViewer{}) + defer func() { + runWorld(w, func(tx *world.Tx) { + loader.Close(tx) + }) + }() + + torchPos := cube.Pos{1, 64, 0} + attachmentPos := cube.Pos{0, 64, 0} + dustPositions := []cube.Pos{ + {1, 66, 0}, + {0, 65, 0}, + } + runWorld(w, func(tx *world.Tx) { + loader.Move(tx, mgl64.Vec3{0, 64, 0}) + loader.Load(tx, 16) + + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(torchPos.Side(cube.FaceUp), Stone{}, nil) + for _, pos := range dustPositions { + tx.SetBlock(pos, RedstoneWire{}, nil) + } + tx.SetBlock(torchPos.Side(cube.FaceDown), Stone{}, nil) + tx.SetBlock(torchPos, RedstoneTorch{Facing: cube.FaceWest, Lit: true}, nil) + }) + + var lit, burnedOut, attachmentPowered bool + var currentTick, burnedOutTick int64 + dustPower := make(map[cube.Pos]int, len(dustPositions)) + + for range 200 { + w.AdvanceTick() + redstoneTorchBurnoutTestSnapshot(w, torchPos, dustPositions, ¤tTick, &lit, &burnedOut, &attachmentPowered, dustPower) + if burnedOut && !lit { + burnedOutTick = currentTick + break + } + } + if burnedOutTick == 0 { + t.Fatalf("redstone torch loop did not burn out through world scheduler; tick=%d lit=%t burnedOut=%t attachmentPowered=%t dust=%v", currentTick, lit, burnedOut, attachmentPowered, dustPower) + } + for currentTick < burnedOutTick+100 { + w.AdvanceTick() + redstoneTorchBurnoutTestSnapshot(w, torchPos, dustPositions, ¤tTick, &lit, &burnedOut, &attachmentPowered, dustPower) + if !burnedOut || lit { + t.Fatalf("redstone torch loop recovered without an external update; tick=%d burnedOutTick=%d lit=%t burnedOut=%t attachmentPowered=%t dust=%v", currentTick, burnedOutTick, lit, burnedOut, attachmentPowered, dustPower) + } + } +} + +func TestBurnedOutRedstoneTorchRelightsWhenLoopWireBreaks(t *testing.T) { + w := world.Config{Dim: world.End, Synchronous: true}.New() + defer w.Close() + + loader := world.NewLoader(2, w, world.NopViewer{}) + defer func() { + runWorld(w, func(tx *world.Tx) { + loader.Close(tx) + }) + }() + + torchPos := cube.Pos{1, 64, 0} + attachmentPos := cube.Pos{0, 64, 0} + loopWirePos := cube.Pos{0, 65, 0} + dustPositions := []cube.Pos{ + {1, 66, 0}, + loopWirePos, + } + runWorld(w, func(tx *world.Tx) { + loader.Move(tx, mgl64.Vec3{0, 64, 0}) + loader.Load(tx, 16) + + tx.SetBlock(attachmentPos, Stone{}, nil) + tx.SetBlock(torchPos.Side(cube.FaceUp), Stone{}, nil) + for _, pos := range dustPositions { + tx.SetBlock(pos, RedstoneWire{}, nil) + } + tx.SetBlock(torchPos.Side(cube.FaceDown), Stone{}, nil) + tx.SetBlock(torchPos, RedstoneTorch{Facing: cube.FaceWest, Lit: true}, nil) + }) + + var lit, burnedOut, attachmentPowered bool + var currentTick int64 + dustPower := make(map[cube.Pos]int, len(dustPositions)) + + redstoneTorchBurnoutTestWaitFor(t, w, func() bool { + redstoneTorchBurnoutTestSnapshot(w, torchPos, dustPositions, ¤tTick, &lit, &burnedOut, &attachmentPowered, dustPower) + return burnedOut && !lit + }, func() string { + return fmt.Sprintf("torch did not burn out before wire break; tick=%d lit=%t burnedOut=%t attachmentPowered=%t dust=%v", currentTick, lit, burnedOut, attachmentPowered, dustPower) + }) + runWorld(w, func(tx *world.Tx) { + tx.SetBlock(loopWirePos, nil, nil) + }) + + redstoneTorchBurnoutTestWaitFor(t, w, func() bool { + redstoneTorchBurnoutTestSnapshot(w, torchPos, dustPositions, ¤tTick, &lit, &burnedOut, &attachmentPowered, dustPower) + return lit && !burnedOut && !attachmentPowered + }, func() string { + return fmt.Sprintf("torch did not relight after loop wire broke; tick=%d lit=%t burnedOut=%t attachmentPowered=%t dust=%v", currentTick, lit, burnedOut, attachmentPowered, dustPower) + }) +} + +func TestBurnedOutRedstoneTorchRecoveryUpdates(t *testing.T) { + torchPos := cube.Pos{1, 64, 0} + attachmentPos := cube.Pos{0, 64, 0} + east := torchPos.Side(cube.FaceEast) + eastTwo := east.Side(cube.FaceEast) + eastThree := eastTwo.Side(cube.FaceEast) + unrelatedWire := cube.Pos{10, 64, 10} + + tests := []struct { + name string + dustPositions []cube.Pos + waitRecoverable bool + wantRecover bool + setup func(tx *world.Tx, opts *world.SetOpts) + update func(tx *world.Tx) + }{ + { + name: "TestBurnedOutRedstoneTorchRecoversFromAdjacentBlockUpdate", + waitRecoverable: true, + wantRecover: true, + update: func(tx *world.Tx) { + tx.SetBlock(east, Stone{}, nil) + }, + }, + { + name: "TestBurnedOutRedstoneTorchRecoversFromVerticalBlockUpdate/up", + waitRecoverable: true, + wantRecover: true, + update: func(tx *world.Tx) { + tx.SetBlock(torchPos.Side(cube.FaceUp), Stone{}, nil) + }, + }, + { + name: "TestBurnedOutRedstoneTorchRecoversFromVerticalBlockUpdate/down", + waitRecoverable: true, + wantRecover: true, + update: func(tx *world.Tx) { + tx.SetBlock(torchPos.Side(cube.FaceDown), Stone{}, nil) + }, + }, + { + name: "TestBurnedOutRedstoneTorchRecoversFromWireNeighbourUpdate", + dustPositions: []cube.Pos{east}, + waitRecoverable: true, + wantRecover: true, + setup: func(tx *world.Tx, opts *world.SetOpts) { + tx.SetBlock(east.Side(cube.FaceDown), Stone{}, opts) + tx.SetBlock(east, RedstoneWire{}, opts) + }, + update: func(tx *world.Tx) { + tx.SetBlock(east.Side(cube.FaceNorth), Stone{}, nil) + }, + }, + { + name: "TestBurnedOutRedstoneTorchDoesNotRecoverFromRedstoneDustPastAdjacentWire", + dustPositions: []cube.Pos{east, eastTwo}, + waitRecoverable: true, + setup: func(tx *world.Tx, opts *world.SetOpts) { + tx.SetBlock(east.Side(cube.FaceDown), Stone{}, opts) + tx.SetBlock(eastTwo.Side(cube.FaceDown), Stone{}, opts) + tx.SetBlock(east, RedstoneWire{}, opts) + }, + update: func(tx *world.Tx) { + tx.SetBlock(eastTwo, RedstoneWire{}, nil) + }, + }, + { + name: "TestBurnedOutRedstoneTorchDoesNotRecoverFromDistantPathWireNeighbourUpdate", + dustPositions: []cube.Pos{east, eastTwo, eastThree}, + waitRecoverable: true, + setup: func(tx *world.Tx, opts *world.SetOpts) { + for _, pos := range []cube.Pos{east, eastTwo, eastThree} { + tx.SetBlock(pos.Side(cube.FaceDown), Stone{}, opts) + tx.SetBlock(pos, RedstoneWire{}, opts) + } + }, + update: func(tx *world.Tx) { + tx.SetBlock(eastThree.Side(cube.FaceNorth), Stone{}, nil) + }, + }, + { + name: "TestBurnedOutRedstoneTorchDoesNotRecoverFromDistantWireUpdate", + dustPositions: []cube.Pos{east}, + waitRecoverable: true, + setup: func(tx *world.Tx, opts *world.SetOpts) { + tx.SetBlock(east.Side(cube.FaceDown), Stone{}, opts) + tx.SetBlock(east, RedstoneWire{}, opts) + }, + update: func(tx *world.Tx) { + tx.SetBlock(east.Side(cube.FaceNorth).Side(cube.FaceNorth), Stone{}, nil) + }, + }, + { + name: "TestBurnedOutRedstoneTorchDoesNotRecoverFromDistantPathWireBreak", + dustPositions: []cube.Pos{east, eastTwo, eastThree}, + setup: func(tx *world.Tx, opts *world.SetOpts) { + for _, pos := range []cube.Pos{east, eastTwo, eastThree} { + tx.SetBlock(pos.Side(cube.FaceDown), Stone{}, opts) + tx.SetBlock(pos, RedstoneWire{}, opts) + } + }, + update: func(tx *world.Tx) { + tx.SetBlock(eastThree, nil, nil) + }, + }, + { + name: "TestBurnedOutRedstoneTorchDoesNotRecoverFromUnrelatedWireBreak", + setup: func(tx *world.Tx, opts *world.SetOpts) { + tx.SetBlock(unrelatedWire.Side(cube.FaceDown), Stone{}, opts) + tx.SetBlock(unrelatedWire, RedstoneWire{}, opts) + }, + update: func(tx *world.Tx) { + tx.SetBlock(unrelatedWire, nil, nil) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + w := world.Config{Dim: world.End, Synchronous: true}.New() + defer w.Close() + + loader := world.NewLoader(2, w, world.NopViewer{}) + defer func() { + runWorld(w, func(tx *world.Tx) { + loader.Close(tx) + }) + }() + + runWorld(w, func(tx *world.Tx) { + loader.Move(tx, mgl64.Vec3{0, 64, 0}) + loader.Load(tx, 16) + + opts := &world.SetOpts{DisableBlockUpdates: true, DisableRedstoneUpdates: true} + tx.SetBlock(attachmentPos, Stone{}, opts) + if test.setup != nil { + test.setup(tx, opts) + } + tx.SetBlock(torchPos, RedstoneTorch{Facing: cube.FaceWest}, opts) + redstoneTorchBurnoutTestForceBurnedOut(tx, torchPos) + }) + + var lit, burnedOut, recoverable, attachmentPowered bool + var currentTick int64 + dustPower := make(map[cube.Pos]int, len(test.dustPositions)) + + if test.waitRecoverable { + redstoneTorchBurnoutTestWaitFor(t, w, func() bool { + runWorld(w, func(tx *world.Tx) { + currentTick = tx.CurrentTick() + burnedOut, recoverable = tx.Redstone().Torch(torchPos).BurnoutStatus() + }) + return burnedOut && recoverable + }, func() string { + return fmt.Sprintf("torch did not become recoverable; tick=%d burnedOut=%t recoverable=%t", currentTick, burnedOut, recoverable) + }) + } else { + runWorld(w, func(tx *world.Tx) { + currentTick = tx.CurrentTick() + burnedOut, recoverable = tx.Redstone().Torch(torchPos).BurnoutStatus() + }) + if !burnedOut || recoverable { + t.Fatalf("torch was not in immediate burnout window; tick=%d burnedOut=%t recoverable=%t", currentTick, burnedOut, recoverable) + } + } + + var updateTick int64 + runWorld(w, func(tx *world.Tx) { + updateTick = tx.CurrentTick() + test.update(tx) + }) + + if test.wantRecover { + redstoneTorchBurnoutTestWaitFor(t, w, func() bool { + redstoneTorchBurnoutTestSnapshot(w, torchPos, test.dustPositions, ¤tTick, &lit, &burnedOut, &attachmentPowered, dustPower) + return lit && !burnedOut && !attachmentPowered + }, func() string { + return fmt.Sprintf("torch did not recover; tick=%d lit=%t burnedOut=%t attachmentPowered=%t dust=%v", currentTick, lit, burnedOut, attachmentPowered, dustPower) + }) + return + } + + for currentTick <= updateTick+10 { + w.AdvanceTick() + redstoneTorchBurnoutTestSnapshot(w, torchPos, test.dustPositions, ¤tTick, &lit, &burnedOut, &attachmentPowered, dustPower) + if lit || !burnedOut { + t.Fatalf("torch recovered from non-local update; tick=%d updateTick=%d lit=%t burnedOut=%t attachmentPowered=%t dust=%v", currentTick, updateTick, lit, burnedOut, attachmentPowered, dustPower) + } + } + }) + } +} +func redstoneTorchBurnoutTestSnapshot(w *world.World, torchPos cube.Pos, dustPositions []cube.Pos, currentTick *int64, lit, burnedOut, attachmentPowered *bool, dustPower map[cube.Pos]int) { + runWorld(w, func(tx *world.Tx) { + *currentTick = tx.CurrentTick() + if torch, ok := tx.Block(torchPos).(RedstoneTorch); ok { + *lit = torch.Lit + *attachmentPowered = torch.attachmentPowered(torchPos, tx) + } + for _, pos := range dustPositions { + if wire, ok := tx.Block(pos).(RedstoneWire); ok { + if dustPower != nil { + dustPower[pos] = wire.Power + } + } else { + if dustPower != nil { + delete(dustPower, pos) + } + } + } + *burnedOut, _ = tx.Redstone().Torch(torchPos).BurnoutStatus() + }) +} + +func redstoneTorchBurnoutTestForceBurnedOut(tx *world.Tx, pos cube.Pos) { + for range 10 { + tx.Redstone().Torch(pos).MarkSelfTriggered() + tx.Redstone().Torch(pos).RecordTurnOff() + } +} + +func redstoneTorchBurnoutTestToggle(tx *world.Tx, torchPos, inputPos cube.Pos, unpoweredInput world.Block, selfTriggered bool) { + setPoweredInput := func() { + tx.SetBlock(inputPos, RedstoneWire{Power: 15}, nil) + tx.SetBlock(inputPos.Side(cube.FaceDown), Stone{}, nil) + } + tick := func() { + if selfTriggered { + tx.Redstone().Torch(torchPos).MarkSelfTriggered() + } + tx.Block(torchPos).(RedstoneTorch).ScheduledTick(torchPos, tx, nil) + } + for range 8 { + setPoweredInput() + tick() + tx.SetBlock(inputPos, unpoweredInput, nil) + tx.Block(torchPos).(RedstoneTorch).ScheduledTick(torchPos, tx, nil) + } + setPoweredInput() + tick() +} + +func redstoneTorchBurnoutTestWaitFor(t *testing.T, w *world.World, ready func() bool, fail func() string) { + t.Helper() + for range 200 { + w.AdvanceTick() + if ready() { + return + } + } + t.Fatal(fail()) +} diff --git a/server/block/redstone_torch.go b/server/block/redstone_torch.go index 6aef001820..7906e3ca51 100644 --- a/server/block/redstone_torch.go +++ b/server/block/redstone_torch.go @@ -2,7 +2,6 @@ package block import ( "math/rand/v2" - "time" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/item" @@ -11,6 +10,12 @@ import ( "github.com/go-gl/mathgl/mgl64" ) +var ( + _ world.RedstonePowerSource = RedstoneTorch{} + _ world.RedstonePowerContextAction = RedstoneTorch{} + _ world.ScheduledTicker = RedstoneTorch{} +) + // RedstoneTorch is a non-solid block that emits light and provides a full-strength redstone signal when lit. type RedstoneTorch struct { transparent @@ -22,12 +27,10 @@ type RedstoneTorch struct { Lit bool } -// HasLiquidDrops returns whether the redstone torch drops its item when flowing liquid breaks it. func (RedstoneTorch) HasLiquidDrops() bool { return true } -// LightEmissionLevel returns the light level emitted by the redstone torch (7 when lit, 0 when unlit). func (t RedstoneTorch) LightEmissionLevel() uint8 { if t.Lit { return 7 @@ -35,21 +38,22 @@ func (t RedstoneTorch) LightEmissionLevel() uint8 { return 0 } -// BreakInfo returns information about breaking the redstone torch. +func (t RedstoneTorch) facing() cube.Face { + if t.Facing == unknownFace { + return cube.FaceDown + } + return t.Facing +} + func (t RedstoneTorch) BreakInfo() BreakInfo { return newBreakInfo(0, alwaysHarvestable, nothingEffective, oneOf(t)).withBreakHandler(func(pos cube.Pos, tx *world.Tx, _ item.User) { - tx.Redstone().ClearTorchBurnout(pos) - updateTorchRedstone(pos, tx) + tx.Redstone().Torch(pos).ClearBurnout() }) } -// UseOnBlock handles the placement of a redstone torch on a block surface. func (t RedstoneTorch) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { pos, face, used := firstReplaceable(tx, pos, face, t) - if !used { - return false - } - if face == cube.FaceDown { + if !used || face == cube.FaceDown { return false } if _, ok := tx.Block(pos).(world.Liquid); ok { @@ -63,202 +67,208 @@ func (t RedstoneTorch) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx face = fallbackFace } t.Facing = face.Opposite() - t.Lit = true + t.Lit = !t.attachmentPowered(pos, tx) place(tx, pos, t, user, ctx) - if placed(ctx) { - // Initialise the freshly placed torch state before propagating its output. - t.RedstoneUpdate(pos, tx) - updateTorchRedstone(pos, tx) - return true + ok := placed(ctx) + if ok { + tx.ScheduleBlockUpdate(pos, t, redstoneTicks(1)) } - return false + return ok } -// NeighbourUpdateTick is called when a neighbouring block is updated. -func (t RedstoneTorch) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { - if !tx.Block(pos.Side(t.Facing)).Model().FaceSolid(pos.Side(t.Facing), t.Facing.Opposite(), tx) { - tx.Redstone().ClearTorchBurnout(pos) +// NeighbourUpdateTick breaks unsupported torches and otherwise schedules inverse-state refreshes. +func (t RedstoneTorch) NeighbourUpdateTick(pos, changed cube.Pos, tx *world.Tx) { + facing := t.facing() + if !tx.Block(pos.Side(facing)).Model().FaceSolid(pos.Side(facing), facing.Opposite(), tx) { + tx.Redstone().Torch(pos).ClearBurnout() breakBlock(t, pos, tx) return } - if t.recoverFromBurnout(pos, tx) { + torch := tx.Redstone().Torch(pos) + if burnedOut, recoverable := torch.BurnoutStatus(); burnedOut { + if t.recoverBurnout(pos, changed, tx, recoverable, false, t.attachmentPowered(pos, tx)) { + torch.ClearBurnout() + tx.ScheduleBlockUpdate(pos, t, redstoneTicks(1)) + } return } - updateRedstone(pos, tx) + tx.ScheduleBlockUpdate(pos, t, redstoneTicks(1)) } -// RedstoneUpdate is called when the redstone power state changes nearby. This method ignores burned-out torches and -// schedules state changes for active torches. -func (t RedstoneTorch) RedstoneUpdate(pos cube.Pos, tx *world.Tx) { - currentTick := tx.CurrentTick() - if burnedOut, _ := tx.Redstone().TorchBurnoutStatus(pos, currentTick); burnedOut { - if t.updateSourceTouchesInput(pos, tx) { - t.recoverFromBurnout(pos, tx) - } +// ScheduledTick refreshes the lit state after the torch's one-redstone-tick inversion delay. +func (t RedstoneTorch) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { + if tx == nil { return } - - shouldBeLit := t.inputStrength(pos, tx) == 0 - if shouldBeLit == t.Lit { + var ok bool + if t, ok = redstoneTorchAt(pos, tx); !ok { return } - tx.Redstone().MarkTorchSelfTriggeredIfActive(pos) - tx.ScheduleBlockUpdate(pos, t, time.Millisecond*100) + redstone := tx.Redstone() + torch := redstone.Torch(pos) + selfTriggered := torch.ConsumeSelfTriggered() + if burnedOut, _ := torch.BurnoutStatus(); burnedOut { + return + } + attachmentPowered := t.attachmentPowered(pos, tx) + lit := !attachmentPowered + if t.Lit != lit { + if !lit && selfTriggered && torch.RecordTurnOff() { + t.Lit = false + tx.PlaySound(pos.Vec3Centre(), sound.Fizz{}) + tx.SetBlock(pos, t, &world.SetOpts{DisableRedstoneUpdates: true}) + redstone.ScheduleUpdate(pos) + return + } + t.Lit = lit + tx.SetBlock(pos, t, &world.SetOpts{DisableRedstoneUpdates: true}) + redstone.ScheduleUpdate(pos) + } } -// recoverFromBurnout relights a burned-out torch after a real neighbouring block update once its rapid-toggle history -// has expired. Redstone propagation alone may visit calculation-only positions, so it must not recover burned-out -// torches that did not receive an actual neighbour update. -func (RedstoneTorch) recoverFromBurnout(pos cube.Pos, tx *world.Tx) bool { - torch, ok := redstoneTorchAt(pos, tx) +// redstoneTorchAt returns the live redstone torch at pos. Scheduled tick callers may carry stale block state, so +// mutation paths must reload the world block before writing torch state back. +func redstoneTorchAt(pos cube.Pos, tx *world.Tx) (RedstoneTorch, bool) { + t, ok := tx.Block(pos).(RedstoneTorch) if !ok { - return false + tx.Redstone().Torch(pos).ClearBurnout() } + return t, ok +} - currentTick := tx.CurrentTick() - burnedOut, recoverable := tx.Redstone().TorchBurnoutStatus(pos, currentTick) - if !burnedOut { - return false - } - if !recoverable { - return true +// RedstonePower emits power from every side except the attached block while lit. +func (t RedstoneTorch) RedstonePower(_ cube.Pos, _ *world.Tx, face cube.Face) int { + if t.Lit && face != t.facing() { + return 15 } - tx.Redstone().ClearTorchBurnout(pos) - - torch.Lit = torch.inputStrength(pos, tx) == 0 - tx.SetBlock(pos, torch, nil) - updateTorchRedstone(pos, tx) - return true + return 0 } -// updateSourceTouchesInput reports whether the current redstone update came from the block the torch is attached to, -// or from a block directly beside it. Dust on top of the attached block can legitimately recover a burnout loop, while -// disconnected dust visited by the broad wire walk should not. -func (t RedstoneTorch) updateSourceTouchesInput(pos cube.Pos, tx *world.Tx) bool { - source, ok := tx.Redstone().UpdateSource() - if !ok { - return false - } - inputPos := pos.Side(t.Facing) - if source == inputPos { - return true +// RedstoneStrongPower strongly powers the block above the torch while lit. +func (t RedstoneTorch) RedstoneStrongPower(pos cube.Pos, tx *world.Tx, face cube.Face) int { + if face == cube.FaceUp { + return t.RedstonePower(pos, tx, face) } - for _, face := range cube.Faces() { - if source == inputPos.Side(face) { - return true - } - } - return false + return 0 } -// ScheduledTick is called when a scheduled block update occurs. -// This method handles state changes and checks for burnout conditions. -func (RedstoneTorch) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { - torch, ok := redstoneTorchAt(pos, tx) - if !ok { +// RedstonePowerActionUpdate schedules torch refreshes and keeps burnout from self-recovering through its own loop. +func (t RedstoneTorch) RedstonePowerActionUpdate(pos cube.Pos, tx *world.Tx, update world.RedstoneUpdate) { + if tx == nil { return } - - currentTick := tx.CurrentTick() - if burnedOut, _ := tx.Redstone().TorchBurnoutStatus(pos, currentTick); burnedOut { + torch := tx.Redstone().Torch(pos) + if burnedOut, recoverable := torch.BurnoutStatus(); burnedOut { + attachmentPowered := update.NewPower > 0 && t.attachmentPowered(pos, tx) + if !update.HasChangedNeighbour || redstoneTorchSelfTriggered(pos, update) || !t.recoverBurnout(pos, update.ChangedNeighbour, tx, recoverable, update.ChangedRedstoneRelevant, attachmentPowered) { + return + } + torch.ClearBurnout() + tx.ScheduleBlockUpdate(pos, t, redstoneTicks(1)) return } - - shouldBeLit := torch.inputStrength(pos, tx) == 0 - if shouldBeLit == torch.Lit { - tx.Redstone().PruneTorchBurnout(pos, currentTick) + attachmentPowered := t.attachmentPowered(pos, tx) + if t.Lit == !attachmentPowered { return } - - if tx.Redstone().RecordTorchToggle(pos, currentTick) { - torch.burnOut(pos, tx) - return + if attachmentPowered && redstoneTorchSelfTriggered(pos, update) { + torch.MarkSelfTriggered() } - - torch.Lit = !torch.Lit - tx.SetBlock(pos, torch, nil) - updateTorchRedstone(pos, tx) + tx.ScheduleBlockUpdate(pos, t, redstoneTicks(1)) } -// burnOut puts the redstone torch into burnout state, turning it off and playing effects. -func (RedstoneTorch) burnOut(pos cube.Pos, tx *world.Tx) { - torch, ok := redstoneTorchAt(pos, tx) - if !ok { - return +// attachmentPowered reports whether the block the torch is attached to is powered. +func (t RedstoneTorch) attachmentPowered(pos cube.Pos, tx *world.Tx) bool { + if tx == nil { + return false } - - tx.Redstone().BurnOutTorch(pos) - torch.Lit = false - tx.PlaySound(pos.Vec3Centre(), sound.Fizz{}) - tx.SetBlock(pos, torch, nil) - updateTorchRedstone(pos, tx) -} - -// redstoneTorchAt returns the current torch at pos. Scheduled redstone updates carry an old block value, so mutation -// paths must reload the live world block before writing torch state back. -func redstoneTorchAt(pos cube.Pos, tx *world.Tx) (RedstoneTorch, bool) { - t, ok := tx.Block(pos).(RedstoneTorch) - if !ok { - tx.Redstone().ClearTorchBurnout(pos) + attached := pos.Side(t.facing()) + attachedBlock := tx.Block(attached) + if source, ok := attachedBlock.(world.RedstonePowerSource); ok { + for _, face := range cube.Faces() { + if world.ClampRedstonePower(source.RedstonePower(attached, tx, face)) > 0 { + return true + } + } } - return t, ok + return world.RedstoneFullPowerConductor(attached, attachedBlock, tx) && tx.RedstoneConductivePower(attached) > 0 } -// updateTorchRedstone updates receivers around the torch and behind the block it strongly powers above. -func updateTorchRedstone(pos cube.Pos, tx *world.Tx) { - tx.Redstone().WithActiveTorchUpdate(pos, func() { - updateDirectionalRedstone(pos, tx, cube.FaceUp) - }) -} - -// EncodeItem encodes the redstone torch as an item. -func (RedstoneTorch) EncodeItem() (name string, meta int16) { - return "minecraft:redstone_torch", 0 +// recoverBurnout reports whether an update should relight a burned-out torch. +func (t RedstoneTorch) recoverBurnout(pos, changed cube.Pos, tx *world.Tx, recoverable, changedRedstoneRelevant, attachmentPowered bool) bool { + if changed == pos || attachmentPowered { + return false + } + touchesRecoveryArea := t.changeTouchesRecoveryArea(pos, changed, tx) + if changedRedstoneRelevant { + return touchesRecoveryArea + } + if changed == pos.Side(t.facing()) { + return true + } + return recoverable && touchesRecoveryArea } -// EncodeBlock encodes the redstone torch as a block for network transmission. -func (t RedstoneTorch) EncodeBlock() (name string, properties map[string]any) { - face := "unknown" - if t.Facing != unknownFace { - face = t.Facing.String() - if t.Facing == cube.FaceDown { - face = "top" +// changeTouchesRecoveryArea reports whether changed is close enough to the torch, its input, or an output wire directly +// beside it to count as a real neighbour update. A dust line extending straight away from the torch is not local. +func (t RedstoneTorch) changeTouchesRecoveryArea(pos, changed cube.Pos, tx *world.Tx) bool { + inputPos := pos.Side(t.facing()) + if changed == inputPos { + return true + } + for _, face := range cube.Faces() { + if changed == pos.Side(face) || changed == inputPos.Side(face) { + return true } } - if t.Lit { - return "minecraft:redstone_torch", map[string]any{"torch_facing_direction": face} + for _, face := range cube.HorizontalFaces() { + neighbour := pos.Side(face) + if _, ok := tx.Block(neighbour).(RedstoneWire); !ok { + continue + } + for _, wireFace := range cube.HorizontalFaces() { + if wireFace == face || wireFace == face.Opposite() { + continue + } + if changed == neighbour.Side(wireFace) { + return true + } + } } - return "minecraft:unlit_redstone_torch", map[string]any{"torch_facing_direction": face} -} - -// RedstoneSource ... -func (t RedstoneTorch) RedstoneSource() bool { - return true + return false } -// WeakPower returns the weak redstone power level provided to adjacent blocks. -func (t RedstoneTorch) WeakPower(_ cube.Pos, face cube.Face, _ *world.Tx, _ bool) int { - if !t.Lit { - return 0 +// redstoneTorchSelfTriggered reports whether this update came from the torch's own scheduled output propagation. +func redstoneTorchSelfTriggered(pos cube.Pos, update world.RedstoneUpdate) bool { + if update.Cause != world.RedstoneUpdateCauseScheduledTick { + return false } - if face.Opposite() == t.Facing { - return 0 + if update.HasSource { + return update.Source == pos } - return 15 + return update.HasChangedNeighbour && update.ChangedNeighbour == pos } -// StrongPower returns the strong redstone power level provided to the block above the torch. -func (t RedstoneTorch) StrongPower(_ cube.Pos, face cube.Face, _ *world.Tx, _ bool) int { - if t.Lit && face == cube.FaceDown { - return 15 - } - return 0 +func (RedstoneTorch) EncodeItem() (name string, meta int16) { + return "minecraft:redstone_torch", 0 } -// inputStrength returns the redstone power level received by the block the torch is attached to. -func (t RedstoneTorch) inputStrength(pos cube.Pos, tx *world.Tx) int { - return tx.RedstonePower(pos.Side(t.Facing), t.Facing, true) +func (t RedstoneTorch) EncodeBlock() (name string, properties map[string]any) { + name = "minecraft:unlit_redstone_torch" + if t.Lit { + name = "minecraft:redstone_torch" + } + var direction string + switch t.Facing { + case cube.FaceDown: + direction = "top" + case unknownFace: + direction = "unknown" + default: + direction = t.Facing.String() + } + return name, map[string]any{"torch_facing_direction": direction} } // redstoneTorchFallbackSides lists the faces to check for placing a redstone torch on a non-solid block. @@ -271,7 +281,6 @@ var redstoneTorchFallbackSides = [...]cube.Face{ } // findTorchPlacementFace finds a valid face for placing a redstone torch on a non-solid block. -// It returns the face the torch should be placed on and whether it was found. func findTorchPlacementFace(pos cube.Pos, tx *world.Tx) (cube.Face, bool) { for _, side := range redstoneTorchFallbackSides { if tx.Block(pos.Side(side)).Model().FaceSolid(pos.Side(side), side.Opposite(), tx) { @@ -281,14 +290,12 @@ func findTorchPlacementFace(pos cube.Pos, tx *world.Tx) (cube.Face, bool) { return 0, false } -// allRedstoneTorches returns all possible redstone torch block states. func allRedstoneTorches() (all []world.Block) { - for _, f := range append(cube.Faces(), unknownFace) { - if f == cube.FaceUp { - continue + for _, face := range cube.Faces() { + if face == cube.FaceUp { + face = unknownFace } - all = append(all, RedstoneTorch{Facing: f, Lit: true}) - all = append(all, RedstoneTorch{Facing: f}) + all = append(all, RedstoneTorch{Facing: face}, RedstoneTorch{Facing: face, Lit: true}) } return } diff --git a/server/block/redstone_wire.go b/server/block/redstone_wire.go index 3c110dcc48..9a9337ff35 100644 --- a/server/block/redstone_wire.go +++ b/server/block/redstone_wire.go @@ -1,8 +1,9 @@ package block import ( + "time" + "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/block/model" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" @@ -20,254 +21,278 @@ type RedstoneWire struct { Power int } -// HasLiquidDrops ... -func (RedstoneWire) HasLiquidDrops() bool { - return true -} - -// BreakInfo ... -func (r RedstoneWire) BreakInfo() BreakInfo { - return newBreakInfo(0, alwaysHarvestable, nothingEffective, oneOf(RedstoneWire{})).withBreakHandler(func(pos cube.Pos, tx *world.Tx, _ item.User) { - updateStrongRedstone(pos, tx) - }) -} - -// EncodeBlock ... -func (r RedstoneWire) EncodeBlock() (string, map[string]any) { - return "minecraft:redstone_wire", map[string]any{ - "redstone_signal": int32(r.Power), - } -} - -// EncodeItem ... -func (RedstoneWire) EncodeItem() (name string, meta int16) { - return "minecraft:redstone", 0 -} - -// UseOnBlock ... func (r RedstoneWire) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { pos, _, used := firstReplaceable(tx, pos, face, r) - if !used { - return false - } - belowPos := pos.Side(cube.FaceDown) - if !tx.Block(belowPos).Model().FaceSolid(belowPos, cube.FaceUp, tx) { + if !used || !redstoneWireSupported(tx, pos) { return false } - r.Power = r.calculatePower(pos, tx) place(tx, pos, r, user, ctx) - if placed(ctx) { - updateStrongRedstone(pos, tx) - return true - } - return false + return placed(ctx) } -// NeighbourUpdateTick ... -func (r RedstoneWire) NeighbourUpdateTick(pos, neighbour cube.Pos, tx *world.Tx) { - if pos == neighbour { - // Ignore the self-update sent after this wire's block state changes. - return - } - below := pos.Side(cube.FaceDown) - if !tx.Block(below).Model().FaceSolid(below, cube.FaceUp, tx) { - breakBlock(r, pos, tx) - return +// RedstonePower returns the wire's current signal strength from connected faces. +func (r RedstoneWire) RedstonePower(pos cube.Pos, tx *world.Tx, face cube.Face) int { + if face == cube.FaceUp { + return 0 } - if changed, ok := r.updateFromNeighbour(pos, tx); ok && !changed { - updateStrongRedstone(pos, tx) + if tx != nil && redstoneWireFaceHorizontal(face) && !redstoneWirePowersHorizontalFace(pos, tx, face) { + return 0 } + return r.Power +} + +func (RedstoneWire) RedstoneWeaklyPowersBlocks() bool { + return true } -// RedstoneUpdate ... -func (r RedstoneWire) RedstoneUpdate(pos cube.Pos, tx *world.Tx) { - r.updatePower(pos, tx) +func (RedstoneWire) RedstoneSignalLoss(cube.Pos, *world.Tx) int { + return 1 } -// updateFromNeighbour updates the wire after a neighbour change. changed reports whether the wire's power changed, -// and ok reports whether the update was allowed by the redstone update handler. -func (r RedstoneWire) updateFromNeighbour(pos cube.Pos, tx *world.Tx) (changed bool, ok bool) { - if redstoneUpdateCancelled(pos, tx) { - return false, false +// RedstoneRelayerNeighbours returns all wire positions directly connected to this dust, including dust stepping up or +// down adjacent blocks. +func (RedstoneWire) RedstoneRelayerNeighbours(pos cube.Pos, tx *world.Tx) []cube.Pos { + neighbours := make([]cube.Pos, 0, 12) + faces := redstoneWirePoweredHorizontalFaces(pos, tx) + for _, face := range cube.HorizontalFaces() { + if !faces[face] { + continue + } + side := pos.Side(face) + if side.OutOfBounds(tx.Range()) { + continue + } + positions := redstoneWireHorizontalConnectionPositions(pos, tx, face) + if len(positions) != 0 { + neighbours = append(neighbours, positions...) + continue + } + if redstoneWireRelevantLoaded(tx, side) { + neighbours = append(neighbours, side) + } } - return r.updatePower(pos, tx), true + return neighbours } -// updatePower recalculates the wire's power and propagates the network when the power changes. -func (r RedstoneWire) updatePower(pos cube.Pos, tx *world.Tx) bool { - if power := r.calculatePower(pos, tx); r.Power != power { - r.Power = power - tx.SetBlock(pos, r, &world.SetOpts{DisableBlockUpdates: true}) - updateStrongRedstone(pos, tx) - return true +func (r RedstoneWire) RedstonePowerUpdate(_ cube.Pos, _ *world.Tx, power int) (world.Block, bool) { + power = world.ClampRedstonePower(power) + if r.Power == power { + return r, false } - return false + r.Power = power + return r, true } -// RedstoneSource ... -func (RedstoneWire) RedstoneSource() bool { - return false +func (r RedstoneWire) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + if !redstoneWireSupported(tx, pos) { + breakBlock(r, pos, tx) + } } -// WeaklyPowersBlocks returns true because powered redstone dust weakly powers conductive blocks it points into or rests on top of. -func (RedstoneWire) WeaklyPowersBlocks() bool { +func (RedstoneWire) HasLiquidDrops() bool { return true } -// WeakPower returns the power emitted by the wire toward a neighbouring receiver. Dust powers upward, never powers -// downward, and only powers horizontal receivers in connected directions. A powered wire with no horizontal -// connections behaves as an unconnected cross and powers every horizontal side. -func (r RedstoneWire) WeakPower(pos cube.Pos, face cube.Face, tx *world.Tx, accountForDust bool) int { - if !accountForDust { - return 0 - } - if face == cube.FaceUp { - return r.Power - } - if face == cube.FaceDown { - return 0 - } - if !r.hasHorizontalRedstoneConnection(pos, tx) { - return r.Power - } - if r.connection(pos, face.Opposite(), tx) { - return r.Power - } - if r.connection(pos, face, tx) && !r.connection(pos, face.RotateLeft(), tx) && !r.connection(pos, face.RotateRight(), tx) { - return r.Power - } - return 0 +func (r RedstoneWire) BreakInfo() BreakInfo { + return newBreakInfo(0, alwaysHarvestable, nothingEffective, oneOf(RedstoneWire{})) } -// StrongPower returns 0 because redstone dust weakly powers conductive blocks rather than strongly powering them. -func (RedstoneWire) StrongPower(cube.Pos, cube.Face, *world.Tx, bool) int { - return 0 +func (RedstoneWire) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { + return false } -// calculatePower returns the highest level of received redstone power at the provided position. -func (r RedstoneWire) calculatePower(pos cube.Pos, tx *world.Tx) int { - return calculateRedstoneWirePower(pos, tx, tx.Block) +func (r RedstoneWire) EncodeBlock() (string, map[string]any) { + return "minecraft:redstone_wire", map[string]any{"redstone_signal": int32(world.ClampRedstonePower(r.Power))} } -// calculateRedstoneWirePower returns the highest level of received redstone power at the provided position. blockAt is -// injected so direct updates and the BFS wire network share the same rules while the BFS path may still read cached -// node state. -func calculateRedstoneWirePower(pos cube.Pos, tx *world.Tx, blockAt func(cube.Pos) world.Block) int { - aboveBlocksVerticalTravel := blocksRedstoneWireVerticalTravel(blockAt(pos.Side(cube.FaceUp))) - var blockPower, wirePower int - for _, side := range cube.Faces() { - neighbourPos := pos.Side(side) - neighbour := blockAt(neighbourPos) - - wirePower = maxRedstoneWirePower(neighbour, wirePower) - blockPower = max(blockPower, tx.RedstonePower(neighbourPos, side, false)) - - if side.Axis() == cube.Y { - // Only check horizontal neighbours from here on. - continue - } +func (RedstoneWire) EncodeItem() (name string, meta int16) { + return "minecraft:redstone", 0 +} - if canRedstoneWireStepDown(pos, neighbourPos, neighbour, tx) && !aboveBlocksVerticalTravel { - wirePower = maxRedstoneWirePower(blockAt(neighbourPos.Side(cube.FaceUp)), wirePower) - } - if canRedstoneWireStepDown(neighbourPos.Side(cube.FaceDown), neighbourPos, neighbour, tx) && !blocksRedstoneWireVerticalTravel(neighbour) { - wirePower = maxRedstoneWirePower(blockAt(neighbourPos.Side(cube.FaceDown)), wirePower) - } +// TrimMaterial delegates to item.RedstoneWire so the block form stays valid for smithing trim decoding too. +func (RedstoneWire) TrimMaterial() string { + return item.RedstoneWire{}.TrimMaterial() +} - if _, neighbourSolid := neighbour.Model().(model.Solid); !neighbourSolid { - wirePower = maxRedstoneWirePower(blockAt(neighbourPos.Side(cube.FaceDown)), wirePower) - } - } - return max(blockPower, wirePower-1) +// MaterialColour delegates to item.RedstoneWire to keep trim metadata defined in one place. +func (RedstoneWire) MaterialColour() string { + return item.RedstoneWire{}.MaterialColour() } -// hasHorizontalRedstoneConnection checks if the dust connects horizontally to redstone wire or a redstone source. It -// does not include passive receivers such as doors, trapdoors, or note blocks. -func (r RedstoneWire) hasHorizontalRedstoneConnection(pos cube.Pos, tx *world.Tx) bool { - for _, face := range cube.HorizontalFaces() { - if r.connection(pos, face, tx) { - return true - } +func allRedstoneWires() (all []world.Block) { + for i := range 16 { + all = append(all, RedstoneWire{Power: i}) } - return false + return } -// connection returns true if the dust shape connects through the given face to another wire or a redstone source. It -// also accounts for valid one-block vertical wire connections. -func (r RedstoneWire) connection(pos cube.Pos, face cube.Face, tx *world.Tx) bool { - sidePos := pos.Side(face) - sideBlock := tx.Block(sidePos) - if r.connectsAbove(pos, sidePos, sideBlock, tx) || r.connectsTo(sideBlock, true) { - return true - } - return r.connectsBelow(sidePos, sideBlock, tx) +// redstoneTicks converts redstone ticks to a wall-clock duration at 10 redstone ticks per second. +func redstoneTicks(ticks int) time.Duration { + return time.Duration(max(ticks, 1)) * time.Second / 10 } -// connectsAbove checks if the redstone wire can connect to the block above it. -func (r RedstoneWire) connectsAbove(pos, sidePos cube.Pos, sideBlock world.Block, tx *world.Tx) bool { - if blocksRedstoneWireVerticalTravel(tx.Block(pos.Side(cube.FaceUp))) || !r.canRunOnTop(tx, sidePos, sideBlock) { +// redstoneWireSupported reports whether redstone wire can stay placed at pos. +func redstoneWireSupported(tx *world.Tx, pos cube.Pos) bool { + below := pos.Side(cube.FaceDown) + if below.OutOfBounds(tx.Range()) { return false } - return r.connectsTo(tx.Block(sidePos.Side(cube.FaceUp)), false) + return tx.Block(below).Model().FaceSolid(below, cube.FaceUp, tx) } -// connectsBelow checks if the redstone wire can connect to the block below it. -func (r RedstoneWire) connectsBelow(sidePos cube.Pos, sideBlock world.Block, tx *world.Tx) bool { - _, sideSolid := sideBlock.Model().(model.Solid) - return !sideSolid && r.connectsTo(tx.Block(sidePos.Side(cube.FaceDown)), false) +// redstoneWireSupportedLoaded checks support without loading neighbouring chunks. +func redstoneWireSupportedLoaded(tx *world.Tx, pos cube.Pos) bool { + below := pos.Side(cube.FaceDown) + if below.OutOfBounds(tx.Range()) { + return false + } + b, ok := tx.BlockLoaded(below) + return ok && b.Model().FaceSolid(below, cube.FaceUp, tx) } -// connectsTo reports whether a block is part of the redstone wire connection graph. Passive redstone receivers are not -// connections; direct source conductors count only when allowDirectSources is true. -func (RedstoneWire) connectsTo(block world.Block, allowDirectSources bool) bool { - if _, ok := block.(RedstoneWire); ok { +// redstoneWireBlocksConnectionLoaded reports whether a loaded block blocks wire connection through face. +func redstoneWireBlocksConnectionLoaded(tx *world.Tx, pos cube.Pos, face cube.Face) bool { + if pos.OutOfBounds(tx.Range()) { return true } - c, ok := block.(world.Conductor) - return ok && allowDirectSources && c.RedstoneSource() + b, ok := tx.BlockLoaded(pos) + return ok && b.Model().FaceSolid(pos, face, tx) && world.RedstoneFullPowerConductor(pos, b, tx) } -// canRunOnTop checks whether redstone dust can be placed on top of the block. -func (RedstoneWire) canRunOnTop(tx *world.Tx, pos cube.Pos, block world.Block) bool { - return block.Model().FaceSolid(pos, cube.FaceUp, tx) +// redstoneWirePowersHorizontalFace reports whether wire power is exposed through a horizontal face. +func redstoneWirePowersHorizontalFace(pos cube.Pos, tx *world.Tx, face cube.Face) bool { + return redstoneWirePoweredHorizontalFaces(pos, tx)[face] } -// blocksRedstoneWireVerticalTravel checks if the block above redstone wire blocks vertical wire travel. -func blocksRedstoneWireVerticalTravel(block world.Block) bool { - if _, ok := block.Model().(model.Solid); !ok { - return false +// redstoneWirePoweredHorizontalFaces returns the horizontal faces connected by the wire shape. +func redstoneWirePoweredHorizontalFaces(pos cube.Pos, tx *world.Tx) map[cube.Face]bool { + connections := make(map[cube.Face]bool, len(cube.HorizontalFaces())) + for _, face := range cube.HorizontalFaces() { + if len(redstoneWireHorizontalConnectionPositions(pos, tx, face)) != 0 { + connections[face] = true + } } - diffuser, ok := block.(LightDiffuser) - return !ok || diffuser.LightDiffusionLevel() != 0 + switch len(connections) { + case 0: + for _, face := range cube.HorizontalFaces() { + connections[face] = true + } + case 1: + for face := range connections { + connections[face.Opposite()] = true + } + } + return connections } -// canRedstoneWireStepDown checks if redstone dust can provide power while travelling down around the side block. -func canRedstoneWireStepDown(from, side cube.Pos, block world.Block, tx *world.Tx) bool { - if stepDowner, ok := block.(RedstoneWireStepDowner); ok { - return stepDowner.CanRedstoneWireStepDown(side, from, tx) +// redstoneWireHorizontalConnectionPositions returns direct, step-up, and step-down connections through face. +func redstoneWireHorizontalConnectionPositions(pos cube.Pos, tx *world.Tx, face cube.Face) []cube.Pos { + side := pos.Side(face) + if side.OutOfBounds(tx.Range()) { + return nil + } + positions := make([]cube.Pos, 0, 3) + if redstoneWireDirectConnectionLoaded(tx, side, face.Opposite()) { + positions = append(positions, side) + } + + above := pos.Side(cube.FaceUp) + sideAbove := side.Side(cube.FaceUp) + if !redstoneWireBlocksConnectionLoaded(tx, above, cube.FaceDown) && redstoneWireAtLoaded(tx, sideAbove) && redstoneWireSupportedLoaded(tx, sideAbove) { + positions = append(positions, sideAbove) + } + if !redstoneWireBlocksConnectionLoaded(tx, side, cube.FaceUp) { + down := side.Side(cube.FaceDown) + if !down.OutOfBounds(tx.Range()) && redstoneWireAtLoaded(tx, down) && redstoneWireCanTransmitDown(tx, pos) { + positions = append(positions, down) + } + } + return positions +} + +// redstoneWireCanTransmitDown reports whether dust at pos may power dust one block lower. +func redstoneWireCanTransmitDown(tx *world.Tx, pos cube.Pos) bool { + supportPos := pos.Side(cube.FaceDown) + if supportPos.OutOfBounds(tx.Range()) { + return false + } + support, ok := tx.BlockLoaded(supportPos) + if !ok || !support.Model().FaceSolid(supportPos, cube.FaceUp, tx) { + return false + } + if stepDowner, ok := support.(RedstoneWireStepDowner); ok { + return stepDowner.CanRedstoneWireStepDown(supportPos, pos, tx) } for _, face := range cube.Faces() { - if !block.Model().FaceSolid(side, face, tx) { + if !support.Model().FaceSolid(supportPos, face, tx) { return false } } return true } -// TrimMaterial delegates to item.RedstoneWire so the block form stays valid for smithing trim decoding too. -func (RedstoneWire) TrimMaterial() string { - return item.RedstoneWire{}.TrimMaterial() +// redstoneWireDirectConnectionLoaded reports whether a loaded block can directly connect to dust. +func redstoneWireDirectConnectionLoaded(tx *world.Tx, pos cube.Pos, face cube.Face) bool { + b, ok := tx.BlockLoaded(pos) + if !ok { + return false + } + switch b.(type) { + case RedstoneWire, world.RedstonePowerSource, world.RedstoneStrongPowerSource, world.RedstonePowerRelayer: + return true + } + return redstoneWireNonSolidComponent(pos, b, tx, face) } -// MaterialColour delegates to item.RedstoneWire to keep trim metadata defined in one place. -func (RedstoneWire) MaterialColour() string { - return item.RedstoneWire{}.MaterialColour() +// redstoneWireNonSolidComponent reports whether a non-solid loaded block is a redstone endpoint. +func redstoneWireNonSolidComponent(pos cube.Pos, b world.Block, tx *world.Tx, face cube.Face) bool { + model := b.Model() + if model == nil || model.FaceSolid(pos, face, tx) { + return false + } + switch b.(type) { + case world.RedstonePowerConsumer, world.RedstonePowerAction, world.RedstonePowerContextAction: + return true + } + return false } -// allRedstoneWires returns a list of all redstone dust states. -func allRedstoneWires() (all []world.Block) { - for i := range 16 { - all = append(all, RedstoneWire{Power: i}) +// redstoneWireAtLoaded reports whether loaded block data at pos is redstone wire. +func redstoneWireAtLoaded(tx *world.Tx, pos cube.Pos) bool { + b, ok := tx.BlockLoaded(pos) + if !ok { + return false + } + _, ok = b.(RedstoneWire) + return ok +} + +// redstoneWireRelevantLoaded reports whether a loaded block participates in redstone propagation. +func redstoneWireRelevantLoaded(tx *world.Tx, pos cube.Pos) bool { + b, ok := tx.BlockLoaded(pos) + return ok && redstoneWireRelevant(b) +} + +// redstoneWireRelevant reports whether b participates in redstone propagation. +func redstoneWireRelevant(b world.Block) bool { + switch b.(type) { + case world.RedstonePowerSource, + world.RedstoneStrongPowerSource, + world.RedstonePowerRelayer, + world.RedstonePowerConsumer, + world.RedstonePowerAction, + world.RedstonePowerContextAction: + return true + } + return false +} + +// redstoneWireFaceHorizontal reports whether face is one of the four horizontal faces. +func redstoneWireFaceHorizontal(face cube.Face) bool { + switch face { + case cube.FaceNorth, cube.FaceSouth, cube.FaceWest, cube.FaceEast: + return true + default: + return false } - return } diff --git a/server/block/register.go b/server/block/register.go index 57aca28432..2ba3bf633e 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -1,11 +1,13 @@ package block import ( + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" ) //go:generate go run ../../cmd/blockhash -o hash.go . +//go:generate go run ../../cmd/blockaccessor -o accessor.go . // init registers all blocks implemented by Dragonfly. @@ -15,6 +17,7 @@ func init() { world.RegisterBlock(AncientDebris{}) world.RegisterBlock(Andesite{Polished: true}) world.RegisterBlock(Andesite{}) + world.RegisterBlock(BambooMosaic{}) world.RegisterBlock(Barrier{}) world.RegisterBlock(Beacon{}) world.RegisterBlock(Bedrock{InfiniteBurning: true}) @@ -23,6 +26,10 @@ func init() { world.RegisterBlock(Bookshelf{}) world.RegisterBlock(Bricks{}) world.RegisterBlock(Calcite{}) + world.RegisterBlock(Cinnabar{}) + world.RegisterBlock(Cinnabar{Chiseled: true}) + world.RegisterBlock(CinnabarBricks{}) + world.RegisterBlock(PolishedCinnabar{}) world.RegisterBlock(Clay{}) world.RegisterBlock(Coal{}) world.RegisterBlock(Cobblestone{Mossy: true}) @@ -46,6 +53,10 @@ func init() { world.RegisterBlock(Emerald{}) world.RegisterBlock(EnchantingTable{}) world.RegisterBlock(EndBricks{}) + world.RegisterBlock(EndPortal{}) + for _, f := range allEndPortalFrames() { + world.RegisterBlock(f) + } world.RegisterBlock(EndStone{}) world.RegisterBlock(FletchingTable{}) world.RegisterBlock(GlassPane{}) @@ -94,6 +105,8 @@ func init() { world.RegisterBlock(Podzol{}) world.RegisterBlock(PolishedBlackstoneBrick{Cracked: true}) world.RegisterBlock(PolishedBlackstoneBrick{}) + world.RegisterBlock(Portal{Axis: cube.X}) + world.RegisterBlock(Portal{Axis: cube.Z}) world.RegisterBlock(QuartzBricks{}) world.RegisterBlock(RawCopper{}) world.RegisterBlock(RawGold{}) @@ -119,8 +132,13 @@ func init() { world.RegisterBlock(SporeBlossom{}) world.RegisterBlock(Stone{Smooth: true}) world.RegisterBlock(Stone{}) + world.RegisterBlock(Sulfur{}) + world.RegisterBlock(Sulfur{Chiseled: true}) + world.RegisterBlock(SulfurBricks{}) + world.RegisterBlock(PolishedSulfur{}) world.RegisterBlock(TNT{}) world.RegisterBlock(Terracotta{}) + world.RegisterBlock(TintedGlass{}) world.RegisterBlock(Tuff{}) world.RegisterBlock(Tuff{Chiseled: true}) world.RegisterBlock(TuffBricks{}) @@ -142,7 +160,9 @@ func init() { } registerAll(allAnvils()) - registerAll(allAzalea()) + registerAll(allBambooBlocks()) + registerAll(allBamboos()) + registerAll(allBambooSaplings()) registerAll(allBanners()) registerAll(allBarrels()) registerAll(allBasalt()) @@ -247,6 +267,7 @@ func init() { registerAll(allCopperLanterns()) registerAll(allCopperTorches()) registerAll(allCopperTrapdoors()) + registerAll(allShulkerBoxes()) world.DefaultBlockRegistry.Finalize() } @@ -256,6 +277,10 @@ func init() { world.RegisterItem(AncientDebris{}) world.RegisterItem(Andesite{Polished: true}) world.RegisterItem(Andesite{}) + world.RegisterItem(Bamboo{}) + world.RegisterItem(BambooBlock{}) + world.RegisterItem(BambooBlock{Stripped: true}) + world.RegisterItem(BambooMosaic{}) world.RegisterItem(Azalea{}) world.RegisterItem(Azalea{Flowering: true}) world.RegisterItem(Barrel{}) @@ -275,6 +300,10 @@ func init() { world.RegisterItem(Cake{}) world.RegisterItem(Calcite{}) world.RegisterItem(Carrot{}) + world.RegisterItem(Cinnabar{}) + world.RegisterItem(Cinnabar{Chiseled: true}) + world.RegisterItem(CinnabarBricks{}) + world.RegisterItem(PolishedCinnabar{}) world.RegisterItem(IronChain{}) world.RegisterItem(Chest{}) world.RegisterItem(ChiseledQuartz{}) @@ -304,6 +333,7 @@ func init() { world.RegisterItem(Emerald{}) world.RegisterItem(EnchantingTable{}) world.RegisterItem(EndBricks{}) + world.RegisterItem(EndPortalFrame{}) world.RegisterItem(EndRod{}) world.RegisterItem(EndStone{}) world.RegisterItem(EnderChest{}) @@ -410,8 +440,13 @@ func init() { world.RegisterItem(Stone{}) world.RegisterItem(String{}) world.RegisterItem(SugarCane{}) + world.RegisterItem(Sulfur{}) + world.RegisterItem(Sulfur{Chiseled: true}) + world.RegisterItem(SulfurBricks{}) + world.RegisterItem(PolishedSulfur{}) world.RegisterItem(TNT{}) world.RegisterItem(Terracotta{}) + world.RegisterItem(TintedGlass{}) world.RegisterItem(Tuff{}) world.RegisterItem(Tuff{Chiseled: true}) world.RegisterItem(TuffBricks{}) @@ -461,20 +496,21 @@ func init() { world.RegisterItem(Wool{Colour: c}) } for _, w := range WoodTypes() { - if w != WarpedWood() && w != CrimsonWood() { - t, _ := w.Leaves() + if t, ok := w.Leaves(); ok { world.RegisterItem(Leaves{Type: t, Persistent: true}) } - world.RegisterItem(Log{Wood: w, Stripped: true}) - world.RegisterItem(Log{Wood: w}) + if w != BambooWood() { + world.RegisterItem(Log{Wood: w, Stripped: true}) + world.RegisterItem(Log{Wood: w}) + world.RegisterItem(Wood{Wood: w, Stripped: true}) + world.RegisterItem(Wood{Wood: w}) + } world.RegisterItem(Planks{Wood: w}) world.RegisterItem(Sign{Wood: w}) world.RegisterItem(WoodDoor{Wood: w}) world.RegisterItem(WoodFenceGate{Wood: w}) world.RegisterItem(WoodFence{Wood: w}) world.RegisterItem(WoodTrapdoor{Wood: w}) - world.RegisterItem(Wood{Wood: w, Stripped: true}) - world.RegisterItem(Wood{Wood: w}) world.RegisterItem(Sapling{Wood: w}) } world.RegisterItem(Leaves{Type: AzaleaLeaves(), Persistent: true}) @@ -489,6 +525,9 @@ func init() { world.RegisterItem(LapisOre{Type: ore}) world.RegisterItem(RedstoneOre{Type: ore}) } + for _, c := range item.OptionalColours() { + world.RegisterItem(Candle{Colour: c}) + } for _, f := range FireTypes() { world.RegisterItem(Lantern{Type: f}) world.RegisterItem(Torch{Type: f}) @@ -559,6 +598,10 @@ func init() { for _, c := range item.Colours() { world.RegisterItem(Candle{Colour: c, Dyed: true}) } + + for _, c := range item.OptionalColours() { + world.RegisterItem(ShulkerBox{Colour: c}) + } } func registerAll(blocks []world.Block) { diff --git a/server/block/reinforced_deepslate.go b/server/block/reinforced_deepslate.go index 149a132ed5..4337003130 100644 --- a/server/block/reinforced_deepslate.go +++ b/server/block/reinforced_deepslate.go @@ -8,7 +8,7 @@ type ReinforcedDeepslate struct { // BreakInfo ... func (r ReinforcedDeepslate) BreakInfo() BreakInfo { - return newBreakInfo(55, alwaysHarvestable, nothingEffective, oneOf(r)).withBlastResistance(6000) + return newBreakInfo(55, alwaysHarvestable, nothingEffective, oneOf(r)).withBlastResistance(1200) } // EncodeItem ... diff --git a/server/block/resin_bricks.go b/server/block/resin_bricks.go index 384dbacae5..168f37b1a6 100644 --- a/server/block/resin_bricks.go +++ b/server/block/resin_bricks.go @@ -11,7 +11,7 @@ type ResinBricks struct { // BreakInfo ... func (r ResinBricks) BreakInfo() BreakInfo { - return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(r)).withBlastResistance(30) + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(r)).withBlastResistance(6) } // EncodeItem ... diff --git a/server/block/sand.go b/server/block/sand.go index f7ad54c696..d2fa6596fc 100644 --- a/server/block/sand.go +++ b/server/block/sand.go @@ -19,7 +19,7 @@ type Sand struct { // SoilFor ... func (s Sand) SoilFor(block world.Block) bool { switch block.(type) { - case Cactus, DeadBush, SugarCane: + case Cactus, DeadBush, SugarCane, BambooSapling, Bamboo: return true } return false diff --git a/server/block/sandstone.go b/server/block/sandstone.go index 466cf2547b..dbb4b99e54 100644 --- a/server/block/sandstone.go +++ b/server/block/sandstone.go @@ -21,7 +21,7 @@ type Sandstone struct { // BreakInfo ... func (s Sandstone) BreakInfo() BreakInfo { if s.Type == SmoothSandstone() { - return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(s)).withBlastResistance(30) + return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(s)).withBlastResistance(6) } return newBreakInfo(0.8, pickaxeHarvestable, pickaxeEffective, oneOf(s)) } diff --git a/server/block/sea_pickle.go b/server/block/sea_pickle.go index 96df4d893b..1c36ef4062 100644 --- a/server/block/sea_pickle.go +++ b/server/block/sea_pickle.go @@ -145,7 +145,7 @@ func (s SeaPickle) LightEmissionLevel() uint8 { // BreakInfo ... func (s SeaPickle) BreakInfo() BreakInfo { - return newBreakInfo(0, alwaysHarvestable, nothingEffective, simpleDrops(item.NewStack(s, s.AdditionalCount+1))) + return newBreakInfo(0, alwaysHarvestable, nothingEffective, simpleDrops(item.NewStack(SeaPickle{}, s.AdditionalCount+1))) } // FlammabilityInfo ... diff --git a/server/block/shulker_box.go b/server/block/shulker_box.go new file mode 100644 index 0000000000..d527218828 --- /dev/null +++ b/server/block/shulker_box.go @@ -0,0 +1,300 @@ +package block + +import ( + "fmt" + "math/rand/v2" + "strings" + "sync" + "sync/atomic" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "github.com/df-mc/dragonfly/server/internal/nbtconv" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/item/inventory" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" +) + +const ( + shulkerStateClosed int32 = iota + shulkerStateOpening + shulkerStateOpened + shulkerStateClosing +) + +// shulkerLidTicks is the number of scheduled ticks between fully closed and fully open. +const shulkerLidTicks int32 = 10 + +// ShulkerBox is a dye-able block that stores items. Unlike other blocks, it keeps its contents when broken. +type ShulkerBox struct { + transparent + sourceWaterDisplacer + + // Colour is the colour of the shulker box. A zero OptionalColour represents + // the undyed variant (minecraft:undyed_shulker_box). + Colour item.OptionalColour + // Facing is the direction that the shulker box is facing. + Facing cube.Face + // CustomName is the custom name of the shulker box. This name is displayed when the shulker box is opened, and may + // include colour codes. + CustomName string + + inventory *inventory.Inventory + viewerMu *sync.RWMutex + viewers map[ContainerViewer]struct{} + // progress is the lid opening progress in [0, 10]. + progress *atomic.Int32 + // animationStatus is the current openness state of the shulker box (whether it's opened, closing, etc.). + animationStatus *atomic.Int32 +} + +// NewShulkerBox creates a new initialised shulker box. The inventory is properly initialised. +func NewShulkerBox() ShulkerBox { + s := ShulkerBox{ + viewerMu: new(sync.RWMutex), + viewers: make(map[ContainerViewer]struct{}, 1), + progress: new(atomic.Int32), + animationStatus: new(atomic.Int32), + } + + s.inventory = inventory.New(27, func(slot int, _, after item.Stack) { + s.viewerMu.RLock() + defer s.viewerMu.RUnlock() + for viewer := range s.viewers { + viewer.ViewSlotChange(slot, after) + } + }) + s.inventory.SlotValidatorFunc(canStoreInShulkerBox) + + return s +} + +// canStoreInShulkerBox rejects nested shulker boxes. +func canStoreInShulkerBox(s item.Stack, _ int) bool { + if s.Empty() { + return true + } + _, nested := s.Item().(ShulkerBox) + return !nested +} + +func (s ShulkerBox) Model() world.BlockModel { + return model.Shulker{Facing: s.Facing, Progress: s.progress.Load()} +} + +func (s ShulkerBox) WithName(a ...any) world.Item { + s.CustomName = strings.TrimSuffix(fmt.Sprintln(a...), "\n") + return s +} + +func (s ShulkerBox) AddViewer(v ContainerViewer, tx *world.Tx, pos cube.Pos) { + s.viewerMu.Lock() + defer s.viewerMu.Unlock() + if len(s.viewers) == 0 { + s.open(tx, pos) + } + s.viewers[v] = struct{}{} +} + +func (s ShulkerBox) RemoveViewer(v ContainerViewer, tx *world.Tx, pos cube.Pos) { + s.viewerMu.Lock() + defer s.viewerMu.Unlock() + if len(s.viewers) == 0 { + return + } + delete(s.viewers, v) + if len(s.viewers) == 0 { + s.close(tx, pos) + } +} + +func (s ShulkerBox) Inventory(*world.Tx, cube.Pos) *inventory.Inventory { + return s.inventory +} + +func (s ShulkerBox) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, _ *item.UseContext) bool { + opener, ok := u.(ContainerOpener) + if !ok { + return false + } + if d, ok := tx.Block(pos.Side(s.Facing)).(LightDiffuser); ok && d.LightDiffusionLevel() <= 2 { + opener.OpenBlockContainer(pos, tx) + } + return true +} + +func (s ShulkerBox) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) (used bool) { + pos, _, used = firstReplaceable(tx, pos, face, s) + if !used { + return + } + s = s.initialised() + s.Facing = face + place(tx, pos, s, user, ctx) + return placed(ctx) +} + +// initialised lazily populates runtime fields on values created via struct +// literal (e.g. those returned from allShulkerBoxes). +func (s ShulkerBox) initialised() ShulkerBox { + if s.inventory != nil { + return s + } + n := NewShulkerBox() + n.Colour, n.Facing, n.CustomName = s.Colour, s.Facing, s.CustomName + return n +} + +// open opens the shulker box, displaying the animation and playing a sound. +func (s ShulkerBox) open(tx *world.Tx, pos cube.Pos) { + s.animationStatus.Store(shulkerStateOpening) + for _, v := range tx.Viewers(pos.Vec3()) { + v.ViewBlockAction(pos, OpenAction{}) + } + tx.PlaySound(pos.Vec3Centre(), sound.ShulkerBoxOpen{}) + tx.ScheduleBlockUpdate(pos, s, 0) +} + +// close closes the shulker box, displaying the animation and playing a sound. +func (s ShulkerBox) close(tx *world.Tx, pos cube.Pos) { + s.animationStatus.Store(shulkerStateClosing) + for _, v := range tx.Viewers(pos.Vec3()) { + v.ViewBlockAction(pos, CloseAction{}) + } + tx.ScheduleBlockUpdate(pos, s, 0) +} + +func (s ShulkerBox) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { + switch s.animationStatus.Load() { + case shulkerStateClosed: + s.progress.Store(0) + case shulkerStateOpening: + s.progress.Add(1) + s.pushEntities(pos, tx) + if s.progress.Load() >= shulkerLidTicks { + s.progress.Store(shulkerLidTicks) + s.animationStatus.Store(shulkerStateOpened) + } + tx.ScheduleBlockUpdate(pos, s, 0) + case shulkerStateOpened: + s.progress.Store(shulkerLidTicks) + case shulkerStateClosing: + s.progress.Add(-1) + if s.progress.Load() <= 0 { + tx.PlaySound(pos.Vec3Centre(), sound.ShulkerBoxClose{}) + s.progress.Store(0) + s.animationStatus.Store(shulkerStateClosed) + } + tx.ScheduleBlockUpdate(pos, s, 0) + } +} + +// pushEntities pushes all entities touching the shulker box lid during opening. +func (s ShulkerBox) pushEntities(pos cube.Pos, tx *world.Tx) { + shulkerBBoxes := s.Model().BBox(pos, tx) + if len(shulkerBBoxes) == 0 { + return + } + searchBox := shulkerBBoxes[0].Translate(pos.Vec3()).Grow(0.35) + for e := range tx.EntitiesWithin(searchBox) { + s.push(pos, tx, e) + } +} + +// push pushes entities when the shulker box lid is opening. +func (s ShulkerBox) push(pos cube.Pos, tx *world.Tx, e world.Entity) { + if s.animationStatus.Load() != shulkerStateOpening { + return + } + mover, ok := e.(interface { + Displace(deltaPos mgl64.Vec3) + }) + if !ok { + return + } + shulkerBBoxes := s.Model().BBox(pos, tx) + if len(shulkerBBoxes) == 0 { + return + } + shulkerBBox := shulkerBBoxes[0].Translate(pos.Vec3()) + entityBBox := e.H().Type().BBox(e).Translate(e.Position()) + if !shulkerBBox.IntersectsWith(entityBBox) { + return + } + + // Move the entity out along the lid's facing axis by the penetration depth + // between the shulker lid box and the entity box. + delta := shulkerPushDelta(s.Facing, shulkerBBox, entityBBox) + if delta != (mgl64.Vec3{}) { + mover.Displace(delta) + } +} + +func shulkerPushDelta(facing cube.Face, shulkerBBox, entityBBox cube.BBox) (delta mgl64.Vec3) { + switch facing { + case cube.FaceDown: + delta[1] = shulkerBBox.Min().Y() - entityBBox.Max().Y() + case cube.FaceUp: + delta[1] = shulkerBBox.Max().Y() - entityBBox.Min().Y() + case cube.FaceEast: + delta[0] = shulkerBBox.Max().X() - entityBBox.Min().X() + case cube.FaceWest: + delta[0] = shulkerBBox.Min().X() - entityBBox.Max().X() + case cube.FaceSouth: + delta[2] = shulkerBBox.Max().Z() - entityBBox.Min().Z() + case cube.FaceNorth: + delta[2] = shulkerBBox.Min().Z() - entityBBox.Max().Z() + } + return delta +} + +func (s ShulkerBox) BreakInfo() BreakInfo { + return newBreakInfo(2, alwaysHarvestable, pickaxeEffective, oneOf(s)) +} + +func (s ShulkerBox) MaxCount() int { + return 1 +} + +func (s ShulkerBox) EncodeBlock() (name string, properties map[string]any) { + if c, ok := s.Colour.Colour(); ok { + return "minecraft:" + c.String() + "_shulker_box", nil + } + return "minecraft:undyed_shulker_box", nil +} + +func (s ShulkerBox) EncodeItem() (id string, meta int16) { + name, _ := s.EncodeBlock() + return name, 0 +} + +func (s ShulkerBox) DecodeNBT(data map[string]any) any { + s = s.initialised() + nbtconv.InvFromNBT(s.inventory, nbtconv.Slice(data, "Items")) + s.Facing = cube.Face(nbtconv.Uint8(data, "facing")) + s.CustomName = nbtconv.String(data, "CustomName") + return s +} + +func (s ShulkerBox) EncodeNBT() map[string]any { + s = s.initialised() + m := map[string]any{ + "Items": nbtconv.InvToNBT(s.inventory), + "id": "ShulkerBox", + "facing": uint8(s.Facing), + } + if s.CustomName != "" { + m["CustomName"] = s.CustomName + } + return m +} + +// allShulkerBoxes returns one shulker box per item.OptionalColour, including the undyed variant. +func allShulkerBoxes() (boxes []world.Block) { + for _, c := range item.OptionalColours() { + boxes = append(boxes, ShulkerBox{Colour: c}) + } + return boxes +} diff --git a/server/block/sign.go b/server/block/sign.go index eaae6b65de..836beb78d7 100644 --- a/server/block/sign.go +++ b/server/block/sign.go @@ -200,18 +200,20 @@ func (s Sign) DecodeNBT(data map[string]any) any { front, ok := data["FrontText"].(map[string]any) if ok { - s.Front.BaseColour = nbtconv.RGBAFromInt32(nbtconv.Int32(front, "Color")) - s.Front.Glowing = nbtconv.Bool(front, "GlowingText") + s.Front.BaseColour = nbtconv.RGBAFromInt32(nbtconv.Int32(front, "SignTextColor")) + s.Front.Glowing = nbtconv.Bool(front, "IgnoreLighting") s.Front.Text = nbtconv.String(front, "Text") - s.Front.Owner = nbtconv.String(front, "Owner") + s.Front.Owner = nbtconv.String(front, "TextOwner") } + s.Waxed = nbtconv.Bool(data, "IsWaxed") + back, ok := data["BackText"].(map[string]any) if ok { - s.Back.BaseColour = nbtconv.RGBAFromInt32(nbtconv.Int32(back, "Color")) - s.Back.Glowing = nbtconv.Bool(back, "GlowingText") + s.Back.BaseColour = nbtconv.RGBAFromInt32(nbtconv.Int32(back, "SignTextColor")) + s.Back.Glowing = nbtconv.Bool(back, "IgnoreLighting") s.Back.Text = nbtconv.String(back, "Text") - s.Back.Owner = nbtconv.String(back, "Owner") + s.Back.Owner = nbtconv.String(back, "TextOwner") } return s diff --git a/server/block/slab.go b/server/block/slab.go index 13cb1d90f5..423e6defb8 100644 --- a/server/block/slab.go +++ b/server/block/slab.go @@ -63,6 +63,9 @@ func (s Slab) Instrument() sound.Instrument { if _, ok := s.Block.(Planks); ok { return sound.Bass() } + if _, ok := s.Block.(BambooMosaic); ok { + return sound.Bass() + } return sound.BassDrum() } @@ -109,11 +112,13 @@ func (s Slab) CanRedstoneWireStepDown(cube.Pos, cube.Pos, *world.Tx) bool { // BreakInfo ... func (s Slab) BreakInfo() BreakInfo { - hardness, blastResistance, harvestable, effective := 2.0, 30.0, pickaxeHarvestable, pickaxeEffective + hardness, blastResistance, harvestable, effective := 2.0, 6.0, pickaxeHarvestable, pickaxeEffective switch block := s.Block.(type) { case Stone, Sandstone, Quartz, Purpur, Blackstone, PolishedBlackstoneBrick: // These slab types do not match their block's hardness or blast resistance + case EndBricks: + hardness = 3 case StoneBricks: if block.Type == MossyStoneBricks() { hardness = 1.5 @@ -123,10 +128,11 @@ func (s Slab) BreakInfo() BreakInfo { hardness, blastResistance, harvestable, effective = breakInfo.Hardness, breakInfo.BlastResistance, breakInfo.Harvestable, breakInfo.Effective } return newBreakInfo(hardness, harvestable, effective, func(tool item.Tool, enchantments []item.Enchantment) []item.Stack { + single := Slab{Block: s.Block} if s.Double { - return []item.Stack{item.NewStack(s, 2)} + return []item.Stack{item.NewStack(single, 2)} } - return []item.Stack{item.NewStack(s, 1)} + return []item.Stack{item.NewStack(single, 1)} }).withBlastResistance(blastResistance) } diff --git a/server/block/slab_type.go b/server/block/slab_type.go index f5e4185bfa..bd787b1538 100644 --- a/server/block/slab_type.go +++ b/server/block/slab_type.go @@ -25,8 +25,16 @@ func encodeSlabBlock(block world.Block, double bool) (id string, suffix string) } else if block.Type == PolishedBlackstone() { return "polished_blackstone", suffix } + case BambooMosaic: + return "bamboo_mosaic", suffix case Bricks: return "brick", suffix + case Cinnabar: + if !block.Chiseled { + return "cinnabar", suffix + } + case CinnabarBricks: + return "cinnabar_brick", suffix case Cobblestone: if block.Mossy { return "mossy_cobblestone", suffix @@ -86,6 +94,10 @@ func encodeSlabBlock(block world.Block, double bool) (id string, suffix string) if !block.Cracked { return "polished_blackstone_brick", suffix } + case PolishedCinnabar: + return "polished_cinnabar", suffix + case PolishedSulfur: + return "polished_sulfur", suffix case PolishedTuff: return "polished_tuff", suffix case Prismarine: @@ -136,6 +148,12 @@ func encodeSlabBlock(block world.Block, double bool) (id string, suffix string) return "mossy_stone_brick", suffix } return "stone_brick", suffix + case Sulfur: + if !block.Chiseled { + return "sulfur", suffix + } + case SulfurBricks: + return "sulfur_brick", suffix case Tuff: if !block.Chiseled { return "tuff", suffix @@ -153,9 +171,12 @@ func SlabBlocks() []world.Block { b := []world.Block{ Andesite{Polished: true}, Andesite{}, + BambooMosaic{}, Blackstone{Type: PolishedBlackstone()}, Blackstone{}, Bricks{}, + Cinnabar{}, + CinnabarBricks{}, Cobblestone{Mossy: true}, Cobblestone{}, DeepslateBricks{}, @@ -171,6 +192,8 @@ func SlabBlocks() []world.Block { NetherBricks{Type: RedNetherBricks()}, NetherBricks{}, PolishedBlackstoneBrick{}, + PolishedCinnabar{}, + PolishedSulfur{}, PolishedTuff{}, Purpur{}, Quartz{Smooth: true}, @@ -180,6 +203,8 @@ func SlabBlocks() []world.Block { StoneBricks{}, Stone{Smooth: true}, Stone{}, + Sulfur{}, + SulfurBricks{}, Tuff{}, TuffBricks{}, } diff --git a/server/block/smoker.go b/server/block/smoker.go index 63959750b0..a4c07e690e 100644 --- a/server/block/smoker.go +++ b/server/block/smoker.go @@ -81,7 +81,10 @@ func (s Smoker) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world // BreakInfo ... func (s Smoker) BreakInfo() BreakInfo { - xp := s.Experience() + xp := 0 + if s.smelter != nil { + xp = s.Experience() + } return newBreakInfo(3.5, alwaysHarvestable, pickaxeEffective, oneOf(Smoker{})).withXPDropRange(xp, xp).withBreakHandler(func(pos cube.Pos, tx *world.Tx, u item.User) { for _, i := range s.Inventory(tx, pos).Clear() { dropItem(tx, i, pos.Vec3()) diff --git a/server/block/smooth_basalt.go b/server/block/smooth_basalt.go index 7030f373bf..c4eb10df69 100644 --- a/server/block/smooth_basalt.go +++ b/server/block/smooth_basalt.go @@ -20,7 +20,7 @@ func (SmoothBasalt) EncodeItem() (name string, meta int16) { // BreakInfo ... func (s SmoothBasalt) BreakInfo() BreakInfo { - return newBreakInfo(1.25, pickaxeHarvestable, pickaxeEffective, oneOf(s)).withBlastResistance(21) + return newBreakInfo(1.25, pickaxeHarvestable, pickaxeEffective, oneOf(s)).withBlastResistance(4.2) } func (s SmoothBasalt) Color() color.RGBA { diff --git a/server/block/stained_terracotta.go b/server/block/stained_terracotta.go index c1ae9c3c24..65cddfacc5 100644 --- a/server/block/stained_terracotta.go +++ b/server/block/stained_terracotta.go @@ -23,7 +23,7 @@ func (t StainedTerracotta) SoilFor(block world.Block) bool { // BreakInfo ... func (t StainedTerracotta) BreakInfo() BreakInfo { - return newBreakInfo(1.25, pickaxeHarvestable, pickaxeEffective, oneOf(t)).withBlastResistance(21) + return newBreakInfo(1.25, pickaxeHarvestable, pickaxeEffective, oneOf(t)) } // SmeltInfo ... diff --git a/server/block/stairs.go b/server/block/stairs.go index 665199100e..ff0e4827d9 100644 --- a/server/block/stairs.go +++ b/server/block/stairs.go @@ -55,6 +55,9 @@ func (s Stairs) Instrument() sound.Instrument { if _, ok := s.Block.(Planks); ok { return sound.Bass() } + if _, ok := s.Block.(BambooMosaic); ok { + return sound.Bass() + } return sound.BassDrum() } diff --git a/server/block/stairs_type.go b/server/block/stairs_type.go index e3b2d9f94b..2c98f00d3f 100644 --- a/server/block/stairs_type.go +++ b/server/block/stairs_type.go @@ -18,8 +18,16 @@ func encodeStairsBlock(block world.Block) string { } else if block.Type == PolishedBlackstone() { return "polished_blackstone" } + case BambooMosaic: + return "bamboo_mosaic" case Bricks: return "brick" + case Cinnabar: + if !block.Chiseled { + return "cinnabar" + } + case CinnabarBricks: + return "cinnabar_brick" case Cobblestone: if block.Mossy { return "mossy_cobblestone" @@ -75,6 +83,10 @@ func encodeStairsBlock(block world.Block) string { if !block.Cracked { return "polished_blackstone_brick" } + case PolishedCinnabar: + return "polished_cinnabar" + case PolishedSulfur: + return "polished_sulfur" case PolishedTuff: return "polished_tuff" case Prismarine: @@ -119,6 +131,12 @@ func encodeStairsBlock(block world.Block) string { return "mossy_stone_brick" } return "stone_brick" + case Sulfur: + if !block.Chiseled { + return "sulfur" + } + case SulfurBricks: + return "sulfur_brick" case Tuff: if !block.Chiseled { return "tuff" @@ -136,9 +154,12 @@ func StairsBlocks() []world.Block { b := []world.Block{ Andesite{Polished: true}, Andesite{}, + BambooMosaic{}, Blackstone{Type: PolishedBlackstone()}, Blackstone{}, Bricks{}, + Cinnabar{}, + CinnabarBricks{}, Cobblestone{Mossy: true}, Cobblestone{}, DeepslateBricks{}, @@ -154,6 +175,8 @@ func StairsBlocks() []world.Block { NetherBricks{Type: RedNetherBricks()}, NetherBricks{}, PolishedBlackstoneBrick{}, + PolishedCinnabar{}, + PolishedSulfur{}, PolishedTuff{}, Purpur{}, Quartz{Smooth: true}, @@ -162,6 +185,8 @@ func StairsBlocks() []world.Block { StoneBricks{Type: MossyStoneBricks()}, StoneBricks{}, Stone{}, + Sulfur{}, + SulfurBricks{}, Tuff{}, TuffBricks{}, } diff --git a/server/block/stone.go b/server/block/stone.go index 3727132bb9..8b73dedf53 100644 --- a/server/block/stone.go +++ b/server/block/stone.go @@ -32,24 +32,24 @@ type ( // BreakInfo ... func (s Stone) BreakInfo() BreakInfo { if s.Smooth { - return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(s)).withBlastResistance(30) + return newBreakInfo(2, pickaxeHarvestable, pickaxeEffective, oneOf(s)).withBlastResistance(6) } - return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, silkTouchOneOf(Cobblestone{}, Stone{})).withBlastResistance(30) + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, silkTouchOneOf(Cobblestone{}, Stone{})).withBlastResistance(6) } // BreakInfo ... func (g Granite) BreakInfo() BreakInfo { - return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(g)).withBlastResistance(30) + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(g)).withBlastResistance(6) } // BreakInfo ... func (d Diorite) BreakInfo() BreakInfo { - return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(d)).withBlastResistance(30) + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(d)).withBlastResistance(6) } // BreakInfo ... func (a Andesite) BreakInfo() BreakInfo { - return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(a)).withBlastResistance(30) + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(a)).withBlastResistance(6) } // SmeltInfo ... diff --git a/server/block/stone_bricks.go b/server/block/stone_bricks.go index 0e043ce5e6..861f184e92 100644 --- a/server/block/stone_bricks.go +++ b/server/block/stone_bricks.go @@ -17,7 +17,7 @@ type StoneBricks struct { // BreakInfo ... func (s StoneBricks) BreakInfo() BreakInfo { - return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(s)).withBlastResistance(30) + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(s)).withBlastResistance(6) } // SmeltInfo ... diff --git a/server/block/sulfur.go b/server/block/sulfur.go new file mode 100644 index 0000000000..e76ab06409 --- /dev/null +++ b/server/block/sulfur.go @@ -0,0 +1,31 @@ +package block + +// Sulfur is a decorative rock that generates throughout sulfur caves and as part of sulfur springs. +type Sulfur struct { + solid + bassDrum + + // Chiseled specifies if the sulfur is chiseled. + Chiseled bool +} + +// BreakInfo ... +func (s Sulfur) BreakInfo() BreakInfo { + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(s)).withBlastResistance(6) +} + +// EncodeItem ... +func (s Sulfur) EncodeItem() (name string, meta int16) { + if s.Chiseled { + return "minecraft:chiseled_sulfur", 0 + } + return "minecraft:sulfur", 0 +} + +// EncodeBlock ... +func (s Sulfur) EncodeBlock() (string, map[string]any) { + if s.Chiseled { + return "minecraft:chiseled_sulfur", nil + } + return "minecraft:sulfur", nil +} diff --git a/server/block/sulfur_bricks.go b/server/block/sulfur_bricks.go new file mode 100644 index 0000000000..19de908078 --- /dev/null +++ b/server/block/sulfur_bricks.go @@ -0,0 +1,22 @@ +package block + +// SulfurBricks is a decorative variant of Sulfur. +type SulfurBricks struct { + solid + bassDrum +} + +// BreakInfo ... +func (s SulfurBricks) BreakInfo() BreakInfo { + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(s)).withBlastResistance(6) +} + +// EncodeItem ... +func (SulfurBricks) EncodeItem() (name string, meta int16) { + return "minecraft:sulfur_bricks", 0 +} + +// EncodeBlock ... +func (SulfurBricks) EncodeBlock() (string, map[string]any) { + return "minecraft:sulfur_bricks", nil +} diff --git a/server/block/terracotta.go b/server/block/terracotta.go index 0490068a92..0ed96029ea 100644 --- a/server/block/terracotta.go +++ b/server/block/terracotta.go @@ -17,7 +17,7 @@ func (Terracotta) SoilFor(block world.Block) bool { // BreakInfo ... func (t Terracotta) BreakInfo() BreakInfo { - return newBreakInfo(1.25, pickaxeHarvestable, pickaxeEffective, oneOf(t)).withBlastResistance(21) + return newBreakInfo(1.25, pickaxeHarvestable, pickaxeEffective, oneOf(t)).withBlastResistance(4.2) } // EncodeItem ... diff --git a/server/block/tinted_glass.go b/server/block/tinted_glass.go new file mode 100644 index 0000000000..251f8cf336 --- /dev/null +++ b/server/block/tinted_glass.go @@ -0,0 +1,29 @@ +package block + +// TintedGlass is a decorative, solid block that is visually see-through but, unlike regular glass, blocks +// all light passing through it. +type TintedGlass struct { + solid + clicksAndSticks +} + +// PreventsSuffocation always returns true. Tinted glass blocks light entirely but, like regular glass, never +// suffocates an entity standing inside it. +func (TintedGlass) PreventsSuffocation() bool { + return true +} + +// BreakInfo ... +func (g TintedGlass) BreakInfo() BreakInfo { + return newBreakInfo(0.3, alwaysHarvestable, nothingEffective, oneOf(g)).withBlastResistance(0.3) +} + +// EncodeItem ... +func (TintedGlass) EncodeItem() (name string, meta int16) { + return "minecraft:tinted_glass", 0 +} + +// EncodeBlock ... +func (TintedGlass) EncodeBlock() (string, map[string]any) { + return "minecraft:tinted_glass", nil +} diff --git a/server/block/tnt.go b/server/block/tnt.go index 4606f45126..8e9923c087 100644 --- a/server/block/tnt.go +++ b/server/block/tnt.go @@ -1,14 +1,14 @@ package block import ( + "math/rand/v2" + "time" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/item/enchantment" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/sound" - "github.com/go-gl/mathgl/mgl64" - "math/rand/v2" - "time" ) // TNT is an explosive block that can be primed to generate an explosion. @@ -16,6 +16,18 @@ type TNT struct { solid } +var _ world.RedstonePowerAction = TNT{} + +func (TNT) RedstoneNonConductive() {} + +// RedstonePowerAction primes TNT when it first receives redstone power. +func (t TNT) RedstonePowerAction(pos cube.Pos, tx *world.Tx, oldPower, newPower int) { + if oldPower > 0 || newPower == 0 { + return + } + t.Ignite(pos, tx, nil) +} + // ProjectileHit ... func (t TNT) ProjectileHit(pos cube.Pos, tx *world.Tx, e world.Entity, _ cube.Face) { if f, ok := e.(flammableEntity); ok && f.OnFireDuration() > 0 { @@ -41,7 +53,7 @@ func (t TNT) Ignite(pos cube.Pos, tx *world.Tx, _ world.Entity) bool { } // Explode ... -func (t TNT) Explode(_ mgl64.Vec3, pos cube.Pos, tx *world.Tx, _ ExplosionConfig) { +func (t TNT) Explode(_ world.ExplosionSource, pos cube.Pos, tx *world.Tx) { spawnTnt(pos, tx, time.Second/2+time.Duration(rand.IntN(int(time.Second+time.Second/2)))) } diff --git a/server/block/tuff.go b/server/block/tuff.go index 5ea6689083..c0404926ce 100644 --- a/server/block/tuff.go +++ b/server/block/tuff.go @@ -11,7 +11,7 @@ type Tuff struct { // BreakInfo ... func (t Tuff) BreakInfo() BreakInfo { - return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(t)).withBlastResistance(30) + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(t)).withBlastResistance(6) } // EncodeItem ... diff --git a/server/block/tuff_bricks.go b/server/block/tuff_bricks.go index c5520354af..4daa98900c 100644 --- a/server/block/tuff_bricks.go +++ b/server/block/tuff_bricks.go @@ -13,7 +13,7 @@ type TuffBricks struct { // BreakInfo ... func (t TuffBricks) BreakInfo() BreakInfo { - return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(t)).withBlastResistance(30) + return newBreakInfo(1.5, pickaxeHarvestable, pickaxeEffective, oneOf(t)).withBlastResistance(6) } // EncodeItem ... diff --git a/server/block/wall_type.go b/server/block/wall_type.go index cf79f3f840..8477e97493 100644 --- a/server/block/wall_type.go +++ b/server/block/wall_type.go @@ -17,6 +17,12 @@ func encodeWallBlock(block world.Block) string { } case Bricks: return "brick" + case Cinnabar: + if !block.Chiseled { + return "cinnabar" + } + case CinnabarBricks: + return "cinnabar_brick" case Cobblestone: if block.Mossy { return "mossy_cobblestone" @@ -58,6 +64,10 @@ func encodeWallBlock(block world.Block) string { if !block.Cracked { return "polished_blackstone_brick" } + case PolishedCinnabar: + return "polished_cinnabar" + case PolishedSulfur: + return "polished_sulfur" case PolishedTuff: return "polished_tuff" case Prismarine: @@ -79,6 +89,12 @@ func encodeWallBlock(block world.Block) string { } else if block.Type == MossyStoneBricks() { return "mossy_stone_brick" } + case Sulfur: + if !block.Chiseled { + return "sulfur" + } + case SulfurBricks: + return "sulfur_brick" case Tuff: if !block.Chiseled { return "tuff" @@ -98,6 +114,8 @@ func WallBlocks() []world.Block { Blackstone{Type: PolishedBlackstone()}, Blackstone{}, Bricks{}, + Cinnabar{}, + CinnabarBricks{}, Cobblestone{Mossy: true}, Cobblestone{}, DeepslateBricks{}, @@ -111,6 +129,8 @@ func WallBlocks() []world.Block { NetherBricks{Type: RedNetherBricks()}, NetherBricks{}, PolishedBlackstoneBrick{}, + PolishedCinnabar{}, + PolishedSulfur{}, PolishedTuff{}, Prismarine{}, ResinBricks{}, @@ -118,6 +138,8 @@ func WallBlocks() []world.Block { Sandstone{}, StoneBricks{Type: MossyStoneBricks()}, StoneBricks{}, + Sulfur{}, + SulfurBricks{}, Tuff{}, TuffBricks{}, } diff --git a/server/block/water.go b/server/block/water.go index 040e83a646..af987fe8f5 100644 --- a/server/block/water.go +++ b/server/block/water.go @@ -5,7 +5,6 @@ import ( "time" "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/item/potion" "github.com/df-mc/dragonfly/server/world" @@ -77,9 +76,9 @@ func (w Water) LiquidFalling() bool { return w.Falling } -// BlastResistance always returns 500. +// BlastResistance ... func (Water) BlastResistance() float64 { - return 500 + return 100 } // HasLiquidDrops ... @@ -126,7 +125,7 @@ func (w Water) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { // Only form a new source block if there either is no water below this block, or if the water // below this is not falling (full source block). res := Water{Depth: 8, Still: true} - ctx := event.C(tx) + ctx := tx.Event() if tx.World().Handler().HandleLiquidFlow(ctx, pos, pos, res, w); ctx.Cancelled() { return } @@ -158,7 +157,7 @@ func (w Water) Harden(pos cube.Pos, tx *world.Tx, flownIntoBy *cube.Pos) bool { return false } if lava, ok := tx.Block(pos.Side(cube.FaceUp)).(Lava); ok { - ctx := event.C(tx) + ctx := tx.Event() if tx.World().Handler().HandleLiquidHarden(ctx, pos, w, lava, Stone{}); ctx.Cancelled() { return false } @@ -166,7 +165,7 @@ func (w Water) Harden(pos cube.Pos, tx *world.Tx, flownIntoBy *cube.Pos) bool { tx.PlaySound(pos.Vec3Centre(), sound.Fizz{}) return true } else if lava, ok := tx.Block(*flownIntoBy).(Lava); ok { - ctx := event.C(tx) + ctx := tx.Event() if tx.World().Handler().HandleLiquidHarden(ctx, pos, w, lava, Cobblestone{}); ctx.Cancelled() { return false } diff --git a/server/block/wood.go b/server/block/wood.go index 787c8573cd..c58a07091c 100644 --- a/server/block/wood.go +++ b/server/block/wood.go @@ -98,9 +98,13 @@ func (w Wood) EncodeBlock() (name string, properties map[string]any) { } } -// allWood returns a list of all possible wood states. +// allWood returns all possible Wood block states, excluding bamboo blocks, +// which are registered separately through allBambooBlocks. func allWood() (wood []world.Block) { for _, w := range WoodTypes() { + if w == BambooWood() { + continue + } for axis := cube.Axis(0); axis < 3; axis++ { wood = append(wood, Wood{Axis: axis, Stripped: true, Wood: w}) wood = append(wood, Wood{Axis: axis, Stripped: false, Wood: w}) diff --git a/server/block/wood_fence.go b/server/block/wood_fence.go index d318d1eb59..3c9c3b45a8 100644 --- a/server/block/wood_fence.go +++ b/server/block/wood_fence.go @@ -22,7 +22,7 @@ type WoodFence struct { // BreakInfo ... func (w WoodFence) BreakInfo() BreakInfo { - return newBreakInfo(2, alwaysHarvestable, axeEffective, oneOf(w)).withBlastResistance(15) + return newBreakInfo(2, alwaysHarvestable, axeEffective, oneOf(w)).withBlastResistance(3) } // SideClosed ... diff --git a/server/block/wood_fence_gate.go b/server/block/wood_fence_gate.go index 60ff01dae7..f7e0c7305d 100644 --- a/server/block/wood_fence_gate.go +++ b/server/block/wood_fence_gate.go @@ -29,7 +29,7 @@ type WoodFenceGate struct { // BreakInfo ... func (f WoodFenceGate) BreakInfo() BreakInfo { - return newBreakInfo(2, alwaysHarvestable, axeEffective, oneOf(f)).withBlastResistance(15) + return newBreakInfo(2, alwaysHarvestable, axeEffective, oneOf(f)).withBlastResistance(3) } // FlammabilityInfo ... diff --git a/server/block/wood_type.go b/server/block/wood_type.go index ded6df1999..cafd9b0648 100644 --- a/server/block/wood_type.go +++ b/server/block/wood_type.go @@ -61,9 +61,14 @@ func PaleOakWood() WoodType { return WoodType{10} } +// BambooWood returns bamboo wood material. +func BambooWood() WoodType { + return WoodType{11} +} + // WoodTypes returns a list of all wood types func WoodTypes() []WoodType { - return []WoodType{OakWood(), SpruceWood(), BirchWood(), JungleWood(), AcaciaWood(), DarkOakWood(), CrimsonWood(), WarpedWood(), MangroveWood(), CherryWood(), PaleOakWood()} + return []WoodType{OakWood(), SpruceWood(), BirchWood(), JungleWood(), AcaciaWood(), DarkOakWood(), CrimsonWood(), WarpedWood(), MangroveWood(), CherryWood(), PaleOakWood(), BambooWood()} } type wood uint8 @@ -98,6 +103,8 @@ func (w wood) Name() string { return "Cherry Wood" case 10: return "Pale Oak Wood" + case 11: + return "Bamboo Wood" } panic("unknown wood type") } @@ -127,6 +134,8 @@ func (w wood) String() string { return "cherry" case 10: return "pale_oak" + case 11: + return "bamboo" } panic("unknown wood type") } diff --git a/server/cmd/argument.go b/server/cmd/argument.go index 1b76ade3af..c11dcc1493 100644 --- a/server/cmd/argument.go +++ b/server/cmd/argument.go @@ -275,6 +275,9 @@ func (p parser) parseTargets(line *Line, tx *world.Tx) ([]Target, error) { if !ok { return nil, line.UsageError() } + if strings.HasPrefix(first, "@") && tx == nil { + return nil, MessageNoTargets.F() + } switch first[:min(len(first), 2)] { case "@p": pos := line.src.Position() diff --git a/server/cmd/command.go b/server/cmd/command.go index c32500f606..1a99e315ef 100644 --- a/server/cmd/command.go +++ b/server/cmd/command.go @@ -31,7 +31,8 @@ import ( type Runnable interface { // Run runs the Command, using the arguments passed to the Command. The source is passed to the method, // which is the source of the Command execution, and the output is passed, to which messages may be - // added which get sent to the source. + // added which get sent to the source. tx is nil for sources not attached to + // a world, such as a console source. Runnables that use tx must nil-check it. Run(src Source, o *Output, tx *world.Tx) } @@ -123,6 +124,9 @@ func (cmd Command) Aliases() []string { // If parsing of all Runnables was unsuccessful, a command output with an error message is sent to the Source // passed, and the Run method of the Runnables are not called. // The Source passed must not be nil. The method will panic if a nil Source is passed. +// tx may be nil for sources that are not attached to a world. Selector parsing +// reports an error when it requires a transaction; Runnable bodies must +// independently nil-check tx before using it. func (cmd Command) Execute(args string, source Source, tx *world.Tx) { if source == nil { panic("execute: invalid command source: source must not be nil") diff --git a/server/cmd/target.go b/server/cmd/target.go index 088dfdbbed..dcb0d6793c 100644 --- a/server/cmd/target.go +++ b/server/cmd/target.go @@ -23,6 +23,9 @@ type NamedTarget interface { // targets returns all Targets selectable by the Source passed. func targets(tx *world.Tx) (entities []Target, players []NamedTarget) { + if tx == nil { + return nil, nil + } ent := sliceutil.Convert[Target](slices.Collect(tx.Entities())) pl := sliceutil.Convert[NamedTarget](slices.Collect(tx.Players())) return ent, pl diff --git a/server/conf.go b/server/conf.go index e14d33fbbf..20df04d2b7 100644 --- a/server/conf.go +++ b/server/conf.go @@ -117,6 +117,10 @@ type Config struct { // ChunkUnloadInterval should not be used to prevent chunks from unloading // altogether. This should be done using a Loader with a custom Viewer. ChunkUnloadInterval time.Duration + // ChunkLoadWorkers is the number of background workers that load and generate + // chunks in each world, defaulting to 1. Values above 1 generate chunks + // concurrently and require a concurrency-safe Generator. + ChunkLoadWorkers int // Entities is a world.EntityRegistry with all entity types registered that // may be added to the Server's worlds. If no entity types are registered, // Entities will be set to entity.DefaultRegistry. diff --git a/server/entity/area_effect_cloud.go b/server/entity/area_effect_cloud.go index 23c52d67a6..52198846c4 100644 --- a/server/entity/area_effect_cloud.go +++ b/server/entity/area_effect_cloud.go @@ -65,8 +65,8 @@ func (areaEffectCloudType) DecodeNBT(m map[string]any, data *world.EntityData) { RadiusUseGrowth: float64(nbtconv.Float32(m, "RadiusOnUse")), RadiusTickGrowth: float64(nbtconv.Float32(m, "RadiusPerTick")), Duration: nbtconv.TickDuration[int32](m, "Duration"), - DurationUseGrowth: nbtconv.TickDuration[int32](m, "ReapplicationDelay"), - ReapplicationDelay: nbtconv.TickDuration[int32](m, "DurationOnUse"), + DurationUseGrowth: nbtconv.TickDuration[int32](m, "DurationOnUse"), + ReapplicationDelay: nbtconv.TickDuration[int32](m, "ReapplicationDelay"), }.New() } @@ -74,11 +74,11 @@ func (areaEffectCloudType) EncodeNBT(data *world.EntityData) map[string]any { a := data.Data.(*AreaEffectCloudBehaviour) return map[string]any{ "PotionId": int32(a.conf.Potion.Uint8()), - "ReapplicationDelay": int32(a.conf.ReapplicationDelay), + "ReapplicationDelay": int32(a.conf.ReapplicationDelay / (time.Second / 20)), "RadiusPerTick": float32(a.conf.RadiusTickGrowth), "RadiusOnUse": float32(a.conf.RadiusUseGrowth), - "DurationOnUse": int32(a.conf.DurationUseGrowth), + "DurationOnUse": int32(a.conf.DurationUseGrowth / (time.Second / 20)), "Radius": float32(a.radius), - "Duration": int32(a.duration), + "Duration": int32(a.duration / (time.Second / 20)), } } diff --git a/server/entity/area_effect_cloud_behaviour.go b/server/entity/area_effect_cloud_behaviour.go index 056b944439..86539a05a8 100644 --- a/server/entity/area_effect_cloud_behaviour.go +++ b/server/entity/area_effect_cloud_behaviour.go @@ -1,12 +1,13 @@ package entity import ( + "iter" + "time" + "github.com/df-mc/dragonfly/server/entity/effect" "github.com/df-mc/dragonfly/server/item/potion" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" - "iter" - "time" ) // AreaEffectCloudBehaviourConfig contains optional parameters for an area @@ -66,6 +67,11 @@ type AreaEffectCloudBehaviour struct { targets map[*world.EntityHandle]time.Duration } +// PortalTravelComputer returns the interdimensional travel state for the behaviour. +func (a *AreaEffectCloudBehaviour) PortalTravelComputer() *PortalTravelComputer { + return a.stationary.PortalTravelComputer() +} + // Radius returns the current radius of the area effect cloud. func (a *AreaEffectCloudBehaviour) Radius() float64 { return a.radius @@ -92,7 +98,7 @@ func (a *AreaEffectCloudBehaviour) Tick(e *Ent, tx *world.Tx) *Movement { } } - if int16(e.Age()/(time.Second*20))%10 != 0 { + if (e.Age()/(time.Second/20))%10 != 0 { // Area effect clouds only trigger updates every ten ticks. return nil } diff --git a/server/entity/base_behaviour.go b/server/entity/base_behaviour.go new file mode 100644 index 0000000000..0f36fe0ab2 --- /dev/null +++ b/server/entity/base_behaviour.go @@ -0,0 +1,20 @@ +package entity + +// BaseBehaviour provides shared runtime state for Ent behaviours. Embed it +// to inherit common functionality, or forward methods to another instance. +type BaseBehaviour struct { + portalTravel *PortalTravelComputer +} + +// NewBaseBehaviour returns a BaseBehaviour initialised with the default Ent runtime behaviour. +func NewBaseBehaviour() BaseBehaviour { + return BaseBehaviour{portalTravel: NewPortalTravelComputer()} +} + +// PortalTravelComputer returns the portal travel state for a behaviour. +func (b *BaseBehaviour) PortalTravelComputer() *PortalTravelComputer { + if b.portalTravel == nil { + b.portalTravel = NewPortalTravelComputer() + } + return b.portalTravel +} diff --git a/server/entity/damage.go b/server/entity/damage.go index 110378661c..beb350b196 100644 --- a/server/entity/damage.go +++ b/server/entity/damage.go @@ -45,7 +45,10 @@ type ( } // ExplosionDamageSource is used for damage caused by an explosion. - ExplosionDamageSource struct{} + ExplosionDamageSource struct { + // Source is the source of the explosion that dealt the damage. + Source world.ExplosionSource + } ) func (FallDamageSource) ReducedByArmour() bool { return false } diff --git a/server/entity/damageable.go b/server/entity/damageable.go new file mode 100644 index 0000000000..6060510097 --- /dev/null +++ b/server/entity/damageable.go @@ -0,0 +1,38 @@ +package entity + +import "github.com/df-mc/dragonfly/server/world" + +// behaviourDamageable represents a Behaviour that may be hurt directly without +// implementing Living. +type behaviourDamageable interface { + Hurt(e *Ent, damage float64, src world.DamageSource) (n float64, vulnerable bool) +} + +// HurtEntity hurts an entity if it is either Living or has a Behaviour that may +// be hurt directly. It returns the damage dealt, whether the entity was +// vulnerable to the damage, and whether the entity could be damaged. +func HurtEntity(e world.Entity, damage float64, src world.DamageSource) (n float64, vulnerable, ok bool) { + if l, ok := e.(Living); ok { + n, vulnerable = l.Hurt(damage, src) + return n, vulnerable, true + } + if ent, ok := e.(*Ent); ok { + if d, ok := ent.Behaviour().(behaviourDamageable); ok { + n, vulnerable = d.Hurt(ent, damage, src) + return n, vulnerable, true + } + } + return 0, false, false +} + +// DamageableEntity checks if an entity may be damaged. +func DamageableEntity(e world.Entity) bool { + if _, ok := e.(Living); ok { + return true + } + if ent, ok := e.(*Ent); ok { + _, ok = ent.Behaviour().(behaviourDamageable) + return ok + } + return false +} diff --git a/server/entity/effect/effect.go b/server/entity/effect/effect.go index b3cad3f887..0cf00802c4 100644 --- a/server/entity/effect/effect.go +++ b/server/entity/effect/effect.go @@ -1,9 +1,10 @@ package effect import ( - "github.com/df-mc/dragonfly/server/world" "image/color" "time" + + "github.com/df-mc/dragonfly/server/world" ) // LastingType represents an effect type that can have a duration. An effect @@ -183,7 +184,7 @@ type living interface { // Heal heals the entity for a given amount of health. The source passed represents the cause of the // healing, for example entity.FoodHealingSource if the entity healed by having a full food bar. If the health // added to the original health exceeds the entity's max health, Heal may not add the full amount. - Heal(health float64, source world.HealingSource) + Heal(health float64, source world.HealingSource) float64 // speed returns the current speed of the living entity. The default value is different for each entity. Speed() float64 // SetSpeed sets the speed of an entity to a new value. diff --git a/server/entity/effect/fatal_poison.go b/server/entity/effect/fatal_poison.go index efed2d4751..5deef04d15 100644 --- a/server/entity/effect/fatal_poison.go +++ b/server/entity/effect/fatal_poison.go @@ -16,7 +16,7 @@ type fatalPoison struct { // Apply ... func (fatalPoison) Apply(e world.Entity, eff Effect) { - interval := max(50>>(eff.Level()-1), 1) + interval := max(25>>(eff.Level()-1), 1) if eff.Tick()%interval == 0 { if l, ok := e.(living); ok { l.Hurt(1, PoisonDamageSource{Fatal: true}) diff --git a/server/entity/effect/poison.go b/server/entity/effect/poison.go index 9bbb80eeb6..9ef3a9c846 100644 --- a/server/entity/effect/poison.go +++ b/server/entity/effect/poison.go @@ -15,7 +15,7 @@ type poison struct { // Apply ... func (poison) Apply(e world.Entity, eff Effect) { - interval := max(50>>(eff.Level()-1), 1) + interval := max(25>>(eff.Level()-1), 1) if eff.Tick()%interval == 0 { if l, ok := e.(living); ok && l.Health() > 1 { l.Hurt(1, PoisonDamageSource{}) diff --git a/server/entity/end_crystal.go b/server/entity/end_crystal.go new file mode 100644 index 0000000000..1f06d4c87e --- /dev/null +++ b/server/entity/end_crystal.go @@ -0,0 +1,85 @@ +package entity + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/internal/nbtconv" + "github.com/df-mc/dragonfly/server/world" +) + +// NewEndCrystal creates a new End crystal entity. +func NewEndCrystal(opts world.EntitySpawnOpts) *world.EntityHandle { + return EndCrystalConfig{}.New(opts) +} + +// EndCrystalConfig holds configuration for an End crystal entity. +type EndCrystalConfig struct { + // ExplosionSize is the size of the explosion created when the End crystal is destroyed. It defaults to 6. + ExplosionSize float64 + // ShowBase specifies whether the End crystal renders its bottom base. + ShowBase bool +} + +// New creates an End crystal entity with the configuration c. +func (c EndCrystalConfig) New(opts world.EntitySpawnOpts) *world.EntityHandle { + return opts.New(EndCrystalType, c) +} + +// Apply applies the End crystal configuration to data. +func (c EndCrystalConfig) Apply(data *world.EntityData) { + if c.ExplosionSize == 0 { + c.ExplosionSize = 6 + } + data.Data = endCrystalBehaviour{ + showBase: c.ShowBase, + explosionSize: c.ExplosionSize, + } +} + +// EndCrystalType is a world.EntityType implementation for End crystals. +var EndCrystalType endCrystalType + +type endCrystalType struct{} + +func (endCrystalType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity { + return Open(tx, handle, data) +} + +func (endCrystalType) EncodeEntity() string { + return "minecraft:ender_crystal" +} + +func (endCrystalType) BBox(world.Entity) cube.BBox { + return cube.Box(-1, 0, -1, 1, 2, 1) +} + +func (endCrystalType) DecodeNBT(m map[string]any, data *world.EntityData) { + b := endCrystalBehaviour{ + showBase: nbtconv.Bool(m, "ShowBottom"), + explosionSize: nbtconv.Float64(m, "ExplosionSize"), + } + if b.explosionSize == 0 { + b.explosionSize = 6 + } + x, hasX := m["BlockTargetX"].(int32) + y, hasY := m["BlockTargetY"].(int32) + z, hasZ := m["BlockTargetZ"].(int32) + if hasX && hasY && hasZ { + b.beamTarget = cube.Pos{int(x), int(y), int(z)} + b.hasBeamTarget = true + } + b.Apply(data) +} + +func (endCrystalType) EncodeNBT(data *world.EntityData) map[string]any { + b := data.Data.(endCrystalBehaviour) + m := map[string]any{ + "ShowBottom": boolByte(b.showBase), + "ExplosionSize": b.explosionSize, + } + if b.hasBeamTarget { + m["BlockTargetX"] = int32(b.beamTarget[0]) + m["BlockTargetY"] = int32(b.beamTarget[1]) + m["BlockTargetZ"] = int32(b.beamTarget[2]) + } + return m +} diff --git a/server/entity/end_crystal_behaviour.go b/server/entity/end_crystal_behaviour.go new file mode 100644 index 0000000000..a0a1c64468 --- /dev/null +++ b/server/entity/end_crystal_behaviour.go @@ -0,0 +1,85 @@ +package entity + +import ( + "math/rand/v2" + "time" + + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +type endCrystalBehaviour struct { + showBase bool + beamTarget cube.Pos + hasBeamTarget bool + explosionSize float64 +} + +func (b endCrystalBehaviour) Apply(data *world.EntityData) { + data.Data = b +} + +// Tick continuously generates fire at the End crystal's position while in the +// End, if the block at that position is air and the block below it is not air. +func (endCrystalBehaviour) Tick(e *Ent, tx *world.Tx) *Movement { + if tx.World().Dimension() == world.End { + pos := cube.PosFromVec3(e.Position()) + if _, air := tx.Block(pos.Side(cube.FaceDown)).(block.Air); !air { + if _, air := tx.Block(pos).(block.Air); air { + fire := block.Fire{} + tx.SetBlock(pos, fire, nil) + tx.ScheduleBlockUpdate(pos, fire, time.Duration(30+rand.IntN(10))*time.Second/20) + } + } + } + return nil +} + +// Explode makes the End crystal explode itself when hit by another explosion, +// causing a chain reaction. +func (b endCrystalBehaviour) Explode(e *Ent, _ world.ExplosionSource, impact float64) { + if impact <= 0 { + return + } + explodeEndCrystal(e, b.explosionSize) +} + +// Hurt makes the End crystal explode when damaged by any source, even by +// damage that deals no health. Void damage removes it without an explosion. +func (b endCrystalBehaviour) Hurt(e *Ent, damage float64, src world.DamageSource) (float64, bool) { + damage = max(damage, 0) + if _, ok := src.(VoidDamageSource); ok { + _ = e.Close() + return damage, true + } + explodeEndCrystal(e, b.explosionSize) + return damage, true +} + +func (endCrystalBehaviour) Immobile() bool { + return true +} + +func (b endCrystalBehaviour) ShowBase() bool { + return b.showBase +} + +func (b endCrystalBehaviour) BeamTarget() (cube.Pos, bool) { + return b.beamTarget, b.hasBeamTarget +} + +// explodeEndCrystal closes the End crystal and creates a non-incendiary +// explosion at its base, if the crystal was not closed yet. +func explodeEndCrystal(e *Ent, explosionSize float64) { + if _, ok := e.H().Entity(e.tx); !ok { + return + } + _ = e.Close() + block.ExplosionConfig{ + SuppressUnderwaterImpact: true, + }.Explode(e.tx, world.EntityExplosionSource{ + Entity: e, + ExplosionSize: explosionSize, + }) +} diff --git a/server/entity/ender_pearl.go b/server/entity/ender_pearl.go index 5fd44e3749..42b905af1a 100644 --- a/server/entity/ender_pearl.go +++ b/server/entity/ender_pearl.go @@ -34,7 +34,11 @@ type teleporter interface { // teleport teleports the owner of an Ent to a trace.Result's position. func teleport(e *Ent, tx *world.Tx, target trace.Result) { - owner, _ := e.Behaviour().(*ProjectileBehaviour).Owner().Entity(tx) + behaviour := e.Behaviour().(*ProjectileBehaviour) + if behaviour.PortalTravel() { + return + } + owner, _ := behaviour.Owner().Entity(tx) if user, ok := owner.(teleporter); ok { tx.PlaySound(user.Position(), sound.Teleport{}) user.Teleport(target.Position()) diff --git a/server/entity/ent.go b/server/entity/ent.go index 1f060e4d4f..08b40857be 100644 --- a/server/entity/ent.go +++ b/server/entity/ent.go @@ -4,7 +4,6 @@ import ( "sync" "time" - "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" @@ -22,10 +21,11 @@ type Behaviour interface { // share a lot of code. It is currently under development and is prone to // (breaking) changes. type Ent struct { - tx *world.Tx - handle *world.EntityHandle - data *world.EntityData - once sync.Once + tx *world.Tx + handle *world.EntityHandle + data *world.EntityData + deferPortalTravel bool + once sync.Once } // Open converts a world.EntityHandle to an Ent in a world.Tx. @@ -42,11 +42,11 @@ func (e *Ent) Behaviour() Behaviour { } // Explode propagates the explosion behaviour of the underlying Behaviour. -func (e *Ent) Explode(src mgl64.Vec3, impact float64, conf block.ExplosionConfig) { +func (e *Ent) Explode(src world.ExplosionSource, impact float64) { if expl, ok := e.Behaviour().(interface { - Explode(e *Ent, src mgl64.Vec3, impact float64, conf block.ExplosionConfig) + Explode(e *Ent, src world.ExplosionSource, impact float64) }); ok { - expl.Explode(e, src, impact, conf) + expl.Explode(e, src, impact) } } @@ -67,6 +67,15 @@ func (e *Ent) SetVelocity(v mgl64.Vec3) { e.data.Vel = v } +// Teleport teleports the entity to the position given. +func (e *Ent) Teleport(pos mgl64.Vec3) { + viewers := e.tx.Viewers(e.data.Pos) + e.data.Pos = pos + for _, v := range viewers { + v.ViewEntityTeleport(e, pos) + } +} + // Rotation returns the rotation of the entity. func (e *Ent) Rotation() cube.Rotation { return e.data.Rot @@ -90,9 +99,7 @@ func (e *Ent) SetOnFire(duration time.Duration) { e.data.FireDuration = duration if stateChanged { - for _, v := range e.tx.Viewers(e.data.Pos) { - v.ViewEntityState(e) - } + e.updateState() } } @@ -111,7 +118,25 @@ func (e *Ent) NameTag() string { // empty string is passed. func (e *Ent) SetNameTag(s string) { e.data.Name = s - for _, v := range e.tx.Viewers(e.Position()) { + e.updateState() +} + +// AlwaysShowNameTag returns whether the name tag of the entity is shown at all +// distances instead of only when the entity is looked at from up close. +func (e *Ent) AlwaysShowNameTag() bool { + return e.data.AlwaysShowNameTag +} + +// SetAlwaysShowNameTag changes whether the name tag of the entity is shown at +// all distances instead of only when the entity is looked at from up close. +func (e *Ent) SetAlwaysShowNameTag(alwaysShow bool) { + e.data.AlwaysShowNameTag = alwaysShow + e.updateState() +} + +// updateState updates the state of the entity for all viewers of the entity. +func (e *Ent) updateState() { + for _, v := range e.tx.Viewers(e.data.Pos) { v.ViewEntityState(e) } } @@ -119,6 +144,11 @@ func (e *Ent) SetNameTag(s string) { // Tick ticks Ent, progressing its lifetime and closing the entity if it is // in the void. func (e *Ent) Tick(tx *world.Tx, current int64) { + e.deferPortalTravel = true + defer func() { + e.deferPortalTravel = false + }() + y := e.data.Pos[1] if y < float64(tx.Range()[0]) && current%10 == 0 { _ = e.Close() @@ -126,9 +156,17 @@ func (e *Ent) Tick(tx *world.Tx, current int64) { } e.SetOnFire(e.OnFireDuration() - time.Second/20) - if m := e.Behaviour().Tick(e, tx); m != nil { + m := e.Behaviour().Tick(e, tx) + if e.finishPendingPortalTravel(tx) { + return + } + if m != nil { m.Send() } + if e.checkPortalInsiders() && e.finishPendingPortalTravel(tx) { + return + } + e.stopPortalContact() e.data.Age += time.Second / 20 } @@ -140,3 +178,66 @@ func (e *Ent) Close() error { }) return nil } + +// TravelThroughPortal handles the entity touching a portal block. +func (e *Ent) TravelThroughPortal(tx *world.Tx, target world.Dimension) { + if tc := e.portalTravelComputer(); tc != nil { + if e.deferPortalTravel { + tc.queuePortalTravel(tx, target) + return + } + tc.EnterPortal(e, tx, target) + } +} + +// portalTravelComputer returns the behaviour's portal travel state, if any. +func (e *Ent) portalTravelComputer() *PortalTravelComputer { + if b, ok := e.Behaviour().(portalTravelComputerProvider); ok { + return b.PortalTravelComputer() + } + return nil +} + +// stopPortalContact resets portal contact state when no portal was touched. +func (e *Ent) stopPortalContact() { + if tc := e.portalTravelComputer(); tc != nil { + tc.StopPortalContact() + } +} + +// pendingPortalTravel reports whether this tick queued terminal portal travel. +func (e *Ent) pendingPortalTravel() bool { + if tc := e.portalTravelComputer(); tc != nil { + return tc.hasPendingPortalTravel() + } + return false +} + +// finishPendingPortalTravel starts queued terminal portal travel, if present. +func (e *Ent) finishPendingPortalTravel(tx *world.Tx) bool { + if tc := e.portalTravelComputer(); tc != nil { + return tc.finishPendingPortalTravel(e, tx) + } + return false +} + +type portalBlock interface { + Portal() world.Dimension +} + +// checkPortalInsiders checks whether the entity is inside portal blocks. +// Other EntityInsider blocks are intentionally left to entity physics. +func (e *Ent) checkPortalInsiders() bool { + box := e.H().Type().BBox(e).Translate(e.Position()).Grow(-0.0001) + low, high := cube.PosFromVec3(box.Min()), cube.PosFromVec3(box.Max()) + + for blockPos := range cube.Range3D(low, high) { + if p, ok := e.tx.Block(blockPos).(portalBlock); ok { + e.TravelThroughPortal(e.tx, p.Portal()) + if e.pendingPortalTravel() { + return true + } + } + } + return false +} diff --git a/server/entity/experience_orb_behaviour.go b/server/entity/experience_orb_behaviour.go index f90453ede9..eea158ddc0 100644 --- a/server/entity/experience_orb_behaviour.go +++ b/server/entity/experience_orb_behaviour.go @@ -1,11 +1,12 @@ package entity import ( + "math" + "time" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" - "math" - "time" ) // ExperienceOrbBehaviourConfig holds optional parameters for the creation of @@ -47,14 +48,18 @@ func (conf ExperienceOrbBehaviourConfig) New() *ExperienceOrbBehaviour { // ExperienceOrbBehaviour implements Behaviour for an experience orb entity. type ExperienceOrbBehaviour struct { - conf ExperienceOrbBehaviourConfig - + conf ExperienceOrbBehaviourConfig passive *PassiveBehaviour lastSearch time.Time target *world.EntityHandle } +// PortalTravelComputer returns the interdimensional travel state for the behaviour. +func (exp *ExperienceOrbBehaviour) PortalTravelComputer() *PortalTravelComputer { + return exp.passive.PortalTravelComputer() +} + // Experience returns the amount of experience the orb carries. func (exp *ExperienceOrbBehaviour) Experience() int { return exp.conf.Experience diff --git a/server/entity/firework.go b/server/entity/firework.go index db1353200e..a9fc8c48d0 100644 --- a/server/entity/firework.go +++ b/server/entity/firework.go @@ -2,7 +2,6 @@ package entity import ( "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/internal/nbtconv" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" ) @@ -49,12 +48,12 @@ func (fireworkType) BBox(world.Entity) cube.BBox { return cube.BBox{} } func (fireworkType) DecodeNBT(m map[string]any, data *world.EntityData) { conf := fireworkConf - conf.Firework = nbtconv.MapItem(m, "Item").Item().(item.Firework) + conf.Firework = item.MapNBT(m, "Item").Item().(item.Firework) conf.ExistenceDuration = conf.Firework.RandomisedDuration() data.Data = conf.New() } func (fireworkType) EncodeNBT(data *world.EntityData) map[string]any { - return map[string]any{"Item": nbtconv.WriteItem(item.NewStack(data.Data.(*FireworkBehaviour).Firework(), 1), true)} + return map[string]any{"Item": item.WriteNBT(item.NewStack(data.Data.(*FireworkBehaviour).Firework(), 1), true)} } diff --git a/server/entity/firework_behaviour.go b/server/entity/firework_behaviour.go index a09a09f554..ae7f68c426 100644 --- a/server/entity/firework_behaviour.go +++ b/server/entity/firework_behaviour.go @@ -51,6 +51,11 @@ type FireworkBehaviour struct { passive *PassiveBehaviour } +// PortalTravelComputer returns the interdimensional travel state for the behaviour. +func (f *FireworkBehaviour) PortalTravelComputer() *PortalTravelComputer { + return f.passive.PortalTravelComputer() +} + // Firework returns the underlying item.Firework of the FireworkBehaviour. func (f *FireworkBehaviour) Firework() item.Firework { return f.conf.Firework diff --git a/server/entity/healing.go b/server/entity/healing.go index 388963fc31..8378ef3fc1 100644 --- a/server/entity/healing.go +++ b/server/entity/healing.go @@ -3,7 +3,7 @@ package entity type ( // FoodHealingSource is a healing source used for when an entity regenerates health automatically when their food // bar is at least 90% filled. - FoodHealingSource struct{} + FoodHealingSource struct{ QuickRegeneration bool } ) func (FoodHealingSource) HealingSource() {} diff --git a/server/entity/item.go b/server/entity/item.go index edd72e250e..4b332f9457 100644 --- a/server/entity/item.go +++ b/server/entity/item.go @@ -51,7 +51,7 @@ func (itemType) BBox(world.Entity) cube.BBox { func (itemType) DecodeNBT(m map[string]any, data *world.EntityData) { conf := itemConf - conf.Item = nbtconv.MapItem(m, "Item") + conf.Item = item.MapNBT(m, "Item") conf.PickupDelay = time.Duration(nbtconv.Int64(m, "PickupDelay")) * (time.Second / 20) data.Data = conf.New() @@ -61,7 +61,7 @@ func (itemType) EncodeNBT(data *world.EntityData) map[string]any { b := data.Data.(*ItemBehaviour) return map[string]any{ "Health": int16(5), - "PickupDelay": int64(b.pickupDelay / (time.Second * 20)), - "Item": nbtconv.WriteItem(b.Item(), true), + "PickupDelay": int64(b.pickupDelay / (time.Second / 20)), + "Item": item.WriteNBT(b.Item(), true), } } diff --git a/server/entity/item_behaviour.go b/server/entity/item_behaviour.go index 807506459e..ed9e575ffc 100644 --- a/server/entity/item_behaviour.go +++ b/server/entity/item_behaviour.go @@ -6,7 +6,6 @@ import ( "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/internal/nbtconv" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" @@ -38,7 +37,7 @@ func (conf ItemBehaviourConfig) New() *ItemBehaviour { if i.Count() > i.MaxCount() { i = i.Grow(i.MaxCount() - i.Count()) } - i = nbtconv.Item(nbtconv.WriteItem(i, true), nil) + i = item.ReadNBT(item.WriteNBT(i, true), nil) if conf.PickupDelay == 0 { conf.PickupDelay = time.Second / 2 @@ -66,6 +65,11 @@ type ItemBehaviour struct { pickupDelay time.Duration } +// PortalTravelComputer returns the interdimensional travel state for the behaviour. +func (i *ItemBehaviour) PortalTravelComputer() *PortalTravelComputer { + return i.passive.PortalTravelComputer() +} + // Item returns the item.Stack held by the entity. func (i *ItemBehaviour) Item() item.Stack { return i.i @@ -93,13 +97,14 @@ func (i *ItemBehaviour) Tick(e *Ent, tx *world.Tx) *Movement { _ = e.Close() bl.CollectCooldown = 8 tx.SetBlock(blockPos, bl, nil) + return nil } return i.passive.Tick(e, tx) } // Explode reacts to explosions. The item entity is destroyed, unless the item // type is blast proof. -func (i *ItemBehaviour) Explode(e *Ent, src mgl64.Vec3, impact float64, conf block.ExplosionConfig) { +func (i *ItemBehaviour) Explode(e *Ent, _ world.ExplosionSource, impact float64) { if impact > 0 { if expl, ok := i.Item().Item().(interface{ BlastProof() bool }); ok && expl.BlastProof() { return diff --git a/server/entity/living.go b/server/entity/living.go index 0c2e30ed79..327ef97bcc 100644 --- a/server/entity/living.go +++ b/server/entity/living.go @@ -27,8 +27,9 @@ type Living interface { Hurt(damage float64, src world.DamageSource) (n float64, vulnerable bool) // Heal heals the entity for a given amount of health. The source passed represents the cause of the // healing, for example FoodHealingSource if the entity healed by having a full food bar. If the health - // added to the original health exceeds the entity's max health, Heal may not add the full amount. - Heal(health float64, src world.HealingSource) + // added to the original health exceeds the entity's max health, Heal may not add the full amount, Heal + // returns the amount of health regenerated. + Heal(health float64, src world.HealingSource) float64 // KnockBack knocks the entity back with a given force and height. A source is passed which indicates the // source of the velocity, typically the position of an attacking entity. The source is used to calculate // the direction which the entity should be knocked back in. diff --git a/server/entity/movement.go b/server/entity/movement.go index c68940c763..02ebf65d2e 100644 --- a/server/entity/movement.go +++ b/server/entity/movement.go @@ -67,7 +67,7 @@ func (c *MovementComputer) TickMovement(e world.Entity, pos, vel mgl64.Vec3, rot velBefore := vel vel = c.applyHorizontalForces(tx, pos, c.applyVerticalForces(vel)) - dPos, vel := c.checkCollision(tx, e, pos, vel) + dPos, vel := c.CheckCollision(tx, e, pos, vel) return &Movement{v: viewers, e: e, pos: pos.Add(dPos), vel: vel, dpos: dPos, dvel: vel.Sub(velBefore), @@ -115,10 +115,10 @@ func (c *MovementComputer) applyHorizontalForces(tx *world.Tx, pos, vel mgl64.Ve return vel } -// checkCollision handles the collision of the entity with blocks, adapting the velocity of the entity if it +// CheckCollision handles the collision of the entity with blocks, adapting the velocity of the entity if it // happens to collide with a block. // The final velocity and the Vec3 that the entity should move is returned. -func (c *MovementComputer) checkCollision(tx *world.Tx, e world.Entity, pos, vel mgl64.Vec3) (mgl64.Vec3, mgl64.Vec3) { +func (c *MovementComputer) CheckCollision(tx *world.Tx, e world.Entity, pos, vel mgl64.Vec3) (mgl64.Vec3, mgl64.Vec3) { // TODO: Implement collision with other entities. deltaX, deltaY, deltaZ := vel[0], vel[1], vel[2] @@ -174,15 +174,23 @@ func blockBBoxsAround(tx *world.Tx, box cube.BBox) []cube.BBox { grown := box.Grow(0.25) min, max := grown.Min(), grown.Max() minX, minY, minZ := int(math.Floor(min[0])), int(math.Floor(min[1])), int(math.Floor(min[2])) + // The maximum bounds are exclusive: A block starting exactly at the box's + // maximum cannot collide with it. maxX, maxY, maxZ := int(math.Ceil(max[0])), int(math.Ceil(max[1])), int(math.Ceil(max[2])) - // A prediction of one BBox per block, plus an additional 2, in case - blockBBoxs := make([]cube.BBox, 0, (maxX-minX)*(maxY-minY)*(maxZ-minZ)+2) - for y := minY; y <= maxY; y++ { - for x := minX; x <= maxX; x++ { - for z := minZ; z <= maxZ; z++ { + // A prediction of one BBox per block, plus an additional 2, in case. Allocate + // it lazily so that entities moving through air do not allocate an empty slice + // every tick. + predicted := (maxX-minX)*(maxY-minY)*(maxZ-minZ) + 2 + var blockBBoxs []cube.BBox + for y := minY; y < maxY; y++ { + for x := minX; x < maxX; x++ { + for z := minZ; z < maxZ; z++ { pos := cube.Pos{x, y, z} boxes := tx.Block(pos).Model().BBox(pos, tx) + if len(boxes) != 0 && blockBBoxs == nil { + blockBBoxs = make([]cube.BBox, 0, predicted) + } for _, box := range boxes { blockBBoxs = append(blockBBoxs, box.Translate(mgl64.Vec3{float64(x), float64(y), float64(z)})) } diff --git a/server/entity/passive.go b/server/entity/passive.go index d1d8b500ab..1759c06693 100644 --- a/server/entity/passive.go +++ b/server/entity/passive.go @@ -1,11 +1,10 @@ package entity import ( - "github.com/df-mc/dragonfly/server/block" - "github.com/df-mc/dragonfly/server/world" - "github.com/go-gl/mathgl/mgl64" "math" "time" + + "github.com/df-mc/dragonfly/server/world" ) // PassiveBehaviourConfig holds optional parameters for a PassiveBehaviour. @@ -36,17 +35,24 @@ func (conf PassiveBehaviourConfig) New() *PassiveBehaviour { if conf.ExistenceDuration == 0 { conf.ExistenceDuration = math.MaxInt64 } - return &PassiveBehaviour{conf: conf, fuse: conf.ExistenceDuration, mc: &MovementComputer{ - Gravity: conf.Gravity, - Drag: conf.Drag, - DragBeforeGravity: true, - }} + return &PassiveBehaviour{ + BaseBehaviour: NewBaseBehaviour(), + conf: conf, + fuse: conf.ExistenceDuration, + mc: &MovementComputer{ + Gravity: conf.Gravity, + Drag: conf.Drag, + DragBeforeGravity: true, + }, + } } // PassiveBehaviour implements Behaviour for entities that act passively. This // means that they can move, but only under influence of the environment, which // includes, for example, falling, and flowing water. type PassiveBehaviour struct { + BaseBehaviour + conf PassiveBehaviourConfig mc *MovementComputer @@ -57,8 +63,8 @@ type PassiveBehaviour struct { // Explode adds velocity to a passive entity to blast it away from the // explosion's source. -func (p *PassiveBehaviour) Explode(e *Ent, src mgl64.Vec3, impact float64, _ block.ExplosionConfig) { - e.data.Vel = e.data.Vel.Add(e.data.Pos.Sub(src).Normalize().Mul(impact)) +func (p *PassiveBehaviour) Explode(e *Ent, src world.ExplosionSource, impact float64) { + e.data.Vel = e.data.Vel.Add(e.data.Pos.Sub(src.Position()).Normalize().Mul(impact)) } // Fuse returns the leftover time until PassiveBehaviourConfig.Expire is called, @@ -80,7 +86,7 @@ func (p *PassiveBehaviour) Tick(e *Ent, tx *world.Tx) *Movement { m := p.mc.TickMovement(e, e.data.Pos, e.data.Vel, e.data.Rot, tx) e.data.Pos, e.data.Vel = m.pos, m.vel - p.fallDistance = math.Max(p.fallDistance-m.dvel[1], 0) + p.fallDistance = math.Max(p.fallDistance-m.dpos[1], 0) p.fuse = p.conf.ExistenceDuration - e.Age() diff --git a/server/entity/projectile.go b/server/entity/projectile.go index 8c8a247071..14e67ae86f 100644 --- a/server/entity/projectile.go +++ b/server/entity/projectile.go @@ -98,16 +98,24 @@ func (conf ProjectileBehaviourConfig) New() *ProjectileBehaviour { if conf.ParticleCount == 0 && conf.Particle != nil { conf.ParticleCount = 1 } - return &ProjectileBehaviour{conf: conf, collided: conf.CollisionPosition != cube.Pos{}, collisionPos: conf.CollisionPosition, mc: &MovementComputer{ - Gravity: conf.Gravity, - Drag: conf.Drag, - DragBeforeGravity: true, - }} + return &ProjectileBehaviour{ + BaseBehaviour: NewBaseBehaviour(), + conf: conf, + collided: conf.CollisionPosition != cube.Pos{}, + collisionPos: conf.CollisionPosition, + mc: &MovementComputer{ + Gravity: conf.Gravity, + Drag: conf.Drag, + DragBeforeGravity: true, + }, + } } // ProjectileBehaviour implements the behaviour of projectiles. Its specifics // may be configured using ProjectileBehaviourConfig. type ProjectileBehaviour struct { + BaseBehaviour + conf ProjectileBehaviourConfig mc *MovementComputer ageCollided int @@ -117,6 +125,7 @@ type ProjectileBehaviour struct { collided bool collidedEntities []*world.EntityHandle + portalTravel bool } // Owner returns the owner of the projectile. @@ -126,8 +135,8 @@ func (lt *ProjectileBehaviour) Owner() *world.EntityHandle { // Explode adds velocity to a projectile to blast it away from the explosion's // source. -func (lt *ProjectileBehaviour) Explode(e *Ent, src mgl64.Vec3, impact float64, _ block.ExplosionConfig) { - e.data.Vel = e.Velocity().Add(e.Position().Sub(src).Normalize().Mul(impact)) +func (lt *ProjectileBehaviour) Explode(e *Ent, src world.ExplosionSource, impact float64) { + e.data.Vel = e.Velocity().Add(e.Position().Sub(src.Position()).Normalize().Mul(impact)) } // Potion returns the potion.Potion that is applied to an entity if hit by the @@ -142,6 +151,16 @@ func (lt *ProjectileBehaviour) Critical() bool { return lt.conf.Critical && !lt.collided } +// HandlePortalTravel records that this projectile has travelled between dimensions through a portal. +func (lt *ProjectileBehaviour) HandlePortalTravel(world.Dimension, world.Dimension) { + lt.portalTravel = true +} + +// PortalTravel reports whether this projectile has travelled between dimensions through a portal. +func (lt *ProjectileBehaviour) PortalTravel() bool { + return lt.portalTravel +} + // Tick runs the tick-based behaviour of a ProjectileBehaviour and returns the // Movement within the tick. Tick handles the movement, collision and hitting // of a projectile. @@ -159,7 +178,7 @@ func (lt *ProjectileBehaviour) Tick(e *Ent, tx *world.Tx) *Movement { } vel := e.Velocity() m, result := lt.tickMovement(e, tx) - e.data.Pos, e.data.Vel = m.pos, m.vel + e.data.Pos, e.data.Vel, e.data.Rot = m.pos, m.vel, m.rot lt.collisionPos, lt.collided, lt.ageCollided = cube.Pos{}, false, 0 @@ -176,11 +195,11 @@ func (lt *ProjectileBehaviour) Tick(e *Ent, tx *world.Tx) *Movement { switch r := result.(type) { case trace.EntityResult: - if l, ok := r.Entity().(Living); ok { - if lt.conf.Damage >= 0 { - lt.hitEntity(l, e, vel) - } - lt.collidedEntities = append(lt.collidedEntities, l.H()) + if lt.conf.Damage >= 0 { + lt.hitEntity(r.Entity(), e, vel) + } + if DamageableEntity(r.Entity()) { + lt.collidedEntities = append(lt.collidedEntities, r.Entity().H()) } case trace.BlockResult: bpos := r.BlockPosition() @@ -234,7 +253,9 @@ func (lt *ProjectileBehaviour) tryPickup(e *Ent, tx *world.Tx) { if !ok { continue } - if _, ok := collector.Collect(lt.conf.PickupItem); !ok { + if n, ok := collector.Collect(lt.conf.PickupItem); !ok || n == 0 { + // The collector could not hold the item, so the projectile must stay where it is rather than being + // destroyed with nobody having received it. continue } @@ -243,6 +264,8 @@ func (lt *ProjectileBehaviour) tryPickup(e *Ent, tx *world.Tx) { for _, viewer := range tx.Viewers(e.Position()) { viewer.ViewEntityAction(e, PickedUpAction{Collector: collector}) } + // Only one collector may pick the projectile up: every further one would receive a copy of it. + return } } @@ -261,6 +284,7 @@ func (lt *ProjectileBehaviour) hitBlockSurviving(e *Ent, r trace.BlockResult, m lt.collisionPos, lt.collided = r.BlockPosition(), true for _, v := range tx.Viewers(m.pos) { + v.ViewEntityTeleport(e, m.pos) v.ViewEntityAction(e, ArrowShakeAction{Duration: time.Millisecond * 350}) v.ViewEntityState(e) } @@ -268,10 +292,9 @@ func (lt *ProjectileBehaviour) hitBlockSurviving(e *Ent, r trace.BlockResult, m } } -// hitEntity is called when a projectile hits a Living. It deals damage to the -// entity and knocks it back. Additionally, it applies any potion effects and -// fire if applicable. -func (lt *ProjectileBehaviour) hitEntity(l Living, e *Ent, vel mgl64.Vec3) { +// hitEntity is called when a projectile hits an entity. It deals damage to the +// entity if possible, and applies Living-specific effects such as knockback. +func (lt *ProjectileBehaviour) hitEntity(victim world.Entity, e *Ent, vel mgl64.Vec3) { owner, _ := lt.conf.Owner.Entity(e.tx) src := ProjectileDamageSource{Projectile: e, Owner: owner} dmg := math.Ceil(lt.conf.Damage * vel.Len()) @@ -279,7 +302,11 @@ func (lt *ProjectileBehaviour) hitEntity(l Living, e *Ent, vel mgl64.Vec3) { dmg += rand.Float64() * dmg / 2 } // TODO: Piercing arrows should bypass shield blocking when shields are implemented. - if _, vulnerable := l.Hurt(dmg, src); vulnerable { + if _, vulnerable, ok := HurtEntity(victim, dmg, src); ok && vulnerable { + l, ok := victim.(Living) + if !ok { + return + } l.KnockBack(l.Position().Sub(vel), 0.45+lt.conf.KnockBackForceAddend, 0.3608+lt.conf.KnockBackHeightAddend) for _, eff := range lt.conf.Potion.Effects() { @@ -337,7 +364,7 @@ func (lt *ProjectileBehaviour) tickMovement(e *Ent, tx *world.Tx) (*Movement, tr } // ignores returns a function to ignore entities in trace.Perform that are -// either a spectator, not living, the entity itself, its owner in the first +// either a spectator, not damageable, the entity itself, its owner in the first // 5 ticks, or an entity it already collided with. func (lt *ProjectileBehaviour) ignores(e *Ent) trace.EntityFilter { return func(seq iter.Seq[world.Entity]) iter.Seq[world.Entity] { @@ -346,10 +373,10 @@ func (lt *ProjectileBehaviour) ignores(e *Ent) trace.EntityFilter { g, ok := other.(interface{ GameMode() world.GameMode }) spectator := ok && !g.GameMode().HasCollision() itself := e.H() == other.H() - _, living := other.(Living) + damageable := DamageableEntity(other) owner := e.data.Age < time.Second/4 && lt.conf.Owner == other.H() collidedEntity := slices.Contains(lt.collidedEntities, other.H()) - if spectator || itself || !living || owner || collidedEntity { + if spectator || itself || !damageable || owner || collidedEntity { continue } if !yield(other) { diff --git a/server/entity/register.go b/server/entity/register.go index 7155ec9864..b4f4440143 100644 --- a/server/entity/register.go +++ b/server/entity/register.go @@ -14,6 +14,7 @@ var DefaultRegistry = conf.New([]world.EntityType{ ArrowType, BottleOfEnchantingType, EggType, + EndCrystalType, EnderPearlType, ExperienceOrbType, FallingBlockType, @@ -30,6 +31,7 @@ var DefaultRegistry = conf.New([]world.EntityType{ var conf = world.EntityRegistryConfig{ TNT: NewTNT, Egg: NewEgg, + EndCrystal: NewEndCrystal, Snowball: NewSnowball, BottleOfEnchanting: NewBottleOfEnchanting, EnderPearl: NewEnderPearl, diff --git a/server/entity/splashable.go b/server/entity/splashable.go index c9d1810fad..a3625c7236 100644 --- a/server/entity/splashable.go +++ b/server/entity/splashable.go @@ -1,6 +1,7 @@ package entity import ( + "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/block/cube/trace" "github.com/df-mc/dragonfly/server/entity/effect" @@ -67,12 +68,13 @@ func potionSplash(durMul float64, pot potion.Potion, linger bool) func(e *Ent, t switch result := res.(type) { case trace.BlockResult: blockPos := result.BlockPosition().Side(result.Face()) - if tx.Block(blockPos) == fire() { + if _, ok := tx.Block(blockPos).(block.Fire); ok { tx.SetBlock(blockPos, nil, nil) } for _, f := range cube.HorizontalFaces() { - if h := blockPos.Side(f); tx.Block(h) == fire() { + h := blockPos.Side(f) + if _, ok := tx.Block(h).(block.Fire); ok { tx.SetBlock(h, nil, nil) } diff --git a/server/entity/stationary.go b/server/entity/stationary.go index 3aaecd18c5..e399f3184a 100644 --- a/server/entity/stationary.go +++ b/server/entity/stationary.go @@ -31,13 +31,15 @@ func (conf StationaryBehaviourConfig) New() *StationaryBehaviour { if conf.ExistenceDuration == 0 { conf.ExistenceDuration = math.MaxInt64 } - return &StationaryBehaviour{conf: conf} + return &StationaryBehaviour{BaseBehaviour: NewBaseBehaviour(), conf: conf} } // StationaryBehaviour implements the behaviour of an entity that is unable to // move, such as a text entity or an area effect cloud. Applying velocity to // such entities will not move them. type StationaryBehaviour struct { + BaseBehaviour + conf StationaryBehaviourConfig close bool } diff --git a/server/entity/tnt.go b/server/entity/tnt.go index 99c639e8e4..2b8519f5ea 100644 --- a/server/entity/tnt.go +++ b/server/entity/tnt.go @@ -31,7 +31,10 @@ var tntConf = PassiveBehaviourConfig{ // explodeTNT creates an explosion at the position of e. func explodeTNT(e *Ent, tx *world.Tx) { - block.ExplosionConfig{ItemDropChance: 1}.Explode(tx, e.Position()) + block.ExplosionConfig{ItemDropChance: 1}.Explode(tx, world.EntityExplosionSource{ + Entity: e, + ExplosionSize: 4, + }) } // TNTType is a world.EntityType implementation for TNT. @@ -51,10 +54,10 @@ func (tntType) BBox(world.Entity) cube.BBox { func (t tntType) DecodeNBT(m map[string]any, data *world.EntityData) { conf := tntConf - conf.ExistenceDuration = nbtconv.TickDuration[uint8](m, "Fuse") + conf.ExistenceDuration = nbtconv.TickDuration[int16](m, "Fuse") data.Data = conf.New() } func (tntType) EncodeNBT(data *world.EntityData) map[string]any { - return map[string]any{"Fuse": uint8(data.Data.(*PassiveBehaviour).Fuse().Milliseconds() / 50)} + return map[string]any{"Fuse": int16(min(data.Data.(*PassiveBehaviour).Fuse()/(time.Second/20), math.MaxInt16))} } diff --git a/server/entity/travel.go b/server/entity/travel.go new file mode 100644 index 0000000000..07760ae888 --- /dev/null +++ b/server/entity/travel.go @@ -0,0 +1,323 @@ +package entity + +import ( + "context" + "sync" + "time" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/portal" + "github.com/go-gl/mathgl/mgl64" +) + +// PortalTravelComputer handles portal-triggered interdimensional travel for an entity. +type PortalTravelComputer struct { + // Instantaneous returns true if the entity should skip the portal wait timer when moving from the source to the + // target dimension. Players use this for game modes with instant portal travel and for End travel. + Instantaneous func(source, target world.Dimension) bool + // Teleport teleports the entity to the final portal position. If nil, Traveller.Teleport is used. + Teleport func(e Traveller, pos mgl64.Vec3) + // SpawnPoint returns the position the entity arrives at when returning from the End; if nil the world spawn is used. + SpawnPoint func(tx *world.Tx) mgl64.Vec3 + // Player specifies if the entity is a player. Players arrive one block lower on the End platform than other + // entities. + Player bool + // CreatePortal specifies if the entity may create a portal at the destination when none is found. Only players + // create portals; other entities only travel through portals that are already linked. + CreatePortal bool + // Cooldown is how long the entity must wait after a travel attempt before it may travel again. Non-player + // entities use 15 seconds (300 ticks). + Cooldown time.Duration + + mu sync.Mutex + start time.Time + cooldownUntil time.Time + inside bool + awaitingTravel bool + travelling bool + timedOut bool + pending *world.World +} + +// NewPortalTravelComputer creates a PortalTravelComputer for instant portal travel. +func NewPortalTravelComputer() *PortalTravelComputer { + return &PortalTravelComputer{Instantaneous: func(world.Dimension, world.Dimension) bool { return true }, Cooldown: time.Second * 15} +} + +// portalSearchRadius is the radius around the scaled arrival position searched for an existing linked portal. +// Bedrock Edition searches 128 blocks in both dimensions. +const portalSearchRadius = 128 + +// portalTravelComputerProvider is implemented by behaviours of entities that can travel through portals. +// Behaviours without a computer never travel. This matches vanilla, where some entities, such as falling blocks, +// cannot use portals. +type portalTravelComputerProvider interface { + PortalTravelComputer() *PortalTravelComputer +} + +// Traveller represents a world.Entity that can travel between dimensions. +type Traveller interface { + world.Entity + // Teleport teleports the entity to the position given. + Teleport(pos mgl64.Vec3) +} + +type portalTravelHandler interface { + HandlePortalTravel(source, destination world.Dimension) +} + +// EnterPortal handles an entity touching a portal block. It teleports the entity to the other dimension after four +// seconds or instantly if instantaneous is true. +func (t *PortalTravelComputer) EnterPortal(e Traveller, tx *world.Tx, target world.Dimension) { + if destination := t.enterPortal(tx, target); destination != nil { + t.travelQueued(e, tx, destination) + } +} + +// queuePortalTravel records portal travel to be completed by a terminal Ent tick step. +func (t *PortalTravelComputer) queuePortalTravel(tx *world.Tx, target world.Dimension) { + if destination := t.enterPortal(tx, target); destination != nil { + t.mu.Lock() + t.pending = destination + t.mu.Unlock() + } +} + +// enterPortal updates portal contact state and returns the destination world if travel should start. +func (t *PortalTravelComputer) enterPortal(tx *world.Tx, target world.Dimension) *world.World { + source := tx.World() + destination := source.PortalDestination(target) + if destination == source { + return nil + } + + t.mu.Lock() + t.inside = true + if t.timedOut { + // Timed out, we can't travel through portals. + t.mu.Unlock() + return nil + } + if time.Now().Before(t.cooldownUntil) { + t.mu.Unlock() + return nil + } + travelNow := t.instantaneous(source.Dimension(), target) || (t.awaitingTravel && time.Since(t.start) >= time.Second*4) + if !travelNow && !t.awaitingTravel { + t.start, t.awaitingTravel = time.Now(), true + } + t.mu.Unlock() + + if travelNow { + return destination + } + return nil +} + +func (t *PortalTravelComputer) instantaneous(source, target world.Dimension) bool { + return t.Instantaneous != nil && t.Instantaneous(source, target) +} + +// hasPendingPortalTravel reports whether portal travel was queued during this tick. +func (t *PortalTravelComputer) hasPendingPortalTravel() bool { + t.mu.Lock() + defer t.mu.Unlock() + return t.pending != nil +} + +// finishPendingPortalTravel consumes queued portal travel and starts the terminal transfer. +func (t *PortalTravelComputer) finishPendingPortalTravel(e Traveller, tx *world.Tx) bool { + t.mu.Lock() + destination := t.pending + t.pending = nil + t.mu.Unlock() + + if destination == nil { + return false + } + t.travel(e, tx, destination) + return true +} + +// StopPortalContact resets the portal timer if the entity was not inside a portal this tick. +func (t *PortalTravelComputer) StopPortalContact() { + t.mu.Lock() + defer t.mu.Unlock() + if t.inside { + t.inside = false + return + } + if t.travelling || t.pending != nil { + return + } + t.timedOut, t.awaitingTravel = false, false +} + +// travel removes the entity from the current world and queues it for the given Nether or Overworld world. +func (t *PortalTravelComputer) travel(e Traveller, tx *world.Tx, destination *world.World) { + source := tx.World() + if destination == nil || destination == source { + return + } + + sourceDim, destinationDim := source.Dimension(), destination.Dimension() + origin := e.Position() + pos := translatePortalPosition(cube.PosFromVec3(origin), sourceDim, destinationDim) + + t.mu.Lock() + t.travelling, t.timedOut, t.awaitingTravel = true, true, false + t.mu.Unlock() + + handle := tx.RemoveEntity(e) + if handle == nil { + t.mu.Lock() + t.travelling, t.timedOut = false, false + t.mu.Unlock() + return + } + + go t.transfer(handle, source, destination, origin, pos, sourceDim, destinationDim) +} + +// travelQueued moves the entity after the current transaction finishes. This is used by callers such as players that +// may touch a portal from the middle of a tick and continue running afterwards. +func (t *PortalTravelComputer) travelQueued(e Traveller, tx *world.Tx, destination *world.World) { + source := tx.World() + if destination == nil || destination == source { + return + } + + sourceDim, destinationDim := source.Dimension(), destination.Dimension() + origin := e.Position() + pos := translatePortalPosition(cube.PosFromVec3(origin), sourceDim, destinationDim) + + t.mu.Lock() + t.travelling, t.timedOut, t.awaitingTravel = true, true, false + t.mu.Unlock() + + h := e.H() + tx.Defer(func(tx *world.Tx) { + // Re-open the entity in the deferred transaction: The wrapper passed to travelQueued belongs to the + // transaction that is about to finish. + e, ok := h.Entity(tx) + if !ok { + t.mu.Lock() + t.travelling, t.timedOut = false, false + t.mu.Unlock() + return + } + handle := tx.RemoveEntity(e) + if handle == nil { + t.mu.Lock() + t.travelling, t.timedOut = false, false + t.mu.Unlock() + return + } + go t.transfer(handle, source, destination, origin, pos, sourceDim, destinationDim) + }) +} + +// transfer adds the removed entity to the destination world at the arrival position. If no destination portal was +// found and the entity may not create one, the entity is returned to its origin in the source world instead. +func (t *PortalTravelComputer) transfer(handle *world.EntityHandle, source, destination *world.World, origin mgl64.Vec3, pos cube.Pos, sourceDim, destinationDim world.Dimension) { + travelled, err := world.Call(context.Background(), destination, func(tx *world.Tx) (bool, error) { + spawn, ok := t.destinationSpawn(tx, sourceDim, pos) + if !ok { + return false, nil + } + if e, ok := tx.AddEntityAt(handle, spawn).(Traveller); ok { + t.finishTravel(e, spawn, sourceDim, destinationDim) + } + return true, nil + }) + if err != nil { + travelled = false + } + if !travelled { + _, err = world.Call(context.Background(), source, func(tx *world.Tx) (struct{}, error) { + tx.AddEntityAt(handle, origin) + return struct{}{}, nil + }) + if err != nil { + _ = handle.Close() + } + } + + t.mu.Lock() + t.travelling = false + t.cooldownUntil = time.Now().Add(t.Cooldown) + if !travelled { + // The entity is back inside the source portal: clear the arrival latch so it may retry once the + // cooldown expires, for example after a linked portal is built. + t.timedOut = false + } + t.mu.Unlock() +} + +// destinationSpawn returns the position the entity should be placed at in the destination world. False is returned +// if no linked nether portal was found and none could be created. +func (t *PortalTravelComputer) destinationSpawn(tx *world.Tx, sourceDim world.Dimension, pos cube.Pos) (mgl64.Vec3, bool) { + if tx.World().Dimension() == world.End { + portal.GenerateEndSpawnPlatform(tx) + return portal.EndSpawnPosition(t.Player), true + } + if sourceDim == world.End && tx.World().Dimension() == world.Overworld { + // Returning from the End leads to the configured spawn point rather than a portal. + if t.SpawnPoint != nil { + return t.SpawnPoint(tx), true + } + return tx.World().Spawn().Vec3Middle(), true + } + if !t.CreatePortal { + n, ok := portal.FindNetherPortal(tx, pos, portalSearchRadius) + if !ok { + return mgl64.Vec3{}, false + } + return n.Spawn().Vec3Middle(), true + } + if n, ok := portal.FindOrCreateNetherPortal(tx, pos, portalSearchRadius); ok { + return n.Spawn().Vec3Middle(), true + } + return mgl64.Vec3{}, false +} + +// finishTravel runs the post-transfer portal hook and places the traveller at +// the destination spawn position. +func (t *PortalTravelComputer) finishTravel(e Traveller, pos mgl64.Vec3, source, destination world.Dimension) { + handlePortalTravel(e, source, destination) + if t.Teleport != nil { + t.Teleport(e, pos) + return + } + e.Teleport(pos) +} + +// handlePortalTravel dispatches portal travel hooks to Ent behaviours and +// non-Ent travellers that implement portalTravelHandler. +func handlePortalTravel(e Traveller, source, destination world.Dimension) { + if ent, ok := e.(*Ent); ok { + if h, ok := ent.Behaviour().(portalTravelHandler); ok { + h.HandlePortalTravel(source, destination) + } + return + } + if h, ok := e.(portalTravelHandler); ok { + h.HandlePortalTravel(source, destination) + } +} + +// translatePortalPosition maps a position in the source dimension to the equivalent position in the target dimension. +// Overworld coordinates are divided by 8 when crossing to the Nether and Nether coordinates are multiplied by 8 when +// crossing to the Overworld; the Y coordinate is clamped to the target dimension's vertical range. +func translatePortalPosition(pos cube.Pos, source, target world.Dimension) cube.Pos { + switch source { + case world.Overworld: + pos[0], pos[2] = pos[0]>>3, pos[2]>>3 + case world.Nether: + pos[0], pos[2] = pos[0]*8, pos[2]*8 + } + r := target.Range() + pos[1] = min(max(pos[1], r.Min()), r.Max()) + return pos +} diff --git a/server/entity/travel_test.go b/server/entity/travel_test.go new file mode 100644 index 0000000000..950a69d68a --- /dev/null +++ b/server/entity/travel_test.go @@ -0,0 +1,594 @@ +package entity + +import ( + "context" + "testing" + "time" + + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/portal" + "github.com/go-gl/mathgl/mgl64" +) + +func TestPortalTravelComputerStopPortalContact(t *testing.T) { + t.Run("keeps timer after portal contact", func(t *testing.T) { + tc := &PortalTravelComputer{inside: true, awaitingTravel: true, start: time.Now()} + tc.StopPortalContact() + if !tc.awaitingTravel { + t.Fatal("StopPortalContact() reset travel timer after portal contact") + } + if tc.inside { + t.Fatal("StopPortalContact() did not clear portal contact for the next tick") + } + }) + + t.Run("resets timer without portal contact", func(t *testing.T) { + tc := &PortalTravelComputer{awaitingTravel: true, start: time.Now()} + tc.StopPortalContact() + if tc.awaitingTravel { + t.Fatal("StopPortalContact() kept travel timer without portal contact") + } + }) +} + +func TestEntProjectileTravelsThroughPortal(t *testing.T) { + var overworld, nether *world.World + overworld = world.Config{PortalDestination: func(dim world.Dimension) *world.World { + if dim == world.Nether { + return nether + } + return nil + }}.New() + nether = world.Config{Dim: world.Nether, PortalDestination: func(dim world.Dimension) *world.World { + if dim == world.Nether { + return overworld + } + return nil + }}.New() + t.Cleanup(func() { + _ = overworld.Close() + _ = nether.Close() + }) + + spawnRecorder := &entitySpawnRecorder{} + nether.Handle(spawnRecorder) + + sourcePos := mgl64.Vec3{80.5, 64, 80.5} + targetPortal := cube.Pos{10, 64, 10} + mustDo(t, nether, func(tx *world.Tx) { + buildActivePortal(tx, targetPortal) + }) + + handle := world.EntitySpawnOpts{Position: sourcePos}.New(EnderPearlType, enderPearlConf) + mustDo(t, overworld, func(tx *world.Tx) { + e := tx.AddEntity(handle) + (block.Portal{Axis: cube.Z}).EntityInside(cube.PosFromVec3(sourcePos), tx, e) + if _, ok := handle.Entity(tx); !ok { + t.Fatal("non-terminal portal contact removed entity before the source transaction finished") + } + }) + + waitForEntityWorld(t, handle, nether) + if entityInWorld(handle, overworld) { + t.Fatal("entity remained in the source world after portal travel") + } + if !spawnRecorder.called { + t.Fatal("destination world did not fire an entity spawn event") + } + if got, want := spawnRecorder.pos, targetPortal.Vec3Middle(); !got.ApproxEqual(want) { + t.Fatalf("destination spawn event position = %v, want %v", got, want) + } + + mustDo(t, nether, func(tx *world.Tx) { + e, ok := handle.Entity(tx) + if !ok { + t.Fatal("entity was not added to the Nether") + } + if got, want := cube.PosFromVec3(e.Position()), targetPortal; got != want { + t.Fatalf("entity position after portal travel = %v, want %v", got, want) + } + ent, ok := e.(*Ent) + if !ok { + t.Fatalf("entity after portal travel has type %T, want *Ent", e) + } + projectile, ok := ent.Behaviour().(*ProjectileBehaviour) + if !ok { + t.Fatalf("entity behaviour after portal travel has type %T, want *ProjectileBehaviour", ent.Behaviour()) + } + if !projectile.PortalTravel() { + t.Fatal("projectile portal travel state was not preserved") + } + }) +} + +func TestEntTravelsThroughPortalOnTick(t *testing.T) { + var overworld, nether *world.World + overworld = world.Config{PortalDestination: func(dim world.Dimension) *world.World { + if dim == world.Nether { + return nether + } + return nil + }}.New() + nether = world.Config{Dim: world.Nether, PortalDestination: func(dim world.Dimension) *world.World { + if dim == world.Nether { + return overworld + } + return nil + }}.New() + t.Cleanup(func() { + _ = overworld.Close() + _ = nether.Close() + }) + + sourcePortal, targetPortal := cube.Pos{80, 64, 80}, cube.Pos{10, 64, 10} + mustDo(t, overworld, func(tx *world.Tx) { + buildActivePortal(tx, sourcePortal) + }) + mustDo(t, nether, func(tx *world.Tx) { + buildActivePortal(tx, targetPortal) + }) + + handle := world.EntitySpawnOpts{Position: sourcePortal.Vec3Middle().Sub(mgl64.Vec3{1})}.New(testMovingEntType{}, testMoveConfig{delta: mgl64.Vec3{1}}) + mustDo(t, overworld, func(tx *world.Tx) { + e := tx.AddEntity(handle) + ticker, ok := e.(world.TickerEntity) + if !ok { + t.Fatalf("entity has type %T, want world.TickerEntity", e) + } + ticker.Tick(tx, 1) + }) + + waitForEntityWorld(t, handle, nether) + if entityInWorld(handle, overworld) { + t.Fatal("entity remained in the source world after tick-driven portal travel") + } + mustDo(t, nether, func(tx *world.Tx) { + e, ok := handle.Entity(tx) + if !ok { + t.Fatal("entity was not added to the Nether") + } + if got, want := cube.PosFromVec3(e.Position()), targetPortal; got != want { + t.Fatalf("entity position after tick-driven portal travel = %v, want %v", got, want) + } + if got := e.(*Ent).Age(); got != 0 { + t.Fatalf("entity age after terminal portal travel tick = %v, want 0", got) + } + }) +} + +func TestEntTravelsThroughEndPortal(t *testing.T) { + var overworld, end *world.World + overworld = world.Config{PortalDestination: func(dim world.Dimension) *world.World { + if dim == world.End { + return end + } + return nil + }}.New() + end = world.Config{Dim: world.End, PortalDestination: func(dim world.Dimension) *world.World { + if dim == world.End { + return overworld + } + return nil + }}.New() + t.Cleanup(func() { + _ = overworld.Close() + _ = end.Close() + }) + + sourcePortal := cube.Pos{50, 64, 50} + mustDo(t, overworld, func(tx *world.Tx) { + tx.SetBlock(sourcePortal, block.EndPortal{}, nil) + }) + + handle := world.EntitySpawnOpts{Position: sourcePortal.Vec3Middle().Sub(mgl64.Vec3{1})}.New(testMovingEntType{}, testMoveConfig{delta: mgl64.Vec3{1}}) + mustDo(t, overworld, func(tx *world.Tx) { + e := tx.AddEntity(handle) + ticker, ok := e.(world.TickerEntity) + if !ok { + t.Fatalf("entity has type %T, want world.TickerEntity", e) + } + ticker.Tick(tx, 1) + }) + + waitForEntityWorld(t, handle, end) + if entityInWorld(handle, overworld) { + t.Fatal("entity remained in the source world after End portal travel") + } + mustDo(t, end, func(tx *world.Tx) { + e, ok := handle.Entity(tx) + if !ok { + t.Fatal("entity was not added to the End") + } + want := mgl64.Vec3{100.5, 50, 0.5} + if got := e.Position(); !got.ApproxEqual(want) { + t.Fatalf("entity position after End travel = %v, want %v", got, want) + } + // Spawn platform: 5x5 obsidian at y=48 around x=100, z=0. + for dx := -2; dx <= 2; dx++ { + for dz := -2; dz <= 2; dz++ { + p := cube.Pos{100 + dx, 48, dz} + if _, ok := tx.Block(p).(block.Obsidian); !ok { + t.Fatalf("obsidian platform missing at %v: got %T", p, tx.Block(p)) + } + } + } + }) +} + +func TestEndReturnSpawnSelection(t *testing.T) { + t.Run("overworld uses configured spawn point", func(t *testing.T) { + w := world.New() + t.Cleanup(func() { _ = w.Close() }) + want := mgl64.Vec3{12.5, 70, -3.5} + tc := &PortalTravelComputer{SpawnPoint: func(*world.Tx) mgl64.Vec3 { return want }} + + mustDo(t, w, func(tx *world.Tx) { + got, ok := tc.destinationSpawn(tx, world.End, cube.Pos{}) + if !ok || !got.ApproxEqual(want) { + t.Fatalf("destinationSpawn() = %v, %v, want %v, true", got, ok, want) + } + }) + }) + + t.Run("overworld falls back to world spawn", func(t *testing.T) { + w := world.New() + t.Cleanup(func() { _ = w.Close() }) + tc := &PortalTravelComputer{} + + mustDo(t, w, func(tx *world.Tx) { + want := tx.World().Spawn().Vec3Middle() + got, ok := tc.destinationSpawn(tx, world.End, cube.Pos{}) + if !ok || !got.ApproxEqual(want) { + t.Fatalf("destinationSpawn() = %v, %v, want %v, true", got, ok, want) + } + }) + }) + + t.Run("nether searches for a portal", func(t *testing.T) { + w := world.Config{Dim: world.Nether}.New() + t.Cleanup(func() { _ = w.Close() }) + tc := &PortalTravelComputer{} + + mustDo(t, w, func(tx *world.Tx) { + if _, ok := tc.destinationSpawn(tx, world.End, cube.Pos{}); ok { + t.Fatal("destinationSpawn() ok = true without a linked Nether portal, want false") + } + }) + }) +} + +func TestTranslatePortalPosition(t *testing.T) { + tests := []struct { + name string + pos, want cube.Pos + source, target world.Dimension + }{ + {name: "overworld to nether", pos: cube.Pos{80, 64, 81}, want: cube.Pos{10, 64, 10}, source: world.Overworld, target: world.Nether}, + {name: "negative coordinates floor towards negative infinity", pos: cube.Pos{-15, 64, -1}, want: cube.Pos{-2, 64, -1}, source: world.Overworld, target: world.Nether}, + {name: "nether to overworld", pos: cube.Pos{10, 64, -3}, want: cube.Pos{80, 64, -24}, source: world.Nether, target: world.Overworld}, + {name: "y clamped to nether range", pos: cube.Pos{0, 319, 0}, want: cube.Pos{0, 127, 0}, source: world.Overworld, target: world.Nether}, + {name: "y clamped to overworld range", pos: cube.Pos{0, -80, 0}, want: cube.Pos{0, -64, 0}, source: world.Nether, target: world.Overworld}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := translatePortalPosition(tt.pos, tt.source, tt.target); got != tt.want { + t.Fatalf("translatePortalPosition(%v, %v, %v) = %v, want %v", tt.pos, tt.source, tt.target, got, tt.want) + } + }) + } +} + +func TestPortalTravelComputerDelayedTravel(t *testing.T) { + overworld, nether := portalWorlds(t) + _ = nether + + tc := &PortalTravelComputer{} + mustDo(t, overworld, func(tx *world.Tx) { + if destination := tc.enterPortal(tx, world.Nether); destination != nil { + t.Fatal("enterPortal() started travel before the portal timer finished") + } + if !tc.awaitingTravel { + t.Fatal("enterPortal() did not start the portal timer") + } + + // Backdate the timer to simulate the entity having stood in the portal for four seconds. + tc.start = time.Now().Add(-time.Second * 4) + if destination := tc.enterPortal(tx, world.Nether); destination != nether { + t.Fatalf("enterPortal() destination after portal timer = %v, want the Nether", destination) + } + }) +} + +func TestPortalTravelComputerCooldown(t *testing.T) { + overworld, nether := portalWorlds(t) + _ = nether + + tc := NewPortalTravelComputer() + mustDo(t, overworld, func(tx *world.Tx) { + tc.cooldownUntil = time.Now().Add(time.Hour) + if destination := tc.enterPortal(tx, world.Nether); destination != nil { + t.Fatal("enterPortal() started travel during the portal cooldown") + } + + tc.cooldownUntil = time.Now().Add(-time.Second) + if destination := tc.enterPortal(tx, world.Nether); destination != nether { + t.Fatal("enterPortal() did not start travel after the portal cooldown expired") + } + }) +} + +func TestEntPortalTravelWithoutDestinationPortal(t *testing.T) { + overworld, nether := portalWorlds(t) + + spawnRecorder := &entitySpawnRecorder{} + nether.Handle(spawnRecorder) + + sourcePos := mgl64.Vec3{80.5, 64, 80.5} + handle := world.EntitySpawnOpts{Position: sourcePos}.New(EnderPearlType, enderPearlConf) + var tc *PortalTravelComputer + mustDo(t, overworld, func(tx *world.Tx) { + e := tx.AddEntity(handle) + tc = e.(*Ent).Behaviour().(*ProjectileBehaviour).PortalTravelComputer() + (block.Portal{Axis: cube.Z}).EntityInside(cube.PosFromVec3(sourcePos), tx, e) + }) + + // The cooldown is stamped once the travel attempt finishes, after the entity was returned to the source world. + deadline := time.Now().Add(2 * time.Second) + for { + tc.mu.Lock() + done := !tc.cooldownUntil.IsZero() + tc.mu.Unlock() + if done { + break + } + if time.Now().After(deadline) { + t.Fatal("timed out waiting for the travel attempt to finish") + } + time.Sleep(10 * time.Millisecond) + } + + if !entityInWorld(handle, overworld) { + t.Fatal("entity did not return to the source world after failing to find a destination portal") + } + if spawnRecorder.called { + t.Fatal("entity was spawned in the destination world without a linked portal") + } + mustDo(t, overworld, func(tx *world.Tx) { + e, _ := handle.Entity(tx) + if got := e.Position(); !got.ApproxEqual(sourcePos) { + t.Fatalf("entity position after failed portal travel = %v, want %v", got, sourcePos) + } + }) + mustDo(t, nether, func(tx *world.Tx) { + if _, ok := portal.FindNetherPortal(tx, cube.Pos{10, 64, 10}, 16); ok { + t.Fatal("a portal was created in the destination world by a non-player entity") + } + }) +} + +func TestPortalTravelClosesHandleWhenBothWorldsClose(t *testing.T) { + source := world.Config{Synchronous: true}.New() + destination := world.Config{Dim: world.Nether, Synchronous: true}.New() + + origin := mgl64.Vec3{80.5, 64, 80.5} + handle := world.EntitySpawnOpts{Position: origin}.New(EnderPearlType, enderPearlConf) + mustDo(t, source, func(tx *world.Tx) { + e := tx.AddEntity(handle) + if removed := tx.RemoveEntity(e); removed != handle { + t.Fatal("RemoveEntity() did not return the entity handle") + } + }) + if err := destination.Close(); err != nil { + t.Fatalf("close destination world: %v", err) + } + if err := source.Close(); err != nil { + t.Fatalf("close source world: %v", err) + } + + tc := NewPortalTravelComputer() + tc.transfer(handle, source, destination, origin, cube.Pos{10, 64, 10}, world.Overworld, world.Nether) + + if !handle.Closed() { + t.Fatal("entity handle remained worldless after destination and recovery worlds closed") + } +} + +func TestPortalTravelRethrowsDestinationPanic(t *testing.T) { + source := world.Config{Synchronous: true}.New() + destination := world.Config{Dim: world.Nether, Synchronous: true}.New() + t.Cleanup(func() { + _ = source.Close() + _ = destination.Close() + }) + destination.Handle(panicSpawnHandler{}) + + origin := mgl64.Vec3{80.5, 64, 80.5} + handle := world.EntitySpawnOpts{Position: origin}.New(testMovingEntType{}, testPortalCreatorConfig{}) + mustDo(t, source, func(tx *world.Tx) { + e := tx.AddEntity(handle) + if removed := tx.RemoveEntity(e); removed != handle { + t.Fatal("RemoveEntity() did not return the entity handle") + } + }) + + defer func() { + if recovered := recover(); recovered != "spawn panic" { + t.Fatalf("transfer panic = %v, want spawn panic", recovered) + } + }() + tc := NewPortalTravelComputer() + tc.CreatePortal = true + tc.transfer(handle, source, destination, origin, cube.Pos{10, 64, 10}, world.Overworld, world.Nether) +} + +func TestFallingBlockDoesNotTravelThroughPortal(t *testing.T) { + overworld, nether := portalWorlds(t) + + spawnRecorder := &entitySpawnRecorder{} + nether.Handle(spawnRecorder) + + targetPortal := cube.Pos{10, 64, 10} + mustDo(t, nether, func(tx *world.Tx) { + buildActivePortal(tx, targetPortal) + }) + + sourcePos := mgl64.Vec3{80.5, 64, 80.5} + handle := NewFallingBlock(world.EntitySpawnOpts{Position: sourcePos}, block.Sand{}) + mustDo(t, overworld, func(tx *world.Tx) { + e := tx.AddEntity(handle) + (block.Portal{Axis: cube.Z}).EntityInside(cube.PosFromVec3(sourcePos), tx, e) + }) + + // Portal travel finishes asynchronously, so give it time to wrongly happen before asserting nothing moved. + time.Sleep(100 * time.Millisecond) + if !entityInWorld(handle, overworld) { + t.Fatal("falling block left the source world through a portal") + } + if spawnRecorder.called { + t.Fatal("falling block was spawned in the destination world") + } +} + +func TestEntPortalTravelCreatesPortal(t *testing.T) { + overworld, nether := portalWorlds(t) + + sourcePos := mgl64.Vec3{80.5, 64, 80.5} + handle := world.EntitySpawnOpts{Position: sourcePos}.New(testMovingEntType{}, testPortalCreatorConfig{}) + mustDo(t, overworld, func(tx *world.Tx) { + e := tx.AddEntity(handle) + (block.Portal{Axis: cube.Z}).EntityInside(cube.PosFromVec3(sourcePos), tx, e) + }) + + waitForEntityWorld(t, handle, nether) + mustDo(t, nether, func(tx *world.Tx) { + if _, ok := portal.FindNetherPortal(tx, cube.Pos{10, 64, 10}, 16); !ok { + t.Fatal("no portal was created in the destination world for a portal-creating entity") + } + }) +} + +// portalWorlds returns an Overworld and Nether world linked to each other through portals. +func portalWorlds(t *testing.T) (overworld, nether *world.World) { + t.Helper() + overworld = world.Config{PortalDestination: func(dim world.Dimension) *world.World { + if dim == world.Nether { + return nether + } + return nil + }}.New() + nether = world.Config{Dim: world.Nether, PortalDestination: func(dim world.Dimension) *world.World { + if dim == world.Nether { + return overworld + } + return nil + }}.New() + t.Cleanup(func() { + _ = overworld.Close() + _ = nether.Close() + }) + return overworld, nether +} + +func mustDo(t *testing.T, w *world.World, f func(tx *world.Tx)) { + t.Helper() + if err := w.Do(f).Wait(context.Background()); err != nil { + t.Fatalf("world task failed: %v", err) + } +} + +// testPortalCreatorConfig configures a test entity that may create destination portals, like a player. +type testPortalCreatorConfig struct{} + +func (testPortalCreatorConfig) Apply(data *world.EntityData) { + data.Data = &testMoveBehaviour{BaseBehaviour: BaseBehaviour{portalTravel: &PortalTravelComputer{ + Instantaneous: func(world.Dimension, world.Dimension) bool { return true }, + CreatePortal: true, + }}} +} + +func waitForEntityWorld(t *testing.T, handle *world.EntityHandle, w *world.World) { + t.Helper() + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + if entityInWorld(handle, w) { + return + } + time.Sleep(10 * time.Millisecond) + } + t.Fatal("timed out waiting for entity to change worlds") +} + +func entityInWorld(handle *world.EntityHandle, w *world.World) bool { + ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) + defer cancel() + inWorld, err := world.CallEntity(ctx, handle, func(tx *world.Tx, _ world.Entity) (bool, error) { + return tx.World() == w, nil + }) + return err == nil && inWorld +} + +func buildActivePortal(tx *world.Tx, origin cube.Pos) { + for x := range 2 { + p := origin.Add(cube.Pos{0, 0, x}) + tx.SetBlock(p.Side(cube.FaceDown), block.Obsidian{}, nil) + tx.SetBlock(p.Add(cube.Pos{0, 3}), block.Obsidian{}, nil) + } + for y := range 3 { + p := origin.Add(cube.Pos{0, y}) + tx.SetBlock(p.Side(cube.FaceNorth), block.Obsidian{}, nil) + tx.SetBlock(p.Add(cube.Pos{0, 0, 2}), block.Obsidian{}, nil) + for x := range 2 { + tx.SetBlock(p.Add(cube.Pos{0, 0, x}), block.Portal{Axis: cube.Z}, nil) + } + } +} + +type entitySpawnRecorder struct { + world.NopHandler + + called bool + pos mgl64.Vec3 +} + +type panicSpawnHandler struct{ world.NopHandler } + +func (panicSpawnHandler) HandleEntitySpawn(*world.Tx, world.Entity) { panic("spawn panic") } + +func (r *entitySpawnRecorder) HandleEntitySpawn(_ *world.Tx, e world.Entity) { + r.called = true + r.pos = e.Position() +} + +type testMoveConfig struct { + delta mgl64.Vec3 +} + +func (c testMoveConfig) Apply(data *world.EntityData) { + data.Data = &testMoveBehaviour{BaseBehaviour: NewBaseBehaviour(), delta: c.delta} +} + +type testMoveBehaviour struct { + BaseBehaviour + + delta mgl64.Vec3 +} + +func (b *testMoveBehaviour) Tick(e *Ent, _ *world.Tx) *Movement { + e.data.Pos = e.data.Pos.Add(b.delta) + return nil +} + +type testMovingEntType struct{} + +func (testMovingEntType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity { + return &Ent{tx: tx, handle: handle, data: data} +} + +func (testMovingEntType) EncodeEntity() string { return "minecraft:test_moving_ent" } +func (testMovingEntType) BBox(world.Entity) cube.BBox { + return cube.Box(-0.125, 0, -0.125, 0.125, 0.25, 0.125) +} +func (testMovingEntType) DecodeNBT(map[string]any, *world.EntityData) {} +func (testMovingEntType) EncodeNBT(*world.EntityData) map[string]any { return nil } diff --git a/server/internal/blockinternal/components.go b/server/internal/blockinternal/components.go index 86a7557d2a..ab9121ddc1 100644 --- a/server/internal/blockinternal/components.go +++ b/server/internal/blockinternal/components.go @@ -4,6 +4,7 @@ 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/item" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" ) @@ -13,18 +14,21 @@ func Components(identifier string, b world.CustomBlock, blockID int32) map[strin components := componentsFromProperties(b.Properties()) builder := NewComponentBuilder(identifier, components, blockID) if emitter, ok := b.(block.LightEmitter); ok { - builder.AddComponent("minecraft:block_light_emission", map[string]any{ - "emission": float32(emitter.LightEmissionLevel() / 15), + builder.AddComponent("minecraft:light_emission", map[string]any{ + "emission": int32(emitter.LightEmissionLevel()), }) } if diffuser, ok := b.(block.LightDiffuser); ok { - builder.AddComponent("minecraft:block_light_filter", map[string]any{ + builder.AddComponent("minecraft:light_dampening", 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 _, ok := b.(block.Breakable); ok { + // The component's value is the seconds the client takes to destroy the block. + // Bedrock has no per-tool speed component, so the bare-handed + // duration is the only one a static definition can carry. + seconds := block.BreakDuration(b, item.Stack{}, block.BreakContext{}).Seconds() + builder.AddComponent("minecraft:destructible_by_mining", map[string]any{"value": float32(seconds)}) } if frictional, ok := b.(block.Frictional); ok { builder.AddComponent("minecraft:friction", map[string]any{"value": float32(frictional.Friction())}) @@ -63,7 +67,7 @@ func componentsFromProperties(props customblock.Properties) map[string]any { if props.Geometry != "" { components["minecraft:geometry"] = map[string]any{"identifier": props.Geometry} } else if props.Cube { - components["minecraft:unit_cube"] = map[string]any{} + components["minecraft:geometry"] = map[string]any{"identifier": "minecraft:geometry.full_block"} } if props.MapColour != "" { components["minecraft:map_color"] = map[string]any{"value": props.MapColour} diff --git a/server/internal/mcrandom/mix_stafford13.go b/server/internal/mcrandom/mix_stafford13.go new file mode 100644 index 0000000000..a8a074036f --- /dev/null +++ b/server/internal/mcrandom/mix_stafford13.go @@ -0,0 +1,9 @@ +package mcrandom + +// MixStafford13 implements the Stafford 13 mixing function, a bijective mixing function +// suitable for use in pseudorandom number generators. +func MixStafford13(seed uint64) uint64 { + seed = (seed ^ (seed >> 30)) * 0xBF58476D1CE4E5B9 + seed = (seed ^ (seed >> 27)) * 0x94D049BB133111EB + return seed ^ (seed >> 31) +} diff --git a/server/internal/mcrandom/xoroshiro128plusplus.go b/server/internal/mcrandom/xoroshiro128plusplus.go new file mode 100644 index 0000000000..b965321b7f --- /dev/null +++ b/server/internal/mcrandom/xoroshiro128plusplus.go @@ -0,0 +1,25 @@ +package mcrandom + +import "math/bits" + +// Xoroshiro128PlusPlus is a member of the Xor-Shift-Rotate family of generators. Memory +// footprint is 128 bits and the period is (2^128)-1. +type Xoroshiro128PlusPlus struct { + seed0, seed1 uint64 +} + +// NewXoroshiro128PlusPlus ... +func NewXoroshiro128PlusPlus(seed0, seed1 uint64) *Xoroshiro128PlusPlus { + return &Xoroshiro128PlusPlus{seed0, seed1} +} + +// Next ... +func (x *Xoroshiro128PlusPlus) Next() uint64 { + s0 := x.seed0 + s1 := x.seed1 + result := bits.RotateLeft64(s0+s1, 17) + s0 + s1 ^= s0 + x.seed0 = bits.RotateLeft64(s0, 49) ^ s1 ^ (s1 << 21) + x.seed1 = bits.RotateLeft64(s1, 28) + return result +} diff --git a/server/internal/nbtconv/item.go b/server/internal/nbtconv/item.go index 1443ddc16d..bb60f7aedb 100644 --- a/server/internal/nbtconv/item.go +++ b/server/internal/nbtconv/item.go @@ -1,6 +1,7 @@ package nbtconv import ( + "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/item/inventory" ) @@ -8,7 +9,7 @@ import ( func InvFromNBT(inv *inventory.Inventory, items []any) { for _, itemData := range items { data, _ := itemData.(map[string]any) - it := Item(data, nil) + it := item.ReadNBT(data, nil) if it.Empty() { continue } @@ -17,13 +18,13 @@ func InvFromNBT(inv *inventory.Inventory, items []any) { } // InvToNBT encodes an inventory to a data slice which may be encoded as NBT. -func InvToNBT(inv *inventory.Inventory) []map[string]any { - var items []map[string]any +func InvToNBT(inv *inventory.Inventory) []any { + var items []any for index, i := range inv.Slots() { if i.Empty() { continue } - data := WriteItem(i, true) + data := item.WriteNBT(i, true) data["Slot"] = byte(index) items = append(items, data) } diff --git a/server/internal/nbtconv/read.go b/server/internal/nbtconv/read.go index 7eae85f7ac..e439c88144 100644 --- a/server/internal/nbtconv/read.go +++ b/server/internal/nbtconv/read.go @@ -1,12 +1,9 @@ package nbtconv import ( - "bytes" - "encoding/gob" "time" "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" "golang.org/x/exp/constraints" @@ -139,38 +136,6 @@ func PosToInt32Slice(x cube.Pos) []int32 { return []int32{int32(x[0]), int32(x[1]), int32(x[2])} } -// MapItem converts an item's name, count, damage (and properties when it is a block) in a map obtained by decoding NBT -// to a world.Item. -func MapItem(x map[string]any, k string) item.Stack { - if m, ok := x[k].(map[string]any); ok { - return Item(m, nil) - } - return item.Stack{} -} - -// Item decodes the data of an item into an item stack. -func Item(data map[string]any, s *item.Stack) item.Stack { - disk, tag := s == nil, data - if disk { - t, ok := data["tag"].(map[string]any) - if !ok { - t = map[string]any{} - } - tag = t - - a := readItemStack(data, tag) - s = &a - } - - readAnvilCost(tag, s) - readDamage(tag, s, disk) - readDisplay(tag, s) - readDragonflyData(tag, s) - readEnchantments(tag, s) - readUnbreakable(tag, s) - return *s -} - // Block decodes the data of a block into a world.Block. func Block(m map[string]any, k string) world.Block { if mk, ok := m[k].(map[string]any); ok { @@ -181,103 +146,3 @@ func Block(m map[string]any, k string) world.Block { } return nil } - -// readItemStack reads an item.Stack from the NBT in the map passed. -func readItemStack(m, t map[string]any) item.Stack { - var it world.Item - if blockItem, ok := Block(m, "Block").(world.Item); ok { - it = blockItem - } - if v, ok := world.ItemByName(String(m, "Name"), Int16(m, "Damage")); ok { - it = v - } - if it == nil { - return item.Stack{} - } - if n, ok := it.(world.NBTer); ok { - it = n.DecodeNBT(t).(world.Item) - } - return item.NewStack(it, int(Uint8(m, "Count"))) -} - -// readDamage reads the damage value stored in the NBT with the Damage tag and saves it to the item.Stack passed. -func readDamage(m map[string]any, s *item.Stack, disk bool) { - if disk { - *s = s.Damage(int(Int16(m, "Damage"))) - return - } - *s = s.Damage(int(Int32(m, "Damage"))) -} - -// readAnvilCost ... -func readAnvilCost(m map[string]any, s *item.Stack) { - *s = s.WithAnvilCost(int(Int32(m, "RepairCost"))) -} - -// readEnchantments reads the enchantments stored in the ench tag of the NBT passed and stores it into an item.Stack. -func readEnchantments(m map[string]any, s *item.Stack) { - enchantments, ok := m["ench"].([]map[string]any) - if !ok { - for _, e := range Slice(m, "ench") { - if v, ok := e.(map[string]any); ok { - enchantments = append(enchantments, v) - } - } - } - for _, ench := range enchantments { - if t, ok := item.EnchantmentByID(int(Int16(ench, "id"))); ok { - *s = s.WithForcedEnchantments(item.NewEnchantment(t, int(Int16(ench, "lvl")))) - } - } -} - -// readDisplay reads the display data present in the display field in the NBT. It includes a custom name of the item -// and the lore. -func readDisplay(m map[string]any, s *item.Stack) { - if display, ok := m["display"].(map[string]any); ok { - if name, ok := display["Name"].(string); ok { - // Only add the custom name if actually set. - *s = s.WithCustomName(name) - } - if lore, ok := display["Lore"].([]string); ok { - *s = s.WithLore(lore...) - } else if lore, ok := display["Lore"].([]any); ok { - loreLines := make([]string, 0, len(lore)) - for _, l := range lore { - loreLines = append(loreLines, l.(string)) - } - *s = s.WithLore(loreLines...) - } - } -} - -// readDragonflyData reads data written to the dragonflyData field in the NBT of an item and adds it to the item.Stack -// passed. -func readDragonflyData(m map[string]any, s *item.Stack) { - if customData, ok := m["dragonflyData"]; ok { - d, ok := customData.([]byte) - if !ok { - if itf, ok := customData.([]any); ok { - for _, v := range itf { - b, _ := v.(byte) - d = append(d, b) - } - } - } - var values []mapValue - if err := gob.NewDecoder(bytes.NewBuffer(d)).Decode(&values); err != nil { - panic("error decoding item user data: " + err.Error()) - } - for _, val := range values { - *s = s.WithValue(val.K, val.V) - } - } -} - -// readUnbreakable reads the unbreakable value stored in the NBT with the Unbreakable tag and saves it to the item.Stack -// passed. -func readUnbreakable(m map[string]any, s *item.Stack) { - if Bool(m, "Unbreakable") { - *s = s.AsUnbreakable() - } -} diff --git a/server/internal/nbtconv/write.go b/server/internal/nbtconv/write.go index 508e70b68d..a19afbb07f 100644 --- a/server/internal/nbtconv/write.go +++ b/server/internal/nbtconv/write.go @@ -1,43 +1,10 @@ package nbtconv import ( - "bytes" - "encoding/gob" - "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/chunk" - "sort" ) -// WriteItem encodes an item stack into a map that can be encoded using NBT. -func WriteItem(s item.Stack, disk bool) map[string]any { - tag := make(map[string]any) - if s.Empty() { - return tag - } - if nbt, ok := s.Item().(world.NBTer); ok { - for k, v := range nbt.EncodeNBT() { - tag[k] = v - } - } - writeAnvilCost(tag, s) - writeDamage(tag, s, disk) - writeDisplay(tag, s) - writeDragonflyData(tag, s) - writeEnchantments(tag, s) - writeUnbreakable(tag, s) - - data := make(map[string]any) - if disk { - writeItemStack(data, tag, s) - } else { - for k, v := range tag { - data[k] = v - } - } - return data -} - // WriteBlock encodes a world.Block into a map that can be encoded using NBT. func WriteBlock(b world.Block) map[string]any { name, properties := b.EncodeBlock() @@ -47,113 +14,3 @@ func WriteBlock(b world.Block) map[string]any { "version": chunk.CurrentBlockVersion, } } - -// writeItemStack writes the name, metadata value, count and NBT of an item to a map ready for NBT encoding. -func writeItemStack(m, t map[string]any, s item.Stack) { - m["Name"], m["Damage"] = s.Item().EncodeItem() - if b, ok := s.Item().(world.Block); ok { - v := map[string]any{} - writeBlock(v, b) - m["Block"] = v - } - m["Count"] = byte(s.Count()) - if len(t) > 0 { - m["tag"] = t - } -} - -// writeBlock writes the name, properties and version of a block to a map ready for NBT encoding. -func writeBlock(m map[string]any, b world.Block) { - m["name"], m["states"] = b.EncodeBlock() - m["version"] = chunk.CurrentBlockVersion -} - -// writeDragonflyData writes additional data associated with an item.Stack to a map for NBT encoding. -func writeDragonflyData(m map[string]any, s item.Stack) { - if v := s.Values(); len(v) != 0 { - buf := new(bytes.Buffer) - if err := gob.NewEncoder(buf).Encode(mapToSlice(v)); err != nil { - panic("error encoding item user data: " + err.Error()) - } - m["dragonflyData"] = buf.Bytes() - } -} - -// mapToSlice converts a map to a slice of the type mapValue and orders the slice by the keys in the map to ensure a -// deterministic order. -func mapToSlice(m map[string]any) []mapValue { - values := make([]mapValue, 0, len(m)) - for k, v := range m { - values = append(values, mapValue{K: k, V: v}) - } - sort.Slice(values, func(i, j int) bool { - return values[i].K < values[j].K - }) - return values -} - -// mapValue represents a value in a map. It is used to convert maps to a slice and order the slice before encoding to -// NBT to ensure a deterministic output. -type mapValue struct { - K string - V any -} - -// writeEnchantments writes the enchantments of an item to a map for NBT encoding. -func writeEnchantments(m map[string]any, s item.Stack) { - if len(s.Enchantments()) != 0 { - var enchantments []map[string]any - for _, e := range s.Enchantments() { - if eType, ok := item.EnchantmentID(e.Type()); ok { - enchantments = append(enchantments, map[string]any{ - "id": int16(eType), - "lvl": int16(e.Level()), - }) - } - } - m["ench"] = enchantments - } -} - -// writeDisplay writes the display name and lore of an item to a map for NBT encoding. -func writeDisplay(m map[string]any, s item.Stack) { - name, lore := s.CustomName(), s.Lore() - v := map[string]any{} - if name != "" { - v["Name"] = name - } - if len(lore) != 0 { - v["Lore"] = lore - } - if len(v) != 0 { - m["display"] = v - } -} - -// writeDamage writes the damage to an item.Stack (either an int16 for disk or int32 for network) to a map for NBT -// encoding. -func writeDamage(m map[string]any, s item.Stack, disk bool) { - if v, ok := m["Damage"]; !ok || v.(int16) == 0 { - if _, ok := s.Item().(item.Durable); ok { - if disk { - m["Damage"] = int16(s.MaxDurability() - s.Durability()) - } else { - m["Damage"] = int32(s.MaxDurability() - s.Durability()) - } - } - } -} - -// writeAnvilCost ... -func writeAnvilCost(m map[string]any, s item.Stack) { - if cost := s.AnvilCost(); cost > 0 { - m["RepairCost"] = int32(cost) - } -} - -// writeUnbreakable writes the unbreakable tag to an item stack if it is unbreakable. -func writeUnbreakable(m map[string]any, s item.Stack) { - if s.Unbreakable() { - m["Unbreakable"] = byte(1) - } -} diff --git a/server/item/armour.go b/server/item/armour.go index 935c453ac0..17f44a5392 100644 --- a/server/item/armour.go +++ b/server/item/armour.go @@ -88,7 +88,7 @@ func (ArmourTierGold) Name() string { return "golden" } // ArmourTierChain is the ArmourTier of chain armour. type ArmourTierChain struct{} -func (ArmourTierChain) BaseDurability() float64 { return 166 } +func (ArmourTierChain) BaseDurability() float64 { return 165 } func (ArmourTierChain) Toughness() float64 { return 0 } func (ArmourTierChain) KnockBackResistance() float64 { return 0 } func (ArmourTierChain) EnchantmentValue() int { return 12 } @@ -115,7 +115,7 @@ func (ArmourTierDiamond) Name() string { return "diamond" } // ArmourTierNetherite is the ArmourTier of netherite armour. type ArmourTierNetherite struct{} -func (ArmourTierNetherite) BaseDurability() float64 { return 408 } +func (ArmourTierNetherite) BaseDurability() float64 { return 407 } func (ArmourTierNetherite) Toughness() float64 { return 3 } func (ArmourTierNetherite) KnockBackResistance() float64 { return 0.1 } func (ArmourTierNetherite) EnchantmentValue() int { return 15 } diff --git a/server/item/creative/creative_items.nbt b/server/item/creative/creative_items.nbt index ebb532f65b18848b4d5b4ce61f77244f85e42770..d8319469be8db2b1336e9ff4ad18982f4d399025 100644 GIT binary patch delta 2923 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 diff --git a/server/item/crossbow.go b/server/item/crossbow.go index ed90384f43..d642087754 100644 --- a/server/item/crossbow.go +++ b/server/item/crossbow.go @@ -2,7 +2,6 @@ package item import ( "time" - _ "unsafe" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" @@ -158,7 +157,7 @@ func (c Crossbow) ReleaseCharge(releaser Releaser, tx *world.Tx, ctx *UseContext // CanCharge ... func (c Crossbow) CanCharge(releaser Releaser, _ *world.Tx, ctx *UseContext) bool { _, found := c.findProjectile(releaser, ctx) - return found && !c.Item.Empty() + return found && c.Item.Empty() } // shoot fires the crossbow's loaded projectiles. @@ -187,7 +186,7 @@ func (c Crossbow) shoot(releaser Releaser, tx *world.Tx, offsetAngle float64, ar } // applyDamage applies damage on a UseContext based on the projectile loaded -// in the crossboww. +// in the crossbow. func (c Crossbow) applyDamage(ctx *UseContext) { if _, ok := c.Item.Item().(Firework); ok { ctx.DamageItem(3) @@ -226,24 +225,14 @@ func (Crossbow) EncodeItem() (name string, meta int16) { // DecodeNBT ... func (c Crossbow) DecodeNBT(data map[string]any) any { - c.Item = mapItem(data, "chargedItem") + c.Item = MapNBT(data, "chargedItem") return c } // EncodeNBT ... func (c Crossbow) EncodeNBT() map[string]any { if !c.Item.Empty() { - return map[string]any{"chargedItem": writeItem(c.Item, true)} + return map[string]any{"chargedItem": WriteNBT(c.Item, true)} } return nil } - -// noinspection ALL -// -//go:linkname writeItem github.com/df-mc/dragonfly/server/internal/nbtconv.WriteItem -func writeItem(s Stack, disk bool) map[string]any - -// noinspection ALL -// -//go:linkname mapItem github.com/df-mc/dragonfly/server/internal/nbtconv.MapItem -func mapItem(x map[string]any, k string) Stack diff --git a/server/item/enchantment/respiration.go b/server/item/enchantment/respiration.go index fb42dd833c..bc966bf933 100644 --- a/server/item/enchantment/respiration.go +++ b/server/item/enchantment/respiration.go @@ -34,7 +34,7 @@ func (respiration) Rarity() item.EnchantmentRarity { // Chance returns the chance of the enchantment blocking the air supply from ticking. func (respiration) Chance(level int) float64 { - return 1.0 / float64(level+1) + return float64(level) / float64(level+1) } // CompatibleWithEnchantment ... diff --git a/server/item/end_crystal.go b/server/item/end_crystal.go new file mode 100644 index 0000000000..e21a5614e1 --- /dev/null +++ b/server/item/end_crystal.go @@ -0,0 +1,53 @@ +package item + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" +) + +// EndCrystal is an item that can be placed on obsidian or bedrock to spawn an End crystal entity. +type EndCrystal struct{} + +// endCrystalSupport represents a block that supports an End crystal. +type endCrystalSupport interface { + SupportsEndCrystal() bool +} + +// UseOnBlock places an End crystal on top of the clicked block if it is +// obsidian or bedrock, the two blocks above it are air and no other entities +// intersect the space the crystal is placed in. The face clicked is ignored. +func (e EndCrystal) UseOnBlock(pos cube.Pos, _ cube.Face, _ mgl64.Vec3, tx *world.Tx, _ User, ctx *UseContext) bool { + support, ok := tx.Block(pos).(endCrystalSupport) + if !ok || !support.SupportsEndCrystal() { + return false + } + + above, twoAbove := pos.Side(cube.FaceUp), pos.Side(cube.FaceUp).Side(cube.FaceUp) + if above.OutOfBounds(tx.Range()) || twoAbove.OutOfBounds(tx.Range()) { + return false + } + if tx.Block(above) != air() || tx.Block(twoAbove) != air() { + return false + } + + // Vanilla anchors the entity check at the clicked block, spanning it and the + // air block above. Any intersecting entity blocks placement, including the + // placing player. + box := cube.Box(0, 0, 0, 1, 2, 1).Translate(pos.Vec3()) + for entity := range tx.EntitiesWithin(box.Grow(2)) { + if entity.H().Type().BBox(entity).Translate(entity.Position()).IntersectsWith(box) { + return false + } + } + + opts := world.EntitySpawnOpts{Position: pos.Side(cube.FaceUp).Vec3Middle()} + tx.AddEntity(tx.World().EntityRegistry().Config().EndCrystal(opts)) + ctx.SubtractFromCount(1) + return true +} + +// EncodeItem ... +func (EndCrystal) EncodeItem() (name string, meta int16) { + return "minecraft:end_crystal", 0 +} diff --git a/server/item/ender_eye.go b/server/item/ender_eye.go new file mode 100644 index 0000000000..e50eec298c --- /dev/null +++ b/server/item/ender_eye.go @@ -0,0 +1,47 @@ +package item + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/portal" + "github.com/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" +) + +// EnderEye is the item used to fill End portal frames. The stronghold-locating throw is not implemented. +type EnderEye struct{} + +// EncodeItem ... +func (EnderEye) EncodeItem() (name string, meta int16) { + return "minecraft:ender_eye", 0 +} + +// MaxCount ... +func (EnderEye) MaxCount() int { + return 64 +} + +// endPortalFrame is implemented by block.EndPortalFrame, which cannot be imported here directly. +type endPortalFrame interface { + InsertEndPortalEye() (world.Block, bool) +} + +// UseOnBlock fills the targeted End portal frame with an Eye of Ender, activating the portal if this completes the +// twelve-frame ring. +func (EnderEye) UseOnBlock(pos cube.Pos, _ cube.Face, _ mgl64.Vec3, tx *world.Tx, _ User, ctx *UseContext) bool { + f, ok := tx.Block(pos).(endPortalFrame) + if !ok { + return false + } + updated, inserted := f.InsertEndPortalEye() + if !inserted { + return false + } + tx.SetBlock(pos, updated, nil) + tx.PlaySound(pos.Vec3Centre(), sound.EnderEyePlaced{}) + if portal.ActivateEndPortal(tx, pos) { + tx.PlaySound(pos.Vec3Centre(), sound.EndPortalCreated{}) + } + ctx.SubtractFromCount(1) + return true +} diff --git a/server/item/fire_charge.go b/server/item/fire_charge.go index b74653b019..7199650c40 100644 --- a/server/item/fire_charge.go +++ b/server/item/fire_charge.go @@ -3,6 +3,7 @@ package item import ( "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/portal" "github.com/df-mc/dragonfly/server/world/sound" "github.com/go-gl/mathgl/mgl64" "math/rand/v2" @@ -27,6 +28,9 @@ func (f FireCharge) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *w } else if s := pos.Side(face); tx.Block(s) == air() { ctx.SubtractFromCount(1) tx.PlaySound(s.Vec3Centre(), sound.FireCharge{}) + if portal.ActivateNetherPortal(tx, s) { + return true + } flame := fire() tx.SetBlock(s, flame, nil) diff --git a/server/item/flint_and_steel.go b/server/item/flint_and_steel.go index db8674601e..a0b2b94162 100644 --- a/server/item/flint_and_steel.go +++ b/server/item/flint_and_steel.go @@ -6,6 +6,7 @@ import ( "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/portal" "github.com/df-mc/dragonfly/server/world/sound" "github.com/go-gl/mathgl/mgl64" ) @@ -35,10 +36,14 @@ type ignitable interface { // UseOnBlock ... func (f FlintAndSteel) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user User, ctx *UseContext) bool { ctx.DamageItem(1) - if l, ok := tx.Block(pos).(ignitable); ok && l.Ignite(pos, tx, user) { - return true - } else if s := pos.Side(face); tx.Block(s) == air() { + if l, ok := tx.Block(pos).(ignitable); ok { + return l.Ignite(pos, tx, user) + } + if s := pos.Side(face); tx.Block(s) == air() { tx.PlaySound(s.Vec3Centre(), sound.Ignite{}) + if portal.ActivateNetherPortal(tx, s) { + return true + } flame := fire() tx.SetBlock(s, flame, nil) diff --git a/server/item/goat_horn.go b/server/item/goat_horn.go index 2a6e1759cc..41af9b694e 100644 --- a/server/item/goat_horn.go +++ b/server/item/goat_horn.go @@ -28,9 +28,7 @@ func (GoatHorn) Cooldown() time.Duration { // Use ... func (g GoatHorn) Use(tx *world.Tx, user User, _ *UseContext) bool { tx.PlaySound(user.Position(), sound.GoatHorn{Horn: g.Type}) - time.AfterFunc(time.Second, func() { - user.H().ExecWorld(g.releaseItem) - }) + user.H().DoAfter(time.Second, g.releaseItem) return true } diff --git a/server/item/leggings.go b/server/item/leggings.go index eea46a4249..c766f71bbf 100644 --- a/server/item/leggings.go +++ b/server/item/leggings.go @@ -65,7 +65,7 @@ func (l Leggings) Leggings() bool { // DurabilityInfo ... func (l Leggings) DurabilityInfo() DurabilityInfo { return DurabilityInfo{ - MaxDurability: int(l.Tier.BaseDurability() + l.Tier.BaseDurability()/2.5), + MaxDurability: int(l.Tier.BaseDurability() + l.Tier.BaseDurability()/2.75), BrokenItem: simpleItem(Stack{}), } } diff --git a/server/item/nbt.go b/server/item/nbt.go new file mode 100644 index 0000000000..2fd6c3cf4a --- /dev/null +++ b/server/item/nbt.go @@ -0,0 +1,337 @@ +package item + +import ( + "bytes" + "encoding/gob" + "sort" + + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/chunk" +) + +// WriteNBT encodes a Stack into a map that can be encoded using NBT. disk +// selects the disk format over the network one, which differ in how an item's +// damage is written and in whether the stack is wrapped with its name and count. +func WriteNBT(s Stack, disk bool) map[string]any { + tag := make(map[string]any) + if s.Empty() { + return tag + } + if nbt, ok := s.Item().(world.NBTer); ok { + for k, v := range nbt.EncodeNBT() { + tag[k] = v + } + } + writeAnvilCost(tag, s) + writeDamage(tag, s, disk) + writeDisplay(tag, s) + writeDragonflyData(tag, s) + writeEnchantments(tag, s) + writeUnbreakable(tag, s) + + data := make(map[string]any) + if disk { + writeItemStack(data, tag, s) + } else { + for k, v := range tag { + data[k] = v + } + } + return data +} + +// ReadNBT decodes the data of an item into an item stack. A nil s selects the +// disk format, in which the item's name and count are read from data itself; +// otherwise the tags are applied to the stack passed. +func ReadNBT(data map[string]any, s *Stack) Stack { + disk, tag := s == nil, data + if disk { + t, ok := data["tag"].(map[string]any) + if !ok { + t = map[string]any{} + } + tag = t + + a := readItemStack(data, tag) + s = &a + } + + readAnvilCost(tag, s) + readDamage(tag, s, disk) + readDisplay(tag, s) + readDragonflyData(tag, s) + readEnchantments(tag, s) + readUnbreakable(tag, s) + return *s +} + +// MapNBT converts an item's name, count, damage (and properties when it is a +// block) in a map obtained by decoding NBT at key k to a Stack. +func MapNBT(x map[string]any, k string) Stack { + if m, ok := x[k].(map[string]any); ok { + return ReadNBT(m, nil) + } + return Stack{} +} + +// writeItemStack writes the name, metadata value, count and NBT of an item to a map ready for NBT encoding. +func writeItemStack(m, t map[string]any, s Stack) { + m["Name"], m["Damage"] = s.Item().EncodeItem() + if b, ok := s.Item().(world.Block); ok { + v := map[string]any{} + writeBlock(v, b) + m["Block"] = v + } + m["Count"] = byte(s.Count()) + if len(t) > 0 { + m["tag"] = t + } +} + +// writeBlock writes the name, properties and version of a block to a map ready for NBT encoding. +func writeBlock(m map[string]any, b world.Block) { + m["name"], m["states"] = b.EncodeBlock() + m["version"] = chunk.CurrentBlockVersion +} + +// writeDragonflyData writes additional data associated with a Stack to a map for NBT encoding. +func writeDragonflyData(m map[string]any, s Stack) { + if v := s.Values(); len(v) != 0 { + buf := new(bytes.Buffer) + if err := gob.NewEncoder(buf).Encode(mapToSlice(v)); err != nil { + panic("error encoding item user data: " + err.Error()) + } + m["dragonflyData"] = buf.Bytes() + } +} + +// mapToSlice converts a map to a slice of the type mapValue and orders the slice by the keys in the map to ensure a +// deterministic order. +func mapToSlice(m map[string]any) []mapValue { + values := make([]mapValue, 0, len(m)) + for k, v := range m { + values = append(values, mapValue{K: k, V: v}) + } + sort.Slice(values, func(i, j int) bool { + return values[i].K < values[j].K + }) + return values +} + +// mapValue represents a value in a map. It is used to convert maps to a slice and order the slice before encoding to +// NBT to ensure a deterministic output. +// +// Its name and field names are part of the on-disk format: gob identifies the +// type by name, so worlds written before this moved package still decode. +type mapValue struct { + K string + V any +} + +// writeEnchantments writes the enchantments of an item to a map for NBT encoding. +func writeEnchantments(m map[string]any, s Stack) { + if len(s.Enchantments()) != 0 { + var enchantments []map[string]any + for _, e := range s.Enchantments() { + if eType, ok := EnchantmentID(e.Type()); ok { + enchantments = append(enchantments, map[string]any{ + "id": int16(eType), + "lvl": int16(e.Level()), + }) + } + } + m["ench"] = enchantments + } +} + +// writeDisplay writes the display name and lore of an item to a map for NBT encoding. +func writeDisplay(m map[string]any, s Stack) { + name, lore := s.CustomName(), s.Lore() + v := map[string]any{} + if name != "" { + v["Name"] = name + } + if len(lore) != 0 { + v["Lore"] = lore + } + if len(v) != 0 { + m["display"] = v + } +} + +// writeDamage writes the damage to a Stack (either an int16 for disk or int32 for network) to a map for NBT +// encoding. +func writeDamage(m map[string]any, s Stack, disk bool) { + if v, ok := m["Damage"]; !ok || v.(int16) == 0 { + if _, ok := s.Item().(Durable); ok { + if disk { + m["Damage"] = int16(s.MaxDurability() - s.Durability()) + } else { + m["Damage"] = int32(s.MaxDurability() - s.Durability()) + } + } + } +} + +// writeAnvilCost ... +func writeAnvilCost(m map[string]any, s Stack) { + if cost := s.AnvilCost(); cost > 0 { + m["RepairCost"] = int32(cost) + } +} + +// writeUnbreakable writes the unbreakable tag to an item stack if it is unbreakable. +func writeUnbreakable(m map[string]any, s Stack) { + if s.Unbreakable() { + m["Unbreakable"] = byte(1) + } +} + +// readItemStack reads a Stack from the NBT in the map passed. +func readItemStack(m, t map[string]any) Stack { + var it world.Item + if blockItem, ok := nbtBlock(m, "Block").(world.Item); ok { + it = blockItem + } + if v, ok := world.ItemByName(nbtString(m, "Name"), nbtInt16(m, "Damage")); ok { + it = v + } + if it == nil { + return Stack{} + } + if n, ok := it.(world.NBTer); ok { + it = n.DecodeNBT(t).(world.Item) + } + return NewStack(it, int(nbtUint8(m, "Count"))) +} + +// readDamage reads the damage value stored in the NBT with the Damage tag and saves it to the Stack passed. +func readDamage(m map[string]any, s *Stack, disk bool) { + if disk { + *s = s.Damage(int(nbtInt16(m, "Damage"))) + return + } + *s = s.Damage(int(nbtInt32(m, "Damage"))) +} + +// readAnvilCost ... +func readAnvilCost(m map[string]any, s *Stack) { + *s = s.WithAnvilCost(int(nbtInt32(m, "RepairCost"))) +} + +// readEnchantments reads the enchantments stored in the ench tag of the NBT passed and stores it into a Stack. +func readEnchantments(m map[string]any, s *Stack) { + enchantments, ok := m["ench"].([]map[string]any) + if !ok { + for _, e := range nbtSlice(m, "ench") { + if v, ok := e.(map[string]any); ok { + enchantments = append(enchantments, v) + } + } + } + for _, ench := range enchantments { + if t, ok := EnchantmentByID(int(nbtInt16(ench, "id"))); ok { + *s = s.WithForcedEnchantments(NewEnchantment(t, int(nbtInt16(ench, "lvl")))) + } + } +} + +// readDisplay reads the display data present in the display field in the NBT. It includes a custom name of the item +// and the lore. +func readDisplay(m map[string]any, s *Stack) { + if display, ok := m["display"].(map[string]any); ok { + if name, ok := display["Name"].(string); ok { + // Only add the custom name if actually set. + *s = s.WithCustomName(name) + } + if lore, ok := display["Lore"].([]string); ok { + *s = s.WithLore(lore...) + } else if lore, ok := display["Lore"].([]any); ok { + loreLines := make([]string, 0, len(lore)) + for _, l := range lore { + loreLines = append(loreLines, l.(string)) + } + *s = s.WithLore(loreLines...) + } + } +} + +// readDragonflyData reads data written to the dragonflyData field in the NBT of an item and adds it to the Stack +// passed. +func readDragonflyData(m map[string]any, s *Stack) { + if customData, ok := m["dragonflyData"]; ok { + d, ok := customData.([]byte) + if !ok { + if itf, ok := customData.([]any); ok { + for _, v := range itf { + b, _ := v.(byte) + d = append(d, b) + } + } + } + var values []mapValue + if err := gob.NewDecoder(bytes.NewBuffer(d)).Decode(&values); err != nil { + panic("error decoding item user data: " + err.Error()) + } + for _, val := range values { + *s = s.WithValue(val.K, val.V) + } + } +} + +// readUnbreakable reads the unbreakable value stored in the NBT with the Unbreakable tag and saves it to the Stack +// passed. +func readUnbreakable(m map[string]any, s *Stack) { + if nbtBool(m, "Unbreakable") { + *s = s.AsUnbreakable() + } +} + +// The readers below mirror the exported helpers in server/internal/nbtconv. +// They are duplicated rather than imported because nbtconv depends on this +// package: the item codec has to live here for the dependency to point the +// right way, and these are the only helpers it needs. + +// nbtBool reads a uint8 value from a map at key k and returns true if it equals 1. +func nbtBool(m map[string]any, k string) bool { return nbtUint8(m, k) == 1 } + +// nbtUint8 reads a uint8 value from a map at key k. +func nbtUint8(m map[string]any, k string) uint8 { + v, _ := m[k].(uint8) + return v +} + +// nbtString reads a string value from a map at key k. +func nbtString(m map[string]any, k string) string { + v, _ := m[k].(string) + return v +} + +// nbtInt16 reads an int16 value from a map at key k. +func nbtInt16(m map[string]any, k string) int16 { + v, _ := m[k].(int16) + return v +} + +// nbtInt32 reads an int32 value from a map at key k. +func nbtInt32(m map[string]any, k string) int32 { + v, _ := m[k].(int32) + return v +} + +// nbtSlice reads a []any value from a map at key k. +func nbtSlice(m map[string]any, k string) []any { + v, _ := m[k].([]any) + return v +} + +// nbtBlock decodes the data of a block in a map at key k into a world.Block. +func nbtBlock(m map[string]any, k string) world.Block { + if mk, ok := m[k].(map[string]any); ok { + name, _ := mk["name"].(string) + properties, _ := mk["states"].(map[string]any) + b, _ := world.BlockByName(name, properties) + return b + } + return nil +} diff --git a/server/item/optional_colour.go b/server/item/optional_colour.go new file mode 100644 index 0000000000..5374b40a8e --- /dev/null +++ b/server/item/optional_colour.go @@ -0,0 +1,47 @@ +package item + +// OptionalColour represents a Colour that may be absent. It is used by items +// and blocks that have one variant per Colour plus an additional uncoloured +// variant (e.g. shulker boxes, where the uncoloured form is encoded as +// minecraft:undyed_shulker_box). A zero OptionalColour denotes the absent +// case; values 1..16 map to the 16 dye Colours. +type OptionalColour uint8 + +// colours is a slice of all Colours. +var colours = Colours() + +// NewOptionalColour returns a new OptionalColour from a Colour. +func NewOptionalColour(c Colour) OptionalColour { + return OptionalColour(c.Uint8() + 1) +} + +// Colour returns the Colour of the OptionalColour, and a bool indicating +// whether the Colour is present. +func (oc OptionalColour) Colour() (Colour, bool) { + if oc == 0 { + return Colour{}, false + } + return colours[(oc - 1)], true +} + +// Uint8 returns the OptionalColour as a uint8. +func (oc OptionalColour) Uint8() uint8 { + return uint8(oc) +} + +// Prepend prepends the Colour to the string if the Colour is present. +func (oc OptionalColour) Prepend(str string) string { + if oc != 0 { + return colours[(oc-1)].String() + "_" + str + } + return str +} + +// OptionalColours returns a slice of all OptionalColours, including the absent case. +func OptionalColours() []OptionalColour { + optionalColours := make([]OptionalColour, 17) + for i, c := range colours { + optionalColours[i+1] = NewOptionalColour(c) + } + return optionalColours +} diff --git a/server/item/recipe/crafting_data.nbt b/server/item/recipe/crafting_data.nbt index 350fc0c124367b7a89df91ee05f6c6d44d75eb14..2a229328462414683d00d592eb96080c276e4b07 100644 GIT binary patch literal 900455 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 r!F^w!;r&V<WC&SI4Gh@CP~9@9i9xSzYJb z50bI>ci((kZY#m@uYSzyLs6Cg@*n^DE2hmipASverAgfMzzLr7Be#}qt5V(z+n9iaIng?aIGG`w!YO}tnvJy zy{iw#V&0~R_7Q`m*b_j6NTKvd@%nt^%SbVGisZE5yyrK?2*HJC)#H=L3|&Tw5mSUR zBBwiZbu&TCr7M@XUFsJ|M_pfx)Y(uetgVrzdSa=52XD$j#_{1{s-B5A;MpWgTPc9 z7bBu(c2sTD&FG=;x=3gnU~97FHdnDTi((r)TVqEL#Ke|Gz1sMA#?b0X*ul{(bDH}& z#t`doU5;VDw(DY@)h~-Gd-3s%C)ZO6v5o4%$2p!}fAMn61MA>-$MSib`&h>j>uH1- z=eGB;jic6In+4Mj`}(+wWm=C|Pa}b8s(mEWder)B7hu|Fsoz2HYE>%;0Z0cf~%94H7_Pw`}kzDY!*6e!(>q;CLiL<(FyVzvgr_aku z*vp;W_0dMm8~}ZwjF=KOFcxLrZ1Q@stlL9DO4NSRA9 z`&wwe1?Aj>Pr!4D68az8c^pIxj9=drp&m9ZZmI>6wV6)q>qs_Ht;~1CjJtztiU@Vh z6Pn(7Uw56;KOjCz~=k;b#(`$umHOePE6466U zlL*;LPw*TP>7@njmQ{uLq{njqRrZ{B(Xhwwmsv}=*yh=jFmpfWUzgn*13tAZkIHX>81C@$S!+r0{y$}g?pW<-igg>x3Tbe z&%PFVB4>1whn$lf$IDApl==Q}pB2IA3+g#7e2-!=GKqMvNZDIQF4NQyQ+jP8- zVVw&OHXMeM=+LuBlf7h5@MD4*GG&%t?NJ+O5cld}v5vnzW0FlIsh&m}4#p5L*ilI(My`H2bcR+Bo)$6Fh?Fryd$A8- zF@snpVAkj>+w+;RMZT-5W+U}}P$GafULv5;ReS)899ZrHm0L3(&k~xAu0ITdItp7&o$c3ERVx$tL7xxF@A35d zi!kX3+yc7)<#B%~*7;&xy{dab#{G+kVH%GI>~CzcgIXxS26x91n4gh$ zH1o1&2TVgVl(Y{>QH1*J4I}LXy{2(rOZz}vpS@w6;RD!!fP)i>C;!@D$+eg-xHQ1J zwV_wais-RYtAp=E3x}sbYsAqeXjJqtZl4i%V|gB5m}z+V0B_1j*S~^%E%f9rxQKv% zp(Ev~ONi@2Bb)o3Ts|DJW>tR;-#*b^C+`XFf{en81*~c{ME_FZ>Gh=Mu~76;1hr74 z5dbLe5$kybQQT`4S|~l@Q9jZ0QX;2Bti|5I&OZy975?d*Wfq==qF5HaP!PSIvb!~5 zmX1Z@SUVOFQ4Ay+lazV}c3|*)pKL-Ri==$dofB)?&m+AQNGinh0iEMT@Pj@6;m~is z(qdJvW1^K7GJ|+`X+$SRAE?e2^sH?k%Kcz!G@-|mYTKZ6qqAK z+RbqpddxDt_C}($=NMBw<@;xt;-sIzi_ZO+$e=0egNp5b?8kOFYArL@7(9IcniuuD z0)?Nfl1cHAK=~&psta`5a+|GQ7D5wjNzjp$dh~W+jE{7g&U@>?QQ~$?W*Q>*R~_MZ zrO!Y{lS~mk@n;Mnuq2ncz7%SzgvkMez3A#oRxX;nuCtZ6|B;zdn`x$?0=N*if`y9p z17AiYNAs8<=-fd;L=X)$!+Y0?2)eU(tOxe4kpQk-Y>Qn^L4g*G1TWQG+au~X&GQ}@ zC_?(9lXk7g_O9^&vW>pv+imqqAwtTYCiM%BZlgKhLj_GbLia|SS~o86t((>8(p7<_ z+O}mUMNZt2)EJIoL*#xT&h0bwXhUEjOp_qw z4CO^S*TZ|)x&p)&{JRy3&@VQsh30ur3sHm@=vt5MUE=|$XV#-|mb|6(i|raazo9~8 zg#y7n*SUClBgS@E%4k&BCYtR^QP4d&K(y@%kHY?lI`?!#e|J_Mg&aM<2X&_~)+q@< z1?y3`U$=WavOOUEOXOb3ofE-z7zVRlXiW>L;v=XJQ91ZvoE~908S4g_Y073r=`%_pYr4v z{)`^&O^3+W9Q|kk8o?0y1xU5fJddy}RC7Xyn8Bc~9ra`gxl!v5Zg`M**cYWIp$~}1 z5OSP#WMNupo=3U1rj_{)fd@l`x;AZkeh@#h+b$Kyy=|Ndu~sq>>_;sn&2=p{vdK9+ zC$K<{$xK6!9OUDw`gf`bsPh=EWX^Q-GffJ$1@2T*DDdyWku)i6n$*akCp0Y(y3KjJ zr$;COK#xz4emZ!h!tV;a4Wy$bZP-kRg>0|ILH>w3m(nFE>F=JU^Ba&(01|uX zMhnzIfNf96+Wv@u?E`cl-p}d}+_Y1wc5)7t{JS=uoR*pv4Bw7K7f(LYX9%Mg@l8FO8_ZH+ z2I4et^}Z^NOsTf$>0djX%p!`hfQCI{)~T1#98(A)FWhx_$#{y;jKqu|UPPl>kUEMY zyja)Qs*#KcE(sy-q|%+#sW1a;-ejmE$vbP_5E?O)!iea)*Dm5aTFwz& zNA%YFy6*Z~A!H_VK7UD7h6Dj0&$}a19t^D?NT3LtDBq{hspaMUk@*}6aE8`DG^Wvb zAGn6;8R%JmgW-!|rj{N8>`#A>WD;h>9WIE05;Y*j|==sFh*PxBFtd&FV$=Q*pTe zVk>7x><*}hXxDdNe=5rTvH3=c(ep|kVx}1quu4DXbyKXw)2*9q`}BD!>P%KUJ4U+% zJzvqQ&qH#qfy9UwyjZAfx1ti9lxSfq%}2|plVz!lA8>5VD=*|@^@Nh-+nVueNqNnH z_ecICNW{`W^W!3>+(L87l$f719|V7$RLhvi3=!#k4{T(X$0n=G{9vth?8%OCmDH7nDI^!) zCuYYe2A2$5Sn4 z;M&TQ4KDYI?nZu+??itlAnvr0W1amMz&*0XzTbt&eNw?Z*RlXAaGE;T_`_&zpC)X8 zH9h)wA?Y-(Not_Or?@6NshoihS7X14neoNIZpvPkCl?(_N$U`;EdoNO8`EHQ4K>(S z%aPw?9G0=YT@aG7rKH6aDPvliT$k870~z0T%%4S=vB@K3Z0a#18Pi;s5(2B+UK%~S zjJuJ)i*?*{r~`{civjF_IZs>mL=n=$*IfrRbzp$K$+(&tIE~%7--UqFSSG2#B0Re> zP8+XBQQwSltfICTn31A(&|oP~TE=u^TJr8P2F6&9=eCYdFvgCIt=Xl;bQV<&WGu&X zlQAP>%Epq*m?FHOgx{qx?l~y5zB3}x>kI@yK)oYU#5AetvG;Y|{iGfj2!N=u-_~(> z9h;18BxG8U5=LNko91T`F<8glhi?LA1Pp?{=>7!~vlJ*#PQxHl*mPrB6}hgLb4t-& zD0&wn?8&R$-E}~~0hyvMJp%5AbMgvSxza#Cw3#P|tX#{)-orLql8o`T&>R|; z0PpSke1pNf|7&~FYtrF+%%ZdRg3UCuY_7}6S<_0Yz>ABJC`+veI82&sx1=Ol>Y;YLt@rZ zwN1!`XZQM7yZ1fTQP;l!sh-_1eTiZL^yi0tLh_FOfLEAma`M6!tt5fyd(>v6lU?`v zK`Vqwp1Y7d`_9__WmUa!8SrC~O*$iN5rHq<&h4P8QJh-<>>4E7> zkubXZQf)C;dr)}YDu#ZHRZt%v9`|)o)kX8gGGDXxX}STB0M$Pq~xeiqT4hYgSdZb(ttl8d&HDarvBZGw_5# z3jyTS#+VuW3>B&Cx+Z$&kAK?zxp>M}Mfq&C+*il)rL11dke;)5@{OKT8~~=Uo4s1( zcT-K;={IejDJz(m^y8cs1PG^e;m}sHOk&!;XHU~?HO zJvCcp|KpDXiwk(*YP~$=~+p*tacmlpCCT_Cwx%EE}VyCPC`dO>|p3f~; zWQ6`tkGy=4eFI$hB#h_88E!S3k9s!pBOJ9}YDufbY&5K|r{)3}9R=H~nABhLYq1R& zxr^~CX65>y*EkuarBiVy7sWYSc}>IcAI~|3)NkQs;5udSc*m$;t&*aw|F`?uI;htF)v+&wkLsjdtkT&2Ieo&YtK9AFT^Sj>uxif zQ?a9|8cwyxMhk$F>2|!{PLqg1tlAt^cE=&ykdt~(A|9C;7)fBJnK+*RF%#2V)LEfZ zFO0K8>^0f88hH1=XnR_$vnG>n&v)`~oeCfh$0DQ0HvFip5&ZTpZ#G{J&Eg;q3W-gQ zT+Fj-;FE%$ji*>RHJ6TDGd^jPY>g6iHlh|VIFA)s?)jl;a&9enJu~l*2_H!yy}&1< zMu$NFLN}Vl3!ihy(`y)<#XVHVIZ;!VOYzN-Vo3sfYb4o*d$ECb-3+y~y5O*u;^S|+ zQ1PlNOPN8{Z5J0^Q2Aq7mHFeZNrG3A#nt_O|BLSTeI;~cyZdD5^Gwk*AMEnBx(<`n zOMJw-Q(>G|!A5gDVy}L{pY{3}Cl|iLH$*)7_Fq~Lh zuWSz(g7@ffY&x$vV?Q=ZC*Ta~u&b(OBMgIB_%g+wNdO5C5Ca1G{Pj-wfx`NfYo)aA z(_J;SjRFIC+7{)>Za+vvd*Vu~u{4kkfP-ZI;X3~)|w-3U)* z50)i}iAvTlJgzcBY_!-3!5m}QEI8ER60rYjgj^s1&&_SQJc>7T^vR#m&BShuYpSPu z|AXvmv9Cq(dmV`W9w#ma_he>G*iF8h(G!@qp zM4j;2{GL-~%mhu(l+gp;D>JDPicj86*R7oh-DSW3!-Gy`_ht7ehWfI5G!*-2lFJ0d zeo&fcx|{A^;8la`s#q|)3)h0t)OfYJ#$Z<+vSMX+8NF;)j+$moVCu$kvC7mv=uuOb zgW`O8Zdj+&V|IZ}c5h_e&W{c!d7Eg)#b2^cxsYW&LZ!h{8Rgw9dw)#*QY!sLKOR3= zj301*ity8j*NXKMSSSTA&`4wGEYW4PqjEhk#uQ-&xU7@Tr&>E7*yKaSE=jGY+*if1 z`o=B`HJfxxIqzvH3&Qa_UM+GP13Z1eKrN?sQ+uqtYA;+Scc_(?@`?9T&3T%YduW5K z?)C+2ABV;0+BB?y#{2&56IG?IjcGVpr6C1{!R)$Hb@p_^{VHX90orHn#(OR6@E_+t zeT)Ow#?2t>RH@f)BmsLz#6wv(kW=)-B0#LtVKy&wqxYgb91)RBV>-bobF~e)S@MuB zVs;C4DG^6Zc)DCUyVp(augn_Gt5xf@_=UHdokYi6V4)IhGc|G6n%OwPu?-+k;aWvoj=)+W5vs=wF+9 zM|bez=P!@@L$S^mYf&0RUjB?#6E5r<;2CzuyW@-(YbU>+6_kWnz>0f$JdI4*Zv$C@)Y~ zKbk7pjH7st7AatuiuN4yO!>wHIGawBIXDCE{A*UXl|lT9Y-;+cP?OT{)N_pQKOI}C z)=+92*Q%bR!_-z6Yz>x$sNooZmNyiU*w;+RvHZX_0LhV2%Cya7vJy%&8BCD#AdX<} zmg~jSH|LH)PD|%8XN^uMD3MiQljcq4Ozbw1eDqOZNr9cKTqB-DPMVe$W2{EACQ_-f z(Pu0H729nr-RN?}g87XtT?7O}CUI&BBqn9{GdihN=ov%0d{rnhUuPmvrKX63M;eO6+SYattZwJHGRL zhFIG?_w1fH;P1mJ9WtuOO`L_7XuT{g<~V6Bl=*UY#NLW(a&}~Y0lu2OvE6hjz9wp@ zb6bGK;bGqUoewMoIXQnDjeW16(xstN$D9S{4-!vBj@jryjw-RnptB`4Oal?waP_qS z7fp_(b)wyk=5m{@UKYaBWS`7|b9O{H0lu zLu1^rbNz)amo}?`M-L_qK#%=vR^TgAS4q(h0+7*&Afd{^5?JxGOsGo1%Y5g8F#((k zDdyJgF2yNuCj;e(AZYm8oQM#cUnLdUd|4^S2To3l@?~+zR-E>m?@VS73Y#Zm)rLBp z@q>|*#jO?nw#k15M?9TT?5TsrJ}Y%v-uHcT5z+88#b7sxt~1@~s%~(cyn%mqN#Yb6 zGLs=k4g;%@2t3fnC8FVJK(c2v3P@n@*A^$vTkE&lW?wig&5b?%At<6W9~@kwWCKJ+KRsdtq`&MO96u~8#(kEFtIKpA)5dRJkT z(^FFrW43`jSvw$4;TxV_ufa1_y-8vY6J(OI_x_mlnIz&1eKvB87$>j*T&t~BXg=&GzJh|gvGKsQ#KVj1~c$aggFzX_|=@1M~UvDa?}RM z;E+&6PuE46iHBD7!z!P~EWh5{0~~Yj!O~l%^;$&`fb-^0Go4_tIA=-{y^j_wqZ}=b zFD^V0m_)M`;PIU+(X<-zNs97q9xH)s&&!sXZ~jz?Mg+YFr)dYFhw#+gz~ElEnj)H0 zL|LsHaSm+wXxzZs1}_jH9IG)ubtNOE6HO3~j1*uHW_jd@IpEPH^PLKoKX8gV1=%}c zz-ohtwRqbbfJ8@I%sx+PkNGlEt_t7^Z?k=ISZuSOkK2OtoaZ~8lW-w<5eJ1HhN5xd zIdb%VmJ`ISk#lk9Z{T60aFu$Rz;o6ZZfDfMAldsi?w3401lBn`j0D?})bGWKvEB%y z0UH&mJi_fxL(!W%XG}m8h>Tp>kt#-4FI*{fROpnEr@x`8cgUFN$J`M0)cm&dmn{NM zM3l$F0ZP8gUQQ|7k<`=0iSgSJRPda5-+%_csYw?r-53kuzZT3`$igOMjey}l=7sUgN_qXrI?(%G_D>14Bof`?} z3zitLB+HhGor0pnj3lWxt>`x0!#C?(Jk7Zh3U_^%blAa%s}m|3N_{$ggYTf>sb08+ zJTBSv(@OXP##3*_RWIuMNfPp-;MBpJX#r9w+Nwh3GiPdIJM@k!`fJ`^ zVuup(H7slJ-DUM^+?C`@$tq*zgO}=R08mS>n>=eGH0gq@~JT^^|9^Vq-4nr^CF2Ih?f^sV}xh(+*Nb zCR^A7T^*>%U_G&F)sk|{DrV|WJ5KfU5+@ym4Y<3ynjB?wiw*p2IFrG?J9k$<~ zFWl($)7N}*!7*I~aBXa;-D)XZgZ{3%kXyIRJZbXRInOiznQC3*1BgGx1gm|PS1Dk) zAOw>6&IJPjma*z))k~pPs?p#CHI039;&UTrQ3s&;Lqg~d= z!^T8Y{5dD)oiMA4l`{v7O}G*f6Z&ytiZkd5GY*EMj-tqfF2jExyH&E@ka|J@^ytrp z0`S>#@bM0}TyJg3g-Ahz#2D+qy9Y44Vk-`WI)>=eb(KB@RMLq=0JDeVb4Dy4{ps#t zAAl@>epaIi_aQ+D=uXU*i0#ivPQd~sG_A51E4Nb*{=1wmfH^5Nwf-MsQS7XLz-?~| znMf#WHebYvz@hbGIQ=(nZ$O(KR!0HULtY*#0zXn#AmJ@ES|c>|S|Yexyvnx4vTiR7 zEZ|3tqV35s+)i7inCH_Tet=(G>^IpVA9!oQYAdzoy^;Z|hOtAE!zXsqTjt=KB90@H zb({sq45il#SZ(5^PL%PJ^dp7@xJtOncKat$29CzSR7pIbuB=VsnUOT5ydax|W@tsp zDF+~NMzjvf+SgY<^*m(&!RR07Ct7?=31;PL;Q@Rb-pK^=>@UOrjRZRE*PBRpc`Pm_ z5GkQ00R;UlvmC;7^aC)_ zJ0S@{!2sxniRQOnI}T2K%oF67tlS( zI7g!!E-va<{FS}PZg8>A>X$`Z&BX(a^R>GH&qP7J@u{%rBKeQP3>j06O;8ZUL5sF;AffxngaCZdN=YMRZ9K@+tqH5=% zD#h*ne8(MVO+`LH@F5Y?z=#(7RrmnoU;y+YHVtp}wCWuR6s%I??R&O^Q;`o?)JaO* ztnk~S{{0^w&>{15$4yf4#`iw}1~=Wl%Yjxt!M;-t{Ov?a6eZ6@A()3xuf@9A*t>!6 z$VQywXh|;P#4im{7se1InU9;*aIFRvQI6RcoUh#t9$j*v0B@Hb9lOsP$C^-Lz8zNT zDtzzWrclnt?WpqRY~+;Qh}oH*uiXH%GVvy5zUK_%*phP|F-lPl_~QIvVYevjw~mmh z&lV*rlD;wk4@r9xvy-`PPTb82Y)(d zVb-~t-LP@NEi1Ux9RZ1Hs0RSR7=k48akCn()c`8yl6eXj(DtcS8$gYpjd+pn%c|Nf z-9!KkW^h4&CCdJTx(&U1;|`fd_`_GS+2QW~VJm9y_OGc05N)b_ki{QV;c~F3pNd2G zCTaRh3{rmHTdTb()-_cG1v3xUna|oL3z2}4i4+V`wcOmj_Se5K6;rL5SA|YMY;d26 zH&9(!%1MRk^x0xptpZF_yz5%NpeyKO|LkX3Q$5$&e)B~h=opsupx0+|zlBC&66y@g z$qVbFb6-jc)OyendC7iGtg~uxiWCVG7$wqx?x3VN;Tm!bfC+&4#R#tG&Z5OBl9W7% z5?)W}bCe`VfSARjgz@7!MS?xCB#84Q391qx7b@I!4WCUKV3D}KUwKyJ(Bv1z#V#!+i5 z-Txq4FT}gxhl8l{)jo0Z@comhOCTO=TZ=MJt40*d9=P+ewe|*r%pJZTl-Em-#q8p4 zT~)PGst`xo0ACanH(B}I zJ(gNLYX|YOR{6v(E5fOcMVNI9m7@Z1;gc|4a@?X;%k86QHe6G^?n2L>85h@jA8Iau z(dG{G6q7O|C1l0TE+8O&Qq)PMniPGDf2gRWSHX>+p7Y>Yx$e(3PDY)wa6IGGYdk%c zB}j@>80h@^W1Y9VInLUJT4~un{Ibs1@_Ffl&`jAj&5W6#s$H1MjHND23B_}^^4iYf zKb~_6so%oba{bf8Cf`0Ojc)9dlnq0ykrd@SeY>w-bAdRyC;dVH)qQo*eAy4J25oJe zf?xc4^XtIV*YiC4_u@P3j>}0gonVx?ioaDgX5Z^S?=+hsD`FK6oR5ejCcMp-^r|A5 zBWTT{0+SSvoQZSdY`fTG+o#XVN<@fu!}c7GvIbw#Hl{o3<7jZdYPO7BKTdDWwy-fP83|Y04zn1;HM9mKlJ1!*Mglz^9U~}@Id*#+KNjn ztt!}Ns~2@LBh(zXwgxrDZ2BMeM&(Mgi5LiGmSjt0W16#rSqAQLi!?WphY^NSQWMOg z-hrCZMBu{?kR)$Htwv@F2wNPE+oz*nEyq%0Rsh%BphbR;1_)1o;`HsEXB*L03K?8( zYmQG(UKJBZi@5-enqD*Qr!WubIZAkNeMGMc4+j9(I}U``M@kFK4G^UjN@-+^ZABfuYdx)k8Gi4@!A z=LR+U6JQza-P?J8oK`e)pBYR%(D#!1HT{? z5R4iKEg%ho{(d{Kwu$2}Ws~83Go__CPLEBBc?8U&7CLEoC5Jd_&7Yu^?P<^e|9F#| zz!a}F^ZuCdkp$8Ud@^cu7#Gyqq4PBOwKF6+V-P?b;~6z&Iq)|}iX};{S;`oiHGFnM z7Ba1Ut;=hoc2==E#hy(nGYEFZt8MmWu@s$gO8K(1SiqnRP&@LJO*v{BU>;%6)E>yI z)j)R4bG2PB#MKb0fQ*#ucs+rIt@FM)d0uFQ%v`a2;Tit6imskx%%|0k#HE-@ock}^ zUR~bcaX_$$W(sFt2S8)a6r7DS>Q5i8sq)|N=U2>UfX4QD3@IMGs^w}SNwp&HtIdFz z2(&202m~Q=>>4emUYZG_aYyj)|M_pf5+mpeyMx0^Ygy{393=sO?Mow11#!U1t2tfBb99j8EOV`))-QS?5*luk887y2y4_xpr5sD4vS&XYQHP(IHP9x@hR&ei+l zkgpACGmAl_4ID!a91}YWLS%|iVPGkMh=v+&1LDQe3!K873JRw&M$YPex541>Dims4 z9HEEWW*Qiwz3kP5i76B+&6j4sy&A>)W7>Dps4w>6*kNMef@D9HDdW*(46z33l-kSW z`zVzaJ72lZ-a0jwD#+83wo1yDS45r$TDpvi8Gg`ovQ8Ve6SX0_m?#sttf9j_3y~GN z+HgLwR|6#X2KK7|e9Y?RXYRAfJ%jL%+$X8$u-O_~?Ot21Cfro-!0~@a8)gk8E4Gun zq{l;l={?ySY%+| zEQ+n~vx82(xR3UOjV1X(-C=0iUOLOmc1$I}=P z0vi(j$j(shlh#!lwHG{TUCn{|%0UBs{4zT>#rAkuv{&8Oldc&y02|FsE;cla>S-bF zF)a>x#@XH(&LUw*7;`-jwp_$`)sRvxsE=)S7WbcXo=cD5^VWwX#VXCGtLt3CKvAtC5~RD1D!k3S`3T5`Z$ zZ=GSUh7EUuxeE4NE6IcGC(YT=7RekA+suwRa&}s|MfZ+5-&-af`ox`Hy*vHBe+ikU zJ#BqEUTXAA+)d2ISI@f`J4TQ@=`NGQ1f+VYF%0o(7y?CBPSvhnz2*+y8wX1yALr8% z(y6LOFYu6VHD=gZrir6vlIr9d9pNk6T?! z(wA!`wYZ?wt4hANc;73*I+8aAcGk*p``_kqS?*mz@!ezYxgYYp(8CaG{+&|w%_MwU z??eUM-H&=%F)MQ5;dSM7@wm$6s84JKQu%INXdF6*#)$lghm#kcfBcnQ7 ziL%U@Uf7|t9%9cLa}4XMli?1_+wKd?+MTUPwl_~GAa9BxHp1;eB3(Vdfh9VQt-wNZ zy*&{n-sLvZ7^w~IX5}ob_A&!_N|z0&ZK@LjYDzh|0`hzNH~LHQ2QMvGrFEMP6Qqe; zS&ol+?GkqR;?0t~-sxgMfTJL_aTo7C2uDW0&Vi0~SaNL?@4r&-oTWV@WCBbMvRuhr z&-U1B5vHBkIG@I7fgZ1nvTyU}_Eo#&K%$kix_%qY4m>7qEh?Qh{3dSnh|aM!_R4b< z=VHn)V41mUWG~8q#-H^O>Ovh0Ckic=b749>MHiNbO(pXGB26tHFZ9aE`(q++ynTfk z5)soMr4weDSRU@=4||cp5h;$*0OtqP$-FGD)+^lq(ES6&RM$FKadS*%oXrSdcM5S_^s|u@!jOT- zod7bPYummS8YzTJ@%?LF)a%N=-!(KMMPWxNJYc2spTt!!@oIzA$sB%W)c}hIWFeT@ z7!hFwNGxb?do8KxBT>@OC`IV~636>ngt)m^MaP_@c|Leg(;a*;6AzL5n66rQc|mo+Z~pN#Zx*G0yv|ZUFjn~I( zfY$1#VqN?k^x)>!0`nZa+hAwJdU4X*@V6OFyEyw<|93e*?mVq9UqKl>g0CJ|E~264 z@cDhYVZ8J@O5PubeQ#K+*$gCg_!xSBYdty=dZbI&bNl5okLpKb(e>sCmoK!L^ZRse zR^F>P0uF7VLDcZ=l4A&f7zmv7g345u!UlF~#Uv;9AQIRa8gJ1iZn!~TI5bXs&qREf z!Qf)Qngwe!^$O{G+mh>^Zekhii+gyw=lDH!6U<2P^cQBN$4G`+X`Y|oLJr`c*YJ3M z9PqILO=d8Jq)}stf#8@6u~=gm?nGnm43j-z9DbY2}l|8`iU;50#M9XnH2&r4y(wA5nkJKa3fW7a!< z5CEECy+{AYx7R{AzX3Rj&t84%{qdyFp451z!x}gz4B5ZOW`KsQ#}1_QpkYfKX1KN? zNx?+9|1j9jIs$f*$1@*H**O)xm_K9#Jj3w}$uG zHeXcP%VJwSbE*JTjH>fPQF3oZ%Wwub@$`3&+1|_d%d?j@J+P}f9+4gh$u)CP{ldpT zS=ErNeFd_~ACLQ5R0hr0_g|G+bF4G*Vo6FA%v=<*2gT}!e{VX)?ogErE|t*B3E6ac zY?>;fN`-jWxBZeN*PM9x9$)>uyZ=FwEcQaAZ6g1&Xa>&Q<~gYhhlH6+$h@-#SlnR6 z=%n3CCOQn2BDnj^Tyj2_v?mw|mKn%e)iQg^%au-LHg)vmlsRg>K$BFC@w{#NvJ%+@ z^Ooa}?KqY$dBKM}#sUI&5qbhzJlg_z=qk6ji5AuUx55CnPcwEJ^AWajOPO}hEe^fm zO~~23!`I#L?rQvU*4yiufm?0ip1`5ZL0m!>#XCjvnld=z0kIu7f?GR$haH0zxU8C? zs;<=HP&{k1kDf&zIpxj3&3ZC!%9|dYPuzq#y97DrF`;udU-p}fQQ0iHRs_hsOj6-? z<8CV05_O@xODke}(%r=2h=38+(@UC<@q(={$a}$(>#nl6k{N+aDn|=m`P9-C_1tBq zYF7}8ncIs6mPZ)2;0#M^%WjW#tn+pQXsYib=OfrmFU*Hunx~Ch+mO0pHLAxhRpnKr zOJDNp-0i#$&F~-3J&pA5;l)PmD%uC;o|MK%?gB@xaTVpt_8!xC_+ekYcBi`CXBW+v zeV!IWAbbu5#wk~9@)DnI125XcdnP$ArW1@d%B{1$=|dnFiIB7kM$SsoF_RWgf8rHp z)(}`^Kjw8)tg`K5lWm_qFGZdFYS(wFB>O48iciGy(^2Ee{AA?s1;@Eogm-MoGo7*l zMh@{U5pXWdBXg*PA4{&GMH~#crw4s}#X`t#_=bl5h|!PcU!4nT33I@*24~jV3>Q#2a8*}_!?JocM_fZI(t`)!y8Dl>RmDCu{FZ4M5lA$Q zEJuLaWmcAXEm8PKMws7xTxX4VDfyo|$2eKEYV$h2xG*5W0dEJMRaWoK`y=QmgQnG5 zD@hsgTT$Q>y>5yoU&s-0t9(}EaCNsgk=I}Hs~C4Q6O+*M*f)G5qKDW~xTfrRn;i}u z`++(*D_sIYE;YpPcQFd!L0Aj>*4Zt^0a3?Ur4bPryiEpa6ao{9Tqc) zes`v#C+5XIEA?>-dPFh^IeICs{NpBLqIQ&gz#g@6sP9 z@os0*yPI1JEIA-^!=PoAo&FT!+$JvfqjMWoj5ubHnV8Nt;Ldte6VrQ6Z!yJKu;4%} z)2t&;dLm1Hn*OssSz>r|O2go;wb=cA*?az#+rXIV35%iF0Onq`dJm$2N-S=&8C`KXcF|)2Tu(oJGoO6M;B{%e>GfeXPz77!(Qu@lrB$4qU8Cx z+j-5%@E^}Tjr8x~#X&iAp42dwTrYbv(Hc3gN;aca9`gz6-w^a2ES>QI09#6mn6qB;7pEkV9*IJN3Hok z^2G+X5}a%DNk{E6Hso~W8+2}Hu>_kwO}(1vMD2a*2S>9ZGd{Ra1y^b?wdZMV=j+k7ZTnkG~;*FRQxQ_z1dcOOk;8!fUR+aQ}<$3*rg;CcW^T zac{}0t18?HQpfkzwh)IeYvGtH`@%UNvu%XY;aOP<8RpPD2w#ngq*#c#Iz-BV2x)58 zFPdVTvuhbfY{qYaxd`)jdSCOHb$=Vp%kx|Hbk2E=ruWD6Z>6`m_yAIdjDZEtFpSRW zfZyH9;jW;ET4``ic@y1Z%~Kd6mCk!?fuX?b3*LnpDSM8 z2Y6|meS4uwX^AN~bnCOuo|@k(F}c{pqU9Es5$-piy2U1aGLBlSgd|I1cH$No$&1Vj z^eM1#@&{uCAf7Zh7y(2U9&wK7;%9&IoFq8nKlqj?u{=Q(NbAbgMntaq#X}>7dcw-M zERRoy6lN<-a#l`&D3&R1y$oL`$;u|4rQYd>?XO%pBSHh+E=d#hw)VfD%^Trn@JMp-UXegVq~Zn%}4_ToS~-eV>lBNY>mM_(-0 z!gP3ggeE?0svmQ`iwx}t%-+fYp;t)WALIGr?Iz5OQ8aTzC(JOhEC_%6u+KA_JeKTd zBm*s;^tt32S z_r&RWBT|tTjc|iF*??o2OA-UliR7oPxGht%7t3Zc!PhI+RDhdaXmcI zv>H({*VZ3?Rp*;8>pJVQ)3&+Tn~Zf91^3|HL|*dkKK3{YFi&!!ZLF9u;6%zrzU?&v z`8VR66v&CJ!_fC2hQ`0AfnEfs9R5_LzNhzw&){9ceOg_Kr)U}9 z(xubjIG<+;#~{PGoQ3%Nov076lIQP7kx-P?yQ*FYcXziq?22Y1BK5*8-E}8{Tp#gY zi>HKsrf@N(ewTRqpj=pnHg2iJQ8V z@A5j^4oO{7El+k7fdo{S5g@!#vRkqkeq!{2fPD`T*e#JOkE3>1>%aWRzy2y6rOHBr zfQD;nzOzbg>3OQj+Xvtjo0ueL3~W!eJQRYui~#9)ohE!giWJKqIQMxe9)2&I&M0K! z2!e)yr@_c50Gpw_O|kS|qVHW*nVsVqm zR;nD`q9jSRXp{j>}Rd*6Z_r1@bY?*^Y5$0_8ChD_#ck?hdaec0CDRU z!>js>D5M#i6hmww-1h@T;kKXiMNQRLmTDGU{o1#?YB7pURUZ$Fr}|hl?t;Qo0Df9K z1@R?}WDQq$`;8*(azJi}0Vma@Y&gh{a<`J5$Z2{_LHRTz7`s=Dls?$SI_`3uX&08J z)kq2)=yYWRl9VIAl)(E_`A?#HK)XJU&0^IBpt@Vz?h=uR8qyN8jOt@k9P({D3^E3$ zS=66CKcaqbgIAi@j|RymhKJqYIo6Ha8(3LEZ8+{w!HNpq&SR#G0RgmipOuT{ajPq% zA|JxvMg!*lVOunJ_uuZT*RjvpnYl?&j?3})&>O}C+z4nDCDqfjIL_8F0tAXcBaMo_ z8VRTeLW?RF6WH`X+~x0V=t#JEa}0+EHyiPjy%Z%zPy*AHxgHSSR>^+Aqpq@-g}>l5 z@Tk3aSCcwTA|81JnDf9kwcPN$*hNvEwo0wLtVA>NT%~@G?KGBdWa(jnfbKhCkS}w| znUSPgBmoz-D_nK|ScxRL?jC&q$*y5q*UB>{M(;JsvBDUo6OaMlz7tSBb8g`%ql)+% z#st`mJc+tqZS>u4b3<2G2&3pOx2LU0rHPm}L#Gb0Dzy~p5uFtlx(7nkb9-Z@!535T>`|TQVQ!n3OxCG_wBZSry z^#BQ4)i#j?!5mDXX1@sM*5W3`T@KE^Rh?wN4Ch0CRnkk|)Pek&r)~8*n_V67;Ffis z`vc{d+N?<>8}oGxq7=5Dysrb!S$A^Dc{8yt7wD+MJqy=U`grT(fejVTssUy$IKeQpp|d|I%0a9((LjrF*R^@B#1#wSG*>KJp{#Gj zQ*Z-g1%IQfCrX2bBprkn>l2%RvAUB(e#712hGx-$OwD5WKCA_X{Zdl;Z zITSWaoHKWmH99o8%A4}0XTlXAL~mwDtW(c)g1L%j-*WYoqUJ+*>YWi9--vF_7@x)5 zbTKNf3yx66npSqMy_RZeKGfjoewEcV?~u-C$gv~Aq6bHBtt}7>NzQnC&LHHZ*UVr16^10vOsPiFc<&tv^COmq z8~qsP9fl;9CK0D2j+oNR6ZX7R2oS+zHGUCW#3`P;s7*nIjF(n|#w8?65vftJ!+h$)lAkB?F0a0@sdi;1W zy5J#4>(1Jni^?-csN(5Q(@74R;A#<9bW86p?adR~fRrhPaZ>%1+%Z{CP7<>(~yV_O?` z{!+hkvN#maWwuRoRES`ys)GX}Y8&AZ3aNI5jyR`qCgesKO5h-oL(O7atzKyVkfzm) z?qgFR@MD?naxiV89Ms1EP!0F3B~n2U(668Sc*0=u9*)nMey_VNOcgQUP>Brl?3Yc2 za$g4Q2E3|v>;?iZ=@we&r8OK=tOmZ089A-OdX`GElW%Nrla2vp$C8wl*9}PODTx@s zt$SIFQx8dr zo>2)2@iV>@CB{q&=asz~@PLnN4%91nosZ?NHatpTM^wXM^{{Lh!)ktaY_Fm)i+&{U zvSU+Q^CY!19Wl_7-bfFluB536cY=ivvT8a9cI2`-KBrg?c{+|>yl2d7;L~(`S!T6P zCq+dpDN5rk^bAd`{jlES5YOS_tbBJ^!5k;|%QROy($mmly^SGC(yOBI#9}Mcg*C|- zpT$cSk9M3oSh!a&55|}l4HWNZlg4;m#8}8+gVv#}+okm5FC$dt3j_GNxR`_~%yMkt z)>UQ05US`9t-|DIfymI#Dqs*zIOQ}IHxp5fp&PJhK!~e@?D!RjMqUYCCWJj=38)i0 z!A!(%^-1-H)FujJE^>Q^n+@CC;cJZ;CDFx>JK^7`clk6N#6a{(KNQL3gHL3y?ruKG zL`+TWUE$MBXpGNdp@PU+x13Hde8s33#zVy8p`olITEh#fPbZY5p1l$r#lB4W1uQEN z?dz~}auF`Klv3Ef$8s%9ho^_3IHy8NVZKZW1gT%c9F6dYm|(`&JAHoSA2obNir1^_03)8QEoK4C~0N?_iL(gZF5QVBvWK6J zN&&!)%)6au3&67grsg^Rlu=y+u7K%`$^xDneLSA#HuCuK%o<1PY!A%lcgOO1+vT2? zF9$B3!+8JFhfeKmKJ-{rU?-{AvaB-uzBk=j78eLCsrB-dm*z!vBrk?|kg$v=@2s`)CmkCo zNTVBHm{>UA4kr}O;+ef<;A^491UKj37NQ1Fw%sn4MRWHE%FT%~E#jV}xHW3-Nq#zr z+x(khyEQ9gCXZ>g5ykojnJyej@%98NGaQ<_*zZMBqvGtgdgi`=Yem;{0DMtQ++^jm z2-bBMR@4^mbHD!P$SU7x5v%guLlK{Z@si^fwVKVx{%kmEz0{Ic3vmTQ%>^(z3bt1< zslT+hsqmyk?dY|;7_VYhuK#(BlTlha6?f8pp0kyAS2O&_b50@kTX-3`PT4^u|2$#! zt5wp&5Bush7gq}x9oeF3OM}N){`+rr!`4^mYc?r(W+Z{!E<%MX_;K3RRSoj{!{wd& zrBr(F$Wl9-jvhnH{ZdtjiIF0$UDo!74Bi2&-zQ;02lHy#F+225m74|?a63b z&2#TH?7+e_+iB~)EAFPMtn=H2)DdC{1e_CE6oEz)$wXyxxNBA9LVMzY)6rvIx(ID+ z5Mle&I|!D5p&J@Df*Jgn*G;h!yVXs$efr$qXRmhAbwljEH)VHX0}x)V9^1*>=}w#^ z1_QW|yUo_?+{qu8qIbx&)E~Zo%2w?b)5Il^D-uv@yte_+)@7(%OSUTjP*c^b4d)$Q z-~2Fkz|oUXZK5PwsgwxHPI2ipdhWkc5sI-u16S$Um`u>I`T}!A?4E+&z&EbhKKv4i5o^1;hWgA5imF+(6@+NZ= zi=qj=3O~A+XhQGjhW{AxeJrByY(PpRX|0uL23+k|UNV46oh`+td9e|BaGZSRK#e&9 z6{$>HGYZ##kV%ORat+c)Kb;x+vCv}Iqd#RIjCox zlXS=8Klqj?u{^=?n3m^=mrk#RbE7X4kT|H>dg|@P+dA=Q8ILYQ5N|Outm;E9%N9ji z4SsL-Zq`Psx*c7Rppdl|HQB?*e`<4hqP;WYlBwk|tuXPVK!!2^o9_upUkklxl2(F= zPC6&Qz#Dtr6iu#Pt8|yVl=rO2VHm}(;y3@8qz@fxp`b^=d@TmyiH}*k>hskT?_KWYsd0aYqj0L|hq@bWU8^J2Gy9byO{X5Pg^heKN-8 zHi7u@V7&bmNLH}k9{)@wkzN_QLC*hTyn^(R>6>BDzbrzud;s9V(|dBZl?9n_^fvn?RS> z=EaFm#FfuEn+#Wu;~&4TkzAh}eX{wQ+XUjr zgYo9<3cKaSZ!Dz%L#>Re>Z!X}y=C#`zV`cEZP(GS{rZS8Bb?PT^tE3ZXJlGy#ypJv z+ON773bwuQoj}fbPeqQo3jo^jr)=-^)s?<=VQzHw@ZuQ6YHeWLxY+Y346%G#TLGicUl^+CU~pH(@%SA>l>SYH%eTH1h|8Ai1y`kS~Vu zmT6}YrTCMPqZH$dD+$deCNVmiuQwWzpzp3H4gqf?wMyI@fgsGNx4lt!0aHn6I2C*F zcF%bqw?=n(vHuM`=)qzpBMNfihuaw$_rMV=@HY`lNKgzH4-81LMh;^zicI|AJSeg| zyEk$qoq#B?1)9Zadi;1Wx_YN<8J@Lmzo9U zpDCt-=ib{cG`fH5Q_9y&2xrXD>N&?;&q~^Un0>6HtHCt)*t*YHV zJFsQ#FEzsOcQH`Fz98T2n=gx9wwHd;M}roBRRzM?Qd$#)-&ULAkZ;@aj!jkOi)CG` z_(uV*r5&((;n%~iWm4(m^gwDWF4A@qH(2UjHUZ!>lv?j3=@bHks@w^|(##p*u`(M(w6|d=;F&Zr(Bjp_?ogEre;vSt2D9a> zwQrO06}T+sE~=lgDJ|YJXMx)}@`FfMz*YA=Ym;X`ep#%lnhnVVW4NsI{O7zqH*q5> zp4sm*^TJ1{2odi?f|G$=_MDeZ=Djlbj>u;a)c%b%a--Z4oJx8aq`(?!Q1#AgP;{Fm z*XBFPx8VcaVER{d3g&D&HA~<8c^``lwJc1Oi^hrLN@>PPEXIaNZ8$=0)4-9yhjg?Jfi%wXV`bThD9CU|CpsRpsp>@2LZ zS61H>CHl1Lp50S%TyseO@BjI4zmhA1Z)*lAypfM(w#&U+QobJ@hwa6Zd2;jF-eC0@ z_;%0YRTh4QEit;@%#$&Gy_ks zv2*`}Y`@so`QaeWvD(+hPJQ*mM6qldZ+QJVlMhLfQ-0K>$BP}GV^mn|6c%Uj%yT-U zfp+&+>`L!By~Pw`!Gh}rM#|^9`Y~U49(-oW^=eIuubHz$#|lm{b-6BLdyb`>LabOw zpqRGN^qr7A4?p|cXmQ;qzxNEP5uPe*cYE8N<$E7&_WCVg8yW5igjdXSvxd8#$DeQQ z@Ov^Cu7E>^p)-028pQ6+@JM;%dGSkK?G()WmwI{iS4hY(2O@=p&!QYQ!0<>Y51(3S z67!7| z9ZY#h7r43wJ~x3!O`LcoSTCnXz$x8nG ze@WuB@xmQWr@Muc<@0Zcr4igmW3farUUDPYcfg<$@=EpBS}9K+fw&&b@>qn zIKhb=@_Zfp%rX&YRZbZ`Ul1j#s%FqOF{!7E6Jz#*JMEpbmeozFjj|bSti$ahBN}Q9x%-EQ??q9-dR^J~-R@3Rq)Kz8aYF*_eYe4g08Xxp zh}UepRT}Vfe}5TlrqS^D>$Vf20K~g)tELc6QX?L5TeMI6eJsVr{m0*szn4|rYZl3mFSPFlb{*XRqWi+W5|0VCIab48`Nkf!$=%E z##a;dj8`~3iqr!VJMe5tTP&8`NDjp6!bdHE*pJ!!IlgfcYrMMH`(x5)l87(# z*~l?sl>7#S-7QKy=Ovy78Y$=0`;pU@`u+_QW4V&cnbO}{XeaB@(KUNtF-r-u(~8wM z)=yQZE&vnzZ1o~88tjfl6%7YKmm$Vt2BR0lR#rzTc$1b$oPm?=G!#NlNSnZh!%Rl^ z>3cno4^8$A%t)J2x?PmV=jXgRs2fVLv32HCm_Z7A7_#0D)rcm>uW!BiEhV7@XAR}(u=Bu|rYbmv%TV7CH= z*1NZX!Vumjg9K{4<(X&$CcREROA>L;L=AA8NR$1e;bXB zbGmX1Bf4L3?-I+8silrJ_Hb9Oe)ZZyd1TGk+pwi>%Sv=r6!Ux)(3xdt=kbY}`G zCjbC!rOCqQGWexj=Gh_vSo54f%BZddTp(&J9Q}R!g3C55`4=tbMxOwxxeYyjJhKOr zo6uWjF99Th0|{?wAmC<(J53@Uc?2FQwFCE*0Rm2np*+n=af}FHOt_YdH&&a}6FAj2 z_ww5*damTZ+a?%4fDtXrW>oS8Y^PXZ;@JS$v~8r7QM)E{qAJ}Nl625|AU5DCE&!Mt z2Iio-^p5XhCKlK_@^b+(yWU*lil30&zMNoyL2q$#U2{;|z$9=`R#KaXvX_mOe6s} z7u@=goWetE&>z-_B*x4`7^Jem2{({vW%vey0%S6Hz)3Xc3r_wwIE0V56*(m}m_C2n zAlFX-%p8Az-yXWvlk^Z(Y>#j@!p| z^;-7*kG`wnaJ5oji-i?9wvv5;8CP{yKIh&?>fQ&n*R6>SIx(GK7ZGYtJCw|*mX`c5ku&~ zpiwbojIX05!J?n#!*oeFneSvU7XH%a&M18n@hn}S(ID+YAVVFQ1ir>OF}G%ac+fc` z&RyI8<`+Bp933rI>Ep!o7cegd0KS8nt9gwsNnHTqE0`cwAu+O7zLqg9#Fj6n8w0^u zEIDQdShK5qiVi?U>1&U>a=nwq7y|(nHSN$tbiNh_Mm-p&V@eg00tGFqpNe(yvq%wH z9UD>X^aZ!{}@zCL@tk8U5WvnD+d~B0Q3VW z_Yd2_#d{nD&CG5Rre7Rz=b0=<2B;;A*4aYj%$p}H5Hv!qG&YiA3dZU4;lNY56EN^b zCCYf!*;Dgdk`ovNz>!e(1Vl4zCgiFkNks$QH!@GIwd=mXv(L8q!dD;DQRBfDcm^Q0Z`=A}pq>UA!;ingT`dlU zxI1J^;r%&H0BYee6rj2k^ATGO@0Z8UU+k`Gq}RhE!m}!X0?b@Djqt%BP^;qk0<{8! z)7L%Zo8H~rT3}3peUf3&*zek!j=57-JSd83k?-1?Z%KL$^UJ+!YX-zk{;eP@eI8is z6c)i_5-7t1fMoS06BRu3T3j)#nkLgiPjbhM4zcdoU>y=`slgN`gUJ+Q!GZ(#u*^Kj zgmz-@&9*gC4D7g#A8mKsN^nz5U7|d;=UBRQAcT9yLIR~n`a+6(*Oq9bC3Jh&w%vM| z0y2Tm=H9T+}ZiYw7 z8_x?2$~2^}JNRdW8tb(fsv>9NlQxsVWq2echfgi!E122zcn}a4KQ&NZ*PUA29fH}4 zjdGgCp3=F*#q6rXn>JoLZulId7CcF7*Od2NTgi@aJB?!EDcclW8_(1(S#^`-TC9k7 zPd)s{`A;8XjdFH9mrYo5jjVayI7wNBA(i5;+qc!ii~6v0FPXt~f>B4Ah_TV@6*$CXb?x9`7wa8;=>oTYfGg!RxcAI+8@zbhmV{2Spc}N=+ z!N33Kzx_%dgP!kum~~) zcp~5`T?ZatdIwFb!JXto@-Zf`*01h@8%+6WSE46h{5q$ZgdTGPL0Y;I(L?M&lr99z ze3n<+5)d-qsbCa<_eVM%;=R^Js|yJcZ$|<9NMJ(kO1-^uhVs_PIU$7y5UqCWi8=Wi zTe!DafjbTn@Tsrhj+caff?~L?>0xCNmb>zykogF=qcuw`ifk}TnG)d4U*+|+1Vml$ zR57|>Jh9bLHc5|06;D4JntDm?VBa9aoOh@F?6_7D)pUUV=!&n3on{sDT24gQz{iWA z1_%w6-ihrP;{o!!XrB_W8#KAumJD>rwDcdo$5m}UB<*voS8`o-`eXSm*>^)!**K+< zg3w{07j3XCq8twhsI?gt>A!(W1xOv&+R;BVP*V=5fR`|f;`}!_T_>DWF;HOrvvQpu z#CvPpX8eMr*r9-jwbix|$yDow9==+%=rXUbg?2NNd#d$~0DP6G}cv3%V}>P1bA)eH0Vnd5U`h8S}h-uKZM zw`9GhI;??n!YKW_D3mthvO8n*5<1XGJ&WyA$4LX(Bo*GR zv2>ZOVPN$x##NO1qFzXsoR4m*q9;5)>ruBo2kxr$n-EW=c`#JH3$x-m2V!Ee6y5jXXcy3ouW|I(gvd(Xqh zahOkScY&#(UbK)Oj%cVEeI~DV7~8$Bq4&pO-y7CyHUmi=K3?nr_2;{J6V+5>?wtN< zDo=*3+_%~aBP5_$cz-jZp+?8!*pA}Ku4_`n#e@kRPZPxL#nUilw#?LnAioFo(j}5E zNQy;Hg?jbyvC}9A?`xrjoWxSXS5(&A_bJ!82@h|aHkgIr2961}X%>1SRKaGNnP;-$ zLzwNgjou%Jd~HaZSqvg=;22+&a<%v(V@n!315B}8MG@Z&ME;GUw7G^Vrj*8wbFJz* ze+MJNt2!tvt?|IvN;U|?(A)Z@v@uTz3-v996hd%%vCwmDG=QE=*;_BxdA=80E&e=y zo^3EBU<$uJkTO%F4Y{|8MoeEm0mfTPZqv2weM)82y`SYQ^f?+~mck@#hov6F9&V?Z zJCS9ln6L7@2l3HsL%lx^`rM#KGZ{wW*fHe5TQ$a&G^^N~I}y`T!)}0A5Lo!u*i+ae zt|mF_>5>ayTAf7Eov#rFiC@1^=AVf6hda?|VSIo~`V6sX{79iu9Sng-x7S5gdlK?4 zMTzAO3(lMMBZp2KcuxZ@+=1rZq(~{V;`tJ{u)PY~`{RI*4QMihAta3&qlH}H5-nas z5^krlGo=|Z0CQKXcuJ;>TdilM#Qdj(rv`*qE1{k}tu^}x9Km{4Yk*hY)!_ZnGZRiV z3}`ZgAta3&qlH>^N+ijvwck#|<(#V(B*Gq!+ovNN2)_s+<~^0%Du<01s`UAIdd=%o zt*Y87^B>|?&a^EsW-=^VkX~<$MLS&D#&XnJm`SX&VQHiOm4)R#!|`xB zddw~=8r8Ckw5-Gi!rNYRF=%9C11%q@K#MZ5sXX(VFz=74UrMFF=*Qy+i=jm6M+qg5 zQ4KWG2qJFXxmGr5u^K)1nZlH^uJO!}CgT0jW8t%UfVZdKCR*^RwvM46(fSTUnYE@$ zU0q~LDpC60K8>;3Tcu52HjCai+!P%l)-<3t2>s)I1^#mzMiGbn>-x(upx87P6_SMU zp1L^fvbreq#i1>q#kmhNY=UI~s@v?%tdx&t1fW^m=cE)1*+IaMARIIzRB48Dbxf9= znW6O0Gcb%XkWphXrmn_esL2ufTF_t&AkKOjW=o(DN8(r4E$bEgCMn&~z}ntk57PT1 z`LVHvHEHMbo)#@HzbflqtDcBwacjkQSA$+FvW1?l>!TBnd|$f{UQqOSmUSU&K5&N8 ziy7~aL%ueo%`66yHgF6rnB!}0s}zj)&?%zfC4dG(uIsFRSydnOLUo>4ILCT?Q|8}Y1*@w#o)MO6g=Id=QwZvRr0uFl5nh2a9( zW^u^#wJo#~`NoXRP*+<3P-}62toPe|AzsB|UzvFy)Lvr*UT>K2y_s+9-f)!nW~LFG z7o{DTe6P;*Fb$}+0!UhJCamHh{5l_Gah11Rop1i-84eX{L72w_h;u-$sR7jBeH~cz zvwVF9x+g)L%y%+apTL7TIjweUUy91CofI%6eakSlcX*H@fA$)1S}cTLL(#|Vd=_5VF?(pt9+RT zKt*jPJnqW%P8MSfh9;%H3cXI_YeAzg9aB_M!2Mp>&ef)>kB7xmeJt4W&b?YgGi!y} z4;=g!b;nvhTC2PRQ?y<4HKGJG(_=seW(#6%1d)0tM<^eg<;*--8fr#k(PQ4BeWBnT zUy6;UHEacgp0K7cBN{S@$k3V*e=E7V$bKpg_aIeboKsBZ>ywT2+~4y?N#X_*B?)PF znGVI0>_Vu_&u@U)c*TNcrgQ^+K&p959Co@VqWw^dWGVNdfEF}yPbY~8qkB9 z5kkyofGGI9xsA)gXT}&}Ed#H~3GkD!D$24m9}Qt1&L%iOs0wf2+g7U=6E?7iI}Bqu z=H5B4M1iHIrPHMha4t+>x%T=Sxo99z_2Vpb(>X&rN^zTHnl8eoS5OP8>;ZcHlt5U3YjU`HSBDTx?F zp}ps5Nu4GUk1=uVHGqp}byjSZ0{{N5mrRL>G&~=^Ynv?AMJ=Btxa+I=!Ds;I&FdoD zRVACV{g1Xpx){nKuNPUptLjBl7rVt_S2P>(q^x*)cPE~><%`J0e=nXASjeczSbWcy zqQuBa^i|1;)mN7sVJ$P22 zJar4@w&}@0Ci-*-B6)Np9h(DKz$`Xs(6Z#yLbh|RmYpO0ahEq4tfP)+ggDkV9UHQ3 zpOKKgk)F%}EMSDew*TId2GX-E`B;!uy9$u9xh@VXae$OP@7kM-|A&Lht;JLG+gb1m zLkxs4cE>}p61xr|(f?y~qCcI~?mVe`>+U8r7e|i%i;TdXyCgB#46Lf%K0C0tc4ji* z$YEgdL)R={RpPwvi`W8m6MpXy07vuNO1xe!URcXDZ#X>{DFw$@zYNqb1P17~h%5wO zn)w?g^#+_`<6%rESg7C(ZJCLOE>&5=4`z9PGfH0h*pkB204u9*Q&+NjpEqg_5B;bb zNjk^{GDmJshys|+*NrI5ss_aAg;|jU+t#wulbJQEVMh-p1v)0x&r!)1WrLt&JtK41 zUY*v0?3preHhJA$KIM?(P>ZWnHJhz>P8J9LS2`fH`bAZK-&G8o9UI%NF0-33Xpu)#rH zyX@OhcUvghLU7btxJj(!%>(xtGM9xTcE<#hqrB?c`y=pL_xrVQ5w9a~udKI@ytXsYNb~nBhEsWFpvc8)BT@>sLMiknG`@c}_>+Shdci-q z*K;;}tMCKSyw$Op8hJy$Hl&RgZ$mnqzRfuSMr;FIm6zmN5ZijUI&WXC_46T#JWI}E zX)WEpc{4N9_BLi=7@RPjv&aIk5m21Chb>Q89d8po@#(if_lcXbVT;jjnUu2uV(7Ch z(-0qmo$pLANP+3-KpHO4;A^2FIt&o@%8}B}zE5$E=lgDh#S5HJN4#Vq2BTEl8d=hS z1^X~k59E#-GwiHcrsQ$e{2tpp10NOSfCwicuYT7*g&%h?@3B;o9*_r ztU2njMEz0v9SPC|+YsiG9-c9U*QTmh8$4w_smDLDbILcUC2}NDm9>tZycE;BAjfiM7CV)2)mzh&WuDer z;LIt)p*l93kEL*1m`tp}<@cEn+6MvJ6Em5A`rWF|o|>*$LAKlq2kNz0Hya{+=qBd^ zeH85)FbTAhm9B|uYx|tqHx@U8aoTD!#9FdRtCYyPQc)0h;&Jy|opzm{re^YWKk1*Q z)f0L6_3pTBigs_2+nvQ?Su}^V;u>b-;8-$n#c@@CX%SZ~4~3|+!QP)eM~jr*^Hj9w zm}km2@QUee%MSbOwOnLnTN{;LGc36Q+kX+N19T(mtY-Cdo(43dqO23Db}t~j8>r&) zcQ$lV7t7g{Z~=!HG8w$icr_S5+e=YmI3;jh0c=3=7UlnVU0%SDt=Kq=nineyp*7`#&g~!q5=%D|M z!YjtXV*4?ljO&p6sOh8kxd}SN@&Ohkknc@Vy2Fmx8xUTH(v#FDkT@jwC_;BEFo@<| zFWw)iZ=pfapXP_Ej7-wFLu-Sw$c8v5M{r!+~|cs+p4lUWbmylqbdEuph)uhkL) z`+bwMZ6-3vn0S#i=yQV_&14vfV}EP#0>@l!PWFVzO8rtQy%$fZolQr70)S3XKT`X9 zRGTm}QPCQ^6;kf+Bubn&R>)BEgqm4>zfoJ$%Z{2J8$XwMjG7-GJs-D-FW9ylMdQhz zX36#UQZt)_K}&`pK8$M77U4IW_=0mf3N~5GY`3f`&d>wGgD4DJK1UEn_vk0|>RT`_ ze=pI5r+yCWT@b81tg@#kQEO7LPi$Qp8$9WIXp@({kFe-+)Wov1j?L)(cH>}m!CYAIdA0+c^vCT@6X`#zW znmU6fpwManD@g$p-ybQwmB&xokYxh z7Bd657>7-s)%uvg-u*mVFrt!zPaiVV-B~3M%(ke%*FK&);$lzjd~WoNfTX-r z2g`JJ9%;MJv>rCc?Thdqh4tQUIn@r5>cz(Uz@B*nUIhBZ(*-95$5K@66a}@^z?Rid z<`f*gxz!H{NpW~P3OFSu`N`R=fr(`+aepHVERXj_&wyHr1fYkxTO;Y1PB2u!rc=q{ zP(16M{6j#**7d>u4oDST7UCLDK*H~coRP~U!UayGl)@;o1vYt|)h~-GdkKj5{kAx# zw1Jv5av4S7pyS^i%ja##jeT$Q=M*x~*Gd+nh!|*!@N$0h#OD+!xU$%CeiUhf1$fxk zN0G(u-+#O}`g6(`ELbIrQA8XQYn495{|gR0ifvvBG|VNu4QeLI6A$seK%Vap=YA$M zo|!R}*u_p1!0;HiiZufDeApJde6h^<&FGDoc@T3n)?qln4O}Z8ydQUW0xgv}BJCZj zrtiUD za=L6=gN1ND{6qfRI0Lw`k5RK6Iq)9BeCa0zT%J?o?S;VprW`gE*Iaf#Wr?voCH89J68 zNJ=>DDsgOrZKBkq4w=@VkKlAdyznNHBD1Z%uKlS?h3y||1hlLTXM0ZanaRnqf;hIe zV238(x%`*pWA+sz^@|UZnebI2!`ff}T9-`kFY^Db*PqJ6VfYf$G+yqL4S^YbvQ&E> zk(~Q!i$8k3X*^tpe!&Zvkvn~>kO5Ek7`5_7HI$%&U+F z_Ia*mSWEqPa34o%sh+49Wu+12Djh;I*)?xr7x&o!0BwdG)>)S=Q!3XoH3rQ$%Mz#t zsTQOPqzEt2HO3OqUcH*gSRXmMu1D=oB%&lJuZv(DLIT{BP#s?n4fxoACSLtBpuee` zV4VDoqjQW%7=0rV6Kl&442{t-`(ue;5=3R;x#%6Q#5nN?jZpDK(Z>-c~ z65o}w8t|@-@|VFC>VtUu*LU6Vf~0s&f^^(=)O7MYBGRkPTC-uAT2(GWy>r@mEHcai zun1ZXh%&NY%q0L**H};a{@ax(pw_xd;vKfThtHe5c-}OqXtC5)a@PLgZRJpr96FsBs{cUuRzEm7Jj%J{v#GMa8ksb zjO}72UNUdTOd}rL99w+O+`L?SU8*NQ6xG_Ck`yKl1(ZPuX~BtspOp4`%qZW+CYAs*CDZ1 z0Nj|qCRmKCEllo(=WrCDXjJl{um}NwhRqv?#kksHR5?4^!r%K17Fkg%*Gz!6dqgr; z;X6iT1Z@F)J0mz>rq*=yLycGe$5a z0J5!Pp&rq7Z$d}^I8?xA^A%}DK?`#JI_OV^WvmeJ8_Ulj?pDmJcO)?%K zynPE*<$wjlb@eEnIH9T$1Pqp`2shqBRlj|MGN#P)wTG&bY`O6O-nda8K_8qW>LiVI z;+Jlo!Entqh2c0~$VtK_{d+gW%nLD;8M=k4vZtmfpBGL0Cd$L7*J9mlsFdAzw(poa z?j&_e(pPTkWd7>p+Cx>hFvc#Q!dU^k+*_zhSe&=G%cUH|nCl+rYj?9CBU+FVQh3fM zd43DyN9a(*+@d4X*bxdbChgmEN#uO(Zitz`j(Ho1x%sNo=$OG(Z_n9qsG2z&Xx64a zPt4X+oN4TVb*N({E%I9x{1E@_2vr=cE-2_xntaJ+F$jG#iMk+M`6v3(E4L=qGXfX2WC zm|0O*<)S=3Kj#fstZF{gunbjO%crU8)n@*~j%6SQ5~u{ym3i7`rKsIh(p8=qVvU>o zuQplvEDnZIeXnRZneESKeZROXodq^Jt18B5(P3Mx z@(7hapK*Iff?)-M9M9WAyn9l&Y6nc+DEGU^PhIiB?^d-pqsJ_olS;Yn+lq%-y{qbl z*tqW&hh5QZx-_DEx8G(BE0XxivGJ6^lo<$9Ek7+42Gh`Jv~Tyt zcAM3U?5E;z|K(1swyxYy{KzgviD3k*!3IUAAT^*G>>o+~fWj~;A4Wi;d^vE-)O$A} zwX^xqV<7;YtJt!vvL-`?nxpY-8YYJE$AQHKw6j_-PkFi0MQf3~7-Fr;ll*d%{_3o?@h2^rAKPy% zU8?ZI{bJ#OH&mc-7SF==8XFfb=7{f3eEybx~$pyP^6dcCgLn z{;}HUWk=qZ;Q!y=+k{G!r0ZedU-x|dO;1nHXgr!bnmf9`bAK%^vJherLdfz(7D5Pd z5kd$Td8+a|r|ws$vTptK>96-%7~5#!g%`$XEemXeu~+uOf{PYf2`{}gUK+IZ=8hvX zG9sRg_l>`-jI7M61KrT4D>V zHBVd|KkmvGF&RaY+hbgKOFx%)?Ug5Z$&flyYWs^09zR)|TD0fkC=RzAVuPV z)9$cdm5Y_A;mgmPeEC$a_p8ceq-Sr0wBO2!jQ-MLUA(c4J0C<^)U7oc-&SG%)~c@c zT27l4T8r+K-DAHPWNRRhoY<=9z$#}Dc!EnofgF+f99_GGMQznz56suz#*#Ahuqp2N z_{6u>V8NutN0P*sh{S64q1e3t^iF&^r+VxccTBq7Lir=}Ikn=kJU)Cp92bWNv3hm| z9*C;TYPT!*3sI%7oO}1t&21#{vtGN8_nYFNT=B~QZ}i#%c0rBYZ&Ve(***T!h%YJB z{YVo1*`B6d=+NQ9zkQ+nNt zE#X_qLak!=^=ngT^+T#7Gk92Y&ZOOk0G5TnhncT&7#OLAX`^d=YKtyZs+ZNBxM^dx zSe4~YWE<|S^0E3{8|3X09a5R*+pdedz4$z2zfEW_+Y$##Y<|I^qi1USD%jZp=)K=L z_I6R>WRy=Fw-y^My!1ym(cIQ^=lYsDCN(D3Z1M4HC{G8iA81wCq+26O-{`lZ)(CjbD2K)$5WjjK@rJD%>?M_L@&5DVu zJf+X8;CmO&i57O%xD^$9XV~gDQv%= z&+e0+JcxZqJS3-bZG-46$m@u_6UG?7QZyT0T%C&^kh$baF(<4MT0PU_ji5N=&058c zwV>=X&a*b7@84|8v>gJTn@>u@5Bf-(;UvLpGRl*9k3bY<>7hY6qd+H>rM~{|zFf(r zW_`Y^*raXN>H?WbPP=cdk!e^pM+QDm64Zg2ZD(UglJvtv^;Nmi723$$o2~z~J4;C= zy|4tah~$^lxWQJ^KJICr0=ItO>suW^PBN2<)q+DN$w|kLHW6{Vx2d-uc4eVn76|QW zM&(f*X@+s&4%xhdGnPsxqxXKy%TxCBl`qz>N!bk)rFs=S&KH4~Q_s31p(tk154%%T z_(mf>(k3mOeh~$*M{xw94T_$#QT{$km{CfvX+c-Zr(5Qp2zY&DZnQldn&5jQQ%u!n&KOc_()s%;2N4pw-lQd z;*4AKCcJjiorJ^JLkSW%NZ#x%8L~!DMdyQtDy>g|^EwCdM#A>qb)4iP4Q;rAyH5}6 zW4RD-keF3y2WPga0yqggy*el0;WaSobq;m(PTZC7=)!XP{cPZW)f{}RX(A4Ct`Ow%ADEd)peEAX<^cNVMhcBf>lPWElRiqvq1|*wsmxh>38w$p6K)s{+ujBU122pu zZt!uPxcBu?NWBFCa-6$wOK3340Vbc}G+F9MsqMWPW$MH7wYS*=RSHyq_e`0;k7Cek z`lc5xb|fV%I@>dEezyyQe0WLUIN#^n`gYlVeb}q@tk(OZz8*@~mp@GQ(mLMOk60uV zhBq*FmAEMELF9+hPG~@vRGC^2n*c5&B$*jJ!i)-;;}iYGMY zF@Z73j5Hxg7>p2B67fnJ+M1*G*-WNpG@hnk+_IX3aYo{q&{+~I96Qi<8gW{Ph8lxyt2Rc`#Xn7?Q0 zUdQ@=hN)aR3f0Zsetmc>MCq14(a*M|cw!Mg9-d3Jw0V`Hrj~L&t{07Q4e(BN+zav! z%aMkV{4^+zHSfT?+*0y`hn=S5X|`IS)F!?WeiUm8e6jzPdNG)?*_1UMkBH4B;lvtA zE5Rk-pSI#YpjEkd;SE=VAL^^mKUp^S#&vcqtTt3Cw9zXWTo)zHxgHk`YRh>z$$5mO z0f%Wj?z>gS?6@JsMhlir^-kJTeUte^RejKLspAw} ztrdDNKQE5QVp-Q)YKFZ2On(yMMfb+CB<|?9<-q@$H}>kqE41Z_@$R ztUYo93-z|(tt!vRZbkAIwtPuhU{c_gm#f7UiUh~g`}cvX4)V0N!3DybIeB*b-0hXG zO)F)_rkJnHt8OI4OhXD%2>P8(?~iHYq_w`-z_HV`i31xsNF4SEv!X_i>ZNoZzQJM{ z1VP#`sNM041wqc{+FRSI=55|x40X3xM7VqbG}8_rL^-{~SDS}x#XFt$w$gc$^NSX~ ztx|SUPVL^+gSDm)(V?2zc))_X@ps&Ao^%gfONGVw!7&mKZ~XE87&$;>vk`|1&d8>K zO>^Hy9^Y2cphqAaBFqIPmIR)4Gh1o4HM+wV*HS4g|NTXFKnjJ(B(JJBdCe-i&3v%( zd)gm2<>DYt=f@oDJYKxH*H=UfW)lWNax0>^4PP9$)dfla30el~;s3tU!jMtL0?%O&{ng zW=oCVPJ5e;;DK8;bKgc}bOuZE<;P;X5U=0ui>0{pzEF&aewo3zGJ0G(z*?!k;VZ`d zXfgaeY;{2n(jvot8&Ru(QJmuu0b65|kXBj43!SFa%Z+a<2jUkhhy6C9l+o8i>2ScW zC#LJTeN_v*#Cvhby#Ca{%1#AUY`Mps91pDtCQxBkwlqEO&E8TqC2@aIjF zLWEwYnXIBEVLmH$Me(7$z&OwMQapB%>3E3EGA8#hpC^AE@H32x=_L@#J#LDv``b79 zC=nsB^ZEw{9MM)A6-;`Ziyxw(bSPd@*ZU*L6E#w5`-=`9KUteGv?sqg&V+bZS4E+? zz4CM`>{-2})18;&jGw=)V^}-4)IaWv9TcGTt47j}-0hWWJLmA+?J`qnDDT_p`C1Pk2}#%K8Iya(G?s>b%wv>ZNc@f=4e) z4Lohm;q^V8RI9*#F**Vv+u`e}5Y&_mg7$XKZg}YF&f_@b&bgu)>zA*Vy*=r#+vU z;#YeP;kB*d{p+tgi7UUV&iPqsNHIMXO~M{IKpkdH7~^kq=-@V=X}g)<2=IpW%aS-z zT@`bZ91O#f)<6HYJ>BZ-^=kc1yn{aD0XB4=NBLbevlNVemc}z+Xgz&2$-51{{hf5* zho*SxzGIB+rism1__F!1!`!&8iXPphS-S0WO)$n$XHmn=KGgO&zv=dVGRk|rbzEP? zpL4m=y6fMuC;U_v$A?l}iWQqspYsG8KHsza9$Q*qeE(iB_H@~>`7G0Fz_`E9?)}sh zr`>yyk-avt9UEeg+m7q22y>|fJC7!A`Kh+^`I*NR2Ay|gXT_p(y>}bw1Py~iDQZD7ZuL25|v*VmSr ze~LehGYhYn$7r+4v*!9S7>~iSf9USOWJLVM+?a z4z=s8sE)G?P+Bit$dN*L7r}gy%N2RAzOmO4g614I2}<2L9%2&#?oz%hwp(@g=ugdt z`cvl%XeY^C+1H-B^N--t#dELbK5X)_KYX2% zI)ueed@-<%cVBkBLPSTNC$StLXUrf_Hr}e4oJ-#mX$9IN-K8kbecXyle%5DHlLBI7^J$4UTQb6@d zd@!k4tv8#>fBT-J<{@p)`(yf8)9f*rkhULzgz2f;yqDW{t%_f!$CS?lpSw8kXQ^;l zA@-@1#XiJ~l=$X~xUuDHwY&%+r7KA$oog3_@TsNH7cJz(^LLmvz9tx%C0GkJSa$Fm z7ZU3+dR!~okP2maVKnvm*NQVZ@T&N*@V}IK#f@!ujqhhyjk|_7Hua`KXPb){y=!wU z5EnUbKSbd3CIOS-_d|6?RBThk{6Wr(rT51{fX+FBZ3$rXPtp?NRb=)$xWGyi`fP#K z3z+xEXMMf+?349&cRIdn^_3brQ`>vh;r;PhS4LOoMT|gi8S1V}7Z5aBKerS2X~r+o zjIT3F7;DW1bB|jFKCA9nh&tTsPoCDR{Q@h>|t`xdUQ`nXu^t_69E2g4k^ zfZ;m5I;rMR{B8mDyb3X?nMY41kwf|ftF*=-Z8}s7aEM81S}zH%dy;R=B=Kar!q{AZ zYtCBwN4zr==%gLWnapU9WdufZd70;Ko(~7Xd+nUs**7D2wMr)u^SoHsV9w>Vt^rJM zHm^p&oWo%S8qYvL+l9pFzRk6)&=S*=XI(6pOck!>5=66Ez8wKn3G-^c!rB~zJDgjw zL+?lUdJN1d&lh)o_u0!F!{`dd#d(vmJ-SpH1!{2O+$I`n}^8Uy^UloWt z*Qv8D^-MZbSJPswURAuB)xMB_Q^i08=hmnc60ivfQo~cYCo%f3TS4X{jb`ZrP|Uj?wRI z9&Yd(y&;buXI`o7gev0ce`*1C2_Wau*>(m@KIkK;}9#=U5Ut?=1No!vR{q)YP zXto8nz1dK*yim_gQ@WgQXslSj@QRDa^xVd(*o*f?G(T7fVPn#2t_0<2;qEuDVQe1( zd6tC-%Lb{}KqrM{8fAtc18gzuPJ8js;%-wdKd>rtwAGXU_D;OVTZnsKzg{0+e&ku< zc*YfsXY&Mh3tME}xjWxh0}Ej`bIMMm2Yuz8OCP|`k`77?o)EsC7Arb_n)rA3xJ>jQ zjjf>)p5B=ijUI4mBpH--7z(a5UmzzO9Bc@e0r!&BNz%5gFZ7qG1&gAmy^@uuhNt7D z2I?qVV(yux7LU>b0=*65;H(IZH3TVuc}sN%b0>vnH5Tlp5$pi33_94|8np8eVln)@ z^$adFs2x_k>kYm#!1&9Unyi22!1Q5uUMDELTzCL<1H z(*bPT7N7MaQo>L#CFb~~fDhw|dPR9yAH@qkoqZN50n<)Ry2}!!8>Yi#A8acb@;?QN zN4c)n3ZL1Hd9mPRZ82l8l}z7d@K`2JY&$<4{M;+x`D%^O-ExT!bO!S z?n>WAD6?&*rW{n}q#tYBCg`V~cbqb@u|j5iWkP7&Rh#wUp}tDlqg79PB?PZtPe;pW z8SCKKsE&+n%W?;T+kBMm3|VC*+(cbgA5Tce9}~KzF6*y_s8}2e!;$( z=PMAxl3Y(l#T^SnIeT9RHFKU8zMgX&%BBXaPzaPPHzc6JZVJ^PA39tcf*hQEwNcDc zn3`i(^HM<1Q+$Se;emp4=S8L&0Ov8nsX(Zg8hw0HKzX}eh4|L~YMvZ10#8!p2sE>p zDD@WT!_Vph1KK_W&QNmTdfZ4%PbK#FqygW*JRKK4#z+PFIw&!^oKo8J^|ZL69VWX) zNhYet?J-n;scecsydX35@FUnwNjE+@U=dqOhMcz<##_Q!8Ruyq>i>BnqKf}%PPxsDM%OzB7) z#}wK{Lk%#Fvr!4FZ%Gk$`&!S>M+Ir;jt44(cKpoJ#L+ftGUJbDa|E`eTV%QIYwh*S zQkL(ir4CUdjy+HrKH335K(9!gMN>E5oE^0iHv4AR?OkF1Q|+u9oJ49RR#z|9*PN0t+EOlNZ? zPa0*;q6B@zxY+NjCxumeN*vzV9sXyiXlz8)trX`i?-#qOerrO$zxsnQXX6yrRyt|; z=CM2$>gI&Ev0C|*J8w7#t^9CCTKSQF$1*ZT&Q>cwnvqt1d^1}4@r<H=~umH6yM3?VHic-=2|H{_tkB@`p3h%HO#et^A!CY31+Uj8^{cjI{FiZbmDA zZ$?`A`!}PNzds|b{DYg(%0HNqR{r75XyqTyNGpGIGg|qh8ENGo-HcZL(Tue6k8egR z|9D1P`6oA{m47lLt^CuQ(aJxakyie*o6*X<8ENJ7o6*YWGt$ZzH=~s=W~7xbZ$>L$ z&PXfYzZtE3e@0sQ!FgKw?C?(39z z`l<-}jdS~^2GXUS-AjE(!@E_v6CdgpmVq0FCVyBOF-F{u6cHDXc>IcaKlmafvEb6!EtE2 zdb;EoeCnX?KR#{mHzhZ52{_->HPgI^gYsK;Z>oEX+=M2kqEwAaEU6WyCVIMd6q~V(awF^0G#`IZ(PZvOGKh{fU zOpMU9jDmmgNke>cxap{QNx7JZ4L)ITyDfH1Xpz(5n?}hq__XovWs&dfcH%n&?nESN zj#9|Lg9jJnmyPqQ6Sqc?Kx*Re5+&>4V!+M6S_~b9oQw+gx{NR!2SVvfpwDFlforO@ zRvd0gvftO?G9qxeh8$I(&t(Mp!WjBpS-uai&%oU-tLlSraCJYj+WAympZlTQzpxgB z5TM0(=$0SK_SV2Z`^9UO$iA>wgm436J7U{4c2M|i{8Fq2o2&nP144*b16Hfp!2r+? zCnmd&v9;d57-+F)AZ;NGVdl+DvnaXi3 z>QiC${i;u#<{7p$GCqa-@lpqcfxx${?(Q~-Z&(~yi)tu%0l1cG%5SaemtH8=dJ9Sb z7NIH;W${&vG9b`w%HsWEvpycnJwJcGt&-sNr}yHP+~dQGw|M1nv0tsX#b&Wui(+DZ zf9)rS>LePa$p^uy*e=)Q_9!giPJC)UtR7&h{J60A#@yl{?1*}Q7ugZrZwdj^Li{^2 zx-pz>#I@7e$3`!wVc(SZ_hJJkKl0#{1m64o#|!wRoJN#)3F9&)@Ju4(-k%PO_xn>! zN(yC}UDecauz$2YfhS&WijQFq0}aHE*s9!$+@BE|#hnjTuZRuM9lwOgqi zDlyW)5_(@eEtWO&_F)Ym%bP9>;j*fDuMfk@As5Bh5zqk7+HQ%_hR!=O%AGxOXN6FM zyhsUhXAe&w5F6uY&qU0rt{s@$WPTiqI^1}%yMD8 zXu@$ge4ur-fe(41UOhwLZMij}K=qp8v$i{!)JVuq++>hf0OnO95 zX~8S%4YuoAJj1F=SJOmdlRMpmg)9!+g!)k&FPMj!z-nEH>hvlu(}fjN+fVZ}lv%Tb zmyhMX*rX5M_KO~z)%blCJ2C{yrc+wHV+U_4AyHa~!JAHLgTtcH=zLS`)`vKaL+p^f zHpMrm%{m%+*nv0Wi!Ucj*BJ!>tjScz=;2|BH5Sof$g$|1XIs>@Yz37TSgKyU-&9Yf zP*FI%14bgWoUG6BYts*l2IktsSHO^CxhHGFqA(nlih~N9nfA|y z>+WLI$aVIao>92&BXcIa*5 z>iWzHz#_MuK=dN_%#kU{VwbpQ4h*S6D{|xlcldGRmbYh)3wd2z#}MWGLpfUc2F7pk zP8$DA>0w)}NlW{V-DgV>qGVl@t{;&Un0w^WFl5s^HAeOk6TEiQJv~D985O*C)4gYW z>@ze_vgzI@y~K>85+H$k=CKSP;tY{o`Vv;ccv=R)Dr4q%DCVE;Tdm z6NvkWYO}dV816Hy4OV&(9%R(y5Zq@}up;%E)aa+C^y)HZDVn$fg`MRgk085N_ORJL zN{L7v6o32rSlmO#EZ)O9ERGMwad9V(hLU6C;6t46=qeG-XWn1h=S#saJQiDVu<)z0 zd1oznRyJjPdr*k3X3(qREZC%$uz5izE3R-LZt&ytic20{wwqIR9v)X$1uxxnN(*Vw zTaH}sk%d!Wh)A+q+Tj=&wRykus%Q&_xfADa7nhMRCWh#MlNe0EZK+o_!ju76La%$c z%mhqx==VKbLxOAnlT)!jezUmSRKh|&Syrd*@s)L8&3P{VU;q1m`8|1<%JD$@nj?5W z@!nK*d|1c{=pBpB87)_9wRP*9S=5Pru-6uasB9bbWz9Xb#|J$Jf6vCHYGAe4dRQEa zZA6hC&c@wuii6`gf!#`JCDHCc$!do3pkJ4bl4!2RbnKd(lz*_v-14t8CQrjaCpvZ% z{lI3#9LC@9+n0y+R-9BlJx09N2BA^*OAC#_{J~%Maxotk2Mz1HbfQW#-puMrS!6Sx zr;Xi;4jIK0SasBLxNU=LNXO*u0?L)GUFflR;XEDam{D?qJ3ff_d+IX8CgPYQIJnLF zu}pex188|y2x3OOu>cOJ1xohhQF66DAEKx7xH#;p?R^A9Ag1iXPnI<6IZG6Jw?8e# zvhk`?VW9rgL;Wh7cw0{_FskFR2q%p22BqvQ0_-;;Fq>!C%28T){pEXc(bx9gyvO&V z8)?ZanvMa2FBQNhaJ3gp|1=5V-PX2uP((TNh>p=%q{z5C?RR2DV>A%fB22aI9h9;2 zwj~kR^%S4LK^b9ja<@69NeHJ{n!bJH3qIp4>A(!{#9Oc1Bn5&Q%FaRgh;=#o`DE&F zAgDeNP`;6v!cY)Uc1{=2(Qh~bp`=-7;VI(50fxG3q;R2L!-)pEYmP!E2?x59orB_a zh((!NAI7E_`J@e!6L!}hi+k}lc#$OHubac(s(VmKDOXHSpJW}Vf?@(=HtYKbQE^Xo zq(3u~!V@BejqE&}yb$_7mK$+kF-fZs7TeyDaz=C^7WqQ1{`AHJb3I;AHnub9q5aPf#^f zg_`=UXnWBp4b#zBKVDBIsz8HJtH+FfvkU5{>dh`$?re&Pj_PutQ%@P-c#po3PhHm| zt{Bd`2MZ~a9@|{Fn{{Y#EP813_F8ViIndR4l-y%;lIwl<`a;{RxP#Zxxa-mL@A|_k zL=WF=+~$^>)3~`BI1}b?q|*aux)b;HGj9;fcJQ*^vU=Q!ZLaXVpu}E2?v5W9k43wA z@0C(tVAv*iN-Y#_9P89og6JnO)u1I=0r%xQmCb%iJny~GA42|623kc{7 zaU0=Xi@~cG9Snlq6b^5p?k*C)<9aFH*nKEA??1g0rRnO?NRR`k;dla>D1+0nSnnMf zLqtb~)ywamc8B#!>=xC9LviEfi`QR1mFxYga#q*%(qtfF4F{N)Ay48$Al|rw-85L= zsUITl6g`OYhk)wsFExKJtF8FtNqwK-;`nh_F3RF?e6tmK;oBdxzi+Gk@xj-ixs^~h z;7528mzp)d&xOj6!dS|I5e#6?vl8M813ccwKWSE!4F zCujt_sUdTP43806SPf=$L7DAhrI%s@4 zz|t(&0pX%Zd;xn8Oxw2hC|Q-_Y}hWZ$gbj|bmH|4 zmSd6VczY;p%uJG#AI?^i^pZabPu09ZW7_LakAd=*;^{z{5G}ixVfi@e5yqBYM*`y@ zJ-sy)Q5NY7MXd0?4ti)kW8NO2YH3o)`fCX6YE7D>zmFO@EfbGI$OE+nP)BMxYfoo2 zWhN2w%i7XIm#@@=(qxZ8H3}ctW9%xEU(33iD>;n+x&roG$h)L5D25A4J83M7Vvx_J zSSBWC>g%B98DbIaS9NLYUB!lasnM@!5gQWOj#g4dpcYe^pxLi5HWr<|dlD>GC7$*Q7c-00 z{Nq_8#bfDqrL02S&GYC(J=HjE@X(iWvHmPBw`;PEh7{@Bl34oZMYT#V;6E9gTda1c|xf9_ih5=2h%%nuaBRDkA ze3*@M^8wEAbp|{*r;bqbN42dx!)V z!pQ^e@&?J^+jMAW^{0nDw!${mc%E-2whMsDo?b#2wugPeZ=0JQnEvW4vhzis=}H{sF!sg#f!oOvf6(!-fmep?1R;OQ(p5kzp6Ajm%A2^cj_$5FnIBKb2>PWkTf>*d zKkB2ne6=^KtHS*}DPbWYt`^)DcQGGahHOs1$g6tu68U;02r_iQhTE02AWY=uWs2;a zAf6`}L)gRbJcn}-Z;%JM^nKcy!AtL>jTYxvjvppIKKuJ9XlGgH9M(krKxF_o-)3)6n`8L` zH`#eS-R%`od(8Yi&+VSlh$8fI#87J2IfDDbDpzX-d@$`zPYruX#p%O$Q;560l5A4Z zlT(8`Zn|14(SnY^(IB5|>S4cqSL5(<2CU*eJ6E0#8c;yI=Qgh$wct8IS!Uq&YV1a8 zJ_b&HmxjmA_FNs)mFq>mqaHSDkzh~SIuR!avK#3k^rU#nJj!atvsS>K4r$Uujn!B~c|=nhCHrx<+ATyn$`25Qo+opNXVYFj*U@-rENrvq zwH>aAIB4vS5uOf8M#nvA-D{%SKM}y6t ziY+b=KB}wT_0);8X)$UV>#24Gj0&=J`1B(^;=0`h54fB)Z1|X_mzXmuThAFYy0?lc zYIbQMU5e`-h1H00L?p`fRt~IL4dUI3u*EE24+X~XahhLE!^;H6S*$3m#S zx}BBaaV}thxwE9S5>b@3+g?pKjkKwx>?x*?V0_^7^-!XYKuK#Foi7K6nTgxzLR+2Q z=y@z92fHZ%2GF$~%3zIJI}iOy*s!qh{zs>$M^JB{q87<}-jvw`q}uA{`BM4Lky0Z~A2AtaaBEww9De1LuYl>On3^{NI7A&n z1@I)vRn~Pagh?UHqxcF%TSsypefRay`hj57vtVlS>vwjf!w;)$S8~GPC&;>`&*I>eSQ!^DyBKzo{M?OBm_jFLh(1^o8WE2`v?IY-L zsz1v-xOgKYqsJoZPD*G;ac#EsRF+7CL#!EWihHqm^GSe-$&R3%FH7JH4#vbuXlF^> zwQxy6uA$aMzkVi$`g zUfwPV0u3kyT)aW0=!PS!$QRR7$;9|s>WmFzA~Z=zdxj?A>IRKLw|2*?Gu%(fA0gU+ zIz>;_A@#zu8xZJp8A&*VwTUOxK4Qf5R1i={#CWR+12!}uz9|C@SpTF8XH5FSaiaSW1qh!n*!~IXDZ;C4Q!8O@ zAGBdjVl|O8+mx0M)eRhiUF8fR5`-N+p0^U#gKknX=SZw@W1K@m;}yjtfBGJ-5#mLk!T& z@|#VnA)jYvP@s7{#qSD0Mx(p2X2qlx&c2FeLEDNlr=F5QF&ZTEn2)D}5@-ao=_xI% z3%8j#n=45jCF=o?^g2dx7RApq4@iOW;*4;(hhzae#@dLlyT&@SCmLlYE(y70(9u>+ z$Ux2Kf$(Z%7ZZFt~Q`38;}2 zD9(`{ZoXorM=xP*Xhd~Y2n_Em24yp=%Wk~%KXsEg}N-7!#iEc+FR$fCr7l9~?<1748T@?x=80)!Euw-Bj zRu}m_WaxHOFm%H&>y>Se!)x*CZu2m)A*AbTmQ-(Eo4y_ivp1;$+34>j2-E?HARhY+ zc2gn^YMpOw-T6QbD={cUt4Yf@1-lI>yDe}p9fRR9I>vOtZFsJZO4i~14>^5$p0ei& z0!bXs1#ND32mk8gdKiw3AyNnv;p60WMYbuA@Ncz}|9cuOCZ5?nbUfdzGUg+%^p zf@6eCMZBJt2zjdUFZ!a|_5eLkzo@PX?yeh3bu$n-)%8qa*U5z()!UYcpQfai?>Vd> zp17~Z56_-UR8;T8+^*A2NjgAoXUpjW>M8~9MN2N1F&aut=&X)PxkI`6VKv-CawgtK zJS7c=3)!$cmU&G-B8E95j3XjttDr|Ao6&1<5G!stu$T;XGWGMS3n> zproy}Z;BAS-2OgF<|J{*0vMRw9=Q&iD@TI1vn>dd5_x)SkpkYw=WU~<;G9m|nf11A zzlpt+2x&e!Aa9hny&l0m-ZolD&Sz*nvEG)UYWN{a-Eod5pynuVn-KxY@S%n7;2J<+ zC~wOxyNtJG&AT&AiA2Oxl>UR}9uS=P<(^?CQ0DSo9UIkE$;*UQY69m)CfI*Cq<`W_ z2U*$Ij!RO(+}46@ujGsn!S+_}hlV37yfIXxVgy$%eTMkQhbzcRywnBs5l(<9L-~d_;&Y8ErU^gWy^(~C>)3H^qloMn! z*2+@fILQAFL=SL>^$ura>ZqX&SWk3};H=X1z!Y|ab$~dY-oH-@E&B6+bx+5xnX|JhP%Cz(GjR_B^1IJIt+2h zmzO1F__hkjZgh-_2|X9~fm(!{4G|rcG_%Y)&7Ksgp@6x=MbNSDt7CmwUqKzbO*GU? zVV%K~o+mEy#1i-5Sg$fwg@~sp1mYYmYg0z&zJj6Ax%CYqH3MV4?A4m4KfO^Ce5`2} z7h0|MYBCuJ0j|ajYGEP09NSXOtP2yxg6AEds4|rCbyrik99IUwhv$mhW4qfcI2`fY zxzKd9A28kg5JAt3Npv`{OsPS*Zy*U&Ua_gb(yWr5f(n%MXqm8=&g6Rx=v|1;Pb2 zlTEe!peEvlh@t?wpsthQV|gbN>J^+!A7_KpvP>xlGF0Qd+hx)PS63G-sR<;w0}m*! z(e#9S7!7#UcH}4z3~lwS0Hm`$Fj-c`Av6wc_$*V74In6t8h@=&qVAUQ$w|J4oJ)eU zPWAVt@Q#&m#U31i-PyC!2=!9ZXIO9er0u41Y7z!rV=tbkP!|h_RVYTD=p62+rZ_xH zNJ`_8_&k=l*BCyQ)V;Ly->Lsvh|AoMi@WvlMgEn9V+6gnF_dw}Ia3}Wz6*Rp=Ywx- z%yTtX;Fzh@?4{&{J&Mj?9_#gXQR5C^x>JD&7XS5k={hj+>5;F80>~&Pf+NHnU24z5 z5jX;&bHoS~g546Ju-BlG^3xHF4|hF++6e4fcEom+9A#t=bW$@L-gT1z?-Dc%SDenl z?dQE+6i)e1kEHG_BpEO{9SsTUzC5hA(O=ers0hYrp1^ygP%i~@giy9A_LiiRQfL7@ zfydK9&419aL%eQpQ9Bd|Q9aq8w~G>&SqxBz5}{L1sO>GYLtC65&@0k96iWB$>8*r! zxP*M)RdNhyF2CmQL^I$L*9_lNG9SwAE<-dnCUSAT6ewZ}_LtS*`c*v$Yh`*UqXuVR zHD1u*0<{g2{c^&wk&0`mW@RYPXT1;-JD(cnou{ic9j#9)$H_6cz~D`E)X-$$rS^77 z5Z#U!qsR;HLyPH1*OVyKaWdZ35X;zzXRHAEC*%(}w=6v|>jLWL8n}6{ze#H5f2A>8 z?ZEQ|)ni;Lpxf>E@&FBq4-UL$6MH%(`XZ^;0a) zk5zSi5dNf!`QSTL1F&Ok2=;Z4mYijP?U^CYwG)*!=`iIXR&;u@GDc6j!(nM6-hna; z2`hR_NKYCc=%ldd5qwx_O|D56FLWlIVn$fW@Qt-r{>}qfT}!RS^i)U;;Mr_mpDp=s zKCZXra$meZ{+yTGLx8xxN_r;cpw#FlA*npTbfxuU;;z<6Uj}uA770kNn9r9pCLDT% zf+9!+R&ZPQK54rPElE{Rdo?iwKfkzueH)4q%M|rws%nN0W_Xi=hKFRq@_37Llb}Ef z;oB39>Usub0Vqf|1bf~|A9_P7)tN%@T&|~s5;tfR9fM;fwJoHtco>RNJeJ{|Y0*$l z`YU^mNRm}=4nNJEZtK_>%^Hoen-!4~I2gGD)=JiMy%n1v>TuF^KuZ-htLCSk#~6oA zGSl7^NRnBSz}rQUeE1+mWX{S3hiP!Ro)X!STU|1H>)4vtgloY9#z^)kdXp{z+~A}& z`aiBk5Fg;)iUQ4XU2PY~kGpbl$vxl>`l;aJdsS-hAogY{EEG4FQF#`eok>cxecd(O zP$FgzZL{*)DM^n@SA3J>cMp6$qQ&XJ6eSw~)ZU{MyIbO$uq|Rg0m6EY67d|4!deI6 ziCrHteH0W!g`NS(nomfk97yFhD>3Kl$QH6cYv(8HIrMeeD3>7kt*!2%s2e<27Y^`LX!Jf$o#vvC4pL5%K;uAk3<|0%r zQoQYrDF6S3!yRcSe*rLIQcP!dAWd%&v zN+q1GXGFQ8s`nS;k;db_B( zXKKr$7>{4nnj(omm$1aA?d`AUlrtAjZ-fA*i z&Pb)XaV^#4rVyuW_f4jg zoP_{YB5RD+hxJ0el=MA}@oXOpnXSvm+NLR$8qx8Ra{W$B>|i6QnspB@N@U`PPUY^| z+IGi8md84~2u0LxZ-w9`_zcSxi_cBI%m*8O!cyp{2eOK^gHD9SgoBS;gE2uUwzmQ# zZ!j7Jka%n3gHwEe1qyRIgZhdH?t6mNAjr6t-8EMpm)PD4l?Z`tgCKFS`g*-uf1^{| zurW?&GqxW=d;<(wB%cqAN4cz>Td+iTSD-}5JG`O8g)6o-c;dQbe_Y=) z#EbDIVPED^g!W}<@Yeb=y1NoIXyYA6l4|h7V3J_7b`(=$_OM||n}o3g;1Hu91{{yZ zS;N<>(vD(-5u*Dn1#8?+B-|D*&GxA*jt^yh#Z+wln&jW`2yGX3jMuYdM2&hNt5Msn z4ImxKJ~`>o>UEu4>Mg#%f+Vlm4*<#U*@2#igk8H!P*R?tVCmr|SdMr?{Q%hb)ZMjH z+akhr>GoH+^oaHY$oVr52ZHfFvhx`dM!Ar1#P?}ayEZg_9rD%O(~LRHf$pdQf|DeF zAXrk&*N*y(zfXCb!Zpqt1n?nYe;|B(wCNhLjZT;V-Tn%W9!>zu?8z}7>zt;uCMWOP z0-Y`;3+8%^ENI?VY>(-wq)KxH0pb$FK1-Rn{9*y{MDn=43WPYT?YM{l62yehLV{Ol z*W`F^$MjSvjBTtK?u~wfQ99V|40!^sARL)_ytM;Dqt!84I@g$iL>HV!#x`fUa_q4>V>wC65XCk_#Va!r#@F@xf5r1k7bzQaF{~B(4@QD zI^}04rSm?B)snr8?k52F4|##>YN6-P+NTmC&FZ9(!ND>L3VT6}pcp(Y-bHj&n8ev! z$Fws_>_wY9BdWx39KoE-A?FA()#7D@7G7T=WJYU3JLTlLVCFm|;HkNX2` z-w3>{R*7Q*${)(jE@{x!el0+*yCn_^H^x1lj^fh*;bCEI*ev=CthCf8c#_ceg%<2d03_p8ZEY6r0C>s+K)t zM}8x{HdxbLxw1ZIqEkBa8Jli<<>+32wh~^VxkUZV%P$_vLTF%JF2!f7*2kAGKd+X{ zllWV>8u4eH^1ay>k0m&f_4dQ!P%OiEcZb)qwr>`z>2#xv91ZFDu@HB?m-~IOtd7S* z=O|w^pXpm~?-$47Zc`esyVCwI1~oMc0Ogep$_pEmAk_I#3*n>W>7tqoH47L3@dW|V z%*aL1Ye$VUVx=zvR~OnB0PUw;B=e!B=4Qq-A0iyjLVc(KD#VppR{O$8A-`KCpF6x? zo0x@K4&Fuu5Pa1|h{HV}*QSLUph6v)Wtq>YaZFLZ1CS(fl_VRTxO(ke^Py%(0~;wo zbu_=Gl@k*Ll=@A{aG-q6i&`gl?SxDMXxD15ooiZmuGCBE%kpi&GZJ5Ot_`LU4ZK{l z;;Ln#NIomBK6C9;aRcA6JOH^!khS5=xDK5@*8r$bZAWJ1b1!epulXz#P$RzUwo!|Y z1^^);o@JpX8bCdAvwkcWtB(;!5B;U0S(zvy8$qgrzX}I830pVacnN$X5y6q?-Xv^* z6R}InwT%axX7yy(2LKx}(u={SGH(;OWAGDRLvHd$){Qi@N*E8ed9%&(Z4TJr?BB<7v#FkL zqjVIArC1bk#L^oD4#EP7rg8r5ihJ7!c3tQ>HM!EpvDOH`c!aL$8*)D0TD!SZDm z;CM!b;2e{2kfIPDXQe1(F7l_J0J{%55SKS*a0qdWXXKcSa|1^R=wKYY%DP$KKO7f# zo6{htl6Y`JkPZ!=ngy69p_COxEuiF#yW5Ilmd$H_*}$8=$pWBcRT1awvVj-knBs(q zU3Fz(Xy$1ZCX|>+d?iR`XWlfxG{Q_8Gi=@%~sZa~bA^ zEcG_Q>*MnX4M{kD_IW8UgIMXyGk6Fd&5FAj^lh4LWWXTN*0R)R@H_z{f@a*EJl^El(3CD#Q&q?xeU2Tcavw<}Bg9XrM@M`cv(bkcw zz_o6V#eKOw7DF6$nug8x$&d*LE$?SuF3f{mC@ApA4jn4gD6sq|G1YIJT(D8;@iimI z$<3fif~+6SXXNxfam3GA$T@PmXU=Uy7Cb6)+BZ-3pjaTQ!!7Mmk()+@@cFs!G9S}l zLcvk9lIe2};w^~~BoWc#U^0BoevXNwBpO&KS(eGM_Bpod*cwWHEi`W(IXgnw+yiTN z9D2%Qt=ch6HnsvXAwT!bIoGxr@qJb%e1ox-i4er6`B8d~5M^_lHEzWQ{2V5Dj`}8? zQLOgG_I{8URi*fhN*HvQQ3me$61VXq;gq+Q`0lLnz5;l3EcD< z*ELnlK1lo+GpbJC$APJm;BdioWLChDXkY<1rm0Q?IOS=9U7=*x0*MPRH=d3FnAE9t z9iRnfR^XJbryEr*Yyd~<@{APO^SEi4Draa)rGOOiaaJDp996M80>BZ!_Y7R~>Q?uK zt{BEyQPsgX_`dI5wJjHqWwAN;TX562)P!=JDH~!79P(U3Kw`tTxRP1!8xzx3F(_AhGRBMJDb~+8*IH`Fo`_z+i;5Pj{ z1pFIaTXhNgoWJq@jl=~j?;X*<&5Ca%E?f?`Y4|tg%fKc{An;?vdB~h`VOD$#`L_}6 zbQ*l)*rGU?uwHN+7rFU(O8lF>=IZ8j&vv>mm+Tvamd}gGLpH_o!}YuZr)<&}7yR7f zES=-qP`FJ$E?B88Kuf|zL|nKX+yE$&_gWkr5x42bQ@|}07e>Tw8g{C+SMT6)uBwLv zGKVtJM^dw|Z@Zu38Nz42xjusC4ia=-roUy7gKyKYQ=e(t>jgV!1OGQ7 z-?Zr^8SI7Zl*CgD!x3?tKHq>(B%VIwwvG8fA@yNhfA`EczTQg~2$$nql3UZxa7_PZ z#|3MRfEzUtb)@Uc-?a8Zd2a%UZ*Dyg>0HI%p_h$fL>*~gJ#B}mBTXZoDhH4dJ1va3 z43MiMT^DYg)C`s+p4z@HT<;wUH#IvmV$4Ey%(ws?B=PiexM`0Z1D6JQOx$cIW?|KN z=gnvzk4EGAbSS<}!@ot^DOoSr7~UFgn)Sgvq!V0`Yt;9#dB{!jE#%)K)>$`+8{w3- z&a!fo>(4_}z=7DASTo{00&*ExVsVURL=O)-@;U^cVk4TW^s!{ zUfJpQAJV40`EqmX`FYv zb=XM!e1gn|7PLl2n)CA z-+*s4es*K+Gj5R^&6nfbtn8G;1v|Dr$AxmGYBXS{BraSIw`uTA&my7lMuLO|qA7*s zhzokg7hNwf+#)f6z@n$_0y}5^rU$<$ZoXAL*lEbW-CR7?+H+BdA!Nwe<+ao4*DUSz zsg-*~RFQJ{x9Q^sc1nDnjp2BCh_;Y9__t69b<~l*XAU6k%hmO~#@06~#IbF7)TYn1 z?(!2|8HoyxSUNmvv+``jLM^V{7;2jF3He7TIGFt77P;27(7>Bl9r)EM;TBPY+=DtY z3sOG!xJA^uz+g0>3$16=dMu@$^EIy8Mq;SHBJ%9lqAp9lvCO0I+Tgai*29Ncyy9@r zXQ4X5ndR!;S{pn^11=hbmII@qrfkDxIx(aZ+`q(CpP?A)Kn*IB_xrNEqAQO$zoaw@ zs{_I|D;_-K20QWz>jNuFsECk*Z%V6|wNo_<;8tkmX>p5VY);(dc$a<^my?>olK3|( zmX64`Y2c` z8;J`RZX@z-8vadb5Au*uj%#7WdB~jpZ5p_7QZrc6LU=bRwy?~Jo0{D` zf3xylYYmZ;Z?m#f;`44DH{~rLE)Z^6HfeE-W31xae_&1=Evsc!Ebhu;dwBiku{;)k z_KVlAKUr3%?eW!%&pxeA$KC1p?iKg6SF+j57w=>VZTgJsI)B_TB?Pq4%!{1T_AFup ziK%h6daIlR3l6QeM&;b}trd_XzR%*EBaeIL+?F`DXXLn8raUlkF68G%<(!(|Wg~=| z1)vo2X%3x!zqPcQM z^(9T0*u^Ii;*!YCz|T3qN|IYI<8;ZZrKwLJ$vM`&jG zIR|n-G*zZev0EP&o8p_(W_|r-ri9@74Bh+V&*97xKu4l0acD za4S!hKmzNCud|?YtRph9PBZ+XVM5O$AvGbZ3WB3rf@Okk`uQ-hF68URW!74+uX>l+yETm+%s@*wlUw>3N6B(8HbfRJ1wz28!*z5(Pc5!>8~OJz+pb;_{>EX z;HDoO5`c4jdEiEY15*t7xG_z2`iukM$b#SUchA6KZ~)*4uLUwB|$h>0UIKD6}B(`Zq)#oLb zBdU@`{sn>4Mly$u8^XA_s0v>eTz)KW*5Mtl3A8xpWqNxhqq z0+(4Ce4BO#F=%Ow$Ak^)AtB#pF)$;gW0U^{Q zx$jN04UCjTQVXePBV|9tNTTaAY|f2mdpFC53fTC}Hp4b|NFX+QYp|)z2AD$((pnK% zQFG*UcN&qD6PkgN(91Gd3rZD9e`L-hZPxb>$Ho1=_;@|vcw>Mfobv<4!Ji#GojqN2 z#h}2mLxF9iV9?eD5ElV&8fL4&18_)wYXjL`k=`(PoahSBZiK}pK5>DL2z%u|uOE2C zE#4-0+Vh6it!^5UwZGX>eP-TTd_neZeMGaRZn5hJvqxLyS(?Ic;}%bV8EW(brqw03p4O`WF;1tq+Bg*(WVL*(lwZ6w%SxPz@{fW*~A2p zA}MiPc*p?TGy;RqNV^=u^o<5tJ|($Y*i?QE%p!iwVwP24;(%B2Gy`nYH&Qp-sCTI|NLQEzi=%1ya2@sJm zX+dNeBxmNgZKT#>)P@hBb`+E{MAGT|Hh_gXGb_v*o52cj2H2*NfGAfH@LibeJ{#%M zdzW07P#8&RMm!qZW@V(IFcD5bcI)kj>p8{5#X)7b;M)kjtnly*8=JS0kYKGCo?&BQ zJHK!=l#wb^FwXQ6>29Qv65nRA&5Eu}x;qqXvkD2sw>`t=Jh~4dA>`Y}W7{;+-AE%1 z`L^+3n}%=G$_wq5`lv^;k&M1=8nE?c8$qgre@5756%t4!wO||1NT&f?ha{2IBVmGEx#r2~r*WGs33FiOF&`#$rN)R(JI*LLnikvXl|FX&9-`F5QJA;_9x+w|y!x zfv*7Ei7&C0i(U1!D);nrY_d62WS0-cepwY8SJOXdjs@SJwywv1_GxuG?oP*dujBym zNy7K*eYvPdxi4RRvaC+q<7<~Wz4+{t^|pRWs#LG#gI~RUeJt+3Yi3*vc4&{4jVZ|M zx4Zqi+OLlvUuv1&NMvw!59Q(L!W|GSmc?>iEbgk}s04js1O69`&~5W&{q&|&3%mGW zB=5!SDSpp9Yt=|mMoe;9-Q8`(h}D*9ZO<0?S8P}1VSQBGc=e)lD4l7jow9bKwJVR6 ztq1&f#p7L7E%xG+Xbk*qa|wUlDHVR0;;z`ajTjhmw_h(mEbcbd@&i?s4gSxJdf%_h z&1&&j9E8@yt0l#DTkaRT;&?3g+g#HISGC-ir4VpY9}-p2cGT}^t0?gL%lE=Ew)cyb z@G;FTNyN3=!vBg9UX4DE*r2WI>++-cbV0E`d<<*rFPaIz-&9Wmt#RfH<36pA4`STI zhjLR2`xgIk+H5R+X=5}QP3Q?eExsTQ?xzNaX8e(00KSx45yiGgVJP?2W>sz%#ZG*J zB1waf)gKpvuH$I%3xn8K>lm;6JEp^3wO^e?++GXs+=zVMlp*$(kGtbX;c|8oxL1lt zZIp)Cv#cI>#o-VJ*!}c3(N63O8Jz*MyXwiA6hTYCaf_0I!qS}Yca z>a>f7J~1(Zi*@hT`{jc_FZwaeu4&yrE&9?tYALc7&h#aTfR7>>7lC>8vD7rT;e!w- z8_Q>F^jz@}8*5McE!p|-;^pg&cu%yurxTkZMav1cTJIPiisdL$O4(j(SreuFzDG%K z9?N6#XTNx@7eF^{HLE z?5jgWrar{j-DikO+`d|cQ`l}6pzC+7;ID!%qRpXzIsOy{v(n8;en-!x5m>q2)opzV z3g1@o$i(bX@)H1cQ|#7UaxeGhvz#gsY$*P)Mo4OIVDZQ z<7*6@Zd3`cUlp|6tWtsv>QE2VlJeijUBZEglOP!AnO*fVH&Bk`>?d$OgXupQ!ONNzV}QJ2b9a3dK; zNBHVPG*eR3JH%{XJ;_hnD4FfG1Mu&zY3xhz9F3J3j-25^;AX7A#zEVc(H{Eo?M zHFOF(j>ZE?%2c7WqMie$if>>qOyNzbqFZBQ1HF16ERbeag_4#TljPy5p6f+n^My=1 z%X64qFV51L1T!8#BUs(@qbNp<{J`*XL{C@E@R_saUW20X^KsUb<~iu=ij7<#)!`zP z-z9Er;nYMDvHvJ{wuoif@f(i*h|R6KU;jNWNR*m~(DY22804T;Ax@t7jwZoN566T3$`{r&a6Cv5{`B3)<@wI#lgUhIQ~Q5)DjUKfcI$es;vsZs2`fZvS+jUN9UjCntVdS|VASDsUoa=* zZ5o3^dSC7zJO@Mgoy*NZEj;*>6jlpc+Z`LtJ2VeSYdMv~ll*Qp#ma0G( zefk^6-iy~{U5El%?#01WA&@)*MA85oSU{rb_`V)R!`aCTK&r;kigU^PVp$!J#d+}n ztTJ%I9U||aoJ1n|4TZy&4<0Up_12tif2mH3bQUqhPqdx1gd*=@Up}&f985Jj@f2gp z4;Cq}WeWoiq-h(OctTqx$KZ*lo;zWjg!BgU#KA=dP6@cDergL`wj^L;;>mSV^fhad zl*AJPqdZ6E%{-qTgtPFT7KPkO(LXiwOq)`(Jyh@WsAU$})95iJPuhvajEy+7v#PTI z@nXy2z{PgkTe;^F9sa<3tB9t2qjW2XpV|b=1J0+mhfkSX7A_gZy|L#8p28n}0Kf1` z3zLjyS>}sBj>gJ@hY3LJ^q(V8w&D!%Y6cMq_Fg2pqiP?aUW&nqG3cFiSdhX!2!|!g z%KUqRvNamfM!bF_YP-jy`(1~LG`9l*Oskc-k)n;2;#(8RDfL6O^Sp}*fk)Mi!wUsi z-N>geRA0Rsikv5`*_vtw9h*?$k5AkCjX3$?tK35{ZkzY7m4F@l;{z=1@J*(PYQ;jf4ipO1!wZj(9I1G?(aQ4Ftrv z{f1F+dTXyDlBYQe%98ijjn-rbkjRgC5HEGX*m(%`}3UlGfzO&X^Gw zd!&Q2s%rcw*&Ya76L3$x1V2MC{CxXkyL98BNSwOl0c?DUB)wQrg7M_d{YtK>CFpcp%MDplBDJ zVT#Uk0tV^N&zfS7xTxfg&%{jlZpn~Rprm@vfO7;DzSKDP5Q4%?grH3_@cEqxM$M72zUGeop5i!8nSyFx&fI@Un$eT%2>!V6+Pa2%-LT4ZOf+{pjC{fwEfD)|aC3 zZB;BACOPBgK8>_YuRERDm_kCY;H{KPx#G^@0IQ9DMrXL^w5+)w1a?LxSz{8iKqm@# zR4UfmB&1?=8>oj9W#}IYWVV|~n-Yy*w*TreB@_OQ7fp3y;A|d?dvR{5@NOxC$5+jH zSElsscUMw%&W8$n^dDwv4;VXyn1(}BNKAK~*<*$fBt<4T^>ya2nxd&@ZH!L~_oL87 zxwSTV1ejD6MF${tXzCP^8y(l|36ODvsDR&tO!%L8JB{VC+R>HVKZct6Z2BYfHEEO*& zWqw_n+DPlu2GS*%b#}i-*2AzvzFCV)8R-srl3R1uZ|s~B4md2FY2j}+aH#Dbs0Gp zqo>>h?5w_u-v2Jy#T(n&WVO(|4i~M@)RGurP>ENv?z>r-Jwc#b+nO(kNs72yE0gq> z%VO(cyadxIhmlZs?giLWcpwx)W>5};&r~SN`m14jlcWv|67S$M6r<~18h;VDEhd4b zS3B`eeafpmEa9UaL^}ZpOf4*tTaV|B-nyq?@d8HqSZ?ZJU>TnkKYC%(7Pq#!HJmCjO#ZK{dgv5Fwe zTQ$RcV^^iSLKFh@Wd+2G+nR^vsqWc-c?zB_dN(|!VAHhaBc=k{9}^`FU}*_Y3@U|Q zP^0HgaAxC7?!9Ksv7vUu2Y(<7i&C9gQq=Po>S*?tfm1Vxmav$T)bhtlDPo`##V2aX z6&os`&K<4*GnLlVI00F$`1;`SbXdej3isgQIMD_v@Tj}8+D3dO%{^$J$k>o#Cq4zU zVHW+ib9N6MEiVS8`laK@B;}lud+;v2Pv)LbSPS&<-R|_b`>@{9;B2f*bUQR@6A%7O*J>#@KEm@ZnVg<7%xult1{! z23TH#ULjUZtQ%USFiSpQpTx|8ZzZTL~*9{3z$x1}hPy+8ik72R#h!eUZU zHYUghyHPEsgjsM<=_d{Xui=tbKbNtcWkxN;?HP z?uvQ^wyTcCv2r=@j-%Y~-8W$+*P_AuEvu@Y$-cO|TSu%SAq2=SX*K3);01b!DaBt5 zt<-?o&jM-)e4!Fd*fdoxW|%n1OuSc+WW%AFYBzGuC#HdOxyAe4p{~==FD63z`%pqe zg~xW?h2hcpSz9~cP=f2qTPSO0mV4rsjfmym`D4O*y8brY;!f-<(qI)WYj}Or%8KI( z=M=fpJ44>q(ZXuUTcm^_c-ZYv%fP`IIGWKm?@^a*57Q-w`ovfV&c>I?Kxz_u0v7f; z4kvL%=yF|&f=RtYEh+Uyv_(t{3Lrz$d!`{gKAk@v&GI4k7KacXpU)Hzu>NKGWql!3 zel$~5AgN6Gl2{0iS2KkJBHl_=aLUcPVrqJJqz$3*Zl-9!_vq`RJM~5L&!E5oKyf(x zkJ~v-Tg%6c1Pyr5$<-0rF<1fIvdl?CeG^x~~-uNXY7{(~YA6-XrO9 zl51rL;<2vJ$S>;pShzG6!sGkV&ZDhZ!G`@&d;z6>1G=-r+ijw1pD({%e!bgND^a>y zA7AjLaj0}QZ*N3>A`Tp@m{Io2dSP$oxL@y{*5abC2eG9U)r4blWny>J{Xjv>dmc+;bnZGvgWpbaV_Sd+|(2H z-nR@sthwJ=d|T-*jLUDCH+7zhuf-saQF)wVdn?@HFyXddTX=rWOt<=ay;^_M=rJQc zzikGT__&7U0gCQE7(%}e6PBOe&JEG;z=#cno?PSd7{&J=3?%T(S*;6hv6t+zM&khq z^%@KY2yD`~EbfXubG}b*wDxl_P!Me)XuMc65-+~5N1EvRq<`x@AX+JTefR$N6Bq_T zfo)lNNyy0&e1z*9n#W%<8a{29u#iQ=VkBE?IgV4qqKCre$lngeBM|7MFnIZ`2l1w}c&{3kz`jUg69DZ|W-&+cqWtil zf+m37(QFVHBMCi1L-MxSN2#Oh%kQ3c2l18J#j1MRHu3iLXL?O5jKpzt$>Ut0%D6lo zv#eiX^RT6h9>_*XveMH*VRE*qAW!sk7KcqSaOOpo+#QWaAk<62@D{2dUzPjgdMSz= z55?yFr}g)*s>icof;7<3qmDf5p%)=gRk2ypj>kh4?x(y%xRmEggT)jL8}nj?CLv$`c}aCs&W1vG>&@FG;GL&y-Lex=>d6H712=vcecrKywcOluw->JqdO{( zOl)rjPxd7&1ZqmBEK6CkTk0H_hcLds0&$$BEmS3xM9rsPx1wAp&f74Th@O*YBVmCvoEJc*xb;m})usMjpqnPjjh2uD76eyu~;uuJcsRx+u=Jayu77`STgzX z8N7ZFBO+5kkgLAP&Ke)sQj0sB5eu5n0*5algsq6HAt)5O&Q}8IE+)7T@YE1+)nr>( zz9PbZXeUaIqOI2C4KCY}lj$)|n*#)Eh%2D_pWy6)>C|Lj_?nykcczHpH8 z1O?@5`l11?HJlw8pvG*Z>MZhywN}h`tXl;-qw<7|?QJm3a!La(&h>x%1m3d^f6EBo zVwR14_qjV7Yr(%IJ0D)ibzvM&>=Jt)Im6%1aZH`pxTEnva<-pPW7RZ`yX&PRh6|l1 z_r+1A3PUmH=iWr-I>T?M;d=F<{*Ll8=JxtfFA7`KE31^T#^9bAs|aS<3wx!O)rRV0 zzF++daj$i?Z(p>xV=>uq=n)#Ol+`HxGJqj(d+QnbTp8z^b*ON>yR73_J0O&@`rNuZ z#1!D2>AQVdepB*?U+v4G;R1`N^|rp;tk~bHkF~vVVXx(HKQ#wbzhkdelWrNly-l-@yTRxpIv{}iqnH%MX)mIz6@Z#@cSH@i5bbs24 z3&vLNw&0@Qi~BvFs=bJJ$6~Joo-GM%`sfb(YWbm5SB<`Y@$Sv4I2P|d`@{BuhtJ;q zC-wEL2XS}k;oYm3pQ|7K=;N+@@vZNuKmX%uBW{X%@h$!P9~P@pT-&04|HI8%jI)37 zQup|yeX-uWc=7U6@zedoQG9*ZkqDWMrYnH#u2928v5 zEQ`WHPFg71YWcRMmYC~@CcxYu4GL}-QyZesEr`N?z`}fP%>D78;5wLVqx_w-QJ#m- z{mG!<`kDL1*_dkO%>&$@jtK5|ZwOphDd_wh%yt9mN!QGs?WZ{@o0`B{SlY^YJ-0~q zHYd0``IpOuH#g)Dn$<&2XB!}5y6>GEb6<_f+?Th;+&6=Qt8*lOGWP0hl;@Fyqn=YaN$*V=ex*%yki{fmrEKzpa3oT1jJvjtfy3^4hVAd~0D+|M(DYv-w- z2GO%Cx0X8Q2JROlg8Qvo1NY0E;10Y(dpkGgem^I;+ULp@+GiJHZhKxLB~Zb+4t4N@ zoHnq7lxqh~@5Z*{lEBczDU^%>wy(~ zPjRz;6kk>N$b2az7A$$rzHbvN{4Cx>|cDA88rAPK(oucxZ9itq1%rFCVK8BpS^?CrQ5&#pZ@#LGb0D56&{QG za(gU>u+kp~VAHXc-QG{%aO%SL&;ImJUuT95thD`__5i-^Cw|myD?NV?sbi&o`iuW4 zGiVTI%~{so!BhIgpQd4!UY@j~aF=QS;(z;kM9enD^22%7nFdyRhT#QkgoFbbwy!_o zG=Nopng&{WrOON2fBQfFKe;mw$kK)in{U%FJAZ?w1-uOWa)ZgqRB)oUh zjhO%EKmRY^We%B;(k`oIRV?nxVjFP)*R@KQiAEp{B*GSs5?$Cv2m|o;=%(1M4~tFl z&1thfcj_UcmV{1lzvwBWFW({ysw|j{dv<{J;J*b4Y*`cPPxh zOvCK$*%j^%X8+f}{?l)dhuN;KbPr&m-%rEr{9S<#X8-EnEqg#V?7JU!+4kIHntfkv zX>(CX$(@@SaxWh&s7~IWs%oS5huRmTo$dHH!@Mtk-8H3-dJw~yPwqD29U?gj#9Y&q zj^leL&y%6YqXPMPv%%qng=S0?G>W~Cev;{IKknYUNJTh8DwG32@TuwHjt6E zq!?sO%iBQauGuwUkm1Frkwy2vw*02q8eo8t5I}yT8}g7{1uo!L+nSFP8NTTiV*^xZ zDmzYOxQ17Z3rXafN`w;~ck?U82q@u4hif<}F7#pbVv_oMy->}%W&m-UT8xu5Tth0# z#MhZnYj7VM^r*rrOz`r1`qK~vuU|M-=67%B?TWDZe!?wI$&Vab4&R_w< zOHYEg>3xBiJZ{BhQz^b#`Y3IQ(<9yW!7{m!n zXM-tXtxA~>V`J~i>nl&sa|LP1S8!` zW7ID)=}}B!22`f)5Jq_`j}UyZ+!ybU>+SubxnbLL5CGzEyI%B|6f`#5^cG?CWc1_> zL~MTw4fuLk!`e<*Ki`1EchocL;csBZVR*nfISLYb&IS{v6HM4XOv6iPLn8^vx782V zbLm;BNrHx>twkJSMU>xq%KUnp7kK*MkkI2#{M^l{ZqEUVJ}qQvxed%wa$t$%XiJ4U z2cqd28rVSIA_WZOancm-OLGX^R?gECR{)PGIIso=K$@0R-`8bcAhan)gy1nPuN@u; z^ZppdHN19kz$N4@VZcDMa?L=Be1(iSaHbd7N!F(q;;j2|H@|i>0~Yxq;u_u%IJ!u| z@&~}t4TlAXJ}pCPNZu!F!qS-~4(S{)70^dys8D?VL(Ef$OMbcPZcj(-xt>pkim6;! zRO+nPC2CA3Edi|rxy7CjOkhd6qG91Aq2U^8Y&uY0Cs2H3W})Jn-V#`j@&=2RqwOuh z1%z>zMR-6+DhLm_PD6yqma{~14bB~)GDQ%}2z|6-`(Hh)OhKd{RHpxRLIs(C&I>K1 zNXnoa@^m`~D4lzl1l?`BdiWlj0m&&SxOI0)MNhv@#fdkEelT@=-_0%WEECau<&}7# zv|1*|Dk6iwj^J*t38iJd-qj%ZEEilI6o>=Uy)K&q8g8v9BbJOKc3cJ*kw25K~EEX~eXnGv@u0L*O}}16Jm$x^zs4 zxFg+5gGH@u#`pl&V2{WmkQSYRihsaWVdRb;7EbBG=IZPX=m7N*7AL+b&$X-K6D;;~ z)>UTgl>i}UIYk;9+w!(JWU3&nnE^tkt#C&K;zVSbYFB@|8*)Qrg+ImO37&9;LT5d`M=b7w;C29GQ`BPKCR= z%m}!o#b#0B8r~K@y37&6M>m`$J`kDPT=~pGi1rM%@sRw;ysD$oqRtxZAM$tG7s+Sy zZS}+TYkW zz|nOJ%L5SytsBk|9MG}n8@N(rNCZrof(3_d8H)~dHVLThe{={m2@l&(585N~Nx_CG zS+K}3?erA2Ong{p<+E|24+{X485h7XUtJ>#8Ny*hE*`YZ$UvNhN(D zpN5L&f>9nw4-_B`jFVgq1Am(m&vTYCqOh47b70cuK!!yeGH26Lt5ECw46D-5m zW1Cay{`el(a8P832Bs6!p#Z_s23!m@9TXk5F4s#-*3%PD zjW;BH(d%uNJMQKhm=2Ivf)X&nj}O;yP-HZ^0fW+mj%QiA7{M|JkkJkGB(+p$#Hz6^ ztGUd>b0ULTZ?nKK?Rwy1$|Cl7eN7xW`6wgQ^}h zvyH*p)2wG$aM+f5np&lJN}5pHN0wMz`;Tx zf~5`LbWn8Y!van@vo8iZj8e03dz;B;g8nr*NgQ=^s`4v}SHK@Fuw`4k{8HUqcW*Uk zKKudEbSc6@hG3u@@|ev8ndX3_r)Gh1TwhkoLc_M~gN;6OY3Df9>*%I*oB`?>;jpB=lK^JaG?*5#zy5B2v4K)~As_?2xYu`p@YFQxooAGKEPIM^L0Rm;VDWJnRH47&) zQ{xRuUtDW)IdOM$s`4|eOIv7wHhR$U_~XY1h^8CrNouLC6B%8Ku#!N@96&}loE zrbKcRy^gl*Nlx3{oVT}rMim*c^^kc(Hz?H)4%bl6QOk5KP8g)s6$Xnl9hnL5buW!k zKemtuYtz#Y))VO&n?P2XqMTWYkW>rhG|FV zdqubazw*^1SQ<>7F_6)?OAUbVxYXu8%bJnHZ_DMrcz^tPz43qfiUqT%=Y28LkOwR)DnNpKYjQ@^DPsbW zJMU_IX1I``q7ymRzqp%Ip7Zg0X81VP&lWhm@G@9Ao4JuYnO zZt+DjgKfzpc1@@_YY_w$({eym=%Y*q!{)tb1YF(fRUu3oGo@aRCfn)fZlVVoAC? zwWKIy{8dcDG(DZnjDZ}+U4xM*@J;nZwPa`ThCa-!16X)7qeH^5EBEaPFX3|c)l&B4 zkbFLihkmr4&26F(>Yj0PV_?C4=0}ubfAYnuKL$JO~mx#}j&5 z8XbvUPZ%opSZRUnL_{xW4P^-nAPEKEl!_G02#t8t_tKE=k5t2$DLQ7E$dU(eG%W|^ zj*f%@lhT7`w=Ib9e*erh)YH^*oncq>VF8e`V*}1)v`WUDEDIkMqh z;0~dknXX# zFSo~H@l;hC79&U7ApxS#^4r^}X67zt&}r8k@s&7ev1guU2Dz!Tmf=H(?SF;fm;wcB zS^$G-$>Vl~;Fwj71rCpZC%e5Ca3HwnEoH=b9jb*(J7t))tCB${ZCoWj4S3l zYMu1(I7z&L;oXG{JsUlL6VCYu(0~r@AgQw1Lq`fBl^Y-uqjiX2?aSSqigsOQ<|t5D zDDfyr-{g2gUoOdq^1P$d=o#Mo8k@VEL8c8&>9ntlLx85UpuzYcwwuPPFMsqvAr}NA zy_H8Gzi6)CT5s*;Uk#8s!=e z3(oYILy{W*s@>iE+JORcxFF&oAl%a3`$n(nu?WD)SK5ek&N={wLYCH659bU^?Z%~C zzPJGz&`yqzTU<+xMF+jSzyO9Le8hAnpn!5*1r4pDI!Y7gqI+qTQZEC8%mfxNeRnB? z0fz5)CNz2+<6$^NVU*;h1K38$zmSORPXOJ`^?bE_d>Y8fI&DwmC_b>OD@WVw1qVz7 zsu318y!a#!fd^cJ*MUW6&XUFa3<`iTB?yZ$k{oPHo=yE2L!agM%6W4d5Z76z=(tyc zlIf-;>jwwPNBGde2&cbq zO5t&vBfnH|x2Kco8Lg6`f<;)a0)<;_8zvqG$yoPOdKg+VOyFE}@qu;GTtg}rAKKj_ z+CVYB$lX~FKL$_`!QDj+sZqYEo~@SgT(Cm?p(BoEd_-riw6TMQLmu5%%X5y$(j%Ic z2AQpn$1q}Y6)4#h9mNaF8ugU4AT^C7bdD#S1D8W56d^sGu{Z`+29=8hwD<>HCs+{p za~4C~mNXwn4^ZVSkHkYYT@wVTOy$F}KskH1CGX0v(*PlT((#N%ifcGLP#td_O|X&k zth_~)YcN0{i!Km`Sfv{d&#Gu5b?7$+s{kmSx>!KzIhlNl>!neCl*jr%Ycn; zsIlo_ISZhmTrflthDJSIZixj60M?X^$fCGzddME8mH-Si%~f*Yq_aUG{M7Ji95g?B zE>=L&5L;lz`3fvBh!)m{r6Y3$MtQjci!XOMh3d=rX8r-x*Tg_W$1%kLu76%=NMhno z7~RdOSbW8a6VYZ*1YtS^%L|dC(7^^ON89TK2Q0%~7?Jwqn_e$8uncvUK_oJ4%bNfu zXVGJRo(6c3v&1n!U|`1n_{255UPA(p>MmdK;uDt)59pWIOe<&u5p)g0ir7A2xbFR6 z=_D!<80F1nNdBfXcncgkQ+IP7ynYZxpwdAxFGQevU29;$;Tp~tmPXbyPlHHBViY^4 z5TVhxmj@JNb_J#TpU0OFFt9uyp%9^kvOyt6q+9ZE53V(2!8$PxRc)H7Xq(uK^-Qae)m^j<(ke4w%+xg@~D$76jyx3K8=IW~4&I z{D2v$5a9%ddaO$(dyv=o;}h4AiV`f5J7Jktx(Ch|>IiIL;+tMCG(ZGbA%f^qH@se0 zNdD$3MDP$u$lMRPhI9{@;{vCX;u0F^UK+!Gu;@|zo)<=mKfnA+&iV`Ua>`v z`Rvo`bgXa8YCC`%Syt7DawWdzTE5q-zZdNc-Y$#B-TU>v)HiOm{l8}hTt2LqAIfcb zOgLoSj@fpNAND&iY_$px38|YJvRcIt2e5u9H@l?qR{OO;weA)_-j_za$J6mxMS%L} zO_x?e*)x7nD8THh{fFg46(K0Nj&@kpH-2D{c2^yXV-?01mSuNE&05*9*O>d_?rwby zk4X`hJ$ns$C^nD%)GB+%4+_>+Y?o{CU5v%5yxXstFE-oG0hGHfj}N7|bFW++#BF?( z%HI(mcRm~!?@#+}u_Rss9<+QY*4xKoy;5XopMrJh z2`nGV!*RDMj%8T+gUsT`+$KV1!Ts-xr^T|`?aF-^a_};;%R<0ZrE(7jZ=X_p9U*bY-PH5#$9yV7q}>fP{$Bvb zHLx#I%x<7b(8t7D9qrl6|4Ty!p@NP>}3fQ$iuEI#?wDD{!_J&SV9aW zKSbbFS?&&X`XK_+IB^itARN4jE+d)&I0G|{)w&QF@hUDIfMsXfFEJxvr%*nY`(l$m zg|=Vf6j&zQSFz)RBWpVK@ki_wnk+kt4KRhKQ=cjPvIz`^3-T+hQF8!$#&5SK>f%?p zS%_l@2eI}p<0uJft?HOK30Px{YYZ^(xF&#@iM+h#aX1hG>v|I(1|HYqtjU+#MO5?$ z4K?#UH}uvI5o1R#1qK-%76R?)DGPx?rT`(lokHv~V36Ud?xPj~gNzMJfL8SQCBPsP zfPmXhAawyS$TT1WZ>JHRfe$i1rC@CSJ;?A7EnD#;v+rD^$K~FGj1I|8yZCyj_J?xh z^(V}t)j-lL20;SqkWJp!cLx3Iu@Hajf?WNeq`==h-GqUgXQW2o{vw0-Zyu5!d;5H7rw{@B~t^$7F*2e;u%5Ap+!ogCu(j}1pXMlT_NkCmIH&2l!bb^!XS olY(=%nf~Nd?2q5X4Go*`vRh)&0<27xJ>muizo0sXhJNw?1OE)~zW@LL diff --git a/server/item/recipe/potion_data.nbt b/server/item/recipe/potion_data.nbt index 3b9f707eb78842e1a17b50eaf73068b322a3e02c..fdd42726f771f1bd888d28bf317d18a7db9fd34b 100644 GIT binary patch delta 257 zcmZ3~%CxAJX~QR`$*!y#%sH94lM7h&C;KoLO*UY%nQWk?2xLo6R?rilEFdg7SwMwn zaskWL$p?6&CeLRr0@4sQd2DGw{W6o+vCB=qpd~)}6}#jlZMn$-;*yhXIl?C^2+3`J z#onwtxzfsi@&a=|Fbxv4w`ybCoNIc5c`~PyKUgPks5zJol0EO(#yhzp*cmKwGZu?_o?x->0bpxDT%h{NK6j delta 254 zcmZ3~%CxAJX~QR`$p%a|lNE&ICi^fKP5#ZKJozpc=j4rihCsgLWCcC($pXTXlLb_G zCKs?=oqUB&W-=c$h|My&kaYo2y*!v5%?i*NU$IM0(w3VX zATBxCmO~b(-3nyY=2z^^x|{#noM7IZYdV2_GN+UOue}lA8~NOi`FzP%1N7u}lW+ Wi1sqcNfp+U@0BHiEpIQA<^cc?xLJDu diff --git a/server/item/register.go b/server/item/register.go index 786af85753..07b7b075d4 100644 --- a/server/item/register.go +++ b/server/item/register.go @@ -52,6 +52,8 @@ func init() { world.RegisterItem(Emerald{}) world.RegisterItem(EnchantedApple{}) world.RegisterItem(EnchantedBook{}) + world.RegisterItem(EndCrystal{}) + world.RegisterItem(EnderEye{}) world.RegisterItem(EnderPearl{}) world.RegisterItem(Feather{}) world.RegisterItem(FermentedSpiderEye{}) diff --git a/server/listener.go b/server/listener.go index 8967ab7c7c..5286f3aa27 100644 --- a/server/listener.go +++ b/server/listener.go @@ -31,6 +31,7 @@ func (uc UserConfig) listenerFunc(conf Config) (Listener, error) { ResourcePacks: conf.Resources, TexturePacksRequired: conf.ResourcesRequired, Compression: conf.Compression, + Allow: conf.Allower.Allow, } if conf.Log.Enabled(context.Background(), slog.LevelDebug) { cfg.ErrorLog = conf.Log.With("net origin", "gophertunnel") diff --git a/server/player/conf.go b/server/player/conf.go index 194156c0d9..8f2ee9af27 100644 --- a/server/player/conf.go +++ b/server/player/conf.go @@ -1,6 +1,10 @@ package player import ( + "math/rand/v2" + "time" + + "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/entity" "github.com/df-mc/dragonfly/server/entity/effect" @@ -11,8 +15,6 @@ import ( "github.com/go-gl/mathgl/mgl64" "github.com/google/uuid" "golang.org/x/text/language" - "math/rand/v2" - "time" ) // Config holds options that a Player can be created with. @@ -82,9 +84,33 @@ func (cfg Config) Apply(data *world.EntityData) { maxAirSupplyTicks: conf.MaxAirSupply, breathing: true, nameTag: conf.Name, + alwaysShowNameTag: true, fireTicks: conf.FireTicks, fallDistance: conf.FallDistance, } + playerUUID := conf.UUID + pdata.portalTravel = &entity.PortalTravelComputer{ + Instantaneous: func(_, target world.Dimension) bool { + // End travel is always instant regardless of game mode; End portals target the End in either direction. + return pdata.gameMode.InstantPortalTravel() || target == world.End + }, + Teleport: func(e entity.Traveller, pos mgl64.Vec3) { + e.(*Player).forceTeleport(pos) + }, + SpawnPoint: func(tx *world.Tx) mgl64.Vec3 { + // Use the player's spawn only while its bed still exists and is unobstructed, like respawning. + pos := tx.World().PlayerSpawn(playerUUID) + if b, ok := tx.Block(pos).(block.Bed); ok && b.CanRespawnOn() { + if safe, ok := b.SafeSpawn(pos, tx); ok { + return safe.Vec3Middle() + } + } + return tx.World().Spawn().Vec3Middle() + }, + Player: true, + // Only players create a portal at the destination when no linked portal exists. + CreatePortal: true, + } pdata.hunger.foodLevel, pdata.hunger.foodTick, pdata.hunger.exhaustionLevel, pdata.hunger.saturationLevel = conf.Food, conf.FoodTick, conf.Exhaustion, conf.Saturation pdata.experience.Add(conf.Experience) data.Data = pdata diff --git a/server/player/context.go b/server/player/context.go new file mode 100644 index 0000000000..5a388aaad4 --- /dev/null +++ b/server/player/context.go @@ -0,0 +1,52 @@ +package player + +import ( + "github.com/df-mc/dragonfly/server/world" +) + +// Context is the context passed to player event callbacks. It embeds the +// world Context, so world operations and Cancel are available directly, and +// adds the Player the event concerns. It is valid only during the callback. +type Context struct { + *world.Context + p *Player +} + +// NewEventContext returns a fresh event context for p. +// tx and p must come from the same active owner callback. +func NewEventContext(tx *world.Tx, p *Player) *Context { + if tx == nil || p == nil || p.tx != tx { + panic("player: transaction and player do not belong to the same callback") + } + _ = tx.World() // Fail immediately if tx has already finished. + return &Context{Context: tx.Event(), p: p} +} + +// Player returns the player the event concerns, valid only during the +// callback. +func (ctx *Context) Player() *Player { return ctx.p } + +// Defer schedules f to run on the owner after the current callback completes, +// with the player re-resolved for that moment. The task fails with +// world.ErrEntityClosed if the player's handle closed, or with +// world.ErrEntityNotInWorld if the player left this transaction's world. +func (ctx *Context) Defer(f func(ctx *Context)) *world.Task { + return ctx.DeferErr(func(ctx *Context) error { + f(ctx) + return nil + }) +} + +// DeferErr schedules f like Defer and records its returned error on the Task. +func (ctx *Context) DeferErr(f func(ctx *Context) error) *world.Task { + h := ctx.p.H() + return ctx.Context.DeferErr(func(tx *world.Tx) error { + if e, ok := h.Entity(tx); ok { + return f(NewEventContext(tx, e.(*Player))) + } + if h.Closed() { + return world.ErrEntityClosed + } + return world.ErrEntityNotInWorld + }) +} diff --git a/server/player/handler.go b/server/player/handler.go index ada7a65461..17ecb573bf 100644 --- a/server/player/handler.go +++ b/server/player/handler.go @@ -6,7 +6,6 @@ import ( "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/cmd" - "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/player/skin" "github.com/df-mc/dragonfly/server/session" @@ -14,8 +13,6 @@ import ( "github.com/go-gl/mathgl/mgl64" ) -type Context = event.Context[*Player] - // Handler handles events that are called by a player. Implementations of Handler may be used to listen to // specific events such as when a player chats or moves. type Handler interface { @@ -53,6 +50,10 @@ type Handler interface { // the original cause of the immunity frame. In this case, the damage is // reduced but the player is still knocked back. HandleHurt(ctx *Context, damage *float64, immune bool, attackImmunity *time.Duration, src world.DamageSource) + // HandleSetOnFire handles the player being set on fire by any source. + // The fire duration passed is after fire protection modifiers and may be changed + // by assigning to *duration. + HandleSetOnFire(ctx *Context, duration *time.Duration) // HandleDeath handles the player dying to a particular damage cause. HandleDeath(p *Player, src world.DamageSource, keepInv *bool) // HandleRespawn handles the respawning of the player in the world. The spawn position passed may be @@ -194,6 +195,7 @@ func (NopHandler) HandleAttackEntity(*Context, world.Entity, *float64, *float64, func (NopHandler) HandleExperienceGain(*Context, *int) {} func (NopHandler) HandlePunchAir(*Context) {} func (NopHandler) HandleHurt(*Context, *float64, bool, *time.Duration, world.DamageSource) {} +func (NopHandler) HandleSetOnFire(*Context, *time.Duration) {} func (NopHandler) HandleHeal(*Context, *float64, world.HealingSource) {} func (NopHandler) HandleFoodLoss(*Context, int, *int) {} func (NopHandler) HandleDeath(*Player, world.DamageSource, *bool) {} diff --git a/server/player/input/lock.go b/server/player/input/lock.go new file mode 100644 index 0000000000..c877f115f6 --- /dev/null +++ b/server/player/input/lock.go @@ -0,0 +1,79 @@ +package input + +import "github.com/sandertv/gophertunnel/minecraft/protocol/packet" + +// Lock represents a client input lock that can be applied to a player to disable specific inputs such as +// camera rotation, movement, jumping, sneaking or mounting/dismounting entities. +type Lock struct { + lock +} + +type lock uint32 + +// Camera is the lock that disables all camera movement. +func Camera() Lock { + return Lock{lock(packet.ClientInputLockCamera)} +} + +// Movement is the lock that disables all player movement, including jumping and sneaking. +func Movement() Lock { + return Lock{lock(packet.ClientInputLockMovement)} +} + +// LateralMovement is the lock that disables all player movement excluding jumping and sneaking. +func LateralMovement() Lock { + return Lock{lock(packet.ClientInputLockLateralMovement)} +} + +// Sneak is the lock that disables the player from sneaking. +func Sneak() Lock { + return Lock{lock(packet.ClientInputLockSneak)} +} + +// Jump is the lock that disables the player from jumping. +func Jump() Lock { + return Lock{lock(packet.ClientInputLockJump)} +} + +// Mount is the lock that prevents the player from mounting entities. +func Mount() Lock { + return Lock{lock(packet.ClientInputLockMount)} +} + +// Dismount is the lock that prevents the player from dismounting entities. +func Dismount() Lock { + return Lock{lock(packet.ClientInputLockDismount)} +} + +// MoveForward is the lock that disables forward movement. +func MoveForward() Lock { + return Lock{lock(packet.ClientInputLockMoveForward)} +} + +// MoveBackward is the lock that disables backward movement. +func MoveBackward() Lock { + return Lock{lock(packet.ClientInputLockMoveBackward)} +} + +// MoveLeft is the lock that disables left strafe movement. +func MoveLeft() Lock { + return Lock{lock(packet.ClientInputLockMoveLeft)} +} + +// MoveRight is the lock that disables right strafe movement. +func MoveRight() Lock { + return Lock{lock(packet.ClientInputLockMoveRight)} +} + +// Uint32 returns the lock as a uint32. +func (l lock) Uint32() uint32 { + return uint32(l) +} + +// All returns all the input locks that are available to be applied to a player. +func All() []Lock { + return []Lock{ + Camera(), Movement(), LateralMovement(), Sneak(), Jump(), Mount(), Dismount(), + MoveForward(), MoveBackward(), MoveLeft(), MoveRight(), + } +} diff --git a/server/player/input/restricter.go b/server/player/input/restricter.go new file mode 100644 index 0000000000..be5e9b8477 --- /dev/null +++ b/server/player/input/restricter.go @@ -0,0 +1,13 @@ +package input + +// Restricter represents an interface that can manage input locks for a player. +type Restricter interface { + // LockInput applies an input lock to the player, disabling the specified input. + LockInput(l Lock) + // UnlockInput removes an input lock from the player, re-enabling the specified input. + UnlockInput(l Lock) + // ClearInputLocks removes all input locks from the player, re-enabling all inputs. + ClearInputLocks() + // InputLocked checks if a specific input lock is currently applied to the player. + InputLocked(l Lock) bool +} diff --git a/server/player/player.go b/server/player/player.go index 375d89b58a..4637be2e5d 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -1,6 +1,7 @@ package player import ( + "errors" "fmt" "math" "math/rand/v2" @@ -10,9 +11,6 @@ import ( "sync" "time" - "github.com/df-mc/dragonfly/server/player/debug" - "github.com/df-mc/dragonfly/server/player/hud" - "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/block/model" @@ -25,8 +23,11 @@ import ( "github.com/df-mc/dragonfly/server/item/inventory" "github.com/df-mc/dragonfly/server/player/bossbar" "github.com/df-mc/dragonfly/server/player/chat" + "github.com/df-mc/dragonfly/server/player/debug" "github.com/df-mc/dragonfly/server/player/dialogue" "github.com/df-mc/dragonfly/server/player/form" + "github.com/df-mc/dragonfly/server/player/hud" + "github.com/df-mc/dragonfly/server/player/input" "github.com/df-mc/dragonfly/server/player/scoreboard" "github.com/df-mc/dragonfly/server/player/skin" "github.com/df-mc/dragonfly/server/player/title" @@ -43,6 +44,7 @@ type playerData struct { xuid string locale language.Tag nameTag, scoreTag string + alwaysShowNameTag bool absorptionHealth float64 scale float64 @@ -91,7 +93,8 @@ type playerData struct { enchantSeed int64 - mc *entity.MovementComputer + mc *entity.MovementComputer + portalTravel *entity.PortalTravelComputer collidedVertically, collidedHorizontally bool @@ -196,7 +199,7 @@ func (p *Player) Skin() skin.Skin { // SetSkin changes the skin of the player. This skin will be visible to other players that the player // is shown to. func (p *Player) SetSkin(skin skin.Skin) { - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleSkinChange(ctx, &skin); ctx.Cancelled() { p.session().ViewSkin(p) return @@ -323,7 +326,7 @@ func (p *Player) RemoveBossBar() { // player and is formatted following the rules of fmt.Sprintln. func (p *Player) Chat(msg ...any) { message := format(msg) - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleChat(ctx, &message); ctx.Cancelled() { return } @@ -351,7 +354,7 @@ func (p *Player) ExecuteCommand(commandLine string) { p.SendCommandOutput(o) return } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleCommandExecution(ctx, command, args[1:]); ctx.Cancelled() { return } @@ -366,7 +369,7 @@ func (p *Player) Transfer(address string) error { return err } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleTransfer(ctx, addr); ctx.Cancelled() { return nil } @@ -441,6 +444,19 @@ func (p *Player) NameTag() string { return p.nameTag } +// SetAlwaysShowNameTag changes whether the name tag of the player is shown at all distances instead of only +// when the player is looked at from up close. By default, the name tag is always shown. +func (p *Player) SetAlwaysShowNameTag(alwaysShow bool) { + p.alwaysShowNameTag = alwaysShow + p.updateState() +} + +// AlwaysShowNameTag returns whether the name tag of the player is shown at all distances. It can be changed +// using SetAlwaysShowNameTag. +func (p *Player) AlwaysShowNameTag() bool { + return p.alwaysShowNameTag +} + // SetScoreTag changes the score tag displayed over the player in-game. The score tag is displayed under the player's // name tag. func (p *Player) SetScoreTag(a ...any) { @@ -524,26 +540,30 @@ func (p *Player) addHealth(health float64) { // the entity healed by having a full food bar. If the health added to the // original health exceeds the entity's max health, Heal will not add the full // amount. If the health passed is negative, Heal will not do anything. -func (p *Player) Heal(health float64, source world.HealingSource) { +// Heal returns the amount of health regenerated. +func (p *Player) Heal(health float64, source world.HealingSource) float64 { if p.Dead() || health < 0 || !p.GameMode().AllowsTakingDamage() { - return + return 0 } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleHeal(ctx, &health, source); ctx.Cancelled() { - return + return 0 } + oldHealth := p.Health() p.addHealth(health) + return p.Health() - oldHealth } // updateFallState is called to update the entities falling state. func (p *Player) updateFallState(distanceThisTick float64) { switch { case p.OnGround(): - if p.fallDistance > 0 { + p.fallDistance -= distanceThisTick + if p.fallDistance > 3 { p.fall(p.fallDistance) - p.ResetFallDistance() } - case distanceThisTick < p.fallDistance: + p.ResetFallDistance() + case distanceThisTick < 0 && distanceThisTick < p.fallDistance: p.fallDistance -= distanceThisTick default: p.ResetFallDistance() @@ -552,15 +572,8 @@ func (p *Player) updateFallState(distanceThisTick float64) { // fall is called when a falling entity hits the ground. func (p *Player) fall(distance float64) { - pos := cube.PosFromVec3(p.Position()) - b := p.tx.Block(pos) - - if len(b.Model().BBox(pos, p.tx)) == 0 { - pos = pos.Sub(cube.Pos{0, 1}) - b = p.tx.Block(pos) - } - if h, ok := b.(block.EntityLander); ok { - h.EntityLand(pos, p.tx, p, &distance) + if pos, lander, ok := p.landedOn(); ok { + lander.EntityLand(pos, p.tx, p, &distance) } dmg := distance - 3 if boost, ok := p.Effect(effect.JumpBoost); ok { @@ -572,6 +585,35 @@ func (p *Player) fall(distance float64) { p.Hurt(math.Ceil(dmg), entity.FallDamageSource{}) } +// landedOn returns the first block.EntityLander the Player came to rest on, along with its position. +func (p *Player) landedOn() (cube.Pos, block.EntityLander, bool) { + low, high := p.blocksUnder() + for x := low[0]; x <= high[0]; x++ { + for z := low[2]; z <= high[2]; z++ { + pos := cube.Pos{x, low[1], z} + if lander, ok := p.tx.Block(pos).(block.EntityLander); ok { + return pos, lander, true + } + } + } + return cube.Pos{}, nil, false +} + +// blocksUnder returns the corners of the range of block positions directly below the Player. Every block in that range +// is one the Player stands on: the Player is narrower than a block, so it may rest on the edge of one with its centre +// over the block beside it, and looking only below its centre would miss the block it is actually standing on. +func (p *Player) blocksUnder() (low, high cube.Pos) { + box := Type.BBox(p).Translate(p.Position()) + // The Y is taken from the box itself, while the horizontal range is taken from a slightly smaller box so that a + // Player resting exactly on the boundary between two blocks does not reach into the column beside the one it + // stands on. + y := int(math.Floor(box.Min()[1] - 0.0001)) + horizontal := box.Grow(-0.0001) + low, high = cube.PosFromVec3(horizontal.Min()), cube.PosFromVec3(horizontal.Max()) + low[1], high[1] = y, y + return low, high +} + // Hurt hurts the player for a given amount of damage. The source passed // represents the cause of the damage, for example entity.AttackDamageSource if // the player is attacked by another entity. If the final damage exceeds the @@ -595,7 +637,7 @@ func (p *Player) Hurt(dmg float64, src world.DamageSource) (float64, bool) { } immunity := time.Second / 2 - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleHurt(ctx, &damageLeft, immune, &immunity, src); ctx.Cancelled() { return 0, false } @@ -694,9 +736,10 @@ func (p *Player) FinalDamageFrom(dmg float64, src world.DamageSource) float64 { } // Explode ... -func (p *Player) Explode(explosionPos mgl64.Vec3, impact float64, c block.ExplosionConfig) { +func (p *Player) Explode(src world.ExplosionSource, impact float64) { + explosionPos := src.Position() diff := p.Position().Sub(explosionPos) - p.Hurt(math.Floor((impact*impact+impact)*3.5*c.Size*2+1), entity.ExplosionDamageSource{}) + p.Hurt(math.Floor((impact*impact+impact)*3.5*src.Size()*2+1), entity.ExplosionDamageSource{Source: src}) p.knockBack(explosionPos, impact, diff[1]/diff.Len()*impact) } @@ -820,7 +863,7 @@ func (p *Player) Exhaust(points float64) { // Temporarily set the food level back so that it hasn't yet changed once the event is handled. p.hunger.SetFood(before) - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleFoodLoss(ctx, before, &after); ctx.Cancelled() { // Reset the exhaustion level if the event was cancelled. Because if // we cancel this, and at some point we stop cancelling it, the @@ -877,14 +920,13 @@ func (p *Player) kill(src world.DamageSource) { // Wait a little before removing the entity. The client displays a death // animation while the player is dying. - time.AfterFunc(time.Millisecond*1100, func() { - p.H().ExecWorld(finishDying) + DoAfter(p.handle, time.Millisecond*1100, func(_ *world.Tx, p *Player) { + finishDying(p) }) } // finishDying completes the death of a player, removing it from the world. -func finishDying(_ *world.Tx, e world.Entity) { - p := e.(*Player) +func finishDying(p *Player) { if p.session() == session.Nop { _ = p.Close() return @@ -928,7 +970,7 @@ func (p *Player) MoveItemsToInventory() { if n, err := p.inv.AddItem(i); err != nil { // We couldn't add the item to the main inventory (probably because // it was full), so we drop it instead. - p.Drop(i.Grow(i.Count() - n)) + p.Drop(i.Grow(-n)) } } } @@ -944,6 +986,10 @@ func (p *Player) Respawn() *world.EntityHandle { return p.handle } +// respawn heals the player and moves it to its spawn position. f, if +// non-nil, runs with the player once it is back in a world — normally the +// respawn destination, otherwise the world it died in — so a quit callback +// from close always completes the player's teardown. func (p *Player) respawn(f func(p *Player)) { if !p.Dead() || p.session() == session.Nop { return @@ -964,8 +1010,20 @@ func (p *Player) respawn(f func(p *Player)) { p.Handler().HandleRespawn(p, &pos, &w) + sess := p.session() + src := p.tx.World() handle := p.tx.RemoveEntity(p) - w.Exec(func(tx *world.Tx) { + // restore re-adds the player through tx and finishes with f or the normal + // quit path; the fallback branches below share it. + restore := func(tx *world.Tx) { + np := tx.AddEntity(handle).(*Player) + if f != nil { + f(np) + return + } + np.quit("respawn failed") + } + task := w.Do(func(tx *world.Tx) { np := tx.AddEntity(handle).(*Player) np.Teleport(pos) np.session().SendRespawn(pos, p) @@ -974,6 +1032,32 @@ func (p *Player) respawn(f func(p *Player)) { f(np) } }) + if errors.Is(task.Err(), world.ErrWorldClosed) { + // The destination refused synchronously: re-add through the still-open + // source context. This also keeps synchronous worlds fully inline. + restore(p.tx) + return + } + task.OnDone(func(err error) { + // Only ErrWorldClosed means the entity was never re-added. A callback + // panic is left alone: the entity is live. + if !errors.Is(err, world.ErrWorldClosed) { + return + } + // Fall back to the source world so the normal quit path still runs. + src.Do(restore).OnDone(func(err error) { + if err == nil || errors.Is(err, world.ErrTaskPanicked) { + return + } + // The source world is gone too; the handle is orphaned. Close the + // session without a world so the stop handler still runs, then + // free the connection. + _ = handle.Close() + sess.Disconnect("respawn failed") + sess.Close(nil, p) + sess.CloseConnection() + }) + }) } // spawnLocation designates a players safe spawn location. @@ -1003,7 +1087,7 @@ func (p *Player) StartSprinting() { if !p.hunger.canSprint() && p.GameMode().AllowsTakingDamage() || p.crawling || p.sprinting { return } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleToggleSprint(ctx, true); ctx.Cancelled() { return } @@ -1023,7 +1107,7 @@ func (p *Player) StopSprinting() { if !p.sprinting { return } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleToggleSprint(ctx, false); ctx.Cancelled() { return } @@ -1039,7 +1123,7 @@ func (p *Player) StartSneaking() { if p.sneaking { return } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleToggleSneak(ctx, true); ctx.Cancelled() { return } @@ -1061,7 +1145,7 @@ func (p *Player) StopSneaking() { if !p.sneaking { return } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleToggleSneak(ctx, false); ctx.Cancelled() { return } @@ -1221,7 +1305,7 @@ func (p *Player) Sleep(pos cube.Pos) { return } - ctx, sendReminder := event.C(p), true + ctx, sendReminder := NewEventContext(p.tx, p), true if p.Handler().HandleSleep(ctx, &sendReminder); ctx.Cancelled() { return } @@ -1348,6 +1432,14 @@ func (p *Player) SetOnFire(duration time.Duration) { if level := p.Armour().HighestEnchantmentLevel(enchantment.FireProtection); level > 0 { ticks -= int64(math.Floor(float64(ticks) * float64(level) * 0.15)) } + + duration = time.Duration(ticks) * time.Second / 20 + ctx := NewEventContext(p.tx, p) + if p.Handler().HandleSetOnFire(ctx, &duration); ctx.Cancelled() { + return + } + + ticks = int64(duration.Seconds() * 20) p.fireTicks = ticks p.updateState() } @@ -1400,7 +1492,7 @@ func (p *Player) SetHeldSlot(to int) error { return nil } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) p.Handler().HandleHeldSlotChange(ctx, from, to) if ctx.Cancelled() { // The slot change was cancelled, resend held slot. @@ -1487,7 +1579,7 @@ func (p *Player) SetCooldown(item world.Item, cooldown time.Duration) { // This generally happens for items such as throwable items like snowballs. func (p *Player) UseItem() { i, _ := p.HeldItems() - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.HasCooldown(i.Item()) { return } @@ -1565,7 +1657,7 @@ func (p *Player) UseItem() { } // Reset the duration for the next item to be consumed. p.usingSince = time.Now() - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleItemConsume(ctx, i); ctx.Cancelled() { return } @@ -1589,7 +1681,7 @@ func (p *Player) ReleaseItem() { useCtx, dur := p.useContext(), p.useDuration() i, _ := p.HeldItems() - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleItemRelease(ctx, i, dur); ctx.Cancelled() { return } @@ -1672,7 +1764,7 @@ func (p *Player) UseItemOnBlock(pos cube.Pos, face cube.Face, clickPos mgl64.Vec p.resendNearbyBlocks(pos, face) return } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleItemUseOnBlock(ctx, pos, face, clickPos); ctx.Cancelled() { p.resendNearbyBlocks(pos, face) return @@ -1683,11 +1775,10 @@ func (p *Player) UseItemOnBlock(pos cube.Pos, face cube.Face, clickPos mgl64.Vec // If a player is sneaking, it will not activate the block clicked, unless it is not holding any // items, in which case the block will be activated as usual. if !p.Sneaking() || i.Empty() { - p.SwingArm() - // The block was activated: Blocks such as doors must always have precedence over the item being // used. if useCtx := p.useContext(); act.Activate(pos, face, p.tx, p, useCtx) { + p.SwingArm() p.SetHeldItems(p.subtractItem(p.damageItem(i, useCtx.Damage), useCtx.CountSub), left) p.addNewItem(useCtx) return @@ -1731,7 +1822,7 @@ func (p *Player) UseItemOnEntity(e world.Entity) bool { if !p.canReach(e.Position()) { return false } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleItemUseOnEntity(ctx, e); ctx.Cancelled() { return false } @@ -1779,14 +1870,26 @@ func (p *Player) AttackEntity(e world.Entity) bool { height += inc } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleAttackEntity(ctx, e, &force, &height, &critical); ctx.Cancelled() { return false } p.SwingArm() if !isLiving { - return false + if !entity.DamageableEntity(e) { + return false + } + i, left := p.HeldItems() + if durable, ok := i.Item().(item.Durable); ok { + p.SetHeldItems(p.damageItem(i, durable.DurabilityInfo().AttackDurability), left) + } + n, vulnerable, _ := entity.HurtEntity(e, i.AttackDamage(), entity.AttackDamageSource{Attacker: p}) + p.tx.PlaySound(entity.EyePosition(e), sound.Attack{Damage: !mgl64.FloatEqual(n, 0)}) + if vulnerable { + p.Exhaust(0.1) + } + return true } dmg := i.AttackDamage() @@ -1847,7 +1950,7 @@ func (p *Player) StartBreaking(pos cube.Pos, face cube.Face) { return } if _, ok := p.tx.Block(pos.Side(face)).(block.Fire); ok { - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleFireExtinguish(ctx, pos); ctx.Cancelled() { // Resend the block because on client side that was extinguished p.resendNearbyBlocks(pos, face) @@ -1868,7 +1971,7 @@ func (p *Player) StartBreaking(pos cube.Pos, face cube.Face) { // can resend the block to the client when it tries to break the block regardless. p.breakingPos = pos - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleStartBreak(ctx, pos); ctx.Cancelled() { return } @@ -1892,25 +1995,29 @@ func (p *Player) StartBreaking(pos cube.Pos, face cube.Face) { // held, if the player is on the ground/underwater and if the player has any effects. func (p *Player) breakTime(pos cube.Pos) time.Duration { held, _ := p.HeldItems() - breakTime := block.BreakDuration(p.tx.Block(pos), held) - if !p.OnGround() { - breakTime *= 5 + return block.BreakDuration(p.tx.Block(pos), held, p.breakContext()) +} + +// breakContext returns the block.BreakContext describing the status effects and environment currently +// affecting how quickly the player breaks blocks. +func (p *Player) breakContext() block.BreakContext { + _, aquaAffinity := p.Armour().Helmet().Enchantment(enchantment.AquaAffinity) + ctx := block.BreakContext{ + Underwater: p.insideOfWater(), + AquaAffinity: aquaAffinity, + Airborne: !p.OnGround(), + Flying: p.Flying(), } - if _, ok := p.Armour().Helmet().Enchantment(enchantment.AquaAffinity); p.insideOfWater() && !ok { - breakTime *= 5 + if e, ok := p.Effect(effect.Haste); ok { + ctx.HasteLevel = e.Level() } - for _, e := range p.Effects() { - lvl := e.Level() - switch e.Type() { - case effect.Haste: - breakTime = time.Duration(float64(breakTime) * effect.Haste.Multiplier(lvl)) - case effect.MiningFatigue: - breakTime = time.Duration(float64(breakTime) * effect.MiningFatigue.Multiplier(lvl)) - case effect.ConduitPower: - breakTime = time.Duration(float64(breakTime) * effect.ConduitPower.Multiplier(lvl)) - } + if e, ok := p.Effect(effect.ConduitPower); ok { + ctx.ConduitPowerLevel = e.Level() + } + if e, ok := p.Effect(effect.MiningFatigue); ok { + ctx.MiningFatigueLevel = e.Level() } - return breakTime + return ctx } // FinishBreaking makes the player finish breaking the block it is currently breaking, or returns immediately @@ -1995,7 +2102,7 @@ func (p *Player) placeBlock(pos cube.Pos, b world.Block, ignoreBBox bool) bool { return false } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleBlockPlace(ctx, pos, b); ctx.Cancelled() { p.resendNearbyBlocks(pos, cube.Faces()...) return false @@ -2062,7 +2169,7 @@ func (p *Player) BreakBlock(pos cube.Pos) { } } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleBlockBreak(ctx, pos, &drops, &xp); ctx.Cancelled() { p.resendNearbyBlocks(pos) return @@ -2088,7 +2195,9 @@ func (p *Player) BreakBlock(pos cube.Pos) { } p.Exhaust(0.005) - if block.BreaksInstantly(b, held) { + // Only blocks that naturally break instantly (zero hardness) cost no durability; a block made to break + // within one tick by a fast tool or status effects still consumes durability. + if block.BreaksInstantly(b) { return } if durable, ok := held.Item().(item.Durable); ok { @@ -2137,7 +2246,7 @@ func (p *Player) PickBlock(pos cube.Pos) { return } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleBlockPick(ctx, pos, b); ctx.Cancelled() { return } @@ -2169,16 +2278,21 @@ func (p *Player) PickBlock(pos cube.Pos) { // Teleport teleports the player to a target position in the world. Unlike Move, it immediately changes the // position of the player, rather than showing an animation. func (p *Player) Teleport(pos mgl64.Vec3) { - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleTeleport(ctx, pos); ctx.Cancelled() { return } + p.forceTeleport(pos) +} + +// forceTeleport teleports the player without calling the Handler. +// It also wakes up the player from sleep. +func (p *Player) forceTeleport(pos mgl64.Vec3) { p.Wake() p.teleport(pos) } -// teleport teleports the player to a target position in the world. It does not call the Handler of the -// player. +// teleport teleports the player to a target position in the world without updating non-positional state. func (p *Player) teleport(pos mgl64.Vec3) { for _, v := range p.viewers() { v.ViewEntityTeleport(p, pos) @@ -2193,6 +2307,8 @@ func (p *Player) teleport(pos mgl64.Vec3) { // Move also rotates the player, adding deltaYaw and deltaPitch to the respective values. func (p *Player) Move(deltaPos mgl64.Vec3, deltaYaw, deltaPitch float64) { if p.Dead() || (deltaPos.ApproxEqual(mgl64.Vec3{}) && mgl64.FloatEqual(deltaYaw, 0) && mgl64.FloatEqual(deltaPitch, 0)) { + p.onGround = true + p.updateFallState(deltaPos.Y()) return } if p.immobile { @@ -2207,7 +2323,7 @@ func (p *Player) Move(deltaPos mgl64.Vec3, deltaYaw, deltaPitch float64) { pos = p.Position() res, resRot = pos.Add(deltaPos), p.Rotation().Add(cube.Rotation{deltaYaw, deltaPitch}) ) - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleMove(ctx, res, resRot); ctx.Cancelled() { if p.session() != session.Nop && pos.ApproxEqual(p.Position()) { // The position of the player was changed and the event cancelled. This means we still need to notify the @@ -2251,7 +2367,7 @@ func (p *Player) Move(deltaPos mgl64.Vec3, deltaYaw, deltaPitch float64) { } p.onGround = p.checkOnGround(deltaPos) - p.updateFallState(deltaPos[1]) + p.updateFallState(deltaPos.Y()) if p.Swimming() { p.Exhaust(0.01 * horizontalVel.Len()) @@ -2260,6 +2376,26 @@ func (p *Player) Move(deltaPos mgl64.Vec3, deltaYaw, deltaPitch float64) { } } +// Displace moves the player by a server-authoritative relative delta, clipped against block collision boxes. +func (p *Player) Displace(deltaPos mgl64.Vec3) { + if p.Dead() || deltaPos.ApproxEqual(mgl64.Vec3{}) { + return + } + pos := p.Position() + deltaPos, velocity := p.mc.CheckCollision(p.tx, p, pos, deltaPos) + if deltaPos.ApproxEqual(mgl64.Vec3{}) { + return + } + res := pos.Add(deltaPos) + for _, v := range p.viewers() { + v.ViewEntityDisplacement(p, res, p.Rotation(), p.OnGround()) + } + p.data.Pos, p.data.Vel = res, velocity + p.checkBlockCollisions(deltaPos) + p.onGround = p.checkOnGround(deltaPos) + p.updateFallState(deltaPos[1]) +} + // Position returns the current position of the player. It may be changed as the player moves or is moved // around the world. func (p *Player) Position() mgl64.Vec3 { @@ -2296,7 +2432,7 @@ func (p *Player) Collect(s item.Stack) (int, bool) { if p.Dead() || !p.GameMode().AllowsInteraction() { return 0, false } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleItemPickup(ctx, &s); ctx.Cancelled() { return 0, false } @@ -2328,7 +2464,7 @@ func (p *Player) ResetEnchantmentSeed() { // AddExperience adds experience to the player. func (p *Player) AddExperience(amount int) int { - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleExperienceGain(ctx, &amount); ctx.Cancelled() { return 0 } @@ -2449,7 +2585,7 @@ func (p *Player) mendItems(xp int) int { // The number of items that was dropped in the end is returned. It is generally the count of the stack passed // or 0 if dropping the item.Stack was cancelled. func (p *Player) Drop(s item.Stack) int { - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleItemDrop(ctx, s); ctx.Cancelled() { return 0 } @@ -2584,6 +2720,17 @@ func (p *Player) Tick(tx *world.Tx, current int64) { } else { p.data.Vel = mgl64.Vec3{} } + + p.portalTravel.StopPortalContact() +} + +// TravelThroughPortal handles the player touching a portal block. +func (p *Player) TravelThroughPortal(tx *world.Tx, target world.Dimension) { + if !p.GameMode().HasCollision() { + // Game modes that pass through blocks, such as spectator, are not affected by portals. + return + } + p.portalTravel.EnterPortal(p, tx, target) } // ViewLayer returns the ViewLayer attached to the player's session. @@ -2601,6 +2748,16 @@ func (p *Player) ViewPublicNameTag(entity world.Entity) { p.session().ViewPublicNameTag(entity) } +// ViewAlwaysShowNameTag overrides whether the entity's name tag is shown at all distances for this player. +func (p *Player) ViewAlwaysShowNameTag(entity world.Entity, alwaysShow bool) { + p.session().ViewAlwaysShowNameTag(entity, alwaysShow) +} + +// ViewPublicAlwaysShowNameTag removes the always-show name tag override of the entity for this player. +func (p *Player) ViewPublicAlwaysShowNameTag(entity world.Entity) { + p.session().ViewPublicAlwaysShowNameTag(entity) +} + // ViewScoreTag overrides the public score tag of the entity for this player. func (p *Player) ViewScoreTag(entity world.Entity, scoreTag string) { p.session().ViewScoreTag(entity, scoreTag) @@ -2624,7 +2781,7 @@ func (p *Player) RemoveViewLayer(entity world.Entity) { // tickAirSupply tick's the player's air supply, consuming it when underwater, and replenishing it when out of water. func (p *Player) tickAirSupply() { if !p.canBreathe() { - if r, ok := p.Armour().Helmet().Enchantment(enchantment.Respiration); ok && rand.Float64() <= enchantment.Respiration.Chance(r.Level()) { + if r, ok := p.Armour().Helmet().Enchantment(enchantment.Respiration); ok && rand.Float64() < enchantment.Respiration.Chance(r.Level()) { // respiration grants a chance to avoid drowning damage every tick. return } @@ -2654,7 +2811,7 @@ func (p *Player) tickFood() { } if p.hunger.foodTick == 1 { if p.hunger.canRegenerate() { - p.regenerate(false) + p.regenerate(!p.tx.World().Difficulty().FoodRegenerates()) } else if p.hunger.starving() { p.starve() } @@ -2675,8 +2832,8 @@ func (p *Player) regenerate(exhaust bool) { if p.Health() == p.MaxHealth() { return } - p.Heal(1, entity.FoodHealingSource{}) - if exhaust { + regenerated := p.Heal(1, entity.FoodHealingSource{QuickRegeneration: exhaust}) + if exhaust && regenerated > 0 { p.Exhaust(6) } } @@ -2755,6 +2912,9 @@ func (p *Player) insideOfSolid() bool { // Transparent. return false } + if immune, ok := b.(block.NonSuffocating); ok && immune.PreventsSuffocation() { + return false + } for _, blockBox := range b.Model().BBox(pos, p.tx) { if blockBox.Translate(pos.Vec3()).IntersectsWith(box) { return true @@ -2849,13 +3009,10 @@ func (p *Player) checkEntitySteppers() { if !p.OnGround() { return } - box := Type.BBox(p).Translate(p.Position()).Grow(-0.0001) - low, high := cube.PosFromVec3(box.Min()), cube.PosFromVec3(box.Max()) - y := int(math.Floor(box.Min()[1] - 0.0001)) - + low, high := p.blocksUnder() for x := low[0]; x <= high[0]; x++ { for z := low[2]; z <= high[2]; z++ { - pos := cube.Pos{x, y, z} + pos := cube.Pos{x, low[1], z} if stepper, ok := p.tx.Block(pos).(block.EntityStepper); ok { stepper.EntityStepOn(pos, p.tx, p) return @@ -2957,7 +3114,7 @@ func (p *Player) EditSign(pos cube.Pos, frontText, backText string) error { return nil } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if frontText != sign.Front.Text { if p.Handler().HandleSignEdit(ctx, pos, true, sign.Front.Text, frontText); ctx.Cancelled() { p.resendNearbyBlock(pos) @@ -2985,7 +3142,7 @@ func (p *Player) TurnLecternPage(pos cube.Pos, page int) error { return fmt.Errorf("edit lectern: no lectern at position %v", pos) } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleLecternPageTurn(ctx, pos, lectern.Page, &page); ctx.Cancelled() { return nil } @@ -3027,7 +3184,7 @@ func (p *Player) PunchAir() { if p.Dead() { return } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandlePunchAir(ctx); ctx.Cancelled() { return } @@ -3077,6 +3234,32 @@ func (p *Player) RemoveAllDebugShapes() { p.session().RemoveAllDebugShapes() } +// LockInput applies an input lock to the player, disabling the specified input and immediately sending the +// updated lock state to the client. +func (p *Player) LockInput(l input.Lock) { + p.session().LockInput(l) + p.session().SendInputLocks() +} + +// UnlockInput removes an input lock from the player, re-enabling the specified input and immediately sending +// the updated lock state to the client. +func (p *Player) UnlockInput(l input.Lock) { + p.session().UnlockInput(l) + p.session().SendInputLocks() +} + +// ClearInputLocks removes all input locks from the player, re-enabling all inputs and immediately sending the +// updated lock state to the client. +func (p *Player) ClearInputLocks() { + p.session().ClearInputLocks() + p.session().SendInputLocks() +} + +// InputLocked checks if a specific input lock is currently applied to the player. +func (p *Player) InputLocked(l input.Lock) bool { + return p.session().InputLocked(l) +} + // damageItem damages the item stack passed with the damage passed and returns the new stack. If the item // broke, a breaking sound is played. // If the player is not survival, the original stack is returned. @@ -3084,7 +3267,7 @@ func (p *Player) damageItem(s item.Stack, d int) item.Stack { if p.GameMode().CreativeInventory() || d == 0 || s.MaxDurability() == -1 { return s } - ctx := event.C(p) + ctx := NewEventContext(p.tx, p) if p.Handler().HandleItemDamage(ctx, s, &d); ctx.Cancelled() || d <= 0 { return s } @@ -3119,7 +3302,7 @@ func (p *Player) addNewItem(ctx *item.UseContext) { n, err := p.Inventory().AddItem(ctx.NewItem) if err != nil { // Not all items could be added to the inventory, so drop the rest. - p.Drop(ctx.NewItem.Grow(ctx.NewItem.Count() - n)) + p.Drop(ctx.NewItem.Grow(-n)) } if p.Dead() { p.dropItems() @@ -3137,6 +3320,9 @@ func (p *Player) canReach(pos mgl64.Vec3) bool { // Disconnect closes the player and removes it from the world. // Disconnect, unlike Close, allows a custom message to be passed to show to the player when it is // disconnected. The message is formatted following the rules of fmt.Sprintln without a newline at the end. +// The player is removed from its current world before Disconnect returns and +// must not be used afterwards. If the player is dead, it first respawns: the +// remaining teardown completes on the owner of the world it respawns into. func (p *Player) Disconnect(msg ...any) { p.once.Do(func() { p.close(format(msg)) @@ -3146,6 +3332,9 @@ func (p *Player) Disconnect(msg ...any) { // Close closes the player and removes it from the world. // Close disconnects the player with a 'Connection closed.' message. Disconnect should be used to disconnect a // player with a custom message. +// The player is removed from its current world before Close returns and must +// not be used afterwards. If the player is dead, it first respawns: the +// remaining teardown completes on the owner of the world it respawns into. func (p *Player) Close() error { p.once.Do(func() { p.close("Connection closed.") @@ -3173,6 +3362,10 @@ func (p *Player) quit(msg string) { if s := p.s; s != nil { s.Disconnect(msg) + // Close the session on this owner directly: teardown must not depend + // on the session goroutine, which cannot schedule work anymore once + // the world starts closing. + s.Close(p.tx, p) s.CloseConnection() return } diff --git a/server/player/playerdb/inventory.go b/server/player/playerdb/inventory.go index 81344b42bd..529ed71638 100644 --- a/server/player/playerdb/inventory.go +++ b/server/player/playerdb/inventory.go @@ -3,7 +3,6 @@ package playerdb import ( "bytes" - "github.com/df-mc/dragonfly/server/internal/nbtconv" "github.com/df-mc/dragonfly/server/item" "github.com/sandertv/gophertunnel/minecraft/nbt" ) @@ -70,13 +69,13 @@ func decodeItems(encoded []jsonSlot, items []item.Stack) { } } -func encodeItem(item item.Stack) []byte { - if item.Empty() { +func encodeItem(stack item.Stack) []byte { + if stack.Empty() { return nil } var b bytes.Buffer - itemNBT := nbtconv.WriteItem(item, true) + itemNBT := item.WriteNBT(stack, true) encoder := nbt.NewEncoderWithEncoding(&b, nbt.LittleEndian) err := encoder.Encode(itemNBT) if err != nil { @@ -92,5 +91,5 @@ func decodeItem(data []byte) item.Stack { if err != nil { return item.Stack{} } - return nbtconv.Item(itemNBT, nil) + return item.ReadNBT(itemNBT, nil) } diff --git a/server/player/playerdb/json.go b/server/player/playerdb/json.go index c453da047c..dd4b39fa7e 100644 --- a/server/player/playerdb/json.go +++ b/server/player/playerdb/json.go @@ -92,7 +92,7 @@ func (p *Provider) toJson(d player.Config, w *world.World) jsonData { MainHandSlot: uint32(d.HeldSlot), }), EnderChestInventory: encodeItems(d.EnderChestInventory.Slots()), - Dimension: uint8(dim), + Dimension: int32(dim), } } @@ -115,7 +115,7 @@ type jsonData struct { Effects []jsonEffect FireTicks int64 FallDistance float64 - Dimension uint8 + Dimension int32 } type jsonInventoryData struct { diff --git a/server/player/ref.go b/server/player/ref.go new file mode 100644 index 0000000000..097dca74bf --- /dev/null +++ b/server/player/ref.go @@ -0,0 +1,49 @@ +package player + +import ( + "context" + "time" + + "github.com/df-mc/dragonfly/server/world" +) + +// Ref is a stable reference to a player that outlives callbacks. The *Player +// value is only handed to scheduled owner callbacks, where it is safe to use. +type Ref = world.EntityRef[*Player] + +// NewRef creates a typed player reference from an entity handle. +func NewRef(h *world.EntityHandle) Ref { return world.NewEntityRef[*Player](h) } + +// Do schedules f to run with the player identified by h on its current world owner. +func Do(h *world.EntityHandle, f func(tx *world.Tx, p *Player)) *world.Task { + return NewRef(h).Do(f) +} + +// DoAfter schedules f to run with the player identified by h after delay. +func DoAfter(h *world.EntityHandle, delay time.Duration, f func(tx *world.Tx, p *Player)) *world.Task { + return NewRef(h).DoAfter(delay, f) +} + +// Call runs f with the player identified by h on its current world owner and +// waits for its typed result. If f panics, Call re-panics with the original +// value on the waiting goroutine. +func Call[T any](ctx context.Context, h *world.EntityHandle, f func(tx *world.Tx, p *Player) (T, error)) (T, error) { + return world.CallRef(ctx, NewRef(h), f) +} + +// Do schedules f to run with the player on its current world owner. Use it to +// re-enter the player from code that outlived a callback. +func (p *Player) Do(f func(tx *world.Tx, p *Player)) *world.Task { + if p == nil { + return world.NewFinishedTask(world.ErrEntityClosed) + } + return Do(p.handle, f) +} + +// DoAfter schedules f on the player's current world owner after delay. +func (p *Player) DoAfter(delay time.Duration, f func(tx *world.Tx, p *Player)) *world.Task { + if p == nil { + return world.NewFinishedTask(world.ErrEntityClosed) + } + return DoAfter(p.handle, delay, f) +} diff --git a/server/server.go b/server/server.go index 3430642eff..efa600629c 100644 --- a/server/server.go +++ b/server/server.go @@ -47,8 +47,9 @@ type Server struct { world, nether, end *world.World - customBlocks []protocol.BlockEntry - customItems []protocol.ItemEntry + customBlocks []protocol.BlockEntry + customItems []protocol.ItemEntry + customDimensions []protocol.DimensionDefinition listeners []Listener incoming chan incoming @@ -115,9 +116,12 @@ func (srv *Server) Listen() { // Accept accepts incoming players into the server, returning an iterator that // yields players that join the server while blocking otherwise. The iterator -// returned ends when the Server is closed using a call to Close. Players -// returned are only valid within the block of the for loop used to iterate over -// them: +// returned ends when the Server is closed using a call to Close. The loop body +// runs on the player's world owner: blocking there stalls that world, and +// calling world.Call, world.CallEntity, world.CallRef, or Task.Wait for the +// same owner deadlocks. Players returned are only valid within the block of the +// for loop used to iterate over them. Use p.H() with player.Do for work that +// outlives the loop body: // // for p := range srv.Accept() { // // p is valid here @@ -137,12 +141,24 @@ func (srv *Server) Accept() iter.Seq[*player.Player] { srv.p[inc.p.handle.UUID()] = inc.p srv.pmu.Unlock() - ret := false - <-inc.w.Exec(func(tx *world.Tx) { + ret, err := world.Call(context.Background(), inc.w, func(tx *world.Tx) (bool, error) { p := tx.AddEntity(inc.p.handle).(*player.Player) inc.s.Spawn(p, tx) - ret = !yield(p) + return !yield(p), nil }) + if err != nil { + srv.pmu.Lock() + delete(srv.p, inc.p.handle.UUID()) + srv.pmu.Unlock() + srv.pwg.Done() + // Join failed before spawn: the entity was never added, so close + // the orphaned handle and fully tear the session down (Disconnect + // only writes a packet; CloseConnection frees the conn/goroutines). + _ = inc.p.handle.Close() + inc.s.Disconnect("join failed") + inc.s.CloseConnection() + continue + } if ret { return } @@ -191,8 +207,14 @@ func (srv *Server) PlayerCount() int { // Players returns an iterator that yields players currently online. If Players // is called from within a transaction, the respective transaction should be -// passed. Passing nil is otherwise valid. Players returned are only valid -// within the block of the for loop used to iterate over them: +// passed. Passing nil is otherwise valid. Each loop body runs on the yielded +// player's world owner, so blocking stalls that world and calling world.Call, +// world.CallEntity, world.CallRef, or Task.Wait for the same owner deadlocks. +// Players in other worlds are yielded by blocking on those owners sequentially; +// mirrored handlers in two worlds can therefore deadlock each other. For +// fan-out, collect Player.H values and schedule each with player.Do instead. +// Players returned are only valid within the block of the for loop used to +// iterate over them: // // for p := range srv.Players(nil) { // // p is valid here @@ -204,7 +226,8 @@ func (srv *Server) PlayerCount() int { // // Collecting all values from the iterator using a function such as // slices.Collect immediately invalidates the players because their transactions -// will be finished. +// will be finished. Use Player.H(), player.NewRef, or player.Do when a +// player must be referenced after the iterator callback returns. func (srv *Server) Players(tx *world.Tx) iter.Seq[*player.Player] { srv.pmu.RLock() handles := make([]*world.EntityHandle, 0, len(srv.p)) @@ -223,10 +246,12 @@ func (srv *Server) Players(tx *world.Tx) iter.Seq[*player.Player] { continue } } - ret := false - handle.ExecWorld(func(tx *world.Tx, e world.Entity) { - ret = !yield(e.(*player.Player)) + ret, err := player.Call(context.Background(), handle, func(_ *world.Tx, p *player.Player) (bool, error) { + return !yield(p), nil }) + if err != nil { + continue + } if ret { break } @@ -348,11 +373,7 @@ func (srv *Server) listen(l Listener) { wg.Add(1) go func() { defer wg.Done() - if msg, ok := srv.conf.Allower.Allow(c.RemoteAddr(), c.IdentityData(), c.ClientData()); !ok { - _ = c.WritePacket(&packet.Disconnect{HideDisconnectionScreen: msg == "", Message: msg}) - _ = c.Close() - return - } + srv.finaliseConn(ctx, c, l) }() } @@ -363,6 +384,7 @@ func (srv *Server) listen(l Listener) { func (srv *Server) startListening() { srv.makeBlockEntries() srv.makeItemComponents() + srv.makeDimensionData() srv.wg.Add(len(srv.listeners)) for _, l := range srv.listeners { @@ -393,7 +415,7 @@ func (srv *Server) makeItemComponents() { custom := world.CustomItems() srv.customItems = make([]protocol.ItemEntry, len(custom)) - for _, it := range custom { + for i, it := range custom { name, _ := it.EncodeItem() rid, _, _ := world.ItemRuntimeID(it) _, isCustomBlock := it.(world.CustomBlock) @@ -401,12 +423,27 @@ func (srv *Server) makeItemComponents() { if isCustomBlock { entryVersion = protocol.ItemEntryVersionNone } - srv.customItems = append(srv.customItems, protocol.ItemEntry{ + srv.customItems[i] = protocol.ItemEntry{ Name: name, ComponentBased: !isCustomBlock, RuntimeID: int16(rid), Version: entryVersion, Data: iteminternal.Components(it), + } + } +} + +// makeDimensionData initialises the server's custom dimensions list. +func (srv *Server) makeDimensionData() { + dimensions := world.CustomDimensions() + srv.customDimensions = make([]protocol.DimensionDefinition, 0, len(dimensions)) + for _, registration := range dimensions { + r := registration.Dimension.Range() + srv.customDimensions = append(srv.customDimensions, protocol.DimensionDefinition{ + Name: registration.Name, + Range: [2]int32{int32(r.Max() + 1), int32(r.Min())}, + Generator: protocol.GeneratorVoid, + DimensionType: int32(registration.ID), }) } } @@ -485,6 +522,7 @@ func (srv *Server) defaultGameData() minecraft.GameData { PlayerMovementSettings: protocol.PlayerMovementSettings{ ServerAuthoritativeBlockBreaking: true, }, + Dimensions: srv.customDimensions, } } @@ -514,8 +552,12 @@ func (srv *Server) handleSessionClose(tx *world.Tx, c session.Controllable) { return } - if err := srv.conf.PlayerProvider.Save(c.UUID(), c.(*player.Player).Data(), tx.World()); err != nil { - srv.conf.Log.Error("Save player data: " + err.Error()) + if tx != nil { + if err := srv.conf.PlayerProvider.Save(c.UUID(), c.(*player.Player).Data(), tx.World()); err != nil { + srv.conf.Log.Error("Save player data: " + err.Error()) + } + } else { + srv.conf.Log.Error("Save player data: player's worlds closed before teardown; data not saved", "uuid", c.UUID()) } srv.pwg.Done() } @@ -563,6 +605,7 @@ func (srv *Server) createWorld(dim world.Dimension, nether, end **world.World) * ReadOnly: srv.conf.ReadOnlyWorld, SaveInterval: srv.conf.SaveInterval, ChunkUnloadInterval: srv.conf.ChunkUnloadInterval, + ChunkLoadWorkers: srv.conf.ChunkLoadWorkers, Entities: srv.conf.Entities, Blocks: srv.conf.Blocks, PortalDestination: func(dim world.Dimension) *world.World { diff --git a/server/session/chunk.go b/server/session/chunk.go index 150142ae33..ef765bd2dd 100644 --- a/server/session/chunk.go +++ b/server/session/chunk.go @@ -84,15 +84,19 @@ func (s *Session) subChunkEntry(offset protocol.SubChunkOffset, ind int16, col * } else if lower { subMapType, subMap = protocol.HeightMapDataTooLow, nil } + var subMapData protocol.Optional[[]int8] + if subMap != nil { + subMapData = protocol.Option(subMap) + } sub := col.Sub()[ind] if sub.Empty() { return protocol.SubChunkEntry{ Result: protocol.SubChunkResultSuccessAllAir, HeightMapType: subMapType, - HeightMapData: subMap, + HeightMapData: subMapData, RenderHeightMapType: subMapType, - RenderHeightMapData: subMap, + RenderHeightMapData: subMapData, Offset: offset, } } @@ -111,19 +115,19 @@ func (s *Session) subChunkEntry(offset protocol.SubChunkOffset, ind int16, col * entry := protocol.SubChunkEntry{ Result: protocol.SubChunkResultSuccess, - RawPayload: append(serialisedSubChunk, blockEntityBuf.Bytes()...), + RawPayload: protocol.Option(append(serialisedSubChunk, blockEntityBuf.Bytes()...)), HeightMapType: subMapType, - HeightMapData: subMap, + HeightMapData: subMapData, RenderHeightMapType: subMapType, - RenderHeightMapData: subMap, + RenderHeightMapData: subMapData, Offset: offset, } if s.conn.ClientCacheEnabled() { if hash := xxhash.Sum64(serialisedSubChunk); s.trackBlob(hash, serialisedSubChunk) { transaction[hash] = struct{}{} - entry.BlobHash = hash - entry.RawPayload = blockEntityBuf.Bytes() + entry.BlobHash = protocol.Option(hash) + entry.RawPayload = protocol.Option(blockEntityBuf.Bytes()) } } return entry @@ -142,13 +146,13 @@ func (s *Session) sendBlobHashes(pos world.ChunkPos, dim world.Dimension, c *chu biomes := chunk.EncodeBiomes(c, chunk.NetworkEncoding) if hash := xxhash.Sum64(biomes); s.trackBlob(hash, biomes) { s.writePacket(&packet.LevelChunk{ - Dimension: s.dimensionID(dim), - SubChunkCount: protocol.SubChunkRequestModeLimited, - Position: protocol.ChunkPos(pos), - HighestSubChunk: c.HighestFilledSubChunk(), - BlobHashes: []uint64{hash}, - RawPayload: []byte{0}, - CacheEnabled: true, + Dimension: s.dimensionID(dim), + SubChunkCount: 0, + Position: protocol.ChunkPos(pos), + SubChunkLimit: protocol.Option(int32(c.HighestFilledSubChunk())), + BlobHashes: []uint64{hash}, + RawPayload: []byte{0}, + CacheEnabled: true, }) return } @@ -203,11 +207,11 @@ func (s *Session) sendBlobHashes(pos world.ChunkPos, dim world.Dimension, c *chu func (s *Session) sendNetworkChunk(pos world.ChunkPos, dim world.Dimension, c *chunk.Chunk, blockEntities map[cube.Pos]world.Block) { if subChunkRequests { s.writePacket(&packet.LevelChunk{ - Dimension: s.dimensionID(dim), - SubChunkCount: protocol.SubChunkRequestModeLimited, - Position: protocol.ChunkPos(pos), - HighestSubChunk: c.HighestFilledSubChunk(), - RawPayload: append(chunk.EncodeBiomes(c, chunk.NetworkEncoding), 0), + Dimension: s.dimensionID(dim), + SubChunkCount: 0, + Position: protocol.ChunkPos(pos), + SubChunkLimit: protocol.Option(int32(c.HighestFilledSubChunk())), + RawPayload: append(chunk.EncodeBiomes(c, chunk.NetworkEncoding), 0), }) return } diff --git a/server/session/command.go b/server/session/command.go index b18920f711..0752286ba6 100644 --- a/server/session/command.go +++ b/server/session/command.go @@ -213,7 +213,7 @@ func valueToParamType(i cmd.ParamInfo, source cmd.Source) (t uint32, enum comman Options: enum.Options(source), } } - return protocol.CommandArgTypeValue, enum + return protocol.CommandArgTypeRValue, enum } // resendCommands resends all commands that a Session has access to if the map of runnable commands passed does not diff --git a/server/session/controllable.go b/server/session/controllable.go index 8a924bf60d..d5386931d2 100644 --- a/server/session/controllable.go +++ b/server/session/controllable.go @@ -11,6 +11,7 @@ import ( "github.com/df-mc/dragonfly/server/player/dialogue" "github.com/df-mc/dragonfly/server/player/form" "github.com/df-mc/dragonfly/server/player/hud" + "github.com/df-mc/dragonfly/server/player/input" "github.com/df-mc/dragonfly/server/player/skin" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" @@ -31,6 +32,7 @@ type Controllable interface { chat.Subscriber hud.Renderer debug.Renderer + input.Restricter Locale() language.Tag diff --git a/server/session/enchantment_texts.go b/server/session/enchantment_texts.go index ed1267f2b8..93d40d83b7 100644 --- a/server/session/enchantment_texts.go +++ b/server/session/enchantment_texts.go @@ -4,4 +4,4 @@ package session // enchantNames are names translated to the 'Standard Galactic Alphabet' client-side. The names generally have no meaning // on the vanilla server implementation, so we can sneak some easter eggs in here without anyone noticing. -var enchantNames = []string{"aabstractt", "abimek", "aericio", "aimjel", "akmal fairuz", "alvin0319", "andreashgk", "assassin ghost yt", "atm85", "azvyl", "blackjack200", "cetfu", "cjmustard", "cooldogedev", "cqdetdev", "da pig guy", "daft0175", "dasciam", "deniel world", "didntpot", "driftlgtm", "eminarican", "endermanbugzjfc", "erkam246", "ethaniccc", "fdutch", "flonja", "game parrot", "gewinum", "hashim the arab", "hochbaum", "hydzilla", "im da real ani", "inotflying", "ipad54", "its zodia x", "ivan craft623", "javier leon9966", "just tal develops", "k4ties", "krivey", "manab-pr", "mmm545", "mohamed587100", "myma qc", "natuyasai natuo", "neutronic mc", "nonono697", "nope not dark", "provsalt", "restart fu", "riccskn", "robertdudaa", "royal mcpe", "sallypemdas", "sandertv", "schphe", "sculas", "sergittos", "smell-of-curry", "sqmatheus", "ssaini123456", "studgi", "superomarking", "t14 raptor", "tadhunt", "theaddonn", "thicksunny", "thunder33345", "tripple awap", "tristanmorgan", "twisted asylum mc", "unickorn", "unknown ore", "uramnoil", "wqrro", "x natsuri", "x toast-dev", "x4caa", "xd-pro"} +var enchantNames = []string{"aabstractt", "abimek", "aericio", "aimjel", "akmal fairuz", "alvin0319", "andreashgk", "assassin ghost yt", "atm85", "azvyl", "blackjack200", "cetfu", "cjmustard", "cooldogedev", "cqdetdev", "da pig guy", "daft0175", "dasciam", "deniel world", "didntpot", "driftlgtm", "eminarican", "emirhan olgn", "endermanbugzjfc", "erkam246", "ethaniccc", "fdutch", "flonja", "game parrot", "gewinum", "hashim the arab", "hochbaum", "hydzilla", "im da real ani", "inotflying", "ipad54", "its zodia x", "ivan craft623", "javier leon9966", "josscoder", "just tal develops", "k4ties", "krivey", "manab-pr", "memoxiiii", "mmm545", "mohamed587100", "myma qc", "natuyasai natuo", "neutronic mc", "nonono697", "nope not dark", "provsalt", "restart fu", "riccskn", "robertdudaa", "royal mcpe", "sallypemdas", "sandertv", "schphe", "sculas", "sergittos", "smell-of-curry", "sqmatheus", "ssaini123456", "studgi", "superomarking", "t14 raptor", "tadhunt", "theaddonn", "theboss9345", "thicksunny", "thunder33345", "tripple awap", "tristanmorgan", "trix new", "twisted asylum mc", "unickorn", "unknown ore", "uramnoil", "wqrro", "x natsuri", "x superr", "x toast-dev", "x4caa", "xd-pro"} diff --git a/server/session/entity_metadata.go b/server/session/entity_metadata.go index 2cdfb2cfe0..cc69b09902 100644 --- a/server/session/entity_metadata.go +++ b/server/session/entity_metadata.go @@ -93,7 +93,7 @@ func (s *Session) addSpecificMetadata(e any, m protocol.EntityMetadata) { m[protocol.EntityDataKeyValue] = int32(o.Experience()) } if f, ok := e.(firework); ok { - m[protocol.EntityDataKeyDisplayTileRuntimeID] = nbtconv.WriteItem(item.NewStack(f.Firework(), 1), false) + m[protocol.EntityDataKeyDisplayFirework] = item.WriteNBT(item.NewStack(f.Firework(), 1), false) if o, ok := e.(owned); ok && f.Attached() && o.Owner() != nil { m[protocol.EntityDataKeyCustomDisplay] = int64(s.handleRuntimeID(o.Owner())) } @@ -107,22 +107,22 @@ func (s *Session) addSpecificMetadata(e any, m protocol.EntityMetadata) { m[protocol.EntityDataKeyFuseTime] = int32(t.Fuse().Milliseconds() / 50) m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagIgnited) } - if n, ok := e.(named); ok { - name := n.NameTag() - m[protocol.EntityDataKeyName] = name - if name == "" { - m[protocol.EntityDataKeyAlwaysShowNameTag] = uint8(0) - m.UnsetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName) - m.UnsetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowName) - } else { - m[protocol.EntityDataKeyAlwaysShowNameTag] = uint8(1) - m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName) - m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowName) - } + if nameTag, alwaysShow, ok := nameTagState(e); ok { + writeNameTagMetadata(m, nameTag, alwaysShow) } if sc, ok := e.(scoreTag); ok { m[protocol.EntityDataKeyScore] = sc.ScoreTag() } + if c, ok := e.(endCrystal); ok { + if c.ShowBase() { + m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowBottom) + } else { + m.UnsetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowBottom) + } + if target, ok := c.BeamTarget(); ok { + m[protocol.EntityDataKeyBlockTarget] = protocol.BlockPos{int32(target[0]), int32(target[1]), int32(target[2])} + } + } if sl, ok := e.(sleeper); ok { if pos, ok := sl.Sleeping(); ok { m[protocol.EntityDataKeyBedPosition] = protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])} @@ -188,6 +188,40 @@ func (s *Session) addSpecificMetadata(e any, m protocol.EntityMetadata) { } } +// nameTagState returns the public name tag of an entity, whether that name tag is shown at all distances +// and whether the entity has a name tag at all. Entities that do not report an always show state show +// their name tag at all distances. +func nameTagState(e any) (string, bool, bool) { + alwaysShow := true + if a, ok := e.(alwaysShowNameTag); ok { + alwaysShow = a.AlwaysShowNameTag() + } + n, ok := e.(named) + if !ok { + return "", alwaysShow, false + } + return n.NameTag(), alwaysShow, true +} + +// writeNameTagMetadata writes a name tag and its related visibility properties to metadata. +func writeNameTagMetadata(m protocol.EntityMetadata, nameTag string, alwaysShow bool) { + show := nameTag != "" + always := show && alwaysShow + + m[protocol.EntityDataKeyName] = nameTag + m[protocol.EntityDataKeyAlwaysShowNameTag] = boolByte(always) + if show { + m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowName) + } else { + m.UnsetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowName) + } + if always { + m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName) + } else { + m.UnsetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName) + } +} + type sneaker interface { Sneaking() bool } @@ -238,10 +272,19 @@ type named interface { NameTag() string } +type alwaysShowNameTag interface { + AlwaysShowNameTag() bool +} + type scoreTag interface { ScoreTag() string } +type endCrystal interface { + ShowBase() bool + BeamTarget() (cube.Pos, bool) +} + type splash interface { Potion() potion.Potion } diff --git a/server/session/handler_anvil.go b/server/session/handler_anvil.go index 143d4c974c..908608046c 100644 --- a/server/session/handler_anvil.go +++ b/server/session/handler_anvil.go @@ -31,7 +31,7 @@ func (h *ItemStackRequestHandler) handleCraftRecipeOptional(a *protocol.CraftRec if !ok { return fmt.Errorf("no anvil container opened") } - if len(filterStrings) < int(a.FilterStringIndex) { + if index := int(a.FilterStringIndex); len(filterStrings) > 0 && (index < 0 || index >= len(filterStrings)) { return fmt.Errorf("filter string index %v is out of bounds", a.FilterStringIndex) } diff --git a/server/session/handler_crafting.go b/server/session/handler_crafting.go index ae90c902fa..5e04bd5260 100644 --- a/server/session/handler_crafting.go +++ b/server/session/handler_crafting.go @@ -72,7 +72,7 @@ func (h *ItemStackRequestHandler) handleAutoCraft(a *protocol.AutoCraftRecipeSta craft, ok := s.recipes[a.RecipeNetworkID] if !ok { // Try dynamic recipes if no static recipe matches - return h.tryDynamicCraft(s, tx, int(a.TimesCrafted)) + return h.tryDynamicCraft(s, tx, int(a.NumberOfCrafts)) } _, shaped := craft.(recipe.Shaped) _, shapeless := craft.(recipe.Shapeless) @@ -83,7 +83,7 @@ func (h *ItemStackRequestHandler) handleAutoCraft(a *protocol.AutoCraftRecipeSta return fmt.Errorf("recipe with network id %v is not a crafting table recipe", a.RecipeNetworkID) } - timesCrafted := int(a.TimesCrafted) + timesCrafted := int(a.NumberOfCrafts) if timesCrafted < 1 { return fmt.Errorf("times crafted must be at least 1") } diff --git a/server/session/handler_inventory_transaction.go b/server/session/handler_inventory_transaction.go index 24b9a191e7..e8cc2eca77 100644 --- a/server/session/handler_inventory_transaction.go +++ b/server/session/handler_inventory_transaction.go @@ -96,13 +96,14 @@ func (h *InventoryTransactionHandler) handleNormalTransaction(pk *packet.Invento expected item.Stack ) for _, action := range pk.Actions { + windowID, hasWindowID := action.WindowID.Value() switch { case action.SourceType == protocol.InventoryActionSourceWorld && action.InventorySlot == 0: if old := stackToItem(s.br, action.OldItem.Stack); !old.Empty() { return fmt.Errorf("unexpected non-empty old item in transaction action: %#v", action.OldItem) } count = int(action.NewItem.Stack.Count) - case action.SourceType == protocol.InventoryActionSourceContainer && action.WindowID == protocol.WindowIDInventory: + case action.SourceType == protocol.InventoryActionSourceContainer && hasWindowID && windowID == protocol.WindowIDInventory: if expected = stackToItem(s.br, action.OldItem.Stack); expected.Empty() { return fmt.Errorf("unexpected empty old item in transaction action: %#v", action.OldItem) } @@ -177,8 +178,11 @@ func (h *InventoryTransactionHandler) handleUseItemOnEntityTransaction(data *pro // handleUseItemTransaction ... func (h *InventoryTransactionHandler) handleUseItemTransaction(data *protocol.UseItemTransactionData, s *Session, c Controllable) error { pos := cube.Pos{int(data.BlockPosition[0]), int(data.BlockPosition[1]), int(data.BlockPosition[2])} - s.swingingArm.Store(true) - defer s.swingingArm.Store(false) + if data.ClientPrediction == protocol.ClientPredictionSuccess || data.ActionType == protocol.UseItemActionBreakBlock { + // Suppress echoing the swing animation only when the client has already predicted it locally. + s.swingingArm.Store(true) + defer s.swingingArm.Store(false) + } // We reset the inventory so that we can send the held item update without the client already // having done that client-side. diff --git a/server/session/handler_item_stack_request.go b/server/session/handler_item_stack_request.go index b7d50b7535..5420e33733 100644 --- a/server/session/handler_item_stack_request.go +++ b/server/session/handler_item_stack_request.go @@ -279,7 +279,7 @@ func (h *ItemStackRequestHandler) handleMineBlock(a *protocol.MineBlockStackRequ // output as usual. func (h *ItemStackRequestHandler) handleCreate(a *protocol.CreateStackRequestAction, s *Session, tx *world.Tx) error { slot := int(a.ResultsSlot) - if len(h.pendingResults) < slot { + if slot >= len(h.pendingResults) { return fmt.Errorf("invalid pending result slot: %v", a.ResultsSlot) } diff --git a/server/session/handler_loom.go b/server/session/handler_loom.go index 7ad41921a4..17fb0ee4d4 100644 --- a/server/session/handler_loom.go +++ b/server/session/handler_loom.go @@ -59,7 +59,10 @@ func (h *ItemStackRequestHandler) handleLoomCraft(a *protocol.CraftLoomRecipeSta // The action contains the pattern that the client wanted to apply, so parse the ID and check if it is a valid // pattern. - expectedPattern := block.BannerPatternByID(a.Pattern) + expectedPattern, exists := block.BannerPatternByID(a.Pattern) + if !exists { + return fmt.Errorf("unknown banner pattern id %q", a.Pattern) + } // Some banner patterns have equivalent banner pattern items that are required to craft the pattern. If the expected // pattern has a pattern item, check if the player input the correct pattern item. @@ -93,5 +96,7 @@ func (h *ItemStackRequestHandler) handleLoomCraft(a *protocol.CraftLoomRecipeSta Container: protocol.FullContainerName{ContainerID: protocol.ContainerLoomDye}, Slot: loomDyeSlot, }, dye.Grow(-timesCrafted), s, tx) - return h.createResults(s, tx, input.WithItem(b)) + // Only timesCrafted banners are consumed above, so only that many may be produced: input.WithItem keeps the count + // of the whole input stack. + return h.createResults(s, tx, input.Grow(timesCrafted-input.Count()).WithItem(b)) } diff --git a/server/session/handler_npc_request.go b/server/session/handler_npc_request.go index a24e21ea98..30e9d33abd 100644 --- a/server/session/handler_npc_request.go +++ b/server/session/handler_npc_request.go @@ -16,6 +16,10 @@ type NPCRequestHandler struct { // Handle ... func (h *NPCRequestHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.NPCRequest) + if h.entityRuntimeID == 0 { + // No dialogue is currently open for this session, so there is nothing to submit or close. + return nil + } switch pk.RequestType { case packet.NPCRequestActionExecuteAction: if err := h.dialogue.Submit(uint(pk.ActionType), c, tx); err != nil { diff --git a/server/session/handler_player_auth_input.go b/server/session/handler_player_auth_input.go index f75fcba2f3..7871ad97d9 100644 --- a/server/session/handler_player_auth_input.go +++ b/server/session/handler_player_auth_input.go @@ -46,20 +46,19 @@ func (h PlayerAuthInputHandler) handleMovement(pk *packet.PlayerAuthInput, s *Se newPos := vec32To64(pk.Position) deltaPos, deltaYaw, deltaPitch := newPos.Sub(pos), float64(pk.Yaw)-yaw, float64(pk.Pitch)-pitch - if mgl64.FloatEqual(deltaPos.Len(), 0) && mgl64.FloatEqual(deltaYaw, 0) && mgl64.FloatEqual(deltaPitch, 0) { - // The PlayerAuthInput packet is sent every tick, so don't do anything if the position and rotation - // were unchanged. - return nil - } - if expected := s.teleportPos.Load(); expected != nil { - if newPos.Sub(*expected).Len() > 1 { - // The player has moved before it received the teleport packet. Ignore this movement entirely and - // wait for the client to sync itself back to the server. Once we get a movement that is close - // enough to the teleport position, we'll allow the player to move around again. - return nil + // The PlayerAuthInput packet is sent every tick, so don't check for teleport if the position and rotation + // were unchanged. + if !mgl64.FloatEqual(deltaPos.Len(), 0) || !mgl64.FloatEqual(deltaYaw, 0) || !mgl64.FloatEqual(deltaPitch, 0) { + if expected := s.teleportPos.Load(); expected != nil { + if newPos.Sub(*expected).Len() > 1 { + // The player has moved before it received the teleport packet. Ignore this movement entirely and + // wait for the client to sync itself back to the server. Once we get a movement that is close + // enough to the teleport position, we'll allow the player to move around again. + return nil + } + s.teleportPos.Store(nil) } - s.teleportPos.Store(nil) } s.moving = true @@ -70,24 +69,36 @@ func (h PlayerAuthInputHandler) handleMovement(pk *packet.PlayerAuthInput, s *Se // handleActions handles the actions with the world that are present in the PlayerAuthInput packet. func (h PlayerAuthInputHandler) handleActions(pk *packet.PlayerAuthInput, s *Session, tx *world.Tx, c Controllable) error { if pk.InputData.Load(packet.InputFlagPerformItemInteraction) { - if err := h.handleUseItemData(pk.ItemInteractionData, s, c); err != nil { + data, ok := pk.ItemInteractionData.Value() + if !ok { + return fmt.Errorf("item interaction flag set without item interaction data") + } + if err := h.handleUseItemData(data, s, c); err != nil { return err } } if pk.InputData.Load(packet.InputFlagPerformBlockActions) { - if err := h.handleBlockActions(pk.BlockActions, s, c); err != nil { + actions, ok := pk.BlockActions.Value() + if !ok { + return fmt.Errorf("block actions flag set without block actions") + } + if err := h.handleBlockActions(actions, s, c); err != nil { return err } } h.handleInputFlags(pk.InputData, s, c) if pk.InputData.Load(packet.InputFlagPerformItemStackRequest) { + request, ok := pk.ItemStackRequest.Value() + if !ok { + return fmt.Errorf("item stack request flag set without item stack request") + } s.inTransaction.Store(true) defer s.inTransaction.Store(false) // As of 1.18 this is now used for sending item stack requests such as when mining a block. sh := s.handlers[packet.IDItemStackRequest].(*ItemStackRequestHandler) - if err := sh.handleRequest(pk.ItemStackRequest, s, tx, c); err != nil { + if err := sh.handleRequest(request, s, tx, c); err != nil { // Item stacks being out of sync isn't uncommon, so don't error. Just debug the error and let the // revert do its work. s.conf.Log.Debug("process packet: PlayerAuthInput: resolve item stack request: " + err.Error()) @@ -97,18 +108,19 @@ func (h PlayerAuthInputHandler) handleActions(pk *packet.PlayerAuthInput, s *Ses } // handleInputFlags handles the toggleable input flags set in a PlayerAuthInput packet. -func (h PlayerAuthInputHandler) handleInputFlags(flags protocol.Bitset, s *Session, c Controllable) { +func (h PlayerAuthInputHandler) handleInputFlags(flags protocol.InputFlags, s *Session, c Controllable) { if flags.Load(packet.InputFlagStartSprinting) { c.StartSprinting() } if flags.Load(packet.InputFlagStopSprinting) { c.StopSprinting() } - if flags.Load(packet.InputFlagStartSneaking) { - c.StartSneaking() - } - if flags.Load(packet.InputFlagStopSneaking) { - c.StopSneaking() + if sneaking := flags.Load(packet.InputFlagSneaking); sneaking != c.Sneaking() { + if sneaking { + c.StartSneaking() + } else { + c.StopSneaking() + } } if flags.Load(packet.InputFlagStartSwimming) { c.StartSwimming() diff --git a/server/session/handler_smithing.go b/server/session/handler_smithing.go index e9f26cd888..1759f74560 100644 --- a/server/session/handler_smithing.go +++ b/server/session/handler_smithing.go @@ -71,6 +71,9 @@ func (h *ItemStackRequestHandler) handleSmithing(a *protocol.CraftRecipeStackReq Slot: smithingTemplateSlot, }, template.Grow(-1), s, tx) + // Only one input item is consumed above, so only one may be produced: input.WithItem keeps the count of the whole + // input stack. + result := input.Grow(1 - input.Count()) if _, ok = craft.(recipe.SmithingTrim); ok { var trim item.ArmourTrim if t, ok := template.Item().(item.SmithingTemplate); ok { @@ -85,7 +88,7 @@ func (h *ItemStackRequestHandler) handleSmithing(a *protocol.CraftRecipeStackReq if !ok { return fmt.Errorf("input item is not trimmable") } - return h.createResults(s, tx, input.WithItem(trimmable.WithTrim(trim))) + return h.createResults(s, tx, result.WithItem(trimmable.WithTrim(trim))) } - return h.createResults(s, tx, input.WithItem(craft.Output()[0].Item())) + return h.createResults(s, tx, result.WithItem(craft.Output()[0].Item())) } diff --git a/server/session/player.go b/server/session/player.go index 47696f65df..67f63cbb75 100644 --- a/server/session/player.go +++ b/server/session/player.go @@ -14,7 +14,6 @@ import ( "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/entity" "github.com/df-mc/dragonfly/server/entity/effect" - "github.com/df-mc/dragonfly/server/internal/nbtconv" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/item/creative" "github.com/df-mc/dragonfly/server/item/inventory" @@ -23,6 +22,7 @@ import ( "github.com/df-mc/dragonfly/server/player/dialogue" "github.com/df-mc/dragonfly/server/player/form" "github.com/df-mc/dragonfly/server/player/hud" + "github.com/df-mc/dragonfly/server/player/input" "github.com/df-mc/dragonfly/server/player/skin" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/sound" @@ -112,9 +112,14 @@ func (s *Session) sendBiomes() { // sendRecipes sends the current crafting recipes to the session. func (s *Session) sendRecipes() { - recipes := make([]protocol.Recipe, 0, len(recipe.Recipes())) - potionRecipes := make([]protocol.PotionRecipe, 0) - potionContainerChange := make([]protocol.PotionContainerChangeRecipe, 0) + var ( + shapedRecipes []protocol.ShapedRecipe + shapelessRecipes []protocol.ShapelessRecipe + smithingTransformRecipes []protocol.SmithingTransformRecipe + smithingTrimRecipes []protocol.SmithingTrimRecipe + potionRecipes []protocol.PotionRecipe + potionContainerChange []protocol.PotionContainerChangeRecipe + ) for index, i := range recipe.Recipes() { networkID := uint32(index) + 1 @@ -122,7 +127,7 @@ func (s *Session) sendRecipes() { switch i := i.(type) { case recipe.Shapeless: - recipes = append(recipes, &protocol.ShapelessRecipe{ + shapelessRecipes = append(shapelessRecipes, protocol.ShapelessRecipe{ RecipeID: uuid.New().String(), Priority: int32(i.Priority()), Input: stacksToIngredientItems(s.br, i.Input()), @@ -131,7 +136,7 @@ func (s *Session) sendRecipes() { RecipeNetworkID: networkID, }) case recipe.Shaped: - recipes = append(recipes, &protocol.ShapedRecipe{ + shapedRecipes = append(shapedRecipes, protocol.ShapedRecipe{ RecipeID: uuid.New().String(), Priority: int32(i.Priority()), Width: int32(i.Shape().Width()), @@ -143,7 +148,7 @@ func (s *Session) sendRecipes() { }) case recipe.SmithingTransform: input, output := stacksToIngredientItems(s.br, i.Input()), stacksToRecipeStacks(s.br, i.Output()) - recipes = append(recipes, &protocol.SmithingTransformRecipe{ + smithingTransformRecipes = append(smithingTransformRecipes, protocol.SmithingTransformRecipe{ RecipeID: uuid.New().String(), Base: input[0], Addition: input[1], @@ -154,7 +159,7 @@ func (s *Session) sendRecipes() { }) case recipe.SmithingTrim: input := stacksToIngredientItems(s.br, i.Input()) - recipes = append(recipes, &protocol.SmithingTrimRecipe{ + smithingTrimRecipes = append(smithingTrimRecipes, protocol.SmithingTrimRecipe{ RecipeID: uuid.New().String(), Base: input[0], Addition: input[1], @@ -188,7 +193,15 @@ func (s *Session) sendRecipes() { }) } } - s.writePacket(&packet.CraftingData{Recipes: recipes, PotionRecipes: potionRecipes, PotionContainerChangeRecipes: potionContainerChange, ClearRecipes: true}) + s.writePacket(&packet.CraftingData{ + ShapedRecipes: shapedRecipes, + ShapelessRecipes: shapelessRecipes, + SmithingTransformRecipes: smithingTransformRecipes, + SmithingTrimRecipes: smithingTrimRecipes, + PotionRecipes: potionRecipes, + PotionContainerChangeRecipes: potionContainerChange, + ClearRecipes: true, + }) } // sendArmourTrimData sends the armour trim data. @@ -279,6 +292,10 @@ func (s *Session) invByID(id int32, tx *world.Tx) (*inventory.Inventory, bool) { switch id { case protocol.ContainerLevelEntity: return s.openedWindow.Load(), true + case protocol.ContainerShulkerBox: + if _, shulkerbox := tx.Block(*s.openedPos.Load()).(block.ShulkerBox); shulkerbox { + return s.openedWindow.Load(), true + } case protocol.ContainerBarrel: if _, barrel := tx.Block(*s.openedPos.Load()).(block.Barrel); barrel { return s.openedWindow.Load(), true @@ -905,6 +922,45 @@ func (s *Session) SendDebugShapes(dim world.Dimension) { s.writePacket(&packet.PrimitiveShapes{Shapes: shapes}) } +// LockInput applies an input lock to the player, disabling the specified input. If the lock is already +// applied, this is a no-op. +func (s *Session) LockInput(l input.Lock) { + s.inputLocksMu.Lock() + defer s.inputLocksMu.Unlock() + s.inputLocks |= l.Uint32() +} + +// UnlockInput removes an input lock from the player, re-enabling the specified input. If the lock is not +// currently applied, this is a no-op. +func (s *Session) UnlockInput(l input.Lock) { + s.inputLocksMu.Lock() + defer s.inputLocksMu.Unlock() + s.inputLocks &^= l.Uint32() +} + +// ClearInputLocks removes all input locks from the player, re-enabling all inputs. +func (s *Session) ClearInputLocks() { + s.inputLocksMu.Lock() + defer s.inputLocksMu.Unlock() + s.inputLocks = 0 +} + +// InputLocked checks if a specific input lock is currently applied to the player. +func (s *Session) InputLocked(l input.Lock) bool { + s.inputLocksMu.RLock() + defer s.inputLocksMu.RUnlock() + return s.inputLocks&l.Uint32() != 0 +} + +// SendInputLocks sends the current input lock state to the client. +func (s *Session) SendInputLocks() { + s.inputLocksMu.RLock() + defer s.inputLocksMu.RUnlock() + s.writePacket(&packet.UpdateClientInputLocks{ + Locks: s.inputLocks, + }) +} + // queueDebugShapeUpdate queues a debug shape mutation to be applied the next time debug shapes are sent. func (s *Session) queueDebugShapeUpdate(update debugShapeUpdate) { s.debugShapesMu.Lock() @@ -940,10 +996,9 @@ func stackFromItem(br world.BlockRegistry, it item.Stack) protocol.ItemStack { NetworkID: rid, MetadataValue: uint32(meta), }, - HasNetworkID: true, Count: uint16(it.Count()), BlockRuntimeID: int32(blockRuntimeID), - NBTData: nbtconv.WriteItem(it, false), + NBTData: item.WriteNBT(it, false), } } @@ -967,7 +1022,7 @@ func stackToItem(br world.BlockRegistry, it protocol.ItemStack) item.Stack { t = nbter.DecodeNBT(it.NBTData).(world.Item) } st := item.NewStack(t, int(it.Count)) - return nbtconv.Item(it.NBTData, &st) + return item.ReadNBT(it.NBTData, &st) } // instanceFromItem converts an item.Stack to its network ItemInstance representation. @@ -998,16 +1053,13 @@ func stacksToIngredientItems(_ world.BlockRegistry, inputs []recipe.Item) []prot items = append(items, protocol.ItemDescriptorCount{Descriptor: &protocol.InvalidItemDescriptor{}}) continue } - rid, meta, ok := world.ItemRuntimeID(i.Item()) - if !ok { - panic("should never happen") - } - if _, ok = i.Value("variants"); ok { + name, meta := i.Item().EncodeItem() + if _, ok := i.Value("variants"); ok { meta = math.MaxInt16 // Used to indicate that the item has multiple selectable variants. } d = &protocol.DefaultItemDescriptor{ - NetworkID: int16(rid), - MetadataValue: meta, + Name: name, + MetadataValue: int32(meta), } case recipe.ItemTag: d = &protocol.ItemTagItemDescriptor{Tag: i.Tag()} @@ -1025,7 +1077,7 @@ func creativeContent(br world.BlockRegistry) ([]protocol.CreativeGroup, []protoc groups := make([]protocol.CreativeGroup, 0, len(creative.Groups())) for _, group := range creative.Groups() { groups = append(groups, protocol.CreativeGroup{ - Category: int32(group.Category.Uint8()), + Category: group.Category.Uint8(), Name: group.Name, Icon: deleteDamage(stackFromItem(br, group.Icon)), }) @@ -1102,7 +1154,7 @@ func protocolToSkin(sk protocol.Skin) (s skin.Skin, err error) { } // shapeAttachedEntityRuntimeID returns the runtime ID of the entity attached to a debug shape. -func (s *Session) shapeAttachedEntityRuntimeID(shape debug.Shape) int64 { +func (s *Session) shapeAttachedEntityRuntimeID(shape debug.Shape) uint64 { var handle *world.EntityHandle switch shape := shape.(type) { case *debug.Arrow: @@ -1129,19 +1181,19 @@ func (s *Session) shapeAttachedEntityRuntimeID(shape debug.Shape) int64 { if handle == nil { return 0 } - return int64(s.handleRuntimeID(handle)) + return s.handleRuntimeID(handle) } // debugShapeToProtocol converts a debug shape to its protocol representation. It also provides defaults // for some fields such as colour, scale and other per-shape properties. -func debugShapeToProtocol(shape debug.Shape, dim world.Dimension, attachedEntityID int64) protocol.PrimitiveShape { +func debugShapeToProtocol(shape debug.Shape, dim world.Dimension, attachedEntityID uint64) protocol.PrimitiveShape { dimID, _ := world.DimensionID(dim) ps := protocol.PrimitiveShape{ NetworkID: uint64(shape.ShapeID()), DimensionID: protocol.Option(int32(dimID)), } if attachedEntityID > 0 { - ps.AttachedToEntityID = protocol.Option(attachedEntityID) + ps.AttachedToEntityID = protocol.Option(int64(attachedEntityID)) } white := color.RGBA{R: 255, G: 255, B: 255, A: 255} switch shape := shape.(type) { @@ -1256,9 +1308,6 @@ func gameTypeFromMode(mode world.GameMode) int32 { if mode.AllowsFlying() && mode.CreativeInventory() { return packet.GameTypeCreative } - if !mode.Visible() && !mode.HasCollision() { - return packet.GameTypeSurvivalSpectator - } return packet.GameTypeSurvival } diff --git a/server/session/session.go b/server/session/session.go index cdd1a69c19..3154c241e2 100644 --- a/server/session/session.go +++ b/server/session/session.go @@ -101,6 +101,9 @@ type Session struct { viewLayer *world.ViewLayer + inputLocksMu sync.RWMutex + inputLocks uint32 + closeBackground chan struct{} br world.BlockRegistry @@ -162,6 +165,9 @@ type Config struct { JoinMessage, QuitMessage chat.Translation + // HandleStop is called once when the Session is closed. The transaction is + // nil if the Controllable could not be restored to any world, such as when + // both its current world and respawn destination closed during teardown. HandleStop func(*world.Tx, Controllable) // BlockRegistry overrides the registry used for network serialization. If nil, world.DefaultBlockRegistry is used. BlockRegistry world.BlockRegistry @@ -286,6 +292,9 @@ func (s *Session) Spawn(c Controllable, tx *world.Tx) { // Close closes the session, which in turn closes the controllable and the connection that the session // manages. Close ensures the method only runs code on the first call. +// A nil transaction may be passed for a Controllable that is no longer in any +// world; world-bound teardown (container close, chunk loader, entity removal) +// is then skipped. func (s *Session) Close(tx *world.Tx, c Controllable) { s.once.Do(func() { s.close(tx, c) @@ -295,8 +304,10 @@ func (s *Session) Close(tx *world.Tx, c Controllable) { // close closes the session, which in turn closes the controllable and the connection that the session // manages. func (s *Session) close(tx *world.Tx, c Controllable) { - c.MoveItemsToInventory() - s.closeCurrentContainer(tx, false) + if tx != nil { + c.MoveItemsToInventory() + s.closeCurrentContainer(tx, false) + } if s.viewLayer != nil { _ = s.viewLayer.Close() } @@ -308,7 +319,9 @@ func (s *Session) close(tx *world.Tx, c Controllable) { _ = s.offHand.Close() _ = s.armour.Close() - s.chunkLoader.Close(tx) + if tx != nil { + s.chunkLoader.Close(tx) + } if !s.conf.QuitMessage.Zero() { chat.Global.Writet(s.conf.QuitMessage, s.conn.IdentityData().DisplayName) @@ -317,7 +330,9 @@ func (s *Session) close(tx *world.Tx, c Controllable) { // Note: Be aware of where RemoveEntity is called. This must not be done too // early. - tx.RemoveEntity(c) + if tx != nil { + tx.RemoveEntity(c) + } _ = s.ent.Close() // This should always be called last due to the timing of the removal of @@ -348,6 +363,22 @@ func (s *Session) Latency() time.Duration { return s.conn.Latency() } +// withControllable runs f with the current Controllable on its world owner. +// It is for off-owner session goroutines; callbacks that already have a +// *world.Tx should use it directly instead. +func (s *Session) withControllable(ctx context.Context, f func(tx *world.Tx, c Controllable) error) error { + _, err := world.CallRef(ctx, world.NewEntityRef[Controllable](s.ent), func(tx *world.Tx, c Controllable) (struct{}, error) { + return struct{}{}, f(tx, c) + }) + return err +} + +// sessionOwnerStopped reports whether err means the session's player can no +// longer run owner callbacks, so session goroutines should stop quietly. +func sessionOwnerStopped(err error) bool { + return errors.Is(err, world.ErrEntityClosed) || errors.Is(err, world.ErrWorldClosed) || errors.Is(err, world.ErrTaskCancelled) +} + // ClientData returns the login.ClientData of the underlying *minecraft.Conn. func (s *Session) ClientData() login.ClientData { return s.conn.ClientData() @@ -360,24 +391,33 @@ func (s *Session) handlePackets() { // First close the Controllable. This might lead to a world change // (player might be dead while disconnecting, in which case it will // respawn first). - s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) { - _ = e.(Controllable).Close() - }) + if err := s.withControllable(context.Background(), func(_ *world.Tx, c Controllable) error { + _ = c.Close() + return nil + }); err != nil && !sessionOwnerStopped(err) { + s.conf.Log.Debug("close controllable: " + err.Error()) + } // Because the player might no longer be in the same world after // closing, we create a new transaction - s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) { - s.Close(tx, e.(Controllable)) - }) + if err := s.withControllable(context.Background(), func(tx *world.Tx, c Controllable) error { + s.Close(tx, c) + return nil + }); err != nil && !sessionOwnerStopped(err) { + s.conf.Log.Debug("close session: " + err.Error()) + } }() for { pk, err := s.conn.ReadPacket() if err != nil { return } - s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) { - err = s.handlePacket(pk, tx, e.(Controllable)) + err = s.withControllable(context.Background(), func(tx *world.Tx, c Controllable) error { + return s.handlePacket(pk, tx, c) }) if err != nil { + if sessionOwnerStopped(err) { + return + } s.conf.Log.Debug("process packet: " + err.Error()) return } @@ -396,20 +436,23 @@ func (s *Session) background() { i int ) - s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) { - co := e.(Controllable) - r = s.sendAvailableCommands(co, softEnums) - enums, enumValues = s.enums(co) - }) + if err := s.withControllable(context.Background(), func(_ *world.Tx, c Controllable) error { + r = s.sendAvailableCommands(c, softEnums) + enums, enumValues = s.enums(c) + return nil + }); err != nil { + if !sessionOwnerStopped(err) { + s.conf.Log.Debug("prepare command updates: " + err.Error()) + } + return + } t := time.NewTicker(time.Second / 20) defer t.Stop() for { select { case <-t.C: - s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) { - c := e.(Controllable) - + if err := s.withControllable(context.Background(), func(tx *world.Tx, c Controllable) error { if i++; i%20 == 0 { // Enum resending happens relatively often and frequent updates are more important than with full // command changes. Those are generally only related to permission changes, which doesn't happen often. @@ -422,7 +465,13 @@ func (s *Session) background() { } } s.sendChunks(tx, c) - }) + return nil + }); err != nil { + if !sessionOwnerStopped(err) { + s.conf.Log.Debug("update session background: " + err.Error()) + } + return + } case <-s.closeBackground: return } diff --git a/server/session/session_list.go b/server/session/session_list.go index dbd50e0c81..c72e615065 100644 --- a/server/session/session_list.go +++ b/server/session/session_list.go @@ -78,12 +78,13 @@ func (l *sessionList) sendSessionTo(s, to *Session) { to.entityMutex.Unlock() to.writePacket(&packet.PlayerList{ - ActionType: packet.PlayerListActionAdd, Entries: []protocol.PlayerListEntry{{ + ActionType: protocol.PlayerListActionAdd, UUID: s.ent.UUID(), EntityUniqueID: int64(runtimeID), Username: s.conn.IdentityData().DisplayName, XUID: s.conn.IdentityData().XUID, + BuildPlatform: int32(protocol.DeviceUnknown), Skin: skinToProtocol(s.joinSkin), }}, }) @@ -96,8 +97,10 @@ func (l *sessionList) unsendSessionFrom(s, from *Session) { from.entityMutex.Unlock() from.writePacket(&packet.PlayerList{ - ActionType: packet.PlayerListActionRemove, - Entries: []protocol.PlayerListEntry{{UUID: s.ent.UUID()}}, + Entries: []protocol.PlayerListEntry{{ + ActionType: protocol.PlayerListActionRemove, + UUID: s.ent.UUID(), + }}, }) } @@ -127,6 +130,10 @@ func skinToProtocol(s skin.Skin) protocol.Skin { if fullID == "" { fullID = uuid.New().String() } + model := s.Model + if len(model) == 0 { + model = []byte("{}") + } return protocol.Skin{ PlayFabID: s.PlayFabID, SkinID: uuid.New().String(), @@ -137,7 +144,7 @@ func skinToProtocol(s skin.Skin) protocol.Skin { CapeImageWidth: uint32(s.Cape.Bounds().Max.X), CapeImageHeight: uint32(s.Cape.Bounds().Max.Y), CapeData: s.Cape.Pix, - SkinGeometry: s.Model, + SkinGeometry: model, PersonaSkin: s.Persona, CapeID: uuid.New().String(), FullID: fullID, diff --git a/server/session/text.go b/server/session/text.go index d8418df4a8..9a4899a077 100644 --- a/server/session/text.go +++ b/server/session/text.go @@ -95,9 +95,10 @@ func (s *Session) SendScoreboard(sb *scoreboard.Scoreboard) { s.currentLines.Store(&lines) } else { // Remove all current lines from the scoreboard. We can't replace them without removing them. - pk := &packet.SetScore{ActionType: packet.ScoreboardActionRemove} + pk := &packet.SetScore{} for i := range currentLines { pk.Entries = append(pk.Entries, protocol.ScoreboardEntry{ + IdentityType: protocol.ScoreboardIdentityRemove, EntryID: int64(i), ObjectiveName: currentName, Score: int32(i), @@ -107,7 +108,7 @@ func (s *Session) SendScoreboard(sb *scoreboard.Scoreboard) { s.writePacket(pk) } } - pk := &packet.SetScore{ActionType: packet.ScoreboardActionModify} + pk := &packet.SetScore{} for k, line := range sb.Lines() { if len(line) == 0 { line = "§" + colours[k] diff --git a/server/session/view_layer.go b/server/session/view_layer.go index 632b580129..f9194a9184 100644 --- a/server/session/view_layer.go +++ b/server/session/view_layer.go @@ -24,6 +24,22 @@ func (s *Session) ViewPublicNameTag(entity world.Entity) { s.viewLayer.ViewPublicNameTag(entity) } +// ViewAlwaysShowNameTag overrides whether the entity's name tag is shown at all distances for this session. +func (s *Session) ViewAlwaysShowNameTag(entity world.Entity, alwaysShow bool) { + if s.viewLayer == nil { + return + } + s.viewLayer.ViewAlwaysShowNameTag(entity, alwaysShow) +} + +// ViewPublicAlwaysShowNameTag removes the always-show name tag override from the entity for this session. +func (s *Session) ViewPublicAlwaysShowNameTag(entity world.Entity) { + if s.viewLayer == nil { + return + } + s.viewLayer.ViewPublicAlwaysShowNameTag(entity) +} + // ViewScoreTag overwrites the public score tag of the entity and immediately refreshes it for this session. func (s *Session) ViewScoreTag(entity world.Entity, scoreTag string) { if s.viewLayer == nil { diff --git a/server/session/world.go b/server/session/world.go index d624576ded..5b7106a8ba 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -81,10 +81,12 @@ func (s *Session) ViewEntity(e world.Entity) { case Controllable: _, actualPlayer := sessions.Lookup(v.UUID()) if !actualPlayer { - s.writePacket(&packet.PlayerList{ActionType: packet.PlayerListActionAdd, Entries: []protocol.PlayerListEntry{{ + s.writePacket(&packet.PlayerList{Entries: []protocol.PlayerListEntry{{ + ActionType: protocol.PlayerListActionAdd, UUID: v.UUID(), EntityUniqueID: int64(runtimeID), Username: v.Name(), + BuildPlatform: int32(protocol.DeviceUnknown), Skin: skinToProtocol(v.Skin()), }}}) } @@ -99,6 +101,7 @@ func (s *Session) ViewEntity(e world.Entity) { UUID: v.UUID(), Username: v.Name(), Yaw: float32(yaw), + BuildPlatform: int32(protocol.DeviceUnknown), AbilityData: protocol.AbilityData{ EntityUniqueID: int64(runtimeID), Layers: []protocol.AbilityLayer{{ @@ -108,8 +111,9 @@ func (s *Session) ViewEntity(e world.Entity) { }, }) if !actualPlayer { - s.writePacket(&packet.PlayerList{ActionType: packet.PlayerListActionRemove, Entries: []protocol.PlayerListEntry{{ - UUID: v.UUID(), + s.writePacket(&packet.PlayerList{Entries: []protocol.PlayerListEntry{{ + ActionType: protocol.PlayerListActionRemove, + UUID: v.UUID(), }}}) } else { s.ViewSkin(e) @@ -152,6 +156,7 @@ func (s *Session) ViewEntity(e world.Entity) { Pitch: float32(pitch), Yaw: float32(yaw), HeadYaw: float32(yaw), + BodyYaw: float32(yaw), }) } @@ -196,11 +201,26 @@ func (s *Session) ViewEntityMovement(e world.Entity, pos mgl64.Vec3, rot cube.Ro if (id == selfEntityRuntimeID && s.moving) || s.entityHidden(e) { return } + s.viewEntityAbsoluteMovement(id, e, pos, rot, onGround, false) +} + +// ViewEntityDisplacement ... +func (s *Session) ViewEntityDisplacement(e world.Entity, pos mgl64.Vec3, rot cube.Rotation, onGround bool) { + if s.entityHidden(e) { + return + } + id := s.entityRuntimeID(e) + s.viewEntityAbsoluteMovement(id, e, pos, rot, onGround, true) +} +func (s *Session) viewEntityAbsoluteMovement(id uint64, e world.Entity, pos mgl64.Vec3, rot cube.Rotation, onGround, authoritative bool) { flags := byte(0) if onGround { flags |= packet.MoveFlagOnGround } + if authoritative { + flags |= packet.MoveFlagTeleport + } s.writePacket(&packet.MoveActorAbsolute{ EntityRuntimeID: id, Position: vec64To32(pos.Add(entityOffset(e))), @@ -259,6 +279,9 @@ func (s *Session) ViewEntityTeleport(e world.Entity, position mgl64.Vec3) { Yaw: float32(yaw), HeadYaw: float32(yaw), Mode: packet.MoveModeTeleport, + TeleportData: protocol.Option(protocol.TeleportData{ + TeleportCause: packet.TeleportCauseUnknown, + }), }) return } @@ -312,6 +335,9 @@ func (s *Session) ViewEntityArmour(e world.Entity) { } inv := armoured.Armour() + if inv == nil { + return + } // Show the entity's armour s.writePacket(&packet.MobArmourEquipment{ @@ -657,6 +683,10 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) pk.SoundType = packet.SoundEventExtinguishFire case sound.Ignite: pk.SoundType = packet.SoundEventIgnite + case sound.EnderEyePlaced: + pk.SoundType = packet.SoundEventEnderEyePlaced + case sound.EndPortalCreated: + pk.SoundType = packet.SoundEventEndPortalCreated case sound.Burning: pk.SoundType = packet.SoundEventPlayerHurtOnFire case sound.Drowning: @@ -853,6 +883,11 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) EventType: packet.LevelEventSoundTotemUsed, Position: vec64To32(pos), }) + return + case sound.ShulkerBoxClose: + pk.SoundType = packet.SoundEventShulkerBoxClosed + case sound.ShulkerBoxOpen: + pk.SoundType = packet.SoundEventShulkerBoxOpen case sound.DecoratedPotInserted: s.writePacket(&packet.PlaySound{ SoundName: "block.decorated_pot.insert", @@ -863,6 +898,14 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) return case sound.DecoratedPotInsertFailed: pk.SoundType = packet.SoundEventDecoratedPotInsertFail + case sound.Custom: + s.writePacket(&packet.PlaySound{ + SoundName: so.Name, + Position: vec64To32(pos), + Volume: float32(so.Volume), + Pitch: float32(so.Pitch), + }) + return case sound.LightningExplode: s.writePacket(&packet.PlaySound{ SoundName: "ambient.weather.lightning.impact", @@ -1084,37 +1127,26 @@ func (s *Session) entityMetadata(e world.Entity) protocol.EntityMetadata { if s.viewLayer == nil { return metadata } - if nt, ok := s.viewLayer.NameTag(e); ok { - metadata[protocol.EntityDataKeyName] = nt - if nt != "" { - metadata[protocol.EntityDataKeyAlwaysShowNameTag] = uint8(1) - if !metadata.Flag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName) { - metadata.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName) - } - if !metadata.Flag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowName) { - metadata.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowName) - } - } else { - metadata[protocol.EntityDataKeyAlwaysShowNameTag] = uint8(0) - if metadata.Flag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName) { - metadata.UnsetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName) - } - if metadata.Flag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowName) { - metadata.UnsetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowName) - } + nt, ntSet := s.viewLayer.NameTag(e) + as, asSet := s.viewLayer.AlwaysShowNameTag(e) + if ntSet || asSet { + nameTag, alwaysShow, _ := nameTagState(e) + if ntSet { + nameTag = nt + } + if asSet { + alwaysShow = as } + writeNameTagMetadata(metadata, nameTag, alwaysShow) } if st, ok := s.viewLayer.ScoreTag(e); ok { metadata[protocol.EntityDataKeyScore] = st } if visibility := s.viewLayer.Visibility(e); visibility.EnforceVisibility() { - invisibleFlag := metadata.Flag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagInvisible) - shouldForceVisible := visibility == world.EnforceVisible() && invisibleFlag - shouldForceInvisible := visibility == world.EnforceInvisible() && !invisibleFlag - if shouldForceVisible { - metadata.UnsetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagInvisible) - } else if shouldForceInvisible { + if visibility == world.EnforceInvisible() { metadata.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagInvisible) + } else { + metadata.UnsetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagInvisible) } } return metadata @@ -1259,6 +1291,10 @@ func (s *Session) ViewBlockAction(pos cube.Pos, a world.BlockAction) { EventType: packet.BlockEventChangeChestState, }) case block.StartCrackAction: + if t.BreakTime <= 0 { + // An instant break has no cracking to animate, and encoding the crack speed would divide by zero. + break + } s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventStartBlockCracking, Position: vec64To32(pos.Vec3()), @@ -1271,6 +1307,9 @@ func (s *Session) ViewBlockAction(pos cube.Pos, a world.BlockAction) { EventData: 0, }) case block.ContinueCrackAction: + if t.BreakTime <= 0 { + break + } s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventUpdateBlockCracking, Position: vec64To32(pos.Vec3()), diff --git a/server/world/biome.go b/server/world/biome.go index 9aa4e32544..3f49197d8c 100644 --- a/server/world/biome.go +++ b/server/world/biome.go @@ -77,12 +77,28 @@ func (br *BiomeRegistry) Biomes() []Biome { return bs } -// ocean returns an ocean biome. +// ocean returns the ocean biome, or an unknown biome if no biome is registered. func ocean() Biome { - o, _ := DefaultBiomes.BiomeByID(0) - return o + if o, ok := DefaultBiomes.BiomeByID(0); ok { + return o + } + return unknownBiome{} +} + +// unknownBiome is returned in place of a Biome that is not registered. It encodes back to the ID it was read from. +type unknownBiome struct { + id int } +func (unknownBiome) Temperature() float64 { return 0.5 } +func (unknownBiome) Rainfall() float64 { return 0 } +func (unknownBiome) Depth() float64 { return 0.1 } +func (unknownBiome) Scale() float64 { return 0.1 } +func (unknownBiome) WaterColour() color.RGBA { return color.RGBA{R: 0x44, G: 0xaf, B: 0xf5, A: 0xff} } +func (unknownBiome) Tags() []string { return nil } +func (unknownBiome) String() string { return "unknown" } +func (b unknownBiome) EncodeBiome() int { return b.id } + func RegisterBiome(b Biome) { DefaultBiomes.Register(b) } diff --git a/server/world/block.go b/server/world/block.go index c32a42f9dd..52034ac942 100644 --- a/server/world/block.go +++ b/server/world/block.go @@ -75,56 +75,6 @@ type Liquid interface { LiquidRemoveBlock(pos cube.Pos, tx *Tx, removed Block) } -// Conductor represents a block that can conduct a redstone signal. -type Conductor interface { - Block - // RedstoneSource returns true if the conductor is a signal source. - RedstoneSource() bool - - // WeakPower returns the weak power level emitted by this conductor toward a neighbouring receiver. - // The face argument is relative to the receiving block, not this conductor. - // Weak power can pass through a solid block to power redstone components on the other side, but - // cannot power solid blocks themselves or travel further. - // The accountForDust parameter indicates whether redstone dust should be considered when - // calculating power levels. - WeakPower(pos cube.Pos, face cube.Face, tx *Tx, accountForDust bool) int - - // StrongPower returns the strong power level emitted by this conductor toward a neighbouring - // receiver. The face argument uses the same convention as WeakPower. - // Strong power can be transmitted through solid blocks. When a solid block receives strong power - // through one of its faces, it can provide weak power to adjacent redstone components on all other - // faces. Strong power can also directly power any redstone component. - // The accountForDust parameter indicates whether redstone dust should be considered when - // calculating power levels. - StrongPower(pos cube.Pos, face cube.Face, tx *Tx, accountForDust bool) int -} - -// WeakBlockPowerer represents a conductor whose weak power may weakly power an adjacent conductive block. Weakly -// powered blocks may activate mechanisms and repeaters, but do not power adjacent redstone dust. For example, -// dust pointing into a stone block opens a door on the stone's far side, but a second stretch of dust there stays -// dark. -type WeakBlockPowerer interface { - Conductor - // WeaklyPowersBlocks returns true if this conductor's WeakPower can make an adjacent conductive block weakly powered. - WeaklyPowersBlocks() bool -} - -// RedstonePowerRelayer represents a block with custom behaviour for whether -// neighbouring redstone power may be relayed through it by Tx.RedstonePower. -type RedstonePowerRelayer interface { - Block - // RelaysRedstonePowerThrough reports whether this non-conductor block may - // relay neighbouring redstone power through itself to receivers on its other sides. - RelaysRedstonePowerThrough() bool -} - -// RedstoneUpdater represents a block that reacts to nearby redstone power changes. -type RedstoneUpdater interface { - Block - // RedstoneUpdate is called when a change in redstone signal is computed. - RedstoneUpdate(pos cube.Pos, tx *Tx) -} - // RegisterBlock registers the Block passed in the DefaultBlockRegistry. // // This function exists for backwards compatibility and works well for the common "single server per process" setup, diff --git a/server/world/block_search.go b/server/world/block_search.go new file mode 100644 index 0000000000..79a773c247 --- /dev/null +++ b/server/world/block_search.go @@ -0,0 +1,103 @@ +package world + +import ( + "errors" + "iter" + "slices" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world/chunk" + "github.com/df-mc/goleveldb/leveldb" +) + +// blocksWithin implements Tx.BlocksWithin. It must only be called during a transaction. +func (w *World) blocksWithin(pos cube.Pos, radius int, blocks ...Block) iter.Seq[cube.Pos] { + return func(yield func(cube.Pos) bool) { + if radius <= 0 || len(blocks) == 0 { + return + } + targets := make([]uint32, 0, len(blocks)) + for _, b := range blocks { + targets = append(targets, w.conf.Blocks.BlockRuntimeID(b)) + } + + // Horizontal bounds of the search: min inclusive, max exclusive. + minX, minZ := pos.X()-radius, pos.Z()-radius + maxX, maxZ := pos.X()+radius, pos.Z()+radius + + minChunk := chunkPosFromBlockPos(cube.Pos{minX, 0, minZ}) + maxChunk := chunkPosFromBlockPos(cube.Pos{maxX - 1, 0, maxZ - 1}) + var logged bool + for chunkX := minChunk.X(); chunkX <= maxChunk.X(); chunkX++ { + for chunkZ := minChunk.Z(); chunkZ <= maxChunk.Z(); chunkZ++ { + chunkPos := ChunkPos{chunkX, chunkZ} + var c *chunk.Chunk + if col, ok := w.chunks[chunkPos]; ok { + c = col.Chunk + } else { + col, err := w.conf.Provider.LoadColumn(chunkPos, w.conf.Dim) + if err != nil { + if !errors.Is(err, leveldb.ErrNotFound) && !logged { + // Log only the first error: a systemic provider failure would otherwise log once per chunk. + w.conf.Log.Error("blocks within: "+err.Error(), "X", chunkX, "Z", chunkZ) + logged = true + } + continue + } + c = col.Chunk + } + if !yieldMatchingBlocks(yield, chunkPos, c, targets, minX, minZ, maxX, maxZ) { + return + } + } + } + } +} + +// yieldMatchingBlocks yields the positions of blocks in the primary layer of a chunk that match one of the target +// runtime IDs and fall within the horizontal bounds passed. It returns false if the iteration was stopped. +func yieldMatchingBlocks(yield func(cube.Pos) bool, chunkPos ChunkPos, c *chunk.Chunk, targets []uint32, minX, minZ, maxX, maxZ int) bool { + baseX, baseZ := int(chunkPos.X())<<4, int(chunkPos.Z())<<4 + // Clip the block iteration bounds to the search area once per chunk. + x0, x1 := max(minX-baseX, 0), min(maxX-baseX, 16) + z0, z1 := max(minZ-baseZ, 0), min(maxZ-baseZ, 16) + for i, sub := range c.Sub() { + if sub.Empty() { + continue + } + layers := sub.Layers() + if len(layers) == 0 { + continue + } + storage := layers[0] + indices := matchingPaletteIndices(storage.Palette(), targets) + if len(indices) == 0 { + continue + } + baseY := int(c.SubY(int16(i))) + for x := x0; x < x1; x++ { + for z := z0; z < z1; z++ { + for y := range 16 { + if !slices.Contains(indices, storage.PaletteIndex(byte(x), byte(y), byte(z))) { + continue + } + if !yield(cube.Pos{baseX + x, baseY + y, baseZ + z}) { + return false + } + } + } + } + } + return true +} + +// matchingPaletteIndices returns the indices in the palette that hold one of the target runtime IDs. +func matchingPaletteIndices(palette *chunk.Palette, targets []uint32) []uint16 { + var indices []uint16 + for i := 0; i < palette.Len(); i++ { + if slices.Contains(targets, palette.Value(uint16(i))) { + indices = append(indices, uint16(i)) + } + } + return indices +} diff --git a/server/world/block_source.go b/server/world/block_source.go index 42d8fe5abe..4af7677ece 100644 --- a/server/world/block_source.go +++ b/server/world/block_source.go @@ -8,7 +8,7 @@ type BlockSource interface { Block(cube.Pos) Block } -// worldSource is a wrapper around a World that implements BlockSource. -type worldSource struct{ w *World } +// worldSource is a wrapper around a world transaction that implements BlockSource. +type worldSource struct{ tx *Tx } -func (w worldSource) Block(pos cube.Pos) Block { return w.w.block(pos) } +func (w worldSource) Block(pos cube.Pos) Block { return w.tx.block(pos) } diff --git a/server/world/block_states.nbt b/server/world/block_states.nbt index 1e4d94632c61146f65df32b325223d1652a8f081..7006d9d46217425aab8e7d998f70c370b6b9c4eb 100644 GIT binary patch literal 2436125 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 T=yC9}STD73 literal 2365370 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
P9JUSz?AS#m30|2Q&3xGljWVh+q3?uGq zMSul>RFDk->ILR*`AjOv1^}rb8vxV`v;d3?8=}3nqQl&@g$Sv@!jl#NQh~W$Ka&cw z0YECq1^}r*3qZIEtQ9Xp1s0xpRA8=l&!hq^0Amgy+FL7T|FQ-FGbx!OIG3bX)>Q-D-pt*Ds-urI)?jS}@bM+Fw1v;dF_vH?IUFju>0Qh^qLF$odvtrfG95Q)OV zla_?27i0r~RFDk-Qh~YJJ(CL5034qgxr1J~JkVbUk9lx8VItCs0GJ6a^N#4pk(p^< zU_q4Cfp88WJ!oI=M;ifincU^8ECSrt47COej|jk+Q>ge`D>}Rs0dQ=M@t|H{;YkYs z^@3~w5ckd1?wNW)HULNk*#IC>XaNAdz%tM4M5KbT?hx%UCJ_MEKcoV4wOgoy@MeUX zgy!vf&p?7ypcVnH86g#@0SHe4>I#WdfJC7d0WJkd1?FnEPz4+S#QkglkP5T_j4L6c zy|toKy+oA|^a8xv81YQ|0t;e_2$+l8?hHPTfKUbDg$U_E_Urv)hY;;?P(&$k2tCGw zL}B4c3jlS1YyeO%Py-N7Lev$6SG&UjAQcD!aLovb!d&kc)&ULxQb9HVNCnvdAQh+q z2qz&@f#%(E&sdJQZ|;^0Nf;ghq=&>2Kq^p+04GDF0xbYwB(u!3{ZjMSdOajW+&5Rd zWpCNA{vj1)1Aw@n4FFO>HULNk*#MwkkPQG*fw|f(Qvs+0mU&($vZ?sk3!L8}^i%|- z2hEq=Jj)^#STV{K0i*|61Qa+hQ!g-AyJafiISyi!4FKYPHULNk*#IE!X9IwGK{f!W z7nqygGpQgO0HlI!0FVl@0YECy0x&)nL$tS6bjqiw8-hY>E^hO4f^c)nZOzcWz=9}> z0Q$*j&uD`5kodG5sX!|N3LKC{Kmou^6Op;vE$jv1V>!}8;s_w_YefKXz%uWYy>aQ3 zy=60@4xp}(I0dLRSTT}SLar2}iO5{-7OH>)fViIx08&9V0H_ya1Aw@n4FFO>HULNk z=4$s$D#!)^sXz@tcsE3P(ERfOo=Fg?KrI4XB}6L71^}tRTuQVN7XLfK*`NNecjV05t&N%?Rm1_Kpdw za-;&a2yk@(^#XI#d!}BX1pw3m%RHMT;oJA&Bt&}nUM9nE07wNw02~RC3ali_BESJa zDlk{OWhw~o1=flVFVjW<^#Th|S^$XqS^&l=KxDU8%$fqE0t-*t6d)CttKBjcIMEzu z1Pw^p03a1)1AtVZ1pstI%RHMT+4BO{JW_!LQKSM80dsLX`M$vam*1xVkX>S7Q-D-p zL7Y1Uvp+A8z2d>90I5JL1uOv63(VE-nR-Ds07wPd0H9u=1prh+%RI}HnR1|WQMiu9291qZZEP>TS!IYqsI z2k`wx~0T_6AeoIH_@37|4lTS{Fm|brTA2?moH_}K9o)O z|5o*GS2oK{v2Dv{dyK&8Rs-@s-XiMVe=!)>WwBni&$nfVuIZR@yQx=?M`$3G|6;yg zZr96wTW?F|EB|4>va2`M{-Fy;`+Rq|T-VRHn^J?ZDoYRq{=vAgYO1Gwz3sr)&C&72 zO}#6(%iF4bv-s}0Pjny4;op3F_~`PX>j7o|FK&LetQF0=+IF!&DdDSq{oFo)=yN!x zEp`1;?pqRjBn)v3wsq4!SjZxAObka^qzT6~7y|)}^NTU1BEum~i^CUGVK|aXVSMGV zBNoTBv|OLFLoX}SV7TlU7o1Gt;A%%67%ms|!a3IGY%jK>q z_xtCj`#+oRZ1H&XarIqyy49ZoE%()ZcankhCoqYQ_yi`=*`L59IusO`M9*FXCece! z0+Zi>N+&Rh-VYF%M8764Fo`aX3QVF~cmk8?CsPF`f7H)7de|;NiGG|;U=lrtvi3*+ z{k*+D9BMz5o4aF?q|5ee^Za^Ozx0m^4#&t>ACLdk8(OxXcO|N8+cx29XJVzax$+^zrsF#ZtAC5g(PDZBO$DX%z zG(3Rfxb9nt)SIb*&)_$rSY-o*vJce7_zt zo~b@=ahg}Yb$s+pQ`3k2?wOOm#UoR$<9VdUCok?Ldt_19Sv*(Hy`JaFxp(m-IUkN$ zSh6QcdEnYfG9I{gWWvwtx_AG>)%C-%c#6_G_S?&~O!@Y%ohjemwIlOQm$tx|BYpqT zoL?UI?v4)HqPx?%Ufyqt{eHPC*b8>noxJ-^{StB~ z?-#T3aCj>`-Ho+}h#PAUkulaDB3P_FbfOz;4;|;m+Cyi#vG&kGZmd0YiW_SW9pT2> zL+7`#_R!&NtUYvc8*2|8+s4{MXST8S(1Gp$HuZ0~sa<~B)%(Le?dSG*SNl+hoBi<& z_we|CU#-jT;_u6F)%WVdOIbDR+T7hmBS4zfQ0+{!8mg?*R~yIe-8i^lkej~R$aMF6 zQ5o~*SuJ7SJge#DEhLhtU;fSL?V-E!zI=J8T4&NZ3JqlbM`PjXdA+=Cs@0>$s{`?N ziNWqa4cPPY(vNxD!|Py8y*_$8eoOw%`ECe=M7qk`EUR2+S>;`VRnAS0vxJ5@X9Jauo>nL_&Usjl|zb5s4+jdI0P{k(0_Og7OBV(vsUs2fi-gWBvw zGpK+~G=nyw6V0H#=R`AT%W1tMxIVo2c-wTxEU#}qz7(6y@~$|&mwENw@J8h6tvtjc zkx|4ckx|4jkx|6Ae>GY>Y@7}+y6!tYhMYiIZ`pQ<*qmmeZx8>(Z>a*mjzS#xbrj6N zucNR9ejUj(@assHfnP^*4E#EhVc^%%K~Lb<(Yel_4PkFL#p-c5{5;72NWhyv_r+jU zZ@c&E4?ow?-@RHM9xhyOyO*7Be)9hJ{s}1cf&Tvd@B#k{himNpe_#A_ze0B&(H#hP zcfr5}5bX+DXp!D^ZC8S_^40#Kdz~2M?Q+sGqc3RGil$hQFV}4+yNgdt8npzuomnuDnw4|H>`p;Cnp9h=Lp!RzRtsObeZHq)jz_>XagII13Ap zc4oo`k}zyeyvR~`(BK#33!D3BUnT_YH4A=;nGuBA{)Z(p+8IKu*}yvijqfpzA&Snt zbL1x`be-tbINnf%Cb3?0HJcDfCmyow_R~>xYJgx9mS#TtX`D$jc@b4dm>B;E7_^U} zCW3gH##b{gNE8Xlf)MY_rEV}^-SsCw+g=7OL{%zkmo}xG#H-&Qv@peRyWh*mOVG}P zRsWDFf3#rc92D=1KmS1t<7;wEGz9&Hq8O*_ z7^rOGqSNAxhAfw<);W0Qk4-pdwOpZMMiOdh9)uh+_yWQlzVd{;1_mQKFJKyjl^=Zm zP$_t*sB93Xo+#!En$?IhnEqmO#oEY%2W>>%*J|E1UWFy!d^Ik1EJQP@;P| z#k+zFGOtsDgVC`N^cE^B{cz>?^}zLkfllclix@GjnbMIYEkP@jMJpy4zn8oWnbuAW zA{v(ig-k zhv=~JhM!7_+N_}& z?J1L-5Qs#YVjtf#{nadJ+d&ox6K7CjaKZdNIrF6`hG3b=s#&c32k>w zps4JHq668$>C<)xu0-`4(z}7Z+=wPjj?)~eilhdX%~gDE)Jrs#J*q>Q(i88@)HV&` z+IL%mMg}SnV&WpehLI$m1rGR+m8qSYph&0kCe4(_@`m`pJgM=QfGNdvDvzk5*^^tF zXPD6ddQVJl`#^1e`cw*i@h8Su2H2sqWY~k8{W9{x7Yt!y{Cy^fGlAG9r#3z_ewKyF zf9OR1a6lT86T*Rp(Y{QW%_!|^`X22IV8DT7B!|L=leonFo}5Y2{rSw0pNKE*f#vvKiWB4hP*me!R^&jwS^e|y3 zDP$+Zj;o}e2u}hFpJ?_63`4GnvKCMpWWM-oZa5({^ENn}q>8bWVU^!oVASCRLog~| zQd*J-KWyr!s#-Khzl^ky$~R>$g6r9rti_dP?etByYN*kE^)snXA&uf zn_%Js&yiu4#IS`d%2z>N{ADO`N+vYbSLG;Y1dZR{EHz8hglaiBpMqttVI{mJKQT*c z{5(VLj_N|BZ+))GPgvf^=f)p;%QPrH$Y1Jolk{fb_Mw1AJ2RQm?1%_HFd;nA&VaZd z>KRpf6PAeIOF8SqH%nef0Zbs3Lv%r6{KYJYWQm+np8#Si6|b@08m^Y=r5cB+m!85)lD>IDj z`kb!=1>=43rzMDCB>ST&mZXYNHOlBzeVm&mEtIe7S^43q+Lo)PxNHBhZ@ZVWmbXo{ zdfdN%w{MHK+=Br8&3x;r-tRvzSM}}frn{$lu+V+8YEBjw-#zd4)w*1+>zD2Fwrby8 zeRwIWW?e7WRa36os@|fiq`b`AJl|1WWfpmI<5?y}ZamAR$c<+il$-x$bn{R& z>*Z5%UvAr?m&~?YFYh$2?xR5#0}s5VE*F8=xOPs?@D+3n@_?eNc> z*raw*2BmgU%A|Htj-+;PBJ+0THWAK0 zTlJ^fJ}%p`X^K_dwgpqTelcHM7tQ0cE*`tR%5%4!S?)K*?eW_;RzLi;Y}#s7Y?cqj z=I-5Xw@Cd2QX;i})z&-SbhW+fHeX%Ewf130;NaUo8Mk+bc&&=N`>H=U%eLJ0;Y<>&_7{_crps5i4gRhBpK%Gl`fyjQs_nhW5Nd_-7LlIg zEh0t7TSS_Uw}^Hr@fOkkJs&#e9_p0t?x_anTa4g>feQGX5DN}PDQj?ks zQECiWr|Y+;3QjZdc{7o0gfC(`lJm z*v&E;xWb4{hu6vLo3k=IUzTMnGcF%SeyeQE<#N9luyBAuJuJ)TPA~yDYzz2>|7!ZO z=ZUlYoJMY+IH~y$1OB#nx~=PF=R;Ag6wjGdmLOcPj(e;#CXEv^iDd%tk0$1;rh3}f z+vWE2rh0xl&TxR?*7x>p^So-Gn{xSU%|1b~65u_2xGl_`2(Yw$g!$a;4ySSTcGYwb zc^S;?+RvNQIp%PvxA=Njzm$i)HZhS-HoHh;w(A2HT2zgNXp!0fGCVpx*Sg(w2bR`# z84VfetwV_7UyU1q{%-fJUDvm|r&q;p z)18$bd&lAr-DM-H+=p90wbH5quWpQwa6#|;)>NkbwthK+4)}EU+=}_M1#4Y&Gr;{r z+dXRBS%Kpc)ThDaM_46!uI7@}pAF?5I=WelAkM;ZGD5Gl&o zx0=R2Xd3&dY3zGVV?Ssb`%%-_PnyPl)-?7DFlO&nk56v9r<&XD;pP2u-&L5T~r*Uc2S*_+C?Q(Y8O>csa;e^ zrFKzGmD)w+RcaSiTB%)BbftC!>Th>MRCN!n4>$Rvuh3X9rEN2<6CW!YBet>ldb0Eq|$0r!ZxN3 z+HQxl#7jOU-^O^ooJ_Wwmy>V&oJ8mE;gR%mQfW1Sc^qIbRndeO?%R|)DUYO=ler`5 zrRvm3PEdFEe9PUCOFi7T>Y2v`1( z$;xU|9bP$Z%Kv%pZl*4G-BqXZq28>!_vb>WY#kN7UUlq#31D+~I@$?%-R|tR*PE-4 z$Gcy>p=JAdSE62%WpVVBE6d{OQCOD6(EzW#_+rkSdl%1@bMNB0a_(I`GUwh5G8e)7 zoACBc&m=M*UZgl%#s@Y@#sk+*lJLN_BlA5qwO+1e%C~pzO!@Y%ohjemwIlN#(4%Qe zgg(Ra4G~_whAelE8VuvIZZ^C}!VA=ed2*;yuul~=>&;-#FGwEGe?UdV*e3tbNQ5bL92{W^9Sd8gZl4c7mHql_ zWg$mu)*C|RcitYVeYrWDIEMddw{?mw5KfZzR(h)Jp>^)X8W znN6cKOD>N^$|C-f#PDYULXsx+Xnqfzn4+OeLrEm-jF!niI}wr;$E8$C3TKG}h|)=5 zjKsWCuHe+)Y~fuRJ|XrA*YHpX+SC)P$zhHx%=}+M2~n3gP0G|0s_D=M61kMHq)~lC z#UrdGibd%>7Apxv49>{l^Zi6fj1E)AD=F=q;y?+3O&VDt!#ka_OH{pMU7|ja&m#$s zbUbirBFzzKl406^<(a>{M2Ydxv&1eoDRWlxZ%L_Uf04@{txGr5#fNxVzsC!(nuvqe90wGnO2li!a|ftNlApldX!vAfe66Ex9FhAmE`AL*miDk z+FxGaUWll7y*)AS{CL8Shf>U};v6|!bkVawet+BV5B)JbGFJ%=ZTmf5>)`I zE+#K~q)(wc-x&7EGbLhQk}5H?qLTQOGEeL#LutBmMx&Hzn#m<`mfR3=yBaVDsa6c} z&MAK_XOAhwKH;jzQ!hz@m{(0nToS7ZsK@L#r^t;M{=yCblMfM8nVtf1YS>ccN$C;i z>ulv$EapO@5&;(^YZga7a(?XhCKtdyu1H)~qJr2*6?-e}m!v3?Dk;l^yh9O#1XcSa zi2#{12L_4Cgjql~;$4=eqKY3+Sn?BA_GisiqEaDOmA^5D5ipTc;)!C)U`Mni)p96i zpTGQB{07A;N_A9bO<1HpY33=k3b0X9c8VPAIPzVLQy^;nl*BnfaY;a5dFk^jYM?ja zlmIhJ1}XWdae*C@hD7-r0<0p`!Fj*pm**0hrNdq2<5J2jk^R%mF+d#xJtkDsTt12OG-2A~eI!3IA#u_~ zIZjcngM{3Zk^DqPr--^oLJS+7E~@asxki@Buu`c^K5`Tz23pNRD0PXT9u-qf7hFlt z=@Kco2^GQdNj^ppd#6O0DM9ws5a)^TiQ=9h4IJCWE)mlnTM&p3#Jikxil8>Ufd!+V z^8Ge7THXh8-{KSfdM5;#9dS0qJI7^h4IhXi)7?u{tC%KmVOzNc&g^281VNsfudR)be3d-*+ zO??qfm?csynng}sP5z=9IEWMK5a@B%Jdv4s%9aSx zho~%zlXP(|iR&F`wS94jiZRu6xU}~Rijzci+QB3L2clhOrIyH}XuwppBbBFD?wtBd zg)k72n8@NR`HMqL%i&!nKVgWG$fvLqh(L^>V4%2tQ5BC2f#Ntcje0`05fh#w^OTD; zRr9E>pjYyy@*I3V?70-tY4vjC6me0ANu?ObASkC|k+9&gEKzd7iK@Kb`HMpskAk$$ zCB^YbeBA2TqskeZH0lFl+Wq`gZOc_t+_nGMmhD5?EN`1?^|-t%x2y8~yM0@< zd-L60wS8Q+&zow;eCMP2PF*}M_nYGO_>G%Cng8}{*|gQF*eoB4&E31(x^3$xfFj1? zRa@_XxxW|?`?jfeyK=qU6`OK-fV-*hPiVj1Rhv!GEQ?Roe(_n=+9y?OzX5CiW;|V& z<*rLbTQ1wGb5$WLLgPe&elf6n)~)L8swq3@yZU8aHplz`r!9`1vZ~;2v{}N@KO0Ba zWwGw$bz3f1byI8@AOF>S@m13=Mbj>CH}&d~`Kq<%uBrAEa^Le?h>l1$$-yZ&H`A}`|%jIp=zPYhV^{Qyr)wU~->#8YN zZB=i->N~;Wrrwp?{^N`9p1a!BmGeW^`+oxzp$g;rFy`o^V0`Z}iuF7!FlD&~w1JK4->EC|qW|88HJ*_IYbY915CoUpYkZ z_&(xr95dWHycpS~kvd#KTN64Z^b#%+#(W%ujtd4+Gsa8Mpf9Lq#1T}+15b|PQsT@R z3p(m9RmQrBBd8k;21cSeVz6I4iXz*6dXpGFl-Cr72P!17+@BMruuPnE{b-$ZHPzF;-Yy?L?;Z-n zilD>viDu9#`b0D6`22qvr*`$G+COxBzugq8$9>!V1j4d=a#A(><8kX@M%!2GvOD5? z8P4afKD?Awv#yN<(5$Gw&|8px^@Xl;E%YwQLW^8E@+_1lN1lb!MlZO$g;0b;5tduBEo|O{i$+Hs5ll6SG zdw#d+o)L9Vu{ULLcm4p7-Y>ik=I+@g%2>)Z(GEam&0+G->ZX6mq+Y)6EV8-@)=cW> zuyx7mrhiG~rTsklI5X~_H+RpC;=CBV$auVYJDyX)WEH%~*y4CuR5Vo=LG?0tkumQ6 z2r;IEK%w5I~h{C}-^viQ_} z$(kyMiSoYbZaN)zX26Pdp=ZBqo>$!+jOXqW+V=Rh#mC{Fm%FCi?+;tQ?uy3M2lrS5 zHB2S*hzXqeG0cF~_=3TdcbC(PH|2(+MWf@z?|t=xFRGQ-W8o=Y4Hc?9Q&Yoo)h*o9LUX ze>&Z3Lj^Qo3Cqe`=N8sk&(>MuI{gyw5-mZQU06{R1j;jUm>k>L0Yx<+-Q*H>b_7ch zq_qQu%2R+BcDPMm`iC$HNNYtt)e=_ZQ!QcXBGnRas9d04IxXE~E_9Y4bD^^YnG3YU z2h%TZi+!8#!=&q4;M|Rz} z4^NQvuCZ&ZKs?^FA}}ByULn%Mudo0C-+JR`Q{6utUX*z5UWDmx%#iQa#M$k8` zw=FhRyX>A`3_;iwPu$B;)=M(44?FJbP5E?SPis9gw|2wm;y=Kqu>ke-T*E&3tXpG& z{)i3)$YNG6-ahqHP8qQ;ua6qJvG(M{`&`{=QF(p1O9JYe#O~W?^lq%g zDj8mpVd_Sc#4trPkzL}}m{p*MgYSpu&Ew%MzWURV3?So{7+)KL-uxxAO^jZayZ`;X zz3&?5%i)RjzPgX-4^hTYi-kdujIgmbL2Raj$g0;B@wbWXS8&E zaMYFTTf~P|Kh2>l$7p$SV7&}OldzyK5AWRC4D+t6uVzoQy(?$Qvv=i~Jo}i+)$IQD zVmkx+X&x@vo~_|Ss-NY+h4qshxNuC4X)TWz_L3_{j=c-#$gy|f969zbjB;$f!nm&L zrs{tEPVY4-72_yZD#lShRE+Dzyu11r`i|#x_|?1j&%5>EcPxp+H<3}aDVb_?u}FaQ%qT)?JxjW4 z>igw>SG=4~+d-18)4|u9sl;8*^Isiwe~N(_DTWu%;$S$xWW=mA?8(k=^qoV`f#P+R z)Q3S?oS2h-F31uL=SM92+kE+R;xI!E#VI8oixX2a&?YV?qxa(`u04#2#+f)2)5Plj zP)yX1_xMWJG#CyuWI_@%K^rJvrNj?9$DYQ{wP)tjS%Z#zMB7-pvhQkas z6w;KqT!{>ZQTt&qJS)jjBvGju%S{KIlzY+v8o(7QINwF+^8(z=#^_}Q#|@O4q^ z0_*9|+`Rr3iphWECdL+e87syXdNa#9nmoRStokDkTJ{6ZZAJJ4j>|>%(dn&Le9+}p zE&J#`D{J5L?#h(kT8OYe>9-tM`(ChPr&Rgl`8@$S)?x=x!_mgA<+ryca-KwFG3}!{XMnn}$aUh(@B10h9 z8W9x;r+qTnaPmK{5g9PJjF<%kTO)D=!ePX?u*4K9>Db`TAu$+6HfT^TBXj~`e*4N} zZz95;E)g|?@!KK5SP8+;amcQrTq9;+geNlU*DkpE*| zDE9qvmf;7g^sY=NS`HlyM=Td|#TQlTh~-Athr8cKcJD%?V?T}MJh!7$dgobAF?XY2 zdX__Hw26X=a%cS*+EcN;Z|YyWUs7{^`qlpvmwjsq`iaYlYgoUlsJn*K7SFL;PNLLI z^uzjPMO{BEsMgP1j@LllD#Q;eR`xHaBGrVfVfkVG(50>))*56!M6v3&WROf&h9qlP zepveGhlBH_!Eg|tk7R3D`Osa%Q+~~F7iZ3gQqC=XjAJEPgN&7(A;2r^<&^(>#g8#ETktfD$5vx+*2 z%qr^TGOOsJtIR5T)GD+34Pc|p>bFU(en?{VV-l<1C$aiN601KZvHDXIt3M~P`U|jn z_WRgPQ*MsG%r(9#Vs%4QS0`ArCKCl~R?kzgX7w@!Yd6+>EYHGwRclsG(PR*p)5+G1 zTb@fes5ef)X61A;wy_qhoGMtea*C{3*Nt8;wGJcHQLbS>ra#RJy2d%p3cA2)y(qS- zyI&FPeiOfHPQO14gkmkddZ?S{{qk;e_(jHJ7_P3mU%p0~4f-+~?t;FIcpCI&G^z!C z8BJtCUq*wM^;bCSraSSO_S5IqA0~KtIR1*o>F*{icg2?b9dv6%>;9npwzw_2AIojJ z-x*i@^9ed@5VyGh?m4YKd82n9U2Pe{6-%R;-08S9H_*UDi=0pg zNh-zSq@xa;j+y+JAs?itm!XeSOLDSK)f&>RDfKU^UO7OiZq5U}Rw-ane#n%rg4+qD zx{;OK#p zu1w%slqaB|9J&aJOBJ~jPSrjV=1Cg4qh~$z@sAq~QHqoti{qBRZcU)H6ADUal%u8| zT!tT1VN2-;F)_;bQ?KR1zXmGtaS8#7K#PdNO;$mR%9_j?_)4V2T6bbBSP6Y0ey}-> z@?%Q9AqFo~Z$RJg^EW7+(MyyY6FEV~Qpe3GH#D=s%lP+^jzB-7@&RZScH+QQv!L+1 zjQD-HF~_`1$U3|@rA`M)X2l}w_@SB)G?Y+{nD?9fgh^nOKHO*!vL|8m2_ipXfPytn z!HaT)GwL`h%Lp~M8$WaSLA7%-9bscf?Yif*fCgQSUJ zv!cM_(*Z#AQwcG2XjUhe5pnrob^^{KEVsedBe}@^ZUm)+l1nt}RJ(cDtji8p{RjMk zI<&k&es01wSv8{{`STG_&AyC;ByuEwMtGrr83{?_hk8Cp-UQAevnK==Mrr`l@?5+2 z^0>%P49zfOO8k?1JPcXRhS+H41d@vyv&a+$&RK^XW^qIo@vuUt=}6eX*h@g0oRJTu z{4f%hS1P4_!m{UsR78BWh)jrozz!l-B1hCaJEhAB6$tF1aaD|CmR}+9ykG^u?t$a{ zAc#d2q_9fx$~^xP+6nQmqO&=Sb3|VuTt;}Q$9WQ2v`2nmd?XIGv9$B)89uftEN+~HRQ_b^dgal?N53&xm9+#2{ z8X<8{sjjSNQ8{ruk!{T9l)K}IkmKAzT$t{nG6M1(v>%~XZ}=(z{yd#TTyOMtRjLPTA#kibOe;I5WnK`L5=Srt2(?S8xWV^UM&~lhD=_E`7F+j ziIs#<6G^HGRUn5SRJ1SAkFZ@ji9Ec_x&qTo$efO-c5~QkWeQ<8T8D}jl@KSZLInij z1fe*EF3JyJRB1mL(}3dK@VCJUbrJ;P6x|647qmgW{y}X;rc7gT>G1mz#Huu9H?CSa zNhgORLc;?Mi4TN#6U}r)a~o_uCb*fy4KiEoC`){x&(=UwO+R{d5X*)Yvsd}}1qd^q z-49;nQI13&mH4}dm1oWFz@>ODW^wf?XF~k+4O2)GKjQiaD3tNUqG}o`DN{VOr5?Df zdMF|@f|sw9BthC5Hl%KPA+_9?z{WAS0CH(z2z^ENY*h(hpp< z*i{orJ#x~zJV)Xbl67bJnnHi=QnM2ux^lb;0sDe8b4-G2X|UM zWYpiG5Uo^;6NSviMZ(7oP)i8fxp4qOIS6 zt-lz}cgkNqb}nwq_MvQ+z4lM42Veime0^8Hluh^brriDicx*D_6{2vU0_| zr7MT}Y?;ax^Omk$F?Y$z74w#^9GYKcDp$-~x^l(bB`b%#{gse9~CTRT%juF#&EldB%f`6>CRYv!0b85we= zL3J`U_sc>UP!a_p@!YK2Z#8VLx+Lwh#B)_sjJ&C$BP= zFJq(qie+qbXbr|j`vw4PdIzL-)!w)3!)(4;?+?3&`C)TfZ7+U2=ER$42=R9`A~b|X z8$vztmyUIdw%zVm-PS)lELSaeX!y!~aDTtq9PT>CcQ1CIISWeid-tg)sT}+8?XLNL zykI=&y@Bqy(j9&_heN}F_>BXx`LJ9pf9Pa7-*hL#-R!Vywu_A;M|2oJ^XYPawb{1A z;}a0?2v?rrH=f}bfvKy@damBqb9F|!LT!9Rt)ilwgX-0*l|fWq3}mfrU3s-SkMfH4 zuQ9bci8Nu}3mNt75w1LIo~{yZO=;oPsacwHR1tx#)fIQwE zVICe<%k48x@F5R(A zJVol0@Pq2(omcLg_3du+-SCva=B=!abyN%9daS$4Se#%4@YZ8fu|LDSC#s6nAi$om zF3)?EnS^!3d>ia2@7`OzxVk1Vf}vHT`OV) z<7uor%*+nWwf7c=eGYRM~y1FVWwR{Ts>_s9O5c(>Q@iyGrm^$Q1tE|+rFW{Wgx?C$A zpXl{crwj)2noeF98N;vs0uLXuci`GTtZ%ya9`17b@v+MB^2YUQ)4c~e9z;Mx7-a+{ z-+RfmU9R8Hwr%${J?j1Z^@yd%?fbAP?rba(UmcR92D^47giIlbXI|Fr3@uy-$A;5(IzpLb`B zhxMX2z`tuT2k>TU0GDL|aEX75+_JCdGE2Y#xSATkyQu-30RV4ml>l+S8UV!oY5B(1RA7|>Ha?_+%c)gh1|WYLB0Zotuz4Ja-$x_bdm#sP1DkgqfWTIa z2h9QxPgwv+1=Ro`6}&Z4L7a2c{5_9B-Bcw2oe+DA5$jao5*dUQJ3{h6Hx=?!5c@&2 z$4OC@fQ=iez{67(08+t~D_&UuNEFoopb20F0LaiY&$2|Rg7kTB3;^Xuls}G<9&`ZW zK7eTd&e1-oib@4B07Uz001)j90f2dpQ-fv!y7!eg3xaaSZqaD)5~AV&3jlH73IJ$C zpN=REdNXcR0wf9#qOJt_jR?sDJrFm_0OGz?1`If$Y{lte&*V#GQX1i{4S(Jb&FsxrU=K%)5Ei9!V+ z4jIwj^BADk0Z4NIdy5h4PRF1Xj{%@N7X;ai<3k&f2T@f5764K~H2_EjUl^%?%>WXG z6#zg&&pZpUhziC4m`DX4M3o8*N}xL#0RUe*jm}n!iDna}}0FnJ$SS=L?fC{j;81Y0Z@F1!(z^WW^-wHsSQPliBj{)kk z89=kZ!&4Rj62-5aC{zG|3Ow^czT=uHRggao(Ny@ADg$hMNCjSw$^t;5s0IM3zzRUj zDMb6T6YbA~x~WPK13-u1LI8qbVmyfZ9v)Q&U|wS=NDs=#bkBVtENlz_=|KsAI~gGr zn3aG7fJEW0cB7vKFj~uzD9ixla{%cv;0(ZAuCquB3dFEM&g)@Lu;6c=JK&paj0FWrG000iSi`)E^EIbX73OtBq zIe;cWH2_Ej)c~MbPz?Z5K{Wtq0=V1fMOv|Eo^8d27ZRib4`NvvA{A5vfK*@w0JLJy zJj(&y4GPvgQh~eLty2LD0L=m`0H6e(c^2YC8<7XGtPGI~tN_GoFlzpu$Dk@@Gk{d! zE`RG(5N80<-t$;B185d_c*>Rl&4OwGkP55-#2i2>@I0DwApZ#o(u3}HeGp8X5F`qB zw_GPf764K~H2_Ej)c_zBR0DuSVFe&&9U{BC%N-t=Rh=*ffM{O`z=+57NDrT@GQi3Z zslZD}SpY}{RsdoSAliE#gS3^+01}0}TVA*oBT<+E$e)Ht1z+e&kPiUqK?y+mq|~eg z8~`MWY5bGqSJtf#}Gi`AALm|k-OThI~RaH;F*7!?*v5UJ)-A*;EZ*I^q|WC z>lUfN%TZYlAW>8UfK*To08)Xw>8;~H+=$Q%0?%VWoU$B1-1qR51%Omw1|Yu=AU!A> z(TkNV(dUo~%rd|MKq_!oyLBoE2%RqqD*&-aM0?L;fO;AgczDVJK-{ke0I8rF0HgwU zwOgrz{6>U^gz}f!@&O=u=m3D7&`WTjxmdS40IYeW0uN$Y86p)_1AtWEE`KXkzzrkB zeJcQfgr0epCAw!s2~v;7q#|Jq0MQ;NMFrq=m5V=`Aw5|B6ii|zLn?4Ly(dyZH2`P= zSOEZ(_sp|0oM;w!5OpfxIw9hIH2_Ej?xwd=1^EjJnhKWxd_bZPAQhNpfa3r<9ajT@ zMng3KNCj2^;%SIz@9uJkbE|A0K-~B6lm&oPPz?Z5K{WtK1=Ro`6<7fPXK3p|Lb46qeODyRkkalaY>Gz+Q$Kq{yP0L=n- zw|t^mPz?Z5K{WtK1=Ro`6;uO&R8S27Qh~eLty2LQMxJ@LS?gZk8vxL~FBtK(9O*&% z{et|J4Cz4$fZK|Z3OtsS-3O2g++BB_3V4nKnpy$i&9e}dD&RPPxc`bWFwwcdDg$hV zkqX@1@`+SX4FKARssTVMs0IK{04o5X5qaiWnkv6vkUtHP9(2Fn4>I6xj`PR!A{)^d z029pu529`#$mamkgN_3{$9a~hGLU%u4?McN+`;$Xl;5&>EV{G52Q~jv z8IW#79v&43SP~-cR|9~~1y%q6<=w??o}4-ovL!&=_aLe`V2}ajEXeNzXg4Y{4ZQ@j zMVjbb;6YSzfQ=9BLGEg|au#r%5UHRV05lq^0YKch0s#7eXP#w=QU&>a08IsDBjQMi zRN$_5Po#os0FVmI0OWH3>0zQfrDhr6NQk&!4FFPsyJ|j>3d{iHlMv}a*$FugAnsdb zAU+wPgMjBT7!PF^5~KnTPgwv+1@3D1L@KBT0CB$>0Hgvl0Qr3Y$wT>|lIsIV1y&gV zJE6O{UHo*MUjlSOnEd+%h5GqIa96tva{zI_8UQp4ssTVM zs0IM3pc(+A0y6;ltr+RS^4I$lU-d#NaCgfK%Mhu+3IG_#p7~%0kN5bAPRAZZ-7v~0 zA<{!(5+d%qtKB*h0uFfQSq|u)mb2!O3OtA^4j5!$A{AI=fGq*yz7+sa0(Wt{^tm8z zY^eETOezwxB|s|h@RS9BHX|lN)Qk_Ule8;kUk@-1^{v2UG3IwL~IGrXs8AN%>pw3`Hcvvpz!s6 zG#Vzz0NRZTX8@_dUG1Jo1=Ro`6;uO&xL*wbQh^zO{62tGp!)|D*j9{G;4Xhpq=ISy zkP6HIT58eOSU;Sje zT&+k_3W`(t$JZx z`JZWai{-jmIgt$A=lp83Z3i6zVfsc)9QUn$0>488pTP0uaTHD9H>@y{Vd)B2^BY#U zn&Y6tc7D%mYS^`c7kz}mKy7YW%wj_9*Dg;zFR__5EiTGZ8fN^K(=g*W=+gBl9n`y; z<4g^I;qR%<;`(8KIxhh({=t3XvH8%QARpW2aMyObaks-hTz4Dp{NXTrTC%&7<@NKb zd-V(V$?w|TVL9*S_g%BPd3(J%95(mW{1L~B-#Uof&1$*7>!P||HS_nw-haJYcK_e! zfB<0c5PD3?yjd?+r{luKkB0xeKRdtZjudFM&$IDHZe#f52@P5WNgROLDPNNFS_<1Tx zjtd$y*-uPs5-x8>C5ckX#M=)D|Ev;{8jVTYWE`KR7eH}hM%;zY$7YIpm~NYl;NTW9 zLs*hiGGdvGP6oAC*e2s8GZN5cJ!5184Q7WG3|gFQi-^l+k__gY9UkywZOKL}lZ=-{ zNH`4zEtE;X3@#Y~GZMH&HDQv$wTQE6k`YXkSSB)NM8PoGc}bOtP(+V*qds0I884|~ zmW*gm7+IvKi7kFJ7v|o=i9U)Rer9|WJ@fP)bKUBQGI=`IneHhE(&4?6>n^Qz&%&Df_Hg(8{xG|K zSUf*0yYikN_xFB|m%!S~~UcMCHrglytMtW z-RzI=UWTh)lRP&q0w@$I(-mstx%ibIg+yl!`k)22bZ=A}duM)+(BY z48Wp2hw&Zq#5_6UY86bYiP7*qN6DO#$>DmA4vl)P2^S@0Lna;}jaHcX*@}}D+jDfu zp=%X36n-nxp@|Al!{d8SRXA+VQKBJu98L8sTA7Y=#$!Mw>rpp~OyWKV4wWsH(7oT76x+}}wqn_<PUI?S5mt=l54hKS+D}Am_ydlPLF=6 z$P9xRy>b4}56$lIgW+$0S=i1v6o_$W92Jtb<6o6v+ufJnLO=`7eBk*4Tsz9s#zaq#4Yc!c2FM2+Ce!RYsV@-=tw&j z4?5J2wS$heW9^`W?N~eLXgk&pI^2%6gO0aj?VtnhSUc#5JJt?5qUl`Zj z8^r!`%xv4NPv4sYHvZB*lj(nM=DKmeISW4G{n+i}-SW`Rx@Xb5X5RfaSM&Tm8t_T~ z&Fo<}eBEi&yq`U8Hts7pSwAa1-V*v%>G77(k4k%%R?FMF?u$jMhwewtj<0kNOk%v0 z9`kY6tZ$Ex`S;7)_46lR{&soy>%Za8Q=^}t=Px?sct2g1U~!%ky=-qhFV=;~o7oBXztZ&bv!33f9FEL)7-%1@ZumFoW~pAHTz{Vz}tM zlHolFBesn=@8Ofj&U#0juj3tYzK(aqd3P~N!Fe|GNCI`dqds59JK}sD?}+ntyd%z= zcn7`CU7b5)c{v5=J$&+BXAt`oecmK?wyKEpCf*G)kDi=aOjy+CuiRZ*IqTTWqds59 zJL>awyrVvE;yqra5Xn7vLAeUn9rbw+OhNC6^Y13*d>!wI^L4x<&e!pdBw*ql^m)&A zAg@@WiasA3LnQyykvssTP@Mj`4SK6%G2;;Dy;M|*9kE{#dt7%!axYl3x&zK*S20`- z=YQV4%Y9fc`UjZqpI-V55*no)g*b@{6L@ePw2qOlA_IVGCJa*PQ zT6Ml~swn6koncM9gCcvj+3aYZ+vF<_i2>be$xs{{8CrF`?8xh$Pjb&)09o$cF*h(7 z!$Ryw_kb)ik2wFO!>x#2y4#p|2a5A-vlT%%?<_KpIPc+;XFetuYV4l7ARP+t>3#ky z$Emw5k4c0$@3{*g%j+H(iou}n{_Z&rx?=w7YGltB_Oi5*PXZycj}RZPHn51-t-LF|e?Z{U53-Nq#I zET<6XO}ytf_HUsKwy`JDIM1yn5S^X?w?6r8W) z9Z8^$cO(HD@A*>(vA^U~BgAx@*rl9zH<_p4yoq;kmho(}-LPc0L7eySDLO4zcf|BM z-Vx_byo0*Co6P+5nc@uV;VW2o)aUDXN1U(Y9dW*ncf@%c@A)$hZ8@4--07PS?jAL- zIL$dOKX$}^MeHMXTEu>hcWIyZYNO~j`I$%TFBv;xy1QvT#cor_J7Rhr?}+JjyrVvE z<6X~r^vi7)&#VyV-A!f<=ksGnPc2Pvup^Rt!3HaSm0pL~uZVrbltJvT;~n*Rh4=U& z6(YI2$s8^m^18>~5w%Nr59k|%K@#x5C}L0Vb)Ib&U&$vYi0K|adF*Uti0O5_BMG>R z&6+;XZIws@b-W|a+j!5Paj4Ii{N@7Uyh-eFdqE`k+yz1@s5r!Vcd=Q+`TV*g_M=~% zwb<(r=WSx=^p2Qr<2^s~i2a((=X*zDPk#Mbct@PC;~jC{#CtyHJ$H7T*Y|n! z3+@^79lVor_msn?9w%#xcTyg{f_g`suj3tY-o$%cM%3p$cXpiTdL8QX9vFGu0q5O4 z<=4r151%6T=+`z7$vt=V>W(%;WNhXYzv;xm6~vj(61d z>v%_;H}MWQ@7ZQ+qu@Mi8})e)pIq;(cf@&jk6OWbka^EGi%)akhMO{o^B%r}nMa&A z@g5I!w9k9)f?~*HH>mp*ecs(v%_;uj3s_z{Gn#$vt<0P$uBKyU9ES=S{rFnMWk| z+||oG;=Bi@VBHbt>v%_f-o!iDtlTZ>;NR9sJYSfC^Bz7$-Jjo#@G&AWnBC$|+|`FL+m%ZL!M9^;=Gp%MYrJ;hd6KJ zJwNk^{iZjW(V^AdU=N#e!OWxX?tv-j9WlL*cf@%U?_j&|YzM6+ZckG@w{bU_r|9!_ zyd%!rc+c-Pi2Y{2KR=?++r%#I^ETe|GmrYb*)8rw=26>s*Q!e@4spJYcf|BM-cg^g z;~nibCf>o6ad))CRU)1;C0U0!@8Oeg7Y4CUareL^c6N6_oUh{@alVds)ZN`pW(DT~ z>pa_RZKik+>*13tj*SfU`8wVa=j(V!oUh{@ao%04R&buJJL>awyrbQwj(5cQI^GfI zO}v9%=h)dTTeDY2kuW5+np1YtJ3h(LffMZuNT(j?hCr(m`^Bz8V z>_HOa$WWiJ;~n*RcawRFK5ybZPBr4Z=PoE$!MdY9?|~`k9dW*ncf|QR-Vx{BU1!C2 z17p^qWyW(?k99}_9vB7do}ZWn-^xk!T*Q7ww}~^4NbUt&FY}1=9vDUDxhJKF^X?{d z70!F^>SZ2r-UB1ge9SsTZO>i3*pUQ0F!I=gFyd4o&e!pdIPb1iSK++pE=X9x%p=Zw zVC3H8LL<(5?&`&kIPZb6j2)5Ob7vNNoOwiYcXc@&#`4&MFk&!h)$zb6yytHhi2aIF zBzSt@MaE`F<30KkC?Yuqq{uwCT_6c~Uj+0li~aFcQD_vs~E0gpAW)_Z6nTm_~fy(-Vx_*yz4oy zxIfRvjyUgbS}QoudPfqd;~lmAI^GfIO}qoWd$!ryOtIUzo6IFyhd6KI9c13K&1Qaz zecr=Yu+t*W*YS?}d>!v-qj5Kxr{KJacffhic1g|~c%Ndo@v@^(T>f@}*kAHn8;JAn zCi4`WH}M`Xv1pm`+yydF)SY`qjdmLkjNCg=oM$@-G)|W0xeXUPV!DS<9y=Qu;=H@b zJO$@XyvJokocG)X36p0YunxP5;VSxkYz!?k7|^7#PqELtyUq&E$C*b9yyvbo>#(aB z?iBmHhfmJBAc=8gNCI`dqdsrqJq{K%c6W6-d`(p_^GE_77`b=QX+7I4Gp69Yhff|m z8yVt!9q)+qb-W`9xSPz1KF?+zalVdswCdFHj`sOF-Vx_byvLmuk=%0^kOk=dZ|;}t zcD`$F4u5*QTOQike6ybK+V20e-8?SZ-MhE@Lvv{N;NxGqkKebe&3d*!wD-@SiD92z zyuICR9uIfuGJ@0|`pT=+9%{}~duXke+Czs!sXes%O6{SS&r*BnrRGR`Z;bhOvS-A< zlRYE;o$ML$?_|%2e$^zY{(q{+;X@@$a26|4#Od_;<2r#J`h0 zBmSN28S&+0>M?8*4|q>s;HeIR^ayYugSx4hqXC*bX>S-;;upMk--`Pc4q`@?Rz z-L{L_qS?KlZJPJ9$IWK({Q0Y&Y?rH5vzs*^mizPXZOwhKHTMHB_gfe2WAmYl^s#LY zckQnG|IHr`-4ll{eC|QRUwbzC=M>#ziSEh8Vs^V~_FT5T58rKe>(1k@SJpO(Anf?u15d# z@V>p?d|;NGU%3yQ4khz$Pv3nnIFg8rp%qYM46TEoyDL{|B8d&pcYGBQ)sev`GrUur$ni^R1YKr>1 ztfmImyqX$V^J;2f&8w+_HLs=y*1VdcX)CL#fiIc6)AX~E+s&%IKR$CgJe;1hTz%_4HE(u{<+^(!cY0*<$$sFZ%y|yV)Q5XM0k=>sn@EQtj+|$qj6kBF;ovN zCw}xuTP6N!LYby2q?EK9QWvJnlw zNL7dfWnK?^qUgA6nU^JR-zS3Yg)fT5yo(`Ch^sdN9=B*9Maw0Pg$^p0kW|$x<=TVH{pg7brWuZ7GhFY*HX@(6*`x9fjY)7sS3=9wIKpcIYi6f|M zCGviphMMo0?AMZH$h(+jRx>(9R&bLP#tM9pP)KRKXNc3?B^n7m6`84bKWiqo_^I`A^dFxK5N`j2aOGA`mvS<{lA?Btqv#JCQKE+fE zs!R=WzF6d6jA;wvaKMf+#&?hrYGje|mfdSIUEAkFp`7#zmA5?;<5g2!IniEUFw<0q zB;rR`no`loYcrx9u?K4E55|;g8r76pIpvih#x`{h%UjZhQl*>B$9Cux#WGD@c>Rll zDY87XBr6omG?ig{*GE*CGO?J~1@{@E^^v3-(fdlw7%OZbe3&ou%H+MKCByCK{p-;T ziM6K<`4{qN6ty9q1jrH#c$HV}-sME9;Z`$IIuw;j?@cx*O;IfJ1t8e}sRuLzvNS74 zbY8$h*WJV<3u2)jt+26JwF7Z5;L%FaUIj{XBDOpjjbe(ZU#=J~NmSt|+D$TJh`JDu z#4^?1TS$92s=ZG}s97<^0z+gYs}q_y6pBF^eMypbiquDY@>s^zUsblu+*rrbw?~#z zY!{Q1%{mk$OI2C;rl(?9Abe`-lLRqrYSv}R+|UUQ4#P!2u_9YCUkCCp#K9C9`Xtoc zb?r4&28my(sI0~(S4{dmBBlt{>G(?btxg_GdRCs+Z)z`~p4N(!VXhZMF)HiS_!!Z% zC#ed_m*@ zA(C@>JIr9BgksFVjU1=umB~9Lli~IQ@aZqo%^#NEx-`fUHT=ogMzwNzH0iBOkyt>h zylNNO%SN~cM>csnil~MfBV*bVC>5e|nK?s@k%F;IQx{V3a*~-B1~GtYW-KXlgO~TR zks|=mzu$gXRuX^U6;d9HbzfGMPT7`;SO(S@7LGW_xppEh%a{i;SxBgXla(8BC~D6^ zF4%KDaOmwh;#dMGB-gpJ@NKGaUD9IcyAp?eKxQpBaallv6-^hL(5*wsTrCOk{jkVl zR6ug`p+8|P=Gz#e7}cam;d@;Q^f6b+X-G!Gci_fqa++X1jz*TL7;aURsNY;M{d^&i zy5))G|451zX=D_E%7rpbRmvz|UY1}`k=q!@IHTA~bTOV{L~!DUEH4X?WuEX$go4!a z)%|ka&Uekt;ZN6_-J;#iu2-A+`*&~mhvv}ksW^y5D^vBoh4c#EFBQlCJs>zL$;TGQ z1og;}_v8gZWheU@N5ss)LEkFLELQ207o_i4T!N|YJTzZH972VJ{etLVBrgnGn2cCK zAd~_=sz8b#3v;+8D+$=fd#aE%kc)t&uP~DI)U`Tf<&+@cxBXxzD>BHtME@1`Euzp= zdo^rRHU$Z+P;IMHT;>X4ag)pw_X4Rxs7KTSp{go6WJXqzfLYWWVJk%aL4JW`aeyh3 z?>0z;svvzLgmK^*!d{!C{-969lZ4VP&?!NxkhGV))wfp=Au&-P1!f3=Gjo3tMKVE2 zo^bvd)P(I)A4^Hf1R_;Qni+YUlq}=KayJ+>@4%!83Wh4J32woBB;;SbrfEU~E!l;I zRS6aiYUAQVGwJ~{r38^sE7L2F1hf*k5`n+ZdlNw{4W6!wmxQ8@WiOKjqRB8xHZR4% z2dug<5@IqILP{yIFS{@jJWqHsaZiPwQjvCHT#^xut5#sbiln*Kb9_KPGM_*a`KLBv z9BG*OZpq_+PrbAUL@eQKw}wG22h?fbTYF3k0@J184c`O{^(Y$ePK=&;;Bn6N#`U!Gc1K0TQ=idzhdk zgPPNgPt2|I6`^1owlVX9RKgu_TUZZj;ULC#pMO#^Vq)1oloxGUMu7{NWh_*x>G4!O zrouE1!&Vw9fa9_-j`UnV5^E4Z0E5goyDAfZFes|X%E+F9s6TMHKmlN?q&+LGkldcs zr`WovrrgRpuV-{$Nun2Lbtzaz+2za&&U9RWPZrLZF48A|5nB%uz~`3AO{mxL5S z!B7j-gbYQ_o?=C*--M}0DH?tSdgC)jN^8>QMHHn_a{@EpJ&2NNend^Ez${yUED*pI zxu7W(4Z-Q0=Y%zvtPofmQMUZAGt;)K?f$=4uK=D13xW4T=Hb(3L;UZ{0V5fC|=G%%Wym} z2}M`XsfNRx6WJT8XkHYO_$6btt;(2gGdk&D zZSAu&Sy{-!DbgXWOL|_CjCeX}IySHgYJoTAJ z!q!X{Nx%%M@vzJwWi+BG%OU}lhN}ddP*B2wty!H;7Kmj@sG60ph)VAL;+x(Pjy3Hg z3sOav^k`335WuXwHJI4nhXM%Vly1T4Y-Yugp>07R!qKiNNSQz;s0c6ZiUmn4T~2UK zh5N(^m7b0xb3u{hn+SAZs%aoa3PLK%rA`<~x@Soy0FRUW3zs|7NX*W=0`}Q=Ei!|d zo0A|A`#CaEAkvDY`a=fcrrbj9lTwjc=?A4sI^K!Hq67vhG9qr*2Zi-8j)RmK7st>n zOatlj!#ANKB{K~~PH@mW@+2ppB5Jpw8H*Dko$Kj-l6NMdj>O_Xq%G^!1u{iN%JhOM zN>V10DQd!Q9Fq1tMM;VTVqXyorduei`>(Vh#v2N`X%RcVON?x;2d64>QQ}}2NnaKS zHP|Zw4{|9~NMJ=(5lT||gT8)2Y;XseMD_Uj{c_#Tcg@Y=Py4&wW^=z<-rgPFz1<(0 zL%RpT|CRgDb#s5c+01U*^}L)B)Xz+-m3Je*y9>pn7Xc8le@SiUkL~Vx-~y<>^FDL8+_$TCG23od%l%#V|G8c@ z^Y{Bh_vH=7$}gRjMYDT9+cfWI-HNc>?Vmq*{?mu;ez|C8i_POO#4kR7Y?r&m#?5&o zQXA`UKn85AUlz6g*3SC5W!__3Z=ymYw(@z8tzQ~ik4?q?P~QLA<9pj~_p5HRogJ2| zwqI$VIZqzP`zPz^=X?E&kK_LyEa~KNhkH`rKi32_$_!^5TJqGMg_g}B31jgQI##aSY`nL4zE*?xcl22nK4)!(Fb&4$rQw5|`5XfHMd4UXB(I$1A zGjCA_InqU>Qjtwt3)+TKMX#$w{q9(zLa+xBjN%trVp4mWQ^gZhn;a4ZzbxChDjrR~ zUuToUErBIap@53bAvPfD4?INx5i`4(w1-0F8Rv{_ zfHvXls*`40pM75LqVxkboz9Y~c*e{hF8xOFtK#93MVHg{jK(>f%xE25Gv=s@>osMuMLUB%%6;d&dI6;WxDRMw{yA){H zVz^X;8w#!>Trf|`*8oH{47WK|I5o3R%`=ye65)8=#yVh@03TUAlzjREmWW3zb~gK< z+^+rT!bvx4bkJgR%c2E;;PR&mXH0M7&IsYH~x4N<^ecFfpmg znhq*Dp0jiTrB#$ix>wof(gDG?K(#R{KG(%2>k3Bj00UvhlyfLupMI`#PYp;xC^V>3 z(Ccb)Jt>L>L{Uyr06@VLDRl9q8Hf4Jp#g$-9U1(AOHhe;`egNsHbF@0@Yln8lGwS#KPeP-ms8s8&7C?~H zA}Ydjf@+LJ-^=SGeGXLx3X@YFF7V(osEP+z&pM3rU2fp)YgrYjry-f^n<$_n3d*Vw z)ie4rB#KA3D3~gU>)Cnnl~NEXkP#gVh}5-!Ag8(l0dIMSJW6A7#wit)-h1S5s6B(P z`S2IhagIbNPl~ceV~2!}rCEJrx)PP?+eIqW&mkh6ueQwvJ(GIyLzTlaz7mAPLxNBc z8*pY1(FJ7PwpArA+qKz*Q-2`u*+muw$T(I0^AeyUT+k7yDNn6AnH`}&%;Krn$Eym5 zeJu?dz_EV4_aGl+`Ld4Yx^U9D$~p&Sw5yzQ1PRfpCorbUu2FoG)rS{%_3{95WYMii zIK{R%l01gZEFgR}_0)>5gnCdfre+f#FZmb4HgVLOE}-=G*7GUTKae2G3DrRdkGg=; z^9P~PRVc56sNxCa$Y&4i6GMj}PJoV3TtQXw0OLmI)B5u#t)Kj4j;JriHa7q!t4^7A zQEmRP*)@uhzr~-CUg3L9g$tQ-79Hvq^sv zFBebNVS!iw(_f6-KS~g8%7B#TABypu;AI7%783n>on}K;f!O{linm~Wh?eDbB#>`O zlN=IINC;&#pdvlaH5^{SASV+kD9tzoMyC&=;vPYZDTr8WoPMeL4H_JmF{OTp1}8y7)-NkR zlU0Xr2a<#+%ai6p);Xw5Jj(8tjt6xEa=5c>;<`a!&JuJ(;poIsKAhm89+iEPT!zi= zwUoyyUR^xtW}Ugrh`JZpCENi?7f`w#IO||o7L2$^f6EJKC{Vh_{rV!Rpsqk^G=spi z8WJFoq(u%I)Ww4dM`hozp@ltG6;5D`oG(PR48v1}1Gv&}I~;tnK~JR~1>4HRQ>?r3 z>LCg^Re93QO^ik-zhi!YdcM40uG{&pxjFplcGc|nvu(3(-@V-*nnSw>pZV5(W`4Ka zx2twBTeR(Vzv@23eD;^#XCDr;`DVLqceC!3*Uulk`i1+G@7mpAId4|8yJmIs_Ih(T zZ0-Rlh2e|CW(%zTt;4e2td{$`F4XgHiLTWlV#SFHtfBPT={f91m19V0k2UDX$} zzT(e$2O03Hv2D6|H_iLm<7TsX{@B$|w#(J3+0B{{%l-NHCRRR}Sor~1`5Tvv<@%<( zvfXE-yR|pF-Yw_v_s@~>A8+Q(eA&#lyLP{S*tN5*FMx}mcB$SUj&}FU+jX--$8`_S zID>Dj5nfswd~0p+%G%&NYlEL!8~n`L;OEu`zpys=rM1DYtPOr`ZSWgwgWp;k{1q_x zFI+o)Y@5Sfdz>(<JywcQkc)$)yq=-o8wny3#Mr z#dSafTa%@iTtZp$4g}ZD{q<%uJM5b6Vzb#jFL)O}_8QUEX4|f3$1ZgK(}(STxoEo$ z|8e+lK$b>%cynIFLk!l7>eBNmj={4*IzxSl&r)Re+>7DN^DmCUvq#p!giC(bYMc|Q@jo+OTTe@>*m99vHYQ1TThR(hUZexg92dmo`c;t>)YMtyLR^2J!X6f&>VUOAT)=jg3ugVCxqtE>HizYja~Ej{FL%~wVA(vt}Ecg*Y1?Wb z?)%y8u6wY`d@jjG(+8v$9*9SbUdrw^yLFedUH9B}WXcxJ?tTA+v3t() zvs)~?hiUE%`APp!{ly-7cJamYv9dQbJAA)wk?g#Rm2{6}TFE__GXmq&Bi8#1)9D#b1N@}FZBi!ngKZvSA;ssq7nfcj`4pyV$xkX%Ec#UR zjLZ|!qobyF%js$Tf_lm5C$4>==r50Vv|a?CZVgiA2}+-UB=t@=w1lG++?s_?K&lmc zlJqqtAIDye^U2t!tvibS3-<`*x@`};&F1iYlD~M{JtccQ+@Ydnd=$;sjE|ypWqcIv zG#MX7kE}92iXKm8d=y#bl1Y>&HK~Uzw?U~ewjbc z3gXyT4$I@+@^JKjgmL28kB0xeKO?c^T6tq)< z)(!KXux^<5gmsj6ud`h*ck{dB)zme6we{z&tKM!_i|#(eu34_0o9g*r9RJg7(Hxro zp2Wpx!+$sf#m^7@SpUAACJ|eS*`iJ+Op?`W<-*(5tRoi^mx?TIT?n__$W%tdS z^l0yqI^2G9UoQZyS!fP%MQ9H3L}(6i#M?2Rwt|~> zJ#S~X-nYix#pLd*Vax91aa;wDOMznZF6fuq$+O&!iE(%fe7Zr8`oFc`;iXq~w@=qP zPkzx}WFBsRt*pA+r`rV8{RY)F%kp`SU_G^i2ANY`+S71d?3CA+ZKYbcrL@bWIjF9t zWzboU)cT@(*Gh9x>soG$X?I+8QMlfLp#8Aj?7OY!>1S8EgCzTdE#BSx$L7Ph&;H(R zOLyJ9{oz~T-FE^WhL>-rpCHNk(HZYe($n@(@avqs2l8u#NN0QuiMNpMORC=2rV4rR zzR)K3;C+wJ`}$h^wOYhiL6-b4m-&?eQ-$~7#o%W>Do>#p3f3Y1!kI$e4mx?}fY_P9LUbw56`Ih>w4 zgJ$6U0@{3&`IR;AGsE?xj5lQ7D*M>%w%sI69Dm202Z!&yol1{nZ0$E#vQ%yz_!s*k zciN|}`J0E^Uq5z00gJNSo~)dY3b!w`#_tGL1=U48DJNX!#1I^bf|S$>_hoeph8wE& z1p_pGUmBv59#mJQU7u9UK_xsO)$%+3_?y?6xp0G8PgLDnZo}%<@*7kax%CdP^X?1v zd-wWN|D&Ti+r(WI?;At=_uUVyc0VII+rNKUovz{JzmpP}LVKdX6j}%crqJdmFooVZ z2~44PP57zH{I2`r^7E@Ax?htwicCE869Eic69+?gDCwc^0|E!#Z{VJ)ko6+4&;Qlzru(Vh?k>)EE&MUs@s?1t7;g#nit(0E zs~B$yb&BzpP@@=c34OO`ye0HipYfK^kIMeeJpvvcA3xm(J&t0#AOE)ULQLe(Tp-;* z-k#KTb^K4W<`(|G4EsSOAohbuFYE`AJlGE+HLxE<<7`$5wjmeKRe8R@Yi4^U_ba9=?8x+{owDUAN)J% z2mfCB!QV?i_z%(#{z3Y|e*_=&4sGWDl!MMTF@C%+KMZp;c$Mq_u5Hel!adOMU(6l< z5sv=1+J@`cpB>H3?z-RVCa+|V0X5Dc9RhV|8H0)T5=FIsxU?OLx46%feKm5q$Rl66~|N`Q%JG?<=V?c*9R5wrwk^#Rv}$t>7Ad4)wf90 z=><)WLplVj9u?6vj+_P(a?GM1&>jN<<>b<$cp)KZkRiolnxhBGQ}(C{Fopbi9f=Cw z+r=s=z^IY0ePZ5QNH6Sr45;vpUzDf`FoleI9f^9$s8@lB?@=bzJ59Y<+Gi5FO(4DK zGC{Pk)&~`g!9;Q`Fol6e>jBdsLXMd*N((CNS{8{qu{@Uc=$ug(kTmN7Q>%bci9k#f z6*G2pqNuA&V-iL0asgd(a9boQSU8#Dg;Otq(gM_p<_Y^_oB44h0!-f5(MNuT()(U# z|4jhfuicI{iC(@aP@>l|3X|xCjlv{)m7_3;Uh;Tv=Y}5+99twblhlS$_0@(E2h@fU z6V!$gAJm2sE4+8iyY5R#v+IZb-Sb=3>{H4pBk1t#^|G7Q@t>nS{u`Ce58c0ZeEJCh z&;`A3NV|VS==5g$IU4X`4|^|&j63AsKNWoX$C&TBe|qQ$`1p?uKYKj?wCR4opu1?a z=%42&E{k3Ky!$2AhjsUFL3bbXe&O)^M^E;U=5is#KHu;@1Kc2Sxe#0|f!!F}CBmkK zz!l;xa_qosA)G48X5k8f(L&&|@D4e)%K``C3_y5e1|eo0QiSI-s9EVkARTy!N<%=3 zFod921X>)jTN6h|qzJ<-a1f9pF3l7XkUL)>6(Jx+RD^&OQ4s=~42BT$MTxY5mhNm( z=0iZ*Ka{6(Jx+RD^&O@#*x6 zs0aZmq9O#O2tx?@8#K}e`of3V28|S9mj$pvf9CkkE;ZVxuOnQc{Rw@6#;g!X5ni53 zZ_r2)6(JxU*g-I$BYKBp#sxGZK6gYe%>|@`iV%<@DndYtu!8{T=s94!BYIb9Mn^Ow zJdma7h!pXKnIht+EJy;L&!9G?vw#%gAu0_4DWW0-G$SfPK#Hgc0V(23GerdC&KHOs zgxDvNfaf#7tyDw|0ZE`3gdm_85mJPQNSOr|0#bw_g#6PK(gym5c=n{shk%BM8Upu9 z1PSCTv%UaU7S938mf|mlkRm*gN<|oCVX||?*JfN`D~)te5du;~MF?nQRD^&OQ4s=~ z5q1!OD8F%hX9=r**2FbvqzDhBvNf?WB1Ke$fD};?0#Zap2uKkXA)sC9Tc?@QbVQ1< zgAlLGNCKYEKt|duY^+}%mpNYUpWaFbAihOQiO-d zJPSw>o=@W}=oNu}4l7#`Tp^GSyrh^H0!hI0S*Q@_f!%zmXtyS?xSgF8$V1a!LNyx(n5G{O+4+;@X*qc%>tSc?oK@V zhL>4)L?govLfnB6@jahG>a<*t&Ik`tX$VLW6(JxUxI6J`I_8TKX+ztf!Hn=?47eBb zR||o4fCS=!EKNr=GVCD4c|{WNd13g@@N{bRH!b7Bl5N82N!1HNbi1gRlvA-Cz_6;4Flw%-FcCX0; zspLXX);P^bAnro?WHX{71SAkU2!M{B1GZFJMX(OgjPO7z>1dFJ$=+GJ3+Y-#fGl_p z*ivZ^_i-aY62Pd+Z~^;^F{>4kf1pF!(0)I{fD4l;!rh5iD5J(%v z3xO12m<6suBY{+efOO#Qr)%j5W`yT}C9HNvunv$SJdn!PWRL~hr>}|DgcR|rvM|{> z!d*x&qzKOeOW4U2;ek{t;`Ah*U*ge>u>Eqv2t^oXK{_KULO?UZ-HF#Kg6BKvpA{jX z8Bq}e(t#ZWKFw^Yii;9$O?Fur(VCDpw0Gj!G^2Bb`|f~Rl=(|M(nj$tAVt_^AwE8# zHOccCR8Ki6V+cqB6(JzvyBp}?TvujcgeZ|VZ2$d;#J+%Z;3Y+y1#VwJim-zKW`yS; zpnuHY$!3HHQdtO)1$W&%q<@?RZG(<2BI09I$}F&zMvCwdDIpjXLVL1~3xQ^Y7j}?3 zWflT!F(NcFJVd1-ppoJ3r>hl_z6b3&VDmKD$=U;1x)5kaRD^&8Vg~{A1Nq`_WKcRA&@`}vmiZ3 zxNG^^?^&~1K#H(~5UUVL!1Ed4){63(j=@*75;GHNL))6-ETExKEDNAPV}CJb?bZY! zxZCG^fwYAPm=(u}6ybr?(vj;6NFWs&+RJdo38K|3SZETHqWyAwaz)?^0(bVttto2SW6)*eVD z9oZO>A}T^a=LkCpfD7)r`753g9!Mn@46-npBJ8ptRzyVzND&nwpc&yVq)#>@DndX@ zR7D8r98nPhnh|yoKx^_Gu(F+OYjXF~CsRa42uKlj5aO*JNx<_N(4)+x#QtK;lbx(R zkjg9sIgMjPGs0a+pG*;U5aO_r1U#QX>Pi;^3B*IBga8_}=O73a{?#jAW5ui1B4p`YvcK6i-S-LeLMO1`< z6k!L!fD4nIBix;Mts;27gFID)fM!HR2uKlz5b|G}BW)D_asrwWhFRd=)}k5VE~HPU z2s;SzLX0Hf`3#h-oe}wTMA}e)IUyecQiK*lTnHqA*H8$g4eby12h56-f^KoRJMmgk zf+6HNU_nmyKEeZ8Pn1XzYA)o95=li10dT=xNU!jQEgu8vtb7ZocejZ3=7WfIk=Sc}6rXwQ07j}SK2_di+ zBSHf45S5026k!J;4jU~;?gn}&5@i-*2xt@(gTQhDO$HB0#oQsl)w~vS|~7uepgUn3jHRWz!ds7t_7yhKangjg`SEDOrggK0#oP^ zBrt_`Xn`s8Em?sn!+!N^FoOiBeq(3qw|1s}XJ_i~>`eW=ovGj3nfeDiQ-82C^^d^R zS8h7b?>4)K{cO8x)`#ce3_kT!_o>HjhVP#L{NmHq@}D1;i`k;x9`4W>&-*l*+VY^>0+Sy|BxSm}v4`&yjKeo%= zVl!JTckTSJ+^kUpFfxxigpqmFLX6C#9wRZ|eRa8H?i1!MbDuD8nfru!l>2WT?;qCN z&11W37qeZvZT%s4<^9#X*)5jqX5~2l$$s;2xV!l5`BU8}Ts7Y#C3vQ!4lf_u{lU~< zWQ>V;z1elc{^Cd9wYz<%`v34B|NYO--kn`tuABSTBEYsW4v>i_SwTd2ivUvl( zzF=XM3p%_^Rj{DHv<6X6DOJIO{?ZjJ=rCEqg8tGKEZDY_U9_OTbOj4KOjZ#2^LCK^ zVY6oA2-k zX%EL{+5hk7f6;r`ZuW8kHW#28m&uV>`RYzon#&T= zYAQqYW&-M~Y-fNY{hXO3mWk?;*_=SSko?h8Dc#Vd^w5*6OfZ_tyry2(r_a2VAqr3n z0$WJx4_u#D#FDN{Z~BQu>0XDTrDh0qD0RJ25lk9t-^$bnW>Gd2iV4ZbFa+|5MhsPj>=dO*w}8AgGmkf6=DGYSg263-q>*bs)KV>qR8(fC@Ce=#C{c{l`#NpP79+LMhmpV9O!)6@8H*fm8IjYc^m^QJ0h zX7mcGRqq3oIQwuF!K6!LbKn&5tc+?XR+Q`k_NvJ$4cR0{G~{4P*w+U2PL}duGZ?cd zO{AJ4E2p9|L5<1G6KhW1(l+QY-E>Y|C{vg^*n?~;>H?VA+PB_5c7=JWf7iHvAoK5Hg-CL zzt1}TP_H&zp%ry8<`C7Z*g_X-W@yc%_@0Rs3@Ve#;Lgl~qyE5Q1H44(Qna$L!voKG ziZE%@g%s28Ch5W*(>f45$Ob7HV%c%MQ4vfUYu@@#U>is#$wry!Pqoq9RYnqxLNk;` zh|HIaW}@2g-RyOg1}dB!G+Zt7qA$_{_!9u7D|rvlsD|2SBs8IFmzkf5YD1!%rZ{-o zkYHbFPa84E`S9{&4u*p3JaVoD(I|P(E@@d>lt%*+mA7FME%~+ShN6vI5XoaoRwY7q zOj8+BMju^i3Pn?BOCuCL5RLeQkFGS7tmMHIstnPt@>;+X5!2Jz&6c1CXtuPpE1P4d zESiaG!}kDLI1?48R5*(2gsi1D16D~~kIO7pndw;zeUMQ_ZHQWsO!~Om8tkCT9h<0k zKcj{eD!4|Rr89{hP=$Kb$roH551u-?&Z$LOmJ*G6URizmrAa|IO?lXb?a#bARQ#@6 z%8L^Snd)45Pz;nLj|MpJ6GgTHudjmH1)Dw9`^qQ>LQz#OH$RH%;6}i6%VeGgX)42OdSG6?(tz%o^@N~MG=(-b zS!Ol)={=eg4GNU(c&x0reqJV+BhN@0z0QUPnrlI@RB?v{Du|Xo-(+rS=DJdclkT2* zR3phKAD)_?mJSED0c!sxqZ`;;x(^Us8<>>-14ZhI7G98QR4@hmgkno0@-$H}_NwXk z8iL|Qd!C|0r+dJZ(M(huiiz?bVXqq4o~c1j1y`p#6Fg%#Na7EM%JhGb_HayBAAN9& z*A#{tBmyX8;fUB>vsdUp9ZPPv5=2XF5&dOEMKEa!^v#URBe}qcSyrf*alue>7ssMe zjG{0_VkuJ>l0#0d;Eeam3NukKQ&olx_oFLKnP@b%A?8dGg;Qf@$c|#pq=ON&gjxW| zO1jle5e(6rx$}(r%LCS%*=vhMqZmajBx%GnEwlh|Br6k)rZUFzr&<-&8!glyoQkMU zlm|t<+?45L?8Z#|K`)pm7N$j6;*gp4ly#P2+p;Jad&Oj)GDT=fQ5}nI&ND;;Qd0!& zMrB74O}d-8=0Yj@)s-e4j9kI0;`JR7 zV;v3EulQ(&zfgpup&AKKQC~$k>FP|>)6~VO9t|(8LbWN=w7B<=ieRvA_;tBRu!=1$ zI15l6i4D%GCn!oosKyFikTFU&+FgK%hAM`<{j%j(lt!Z(;&FjIScPgpb3vy8GOa{M z$Xn{PEF%hIgKcEC?CX+sfad$dpt*FqVCfZ#A>>~MEZNJBTqqL^+sj0W2dq1B!913} z6G@=5X$Plqx7x~M38opRE*{XW6My2Ce3KaDwTq@E{fW>_&SsmcVSPz*6N*Xh(Qs7?t_h3b^>gzEHjr>Az+-nZ++ z?2mu=!?S)sag!hVqvS*XDEZKTCi&36mVD?xmwf2oNIvvmNIvv$B_H}P1t0pOB;|jU z6!(u38vRj1*gr~A{zpl1|0to+A0>qSBgcqeIJtKtv)wNb?d-8xt)4kS?KJvln!Sbe z3fS-F$NxRp&CzuJ=mJ5DCL0K`nPsfRl9Y2K08>F2$*@8SaFQ!zZ;wr#Fp`j?T*BDL zkl+vUQzHyydf-7S#VSd6mCQ#m1X=P+5JXa-aglP*d>A3onUq|t{M57$b58JiNS-tl z4ZX;+iTNc~3Q-mQsf2(&)RV*#MM0YcH7>{!4a#C4f*ez0PcNz@>{A3FoaIB7RMx6s~!RA`#c5es>~- zGK6lc6x1b^t1!_Jy(S_Jg{Vp)QB%vAt}N+Tyd(ialqp<^SW0#%kR10=M@q4DGcJ>K zYPnvoHr-aA?vHN8t{$qiCP8WW zj8TypH2`~^kiat6E(rmpRRKvZC_c@5Bmbc%Q4wD9}6LX3@%r-e-ziMS@#YYVwwaaA6~C{#+wQbIZGNrdbQ zBYLwi6b*DLwh_pKY`%u6LPa*BQx~X7zbU4bl-IqU_?B*va5H)pMKOw9S`{MeyoiNq zr{sA|hixpqXk|#L)tPlBTlF8B&%Ev04aYpM7C+qZd{tT!V<#Pa?_NPo7M0%ofJ-DzFC$6`xzuIkGu{WtJfb zMcl~JJy)Szs?|{w%k8RChM9-*|Ci%rCgr4cNvj3UN>$b=hIyp64JA+b#PbIa8R(NR zTP1oJv1>V%l8^NJSjW~~Sd^d>lKrPrWWq>5O8cCX1HR|aK>e{v2Yfr7m4u;K7R4%b zVJKXNsH(B;NLI8zn@FVHE1(xOGD!AVwlir_(rb$)vLIVjha9q1AQZ9;W%`#cmUJUM z_C+$3lFeGG7SO@qPN9?&x9IdePf9UOS;}-{lf)vU5R+|DAit;vMAc|SPn1TI zZpMm~0_|m0S=GQsG<`PbWXBtk9AQ<0P^k9AhC=;8^+ginhf3zSCBrW+7jz^|~EV?F;^k_U*+Poqe4Z%K=)0vbyab+SKTQe!S(lyD7gzr?n zZqgoL)&E=qDX?{s)G5~w#EfdGM@CT&sJcp2jYuMAR)^Axq-ROqt|AG9(%B#*X(Z|XnE7^x z(2bZ}1vX)_(PZu>qN>>z1vO!c@-JN?n|aq{n%Oboi_K>Doa>7pyRLJ7wb{1o+4b^p zcK*|c?S8pvXN%3_@ZVT?cvIZNWf>1~I=sBS^?Zu6j4c4I}d1V z*m*!hg!ka5+GXrKs)@V~o(E44mL5plo_l#6JR=_aEo1OJoKf+hL&~U1lXWoh;Ps_A zOC}!3Iv@|;vhvtAhr4#yttsp6C7Otuw(3Xf9*x9Age`o!4k8&>Wh8LUZT@B{YW)PeOB_g0)L%?lW6+pWB-I!q(iEw&uRFHTSix zxo>REeQRs(SG>9F=KgxKnQd3i`u+a7r-P6E+>Mjl)#h=wT6QDdHOXJzKdcVRS@-Oz zJLNq;c{(2c_NY;c4Wd>lHi(*~*dS_`VuPq*iVdQcDK?0jrr02An_`2gaf%J1)+siK zny1(xYM)|*sDX+Nq82JPh?=O_AZnvxgTJjp_;0NZ{>j?le`Rg(zqU5`-&h;`Z>Yuf(WbN7O_8v58LbO2odwvEubPVb5n!Ze^G$G@F1Pn36p)JgC$3xB16K2L`@(Lk%ksb8L4z>Cw$8|3o|MO64{}V1(b;;gPsMAZSLnZj*uaC zKxn@~`4euVgi4tjjDId6{dY87caJ3_uFUB>iV2%hv`qT)zV4Z|x^oE$_R<7+X=N-E zLVT6fS~3AcV(z|{9#RPh-$N<^VVg=N9O2lBB_jor(G{rH6%A=Aj*;yuWQbitMgj>K zl8@sr-TR#5qmY|+JwN>%0%(8U1EX!zJ?^>R?DtvUi1U7$plda&S@(^J?x#0SUw=6N z(Ot8??H;_KL6rM>RQ%6etlhUOPQPsc!t@?*U9TRxr&@>ZOLpC}vF=Ah_NTX9;IqD` zcK!YH!!YpIUpQRXZ9D5;rk$|)-?_c`$zAu))qOu(wcoWXl%qUbZwzf+8rphmXzT2S zE%eq-zx zw>bQGv1_{D>gs+M2aUxMCeZfaWqRJ+Z*RKqXFkvM^H19k$D1$@%l%!uIHQ+Y^2d@0 zC$!b!xywr&{JNnFg!%y`B9Kwp>KEJbfzZi%I>)@omb&cBiuOMt zeEiks@ORun=v*AsPfPYc4m}PA`O}ibk4?M8$l?gIp{Y;Cubk#&Mi$5`03PHL^=*z> zY2xH5g#Go^oB{-Kpv~oDi$hwEw9=jMz>mJlI}$Vm&Nb~!SRfs9#g&NL89U?6ssD|?l&zpVbMHZ837 zY2vW02dX;t@1C%6lM-^Rz+53#z$z((n(SFEW+YCwru7R#Rm>4(WSmlkQ1u|rKt0YU z%BP!!NMuKkvVx2l>r|)BTfFiDmjyDi?j*m70%oo+L!_pWWSy$`ZkktvkQVhLVbv}Q zM0!9>%U!N!YJKK)c5L|O6(Ol(sqcVW(vqaxXXf%tC|pt032H)CO&*GEQP51IR6qt+ zp$msB1@k47Z`_KO*UW_cs@c5wvh|H!7RiX3P~*e0naKhn$W#k>kjnylFi8bPYvPh; zS`ta?VoR8tnJSTxn=sM01{D#1;FZ`kkbV&+{Eh(zxCEcpiW5Mf0EE0jv8F<{ungc?bo*$r_8nIb3Ak*UHsh)s*&e;+5wyy^-e z3Q`8>E3615nO9N}F$^}{;O>-bf%Xc~pACBpX$>Ab@4(!k71+-Zew8I69V#jbL^wmh zq{=#5WV8ZFBomaRP$U!7gzs7(NlD5CBCSYzAj;KB?P??$-BKj%z}Asaqoiyk=}w<_ z9wCt_$|@|%D-1=d6ip~)G;cpoNcOlL2`o`^dfC^6%D8Ja=7HFkWTHSOs0c|uIkzS# zNufw4s0qcskE$eP0x3`t;tZA7x1PwC9#*v+>t_6*OruGdhN ztW!KId08aVd6GV#0C)KdQ(O+ zK~2m=K7CP2g(9s=dT-GXo#-P}mvAjp*uK({IJ2NtD?+6vS0Ab{5<>Ip^W#v0v$_k~^`@^o>o#J==<8E|5e+ zkVj&_b&(<%xKbpNDQZH-TE8g7fG>oU8t{WlVHZw^qDU#x+{oOx?k0;QSQPk#>ANfl z)h#cwNPt__mQe|t8japZs3a2ykkN?TOrVk?%gPigDH4eN zgr^>5Wy-zZW$>omvo&CMfr1blRk=7Os0TT!2W@{G1t}BA1Qns$@hd6Faq&s%klMxg z&cL>~BN2r&Bt6&5DY^0+6G=fx!Hmd^up;RWpLcR6q1V<(FpDS+khuGllYfyPnXo35 zO0v`mPBe&~rBq^C5}ST3t4I(Cl`jhB2sLvRl*Iwg4u-gbOi?QAGbM}zcP%#mM$!%f z6|TO46*gT|0gVEyUlFQg`MowsrLZ7CcX`i(O7!L_Hece<)z{;RX=xF{A!AA`7PLVZ<{sEYQgA|W>et|o;BVwPJlSmf-K_N}x^#>f-M0(A_k zWGZf@+hg9ziaMp&uZbp-A%*7jl8*N@y%Z@0Y|AV2bo}C0m8Y_$D-We4oQ&hVP}72B z+t5VvB9}!H?w|xYsHNAk+sOg}dbI==gHMcxvqf%0)hGC@r!S-ifI zq#KvbVNuk9gC~Mji)q`+cN-*v6aW#Y>yyGb(%I>W#ku9X zT2=vqOi1^wR~N`@PzbkzWEDurEMgwAy*CS2Kpa*=!ZBP)OG0R*3{XlWgGW=9oThA`Rch{&fe)(6YHOx}8; zQ%E6yeZO3{^Idau_|ty7dziPgyJmg6T;I<2%iHz4xBEkLX!qwodirnA&OdDqhh~1) zE@s!u!`a2>H_iO`*Nf$@ogbE)_1VSGZ+DxA^`bZ6{r7+nc>rQRZ;&5|RRw^J`;y0_ z0uX25E$=Zvtpfn+3iBN!zES}Qf{D$);}cRA0OI}|mm?K`IAo*(&ts4_9e^|ku(ufT z<GPRA-+=qV9g^HTvk_s=P^KCHUnrDczDVJK%=1=0HlIy z0MIO`1^}txt(6MWt=KakP(E%|Z&V}%jc5#jBC}u&fQe>-2T@f5w!%mS)c_zBTv@3g zI)9*z$nzMat!xR3$i=}rLMr%Fl>yc* z;=Y%ovH*|@ssTVMs0IM3;4{Y_T?z7AF`5OR=>Tw@5OLos1E3Fh=6Riz%>Yt?2T=zg zp94q_Iu69071924NBf{Ex(sk6MBMlAr~t$nK(zNfR?PsK1s)z<2GS2GJo7=gamtkE z0=)`8FOh*SAm!r>pnFAmZ_P$HLVEaO>I~?f5wR3QD)5S=%Ruyi0yTfnV}M!(Ag~ov z3{3zJPgwwHG<@mks{#P_0na>}Bi*?m&N-R|7?Umo5eX6P3jtsgj6~t#(Pbb#j6Cx! z#KO-9kP5ysQUMzungCV+fDAqJEX0Xsfd^4X!bpaQ_MXQeDaHU$^DhJ-2qtcfNCjU{ zt%7O*&}gUz0F8!f0FWrE0YIX#0suPUH;&`HPE>t>HIHV22eB*wq=ISykP4~+Kq@c; zpg$ddQ(h8Q>Zb;(j#%NEFooAQiZ)-8vP-&t_5c_dHhZ1Bm+`p0bSy z%>pw3`F#NCLH7j*ux@zq1qF{uGtpZ!cePum0yaJ*ifRCm3akJC4tVC-92Ftq7yuKU z3p|LbMg&UWE^hO?;zXxo527jqEX9xtssTW=pc())0n7m841F{?fxEcNCj3I01Js{o`tB}h*w6Gz-iCz( z0NmyF&|8dH`Mw~(1V|pb60mIs%>u6?Q z23Qgz?t6I30zfLL1^~^1Y5)-T-PLa0M#ORejfQFf&>mC`08)V!fVdAJ+It>@x|Nk7 zQh|r3EC8eeceh-pg17{T_MS)6668-qqzB#Qb})HxxN*vKZ_R>i#^#YIJcwmUh*VGw z08)Xw+O1OoTLL5sD*$oP5!pSD0qU|GK(oNZQx*VHK{WtK1=Ro`6}YS2Iu(FU=$Q`~ z7qeD(E?~_gQFsu`k`Sq&8UUn%Y5H2_Ej?xwengkTnU=3h4J1WhexLBc#zfd^4F3*yd$ zXzzIplA;1|dSk*jBBTdh3E~VO+Ly`z*NBh`+|_QS3Zi#LsO5VeO*z120I9&kqatC< z0i*)YW7P~G?t6Gt83=-j6M{rh4FFPsyZo(F0h<9NifRCm3aSA>qNoM{sh}DFq=ISy zkP6(@ZlwzJ=K|&B_Iv`# zC_IR&46p!@3aSA>Dlh|(KMj#QEHAew9#kR~xZCH-VU(W%G$aaV0I9$%1Kd72B`pJ(vgrW0aAg7rz`-({b~S^3akLcx=Du4 z^BABm%K;<`4^LSDNCj2^;u0X*dmb%I5CcH8cXzo>XF&`A(Y_FXAe%8BBnl6Yj)eJ# zQ>2G)RRF+J>cz+ANcZ+U&N)&6#-z%?i2H;{9>3B7;Le6f6z*<$;bDYSUK?$Zy3+4@v;ijmWG78~`){+|_QK3IMY`^K7%$%>n}eg*kw@ z@8w7}j7Bsfq=Lf32<<^;8IY>LUG3JXfaf@%sTBa;JPT2&g8WvD^q_l0!^VfWZ&iX3 zGDLbPoB=co++FvHR8S27+K8+G#Lo#4**%W|b5*k-27qW^2tW`_j0bVw!(*HQM0;A{A5vfK*To08&9U07wPZ03a1q1AtWEu69qPf@%Pe3akKt zD+uT>?8Gk|DcDg#_AMxyZWs4@_gGfoJa1=Ro`6}X$;Iu)=PK%%e$5a%3` z-SZfrF3SNV3J*_N07wN^0Kmz}GauwTuGvKQh}=!@iBwPx01`zt07wN^0Afxd+It=Y zUaDjm13=Bc5P%?<7!R5S?rQf$DzE|o8j)w7&5`b{S)6l3dyGj%LN-1m3J*_N07w)( zfN$=X>vq0tZVrFiZdS|vUAveqmYd!3(7t=SKQxDS51{|beR9_>*EgHp{P@|n-R@W2 z$Cyw5(tY}7)gI<|%k}N-&|I&$5B|b^aDLbBcHhrdZS!5bfA)9&*|y#7m-|DzKFqF{ zhqLo{58FitsQ(Wj7L`$?@3BVDOCWt=6e0b}%jvEwV7r^$wawx=rLh3fJ^j&!&=tqg zhER(cZ3sP19&HHSSsHB!t#SXtRqcA!%-_#$SIrNd1P`4mn)&8%XvP;%V-wXAWJ6miX z*L@?r`24Y5?iQP_+3(uq~!!Hq|g+YxN z8UDG;@nYB9Zq~v@WUL*;=&^PXRma*v92{#0&84w+&@9RzLgU5|;J-H{=gn$)z3aC1 z#cV%+SiPU^+x334dluEzw~in4X17?bo0XejpX@gehr5d}{7=rdcg?;<%FFkhvic?m zjymR)=gTUdX(yMD?f#Ggr%$%fF%ofpm4%Lx>a84qvfk`U5t|MfdoG=9>{*}fBx~tJ zT(-^rpw@DRj}*4jx$c8YIYz1Wp8YJE-TT>gx7^=1yY5latb6!#+JUaTM$ zGjHb0X13bgo{k|u*)CVBW;bg-EcfT%18qrceK5531F+?1ak*YM*IgjjyXE}->2V?W z;JpA!C>1!_m$5&>#@2tLb zpWC<1Y}@79z3004i}~uIdtJ5bo+3T0yGKS>i|*YE^|oq^xvryHn(M}xrMXU{mgcymn&tM*PG@67OH%v_dizk?!j0_&%L2GjGh+V@E(!RP$A7H+$V`|Q~RXpkD&JG z0R}s0N7p`?p+dF4JiasLnI|qi9ek$uQvE(Jc5%NqiM?9gldP*&_oVk~bx(S)R`;ZL zRCn*X!gkYtfn@)%x_NNd&;Ihoe|@}UeP}NhJM;!fX78~@7fnR=eze%^?wjt#!o`oe zI~e=!ruTpNkN^H>XYbCgF4xU{E3$dj?jE+s*B_Qf-_1Ac&gR4B0e$7@Z`^y7>)r7t zd-sX?uI;|t(>-Qcw5K`&RNlwCgxu~n-?iOsl=W@+JpASNz0ML}e?d>oPd{~ww4Dv% z`DX}%f3Yh827MP?4dKP_03;lvZ@;>K(4;s7^hH2J2>FFT-vZQTfhQYFSoCrrn~oe8 z(6?aSzpA0lf|C{-Em8y;d1hHaiZILqR|uqtiV%A(Z2os{|A5orT$g4v?Xhk&%9hQJj9DZ;Q290a6@iV)C@aR1oFWQwQ=0qLM31f+wC z5RfA5Ab8%9A`BtuX9T*mlRKv3LnM-b7k0o!RP4$0wgxZK&u82vC~vcrNDbEeo0>43N3GfCMrk1k^RjK>%FP-y|7f9T8t0 zkyOwz!Sw2gCIuXr+;l{WFc5Je#3ur2gPVu}yDUf>TnKKlLz9C38ip+rZX!Z6CDMU` zC^rNo5IG2LB9H`(F=b~;%bJS_>A*mgI}u0+BSJs|(I3j-c1^)VAVrJ_0VzTb0+_6g z0fKQtw;p3>%HRM<5eCSq6fq(MqzL`djL8%+A_Sy}5h0){V?+o@5hFrCbHs=ckRtR4 zG$vETh!Bt>MudPAF(L${h!G(mMT`gmDMEitV=_gI2mvW#LS_|i1)*?k1h;ld_|!52V_@-{{F{giV%blp9rK4b|T^-AZ>6VfIVm<%+|E%POX!jYtqN^ z(F1?ciZ~@=7W5YNPm?J^Fbbi@4e4M+2uKHl5aPLjW{d1xK&uCRAf2s<5EqaR1R=yH z0<9?6k7&e0K-%C!aO*9SfDw7CicPhSFc9U2fE1w*q_Y(f&jqB7?8oeo4g{kRstZU5 zBSJs|84&`S83ZB3(-CPS`{4nk1AS_KL{TDbWRC(;gkTgxqC|=q5du=gh!Btt^nr9X z9YcvgiWm_B(!q!jkRnEefD|zz1f+-&As|KQ1L8)10x3ct%jYTr?6?^NSgSX81;|}f1_wZjFhJ&B&>=;P2m$F}Lv4fN%b*hBzD7(NE$G@c?bDy||zGbPf2fhacw zqzHW=eKJLi2m$Fp4uV?*QJZ9pX^A8^7toYpAj%B^DMAoJe04OxdB{ zpqXJr2uKlf5I||t$IbIAB1lIh5CddxIwD1k2mvW#L^Z{U zI~oIoH8YhW43N1YAVrJ_0VzTbg4-`32^eEq^k60e%n|yy`3s&S43NwyoOfBm-lExp z-NaukWju`=QiNa>LJ*K5B2g7!d+e#E1~k93ck*h|(A!C`#@}U4jE3MHnD+ zixMe9A4umaB7RaZ#Ui~b%7O{EBZM4qjP5EqaRMudPAAqN3) z!5ARO1@6jP0K#PJ2qR8RMSv*iYFvheP%7kE9b88=(BMd}b z2=S9Onl0E*i8zTsU6bLq6*_JdLIoOiP5MCkWQq`k5YGi9m2Yz=0x3c;3L!2a9gGM8 z2}BM8m?QLY^A|iv7$9@g5$Rw=2uKklLO_Zb5du=gh!D^mp%3m)Hb;yI0V!fc2uKkl zLO_Zb5du=gh!Bt>^pW++6d?z}?b?w9j4?-UUy&jVM7d{5qzFL>@$Dj0&qc} zcn?F9eW#ztcccgdB$o^Ei9oXjHxbE-kc)y_k&y(9F)iH8M7R(T@%0IGo1shymP0Ng zbU(sClp6w4#E1}(4n~B4=7T;l>qB3dWeST(GRUh>#)-L`*uKAJMR9U>6LM z3VR(9Pe(LcupxwQi69*aCL#m@DMBAeA5Rg+m==e}qa)G=yE=vvfplO5WxOaLMP!cx zQiNa>LWw|%&Z7N;+4ILM>;S-GUte(k4OiAs|KQ1L<5vxNalj8)I6b%N+$YM;M55LqLic5dsp( zh!Bt>f`45=ZJU-U{q7h5e6bA1c5|Mwz7Uv zQQ*!IAQAewd45GC&k+VlCLMzi&>S%$1T;sC2m#FzBSJuPgd7C7K%=F!J_K)zBsU$= zI>JDd8v;^!)>dWaL)qj`uZdW$* zLsRTlb=|za-5-iWxxfC!`P;cyv%7j%Zs&{Ya5ek#xZ78&a=xmc`o94#Gsb&0@_08R zk9Rxrcx3+2HptNYImDP53^Fq+&lfU(QvMmMEsO1HQ=X^0Py0XbuV&X9HP3-6 z6>H^{h?N@=E4Lz6`nYhT+sB1#rH>2ON*@eG--y*kOnUE(2846%WtI>ZJF*$^9ut3zy{hX97yK)dF~%(ZN)$9=t3 zRjhnGCFkz5UA5U1&AfPD?Pnjt(_h%gdkG^y0VCgOnmsh7TAVKH?XoEk=LsEL_d9*v z;dp=lT+dhaaj_}q`%SSp2jtiKm+#8vP%Vqid|hnrZx{99P(Nm~e|@NTz_>4me8{O* z37|&oo{DCtb{W18Uy0ksWx%&X48Z~2CG);XtUvPb^KHK|rSVvS} zaAicz1y@E?T5x4VeFaxWR8?>#s-@xBqNDoZE588w{6-V7D_n9dDBm*T1K%>@3*R#0 z(?H9f1`O$nZ&@hL9?U{<_E<)7Hp=pca+{zd!m^L4d- zP>V>l!Ek4KQZ%b-tNM%cmis6B`gm9y`^3PdVW@^-xOpn~2LpoZln5wxsJ3-8d8o=_ zj8F~BpUt6euk(_XA_*LZ%3@V6P|~bI%iXL3ykyl_p_OwRk=W&^XDqunV|zZ=nI)hsDjNx0E%6`L_iHIp!N*t ziLVGM!31`c69F|aRKemkNm;+#S#Sj?1L~YV&j8NsMi=6GAL0tyiddfmxUzhPu#gFU5W&CCAoB6%uzr1}b9f|eL$gdUf z%gD*&kBk2w^lY~*-j(xr>TZ&nIqKDi`BQaR&lhFWROR{3()BOfd*|vV&V08iwugD~ zP$FjXEu-6ge9P!|AKx;%-N(0#ZsYSUqucy^%jg&Y-!eKTz_*NE<;A!B6{sP6%jl&l ze9P$jp?u5VWsAo5*({@nPWjRJF&pOJWwZSIY?l8ro8>=bv;5cDEdNb5%YU2A^512% z{P)=`|0A&c=ekE)70uguUA&#|)KSF!Q7x8LAEXc6UVo{U!~KI2`|3fRXhF>p!zAj8 z7$#9W#4w5aAcjfQ05MFW&WB+VwLA=ysMlebL`@FEB(@?O3Vc+dZgK=htd*5VedeWIe-zTxt9)mP37%XOZV5Olrr^Mx&b4MsZ zUh-bDtV}L%^Le1B1!GuBY^qEI!@9FK5eoZ6luJ;w5u1}4QYg4J!QN$R%7e(t_bQ6NpZxKEo=Yp^#8bc`2=Tl|@|2h~(a?d$qj}JunUsbkQoaLd(xcCcCYC3{P<(T7G~pKST%s`b3fx0M}*o{TE|ID{OgFfW8f5;Nbx{XVP-Z4GsDzOlA(C z55b5<;Pc{eD3WR*RB$u33PuBfL@^owB#O}hAQeaf0PTihK1fq^&R?h@K`OYNS_NVN z;`;%l2lPOQPzsO=#3B&t2apOz1AwN6(EuP7+zF`w)B(eMunwSSry@znR{?s4DGq=@ z1TxDIslZ55-i8FJU^D4^5mh*sWMDgdSUji#72nu|E| zUNKUE0deXGpf@nY)d7JNFxLg~GDLbnZzPC|K#*cc6jB@r0zfmsS6U}55^6I+C`b?J z4Gxhhi01&B6_@}*Eg6~tq*4H6XqXSCW}2t>=Yk)kWEdX- zq=(EAKq?T6K&TQT6-WU9J)vPf+%xf7vUm<46`)rxW>&$EQYr{>0C8UoKzs_29x|r@ zsX#0Op%kFGKnehmp+2~6fBD2+noV@Sz<|iA1Fl1e_Qr@-ZDIkiY`J(4_YFLG0U#BO z1^}sGGyrG@&?mjoYbr9&1)~8#qL2a*PkUoTOFAqSxBw9CGXb!iaq%D(7MdFh>*MA&R$c@`(=k$kfhR8jG#5w#KzSX&4d(rV zPzsO=q#_WMA({*H(e8=n0x1A)$oQVKe|p1yTS&9nc52?Yird zW}gX|B5!|Sz#f9WF|oT0 zw<4ffffoV50ex^g3~ZuxfdMgZB}7xhXaErRM+1Q70x>h}QhXi$FXHksg>&Si}QBbHQi;kUW@2yF+`$NCo=%J8wF6 zNr-4~j3`RNPy~=D3_Ls)ga9D!j|Kp7UkU)|i1fj2yYRcDnW!T&Am-%&5``21H=>C4 z#)zZJ5UIex!%Bf00YrOa#8D%FW&nMz)3XWW3Gxj+m+{B(@;z&zX@4*fV3L@7M4^TRao>QLmjg%?QUC-} zz}qPW0QAxB+#QiCd{q1~ro0?LGk}37F94*1(EuP7j0OPB1)~8#bAdkEJ<(hs1pw3m z!~9E~3k--X845&zcbZNBK$f{cAKbpW?-O?mPv^PeyP_t5C z*>X96L}B1z0dPk|DlkU0P_qD7wp=_&1qL1-fcR33W(DTi@^}D954@GpfSwS^g9#wi z4DE3_L6VKm~?* z%Uw5QJQcY15bZH0Rs@3nAyF83@&Z6A(C6=YDhNgZiDEPWNCl$-Kz-0?0FWr80DwAR zm=CgKqSdiJTh3HL{K5q3fqAyvzd>R67fbg65Lp1+$qkWRpXIif3M>G@03#I`c=7^3Di{p_Qh^wN_)3Uo1<5Zsc$!kA zf(askkzw9)*A3Z3_Y3q< zbIGS{2B=^(0B9~44FD2_7=Y7UU_DppNe0qG=0b$#f(ask<^tZ~c30M@`0K;ucD8th z$a%&D(LNJ^^JzNUEjKhs57H?>WX~0W)YY+pN0NK(#C-!ID*{3PkSL@82&9175yj7jNFLHhxe)F3Y4&kth^7VuPhJut6^sS| zsbDk!s3Q^sz*hn9jtQs(`sBDxdACns_5=QNL_+{fG#3~USu%7ZifC_)XiXq20G2Hm z4^n}FhXueL5tV#>wAn^GF2-#JqI?sbDk!NCo<6H%|qC1BUrvl6dQapn0SM17cndpt(Q_fItd( zYr_G1#YhjCpRhnG&_}y@DhQGgtqY_8xRn*r-WbtBJ&g(sJb3}2xnMK^i2I`fKq}BD zy(dzE7y$mdfcFIlP>K!zEX}#P<#j}^Jw$tqDX$EX3Jg3v0P!3^ddPgdAI$~&Y&lPc zfP{wmAPFbhDK#MSIFPzJ76TAZLZpYxBt+cTN4v9-&@dk)A#+_2p90h>Wj@}I)&)k4 zrcuFY01)>_1AtVZk9IRv;2h&Y#orjw5=LGQAnqG@@&Z6A7!3ewkVXT5R4^IrQAawC95Vc_8bh;K^KtbiVJ`PE~!EtgGk zfB563dMxKpb-j6gyFV0%at|)fi~+0m_Vg;fC+R1 zeSit{>F(caUhe8mwO^O3`C?Nn-|i3fww&)b#p1+|yIUDSs| z{Ro&ZF~aqs-U0JpYEVsCuJ$Li*Vn~EiKt9ogvd)?gy>3Mga}GrgeXZ~gh)tUglI=z zgos97gl`nY*p zHuFXO{-iA+#lO;*K9$8`T{fziEbHA)UHwpR%Ex&-MIO%+-rW!Sf@RUHs;w$0tEwrN zhpOIwvagS7GP?d$)tbHf^MCk{|NfU(udnWIw#8#9JJiinxj&3D(kI)x8Oy1%fKGj< z1OHFQ?SpDW_Qh^fZ68hHIifo6~aDA0T|zyz9I2ADu|%>WZ<<{4lD z%|ioBpjqi#gGg=HqZ;yNR~}BB0Q57i+7(JXb@6t-sn6oM`)pTjHbpZp-dFqC2MHtZ zC5-$8jQpsBaH<9eB}7#Yx-Xraf&sP_P>;p4iX_Cdid4n3iiF0qinPbGisZ<%iWJJT zibTt^ih50+Rn&~~tfFp}XBD-xJgcbR=woAV*Ai8As=f!Z2(n&V?Urn(2i^^=}B`}*cIUs z5C10}+ff=PO43VpDT9&-lJlZ)b0{{Oa9$a_1JdVkhAYO_XNE_h5Vh5g*2#-4cNB-F zbSZ<9BC|w$%^@$Es2MdJR|irlw2vKG>7VE@$g2tabj4+B?vgjGd7wkwmy9^XC@$wv zzL+eLv(AtzDeqC&vr_RM0xy^mmnu_vMAJ|!eYi_6r{INY7aIjemOwuwDs+cdWXXqQXH2u%u8`;3fHexmojCE=9Shm zk{7GUWJ=SdK24V=+U=K?sKge6c5+y;&r{1I5az2A97U0x5w&b$QHyLe`dG^636qa* zG^i*Hs+H+3IddHuK$2;L5To|x9LjW;^2{v=aNI7y_{?MGkuE!iL>YMk%bvK1l2kPC zp}d`m<|u$s^c-8vEQmSD$P%dIU5I;pAdf_ZQBoqiV&(!D;2khHSz;1{!s+xtj$-?l zM8x#umM2M=RJ7EBTMQ8euM1PUlAPiYjvwM#Kh;w0!vJ{-dc@2@&qF}wQ9Pmw6U~sw z?k81h=}CI2E-~ueFeOFgjsD5Uz&tSzoY=mkJiUMd!LLqkgt-!JCxSc$f(-aaSMpMu z2Wp}n5`?L8l&?52)=CuV!?eri5~JRsc5T1};uHtENenY{(%L}5;{=6~qk#bqt{cev zQoWCz4Y25JOH$8eMsXnLq`1+Yuy9-XcGz^695}QLf?0|w7jc0~_X!m|O##PiW)fn( zX7~iC7rQ`>i@6h*FdNccat322ft<@mDiFcq<2X%5nC4Y%N5rcTNKNf7o*X_j3Z>a} z>LrZE%@Z9eP0vxCA{9p??#M^~M8qjj_q6Q^a@kcH`9#GkD%|WPR(nRbM|b3jcu38W zrpal1$hn8~u@|>g6=W0$ zFgNMqL+byTdcrMZw@gm=X<(YV!ahzI(tS$ME zoC0EqJTVX~3F(si8TyoMexl^j%}?oRKz!A$bDl)eg34X+Ihi6972y>96J|0QsRG51 z2Z*d}bR>)hn&A~MS7gt`i%h1wPZ2TA=u{?Ek%)6wicE=`)ThbvMAP#{Dk_|)oK8)a zDbnp1mndpan`&@KFZ;?8*#XPXQG`#-wN%7?-_~n+mm(J(!EU!t8<^XPxMffiF&Z;V z5oM)|10zjT&zBryQg2jhvv+(X?<3Wo8kSsbL@=N|;4GO=XP&bsMrwstLSzFNnK(h8 zxQic>uH6)z-6u-R5m}*sGQultzY2AWbZNlSznl`h~f~%wu0eO~Ey7A4z$a zBGnX$Z+IEv%u7JJk!uUG#&tzImAwo#4V#&s0fH(ws7?nnmrgy$CsHgCKG8oJk~%@B z$dW}P7Ow8;PLVClvkyRYiKQ_jFw9;L5j(a@d~B63qFbfr&mrn@h5f>)M`|)MPz;xV zRJtjbqLpwx7$Z%&A%`=Wo+mdxRLzK>4VBv=`bu}{g*f0uHbb|UIPs9(Uu5T$X=cJ6 z(JG+I`m`R!Ei&j{pc76nK2TS2+!zfxsia`U^SP8kNpV>s>)%J2q--8>as<|NtN>G7 zrkSeC2V+hGE@|83@*`3DWXbHEX1D|@QRLc!jyWsUKSQ55YU1T7TY<GjiAny@J+GIf^nW_sd!y`gT;lXKQa-xz!Nw}S4rXV&7Q)P+F;|oht z++IpHk0R!za=V)E>PFSu`F&G7malL3hvHD~XI~lrQhq4si|TN7_rqhgEtgGkfB56F zXjau$UAC&4a(Srg?I&IE!?Z9AbBDZXRWRy^pLAtv)FE$L7Rin{%bQ8sdY3dGzZi6- zN9>a(Jfzngjf2YMpJ{0=ipND=&v#9^-yfTDzS}58o`iY*WmB&92la}p`M!Ek0z`95 zvcXq?Ysm(0qz&Fm8@!V?_*&ZF=h6nhkT&>6+Td5x2EUdz_^q_T@1zZWFKzG#X@frk zgJ0`$cu2Gcl zclEwp&6o9VS2puQQ|wlC-JIOG{>8a@x|-e9yK>vLS?zs^eYGm*tNN+`8vr(bB-f-d z_A6uTyd&L6j%0XnYuI&NW&31|MCM795oT5#pF~G8vyyitGEbsD8F`vHJ|XeP>&ak* zk*CZf8F|Vvk{c$&OCxjR<%vI%t_yC&W`=vH=Z$AX>`15?jvvW@ZDz$uABx_u_5AdB z+^?Iuew^Pr)`f7cADCg&YQ!D>HQcQ|16zddQD4Hp@(5fQE7FtlG*g|WG6kBK+ zkzxz2AX03h#Y2iMv~EbTg_aB{w$N%J#THs9q}W1hgcMt7d5~fYtqi``^=q*ymT%{W zvT2HCeK-`S5(YXxWBXF=tyHSFZYp)X+VQNK{pnh#j$U2Ac`B=BRqL%zR1U=kKs7RI z02l9#>s0O#p=uiCfdhBu`XrkJ&j73(#CU)vFR>7z92h%!%WAtV7DXd21<5f`CM3r| z$&egFJRcs`B3UH+;Tc0bAD%J9^Whl-<--u*sw{W=cK?0;P;XZAx;bwnfdcxSo}G&A zvQnE+^Hr(lne+StuKQA7_pUf@4yWmezN66JbDMoytE=nd{_5_t?&5YzJCt|o{+zlq z_YQpwBW=k3_Koa_AqN6Mpzg^fI@#WH3!xe>leZ)1F~@k%H{txm$uVGew+-Yl$9SX` ze{tMy>gC&N`yk31N{q=vf|zN0FG`LYmS|+jY~Gds)Sh`5F`20&f?S@>y{J^4QRU3! zZ1g#Dj1jHsDm0|Go1iIkS#{AKWZJB3Y4Xy^93}m-yyhQ|%wiS?FZ!Gj9PG}Bor}`t zfXS6SXfHrn;uKjTQM-;b2urlgS;D)ha>io+>qVJ6P3`nJ zwo-=_c0!-)Iy6blg6uV}$16 zz1bJ-pXO_|3lRTwggzy5)!rd$4+SpTeWC8$ADGcaVe~7S%d>_OC@70VBSmfJ$_Arb z{ZP=yNXc^-eTLDURFcTaFoIglmI7SOz5SJMFh{F zj-biOv`(!Zl+AX1J=DIcXau82xU`nNA*DW5DQF?b%U8 zJ-e5`&j`Q>77^5#(v{2RR{(`Qo49ICgFzP~iwK@U9XCYy8489qo47K`5{!0pRnyTk zs8BEn;axdH&s5VRs0|c#lx}gdaAzC}e^)k#YN_5Pu`V|Ew+nTayMD|#!u6rv z0rSR;X|D0m zo}tKio^z&X_RKY2*8F+=lgyuc(ErvwOAqvCoV|TiyS+{Qu2eOvK7=-UjY9OH+-&Nn z=f}oRa=-qxe~f&8MLs(oZRVAXnHw21w=6T=#WNitkeY$9L7iT-nLajbGkt6#%oNLF zSrvk{-S9NoHmU;>wvpx~Y@_--*tVxup;|0qJ2l>J&2t~v662sJCMe!N>VD?=okWlI zzNnUhG>;s@jnFG+RzR7J9KyBd4vI)GID~XAJn5DWlxGN}gw#NJhG6ExO>R=3v1r}@X9vY`1Ou^vh^N-TlQN5$6FzTBwwi}Ac{sGIuMs2aybyXsr2 zo9~-bGX-K}th-LP>et83+x7-|{XTH*Z}o_q>S2ADKQzUMv>tAdDY}mvWQy+P2AQJ! zxk0A-eiuzWgG}|Y5-~;hc7ssS{oUVcUhV2lwO=bCbdQ|(@84s-pijm+s*$!6jCFK`Nya*QPlt?k^yUs3>*!q`GS<=CJ!Gt-_kGA%M{fX;v5wveB4ZuB zB}B$LdT)q~b@V0?qZ`$0?5%pO@k94+)l=7T{f*j6I;s~Xw=Y}`zXXJ571eW|RaD1$ zR#E-tSw(f5XBE|Jo>f$*c~((<=2=B`nP(N%W1dx1hj~^}{pDFjb(d!q)mxrbRA+fs zQGGQQlIQ+eEzP!-dYy*Aoj|sEVh#M2Zv9WM$T>|1U;+C#`ilLdy6Lc1Yi2cNsQ1tI zs|C3`ppqjIAutwFhvWUd_#1+x?4ub|%08MqW9-N5GNJ9b82iqqrDvlow|&w0d+>AO z9}^$c#az5+?x-qO^nR4_9_sTMg8mKAi{s{e{-M9^Hv4Q>KXpe0+AnN?+A_f4Owyo< zZARWEOh$-ygb0I$epw%g*Kc$!UX{gaey_e!6159yd$t~aaqnc^~0&XyZdZcZ8qwSh{gMAKl=bGOp2}dg0_AF zw!(2rn}LSgjhl#u*^Qr%hS_bu2yuxQO-shjt=;i)r(Wo#>aFqa>^Jx79EjRK(jRuX zzE!(NPlq)Ue8}SS<*%+?(|`jZi>{#)MkMWYi%8Gu7SZ-lx<#}#lx`7i3xzB`=lY9` zxqfxZbt)en_M9!EStsN*wkXEOGe$8!o-K;;@oZ6yk7tXie8ibSTa=+(pNgT&aIsw9 zoER>~$FoIwjADE|V-(}#*`gR9&lbh_c($0zM~C6JC-#Wd$FoH-KAtU#@$qa?jE`rF zVthPX6yxLBq8J~~7RC5@$K@mLAH_0r#wffZGpIiPPVe&+KNXv@P_HN~ z-j)0Ly?QLC-6lV6)6Bk5ugKX~YVob!u-|T-&t4yQtM*0x#6G%=b+k8-Y<-p$`-OFc z{Xf%j?_QU6`hMoc`?BV{BKQr?f1wTS)gzs6Kg@S3NorfVXdcyG-9{awRNo3eWziKF zYP-00QE}ngMJ0x77fpOzyQti7?V_S%+{Ij;4wFaTQ8ymRc`BNAw|4tsQytGAssr0! z#<2kX^^WqfRA3y{Ob8H_Q!?#)-dATdTVWESOZ;Jqu< zyDTDc1SHeH!y*oAR>^elQJWrzpCPl^%cvm};PBUw34nNO$OJfsFoan{XzhAu8Gk^s z;uyk^tUv}Z1mQ5Ap4?QA>X49XoHpl|-00UDkcXcljnIz^MjD|X6pS=NKOz`ugnl?M z(g^)nV5AZHfxt*3^rL{ri86I|)rqq1)y=16!QI!^KgXV7ynB*3{tXBb z=ADDGp06IGtqvjcXvZVPyys*Y+E@}YkM@^@%rj!|GAI5`2gdHr?%3?qv;5H?+kT@r z0uM#=P@cbN3M6LiGCbAws#IU+KC0Kiowo{SU-f^UcaL4G&7gLlH-7(6$UI7O$h-^m z#_Let;2?>G(0ie}E}^}}ka^eAYww7H!BGXmIjNV|d}ijL1~(LYU!Xm~kz|g?d@6Gy z(WA|2BRJ<5#$9~lr4&^t;T-qfEw0*>vJ+ghB5Vdx9N<`Ej$sGev}$A z5)PXoase5{5YMnLgdwhEQZo|>$mOKs8CEPH_Dl4gSAvJ;TKi4i;}o-CQAtfqyE?!8 zB9tBKz+O&vOm{b&)s3x?#}MTN!HU%J5V}| zBZS5$Jo*RCFZPEl#e~{Y>kC|N2|?1$1SnWr*?8>IHh53O(i9ueu#8zsnml3Qn#&Ck zv-Z0A9a&-)U)v1547N^(& z;sUZb#l|zNCfby8DUJHAab~f}@r2isV+ZJRk0F16hOQ*tJshUyX+ekE9=pi16J|@^ znI}Ub%Uy$p0?J&2LgBDFX_wN{MKJ)#qErSv(hOo#+L|>&0SeWikrQUmVpH@Tp^#af z5wj*bHH|Kk_AK@i19Zu$i5cW-er$__wGQW4IW?(y*)6^8hU@Aaqi~%lW)0BVRFnzoTcNKM~7{Nf)jC; zj)OkpEFH&u#96v%^PZD{sECJR@1Ey;+QC*lgObx9!i5x<@Y!hgm^Wv+@0MXXk%YY}M&^eIWhQwfe}gdNBZ^Y>F*}%Xp_i z|B1Wq*`xVC{Nrir0lY@n{maDb{yy=#e@MLUUnO4muj8+K{!YDm6R>)1z?pX-p2n@E z<%gnw+4fdFjPS5goQ-)uiZNZU+EalE*37ZYORfxqX^R&MI$+W;Yqi=JOLBJhQhM5Dy5O^-$6^}})f z=MWnx=R<6uY#X)7P{Dn1uCUj?>b@L0SAS)^P#GvA%^K*D;Va~yS2?I9Ya+C!B3QWubAy{wCq z=&!HUJA1CKf1zK?4ZWZp1a@9Ai+r6~;CdmaPpJzCB@l&L(iU zD$ou)o2FcYUmSPy?$*!Ty5qr)<>$(Bj-0>g*pvq|WGb5f5L0J*C)1HbM_O*)?E67n zdp1)g9qvgx5Fr&oi?jnRI|%_y9U211F2Ph}+jwQi=_Q7O-&>Ngj%Fko>u9=?v5w}l z1nX)8>Zz=nRjnsFR5QcYU0d$kH_|!dTEA(xz4q0roOjh!z3L#{x@*cE55?VB<~?uf z%C3rs7$sK4#!lPwzCpKSI~3Jw7dK+v?a+%1A$A2!JL&Pfc?C&Y zz%Sm3=HDJ9)+;xnX8dfE(mkW(EbuG8j(g zOSo>3`HC28|5(?D^}KCy_UbLJ{lkj_%N0Xb=|d1xuyLp7u~s*cFWxE&(rJ8Z*4a04WNnfRha&(%SMl=KDhAw>16?p2@XUk&A3IZzX;#nT|TXY zQg1j9u!gv9>_I($lUlm{ju?U2bcaXqPc5Dn9}sfT2*!kxX#``EcSk@|R@jHw$sU#B z@CdPaVE90AX=V(Ms%qE=-F$o6J7WZqD48vmlOB-~$W}{a1Vay=h)F2U(1SZdI+u)O zl6eprfy{%*2xK1622s@pUv8J>{6T%a^mOU$GgacB+NUAf*W3r5 z)QmhD6g95Vwhe$l-OGs?HsGNB@cU(SMhbJ}VYV1|FYyPQx3Y^Ggs5@P*_{o6z|HQE zvFsCc9Czu!TKTjhB1NjeGv4;OE;OOWWH;chb$0S1DV!)@8lpki+2y!9WLzuPV-0I( zcdRf+o<8K0%FgGdh(>ew@;v}(Sl`4gx{E0=KwRJ9woK}%Q*(ZH&E24IkB}e<7(NJ$ zg1UFvQBVgjI|}OJ8KbC9KK6{F>?j%X;_)RzUOb~jp0~q_{c?K}H$pr`S zbT0atO=I6YIYj!Vk&D{AEtNcgkz_Ils7W1?WHJZI1H^_1%@adBFm&MFFcQu|o&k&; zk$*$3iI`9kz>hlm5kvT}#fuUqF2WSA}#LeM&f8T#JIMjt0x4q9@z@}8+ zdp=({`$ByQXkYCQ?HkD2&EeVW<4%2LT78aM{Q*RiX%@LS-0Un5+6%J?t+Cy)tQU)o zYLw2M-l^>Y@iTJy&&Q%U{4^Ih^pt3n*dq9qKFz$m%Lxw#K`_9yz1$KxvSV*~M~ zI?Q*+$KBiNe0qw}GI9)4jU&e{`K$-WF!g}3k&bE^T5=daR@*;;+!;S!l;=dxKChR{ zFD^VAE0*X;|6)W4~FQ$%eUYWNk;umSPLv1GXe)C>^Y{#wS2fl=#|WH@HJ z1%@w@XWAJr#;Dj1MSSbzl9jv}P9B;g{fLdMJ3+Mv8HCs{?C_Xn7<=x-SSoo=tr%w5 z2t_Ey$%aEQPB!cY^)dm-u-V{lisf6iY15zJkFVp0?cwy}*=Oo3!lqUajK@`KbSXmn zgKiT}4;-k&XKHV4S-d?@Q?qN;6(D)VTR<2Lfj*Vz6Cyx##tEcVJB=NV3k9}rcjHnI zd^1dtMe;Iy@2G=gq2-hyUN5Zv>nn12y zbgzVK7u_%6+Wj0kF#XvZJ87%^1aVDj`F}JfomFQ`VOZ zdGYvy^75lzdMq}_wuv9|q_eR_t(yO>dL8fS`cp>-WNKWvY(BKTQN2*FVW{-eqTs@d zt-;53h1BfIb0-fjGWVu8-I?<8L%C4@Z(lzKfZEHF0S}`hk}^ypMjGe%S7o~nJE*d^ ztIu&U_5cv70VV`cAkq&&ff#LsR~^yVwuN8r^U6%)#NCDOwGh?w3hLtrhgKDbLfuIT zP}F!o+j%ltw3{CN;hOe2hm(c7-*itsD3NZiWJO% zQU->|sVINxOBOkm>BuPY*+;R71d;6a708?>M>emLG4oQ@$5Fs5T++!@36=({Smm9k zCUQz(o&pqy#R>er>p+N-KA>C*u+Lxq24DbQqNq@!yJ@2$;1gCEo*y=7f1B5Lpy z^)h`5x7c85@lwhJSp^1w1Tz5S!szv!mnkw#U8|VCqLUc9RFS{XC@093f9;zLiI>cm zkg9%e6%NY_o2Q79J)p9TUMa|V>ZSNJEk@Sje=oHkXNyv~=a zjG3U=@d8ps$u2!A5}ER^QDs)a&jj@k0b4SGWw2d+sDjuv3Dn$1{Gdx+q?i0!VR=PX zCjxY2_cWSYoaBT{V5A#XZ1X7#)1DKDV14Psz~|ryAi)qYaQSl8RzZ?sacy531qI3= zIVlnqVn|uDg#)suLz$I>;cSt=U^p(3F8@TSLr3K^mtT{sE3~wnPFlCBMjLn2)!azL9|+NWwXmZ->D%*PzE!|wGN}WQ>;XUi$||Re|?hlDi}D)Tt>Rh z&lT{hlYWqJA?e4Z#)@0sww*ds5m(QWoXR^_k!%@I&j1;;iZZ0i-!FADM_QzT6LD*{ zjvfN4@_W^#f=pJr-&B?DhZJzjUuEM$o5^npWf9rxIq3hv&p26m+?L1{rXYp+g%?4; zQ?Tw*x8a4Nz%Lf|TtU?3AZDrB!S@Pm_@$QuRxZT~`2(7kOGSAZ(|geEmMen;gu_Bx`q_T zY+l9jk4#=N0R^fbF)xvrBfF>=!dV&+P}R$GN+MHKkDd}|B1%+E*QG{4Q^Gz`^>bpM zh7>E1nUn(Bc`cAA|DvEiLz!rG7*Fix*CHfX@HZ{W{PY#26$lwRv=X%vXR9tiC{rGOq;7<^-FSE zB2|7J$8C>=%2|?AdFLroG?KL;gQ{?9s;YRbWvrv;=U4;w#IKme$J;boG{wGLA@uf*>Bb{$4wuC+8EQ2i{6yj=8z^ix`=B1Dgg zQzhes6<2{wg~DcyNM(|8aX@*@mMByy$XSNy(8u-3&;JfXWU-P?^~aT{$RG(g<Q!u_@=v<6*w6 zce}Eg?>EKb_3i#p9LoLOul05B%H~ili_LspZ0>It_2E!Ijx^f!q27U^{!9a092cD{ zb)!H&sr=6Gi|xsi>o1qx-><5sTpp@=ixz0f246`dypc9|3k-g%W2j=ZeJJOLHcCK*H7{6=T9SSs}u^QXFAouqvC*{<4bie_HCulBPKfDuD% zyce+X6R=?v!2NM^e^iyPEleFh&mQ@gm~m$#f;$r35Bwu^O?~_CcGtvk#xh0rA_Vrx z^+Akt3}-;sAC;Mio;e1+6;ITWyZj+I^;n%P45}3+T&A^#}5JfmQb~Z5U+D8EN@t<@t3q@EM%_`B5EHV4$=JHX;{}4G;V+oKXeK&DXyd-|mr7F(5iEPsMtWYN zKbbmdGWxm@fk!ew!t_MeDnJ3$<7CPX1y-_@S@nq16j-LH(l(ZNoJ!!IMi{&Y#EM#M z^kU_>BiIzgtue!eL*AXJSW@I;q?hB3G}n0AYxB$TW*F7T^?k5cCVtT6vP(9@nIT1u zz!F!odu9g0$8d&{WYpMJG+3$2Rnnr?SY-2~i33XZ*u6lISVTomc81dm0HVcpl_^Xl z3tk&Oj%xgZYMF@`2B#)&Y>>L-XiT*$ zkf%uGY6bG3C3}H9Yn!g;B$}{%jSnX^R#y>>8xV=0bMhsiol1I|S>95u0RMJrXF=YZ zj%UHu0P%yqpmjg+mrNKhPDPUpeMHTOa&MyKNWr&4%E=DVgk?|;Q6)7w8OgB&1H6k< zQWRJ)f>sj4o1ksWU8<^))YEMksPZDo0q#V$&@Zm8vD_ikp`#7P(Co4qAkVhA^knjf zRoVPY4n}Z6HsT`m!WW=X)278XR(AXyNLQJO!aJZ+fo$c`jx^VJ?Rc9^MkTP)Oie4n z1-4XSV7LQ_5Jfd9_*O_KQw||5Mq!*NITZQQy~&juqJfF4YR_Es7tS%uT28oR;bYb{ zs<}-JE>&AX3Gau@q@B3IgF~OYl5KTcEnxHs9K6}Bo5#qSxFFcXmFTuyI zRHp@mvjGaE3kxX~{Acw3gbyi~Q{{(r;6I53#YSMN3+cnr;n2!JfBYHPpI8+Rq=$I>N-=rrRxu_SLqL~jL@lN&15B-a>)3#?7g3ldIp{PRvaSZZ1crn?Uy z!yPNpJmnBe*8%ZicoWJy#YHOC1JGLmWhn<2X`zF+AYSb{wF`&kOokZ;4MnPgzigdw zO&wA5I{*C1NAY5%#$VF9`6Lh8V0JUrfFN;#73^iCrmsf{Sb~8MR!B}}{5(r`hMKHM z-s(tT^Y|AN;faS8A3IYB?u%dIl1zwIj?C2D%3!4 zS!)}AZglYe8C$Am4N%fV$q_W!!^H*yU|H}|9^T8>(~Hs@`DRCIeJf@P{;CY z!aoM0G^Z&5y`|6sG|3%KZ5vQ{sKPu%>mY*XWO2CWuLbE!r;1-(JUHI?SuM0`OQIS} zIsWA2EY!jDO_UvqI_YHPB*&@)!N8F{kxbEMOiLXh75f#tx#Jw z`h4S`F7nj@N_(C24Bc2Vr%Db*D?#TO&QRSwNihBKa9z^4fHn6UD zyRtd`e)jrMKRj&8`Jy^pUB9_6meuxQzN(sXd8q0wnl7L{*Tk!fA%@~}?cE65yA`%~ zCv5Mvu)QnO9wJ<7eg*7ZA1iTu2xBMfQGY=(lKA^G1pPq6nPy z4E*~-U%x0fo71oFUKfW$v0Oiv+ryo*w_PdqU;jeO-wY?J1;F;jrJ;dd4a03fyr9dK zcbs9iy(WwSqv{2SiDBkKh8MU$T^HL2m4|fLCkw`KQB@eAHY4IOeVKt_s3A!_;~?!u zpuX8xtI~+ZW$=eVHltzNm5Q=$H%o}^K^XD9dpvFq)ovpdV@M0%Km%)snLnR7POL+= zbJUL6#h|AmK8(|5y#4kJ@eGXjAlo+?aS!A-#O*0f1E)qaV;*Z4RRnbepy~}lJuuh- z)EC?yh}tx5m!*qZEohe$WeV>_{Sd^@z17XP<4USo>^8#rv1KuimH? zbJ=Xqt@RK3pO&g+ueQZTud6@l_B&>u?dqqp?d99mHz<9nL)^5RU0Fx?q}wjcI>b%8 zv6OWLPbQFPricqg5d*U|<*mp1r?w81yh2EURv__egbZ>0@>CvEV1X@fsV z8~hO%H1;$%)x-KQUu=%$e7`aLQNKTxesKM-bv^y3tr(e8*g`_pB!FOR!+ z-)Fw6pSI8UH=fF>S=HgvDq$N{Sqa;y;7Zs=wRf=X+4Y-j@%Gpji?_$NSiC*9QM`=> z#G~2~Zt8dCeAm>w;^DYC-|w59_7ASEzdZkAD};vLE{o;ic%DeEuL(8LS0Sy?SD}c~S0SF$SEWS* zb-(mjsKuqPLVYZK6>3)Lt58QuUxnIE`YP00(pRAdlD-OciS$*d6{N31{oePwUt1hk z?VaX%@mL<#ANGe+%LY2VZ}gS>ZFPUI+QahU;dIg6pQw{D%eVa<@k9BGZLKyC)L)>p zFoUdp`fOVQ*=LW%`yN`k0`~r(J>AutYQI)qt*W}I4s9EIT(nK@rdXU^H+r&nWpk*O zYLjtYZ0>It_2E!IW*+7GQ18GH-)ct|RkK_R_T~bnQCSu+jcT`mX;k2cn)dXds8##0)E;#CmC~#o5 zKU3LS03wZX#sW*8ES3RFo!wOv zaa7}f&M+A(*?|)!zz841o1uZDa!D~WK0FL(KzSB=m@TG{vBH^4HPmZHx=26EYFM{V zswU4|bM7(hz)`8}65&^2T=njw!yy_mdbpx}EXfgCcqX}e5KCJsG2l6sR&tVJYsu-1SZH3`_q5f8~T#I#XRETak~fu;qkHsjH76M?HQn#4V?VN|{wn zIP;EWDjYgc3;XI3Y~Z4*N1p6ZvCmoiepcSFDx1H>$yo`giHALn`U?S`6sR7E5YCYJ zOvN<draPI~-Z4#OWx8y}-Ty!`YYa}tF06OJbN3r>!wawt~;WcXvrnMr@4ipTH=%+=?+ zE!2EV^+OE0v07_?fz!nk_``rpQ6SSYE-E{@ssSrK{_+Ob3c=WdbVe>qbCql84>h|` z#jfXXT(y|07{s9i^#(?a)xG)I7T1i17Mgv!{3AebSlaQIwyts|fg#03xUY+8CaQ(T z4p+9McyN+4pe>+g&B(eH=N>0J(p)18Unt+5y9{q6lhcQu9Ug`^pn|qljTDBKEtoXZ zD+xzAFVLM#b51xrOF}^{^|E1%_%0$aLx!HBW7$V*hcKHrO3f zu@`cGqTG#u5NK7(Px{2n-9>OBu2I=bry46a{#ByTI<)Ja{UfxM zrVEEv|%S2bL@m8gBU-d*|RMAm_iq^cpRQIVxu&;ey5Pilm~fx>~p#j0t9Y9ci| z5N$BI@~MG=1JJuZh|thT70q5*aL`iB)a4h9aE_mRYNA3d1KtWJOEtJl3*LgFe$Zv& zS$6&*&Z)4xnXG~CCQg45N%w}68#^$7(;e{$@R1siOBP2r;<4oigyD{4c7kOge{%dH zjpCLNdMltDAooUt~(TWyk zi?A&9;93y&Cr>e$EIoPL$&ek`P7!LOoYRFXTukL_0rG|_eJ{ZqViF*w!zf&#{K_XM zYNeCO-H%2&#G>0s_C(2{q+*~0)Pmy7|>Sh6g(;hQKk z;UX}0YV0-%P&{hgBjlZYZXl;Q-Ue$%_XCDAlBtOU3&L>@AHx~YG{tpQNumJGv`p@a zD(_7J_Of9Y2mY{+U2Wib@^{FF7Er+2-ZB!|BqjGQiw<3AJHS?B#IH-wGrX zg{ho292{>j)u@WARST4r91fOl{He(YmdafhK2B!*&ZGyHm`;MJ=Fc|_p5-afoQeiW z-xE!wKnwkV%Hiv3iZ074?bAP)~)UF?aV~Iwu5A_aM{H?~Z*c8jR`-9qVm@8pb z&He=T`pr{WHLKbX*zC*WZeOj+`Ko>*0~urAHRg_p)F)*h2~o;E(qxkTnHwKOWGVZc z_`7ItoRlLON#RHF|H|b5v7A@S@oBRk^QJb-dKA6XYv56Nop$!yx(y4}(-J zJPaZeXP5z+79J)?Zam)P$c<;1h}^8oVl`jassmWojhc6#)UtYhUu;kEz5cTQ`yu`0 zV4D$9A8a!s#Di_3cp9z9u6o#1+xc3B^&Gn2Eoo#tocZ;{s*hX*sJnT}QVr=l#{} zdV_oTXfv;5%-qPBxs@@~#|E`k17pKB)5nHwrjHFW^P}e4X~}g^#A&w%_SJ(f@83K$ z^>M3uFZGwkng%F=XBCwzo>f%5cvev<<5@+8jb{~=IiA(evvA;xELPuSvHDdOt6yia z`fV1g-(|7-eHN=fWU=}qu=*QaUmuIDDx&Yo`QoUytInl$_ErDq`K~GVd$lrcx7gzM zXT=6URaa~PRDi_>K($zG092O620#@$Z2+~>i{0fH8$gyt9uH($zc2 zvB+p()f#^LP|lY{vnvmQWzC=J5@xT2%?*HG^&0@{ufqKy!z3b@ara?UY}FkvU0^?5 zD`!2q50v%u0+7c868D@oY9fq#~B?J1M$&k*Hh-*JNjsO_l`bV-o2ye$@@)v z&T=ZbM-HAR>yd-!NqXeqsX6y5moEG+qC69fXB;2ev2yN}+E_XF4xXBGI=!EC2c4!8 zdhFa)(wa%@pmrO(67{@x*%SE=u zqEPofno8aK5O|LR(>U=yhdgqc;;rUU^;rB3uZy?y`*N$NYB0=S7}u5RF@vRg1VQJ zaX<QM$qr|<)VUu^7p zpC<_%$?q#C$bDe&s4aoubL;37zVzVUhaXTa3=Dn|cCX=AF~J!r>|TKAX`6uOF>ZyU zlbCbgBPW41Q{SmwkT6HoJ;V6GCaRncdgRzy5*|5rV!ns^?In7m#6`v)Dc@edN6NQ% z>@4|y?qe^s>=Rj*gvSn^CF8M!XGwYN;A7?7OSrLe?j3xroO=gP%z4BdO;^C47HM;- zSGDwT{nFt=`)Okj9>xbYOU6Ae`o>O7xQO3bH17KCC3+T#M~*#GzP)~rlyC3YDBs4B z6ty1TpI;6~lZ;$zrkU*ePf!TbERuCC&16S10Md91`SZ?(IuUd}&>45mW{3@R#$<>M zbgJYFW0z)Ad^jDO0iK(0QF-3ex%fB^=OY^-+wSK&Ub(|%#Emo^!tH`|JwcisY+JoF z()m1*R7^a!QH_oB0q2(AEvS5iE1vw$dX3^{HV zkQNL;nX`bjFdl-t+&r*S>e2#fVZ0b1E!;?HAs7QBjqwm5EsTc%X<WQT~6r=?MW9F)iv@jk5qy;eq0z5$Lgh(F5PXvb37CCu5a1o%1;10@z zhrn{j1%$L<0LqL2X<jdjR7LRG32;0Kw2;WNyh+@-xzY-7$5~0fOs*m+;M4v zr16!O1`|PiJwP)=#5aG*df>mCXm}Wm61vM9*`erI21i1wV8$RgE|P{3B_0nzpJ><* zVnjEsW3v#d%18@e>-_Oz5cCi!U_1n9nh-+(cwpEMMv1vz2--(lFfeB30h$-aLx8mK z?c`bzLjag)*bg#=J1>MP5}FMRjG38;v>=ASZAlRMjUlbXWR3yS!go?yaASZb0%OQ= zV}Rxb15oBHAT7uti0@R9D!${zz~uqbg5mVIJV09betaz$Lyj8*qy+;Ij|Y}JE-jE2 z#1ObcBFY;>TCg+607=6DlowD9BPS`b6v$`4Hh#*mggcuWLp#KXCexoc;BkMtui239~_50Ms(C}l=~v~YFS z0(xPR)J_(f7sf+?q%j@>)FX)@aH)qT0%J&vzC0$n2oU*mA+X$W0U<5uQ{!AMoGQ{R zhy|$vJpd-9Mx+I?7=U@fupf*KcU}nXWFcu77knsQep4TB+ml0B8XZ=R%%J z3kJr?W59fV!AhwcW26PS7`(YJ)F*l#=BrA54(3qRTSR_+YTPc=yetG`fTUpn%8UT@ zNaGNRSr95P*5Xuph)Y)x2O}#3<0PI`b@aJIB#pDSn13xEfP{a}=)y1!sx%&bN@1-Tf6?sOq7jE4YeVLSv#3*#X`TF{5cnObn0F0^rE3~9+`A}ttzG9y4*7!Lu` zf*1mq2Z;Q}kmK?IX+a+%XKKN6#|;UR#&`(Oyf7XDqy;eqZrBm|jUlbn@#+DX^YL&l zWad5+K%mcrzudL6fsvO5E2VCXkru{7fV3coApR^463O?xEC?`>sf7XEI+`JV;Kd-w zL^LnxbL+WhCZvV&5FjncA>eBvHxE$DBo~9wstqYXpBm52M5KlB5Fjm#hX83|JOpSq z5JLdw1;c)@YI5g=3oWQmal{@^(uc@J?=N@|AT5lC0BK=71V{^V2u}0D)g|LR`9P}3 z%>y(q$i*O43;NV}?ie5~jE4YeK@LGY6Ok%J@39O}fLILNGK|P?3~33M*R6w@NuLQ1 z*9oQ;1Q2j}5YI%U3g+27ff$J1cOIYxgDE^FTC|Rr#&`(OdO@EW=W4-e%g{t%3~2=} za~6;S3_zI?pxHnUL3}+xJyLEpqG>`d2B|%gK19ycf<Q3z^A80R+r*s#ZW<50Ms(DDkooM1bZ6eR6&(EsTc%Nka^Qn|L%47(-gAh#_!^g~)FVX_03%9-s+>`RxJA9XBLM3r0wI zF#z?zuph)Y)x4ljjWf02h8>aL7}5$0GYih@9cdvK0?QpYBuD`UAYKdxRAr=!H%tVf znuultxfp~Hpm{-`8fR)D$O9ye@em+sh#`n4zcHj0xXesM3NQd=Mu4;+h5*b=`b>DZ zdz(rN21aHU;#Vw470f3n1hO#IdSQwzAXPB4U==$QC^56(^va0*`XISIRqzm4q;j*2dL#o-W&}tY;~_xn z1vv!qb0g}OnBQLr@&IW;E(RerB5CN8^Ha?WVhBJ@H0%fS#5{u$w2!o4U}V+p|+g zpwE>v=LJxe4g0~W$vvAFiUE>_fiZIykOIa-fI1^N1o1NynisfVfH)zrp5^r1eMR#E zQww3H1GVUL>!ME+c|1p25JTV=SwwzgNGoue)rhoU0OBDCXW4St4cb)q+Vmmv+)PAk zXE_A%^#G}YIWqws7#;?Bz}!a)+DBS2FlNz$VLynGtA%(Tpm`zpx)EtXpIgtZMx=o8 z5TI#dJOoG!VhB8C9+U`UNJ}1B)TqydNAylS1eQB)NRSo`jF}N2EsTc%X<to`!Ja|k5;>W|ekeMAyP>o0n21Z^Cf(Vcn#zTO#AcnwAE26wH zq!qZ#SwPazC);y(C}>_74*}AG9D?|oh*ZJtl|y@JNDFc?2q8ey7!Lu`fFyoT&x3=|YQTV@Rt=Wv&NE3kIOf2#^-WLx8j}9s;BVIRx=L+ej76Q{(Yk z(1-QyMNDQUA}ttzG9y4*7!Lu`!gvUf7Q_&MK2e_uw`u26cdE6sfsw~Vffz9NRL{>Z zgrkjA!F+zfnrPiDBQ1z!VL%L!Dwr_7J@uL^TK!tkOIa-fV3cn zz?C1OyfLJeI>3XkAFFM-Y>NBCANR}S=I#7lS#4k6?hnPG++Y9Z@whoubM>)>a=xmX za(Srg?R-%k_K4Z3_R!vcsy$SJQ|+PiT&ecZ(_N|d(AkPqd+6PAsrJx2%2Ms2w=AXF z`$||J=v^DB_`Vg!hn_l5#fMfvsrJy^ooWv~zm;n5cf$Gjy|BH%6t?#VVS9fqZ0~P` z?ftE=y}uK-_xHl~{!!T8zY@0hZ-njrTVZ>D6t?$Y3ETUxh3)+}!uI}KVSE3bu)Y6Y z*xtVrw)Z~>+xs7d?fp-}_WoyKd;g2Dz5i9%-v1_S?|&Dz_kRf6`#**4{a?cN{%>J> z3t@XJVSD$&_SVAo{z=&0M%dm~*xpXqUL$PpAZ+iQu)X)f_I?t!_x}jn`+tGG@AdxQ zsw`IXMN?GU`Lb?`&1n}9T=|1}Wx3mL)Q+LLu-MG&rVL#BqrUd3EDr0kDOdC5@u05X z?aF4r)kU*Ym#Xc`?W)>7%=guU-o5m{g9+_inwW-LMf;aLt7sRKXBF*b@~omAO`cV> zugSBDb~kxe(Hf9seyfq4U0GJ!ZLugqv&`S>OV$0c+T7AE%pW2ncrg&GJs`B;co9dsB)oQ*f zcZW4v01PsMs^=ghs8|j%f@I3k6Z8Gsa#N~?PyL_c=DaoH9?U|;CEX&b zH0c&m*-5vEYEZgGRFKjwqUvNUUe5jf;dp<4nmj;DZvc5L9?I>ZXnT5fm~J(H*cAKF ziojU>tczwfe_FSTs|ySBU9k;qq#59gWwES^`QoUST<1No*;oCa=ewrd?-e$6cJC^l zwL<0*g@w!`J`0&gx3kX7RN*+kn5r&0qPEpX}?S>f^6JRl7}lbt3sc{KtR)%d6K{cQ@POvE&%Oc`Em+K8oA@WLr0R0G9;> z{5u`t<+|Fd{$(}amR&a!wP|HY_^H1kl51|^n2)Fs9P<%#3&(s!h47kpE9IR})uwBjy$_F(b_*=KQIyS=}w+db?~&)fn&U zr&aL2@L%eSH`T-XFn?%@59*fk5E}-7Re8Fry(xCpKH|2vakrtrnSNZf_ex^!rhlig zFV%Xp`7qxXyN%k&I?af)Pt_`;Yu~~A6mJ3bY{psmXAGPAJpSEhyK1v3ntAcQ+Rr`! z5Ccp^oWKa7K!+}jJnWik|5!9?UDmZI&3Rom`|`NkSF84h)>HRLlUf!%l~uE zP>g-o+ADWR5XBh#t}%B+RDz`JqZ$-r-$i@nxP*urW8XFAj)-OeDf^=FS4G<9CyKwZ z2iET^s1|xx9!{kMu+F$idEN-wA2+Hqm$=)@wNBjRHQJ4CEpn>(3I4VlKZ1qA=XBx_ zh^vA_Ai72l;o+wvLR3*Bhj8q9L@~k;(z)<>!OTTWpiuQN=I~WhJg7bI^5NVDfZAb5 zaIrbIEl=Nc1te$`DsyETxXRcY&^=n!D4^k!hriN&mRb-$s(st}KLm#JT=MtBFZq|l zFZsvem;CGDm;9^Ym;4vQFZnmaFZnNrU-EAQml%6P_51RJYW|99{~>VE8%>#|+FxqU zx0QhwzSO^eTC3BoC-c|04^91q?>~pGLschq9V#WE>rj0NU5A#Aq3h6^FmxTdV;s5; z-6S@SL_Zbp)uvhW!DNFeedBaWJz$DaX}j{ZT&o*Mr)vf;xI!i(7F;0{Xu)NKOxq`%v=wSsq%Pjh4^6RK)%E!(rqi}uUH{^|>wh)7t9NC;RtJJI z7j9?_!{RH$qC2$A7&p1bAnh7XqB1@(R%Y~_SaEW0Kqd1bGzOUu&KOiaW`>jU`ItHR zKx?3ZIXChlJBAC`2#}E>RzBPfK3z+0b+$NT7^n?>xUmu%gUpA}7-saHGzpDiMn8HC zvuN^Q znng=CrdhP2W12+^KBif;9%PzD$B3C`(ZOP-S#;ExX%-zmW|~FEk(p-E0cECHbYz)n zHsTQTs%YM}>n-7Wn_@Pi-lmw1sJAI*BkFC6*@$|ZVm6}QrkIVWw<%^L>TQbIhLNU8)M)9{8EeO$EEcavc#sS@*-^ASsHqd=%DFD#yXl+WUQmfCBeGq4zgg}J=O)| z?y)WycaL=xcjNA$x-+cafU}CYp=_L7Sg6~3+xg>hzi#UK@p4z<`m6O(o!`^{NW4Rv zVG^ZPVG?yz43nsvVwgmo6vHIyq8KJo2gNXnx+jK7)HyLsqOOTy5_L=r zlc-x_m_(ft!zAjG43i1RvyG!a>Of;t-B)UVvs>Bn-$%oDXh6SNUMC?-rJeN31}`j{Xi z#`aPB%!GN6ZvIs0ht+3)(*8+3O0hq5H~Q~>)BRZ;)l**p=|1dn_^>PI?Gt4;TlH$A zyYIdK-aX1j{h)g|um6F6g!Uo3?%%iii-zr;Mp04(TXX#yenfj+-H>ZGPrdV6|xhA}eLo`j?v`&iXpZWwMsG zE^|HdXCP`vAzT-dTxOR#f08;GBmoNM01(|Mo)cy%SQ?UI!cG!{cX>QwS3B$xDU`T2 zZki;Gsieptag`+P5;Z4RB+SB#(`1lgFj5X$=KE79v=Uq2j>q9{lrC&lAEY zBd7xr2NOMEk|?63d(6&}UrAAGrSRR?F3J885b4-6lAm;4g!>c>!$VfXsn_RW;itGv zswm(395I5GfyfADtP4SogeqZD3b_uwC>6C4%HvR6&yth@Fm<(+wI?OlBYz#!_Q3)v zpUdpI$WOWsaHNhw&{1ijD&6!+%HuOPO9S?=NeANEz$oc)l|=BaAK(9jEImbT+EEVW zD-G~J)nYt5wax-j5nc2?20#jbs*dD z%-h9ebx5z#lv=xfWJ)CwQcL%E(oLZ(9_3V0@NVlfFNe-KR6P>t@CM9Dl!tKzJu(Db zC9&#VBt|Q4WWf_iQ-}prrw}fPX+CFCQ&+Ik5CyY9g(-It?>bI`n!!bFT1?nk@)xYQ z^%7MwICPsM1wGc!01RaodWhF6%}fX z(y9cz*PEcQ1>=}6PgJcIZG|iWu+xMK-&8JTs(VrEb+Tp{RSYmqR|M%a5o*eXnX0Y1 zTO*>T^=cx1FkzaktOp|)>^{K{hParq<^>yA;s>1zeTngxpnO{LkJr@)0&rXvPx6Re z@%+_+5n@o^qGHt7qX}sjE{M})iNm{4y-V^aXY&wLQWlTcO2RoJS(ULku#+TR9H?0> zGDzIw;CHD@0*N3`MeK*<`s8OGoCSGVh>TJmpM=)6FRkQ}F^%h5lU(A~JgaFXw@itm zEm;DYyt6!_q|!&6s3*Y<%eb5|J)-)tP)O&C#&B zK+Pm!z~nD<5XW_ik_icTNVVs>t^*kabq;$k&)6o)x%R9eE>414!Y-7mu$YwD?Ly~15M{yX<1uLvA6x{F`aaUM3uJW(w(Va=u$u?DH$=;9Gw`DV5)bC&4?hnPG+=FYs z)7LJG=3Tj(?~aeVx7GG^-JM}w{bE&Zi_LshHRbY9)!R?H?Bub1(`qk|=})@SmdEx@ z%Xnb=mpa<3rrzz!<`m}ZH}}P|+CJ#eUd{gG&{Pi(Wuua^s1B$D9%A!V2Aek-Y~E(D zd6&WF>kKySyra5|^KOBfx!igsz)w5x!*RN}?7U0ZwDT@u)6P4x`MuWre%BnA<@`zA zW}fe>hjV>(gMfsdWDI3G$=D5`O_H%&Nn>}C#$E$sKN{IorCF8lmC9$+{%3zEA5W$H z?z3IB*%ZyZcwg;jA0&;vmo)YhF!n~fxh%G;O?h(c`qTc;`>WaY22V)g>t8XizhPc~ zn|6J7)l5erIexwWsAz=a*ZZG-eeXZI{&zb6yLwaY*NWeY2j#vxsLpazEKYp5`?db% zyRtb{OVt{#i_QJ*qCOnzM-U&r{p&-$`~TYe79~fH>&j`)aHch+L|URLiJ~RhYs(uO z8@o2&uR-a*S=`2y4ki5EbZ5Q@#?l%-L%ctv&-Kd{^M+WTg~6B%FX8awvvn< z9P`z6v)gQUcf-_z^#^Boc~>o3ui8!U;;Psf4*%g@+1wb8-?4fq4ki68^;`sb*fVX- zuLMGH;g9)MAKv4a8vnDwzumc~J#ntf?cTUVO-C`bDz~>~)0D-0TOZC=#M?$WG~aD! z-E^Ba=Uh}M=NxlXzi~4@$&XLbQ zy)Ua~QIC$A@e0W&p*qoIB**K1aSZMhNT|*=8OhPI+asDiCxJ=CoTMP3P7SK%O5vnv z-ptlbwYe*r_QK4_C*ny`I1x{h!ijhieNF-rrcTRab>$%mOrp<8U=n>!0+UdjjPHTl z?X$fAzM0*YMRVADFF%_<`uClEw&mq+-PCp4Wna8n7291?ET5$vf34=DPRQ~39-Z2M zNrp>oIMMmYf`w;(wWyoBVntmnv!{D&GaVk4!cN^tMA3Pg3(x*xabLV?PZ)cla+ymT z{qd6cvJBeJhw^wbei?(49ee!Z@qE-3SDZH9e9Fa);^WuzQKq~L3uo`_gD|P15wo1f z<3DCkxtg)*_{9v0#dCt1G9NKVgrq;WX`pz>s51U>I6tq8_LJbccr&}&wYPDH4;(Ij z+yC#j6>cxP+Cy*m*<$*q5NZ=pJ(b!7R6nIQ0o6;XO+bCBp*8{aNe6F&w%t7X5S#FX z+61~P3Lwx`QD6dH6$K{XtLT`|*P-;FHmuR-W%Ju=Q!d-E+3v2d`+KUx4HY4ge;AT% zS>4=jXIINzIh)t3_8hW3+~pH*{+Hp+<=b`L#oXLIZ!x>6m*t&1yzCBx2%d4#oCrPe zqB#$I`bBdh^eD{l28Ozu*X?dut@n491gWEY-uZ2L*MHnp7KbA$@wU-S-KVy*`}X0b z{Y)1h7u#*oj@EC37auRGP0`%#|K~^R`u_0z&Bd=byUn_6KM8}+<0wscaoXwHr%oQB zqdaYdPCnhmX}N>b>2Q}W#da}c8S{!sPv^r?nQpw`XOFqNxX}0A7~SLXeRS<&9<7a& zDxR0T5g}U6bdMK|hWEkpq%y9LXN1SK^;3nqtc9uaxR@S2aC#Oxq&;8xUa7_0VKj*m z{o7$@ofo@h`=IIJ?05Oe{@=`&)qn1)#fSIpC2sTl^>zEi(rj7&TrMe!WTR4Tx@=Uc zHwUARp}r)_LC^?BB?cN#Ms?w`QKM*=t+ zlse|+Fm*JjhNzAPWoT;5^T**@C`hVH=0*FhhRv$JAJ3?vENW~Gfl*^?=!_a$LvGaA z8mgnlW{B@dZ`9aSto?1gVg6jser}%**mR$MwvYE#+uPYy`$SPy9_}li|J_Yf-*+F7 zu9xj6abfn`7jU z<2fXJj2IsI!)y-7hY}9R2Q|m&e0$seBFSuBt@>|0t=doKZbKRR`lbDc{z;g0 zjNSg+U3I-VT!9gF88wNU{TJlBH!tf$ck%eyQ|ig#?189Xdr;mxJTdPj*$QNgLXD$G3%~5%jVE#6YP)s!lrZ=r0sV^R*U^y7k{`K z|LeANb$fZ*w{@ZabVdKCd1`+;o)E4hIvKrsDyCTV=#kNrJq!LPw|doeI9@KmWWoQW z&x=4GC#^@A7grDcb%ZDDML_eSzYd;0sCA6K-*8pjUDb8=$uxZgY7OH}vwgC#{Vw@o zzic0)Umkw?!h1pq{Txd2kli5O<1<9kW)1o5(OSj8k5|bWvKYRhPJkT8XWP?lXV<%C zRm=~MZVDdSl?{y^)0K@3AIy~vjUL4vjXZCvyG^~Cbzka?b$g>Gp_sQnURP)|L(D{ek)GtELruwtN4gsQa^yqL{GYq_8^yn9=tMSlqFi>% zHgopBsv$q(jjNqKG+6TJhW%Z+T-Nu8Z%NGhCua7YFOjsPb=}`~Kl0T7%vbyW-W<{v z{$f`00Mu47a((XH32&ul{m0>yv#jp=AB4!dLKvOd+ph(*#ctZ4d5si%^re^wvbx`E zIZPXQ_vizG!+lUHr#xb<=(^c2V!Hx?f6`$GU86 zkn5v+&Vz1m+N;VgqV<-qH46YT%IN*ipw{)_UMTHKFlP+vHZW%l>LPHI`B%$g{>C+Z zk<=dTj}M*lb-8MP4ECmd_VokV^`#%3{m;Xe5!dpcByIq zd3?az&W7KxY;Se@kMHW?tKbAvqn_cuS}v~J<8?XQT|R%=eHUPR`%K{Uv>84ypW%i1 z3@^=RxHO;PwfPJmo6qnY^BE$3Kv$kBc92gHaf5tUUo&*@73V#SwSC2?!B>o0 zf5qVRD~5KyVrcX$h8MnKcz~J-;hIW2n zX!Hk8tr?vDz|hVQ85(WB7c^@hpnp>~`#T4Gl?R_I8QnGIj*C8T5ey42t_9q#EMf;Pj)L# zL4Cf&oud6i9I1cOQ*jDyE&?dDxd==_aWVR^{#3E&-qdyBq{dz^#Ak?$!S%XjytB-#E zqWv2uP4ljQWPDTI9DdB<_;37Bx@8SU1rlp8DtcIhQ6a+`jQYToH5m0?|$V-hu`kGL#&#H-VkiI6uLHe3dgN&kMyix6i zLVO54`Zi=!uG=3!=zigPUf;EUb+-L`N8=xQyl8)BakZ$nRlVwd12XH+2kOp3rHk$? zRP5-^Lgi8VEXPrPc}&$w*ryUK9m~ec`LKmY6Rp1po{`C?VvmBO*pzLL5s zyWja~JLBPx-9LX0{;0#KTcF-b?Mn%7rGg;gt(3b*XS&`4ZRgmm%Wb3~_@Bdtm-hpL z4_`iRe@W%xw*)o5My);_V1M*OQTt!Kxo*D}zS~4fGOFamc`E655l7z;KYWq<@MWaP zl%o@b``f8{U;LJg3~D>_?8D_{7r~phi%j{d^r*rQ2k^(gY7w|qJNlIasxOF*eLT?d zY?QM##-^;VF?M1$v=<)v^`<(c>~#mO0@dmr7+bC0fw8H2k3P>Be#kx&0i&-1kI!@M zZ$J#cmwxe^?tjg0+e2IbGqNeXjNjQh2sM~Z20*ckyNS&Uzp+!ZKhOeAalpFlXW@$u3pEzD!ZP~DV)t&e^^ z;kZ(g!sIE`)5o!`skSGnFb69I#?7@7`if`LVloT!SRVlZiX=sT_rh1qaWHAhH?kN{ zF_W&v-<5P1g99(#w)hGcS;^lB+RwTlZ$%N>d<}^flQ*@em}4uQP`$ollota6FPAX% zU>7IvGWBt6C6Qtdg$azCsbnW7LHJ@@`vOT>uV67l&zdq3Ui!p}Iku8WG0GWp!nODk z(k)Da1_n_Drap2Tq25o2i}A-l`P=@IrTYosS@E{oL_SDL8{~p|JSg$))Pq6)FhDr` zd~pA#jJm(6xWA9O{G)|KO>$CklshULqV&{SP;{tr7`-_~nlYf-p=Xp0E~@)MkU% zgde-=6Lj}>Y=4=2IT<}Ky4i{4vFl;Nj$w8I^Yrqyq6eD84zP?LKl!88Bjn}q!3v}c zP)1NVuG44tDsrJv5g8yluVKB1JR&G#mxMYkC+=$Yjwdd5QqWxG!pPJb)N5R1%S z-GnkY;o5e!f_r5~SP^tkd1K-pYBE%PE&MWml;wstr#RsL6&8S;p5OvZ9Hs$CryU&dDOF!h_X3*V-VI*fK!UpgpBNsHl z9giwQki(1;h%`q~lrg<7!u(RVx=iHj;8{f?`ia^UCK8W)Goh<8+1frp$8mLK3t zYkQ~VC!PuKJ3)yRIH}@9cLzOTAm!zX^STb1L1G!6CO`{*x=s$tBg5fDe@=#u-5!)y z&kTwaOEeuraDkKULScs{_L@c{PRCIXC|r}NDAM)7`Q{uQLPTom9C01xR*FMy0xw9YIh*>_8AQBvPD80{9-Bjv$l37V;Bz5L+$C9|*pEz@^Pj zJbJ;dhmf7Hd&th$_%z$)rImZ4^gOY2i{e2qV9V{CkEV>>R^}c*_>nu@;=)-tL=y^! z2YE&eYv!I4t=}uDj30szR=UOT$J3*K#bgAfE-nv3T_b2jLKsk1CJ_qa z1m(#UC#pY^>T!G-apb=sg6?{5+ufba~I9EE)x@Q?gw?m9SLV8Dp% z>SLj{BzE8qI~6@)Elh~#jL)fkoP{f9mD5FQQJpj9ZPQ0HIkBGmtu&{vRn(J7()0Ri1H`R<%-NEN~)vBB~#r5{5 ztE!pb&aQUbZM}N^aunI{r{e=n{u-$7qhEs`|SCr*TuYA-OLtMQ_idj7_xi*x`g>3%<~U|!$d6{|MP zx=DwX%f)?eX6Bs6*py;5#-{Wd8~fO`FE2jv%R~?s8~b>ms)+2u!dQA78xbRk?JcqmYF`J_=O?S9Ao8r}O?P_l}!DTyvEfd`gwmfu2vSpzgeYRW6S@7ge zneJ8Rw{}7`J6t-BPYL1DajuDBNFt91M)+kXllB}TN9fH#CX?P2L^A13KqZsz47NNF z$)pG2RXCXxCwNFAGs{uQ#DMTPTra3_)kvlbqDS(|M78WGJ=5bUYHcx(qMnS>Ay@pB3ciapaK z9_Tn4#jYL|{d6HmTKbOt6nzx?{-WJR2msfTBgv6coG|#tn!kPSf z*Q|d@&TexGT4}@7jx$`Es|}4p*O-pAE0+KjLm5ohe^*x02d}en8&g zu-6HB;E#%A$Q~8xkUuEU%wGm(?~AwX)%N_h+LX)gAL{OrhKP8M-@a?rpl^{De8{C**T=%g7>Nv((oi;J%vn0slk#g6jM8X_i8 zieywtj0v?7v9c;*@XBiDsKgEypw1x110~;~08!A@X$>DM&&`Jx zI~1Y`U~6uFeLVhVW@!;jWS1$DNh&CBBLLk%_4k=8N zOVyZCqCwAcn;g;!S%S(NQdEv9OcRx|$W4h$Cj6%9rZA?oqXR@SERtv%sJbcaPUiN5i5VzJrn?hbv0`p}0inPW3JBN1(st|J z;VnsYcyfyLpnY@HDIswQu!o(9DLVV&px`S6WLtTv>t6abdmAfiy&=e) zJ>cZ1gr-T#7CESsM|zxWTTo#46?g{iQyr65nD|RZJj7f6u8ML@boxbC0CRy~6@|_( z>?r<{L`3yvvSNx%bd-HjYS9aZGKw)}Y(i*7n%%D$6N(`ad_$XtdZY)-g-uIKK@JKy zink_csiZocAt>m%7%1DEI2_jqc_sF7OtR%%rD6B32hUQwS9B zfZJAhppfmfs0!;(>FCRYt#C^rLG`fcFx?V1lh`4|jc!prkZH<Rzu*RSQ zL#z>69!g{d$+;=E#DrYr(D)u=l6QQ$S|^l~m?$Z<$!QXWUdj@^qDB2EhZGKo#J(K( zZE!r%-I1aA9%7Qai6~+kPiGr@VL!2@gX{up;6lc}+l#LOogLU^Q8?&|QX<;Ia*|P= z63XI>9**D!*OfpHiKYn&E2DcOVu;Feh1@$Qmb4NgjA;}?gjCoGfP&E}V_NB11cYhm z#oc59Nx@AZAV4pd77!1+TX+g8Arxo|yUBTlwIpbkq$Si`!e61Vr6}R z6fTQ^YB5vZfh}4^BH4R~#X=*ESTQDIlTW%~LCH8JPXH6t)>%iNCq5KhCj)7zzBmZ@ z3cmgncB$~TsC9mSz#X21+ItoHiEtomrkSU46tH~}}s zoVXNY@}z;vthHuh8#y>kjER`Q?Y*EqaCLu*;TI*w2-N$QeXfpj^_?>MqSPy43MXG$ zHw(@sZa?TTgl9N}f|pbBX2Kf~1|L9>7S>4+<;n}!Fo~sjL?mJGsw$@mWqFJi8%z_Y z9*c%p6|JO4*;nQ^u!j$UVg)Jfw1NJ7_pnBS7OH9rZUTD~M_7nQ=)NH~Id;H}Y;2~a~7tuQ665ovQ+B2fjZY6@*cILN1i zM7im2ch#z#H^ue#r*%_p?uw@U-?OWxn!lMX>fP0{oNboH)$5m=ZLuvkm%krg|8v=F zt9h}U-4@I1msj<6Ti+2{(3<7>wq6rcd@(F+(NyJP_NH8}XIIO5{${^;;+_9Gyz{nb z7PDn_bGx10G{w7F3%V`GVs^7EHk-&Wqsq*i>TXl7X4mCvUS_EJd2_RW z&*iUM>CWG@>u~b?)vBJkf3dmzba-();cm5k)js;vy_FDw+BDny`UKPbYJ1z1r@(W0 zUGEwtK(8LiG9?F0qDKuqi7GMlG@7)0bXUCXZBs5xsfkRaM-QGzl_ESIaOq9cR z)KRZ{)ztT^*|u!jKB(=$V+rz)LxRlftG2t}Y`eZluhUnU0Cb>#8J52&%XND)Y1_y} zRovC9#jI{(z4oZdyJ^ok3kQCmxtFhtwtn;5yK=R?Y}?7zqD{}|zlz4EXizFWMT5Q| z!c#QpOX;AmZj058a!CVYiYpozlU2}>bY70NwI5$v&lj6&QH~~|w)bS#{Q zaa=loK(Qt-?{>>=wO*>$F*K;SmZ3q#^$ZP4)%3p%(R*LKZJW$_yV+p5pNvb=d_e8XY=B2echfG_HVoRxO}^96MI)}Zp+0p0jsf@@X(G)Z!sriD#Pbb(6=y%&S@c)gLCko{CAf(}%j37POug9-h6^w`DmsHtsDyC_$)byIFOyQaM% z?Hb&@9iM;Fp6&a7eN)}EkxK3O>;b7=iaj8;L$L>>b|?0Lmqd`T2YjtP;K$kn{ziMi zPqhd9t@ePQX%G0h_JDuT9`Fn80l(B9@Xy46e;A^4)1Jcn#Jg|5p0E!>;?rFB;cD!O z$M|DsZiMj1x9;=e?!PBK#QF4)P&ZDdNZ|8Y{fGEZg%leo+N_W|7C8$%PBM@ZMQY6+`w6b6*T) zEWH^le2pIR6G90$6={-+Kj+5SBH3;sl!NI(h*+%-N)fJP$&FjYQiihke(RAKDR6KbDpe!aW6S^$*Gq3y=Z} zz2>%<@Qedku2VFt9yfhjakyBBXXagzwASX;D@vRNmS{qymDz(5H5?mC*bA<0iE29* zVJxWH8J5^g05|35(D%(_$g|N~%FYYDe2qPB?IBA)x3Siys^pwj@<8i~S{9Wta_u zoST9cBP@Jg!2W`W5tKcq>M3Zkfy!qk?8>ypGS-asSi>_=zGwGh#HQ!u!&wO0n$ix> z%JL#BhGY&XO6+x9?UbP;$txaUELx|4(l0a|fNX*Cos#EBnGs?wnw7kudeT}dj=Z!< zQ92fLpjhlFaaQ0f&c-WgwAf&ohLZSMkcS5w#8txA9b6``eOCPv6Ujh4l+c+8LJC<64i_WCAD+Z4f($7v@fdurITnScl>}_J z2o$EZc28pb6cb=UM4gXYcxM;+3AD2pS79EzDELwDl@Xae-hddWZ%ednX0oe>ECb=^B>5!NsIA_sDVrOR0C26n&_M5!Lxl&1 zeTwnML7VgGz;Sivfk9b=wh`4t;9^0Bo}x4Bbj!t}$qQ6rj)RoIAjd>SHaQqVWjS@! zB-a&;OAL|A!y(CAsfam5?WlTeERxQwUt8t@0*0mJxJ=NJ)-Xq}{M>e$36;I`djg7< zjBuCRU*gtJqtQZPT5I#_1r0Zu#9|Fu8Vw#)c0VR^iwP9ATL|^LbmSEk6bDLnXy~|Z z@#sZLPF#t+I$SKGDnfJuYTqX??~P+5JwKr`RyPJkmv*dnfx{Nrd39qFS~7yH&uDw- zY*xZ;U(#pEP*LXuA1dqbdInVfxdo77v(&R>C2snYk`mSzTjzwO z6O;N$c^>Ury`ByB6lj!;WX;Ta?LDQ-uQ(L=ihhH9qF#Z>9F6c51NLbo270n#X_*q^ z?<;x{28(`*3=2a^I&Vk^s~oy!Ta4kv84Pluha|<#FF*aB%ctI;2Vu!Gtz)%K$ znFz`xtUxREVA>e;8ylGk8m9u%VS}eQ& z_vY}R)xGQej zcNeRltL6S}mtPJqm>12WS{2LLqH4_Z4`Sp$ z4TbDAwy1YktzI_E;_6WCQO$lXn{733t9@H6uU}r(+iiVkG4%6oy(Y#T{e|EA+iKft zyI#$kGAgQ(vhP|6HdTwFDHpqW+3b-%|J6Z63I0eJ6`!so-bP|S;YRV4KupAGM7clpg`wl21}`)nmNH%h{$ z+*GUCRa4F1%uH#Qu0C!)qAn{AjuOFhdtLro#jkR28Y zFX1$EYlQ*8-{52AGjU8*WHw+W%+Sdk1%qC|Cr+Ih>ez)E1O%;j7_i`gU4SL*7&r8T zh{DWKXi)=@_HdT~On9kDXBo6Mel?67BR`n&xSfWYc$l%!OvDYcJYQ&n^9<4Pro|7h z@@UiSv~8j@D5JD?hJ{u@&1sLCY3S6s;p`-FF5xRiJ`=#v-a1jE0O5@KTLBz;^Ndga zR6=b;Jdr_^8fE0T`tZ5&asc*NpkdSuIX)O-zpb`&z=X?|L=7@QhORuVPt`wz!;b^gqYamfQkkGfL!JRk9l{6Y zAqUGuyu{Os14clMFINdsO5kklA5|G>37(u|EU?%(25QF6{sPAkNo2RycvCHK4A}iy zNd)pEPIdo^^Fb4@JQ9N#O?Y?inwfYhd-Al|6;N|_U<7&QZJM%<7i>)F`;LGZWsM-u zAW;taDZoODEIKu`R5}s9Gz4J6#JD=yEH+A=Iv9Rk!9EkE+Py%4MW8aYHg+)tNruV@ zf@v@-u>tm2+z8=nb!W_8#fpvzPo?w8X8+M=cm@*8M9d__!-=W!NQOxh)14MN^#mYg z02_oD+<0VkBM88R_Zhx}MdaFLGnCDl23nXJ!^I-*`T`1s77ov}^9*ECrk(Ka?aq3Au|3+~YMkXw!gnrFA%o@Ft?iD5+1w4qCiw)R~ZT zvekQf7jOeYCW;1HNs@fikYfdBB6bjcPmdfUG!kyYJ%KB2zuL_G5A9lMCtG#j(0et? zjo7gaff>rD4kqbj5Dx(!x9=4ieb=NlUcA!Bq$;2$907)4&^gK3N*&B#NTQBpMxZ={ zHX9l}0h)05rk!xo0xGv20@rx`o~*L4_8D}mlS3*AsUD(nN`kK#@T$W})LQH+iWho( zG@jOEweJ@nj>)0)Q77@ypv!(v{j*&P9x1EJS3zSqZ$IoXx3E;oIqt^{kklRIwWfU(-H%_8OBm%j_HbH z!VA>Q;8yKw2pj|KKM@$g9wPz+;r3f5L#PsJ!n=%8% zg(Z}mc-#|{6PB856?vr<(Jh7()2R`($~=)My3%Z(7`--UPg-@Oo_ zY$HWP%?CqErvgN(Nf9M+ln@6~kmQjrB0&kFm?cr0!yzH&T_Z$&@G=%GV%f##xgkQg zGfg zN$dR&KHwROD0Wkrr6GaDe)l2>g`y=%t{(y=wL|=*oP^FHu0*|X#j*=KDTyR#oxvGI zT#%i<7*g0t>;*-QLO62>4=EzgxhT$}vOzCMFOd`xQ8KrH2U&%MNvQ^sX;qjocro*|^DA;plw0g-mL zK#zY2{P-SXlDkR5lTk!i9c-!X6qx}+bOwd57^!ty^?@_UXpOnSD8{6{=gO$mISFcG zg4++^Rk7RChJ6ES|$ksT@N{6cT4Zm&xaBbKNQ z7kVXjE{QgWDk+}hC7l%$Nd&%CRJZrkP#T@El`Xp z%#*+l1cbDX*f`7)35HoN?)GvzYGAIM03c`_lxUnB+W#yS>4(`Z0nGW!jbx+3G0+1>1vv zaiG1}rSV44)AnDosc;YlU* zG->Kd0NdT9ETx@x)0XH}4eedMcBRVmCPZ$6X3;Ey6cgd5QI|rz!X_8bkSIuHMisB1ridnE25E;C`d5QVx*o5QaHmCJ0gx#Vmup> zl%%70+%XWJlarvThHi)GlZSXTBuQ(Dgw9B_P6`Qv9?G7HNxmzE(}BGpl!&kdPMZBI zCgsT_AjC9z&4{eZIuNBSd5~m%IM{2`rL2qyk%TD72=` z&Jc0A6a9J4nq#pd#YBwn_9xJuS|+0yQaA%L#GgSlI<*W%5<}8+Q*^=5ZlfZe7!z{a z`C!tP9?HciYWD&$O<_oy8J24|F{H4Y(t2`m{&VK&SqaZT?tWrKh$M>@LpK^8*^@Ym zC)hM3<%uF%V$@wEmO!k#Fr*}wP<&12q?{}Q#!8FFBpDiV1XVJ0FNDyhBMCIE6G5`N zb>-JsRHMF%lPV7q$u5hWCpz)qiWLLFizxB^gGebRDK2KoFA;!<04PSF*h}FKk8cEV zNyLc4P9l3`$%3IZhZ>$36729|moL*oO1mEd7>1$~yplQ)lXO;2>Pkc)`q#T^RnD8@ zdi&FT@wQyd?#p6(TQ==KEvuW`?W$Vc%$j=f`sHR@Y|G8_Pp^x4_wN=}Q_i z(Yn5O@3^YA)J7qn=Ytc?^WsGFygbo7mnWL%^@-+rtT$>G*VdceJdgEeH_v0e+0FA< zZ+7!M)*Cg?Ux$3Fn__iS&gS)M-jv&N)~42?Z1%xM)Mo6Wn~X4^jLGP`Q3`J35&yDRJw5U20N-qhVufKIFX z?>XIa+TC6RQFuLRw?^R7rSB;ih$(i{IT9dBXyh1>sLlQfLzV7Dd2Y42Y#DNS&Z?0m4|DZ|eM zF;v2!&#TXXCX*DGvjn;aaE0@H$DHV=om?!Xe+2`(o| zk_{G!m_D%vTa0wpIJ08!{4o~wmq=iw6 zNwGhRdnJ}9(@PJf{Ky@dRg-64IMLFMH;5NLCd-`(nj@{rV8yBiL>fUF5{UTxb5zybZjN~UA(Oj87OUA?M!wn zMmv}5ho|Hy?)Gyc$r**SpeP4~Gwm}x%}F4%*)@?nDbKkg4tmnTmf>Dt=p<+qyYP^c z6t0=wiV@9y5hrB{@;ITF08KVaK69SZ3{SK_cFjXL^12X*EcJG?NOAG)gLecJopz$v zdr5n@VLl*L8AhTQ(aF;xDg!Yj+DTE8pw-wJE_MM;>!HX^I$fxRHK=b#D< z@o6NxZo}wRl@o{Z18_Hj6q2`MSYw?P6G9(~D1{WAz{3pFDuy{BO*ClA#U$c!T1-l3 zT1Q-Xn;4|#E5S&56GF1wm!M7(8=^^Q@@Pk9IT=PH>CsvzA|>rK0?X3x@G7wvZe527 z2oNM{8Pz>%DN~;M(Dnn%JoGh0ZojmoTD?%^GplwR&DM3)ryl_CIMbv|x z!iMCv4aH$k$BROD5!tSyq}gS2zyGZd_4n?K@5qFZKBJj6(H!} zr=*IYhP6+{xl9awDN~;Q;64R-QB4jhCgNU1b&X!#U{BK3O^V5zPs9@xUyj41pa@4P z9pV$S&a14|K1Ifah{_YC1auULg&Z;YJjBFdBt9Nbpb<=q9Y^G;A;Hu-4@jOWpjl!o zj^|!)r`Sz~5lu6NxWORK^Otvc!2neTxoLx!RFY*%KA3qf~vU zYH2ITtxB9pBTRhpAtF(chFmg&G)zG7$j6Lma+t-C!mXFmfv~3~JOvr{;!kxmLMy79 zGmr9KdOEF+SGK6#rj#DyP2e^$r0_MVrN8&LOEAPmCzMo}`mg{Cg%0#PV}f z7*Q8TtO^sE?>J9LkEuyYTq5W%LREWGEXBh_g`{vc^l8O00zpZdH8pO?!2+PF4C7E7 zcVUw3R*V|FzKCK-;k@f)5@!j%f-PNfheu>CdqHs)h#5yIKPgXX5k?r6-1>|lQS-@I zNGpLD(y0UDyxVZMZ-q<3H=~#%I`NQ5MqVb|&W0opyt9L3)!`-GJRlmMCP^lUOgRG9 zPivPRdW*2KsXapMB<}!?_8fG6)r2U`I}Z}vF=DL68@@E!q?m|SCFyXZg~6+<+)ZGk zOslH5A`#Orai*jx8hNam$XN;pQrdY5MTXm$SeV422iyeBqM34@cBkbDqJFAG8G)LX zDVvm-*io?$Y*;B_$rED1FeEH#nh4UQEEy$)6;0G305efSC`;)J?IoQncCYixTc9DQ%yXmvky1b&}UPicURo_a~7ig+yF1CI(c}61}Ry z%BNrG!-WF}@zZirEFN72q#SvgKu&^2No)C%NRvXcJ=!Kg@Tw}_lf3a{ zEI}UAy(Ok8eD^g`;PpijW1t=rY`sHR@Y|G8%=k9;bu8VoKx|uDirkro9di83zdb6tU zS2m-+=+3f6LwCmqo1s5+)-oE~N!n)Y=i7Q6A9-CbtIch>m~D2;>s>RuZ+{Wtpc1fu z$WQ!kgzyaR&ZGP9UN}-=J5v3V!b9zcC%}`i8=eh8lpNq*853w?0VZ@%bSIpEAk?y2 ztT#%rkROBxD5&p5IQ0h`f3HX(pu9ju<%I?r4k@rT>MA4z6lTbf00Pjc!ImNN_B0s< zkfih?$q+>~jY^8nZRr5KqS`bHoP7uq)VnAtH%%C#0HK>i#MjAE!J`{_NNK%<#t#*g z@#EnU)gLM@-BB_?v2uBUMFb`S76LlqJ`|U17a1}@+v7TB2vuJ<| z=!KaUUc{b9mCeRUe9_2gbonXd=;YY|w={X%O$frZ3_wzrVJKuM91KZ-pw0NIeM&Au zJ;o>tg&c)lB&e`R#uK{;*okO7L; zOLk;CuaJvV0$;&S1)G$kQUM)iAimSZ0=*AuEs)SPRWl(&UMwI6(6)0vsSrLpD6Y?| z7z$F2eTWuZln1?-AQ2@)vI1dlM`h&)v~+qqAT z!WHWhySJbh8HNN!Kg0qr=mp0CBxMLi0%tvN7XuY=yF^c!T_&W>Po&m#9ECNW=AzgY zR)69P&v2G5G67`cqQuTe+h!^*3g?0$BGgP6BI3ImC0)N@B1ldVb*T#=mdLM))Jfqa z>as2lDc%CHtR~!#kdth}476QO&Pj;VA&rC-(V|P6{t3A#3JBVap9_YKH!#sCYTqZ0 z5Q)=K6b?H6dQeAzD$vJ5rwU;#06-5e2?qz=H-lu3fArFAUgw4gauhpmImkjpVFr@U zM)9O$vMht--B^e!tOijANhEs^?=D*=Aqnj!yakDPc5!IN5meF8gz4ABrfDGK8Bw5C zg9%xJ7$)=*o#AlBiOt=^7~x51Ey#HCLR@xNfSR2g+GLOjdk@_f@q(NcdS#0nQV1z5 z3fDb=ZRh`y$V0NrDS-(`q;ZkeqKIJLjD&0gDM@yzDbV&K0b$XgJ?V#Av@c$@x5Uy&{IOl77Non8b}Yco98_S4cA@6NfQHn8wvRmc<2d%ZEa)MH&tie% z!|C6MTkhkX{3&8b{QdgWJGDMt{CL3C_S1ZYBr5aOpukO>|`exi*ec!A^R!D3-ecUQBnB3|el zMjTjgT4b{TO~_C@<5-BK6opSiiH#X+#i(0Uc(&V@FzD>!AI^gtsti{N8=o*m8S{H3 zi4s8cj=eACCsuDc=*k9~Ml&ab6oq3o@$(@Ng#wZ%LY*M2G^^)(&wg1Vo2j+Vtw8H0pQ8SCBYNKhD%YkWW-kh4Id22{<2`;eE~QGF9fpWbnx0_hne zBq*E+>C<}!i5etYkOVx%vYge$o7tk?UA6zXSr%8XUv9R=w%lC){;pb;^QO4o{`7O% zY^!;(oZS}7>z7ycc3a;WjsARFuZf}mI4o#c-IcR#*)+wxT}-iGGx5eh4R4&^wr_ej zyDnGra(2_=+`skWqiQv~Z-3cpc2#YkU4A)wOVKQP=D2BX9fLFw2r5A{ zhM*F2wEN7P>Tc7DZQYcc&8{hD>*eTx`uvkNL!EhMo9d=Lk0xx9nIlqs@z+Sv#~+bm zlRqLwE`LOQu0JM%lE22^sE_!m`iQ?(AMrEw5kFTS@ek@FexW|%m+B+_nHcde!;ZDt zHr0CFZsm8y>ZYlGE@#X7X1{@5ezdNZ%c7YTZ>!D4JJr$Ns*d&xG1{o_nAa`NX0|L= z?H%W85AovFZ410TCQ-?sJ_2<>9c^fB_wsWY>p@06Puu8h>-J6S+jp~V(cUKSmrcO^ z^FVCnZn z$GbwHv}IJD#}^6u<;i2qzu3P}rz>wgo_eNUj(?dr^?JNZ#;KR%tLIqFRO}|23*T&e zVhPNCl$^rs$rH1$p1=Zy>+!dWizC313VLZITux?rybmUh)htv`ZZRHz*r>mp(Y3ym zICx){Xs+DyJl2YOd9YIBQS;2lBUBGwPc(b9mve*;uP>fG(!R%+@G)S#LT@jVF2~EB z$j4W{oQ~Ce+VyyHQeO_H!WTIzj>yxo{ zbSOCLGh%kLJ#$57_vEBso}7(6vesT}Lb~#@Fq%EWjWkot@i02=dOYeIul--s$~<=f diff --git a/server/world/chunk/block_registry.go b/server/world/chunk/block_registry.go index d5a749dde4..24888d2bae 100644 --- a/server/world/chunk/block_registry.go +++ b/server/world/chunk/block_registry.go @@ -27,4 +27,6 @@ type BlockRegistry interface { LiquidBlock(rid uint32) bool // HashToRuntimeID resolves a "network block hash" to a runtime ID. HashToRuntimeID(hash uint32) (rid uint32, ok bool) + // RuntimeIDToHash resolves a runtime ID to its "network block hash". + RuntimeIDToHash(runtimeID uint32) (hash uint32, ok bool) } diff --git a/server/world/chunk/decode.go b/server/world/chunk/decode.go index da535e8491..ac34b1dcee 100644 --- a/server/world/chunk/decode.go +++ b/server/world/chunk/decode.go @@ -24,17 +24,20 @@ func NetworkDecode(br BlockRegistry, data []byte, count int, r cube.Range, hashe // The sub chunk count passed must be that found in the LevelChunk packet. // noinspection GoUnusedExportedFunction func NetworkDecodeBuffer(br BlockRegistry, buf *bytes.Buffer, count int, r cube.Range, hashedRids bool) (*Chunk, []map[string]any, error) { - var ( - c = New(br, r) - err error - ) - + c := New(br, r) + if count < 0 || count > len(c.sub) { + return nil, nil, fmt.Errorf("invalid sub-chunk count %d: chunk range has %d sub-chunks", count, len(c.sub)) + } for i := 0; i < count; i++ { index := uint8(i) - c.sub[index], err = decodeSubChunk(buf, c, &index, NetworkEncoding, hashedRids) + sub, err := decodeSubChunk(buf, c, &index, NetworkEncoding, hashedRids) if err != nil { return nil, nil, err } + if int(index) >= len(c.sub) { + return nil, nil, fmt.Errorf("invalid sub-chunk index %d: chunk range has %d sub-chunks", index, len(c.sub)) + } + c.sub[index] = sub } err = DecodeNetworkBiomes(c, buf) diff --git a/server/world/chunk/paletted_storage.go b/server/world/chunk/paletted_storage.go index ac49c67c44..f20fcd809a 100644 --- a/server/world/chunk/paletted_storage.go +++ b/server/world/chunk/paletted_storage.go @@ -75,6 +75,12 @@ func (storage *PalettedStorage) At(x, y, z byte) uint32 { return storage.palette.Value(storage.paletteIndex(x&15, y&15, z&15)) } +// PaletteIndex returns the index in the Palette that the value at a given x, y and z points to. It is a cheaper +// alternative to At when scanning a storage for specific palette entries, as it does not dereference the Palette. +func (storage *PalettedStorage) PaletteIndex(x, y, z byte) uint16 { + return storage.paletteIndex(x&15, y&15, z&15) +} + // Set sets a value at a specific x, y and z. The Palette and PalettedStorage are expanded // automatically to make space for the value, should that be needed. func (storage *PalettedStorage) Set(x, y, z byte, v uint32) { diff --git a/server/world/chunk_request.go b/server/world/chunk_request.go new file mode 100644 index 0000000000..ef35909eea --- /dev/null +++ b/server/world/chunk_request.go @@ -0,0 +1,143 @@ +package world + +import ( + "sync" + + "github.com/df-mc/dragonfly/server/world/chunk" +) + +// chunkRequest tracks a chunk that is being loaded or generated in the +// background. All callers waiting for the same chunk share a single request. +type chunkRequest struct { + pos ChunkPos + callbacks []chunkCallback + signalled bool + + done chan struct{} + col *chunk.Column + err error + result *Column +} + +// defaultChunkLoadWorkers is the number of chunk load workers started when +// Config.ChunkLoadWorkers is not set. +const defaultChunkLoadWorkers = 1 + +// chunkCallback is called with a chunk once it has been added to the world. +type chunkCallback = func(tx *Tx, col *Column) + +// chunkWorkerPool runs chunk requests on a fixed number of background workers. +type chunkWorkerPool struct { + w *World + queue chan *chunkRequest + wg sync.WaitGroup + + mu sync.Mutex + closed bool +} + +func newChunkWorkerPool(w *World) *chunkWorkerPool { + return &chunkWorkerPool{w: w, queue: make(chan *chunkRequest, 4096)} +} + +// doImmediate blocks until the chunk is ready and returns it. +func (r *chunkRequest) doImmediate(tx *Tx) *Column { + <-r.done + r.signal(tx) + return r.result +} + +// load loads or generates the chunk and hands it back to the world to be +// added. +func (r *chunkRequest) load(w *World) { + r.col, r.err = w.loadChunk(r.pos) + close(r.done) + w.Do(r.signal) +} + +// abort cancels a request that will never be carried out because the world is +// closing, releasing any callers waiting on it. +func (r *chunkRequest) abort() { + close(r.done) +} + +// schedule hands r to the workers without blocking. It returns false if the +// request cannot be accepted, e.g. when the world is closing. +func (p *chunkWorkerPool) schedule(r *chunkRequest) bool { + p.mu.Lock() + defer p.mu.Unlock() + if p.closed || p.w.closed.Load() { + p.closed = true + return false + } + select { + case p.queue <- r: + return true + default: + return false + } +} + +// handle continuously processes chunk requests until the world starts closing. +func (p *chunkWorkerPool) handle() { + defer p.wg.Done() + for { + if p.w.closed.Load() { + p.drainAndAbort() + return + } + select { + case r := <-p.queue: + r.load(p.w) + case <-p.w.closeStarted: + p.drainAndAbort() + return + } + } +} + +// drainAndAbort cancels all remaining requests and stops accepting new ones. +func (p *chunkWorkerPool) drainAndAbort() { + p.mu.Lock() + defer p.mu.Unlock() + p.closed = true + for { + select { + case r := <-p.queue: + r.abort() + default: + return + } + } +} + +// signal adds the finished chunk to the world and calls all callers waiting +// for it. It always runs inside a world transaction. +func (r *chunkRequest) signal(tx *Tx) { + if r.signalled { + return + } + r.signalled = true + + w := tx.World() + pos := r.pos + + delete(w.chunkRequests, pos) + if w.closed.Load() { + return + } + if r.err != nil { + w.conf.Log.Error("load chunk: "+r.err.Error(), "X", pos[0], "Z", pos[1]) + for _, recv := range r.callbacks { + recv(tx, nil) + } + return + } + r.result = w.addChunk(pos, r.col) + if w.closed.Load() { + return + } + for _, recv := range r.callbacks { + recv(tx, r.result) + } +} diff --git a/server/world/conf.go b/server/world/conf.go index 4c8092b820..380a1bcf0a 100644 --- a/server/world/conf.go +++ b/server/world/conf.go @@ -49,6 +49,10 @@ type Config struct { // ChunkUnloadInterval should not be used to prevent chunks from unloading // altogether. This should be done using a Loader with a custom Viewer. ChunkUnloadInterval time.Duration + // ChunkLoadWorkers is the number of background workers that load and generate + // chunks, defaulting to 1. Values above 1 generate chunks concurrently and + // require a concurrency-safe Generator. + ChunkLoadWorkers int // RandomTickSpeed specifies the rate at which blocks should be ticked in // the World. By default, each sub chunk has 3 blocks randomly ticked per // sub chunk, so the default value is 3. Setting this value to -1 or lower @@ -73,6 +77,19 @@ type Config struct { Blocks BlockRegistry Biomes *BiomeRegistry + + // Synchronous removes the World's own background goroutines. Immediate tasks + // from World.Do and Call run on the calling goroutine, the World is not saved + // or unloaded automatically, and time only passes on explicit + // World.AdvanceTick calls. World.DoAfter and entity work scheduled before an + // entity enters a world still use background goroutines and wall-clock + // delays; callers must synchronise on the returned Task. This makes + // Synchronous Worlds well suited to unit tests that need a World to interact + // with. + // A Synchronous World must be driven from one goroutine. Do, Call and + // AdvanceTick are not safe to call concurrently, including from delayed + // item or death callbacks. + Synchronous bool } // New creates a new World using the Config conf. The World returned will start @@ -90,6 +107,9 @@ func (conf Config) New() *World { if conf.ChunkUnloadInterval <= 0 { conf.ChunkUnloadInterval = time.Minute * 2 } + if conf.ChunkLoadWorkers <= 0 { + conf.ChunkLoadWorkers = defaultChunkLoadWorkers + } if conf.Generator == nil { conf.Generator = NopGenerator{} } @@ -119,12 +139,23 @@ func (conf Config) New() *World { conf.RandSource = rand.NewPCG(t, t) } s := conf.Provider.Settings() + + // Serialise Provider calls (made by both the owner and the chunk load workers) + // and, with a single worker, Generator calls, for implementations that aren't + // concurrency-safe. + conf.Provider = &lockedProvider{p: conf.Provider} + if conf.ChunkLoadWorkers == 1 { + conf.Generator = &lockedGenerator{g: conf.Generator} + } w := &World{ scheduledUpdates: newScheduledTickQueue(s.CurrentTick), + redstone: newRedstoneEngine(s.CurrentTick), entities: make(map[*EntityHandle]ChunkPos), viewers: make(map[*Loader]Viewer), chunks: make(map[ChunkPos]*Column), + chunkRequests: make(map[ChunkPos]*chunkRequest), queueClosing: make(chan struct{}), + closeStarted: make(chan struct{}), closing: make(chan struct{}), queue: make(chan transaction, 128), r: rand.New(conf.RandSource), @@ -133,18 +164,25 @@ func (conf Config) New() *World { ra: conf.Dim.Range(), set: s, } + w.chunkWorkers = newChunkWorkerPool(w) w.weather = weather{w: w} var h Handler = NopHandler{} w.handler.Store(&h) - w.queueing.Add(1) - w.running.Add(2) - t := ticker{interval: time.Second / 20} - go t.tickLoop(w) - go w.autoSave() - go w.handleTransactions() + if !conf.Synchronous { + w.queueing.Add(1) + w.running.Add(2) + + go t.tickLoop(w) + go w.autoSave() + go w.handleTransactions() + w.chunkWorkers.wg.Add(conf.ChunkLoadWorkers) + for range conf.ChunkLoadWorkers { + go w.chunkWorkers.handle() + } + } - <-w.Exec(t.tick) + <-w.exec(t.tick) return w } diff --git a/server/world/dimension.go b/server/world/dimension.go index 6eb0d818dc..a61b493e2c 100644 --- a/server/world/dimension.go +++ b/server/world/dimension.go @@ -1,6 +1,9 @@ package world import ( + "fmt" + "math" + "slices" "time" "github.com/df-mc/dragonfly/server/block/cube" @@ -41,6 +44,14 @@ func DimensionID(dim Dimension) (int, bool) { type dimensionRegistry struct { dimensions map[int]Dimension ids map[Dimension]int + custom []DimensionRegistration +} + +// DimensionRegistration holds a custom dimension's registration data. +type DimensionRegistration struct { + ID int + Name string + Dimension Dimension } // newDimensionRegistry returns an initialised dimensionRegistry. @@ -75,6 +86,54 @@ func (reg *dimensionRegistry) LookupID(dim Dimension) (int, bool) { return id, ok } +// RegisterDimension registers a custom dimension. +func (reg *dimensionRegistry) RegisterDimension(id int, name string, dim Dimension) error { + if id < 1000 || id > math.MaxUint16 { + return fmt.Errorf("custom dimension ID must be between 1000 and %d", math.MaxUint16) + } + if name == "" { + return fmt.Errorf("custom dimension name must not be empty") + } + if dim == nil { + return fmt.Errorf("custom dimension must not be nil") + } + r := dim.Range() + if r.Min() > r.Max() { + return fmt.Errorf("custom dimension range must not be empty") + } + if r.Min()%16 != 0 || (r.Max()+1)%16 != 0 { + return fmt.Errorf("custom dimension range must align with 16-block sub-chunks") + } + if r.Min() < math.MinInt16 || r.Max() > math.MaxInt16 { + return fmt.Errorf("custom dimension range must be between %d and %d", math.MinInt16, math.MaxInt16) + } + if _, ok := reg.dimensions[id]; ok { + return fmt.Errorf("dimension ID %d is already registered", id) + } + if existing, ok := reg.ids[dim]; ok { + return fmt.Errorf("dimension is already registered with ID %d", existing) + } + for _, existing := range reg.custom { + if existing.Name == name { + return fmt.Errorf("dimension name %q is already registered", name) + } + } + reg.dimensions[id] = dim + reg.ids[dim] = id + reg.custom = append(reg.custom, DimensionRegistration{ID: id, Name: name, Dimension: dim}) + return nil +} + +// RegisterDimension registers a custom dimension. +func RegisterDimension(id int, name string, dim Dimension) error { + return dimensionReg.RegisterDimension(id, name, dim) +} + +// CustomDimensions returns all registered custom dimensions. +func CustomDimensions() []DimensionRegistration { + return slices.Clone(dimensionReg.custom) +} + type ( // Dimension is a dimension of a World. It influences a variety of // properties of a World such as the building range, the sky colour and the diff --git a/server/world/entity.go b/server/world/entity.go index 89552ffa3a..3daa7914f4 100644 --- a/server/world/entity.go +++ b/server/world/entity.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "io" "maps" + "math" "slices" "sync" "sync/atomic" @@ -17,7 +18,7 @@ import ( // EntityType is the type of Entity. It specifies the name, encoded Entity // ID and bounding box of an Entity. type EntityType interface { - // Open returns an Entity implementation in the context of a transaction. + // Open returns an Entity implementation in a transaction. Open(tx *Tx, handle *EntityHandle, data *EntityData) Entity // EncodeEntity converts the Entity to its encoded representation: It @@ -51,6 +52,17 @@ type EntityHandle struct { worldless *atomic.Bool weakTxActive bool w *World + // worldReady becomes true only after AddEntity finishes registering and + // opening the entity in w. Scheduled callbacks wait for this handoff. + worldReady bool + // worldVersion increments on every change to w, letting weak transactions + // detect that the entity moved while they were queued. + worldVersion atomic.Uint64 + // closed closes once the handle is closed. worldChanged, created lazily + // for delayed schedulers, is closed and dropped on every change to w. + closed chan struct{} + worldChanged chan struct{} + closeOnce sync.Once data EntityData @@ -83,7 +95,14 @@ func (opts EntitySpawnOpts) New(t EntityType, conf EntityConfig) *EntityHandle { opts.ID = uuid.New() clear(opts.ID[:8]) } - handle := &EntityHandle{id: opts.ID, t: t, cond: sync.NewCond(&sync.Mutex{}), worldless: &atomic.Bool{}} + handle := &EntityHandle{ + id: opts.ID, + t: t, + cond: sync.NewCond(&sync.Mutex{}), + worldless: &atomic.Bool{}, + closed: make(chan struct{}), + data: EntityData{AlwaysShowNameTag: true}, + } handle.worldless.Store(true) handle.data.Pos, handle.data.Rot, handle.data.Vel = opts.Position, opts.Rotation, opts.Velocity handle.data.Name = opts.NameTag @@ -102,7 +121,13 @@ func NewEntity(t EntityType, conf EntityConfig) *EntityHandle { // entityFromData reads an entity from the decoded NBT data passed and returns // an EntityHandle. func entityFromData(t EntityType, id int64, data map[string]any) *EntityHandle { - handle := &EntityHandle{t: t, cond: sync.NewCond(&sync.Mutex{}), worldless: &atomic.Bool{}} + handle := &EntityHandle{ + t: t, + cond: sync.NewCond(&sync.Mutex{}), + worldless: &atomic.Bool{}, + closed: make(chan struct{}), + data: EntityData{AlwaysShowNameTag: true}, + } binary.LittleEndian.PutUint64(handle.id[8:], uint64(id)) handle.decodeNBT(data) t.DecodeNBT(data, &handle.data) @@ -137,37 +162,69 @@ func (e *EntityHandle) UUID() uuid.UUID { return e.id } -// Close closes the EntityHandle. Any subsequent call to ExecWorld will return -// immediately without the transaction function being called. Close always -// returns nil. +// Closed reports whether the EntityHandle has been closed. +func (e *EntityHandle) Closed() bool { + if e == nil { + return true + } + select { + case <-e.closed: + return true + default: + return false + } +} + +// Close closes the EntityHandle. Any subsequently scheduled work will fail +// with ErrEntityClosed without the transaction function being called. Close +// always returns nil. func (e *EntityHandle) Close() error { - e.setAndUnlockWorld(closeWorld) + e.closeOnce.Do(func() { + e.setAndUnlockWorld(closeWorld) + close(e.closed) + }) return nil } -// ExecWorld obtains the EntityHandle's World in a thread-safe way and opens a -// transaction in it when it does. If the EntityHandle has not been added to a -// world, ExecWorld will block until the EntityHandle is added to a World and -// run the transaction function once it is. If the Entity is closed before -// ExecWorld is called, ExecWorld will return false immediately without running -// the transaction function. -func (e *EntityHandle) ExecWorld(f func(tx *Tx, e Entity)) bool { - return e.execWorld(f, false) +func cancelled(c <-chan struct{}) bool { + if c == nil { + return false + } + select { + case <-c: + return true + default: + return false + } } // execWorld uses a sync.Cond to synchronise access to the handler's world. We // are dealing with a rather complicated synchronisation pattern here. The goal -// for ExecWorld is to block until e.w becomes accessible. Meanwhile, World.Exec +// for execWorld is to block until e.w becomes accessible. Meanwhile, World.exec // may also affect e.w, which execWorld needs to deal with. -func (e *EntityHandle) execWorld(f func(tx *Tx, e Entity), weak bool) bool { +func (e *EntityHandle) execWorld(f func(tx *Tx, e Entity), weak bool, cancel <-chan struct{}, allowedCloseWorld *World) bool { e.cond.L.Lock() - for e.w == nil || (!weak && e.weakTxActive) { + for e.w == nil || (e.w != closeWorld && (!e.worldReady || (!weak && e.weakTxActive))) { + if cancelled(cancel) { + if weak { + e.clearWeakTxActiveLocked() + } + e.cond.L.Unlock() + return false + } // Wait suspends the current goroutine and unlocks e.cond.L, until // e.cond.Broadcast() is called. After this, one of the goroutines // waiting will acquire a lock of e.cond.L again. This means that only // one goroutine will run the code after this simultaneously. e.cond.Wait() } + if cancelled(cancel) { + if weak { + e.clearWeakTxActiveLocked() + } + e.cond.L.Unlock() + return false + } // If a goroutine manages to exit the for loop, it will have acquired a lock // on e.cond.L. This also means that e.w can be assumed to not be nil here. // Because of the lock on e.cond.L, no other transaction will be able to @@ -176,28 +233,52 @@ func (e *EntityHandle) execWorld(f func(tx *Tx, e Entity), weak bool) bool { e.worldless.Store(false) if e.w == closeWorld { // EntityHandle was closed. No need to continue. + if weak { + e.clearWeakTxActiveLocked() + } + e.cond.L.Unlock() + return false + } + if e.w.closed.Load() && !e.w.closeAcceptingEntityTasks.Load() && e.w != allowedCloseWorld { + if weak { + e.clearWeakTxActiveLocked() + } e.cond.L.Unlock() return false } - // We now arrive at the more complicated part. When we call e.w.Exec(), our + // We now arrive at the more complicated part. When we call e.w.exec(), our // transaction must await earlier transactions in the world. If one of those - // earlier transactions tries to change e.w (through e.unsetAndLockWorld() - // or e.setAndUnlockWorld()), it must lock e.cond.L. This would lead to a - // deadlock, because we already have e.cond.L locked here. + // earlier transactions tries to change e.w (through e.unsetAndLockWorld(), + // e.setAndUnlockWorld(), or e.setAndUnlockWorldAt()), it must lock + // e.cond.L. This would lead to a deadlock, because we already have e.cond.L + // locked here. // We work around this with so-called "weak transactions". This is a // transaction that may be invalidated before it is executed. In this case, // this invalidation happens by setting e.worldless to true. If the // transaction turns out to be invalidated (ret == false), we simply try // again, this time with e.execWorld(f, true) to make this goroutine bypass // any goroutines still awaiting e.cond. - ret := e.weakExec(func(tx *Tx) { f(tx, e.mustEntity(tx)) }) + var ran atomic.Bool + ret := e.weakExec(func(tx *Tx) { + ent := e.mustEntity(tx) + ran.Store(true) + f(tx, ent) + }, allowedCloseWorld) + if !ret && e.w != nil && e.w != closeWorld && e.w.closed.Load() && !e.w.closeAcceptingEntityTasks.Load() && e.w != allowedCloseWorld { + e.clearWeakTxActiveLocked() + e.cond.L.Unlock() + return false + } e.cond.L.Unlock() + if ran.Load() { + return true + } if !ret { // Our weak transaction was suspended. We try again, this time with // e.execWorld(f, true) to make this goroutine bypass any goroutines // still awaiting e.cond. - return e.execWorld(f, true) + return e.execWorld(f, true, cancel, allowedCloseWorld) } return true } @@ -205,17 +286,20 @@ func (e *EntityHandle) execWorld(f func(tx *Tx, e Entity), weak bool) bool { // weakExec performs a "weak transaction". It adds a transaction to the world // that is invalidated when e.worldless is set to true. In this case, weakExec // returns false. If the weak transaction is successfully executed, it returns -// true, and any calls to ExecWorld waiting on e.cond are awakened. The goal of +// true, and any calls to execWorld waiting on e.cond are awakened. The goal of // weakExec is to suspend the current goroutine and unlock e.cond.L while // waiting for previous transactions to finish. -func (e *EntityHandle) weakExec(f ExecFunc) bool { +func (e *EntityHandle) weakExec(f execFunc, allowedCloseWorld *World) bool { e.weakTxActive = true + w, version := e.w, e.worldVersion.Load() // We create a weak transaction and start a for loop to listen for the // length of the channel. This might look weird, but the crucial part here // is the call to e.cond.Wait(), which unlocks e.cond.L. This is required // to prevent a deadlock if an earlier transaction tries to change e.w. - c := e.w.weakExec(e.worldless, e.cond, f) + c := w.weakExec(func() bool { + return e.worldVersion.Load() == version && !e.worldless.Load() + }, e.cond, f, w == allowedCloseWorld) for len(c) == 0 && e.w != closeWorld { // Calling e.cond.Wait() here will free the lock on e.cond.L until our // transaction finishes. e.w.weakExec() ensures that e.cond.Broadcast() @@ -223,8 +307,6 @@ func (e *EntityHandle) weakExec(f ExecFunc) bool { // continue after that. e.cond.Wait() } - // If the EntityHandle was closed (e.w == closeWorld), we treat the - // transaction as successful, because all transactions must be cancelled. if e.w != closeWorld && !<-c { // Weak transaction was suspended. Return false and try again. return false @@ -232,14 +314,22 @@ func (e *EntityHandle) weakExec(f ExecFunc) bool { // After setting e.weakTxActive back to false, we must Broadcast to make // sure any goroutines waiting in e.execWorld as a result of the // e.weakTxActive condition can continue. + closed := e.w == closeWorld e.weakTxActive = false e.cond.Broadcast() - return true + return !closed +} + +func (e *EntityHandle) clearWeakTxActiveLocked() { + if e.weakTxActive { + e.weakTxActive = false + e.cond.Broadcast() + } } var closeWorld = &World{} -// unsetAndLockWorld sets e.w to nil, causing any subsequent calls to ExecWorld +// unsetAndLockWorld sets e.w to nil, causing any subsequent calls to execWorld // to block until e.w is set to a non-nil value. func (e *EntityHandle) unsetAndLockWorld() { e.cond.L.Lock() @@ -247,10 +337,13 @@ func (e *EntityHandle) unsetAndLockWorld() { e.worldless.Store(true) e.w = nil + e.worldReady = false + e.worldVersion.Add(1) + e.notifyWorldChangedLocked() } -// setAndUnlockWorld sets e.w to a World passed and broadcasts e.cond, so that -// any goroutines waiting for a non-nil world are awoken. +// setAndUnlockWorld starts binding e to w. Scheduled callbacks remain blocked +// until markWorldReady is called after AddEntity finishes. func (e *EntityHandle) setAndUnlockWorld(w *World) { e.cond.L.Lock() defer e.cond.L.Unlock() @@ -259,6 +352,41 @@ func (e *EntityHandle) setAndUnlockWorld(w *World) { panic("cannot add entity to new world before removing from old world") } e.w = w + e.worldReady = false + e.worldVersion.Add(1) + e.notifyWorldChangedLocked() +} + +// markWorldReady wakes scheduled callbacks after AddEntity has fully opened +// and registered the entity in w. +func (e *EntityHandle) markWorldReady(w *World) { + e.cond.L.Lock() + defer e.cond.L.Unlock() + if e.w == w { + e.worldReady = true + e.cond.Broadcast() + } +} + +func (e *EntityHandle) notifyWorldChangedLocked() { + if e.worldChanged != nil { + close(e.worldChanged) + e.worldChanged = nil + } + e.cond.Broadcast() +} + +// setAndUnlockWorldAt sets e's position before publishing e to the World +// passed, then broadcasts e.cond so waiters can open the entity. +func (e *EntityHandle) setAndUnlockWorldAt(w *World, pos mgl64.Vec3) { + e.cond.L.Lock() + defer e.cond.L.Unlock() + + if e.w != nil { + panic("cannot add entity to new world before removing from old world") + } + e.data.Pos = pos + e.w = w e.cond.Broadcast() } @@ -282,18 +410,19 @@ func (e *EntityHandle) encodeNBT() map[string]any { "Yaw": float32(e.data.Rot[0]), "Pitch": float32(e.data.Rot[1]), "Fire": int16(e.data.FireDuration.Seconds() * 20), - "Age": int16(e.data.Age / (time.Second * 20)), + "Age": int16(min(e.data.Age/(time.Second/20), math.MaxInt16)), "NameTag": e.data.Name, } } // EntityData holds data shared by every entity. It is kept in an EntityHandle. type EntityData struct { - Pos, Vel mgl64.Vec3 - Rot cube.Rotation - Name string - FireDuration time.Duration - Age time.Duration + Pos, Vel mgl64.Vec3 + Rot cube.Rotation + Name string + AlwaysShowNameTag bool + FireDuration time.Duration + Age time.Duration Data any } @@ -374,6 +503,7 @@ type EntityRegistryConfig struct { BottleOfEnchanting func(opts EntitySpawnOpts, owner Entity) *EntityHandle Arrow func(opts EntitySpawnOpts, conf ArrowSpawnConfig) *EntityHandle Egg func(opts EntitySpawnOpts, owner Entity) *EntityHandle + EndCrystal func(opts EntitySpawnOpts) *EntityHandle EnderPearl func(opts EntitySpawnOpts, owner Entity) *EntityHandle Firework func(opts EntitySpawnOpts, firework Item, owner Entity, sidewaysVelocityMultiplier, upwardsAcceleration float64, attached bool) *EntityHandle LingeringPotion func(opts EntitySpawnOpts, t any, owner Entity) *EntityHandle diff --git a/server/world/entity_ref.go b/server/world/entity_ref.go new file mode 100644 index 0000000000..7251187a95 --- /dev/null +++ b/server/world/entity_ref.go @@ -0,0 +1,77 @@ +package world + +import ( + "context" + "fmt" + "time" +) + +// EntityRef is a stable, typed reference to an entity. The entity value T is +// only handed to scheduled owner callbacks, where it is safe to use. +type EntityRef[T Entity] struct { + h *EntityHandle +} + +// NewEntityRef creates a typed reference from an EntityHandle. +func NewEntityRef[T Entity](h *EntityHandle) EntityRef[T] { return EntityRef[T]{h: h} } + +// Handle returns the underlying stable entity handle. +func (r EntityRef[T]) Handle() *EntityHandle { return r.h } + +// Do schedules f on the entity's current world owner, like EntityHandle.Do, +// but hands f the entity as T. If the entity is no longer a T when the task +// runs, the task fails with ErrEntityType. +func (r EntityRef[T]) Do(f func(tx *Tx, e T)) *Task { + return r.h.schedule(typed(f)) +} + +// DoAfter schedules f on the entity's world owner after delay, typed like Do. +func (r EntityRef[T]) DoAfter(delay time.Duration, f func(tx *Tx, e T)) *Task { + return r.h.scheduleAfter(delay, typed(f)) +} + +// typed wraps f so the scheduled entity is asserted to T before f runs. +func typed[T Entity](f func(tx *Tx, e T)) func(*Tx, Entity) error { + return func(tx *Tx, e Entity) error { + v, err := assertEntity[T](e) + if err != nil { + return err + } + f(tx, v) + return nil + } +} + +// CallRef runs f with the ref's entity on its current world owner and waits +// for the typed result. Off-owner code only, like Call. If f panics, CallRef +// re-panics with the original value on the waiting goroutine. Context +// cancellation stops pending work, but CallRef waits for a callback that has +// already started. +func CallRef[T any, E Entity](ctx context.Context, ref EntityRef[E], f func(tx *Tx, e E) (T, error)) (T, error) { + var zero T + ctx, err := callContext(ctx) + if err != nil { + return zero, err + } + var result T + task := ref.h.schedule(func(tx *Tx, e Entity) error { + v, err := assertEntity[E](e) + if err != nil { + return err + } + var callErr error + result, callErr = f(tx, v) + return callErr + }) + return awaitTask(ctx, task, &result) +} + +// assertEntity converts e to T, returning ErrEntityType if it no longer is one. +func assertEntity[T Entity](e Entity) (T, error) { + v, ok := e.(T) + if !ok { + var zero T + return zero, fmt.Errorf("%w: got %T", ErrEntityType, e) + } + return v, nil +} diff --git a/server/world/entity_schedule.go b/server/world/entity_schedule.go new file mode 100644 index 0000000000..bd2a6b4748 --- /dev/null +++ b/server/world/entity_schedule.go @@ -0,0 +1,179 @@ +package world + +import "time" + +// Do schedules f to run with the entity on its current world owner and +// returns immediately. If the entity is in no world yet, the task waits until +// it enters one or the handle closes. The entity passed to f is only valid +// inside f. On a synchronous World, f runs before Do returns. +func (e *EntityHandle) Do(f func(tx *Tx, e Entity)) *Task { + return e.schedule(func(tx *Tx, e Entity) error { + f(tx, e) + return nil + }) +} + +// DoAfter schedules f to run with the entity after delay, following the +// entity if it changes worlds in the meantime. +func (e *EntityHandle) DoAfter(delay time.Duration, f func(tx *Tx, e Entity)) *Task { + return e.scheduleAfter(delay, func(tx *Tx, e Entity) error { + f(tx, e) + return nil + }) +} + +// schedule runs f on the entity's current world owner from a goroutine that +// waits for the entity to be world-bound. It deliberately never enqueues onto +// the current world directly: that would commit to one world and fail instead +// of following the entity when it migrates. +func (e *EntityHandle) schedule(f func(tx *Tx, e Entity) error) *Task { + task := newTask() + if e == nil { + task.failIfPending(ErrEntityClosed) + return task + } + task.setCancel(e.wakeScheduled) + w := e.trackCloseSchedule(task) + if !task.pending() { + return task + } + run := func() { + if w != nil { + defer w.scheduling.Done() + } + e.runScheduled(task, f, w) + } + if e.currentWorldSynchronous() { + run() + } else { + go run() + } + return task +} + +// scheduleAfter runs its own timer loop instead of reusing World.DoAfter: +// the entity may change worlds during the delay, so the loop re-reads the +// current world's signals every iteration. +func (e *EntityHandle) scheduleAfter(delay time.Duration, f func(tx *Tx, e Entity) error) *Task { + if delay <= 0 { + return e.schedule(f) + } + task := newTask() + if e == nil { + task.failIfPending(ErrEntityClosed) + return task + } + task.setCancel(e.wakeScheduled) + go func() { + timer := time.NewTimer(delay) + defer timer.Stop() + for { + closeStarted, worldChanged := e.currentWorldSignals() + select { + case <-timer.C: + if e.currentWorldClosing() { + task.failIfPending(ErrWorldClosed) + return + } + e.runScheduled(task, f, nil) + return + case <-task.Done(): + return + case <-closeStarted: + if cs, _ := e.currentWorldSignals(); cs == closeStarted { + task.failIfPending(ErrWorldClosed) + return + } + case <-worldChanged: + case <-e.closed: + task.failIfPending(ErrEntityClosed) + return + } + } + }() + return task +} + +// trackCloseSchedule registers entity work created during the world's close +// transaction, so World.close drains it before shutting the queue down. +func (e *EntityHandle) trackCloseSchedule(task *Task) *World { + e.cond.L.Lock() + defer e.cond.L.Unlock() + w := e.w + if w == nil || w == closeWorld || !w.closed.Load() { + return nil + } + w.scheduleMu.Lock() + defer w.scheduleMu.Unlock() + if !w.closeAcceptingEntityTasks.Load() { + task.failIfPending(ErrWorldClosed) + return nil + } + select { + case <-w.queueClosing: + task.failIfPending(ErrWorldClosed) + return nil + default: + w.scheduling.Add(1) + return w + } +} + +// wakeScheduled wakes goroutines waiting on the handle's cond, so a scheduler +// blocked in execWorld re-checks its cancel signal. +func (e *EntityHandle) wakeScheduled() { + e.cond.L.Lock() + e.cond.Broadcast() + e.cond.L.Unlock() +} + +// currentWorldSignals returns the current world's close channel and the +// handle's world-change channel, creating the latter for this waiter if +// needed. +func (e *EntityHandle) currentWorldSignals() (<-chan struct{}, <-chan struct{}) { + e.cond.L.Lock() + defer e.cond.L.Unlock() + if e.worldChanged == nil { + e.worldChanged = make(chan struct{}) + } + if e.w == nil || e.w == closeWorld { + return nil, e.worldChanged + } + return e.w.closeStarted, e.worldChanged +} + +// currentWorldSynchronous reports whether the entity is bound to a +// synchronous World. +func (e *EntityHandle) currentWorldSynchronous() bool { + e.cond.L.Lock() + defer e.cond.L.Unlock() + return e.w != nil && e.w != closeWorld && e.worldReady && e.w.conf.Synchronous +} + +// currentWorldClosing reports whether the entity's current world has started +// closing. +func (e *EntityHandle) currentWorldClosing() bool { + closeStarted, _ := e.currentWorldSignals() + return cancelled(closeStarted) +} + +// runScheduled executes the scheduled entity callback via execWorld with the +// same completion model as scheduledTransaction: run, drain deferred work, +// finish the task. +func (e *EntityHandle) runScheduled(task *Task, f func(tx *Tx, e Entity) error, allowedCloseWorld *World) { + run := e.execWorld(func(tx *Tx, ent Entity) { + if !task.begin() { + return + } + err := executeWithRecovery(tx.w, func() error { return f(tx, ent) }) + tx.runDeferred() + task.finish(err) + }, false, task.Done(), allowedCloseWorld) + if !run || task.pending() { + err := ErrEntityClosed + if e.currentWorldClosing() { + err = ErrWorldClosed + } + task.failIfPending(err) + } +} diff --git a/server/world/explosion.go b/server/world/explosion.go new file mode 100644 index 0000000000..187990cab3 --- /dev/null +++ b/server/world/explosion.go @@ -0,0 +1,62 @@ +package world + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl64" +) + +// defaultExplosionSize is the size used if a source does not specify one. +const defaultExplosionSize = 4 + +// ExplosionSource represents the source of an explosion. +type ExplosionSource interface { + // Position returns the position at the centre of the explosion. It must + // return the same position for the duration of an explosion. + Position() mgl64.Vec3 + // Size returns the radius which entities/blocks are affected within. + Size() float64 +} + +// EntityExplosionSource is used for an explosion caused by an entity. +type EntityExplosionSource struct { + // Entity is the entity that caused the explosion. + Entity Entity + // ExplosionSize is the size of the explosion. Defaults to 4 if 0. + ExplosionSize float64 +} + +// Position ... +func (e EntityExplosionSource) Position() mgl64.Vec3 { + return e.Entity.Position() +} + +// Size ... +func (e EntityExplosionSource) Size() float64 { + if e.ExplosionSize == 0 { + return defaultExplosionSize + } + return e.ExplosionSize +} + +// BlockExplosionSource is used for an explosion caused by a block. +type BlockExplosionSource struct { + // Block is the block that caused the explosion. + Block Block + // Pos is the position of the block that caused the explosion. + Pos cube.Pos + // ExplosionSize is the size of the explosion. Defaults to 4 if 0. + ExplosionSize float64 +} + +// Position ... +func (b BlockExplosionSource) Position() mgl64.Vec3 { + return b.Pos.Vec3Centre() +} + +// Size ... +func (b BlockExplosionSource) Size() float64 { + if b.ExplosionSize == 0 { + return defaultExplosionSize + } + return b.ExplosionSize +} diff --git a/server/world/game_mode.go b/server/world/game_mode.go index 0ae79f024e..b1769b8c42 100644 --- a/server/world/game_mode.go +++ b/server/world/game_mode.go @@ -20,6 +20,9 @@ type GameMode interface { // Visible specifies if a player with this GameMode can be visible to other players. If false, the player will be // invisible under any circumstance. Visible() bool + // InstantPortalTravel specifies if a player with this GameMode travels through nether portals instantly, + // without the four second wait. + InstantPortalTravel() bool } var ( @@ -96,47 +99,51 @@ func (reg *gameModeRegistry) LookupID(mode GameMode) (int, bool) { // taking some time. type survival struct{} -func (survival) AllowsEditing() bool { return true } -func (survival) AllowsTakingDamage() bool { return true } -func (survival) CreativeInventory() bool { return false } -func (survival) HasCollision() bool { return true } -func (survival) AllowsFlying() bool { return false } -func (survival) AllowsInteraction() bool { return true } -func (survival) Visible() bool { return true } +func (survival) AllowsEditing() bool { return true } +func (survival) AllowsTakingDamage() bool { return true } +func (survival) CreativeInventory() bool { return false } +func (survival) HasCollision() bool { return true } +func (survival) AllowsFlying() bool { return false } +func (survival) AllowsInteraction() bool { return true } +func (survival) Visible() bool { return true } +func (survival) InstantPortalTravel() bool { return false } // creative represents the creative game mode: Players with this game mode have infinite blocks and // items and can break blocks instantly. Players with creative mode can also fly. type creative struct{} -func (creative) AllowsEditing() bool { return true } -func (creative) AllowsTakingDamage() bool { return false } -func (creative) CreativeInventory() bool { return true } -func (creative) HasCollision() bool { return true } -func (creative) AllowsFlying() bool { return true } -func (creative) AllowsInteraction() bool { return true } -func (creative) Visible() bool { return true } +func (creative) AllowsEditing() bool { return true } +func (creative) AllowsTakingDamage() bool { return false } +func (creative) CreativeInventory() bool { return true } +func (creative) HasCollision() bool { return true } +func (creative) AllowsFlying() bool { return true } +func (creative) AllowsInteraction() bool { return true } +func (creative) Visible() bool { return true } +func (creative) InstantPortalTravel() bool { return true } // adventure represents the adventure game mode: Players with this game mode cannot edit the world // (placing or breaking blocks). type adventure struct{} -func (adventure) AllowsEditing() bool { return false } -func (adventure) AllowsTakingDamage() bool { return true } -func (adventure) CreativeInventory() bool { return false } -func (adventure) HasCollision() bool { return true } -func (adventure) AllowsFlying() bool { return false } -func (adventure) AllowsInteraction() bool { return true } -func (adventure) Visible() bool { return true } +func (adventure) AllowsEditing() bool { return false } +func (adventure) AllowsTakingDamage() bool { return true } +func (adventure) CreativeInventory() bool { return false } +func (adventure) HasCollision() bool { return true } +func (adventure) AllowsFlying() bool { return false } +func (adventure) AllowsInteraction() bool { return true } +func (adventure) Visible() bool { return true } +func (adventure) InstantPortalTravel() bool { return false } // spectator represents the spectator game mode: Players with this game mode cannot interact with the // world and cannot be seen by other players. spectator players can fly, like creative mode, and can // move through blocks. type spectator struct{} -func (spectator) AllowsEditing() bool { return false } -func (spectator) AllowsTakingDamage() bool { return false } -func (spectator) CreativeInventory() bool { return false } -func (spectator) HasCollision() bool { return false } -func (spectator) AllowsFlying() bool { return true } -func (spectator) AllowsInteraction() bool { return false } -func (spectator) Visible() bool { return false } +func (spectator) AllowsEditing() bool { return false } +func (spectator) AllowsTakingDamage() bool { return false } +func (spectator) CreativeInventory() bool { return false } +func (spectator) HasCollision() bool { return false } +func (spectator) AllowsFlying() bool { return true } +func (spectator) AllowsInteraction() bool { return false } +func (spectator) Visible() bool { return false } +func (spectator) InstantPortalTravel() bool { return false } diff --git a/server/world/generator.go b/server/world/generator.go index 6afd07b81c..7e901d31d5 100644 --- a/server/world/generator.go +++ b/server/world/generator.go @@ -1,6 +1,8 @@ package world import ( + "sync" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world/chunk" ) @@ -9,7 +11,7 @@ import ( // generate chunks when the provider of the world cannot find a chunk at a given chunk position. type Generator interface { // GenerateChunk generates a chunk at a chunk position passed. The generator sets blocks in the chunk that - // is passed to the method. + // is passed to the method. With more than one chunk load worker, GenerateChunk is called concurrently. GenerateChunk(pos ChunkPos, chunk *chunk.Chunk) // DefaultSpawn returns the default spawn position for worlds using this generator in the dimension passed. DefaultSpawn(dim Dimension) cube.Pos @@ -24,3 +26,20 @@ func (NopGenerator) GenerateChunk(ChunkPos, *chunk.Chunk) {} // DefaultSpawn ... func (NopGenerator) DefaultSpawn(Dimension) cube.Pos { return cube.Pos{} } + +// lockedGenerator wraps a Generator, serialising GenerateChunk calls for +// generators that are not safe for concurrent use. +type lockedGenerator struct { + mu sync.Mutex + g Generator +} + +func (l *lockedGenerator) GenerateChunk(pos ChunkPos, c *chunk.Chunk) { + l.mu.Lock() + defer l.mu.Unlock() + l.g.GenerateChunk(pos, c) +} + +func (l *lockedGenerator) DefaultSpawn(dim Dimension) cube.Pos { + return l.g.DefaultSpawn(dim) +} diff --git a/server/world/handler.go b/server/world/handler.go index bdd248ae16..dd4cff22ba 100644 --- a/server/world/handler.go +++ b/server/world/handler.go @@ -2,12 +2,9 @@ package world import ( "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/event" "github.com/go-gl/mathgl/mgl64" ) -type Context = event.Context[*Tx] - // Handler handles events that are called by a world. Implementations of // Handler may be used to listen to specific events such as when an Entity is // added to the world. @@ -52,8 +49,16 @@ type Handler interface { // Leaves decaying happens when there is no wood block neighbouring it. // ctx.Cancel() may be called to prevent leaves from decaying. HandleLeavesDecay(ctx *Context, pos cube.Pos) + // HandlePortalCreate handles an active portal being built. portalType is + // Nether or End, and positions contains every block changed to build it. + // ctx.Cancel() may be called to prevent the portal from being built. + HandlePortalCreate(ctx *Context, portalType Dimension, positions []cube.Pos) + // HandlePortalActivate handles a portal frame being filled with portal + // blocks. portalType is Nether or End. ctx.Cancel() may be called to prevent + // the portal from being activated. + HandlePortalActivate(ctx *Context, portalType Dimension, positions []cube.Pos) // HandleEntitySpawn handles an Entity being spawned into a World through a - // call to Tx.AddEntity. + // call to Tx.AddEntity or Tx.AddEntityAt. HandleEntitySpawn(tx *Tx, e Entity) // HandleEntityDespawn handles an Entity being despawned from a World // through a call to Tx.RemoveEntity. @@ -62,10 +67,10 @@ type Handler interface { // to cancel the explosion. // The affected entities, affected blocks, item drop chance, and whether the // explosion spawns fire may be altered. - HandleExplosion(ctx *Context, position mgl64.Vec3, entities *[]Entity, blocks *[]cube.Pos, itemDropChance *float64, spawnFire *bool) - // HandleRedstoneUpdate handles a redstone update at a position. ctx.Cancel() may be called - // to cancel the redstone update. - HandleRedstoneUpdate(ctx *Context, pos cube.Pos) + HandleExplosion(ctx *Context, src ExplosionSource, entities *[]Entity, blocks *[]cube.Pos, itemDropChance *float64, spawnFire *bool) + // HandleRedstoneUpdate handles a redstone update proposed by the World redstone engine. ctx.Cancel() may be + // called to suppress the proposed redstone mutation and any propagation from that mutation. + HandleRedstoneUpdate(ctx *Context, update RedstoneUpdate) // HandleClose handles the World being closed. HandleClose may be used as a // moment to finish code running on other goroutines that operates on the // World specifically. HandleClose is called directly before the World stops @@ -81,16 +86,19 @@ var _ Handler = (*NopHandler)(nil) // Users may embed NopHandler to avoid having to implement each method. type NopHandler struct{} -func (NopHandler) HandleLiquidFlow(*Context, cube.Pos, cube.Pos, Liquid, Block) {} -func (NopHandler) HandleLiquidDecay(*Context, cube.Pos, Liquid, Liquid) {} -func (NopHandler) HandleLiquidHarden(*Context, cube.Pos, Block, Block, Block) {} -func (NopHandler) HandleSound(*Context, Sound, mgl64.Vec3) {} -func (NopHandler) HandleFireSpread(*Context, cube.Pos, cube.Pos) {} -func (NopHandler) HandleBlockBurn(*Context, cube.Pos) {} -func (NopHandler) HandleCropTrample(*Context, cube.Pos) {} -func (NopHandler) HandleLeavesDecay(*Context, cube.Pos) {} -func (NopHandler) HandleEntitySpawn(*Tx, Entity) {} -func (NopHandler) HandleEntityDespawn(*Tx, Entity) {} -func (NopHandler) HandleExplosion(*Context, mgl64.Vec3, *[]Entity, *[]cube.Pos, *float64, *bool) {} -func (NopHandler) HandleRedstoneUpdate(*Context, cube.Pos) {} -func (NopHandler) HandleClose(*Tx) {} +func (NopHandler) HandleLiquidFlow(*Context, cube.Pos, cube.Pos, Liquid, Block) {} +func (NopHandler) HandleLiquidDecay(*Context, cube.Pos, Liquid, Liquid) {} +func (NopHandler) HandleLiquidHarden(*Context, cube.Pos, Block, Block, Block) {} +func (NopHandler) HandleSound(*Context, Sound, mgl64.Vec3) {} +func (NopHandler) HandleFireSpread(*Context, cube.Pos, cube.Pos) {} +func (NopHandler) HandleBlockBurn(*Context, cube.Pos) {} +func (NopHandler) HandleCropTrample(*Context, cube.Pos) {} +func (NopHandler) HandleLeavesDecay(*Context, cube.Pos) {} +func (NopHandler) HandlePortalCreate(*Context, Dimension, []cube.Pos) {} +func (NopHandler) HandlePortalActivate(*Context, Dimension, []cube.Pos) {} +func (NopHandler) HandleEntitySpawn(*Tx, Entity) {} +func (NopHandler) HandleEntityDespawn(*Tx, Entity) {} +func (NopHandler) HandleExplosion(*Context, ExplosionSource, *[]Entity, *[]cube.Pos, *float64, *bool) { +} +func (NopHandler) HandleRedstoneUpdate(*Context, RedstoneUpdate) {} +func (NopHandler) HandleClose(*Tx) {} diff --git a/server/world/loader.go b/server/world/loader.go index e76e867747..16c985db5e 100644 --- a/server/world/loader.go +++ b/server/world/loader.go @@ -1,10 +1,11 @@ package world import ( - "github.com/go-gl/mathgl/mgl64" "maps" "math" "sync" + + "github.com/go-gl/mathgl/mgl64" ) // Loader implements the loading of the world. A loader can typically be moved around the world to load @@ -19,6 +20,7 @@ type Loader struct { pos ChunkPos loadQueue []ChunkPos loaded map[ChunkPos]*Column + pending map[ChunkPos]struct{} closed bool } @@ -28,7 +30,7 @@ type Loader struct { // The Viewer passed will handle the loading of chunks, including the viewing of entities that were loaded in // those chunks. func NewLoader(chunkRadius int, world *World, v Viewer) *Loader { - l := &Loader{r: chunkRadius, loaded: make(map[ChunkPos]*Column), viewer: v} + l := &Loader{r: chunkRadius, loaded: make(map[ChunkPos]*Column), pending: make(map[ChunkPos]struct{}), viewer: v} l.world(world) return l } @@ -47,12 +49,13 @@ func (l *Loader) ChangeWorld(tx *Tx, new *World) { defer l.mu.Unlock() loaded := maps.Clone(l.loaded) - l.w.Exec(func(tx *Tx) { + l.w.exec(func(tx *Tx) { for pos := range loaded { tx.World().removeViewer(tx, pos, l) } }) clear(l.loaded) + clear(l.pending) l.w.viewerMu.Lock() delete(l.w.viewers, l) l.w.viewerMu.Unlock() @@ -84,33 +87,64 @@ func (l *Loader) Move(tx *Tx, pos mgl64.Vec3) { l.populateLoadQueue() } -// Load loads n chunks around the centre of the chunk, starting with the middle and working outwards. For -// every chunk loaded, the Viewer passed through construction in New has its ViewChunk method called. -// Load does nothing for n <= 0. +// Load queues up to n chunks around the loader's centre, from the middle outwards, to be loaded in +// the background. The Viewer's ViewChunk is called for each chunk once ready, which may be after Load +// returns. Load does nothing for n <= 0. func (l *Loader) Load(tx *Tx, n int) { - l.mu.Lock() - defer l.mu.Unlock() - - if l.closed || l.w == nil { - return - } for i := 0; i < n; i++ { + l.mu.Lock() + if l.closed || l.w == nil { + l.mu.Unlock() + return + } if len(l.loadQueue) == 0 { + l.mu.Unlock() break } - pos := l.loadQueue[0] - c := tx.w.chunk(pos) - - l.viewer.ViewChunk(pos, l.w.Dimension(), c.BlockEntities, c.Chunk) - l.w.addViewer(tx, c, l) - - l.loaded[pos] = c + w := tx.World() + l.pending[pos] = struct{}{} // Shift the first element from the load queue off so that we can take a new one during the next // iteration. l.loadQueue = l.loadQueue[1:] + l.mu.Unlock() + + if !w.loadChunkAsync(tx, pos, func(tx2 *Tx, col *Column) { + l.viewChunk(tx2, pos, col) + }) { + l.mu.Lock() + delete(l.pending, pos) + l.queueLoad(pos) + l.mu.Unlock() + } + } +} + +// viewChunk passes a loaded chunk to the Loader's Viewer. If the chunk failed +// to load, it is queued to be loaded again. +func (l *Loader) viewChunk(tx *Tx, pos ChunkPos, c *Column) { + l.mu.Lock() + defer l.mu.Unlock() + + if l.closed || l.viewer == nil || l.w == nil || l.w != tx.World() { + return + } + delete(l.pending, pos) + if c == nil { + l.queueLoad(pos) + return + } + if _, ok := l.loaded[pos]; ok { + return } + if !l.withinLoadRadius(pos) { + return + } + l.viewer.ViewChunk(pos, l.w.Dimension(), c.BlockEntities, c.Chunk) + l.w.addViewer(tx, c, l) + + l.loaded[pos] = c } // Chunk attempts to return a chunk at the given ChunkPos. If the chunk is not loaded, the second return value will @@ -132,6 +166,7 @@ func (l *Loader) Close(tx *Tx) { tx.World().removeViewer(tx, pos, l) } l.loaded = map[ChunkPos]*Column{} + clear(l.pending) l.w.viewerMu.Lock() delete(l.w.viewers, l) @@ -153,15 +188,44 @@ func (l *Loader) world(new *World) { // and should therefore be removed. func (l *Loader) evictUnused(tx *Tx) { for pos := range l.loaded { - diffX, diffZ := pos[0]-l.pos[0], pos[1]-l.pos[1] - dist := math.Sqrt(float64(diffX*diffX) + float64(diffZ*diffZ)) - if int(dist) > l.r { + if !l.withinLoadRadius(pos) { delete(l.loaded, pos) l.w.removeViewer(tx, pos, l) } } } +// withinLoadRadius checks if a chunk position is within the Loader's radius. +func (l *Loader) withinLoadRadius(pos ChunkPos) bool { + return chunkDistance(pos, l.pos) <= int32(l.r) +} + +// chunkDistance returns the rounded distance between two chunk positions. +func chunkDistance(a, b ChunkPos) int32 { + diffX, diffZ := float64(a[0])-float64(b[0]), float64(a[1])-float64(b[1]) + return int32(math.Round(math.Sqrt(diffX*diffX + diffZ*diffZ))) +} + +// queueLoad adds pos back to the load queue, unless it is already loaded, +// queued, or no longer within the radius of the Loader. +func (l *Loader) queueLoad(pos ChunkPos) { + if l.closed || l.w == nil || !l.withinLoadRadius(pos) { + return + } + if _, ok := l.loaded[pos]; ok { + return + } + if _, ok := l.pending[pos]; ok { + return + } + for _, queued := range l.loadQueue { + if queued == pos { + return + } + } + l.loadQueue = append(l.loadQueue, pos) +} + // populateLoadQueue populates the load queue of the loader. This method is called once to create the order in // which chunks around the position the loader is now in should be loaded. Chunks are ordered to be loaded // from the middle outwards. @@ -173,27 +237,26 @@ func (l *Loader) populateLoadQueue() { r := int32(l.r) for x := -r; x <= r; x++ { for z := -r; z <= r; z++ { - distance := math.Sqrt(float64(x*x) + float64(z*z)) - chunkDistance := int32(math.Round(distance)) - if chunkDistance > r { + pos := ChunkPos{x + l.pos[0], z + l.pos[1]} + dist := chunkDistance(pos, l.pos) + if dist > r { // The chunk was outside the chunk radius. continue } - pos := ChunkPos{x + l.pos[0], z + l.pos[1]} if _, ok := l.loaded[pos]; ok { // The chunk was already loaded, so we don't need to do anything. continue } - if m, ok := queue[chunkDistance]; ok { - queue[chunkDistance] = append(m, pos) + if _, ok := l.pending[pos]; ok { + // The chunk is already queued to be loaded. continue } - queue[chunkDistance] = []ChunkPos{pos} + queue[dist] = append(queue[dist], pos) } } l.loadQueue = l.loadQueue[:0] - for i := int32(0); i < r; i++ { + for i := int32(0); i <= r; i++ { l.loadQueue = append(l.loadQueue, queue[i]...) } } diff --git a/server/world/portal/end.go b/server/world/portal/end.go new file mode 100644 index 0000000000..c64f0dad52 --- /dev/null +++ b/server/world/portal/end.go @@ -0,0 +1,185 @@ +package portal + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" +) + +// endSpawnX, endSpawnY and endSpawnZ are the centre of the End arrival platform. +const ( + endSpawnX = 100 + endSpawnY = 49 + endSpawnZ = 0 +) + +// EndSpawnPosition returns the Bedrock End platform arrival position: y=49 for players and y=50 for other entities. +func EndSpawnPosition(player bool) mgl64.Vec3 { + y := endSpawnY + 1 + if player { + y = endSpawnY + } + return mgl64.Vec3{float64(endSpawnX) + 0.5, float64(y), float64(endSpawnZ) + 0.5} +} + +// GenerateEndSpawnPlatform builds the 5x5 obsidian arrival platform at (100, 48, 0) and clears the 5x5x3 air column +// above it. It runs on every travel into the End, matching vanilla's unconditional regeneration. +func GenerateEndSpawnPlatform(tx *world.Tx) { + ob := obsidian() + for dx := -2; dx <= 2; dx++ { + for dz := -2; dz <= 2; dz++ { + tx.SetBlock(cube.Pos{endSpawnX + dx, endSpawnY - 1, endSpawnZ + dz}, ob, nil) + for dy := 0; dy < 3; dy++ { + tx.SetBlock(cube.Pos{endSpawnX + dx, endSpawnY + dy, endSpawnZ + dz}, nil, nil) + } + } + } +} + +// endRingFrame is one of the twelve canonical ring positions and the Facing each frame must have. +type endRingFrame struct { + pos cube.Pos + facing cube.Direction +} + +// endFrameBlock is implemented by block.EndPortalFrame, which cannot be imported here directly. +type endFrameBlock interface { + world.Block + EndPortalFrameState() (eye bool, facing cube.Direction) +} + +// ActivateEndPortal fills the 3x3 interior with end_portal blocks if a complete twelve-frame ring exists around the +// frame at the position passed. All twelve frames must hold an eye and face toward the centre, as in vanilla. +func ActivateEndPortal(tx *world.Tx, framePos cube.Pos) bool { + f, ok := tx.Block(framePos).(endFrameBlock) + if !ok { + return false + } + _, facing := f.EndPortalFrameState() + + // The frame may be the left, middle or right of its side: walk inward twice, then try the three candidate centres. + inward, tangent := facing.Face(), facing.RotateRight().Face() + base := framePos.Side(inward).Side(inward) + for _, center := range []cube.Pos{base.Side(tangent.Opposite()), base, base.Side(tangent)} { + interior, ok := matchEndRing(tx, center) + if !ok { + continue + } + ep := endPortal() + active := true + for _, pos := range interior { + if tx.Block(pos) != ep { + active = false + break + } + } + if active { + return false + } + ctx := tx.Event() + positions := append([]cube.Pos(nil), interior...) + if tx.World().Handler().HandlePortalActivate(ctx, world.End, positions); ctx.Cancelled() { + return false + } + for _, pos := range interior { + if tx.Block(pos) != ep { + tx.SetBlock(pos, ep, nil) + } + } + return true + } + return false +} + +// EndPortalRingIntact reports whether an intact twelve-frame ring still surrounds the end_portal block at the +// position passed, which may be any of the nine interior positions. +func EndPortalRingIntact(tx *world.Tx, portalPos cube.Pos) bool { + for dx := -1; dx <= 1; dx++ { + for dz := -1; dz <= 1; dz++ { + if _, ok := matchEndRing(tx, portalPos.Add(cube.Pos{dx, 0, dz})); ok { + return true + } + } + } + return false +} + +// DeactivateEndPortal clears every end_portal block reachable from the position passed through cardinal neighbours +// on the same y plane, removing a single portal's interior without touching unrelated portals. +func DeactivateEndPortal(tx *world.Tx, portalPos cube.Pos) { + ep := endPortal() + if tx.Block(portalPos) != ep { + return + } + var positions []cube.Pos + queue := []cube.Pos{portalPos} + seen := map[cube.Pos]struct{}{portalPos: {}} + for len(queue) > 0 { + p := queue[0] + queue = queue[1:] + if tx.Block(p) != ep { + continue + } + positions = append(positions, p) + for _, face := range cube.HorizontalFaces() { + n := p.Side(face) + if _, ok := seen[n]; ok { + continue + } + seen[n] = struct{}{} + queue = append(queue, n) + } + } + deactivate(tx, positions) +} + +// matchEndRing returns the 3x3 interior positions if the twelve canonical ring positions around centre all hold +// matching frames. +func matchEndRing(tx *world.Tx, center cube.Pos) ([]cube.Pos, bool) { + for _, want := range expectedEndRingFrames(center) { + b, ok := tx.Block(want.pos).(endFrameBlock) + if !ok { + return nil, false + } + eye, facing := b.EndPortalFrameState() + if !eye || facing != want.facing { + return nil, false + } + } + return endRingInterior(center), true +} + +// expectedEndRingFrames returns the twelve (position, facing) pairs a complete ring around the centre must have, with +// every frame facing toward the centre. +func expectedEndRingFrames(center cube.Pos) []endRingFrame { + frames := make([]endRingFrame, 0, 12) + for _, side := range cube.Directions() { + base := center.Side(side.Face()).Side(side.Face()) + tangent := side.RotateRight().Face() + inward := side.Opposite() + for _, pos := range []cube.Pos{base.Side(tangent.Opposite()), base, base.Side(tangent)} { + frames = append(frames, endRingFrame{pos: pos, facing: inward}) + } + } + return frames +} + +// endRingInterior returns the nine 3x3 interior positions on the y plane of centre. +func endRingInterior(center cube.Pos) []cube.Pos { + out := make([]cube.Pos, 0, 9) + for dx := -1; dx <= 1; dx++ { + for dz := -1; dz <= 1; dz++ { + out = append(out, center.Add(cube.Pos{dx, 0, dz})) + } + } + return out +} + +// endPortal returns the end_portal block. +func endPortal() world.Block { + p, ok := world.BlockByName("minecraft:end_portal", nil) + if !ok { + panic("could not find end_portal block") + } + return p +} diff --git a/server/world/portal/end_test.go b/server/world/portal/end_test.go new file mode 100644 index 0000000000..043d9bdff1 --- /dev/null +++ b/server/world/portal/end_test.go @@ -0,0 +1,225 @@ +package portal_test + +import ( + "testing" + + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/portal" +) + +// ringFrame is one (position, facing) pair on an end portal ring. +type ringFrame struct { + pos cube.Pos + facing cube.Direction +} + +// buildEndPortalRing places a complete twelve-frame end portal ring around center, with every frame holding an eye +// and facing the centre. +func buildEndPortalRing(tx *world.Tx, center cube.Pos) { + for _, fp := range endPortalRingFrames(center) { + tx.SetBlock(fp.pos, block.EndPortalFrame{Facing: fp.facing, Eye: true}, nil) + } +} + +// endRingOffsets is a hand-written oracle of the twelve (offset from centre, facing) pairs of a valid ring, +// independent of the production ring geometry. +var endRingOffsets = []ringFrame{ + {cube.Pos{-1, 0, -2}, cube.South}, {cube.Pos{0, 0, -2}, cube.South}, {cube.Pos{1, 0, -2}, cube.South}, + {cube.Pos{2, 0, -1}, cube.West}, {cube.Pos{2, 0, 0}, cube.West}, {cube.Pos{2, 0, 1}, cube.West}, + {cube.Pos{1, 0, 2}, cube.North}, {cube.Pos{0, 0, 2}, cube.North}, {cube.Pos{-1, 0, 2}, cube.North}, + {cube.Pos{-2, 0, 1}, cube.East}, {cube.Pos{-2, 0, 0}, cube.East}, {cube.Pos{-2, 0, -1}, cube.East}, +} + +// endPortalRingFrames returns the twelve (frame position, facing) pairs of a valid ring around center. +func endPortalRingFrames(center cube.Pos) []ringFrame { + frames := make([]ringFrame, len(endRingOffsets)) + for i, f := range endRingOffsets { + frames[i] = ringFrame{pos: center.Add(f.pos), facing: f.facing} + } + return frames +} + +// interiorPositions returns the 3x3 interior block positions around center. +func interiorPositions(center cube.Pos) []cube.Pos { + out := make([]cube.Pos, 0, 9) + for dx := -1; dx <= 1; dx++ { + for dz := -1; dz <= 1; dz++ { + out = append(out, center.Add(cube.Pos{dx, 0, dz})) + } + } + return out +} + +func TestActivateEndPortal(t *testing.T) { + w := world.New() + t.Cleanup(func() { _ = w.Close() }) + + center := cube.Pos{8, 10, 8} + mustDo(t, w, func(tx *world.Tx) { + buildEndPortalRing(tx, center) + + first := endPortalRingFrames(center)[0] + if !portal.ActivateEndPortal(tx, first.pos) { + t.Fatal("ActivateEndPortal() = false on a complete ring, want true") + } + for _, p := range interiorPositions(center) { + if _, ok := tx.Block(p).(block.EndPortal); !ok { + t.Fatalf("interior block at %v = %T, want block.EndPortal", p, tx.Block(p)) + } + } + }) +} + +func TestActivateEndPortalMissingEye(t *testing.T) { + w := world.New() + t.Cleanup(func() { _ = w.Close() }) + + center := cube.Pos{8, 10, 8} + mustDo(t, w, func(tx *world.Tx) { + buildEndPortalRing(tx, center) + + frames := endPortalRingFrames(center) + broken := frames[5] + tx.SetBlock(broken.pos, block.EndPortalFrame{Facing: broken.facing, Eye: false}, nil) + + if portal.ActivateEndPortal(tx, frames[0].pos) { + t.Fatal("ActivateEndPortal() = true with one missing eye, want false") + } + for _, p := range interiorPositions(center) { + if _, ok := tx.Block(p).(block.EndPortal); ok { + t.Fatalf("interior block at %v became EndPortal despite incomplete ring", p) + } + } + }) +} + +func TestActivateEndPortalWrongFacing(t *testing.T) { + w := world.New() + t.Cleanup(func() { _ = w.Close() }) + + center := cube.Pos{8, 10, 8} + mustDo(t, w, func(tx *world.Tx) { + buildEndPortalRing(tx, center) + + frames := endPortalRingFrames(center) + bad := frames[3] + tx.SetBlock(bad.pos, block.EndPortalFrame{Facing: bad.facing.Opposite(), Eye: true}, nil) + + if portal.ActivateEndPortal(tx, frames[0].pos) { + t.Fatal("ActivateEndPortal() = true with one mis-facing frame, want false") + } + }) +} + +func TestEnderEyeCompletesRing(t *testing.T) { + w := world.New() + t.Cleanup(func() { _ = w.Close() }) + + center := cube.Pos{8, 10, 8} + mustDo(t, w, func(tx *world.Tx) { + frames := endPortalRingFrames(center) + for i, fp := range frames { + tx.SetBlock(fp.pos, block.EndPortalFrame{Facing: fp.facing, Eye: i != 0}, nil) + } + + first := frames[0] + ctx := &item.UseContext{} + if !(item.EnderEye{}).UseOnBlock(first.pos, cube.FaceUp, cube.Pos{}.Vec3(), tx, nil, ctx) { + t.Fatal("EnderEye.UseOnBlock() = false on the last empty frame, want true") + } + if ctx.CountSub != 1 { + t.Fatalf("EnderEye.UseOnBlock() subtracted %d items, want 1", ctx.CountSub) + } + for _, p := range interiorPositions(center) { + if _, ok := tx.Block(p).(block.EndPortal); !ok { + t.Fatalf("interior block at %v = %T, want block.EndPortal", p, tx.Block(p)) + } + } + }) +} + +func TestEndPortalDespawnsOnFrameBreak(t *testing.T) { + w := world.New() + t.Cleanup(func() { _ = w.Close() }) + + center := cube.Pos{8, 10, 8} + mustDo(t, w, func(tx *world.Tx) { + buildEndPortalRing(tx, center) + first := endPortalRingFrames(center)[0] + if !portal.ActivateEndPortal(tx, first.pos) { + t.Fatal("ActivateEndPortal() = false on a complete ring, want true") + } + + // Break one frame: the portal blocks should despawn once the ring is no longer complete. + broken := endPortalRingFrames(center)[5] + tx.SetBlock(broken.pos, nil, nil) + + // Drive a neighbour update on an interior end_portal block, mimicking what the world does after the removal. + ep, ok := tx.Block(center).(block.EndPortal) + if !ok { + t.Fatalf("centre block = %T, want block.EndPortal", tx.Block(center)) + } + ep.NeighbourUpdateTick(center, broken.pos, tx) + + for _, p := range interiorPositions(center) { + if _, ok := tx.Block(p).(block.EndPortal); ok { + t.Fatalf("interior at %v still EndPortal after frame break", p) + } + } + }) +} + +func TestEndPortalKeptOnPortalBlockBreak(t *testing.T) { + w := world.New() + t.Cleanup(func() { _ = w.Close() }) + + center := cube.Pos{8, 10, 8} + mustDo(t, w, func(tx *world.Tx) { + buildEndPortalRing(tx, center) + if !portal.ActivateEndPortal(tx, endPortalRingFrames(center)[0].pos) { + t.Fatal("ActivateEndPortal() = false on a complete ring, want true") + } + + // Break the centre portal block: the ring is still complete, so the other portal blocks must stay. + tx.SetBlock(center, nil, nil) + neighbour := center.Add(cube.Pos{1, 0, 0}) + ep, ok := tx.Block(neighbour).(block.EndPortal) + if !ok { + t.Fatalf("neighbour block = %T, want block.EndPortal", tx.Block(neighbour)) + } + ep.NeighbourUpdateTick(neighbour, center, tx) + + for _, p := range interiorPositions(center) { + if p == center { + continue + } + if _, ok := tx.Block(p).(block.EndPortal); !ok { + t.Fatalf("interior at %v despawned after breaking a portal block", p) + } + } + }) +} + +func TestGenerateEndSpawnPlatformClearsThreeLayers(t *testing.T) { + w := world.New() + t.Cleanup(func() { _ = w.Close() }) + + mustDo(t, w, func(tx *world.Tx) { + for y := 49; y <= 52; y++ { + tx.SetBlock(cube.Pos{100, y, 0}, block.Obsidian{}, nil) + } + portal.GenerateEndSpawnPlatform(tx) + + for y := 49; y <= 51; y++ { + if _, ok := tx.Block(cube.Pos{100, y, 0}).(block.Air); !ok { + t.Fatalf("block at y=%d = %T, want block.Air", y, tx.Block(cube.Pos{100, y, 0})) + } + } + if _, ok := tx.Block(cube.Pos{100, 52, 0}).(block.Obsidian); !ok { + t.Fatalf("block at y=52 = %T, want block.Obsidian", tx.Block(cube.Pos{100, 52, 0})) + } + }) +} diff --git a/server/world/portal/nether.go b/server/world/portal/nether.go new file mode 100644 index 0000000000..9d1a7dfe10 --- /dev/null +++ b/server/world/portal/nether.go @@ -0,0 +1,335 @@ +package portal + +import ( + "math" + "math/rand/v2" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "github.com/df-mc/dragonfly/server/world" +) + +// Nether contains information about a nether portal structure. Values returned from this package are tied to the +// transaction that created them and must not be retained after that transaction finishes. +type Nether struct { + w, h int + framed bool + axis cube.Axis + tx *world.Tx + spawnPos cube.Pos + positions []cube.Pos +} + +const ( + minimumNetherPortalWidth, maximumNetherPortalWidth = 2, 21 + minimumNetherPortalHeight, maximumNetherPortalHeight = 3, 21 + + minimumNetherPortalArea = minimumNetherPortalWidth * minimumNetherPortalHeight +) + +// NetherPortalFromPos returns Nether portal information from a given position in the frame. +func NetherPortalFromPos(tx *world.Tx, pos cube.Pos) (Nether, bool) { + if tx.World().Dimension() == world.End { + return Nether{}, false + } + + axis, positions, width, height, completed, ok := multiAxisScan(pos, tx, matchesNetherPortalInterior) + if !ok { + axis, positions, width, height, completed, ok = multiAxisScan(pos, tx, matchesNetherPortal) + } + if !ok { + return Nether{}, false + } + return Nether{ + w: width, h: height, + spawnPos: pos, + positions: positions, + framed: completed, + axis: axis, + tx: tx, + }, ok +} + +// ActivateNetherPortal activates an inactive framed Nether portal at the position passed. +func ActivateNetherPortal(tx *world.Tx, pos cube.Pos) bool { + p, ok := NetherPortalFromPos(tx, pos) + if !ok || !p.Framed() || p.Activated() { + return false + } + ctx := tx.Event() + positions := append([]cube.Pos(nil), p.Positions()...) + if tx.World().Handler().HandlePortalActivate(ctx, world.Nether, positions); ctx.Cancelled() { + return false + } + p.Activate() + return true +} + +// DeactivateNetherPortal deactivates the connected Nether portal at the position passed. +func DeactivateNetherPortal(tx *world.Tx, pos cube.Pos) bool { + _, positions, ok := connectedNetherPortal(tx, pos) + if !ok { + return false + } + deactivate(tx, positions) + return true +} + +// FindOrCreateNetherPortal finds or creates a Nether portal at the given position. +func FindOrCreateNetherPortal(tx *world.Tx, pos cube.Pos, radius int) (Nether, bool) { + n, ok := FindNetherPortal(tx, pos, radius) + if ok { + return n, true + } + return CreateNetherPortal(tx, pos) +} + +// portalBlock represents a block that can be used as a portal to travel between dimensions. +type portalBlock interface { + // Portal returns the dimension that the portal leads to. + Portal() world.Dimension +} + +// frameBlock represents a block that can be used as a frame for a Nether portal. +type frameBlock interface { + // Frame returns true if the block is used as a frame for the given dimension. + Frame(dimension world.Dimension) bool +} + +// FindNetherPortal searches a provided radius for a Nether portal. +func FindNetherPortal(tx *world.Tx, pos cube.Pos, radius int) (Nether, bool) { + if tx.World().Dimension() == world.End { + return Nether{}, false + } + + closest, closestDist, found := Nether{}, math.MaxFloat64, false + seen := make(map[cube.Pos]struct{}) + for selectedPos := range tx.BlocksWithin(pos, radius, portal(cube.X), portal(cube.Z)) { + if _, ok := seen[selectedPos]; ok { + // Part of a portal that was already validated through an earlier block. + continue + } + if n, ok := NetherPortalFromPos(tx, selectedPos); ok && n.Framed() && n.Activated() { + for _, p := range n.Positions() { + seen[p] = struct{}{} + if dist := p.Vec3().Sub(pos.Vec3()).Len(); dist < closestDist { + closestDist, closest, found = dist, n, true + } + } + } + } + return closest, found +} + +// CreateNetherPortal creates a Nether portal at the given position. +func CreateNetherPortal(tx *world.Tx, pos cube.Pos) (Nether, bool) { + if tx.World().Dimension() == world.End { + return Nether{}, false + } + + resultPos, random, distance, a, r := pos, rand.IntN(4), -1.0, 0, tx.Range() + searchValidArea := func(directions int, valid func(pos cube.Pos, riv int, coEff1, coEff2 int) bool) { + for tempX := pos.X() - 16; tempX <= pos.X()+16; tempX++ { + offsetX := float64(tempX-pos.X()) + 0.5 + for tempZ := pos.Z() - 16; tempZ <= pos.Z()+16; tempZ++ { + offsetZ := float64(tempZ-pos.Z()) + 0.5 + for tempY := r.Max() - 1; tempY >= r.Min(); tempY-- { + entryPos := cube.Pos{tempX, tempY, tempZ} + if tx.Block(entryPos) != air() { + continue + } + + for tempY > r.Min() && tx.Block(entryPos.Side(cube.FaceDown)) == air() { + tempY-- + entryPos[1]-- + } + + for riv := random; riv < random+directions; riv++ { + coEff1 := riv % 2 + coEff2 := 1 - coEff1 + + if !valid(entryPos, riv, coEff1, coEff2) { + break + } + + offsetY := float64(tempY-pos.Y()) + 0.5 + newDist := offsetX*offsetX + offsetY*offsetY + offsetZ*offsetZ + if distance < 0.0 || newDist < distance { + distance = newDist + a = riv % directions + resultPos = cube.Pos{tempX, tempY, tempZ} + } + } + } + } + } + } + + // Search for a valid area in all four directions, adding some extra space for comfort. + searchValidArea(4, func(pos cube.Pos, riv int, coEff1, coEff2 int) bool { + if riv%4 >= 2 { + coEff1 = -coEff1 + coEff2 = -coEff2 + } + + for safeSpace1 := range 3 { + for safeSpace2 := -1; safeSpace2 < 3; safeSpace2++ { + for height := -1; height < 4; height++ { + b := tx.Block(cube.Pos{ + pos.X() + safeSpace2*coEff1 + safeSpace1*coEff2, + pos.Y() + height, + pos.Z() + safeSpace2*coEff2 - safeSpace1*coEff1, + }) + _, solid := b.Model().(model.Solid) + if height < 0 && !solid || height >= 0 && b != air() { + return false + } + } + } + } + return true + }) + + if distance < 0.0 { + // If we couldn't find a valid area under those specifications, we can search the two main directions instead, + // reducing comfort but at least allowing us to have a portal in the area. + searchValidArea(2, func(pos cube.Pos, riv int, coEff1, coEff2 int) bool { + for safeSpace := range 3 { + for height := -1; height < 4; height++ { + b := tx.Block(cube.Pos{ + pos.X() + safeSpace*coEff1, + pos.Y() + height, + pos.Z() + safeSpace*coEff2, + }) + _, solid := b.Model().(model.Solid) + if height < 0 && !solid || height >= 0 && b != air() { + return false + } + } + } + return true + }) + } + + coEff1 := a % 2 + coEff2 := 1 - coEff1 + if a%4 >= 2 { + coEff1 = -coEff1 + coEff2 = -coEff2 + } + + axis := cube.X + if coEff1 == 0 { + axis = cube.Z + } + + // Buffer the blocks the portal is made up of so a world.Handler may cancel the creation before any of them are + // written to the world. affected holds each position exactly once, in the order it was first set. + ob, pb := obsidian(), portal(axis) + blocks := make(map[cube.Pos]world.Block) + var affected []cube.Pos + setBlock := func(pos cube.Pos, b world.Block) { + if _, ok := blocks[pos]; !ok { + affected = append(affected, pos) + } + blocks[pos] = b + } + + if distance < 0.0 { + // If all else fails, we can simply create a floating platform in the void with the portal on it. + resultPos[1] = min(max(resultPos[1], 70), r.Max()-10) + for safeBeforeAfter := -1; safeBeforeAfter <= 1; safeBeforeAfter++ { + for safeWidth := range 2 { + for height := -1; height < 3; height++ { + entryPos := cube.Pos{ + resultPos.X() + safeWidth*coEff1 + safeBeforeAfter*coEff2, + resultPos.Y() + height, + resultPos.Z() + safeWidth*coEff2 - safeBeforeAfter*coEff1, + } + + if height < 0 { + setBlock(entryPos, ob) + } else { + setBlock(entryPos, nil) + } + } + } + } + } + + // Build the portal frame and activate it. + var positions []cube.Pos + for width := -1; width < 3; width++ { + for height := -1; height < 4; height++ { + entryPos := cube.Pos{ + resultPos.X() + width*coEff1, + resultPos.Y() + height, + resultPos.Z() + width*coEff2, + } + + if width == -1 || width == 2 || height == -1 || height == 3 { + setBlock(entryPos, ob) + continue + } + positions = append(positions, entryPos) + setBlock(entryPos, pb) + } + } + + ctx := tx.Event() + handlerPositions := append([]cube.Pos(nil), affected...) + if tx.World().Handler().HandlePortalCreate(ctx, world.Nether, handlerPositions); ctx.Cancelled() { + return Nether{}, false + } + for _, pos := range affected { + tx.SetBlock(pos, blocks[pos], nil) + } + + return Nether{ + w: minimumNetherPortalWidth, + h: minimumNetherPortalHeight, + framed: true, + spawnPos: resultPos, + positions: positions, + axis: axis, + tx: tx, + }, true +} + +// Activate ... +func (n Nether) Activate() { + for _, pos := range n.Positions() { + n.tx.SetBlock(pos, portal(n.axis), nil) + } +} + +func deactivate(tx *world.Tx, positions []cube.Pos) { + for _, pos := range positions { + tx.SetBlock(pos, nil, nil) + } +} + +// Framed ... +func (n Nether) Framed() bool { + return n.framed +} + +// Activated ... +func (n Nether) Activated() bool { + for _, pos := range n.Positions() { + if n.tx.Block(pos) != portal(n.axis) { + return false + } + } + return true +} + +// Spawn ... +func (n Nether) Spawn() cube.Pos { + return n.spawnPos +} + +// Positions ... +func (n Nether) Positions() []cube.Pos { + return n.positions +} diff --git a/server/world/portal/nether_test.go b/server/world/portal/nether_test.go new file mode 100644 index 0000000000..fd61f9ddf8 --- /dev/null +++ b/server/world/portal/nether_test.go @@ -0,0 +1,245 @@ +package portal_test + +import ( + "testing" + + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/portal" +) + +func TestNetherPortalFromPos(t *testing.T) { + tests := []struct { + name string + build func(tx *world.Tx, origin cube.Pos) + pos cube.Pos + ok bool + }{ + { + name: "valid vertical frame", + build: func(tx *world.Tx, origin cube.Pos) { + buildVerticalFrame(tx, origin, cube.Z, 2, 3) + }, + pos: cube.Pos{}, + ok: true, + }, + { + name: "valid X axis frame", + build: func(tx *world.Tx, origin cube.Pos) { + buildVerticalFrame(tx, origin, cube.X, 2, 3) + }, + pos: cube.Pos{}, + ok: true, + }, + { + name: "maximum size frame", + build: func(tx *world.Tx, origin cube.Pos) { + buildVerticalFrame(tx, origin, cube.Z, 21, 21) + }, + pos: cube.Pos{}, + ok: true, + }, + { + name: "too wide frame", + build: func(tx *world.Tx, origin cube.Pos) { + buildVerticalFrame(tx, origin, cube.Z, 22, 3) + }, + pos: cube.Pos{}, + }, + { + name: "too tall frame", + build: func(tx *world.Tx, origin cube.Pos) { + buildVerticalFrame(tx, origin, cube.Z, 2, 22) + }, + pos: cube.Pos{}, + }, + { + name: "horizontal frame", + build: func(tx *world.Tx, origin cube.Pos) { + buildHorizontalFrame(tx, origin) + }, + pos: cube.Pos{1, 0, 1}, + }, + { + name: "crying obsidian does not complete frame", + build: func(tx *world.Tx, origin cube.Pos) { + buildVerticalFrame(tx, origin, cube.Z, 2, 3) + tx.SetBlock(origin.Side(cube.FaceNorth), block.Obsidian{Crying: true}, nil) + }, + pos: cube.Pos{}, + }, + { + name: "missing side frame does not complete frame", + build: func(tx *world.Tx, origin cube.Pos) { + buildVerticalFrame(tx, origin, cube.Z, 2, 3) + tx.SetBlock(origin.Side(cube.FaceNorth), nil, nil) + }, + pos: cube.Pos{}, + }, + { + name: "soul fire does not activate frame", + build: func(tx *world.Tx, origin cube.Pos) { + buildVerticalFrame(tx, origin, cube.Z, 2, 3) + tx.SetBlock(origin, block.Fire{Type: block.SoulFire()}, nil) + }, + pos: cube.Pos{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := world.Config{Synchronous: true}.New() + t.Cleanup(func() { _ = w.Close() }) + + origin := cube.Pos{8, 10, 8} + mustDo(t, w, func(tx *world.Tx) { + tt.build(tx, origin) + p, ok := portal.NetherPortalFromPos(tx, origin.Add(tt.pos)) + if ok != tt.ok { + t.Fatalf("NetherPortalFromPos() ok = %v, want %v, portal = %#v", ok, tt.ok, p) + } + if ok && !p.Framed() { + t.Fatal("NetherPortalFromPos() returned an unframed portal") + } + }) + }) + } +} + +func TestPortalModelHasNoCollisionBBox(t *testing.T) { + for _, axis := range []cube.Axis{cube.X, cube.Z} { + if boxes := (model.Portal{Axis: axis}).BBox(cube.Pos{}, nil); len(boxes) != 0 { + t.Fatalf("BBox() returned %d boxes, want 0", len(boxes)) + } + } +} + +func TestActivateNetherPortal(t *testing.T) { + for _, axis := range []cube.Axis{cube.Z, cube.X} { + t.Run(axis.String(), func(t *testing.T) { + w := world.Config{Synchronous: true}.New() + t.Cleanup(func() { _ = w.Close() }) + + origin := cube.Pos{8, 10, 8} + mustDo(t, w, func(tx *world.Tx) { + buildVerticalFrame(tx, origin, axis, 2, 3) + if !portal.ActivateNetherPortal(tx, origin) { + t.Fatal("ActivateNetherPortal() = false, want true") + } + for x := range 2 { + for y := range 3 { + pos := origin.Add(widthOffset(axis, x)).Add(cube.Pos{0, y}) + pb, ok := tx.Block(pos).(block.Portal) + if !ok { + t.Fatalf("portal block not placed at interior offset %d,%d", x, y) + } + if pb.Axis != axis { + t.Fatalf("portal block at interior offset %d,%d has axis %v, want %v", x, y, pb.Axis, axis) + } + } + } + }) + }) + } +} + +func TestFireChargeActivatesNetherPortal(t *testing.T) { + w := world.Config{Synchronous: true}.New() + t.Cleanup(func() { _ = w.Close() }) + + origin := cube.Pos{8, 10, 8} + mustDo(t, w, func(tx *world.Tx) { + buildVerticalFrame(tx, origin, cube.Z, 2, 3) + ctx := &item.UseContext{} + if ok := (item.FireCharge{}).UseOnBlock(origin.Side(cube.FaceDown), cube.FaceUp, cube.Pos{}.Vec3(), tx, nil, ctx); !ok { + t.Fatal("FireCharge.UseOnBlock() = false, want true") + } + if ctx.CountSub != 1 { + t.Fatalf("FireCharge.UseOnBlock() subtracted %d items, want 1", ctx.CountSub) + } + if _, ok := tx.Block(origin).(block.Portal); !ok { + t.Fatal("FireCharge.UseOnBlock() did not activate portal") + } + }) +} + +func TestActivatedPortalCleanupOnBrokenFrame(t *testing.T) { + w := world.Config{Synchronous: true}.New() + t.Cleanup(func() { _ = w.Close() }) + + origin := cube.Pos{8, 10, 8} + mustDo(t, w, func(tx *world.Tx) { + buildVerticalFrame(tx, origin, cube.Z, 2, 3) + if !portal.ActivateNetherPortal(tx, origin) { + t.Fatal("ActivateNetherPortal() = false, want true") + } + + broken := origin.Add(widthOffset(cube.Z, 2)).Add(cube.Pos{0, 1}) + tx.SetBlock(broken, nil, nil) + + updated := origin.Add(widthOffset(cube.Z, 1)).Add(cube.Pos{0, 1}) + pb, ok := tx.Block(updated).(block.Portal) + if !ok { + t.Fatalf("block at updated position = %T, want block.Portal", tx.Block(updated)) + } + pb.NeighbourUpdateTick(updated, broken, tx) + + var remaining []cube.Pos + for x := range 2 { + for y := range 3 { + p := origin.Add(widthOffset(cube.Z, x)).Add(cube.Pos{0, y}) + if _, ok := tx.Block(p).(block.Portal); ok { + remaining = append(remaining, p) + } + } + } + if len(remaining) != 0 { + t.Fatalf("after frame break: %d orphan portal blocks remain at %v", len(remaining), remaining) + } + }) +} + +func buildVerticalFrame(tx *world.Tx, origin cube.Pos, axis cube.Axis, width, height int) { + for x := 0; x < width; x++ { + p := origin.Add(widthOffset(axis, x)) + tx.SetBlock(p.Side(cube.FaceDown), block.Obsidian{}, nil) + tx.SetBlock(p.Add(cube.Pos{0, height}), block.Obsidian{}, nil) + } + negative := cube.FaceNorth + if axis == cube.X { + negative = cube.FaceWest + } + for y := 0; y < height; y++ { + p := origin.Add(cube.Pos{0, y}) + tx.SetBlock(p.Side(negative), block.Obsidian{}, nil) + tx.SetBlock(p.Add(widthOffset(axis, width)), block.Obsidian{}, nil) + } +} + +func buildHorizontalFrame(tx *world.Tx, origin cube.Pos) { + for x := 0; x < 3; x++ { + for z := 0; z < 3; z++ { + if x == 1 && z == 1 { + continue + } + tx.SetBlock(origin.Add(cube.Pos{x, 0, z}), block.Obsidian{}, nil) + } + } +} + +func widthOffset(axis cube.Axis, width int) cube.Pos { + if axis == cube.X { + return cube.Pos{width, 0, 0} + } + return cube.Pos{0, 0, width} +} + +func mustDo(t *testing.T, w *world.World, f func(tx *world.Tx)) { + t.Helper() + if err := w.Do(f).Err(); err != nil { + t.Fatalf("world task failed: %v", err) + } +} diff --git a/server/world/portal/scan.go b/server/world/portal/scan.go new file mode 100644 index 0000000000..65d62b44ab --- /dev/null +++ b/server/world/portal/scan.go @@ -0,0 +1,192 @@ +package portal + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "github.com/df-mc/dragonfly/server/world" +) + +// blockMatcher reports whether a block belongs to a portal interior on the given axis. +type blockMatcher func(world.Block, cube.Axis) bool + +// multiAxisScan performs a scan on the Z and X axis, favouring the Z axis unless only the X axis reaches the minimum +// area. The last return value reports whether a portal-like interior was found; use Framed to check completion. +func multiAxisScan(framePos cube.Pos, tx *world.Tx, matches blockMatcher) (cube.Axis, []cube.Pos, int, int, bool, bool) { + zPositions, zWidth, zHeight, zCompleted := scan(cube.Z, framePos, tx, matches) + xPositions, xWidth, xHeight, xCompleted := scan(cube.X, framePos, tx, matches) + if len(zPositions) < minimumNetherPortalArea && len(xPositions) >= minimumNetherPortalArea { + return cube.X, xPositions, xWidth, xHeight, xCompleted, len(xPositions) > 0 + } + return cube.Z, zPositions, zWidth, zHeight, zCompleted, len(zPositions) > 0 +} + +// scan validates a vertical rectangular portal interior on the given horizontal axis. +func scan(axis cube.Axis, pos cube.Pos, tx *world.Tx, matches blockMatcher) ([]cube.Pos, int, int, bool) { + // Return if the starting block isn't part of a portal interior. + if !matches(tx.Block(pos), axis) { + return nil, 0, 0, false + } + negative, positive := axis.Faces() + + // Walk down then towards the negative face to land on the bottom-left interior corner. + origin := pos + for down, next := 0, origin.Side(cube.FaceDown); matches(tx.Block(next), axis); down, next = down+1, origin.Side(cube.FaceDown) { + if down >= maximumNetherPortalHeight { + return nil, 0, 0, false + } + origin = next + } + for left, next := 0, origin.Side(negative); matches(tx.Block(next), axis); left, next = left+1, origin.Side(negative) { + if left >= maximumNetherPortalWidth { + return nil, 0, 0, false + } + origin = next + } + + // Measure the bottom row and the leftmost column from the origin. + width := 0 + for p := origin; matches(tx.Block(p), axis); p = p.Side(positive) { + width++ + if width > maximumNetherPortalWidth { + return nil, 0, 0, false + } + } + height := 0 + for p := origin; matches(tx.Block(p), axis); p = p.Side(cube.FaceUp) { + height++ + if height > maximumNetherPortalHeight { + return nil, 0, 0, false + } + } + // Reject anything smaller than the minimum frame size. + if width < minimumNetherPortalWidth || height < minimumNetherPortalHeight { + return nil, width, height, false + } + + // Validate each row: side frames intact and every interior block matches. + positions := make([]cube.Pos, 0, width*height) + for y := 0; y < height; y++ { + row := origin.Add(cube.Pos{0, y}) + if !isFrame(tx.Block(row.Side(negative))) || !isFrame(tx.Block(row.Add(widthOffset(axis, width)))) { + return nil, width, height, false + } + for x := 0; x < width; x++ { + p := row.Add(widthOffset(axis, x)) + if !matches(tx.Block(p), axis) { + return nil, width, height, false + } + positions = append(positions, p) + } + } + // Validate the top and bottom frames over each column. + for x := 0; x < width; x++ { + p := origin.Add(widthOffset(axis, x)) + if !isFrame(tx.Block(p.Side(cube.FaceDown))) || !isFrame(tx.Block(p.Add(cube.Pos{0, height}))) { + return nil, width, height, false + } + } + return positions, width, height, true +} + +// connectedNetherPortal flood-fills the region of portal blocks reachable from pos and returns its axis and positions. +// Used to clean up an entire portal when its frame breaks, where scan would only return a partial rectangle. +func connectedNetherPortal(tx *world.Tx, pos cube.Pos) (cube.Axis, []cube.Pos, bool) { + for _, axis := range []cube.Axis{cube.Z, cube.X} { + if !matchesNetherPortal(tx.Block(pos), axis) { + continue + } + positions := connectedPortalBlocks(tx, pos, axis) + return axis, positions, len(positions) > 0 + } + return 0, nil, false +} + +// connectedPortalBlocks returns every portal block of the given axis reachable from pos via face neighbours. +func connectedPortalBlocks(tx *world.Tx, pos cube.Pos, axis cube.Axis) []cube.Pos { + var positions []cube.Pos + queue := []cube.Pos{pos} + seen := map[cube.Pos]struct{}{pos: {}} + faces := portalFaces(axis) + for len(queue) > 0 { + p := queue[0] + queue = queue[1:] + if !matchesNetherPortal(tx.Block(p), axis) { + continue + } + positions = append(positions, p) + for _, face := range faces { + next := p.Side(face) + if _, ok := seen[next]; ok { + continue + } + seen[next] = struct{}{} + queue = append(queue, next) + } + } + return positions +} + +// portalFaces returns the four neighbouring faces used to flood-fill a portal of the given horizontal axis. +func portalFaces(axis cube.Axis) []cube.Face { + negative, positive := axis.Faces() + return []cube.Face{cube.FaceDown, cube.FaceUp, negative, positive} +} + +// widthOffset returns the position offset for moving by the given number of blocks along the portal's width axis. +func widthOffset(axis cube.Axis, offset int) cube.Pos { + if axis == cube.X { + return cube.Pos{offset, 0, 0} + } + return cube.Pos{0, 0, offset} +} + +// isFrame reports whether the block can act as a Nether portal frame block. +func isFrame(b world.Block) bool { + f, ok := b.(frameBlock) + return ok && f.Frame(world.Nether) +} + +// matchesNetherPortalInterior reports whether the block may sit inside an unactivated Nether portal frame. +func matchesNetherPortalInterior(b world.Block, _ cube.Axis) bool { + i, ok := b.(interface { + PortalInterior(target world.Dimension) bool + }) + return ok && i.PortalInterior(world.Nether) +} + +// matchesNetherPortal reports whether the block is an active Nether portal block aligned with the given axis. +func matchesNetherPortal(b world.Block, axis cube.Axis) bool { + p, ok := b.(portalBlock) + if !ok || p.Portal() != world.Nether { + return false + } + m, ok := b.Model().(model.Portal) + return ok && m.Axis == axis +} + +// air returns an air block. +func air() world.Block { + a, ok := world.BlockByName("minecraft:air", nil) + if !ok { + panic("could not find air block") + } + return a +} + +// portal returns a portal block. +func portal(axis cube.Axis) world.Block { + p, ok := world.BlockByName("minecraft:portal", map[string]any{"portal_axis": axis.String()}) + if !ok { + panic("could not find portal block") + } + return p +} + +// obsidian returns an obsidian block. +func obsidian() world.Block { + o, ok := world.BlockByName("minecraft:obsidian", nil) + if !ok { + panic("could not find obsidian block") + } + return o +} diff --git a/server/world/provider.go b/server/world/provider.go index 713c38bfb1..f54cf0f59d 100644 --- a/server/world/provider.go +++ b/server/world/provider.go @@ -1,6 +1,8 @@ package world import ( + "sync" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world/chunk" "github.com/df-mc/goleveldb/leveldb" @@ -58,3 +60,52 @@ func (NopProvider) LoadPlayerSpawnPosition(uuid.UUID) (cube.Pos, bool, error) { } func (NopProvider) SavePlayerSpawnPosition(uuid.UUID, cube.Pos) error { return nil } func (NopProvider) Close() error { return nil } + +// lockedProvider wraps a Provider, serialising all calls for providers that +// are not safe for concurrent use. +type lockedProvider struct { + mu sync.Mutex + p Provider +} + +func (l *lockedProvider) Settings() *Settings { + l.mu.Lock() + defer l.mu.Unlock() + return l.p.Settings() +} + +func (l *lockedProvider) SaveSettings(s *Settings) { + l.mu.Lock() + defer l.mu.Unlock() + l.p.SaveSettings(s) +} + +func (l *lockedProvider) LoadPlayerSpawnPosition(id uuid.UUID) (cube.Pos, bool, error) { + l.mu.Lock() + defer l.mu.Unlock() + return l.p.LoadPlayerSpawnPosition(id) +} + +func (l *lockedProvider) SavePlayerSpawnPosition(id uuid.UUID, pos cube.Pos) error { + l.mu.Lock() + defer l.mu.Unlock() + return l.p.SavePlayerSpawnPosition(id, pos) +} + +func (l *lockedProvider) LoadColumn(pos ChunkPos, dim Dimension) (*chunk.Column, error) { + l.mu.Lock() + defer l.mu.Unlock() + return l.p.LoadColumn(pos, dim) +} + +func (l *lockedProvider) StoreColumn(pos ChunkPos, dim Dimension, col *chunk.Column) error { + l.mu.Lock() + defer l.mu.Unlock() + return l.p.StoreColumn(pos, dim, col) +} + +func (l *lockedProvider) Close() error { + l.mu.Lock() + defer l.mu.Unlock() + return l.p.Close() +} diff --git a/server/world/redstone.go b/server/world/redstone.go new file mode 100644 index 0000000000..df1de21313 --- /dev/null +++ b/server/world/redstone.go @@ -0,0 +1,1214 @@ +package world + +import ( + "maps" + "slices" + + "github.com/df-mc/dragonfly/server/block/cube" +) + +// RedstoneUpdateCause describes the world event that caused a redstone update to be evaluated. +type RedstoneUpdateCause uint8 + +const ( + // RedstoneUpdateCauseBlockUpdate means a block or liquid change invalidated nearby redstone. + RedstoneUpdateCauseBlockUpdate RedstoneUpdateCause = iota + // RedstoneUpdateCauseScheduledTick means a scheduled redstone tick invalidated a component. + RedstoneUpdateCauseScheduledTick + // RedstoneUpdateCauseCompilerRebuild means a redstone compiler rebuild invalidated a component. + RedstoneUpdateCauseCompilerRebuild +) + +// RedstoneUpdate represents a redstone state transition proposed by the world redstone engine. Handlers may cancel +// the event to suppress the proposed mutation and any propagation from that mutation. +type RedstoneUpdate struct { + // Pos is the block position that will receive the update. + Pos cube.Pos + // ChangedNeighbour is the neighbouring block that caused the update, if any. + ChangedNeighbour cube.Pos + // HasChangedNeighbour reports whether ChangedNeighbour is set. A zero block position is valid, so callers must not + // use ChangedNeighbour == cube.Pos{} as an absence check. + HasChangedNeighbour bool + // ChangedRedstoneRelevant reports whether ChangedNeighbour was a redstone component before or after the change. + ChangedRedstoneRelevant bool + // Source is the original block position that caused this redstone propagation, if known. + Source cube.Pos + // HasSource reports whether Source is set. + HasSource bool + // Before is the block currently at Pos. + Before Block + // After is the block that will replace Before, if the update is a block-state update. After is nil for updates + // that perform side effects instead of replacing the block. + After Block + // OldPower is the last redstone power observed by the engine at Pos. + OldPower int + // NewPower is the redstone power observed by the engine at Pos for this update. + NewPower int + // CurrentTick is the world tick during which the update was evaluated. + CurrentTick int64 + // Cause identifies why the update was evaluated. + Cause RedstoneUpdateCause +} + +// RedstonePowerSource is implemented by blocks that emit redstone power. The face passed is the face of the source +// block that power is being read from. +type RedstonePowerSource interface { + RedstonePower(pos cube.Pos, tx *Tx, face cube.Face) int +} + +// RedstoneStrongPowerSource is implemented by sources that strongly power blocks from specific faces. Strong power may +// pass through solid blocks, unlike weak redstone wire power. +type RedstoneStrongPowerSource interface { + RedstoneStrongPower(pos cube.Pos, tx *Tx, face cube.Face) int +} + +// RedstoneWeakBlockPowerer may be implemented by sources whose weak output can make an adjacent conductive block +// weakly powered. Weakly powered blocks activate adjacent mechanisms, but do not power adjacent redstone dust. +type RedstoneWeakBlockPowerer interface { + RedstoneWeaklyPowersBlocks() bool +} + +// RedstonePowerRelayer is implemented by redstone wire-like blocks that relay power through a compiled redstone +// network. The returned value is the signal loss when power crosses one relayer edge. +type RedstonePowerRelayer interface { + RedstoneSignalLoss(pos cube.Pos, tx *Tx) int +} + +// RedstonePowerRelayerNeighbourer may be implemented by relayers with non-adjacent connections, such as redstone +// wire stepping up and down block edges. +type RedstonePowerRelayerNeighbourer interface { + RedstoneRelayerNeighbours(pos cube.Pos, tx *Tx) []cube.Pos +} + +// RedstonePowerConsumer is implemented by blocks whose block state changes when their input power changes. The +// returned block is written to the world if changed is true and the redstone update event is not cancelled. +type RedstonePowerConsumer interface { + RedstonePowerUpdate(pos cube.Pos, tx *Tx, power int) (after Block, changed bool) +} + +// RedstonePowerPostUpdater may be implemented by consumers that need to apply side effects after an uncancelled +// redstone state update, such as syncing the other half of a door. +type RedstonePowerPostUpdater interface { + RedstonePowerPostUpdate(pos cube.Pos, tx *Tx, before, after Block, oldPower, newPower int) +} + +// RedstonePowerAction is implemented by blocks that perform a side effect when their input power changes, such as TNT +// priming on a rising edge. The action is run only if the redstone update event is not cancelled. +type RedstonePowerAction interface { + RedstonePowerAction(pos cube.Pos, tx *Tx, oldPower, newPower int) +} + +// RedstonePowerContextAction may be implemented by action blocks that need the proposed update metadata to distinguish +// self-caused redstone changes from external block updates. +type RedstonePowerContextAction interface { + RedstonePowerActionUpdate(pos cube.Pos, tx *Tx, update RedstoneUpdate) +} + +// RedstoneNonConductive may be implemented by solid redstone blocks that should not conduct strong power. +type RedstoneNonConductive interface { + RedstoneNonConductive() +} + +// redstoneEngine evaluates the immediate-power network for a world and caches transient power state. Neighbour updates +// and scheduled block ticks remain the semantic model for block behaviour; the engine just resolves wire/relayer power +// and lets consumers and actions schedule any delayed or directional changes themselves. +type redstoneEngine struct { + currentTick int64 + dirty map[cube.Pos]redstoneDirty + power map[cube.Pos]int + output map[cube.Pos]int + evaluating map[cube.Pos]struct{} + suppressedSources map[cube.Pos]int + torchBurnout map[cube.Pos]redstoneTorchBurnout +} + +// redstoneDirty records why a position needs redstone evaluation. +type redstoneDirty struct { + changed cube.Pos + hasChanged bool + changedRedstoneRelevant bool + source cube.Pos + hasSource bool + cause RedstoneUpdateCause +} + +// redstoneTorchBurnout tracks a torch's recent turn-off history and whether it is currently burned out. +type redstoneTorchBurnout struct { + offTicks []int64 + burnedOut bool + pendingSelfTriggered bool +} + +const ( + redstoneTorchBurnoutThreshold = 8 + redstoneTorchBurnoutWindowTicks = 60 +) + +// redstoneGraph is the immediate-power network compiled for one engine tick. Edges are signal loss between relayers +// only — never delay, direction locking, or mechanical state — so delayed or directional components stay as graph +// endpoints and drive their own state changes via scheduled ticks or actions. +type redstoneGraph struct { + nodes []redstoneNode + edges []redstoneEdge +} + +// redstoneNode represents one redstone-relevant block in a compiled graph. +type redstoneNode struct { + pos cube.Pos + source bool + sink bool +} + +// redstoneEdge connects two graph nodes with a signal-loss weight. +type redstoneEdge struct { + from, to int + weight int +} + +// newRedstoneEngine creates a redstone engine initialized at tick. +func newRedstoneEngine(tick int64) *redstoneEngine { + return &redstoneEngine{ + currentTick: tick, + dirty: make(map[cube.Pos]redstoneDirty), + power: make(map[cube.Pos]int), + output: make(map[cube.Pos]int), + evaluating: make(map[cube.Pos]struct{}), + } +} + +// invalidateAround marks pos and its direct neighbours dirty for redstone evaluation. +func (e *redstoneEngine) invalidateAround(pos, changed cube.Pos, cause RedstoneUpdateCause, r cube.Range) { + e.invalidateAroundWith(pos, redstoneDirty{changed: changed, hasChanged: true, source: changed, hasSource: true, cause: cause}, r) +} + +// invalidateAroundBlockChange marks pos and its direct neighbours dirty for a block change. +func (e *redstoneEngine) invalidateAroundBlockChange(pos cube.Pos, before, after Block, cause RedstoneUpdateCause, r cube.Range) { + d := redstoneDirty{ + changed: pos, + hasChanged: true, + changedRedstoneRelevant: isRedstoneRelevant(before) || isRedstoneRelevant(after), + source: pos, + hasSource: true, + cause: cause, + } + e.invalidateAroundWith(pos, d, r) +} + +// invalidateAroundWith marks pos and its direct neighbours dirty using the same update context. +func (e *redstoneEngine) invalidateAroundWith(pos cube.Pos, d redstoneDirty, r cube.Range) { + if e == nil || pos.OutOfBounds(r) { + return + } + e.invalidate(pos, d, r) + pos.Neighbours(func(neighbour cube.Pos) { + e.invalidate(neighbour, d, r) + }, r) +} + +// invalidate marks a single in-range position dirty for redstone evaluation. +func (e *redstoneEngine) invalidate(pos cube.Pos, d redstoneDirty, r cube.Range) { + if pos.OutOfBounds(r) { + return + } + if existing, ok := e.dirty[pos]; ok { + e.dirty[pos] = mergeRedstoneDirty(existing, d) + return + } + e.dirty[pos] = d +} + +// mergeRedstoneDirty keeps the strongest cause when multiple invalidations touch the same position before evaluation. +func mergeRedstoneDirty(a, b redstoneDirty) redstoneDirty { + if redstoneDirtyPriority(b) >= redstoneDirtyPriority(a) { + b.changedRedstoneRelevant = a.changedRedstoneRelevant || b.changedRedstoneRelevant + return b + } + a.changedRedstoneRelevant = a.changedRedstoneRelevant || b.changedRedstoneRelevant + return a +} + +func redstoneDirtyPriority(d redstoneDirty) int { + switch d.cause { + case RedstoneUpdateCauseBlockUpdate: + return 3 + case RedstoneUpdateCauseScheduledTick: + return 2 + case RedstoneUpdateCauseCompilerRebuild: + return 1 + default: + return 0 + } +} + +// removeChunk drops transient redstone state tied to an unloaded chunk. +func (e *redstoneEngine) removeChunk(chunkPos ChunkPos) { + if e == nil { + return + } + maps.DeleteFunc(e.dirty, func(pos cube.Pos, dirty redstoneDirty) bool { + return chunkPosFromBlockPos(pos) == chunkPos || + (dirty.hasChanged && chunkPosFromBlockPos(dirty.changed) == chunkPos) + }) + maps.DeleteFunc(e.power, func(pos cube.Pos, _ int) bool { + return chunkPosFromBlockPos(pos) == chunkPos + }) + maps.DeleteFunc(e.output, func(pos cube.Pos, _ int) bool { + return chunkPosFromBlockPos(pos) == chunkPos + }) + maps.DeleteFunc(e.torchBurnout, func(pos cube.Pos, _ redstoneTorchBurnout) bool { + return chunkPosFromBlockPos(pos) == chunkPos + }) +} + +// forget clears cached redstone input and output power for pos. +func (e *redstoneEngine) forget(pos cube.Pos) { + if e == nil { + return + } + delete(e.power, pos) + delete(e.output, pos) + delete(e.evaluating, pos) +} + +// tick evaluates all dirty redstone positions for the current world tick. +func (e *redstoneEngine) tick(tx *Tx, tick int64) { + if e == nil || len(e.dirty) == 0 { + return + } + e.currentTick = tick + dirty := maps.Clone(e.dirty) + clear(e.dirty) + + candidates := slices.Collect(maps.Keys(dirty)) + slices.SortFunc(candidates, compareBlockPos) + + graph := e.compile(tx, candidates) + cancelledSources, checkedSources := e.updateGraphSources(tx, graph, dirty) + previousSuppressed := e.suppressedSources + e.suppressedSources = cancelledSources + defer func() { + e.suppressedSources = previousSuppressed + }() + + powers := e.graphPower(tx, graph) + for i, node := range graph.nodes { + d := redstoneDirtyContext(dirty, node.pos) + if node.sink { + e.update(tx, node.pos, d, powers[i]) + } + } + for _, node := range graph.nodes { + if _, ok := checkedSources[node.pos]; ok { + continue + } + d := redstoneDirtyContext(dirty, node.pos) + if node.source { + e.updateSource(tx, node.pos, d) + } + } +} + +// redstoneDirtyContext returns the direct dirty context for pos, or the nearest dirty context that pulled pos into +// the current graph. Graph compilation intentionally includes connected blocks that are not dirty themselves. +func redstoneDirtyContext(dirty map[cube.Pos]redstoneDirty, pos cube.Pos) redstoneDirty { + if d, ok := dirty[pos]; ok { + return d + } + var ( + bestPos cube.Pos + best redstoneDirty + bestDist int + ok bool + ) + for dirtyPos, d := range dirty { + dist := redstoneManhattanDistance(pos, dirtyPos) + if !ok || dist < bestDist || (dist == bestDist && compareBlockPos(dirtyPos, bestPos) < 0) { + bestPos, best, bestDist, ok = dirtyPos, d, dist, true + } + } + if !ok { + return redstoneDirty{changed: pos, hasChanged: true, source: pos, hasSource: true, cause: RedstoneUpdateCauseCompilerRebuild} + } + return best +} + +// redstoneUpdate builds the public update payload for a dirty context. +func (d redstoneDirty) redstoneUpdate(pos cube.Pos, before Block, oldPower, newPower int, tick int64) RedstoneUpdate { + return RedstoneUpdate{ + Pos: pos, + ChangedNeighbour: d.changed, + HasChangedNeighbour: d.hasChanged, + ChangedRedstoneRelevant: d.changedRedstoneRelevant, + Source: d.source, + HasSource: d.hasSource, + Before: before, + OldPower: oldPower, + NewPower: newPower, + CurrentTick: tick, + Cause: d.cause, + } +} + +// propagatedFrom keeps the original source but records the block state that changed during propagation. +func (d redstoneDirty) propagatedFrom(pos cube.Pos) redstoneDirty { + d.changed = pos + d.hasChanged = true + d.changedRedstoneRelevant = true + return d +} + +func redstoneManhattanDistance(a, b cube.Pos) int { + return abs(a[0]-b[0]) + abs(a[1]-b[1]) + abs(a[2]-b[2]) +} + +func abs(v int) int { + if v < 0 { + return -v + } + return v +} + +// compile builds a deterministic graph around the candidate positions. +func (e *redstoneEngine) compile(tx *Tx, candidates []cube.Pos) redstoneGraph { + nodes := make([]redstoneNode, 0, len(candidates)) + seen := make(map[cube.Pos]struct{}, len(candidates)*2) + for _, pos := range candidates { + nodeCount := len(nodes) + e.compileRegion(tx, pos, seen, &nodes) + if len(nodes) == nodeCount { + e.compileAdjacentRedstone(tx, pos, seen, &nodes) + } + } + for i := 0; i < len(nodes); i++ { + e.compileAdjacentRedstone(tx, nodes[i].pos, seen, &nodes) + } + slices.SortFunc(nodes, func(a, b redstoneNode) int { + return compareBlockPos(a.pos, b.pos) + }) + edges := e.compileEdges(tx, nodes) + return redstoneGraph{nodes: nodes, edges: edges} +} + +// compileAdjacentRedstone adds redstone blocks that can interact with pos directly or through an adjacent conductor. +func (e *redstoneEngine) compileAdjacentRedstone(tx *Tx, pos cube.Pos, seen map[cube.Pos]struct{}, nodes *[]redstoneNode) { + if b, ok := tx.World().blockLoaded(pos); ok && e.redstoneBlockMayConduct(tx, pos, b) { + pos.Neighbours(func(neighbour cube.Pos) { + if b, ok := tx.World().blockLoaded(neighbour); ok && isRedstoneRelevant(b) { + e.compileRegion(tx, neighbour, seen, nodes) + } + }, tx.Range()) + } + pos.Neighbours(func(neighbour cube.Pos) { + b, ok := tx.World().blockLoaded(neighbour) + if !ok { + return + } + if isRedstoneRelevant(b) { + e.compileRegion(tx, neighbour, seen, nodes) + } + if e.redstoneBlockMayConduct(tx, neighbour, b) { + neighbour.Neighbours(func(conductedNeighbour cube.Pos) { + if b, ok := tx.World().blockLoaded(conductedNeighbour); ok && isRedstoneRelevant(b) { + e.compileRegion(tx, conductedNeighbour, seen, nodes) + } + }, tx.Range()) + } + }, tx.Range()) +} + +// redstoneBlockMayConduct reports whether a block should include its adjacent receivers in graph compilation. +func (e *redstoneEngine) redstoneBlockMayConduct(tx *Tx, pos cube.Pos, b Block) bool { + for _, face := range cube.Faces() { + if redstoneStrongPowerConductor(pos, b, tx, face) { + return true + } + } + return false +} + +// compileRegion walks a connected redstone-relevant region into nodes. +func (e *redstoneEngine) compileRegion(tx *Tx, pos cube.Pos, seen map[cube.Pos]struct{}, nodes *[]redstoneNode) { + if _, ok := seen[pos]; ok || pos.OutOfBounds(tx.Range()) { + return + } + queue := []cube.Pos{pos} + for len(queue) != 0 { + p := queue[0] + queue = queue[1:] + if _, ok := seen[p]; ok || p.OutOfBounds(tx.Range()) { + continue + } + seen[p] = struct{}{} + + b, ok := tx.World().blockLoaded(p) + if !ok { + continue + } + source, consumer, action, relayer := classifyRedstoneBlock(b) + if !source && !consumer && !action && !relayer { + continue + } + *nodes = append(*nodes, redstoneNode{ + pos: p, + source: source, + sink: consumer || action, + }) + if !relayer { + continue + } + for _, neighbour := range e.redstoneRelayerConnectedPositions(tx, p, b) { + if b, ok := tx.World().blockLoaded(neighbour); ok && isRedstoneRelevant(b) { + queue = append(queue, neighbour) + } + } + } +} + +// update applies a computed input power to a consumer or action block. +func (e *redstoneEngine) update(tx *Tx, pos cube.Pos, d redstoneDirty, newPower int) { + b := tx.Block(pos) + oldPower, newPower := e.power[pos], ClampRedstonePower(newPower) + + after, blockChanged := b, false + if consumer, ok := b.(RedstonePowerConsumer); ok { + after, blockChanged = consumer.RedstonePowerUpdate(pos, tx, newPower) + } + action, hasAction := b.(RedstonePowerAction) + contextAction, hasContextAction := b.(RedstonePowerContextAction) + + update := d.redstoneUpdate(pos, b, oldPower, newPower, e.currentTick) + if blockChanged { + update.After = after + } + shouldRunAction := hasContextAction || (hasAction && oldPower != newPower) + if oldPower != newPower || blockChanged || shouldRunAction { + if !e.redstoneUpdateAllowed(tx, update) { + return + } + } + + if !blockChanged && !shouldRunAction { + storeRedstonePower(e.power, pos, newPower) + return + } + + if blockChanged { + tx.SetBlock(pos, after, &SetOpts{DisableRedstoneUpdates: true}) + e.invalidateAroundWith(pos, d.propagatedFrom(pos), tx.Range()) + if postUpdater, ok := b.(RedstonePowerPostUpdater); ok { + postUpdater.RedstonePowerPostUpdate(pos, tx, b, after, oldPower, newPower) + } + } + if hasContextAction { + contextAction.RedstonePowerActionUpdate(pos, tx, update) + } else if shouldRunAction { + action.RedstonePowerAction(pos, tx, oldPower, newPower) + } + if blockChanged || shouldRunAction { + storeRedstonePower(e.power, pos, newPower) + } +} + +// updateGraphSources updates non-relayer source outputs before graph propagation. +func (e *redstoneEngine) updateGraphSources(tx *Tx, graph redstoneGraph, dirty map[cube.Pos]redstoneDirty) (map[cube.Pos]int, map[cube.Pos]struct{}) { + var cancelled map[cube.Pos]int + var checked map[cube.Pos]struct{} + for _, node := range graph.nodes { + if !node.source { + continue + } + b, ok := tx.World().blockLoaded(node.pos) + if !ok { + continue + } + if _, ok := b.(RedstonePowerRelayer); ok { + continue + } + if checked == nil { + checked = make(map[cube.Pos]struct{}) + } + checked[node.pos] = struct{}{} + + d := redstoneDirtyContext(dirty, node.pos) + if !e.updateSource(tx, node.pos, d) { + if cancelled == nil { + cancelled = make(map[cube.Pos]int) + } + cancelled[node.pos] = e.output[node.pos] + } + } + return cancelled, checked +} + +// updateSource updates cached output power for a source and reports whether it was allowed. +func (e *redstoneEngine) updateSource(tx *Tx, pos cube.Pos, d redstoneDirty) bool { + b := tx.Block(pos) + oldPower, newPower := e.output[pos], e.sourcePower(pos, tx) + if oldPower == newPower { + return true + } + update := d.redstoneUpdate(pos, b, oldPower, newPower, e.currentTick) + if !e.redstoneUpdateAllowed(tx, update) { + return false + } + storeRedstonePower(e.output, pos, newPower) + e.invalidateAroundWith(pos, d.propagatedFrom(pos), tx.Range()) + return true +} + +// storeRedstonePower stores non-zero redstone power and removes zero entries from cache maps. +func storeRedstonePower(cache map[cube.Pos]int, pos cube.Pos, power int) { + if power == 0 { + delete(cache, pos) + return + } + cache[pos] = power +} + +// directPower returns the strongest direct power reaching pos from any side. +func (e *redstoneEngine) directPower(pos cube.Pos, tx *Tx) int { + power := 0 + for _, face := range cube.Faces() { + power = max(power, e.directPowerFrom(pos, tx, face)) + } + return power +} + +// directPowerFrom returns direct power reaching pos through face. +func (e *redstoneEngine) directPowerFrom(pos cube.Pos, tx *Tx, face cube.Face) int { + neighbour := pos.Side(face) + if neighbour.OutOfBounds(tx.Range()) { + return 0 + } + b, ok := tx.World().blockLoaded(neighbour) + if !ok { + return 0 + } + if source, ok := b.(RedstonePowerSource); ok { + return ClampRedstonePower(e.redstonePower(source, neighbour, tx, face.Opposite())) + } + return 0 +} + +// strongPower returns the strongest strong power reaching pos from any side. +func (e *redstoneEngine) strongPower(pos cube.Pos, tx *Tx) int { + power := 0 + for _, face := range cube.Faces() { + power = max(power, e.strongPowerFrom(pos, tx, face)) + } + return power +} + +// strongPowerFrom returns strong power reaching pos through face. +func (e *redstoneEngine) strongPowerFrom(pos cube.Pos, tx *Tx, face cube.Face) int { + neighbour := pos.Side(face) + if neighbour.OutOfBounds(tx.Range()) { + return 0 + } + b, ok := tx.World().blockLoaded(neighbour) + if !ok { + return 0 + } + if source, ok := b.(RedstoneStrongPowerSource); ok { + if power, ok := e.suppressedSources[neighbour]; ok { + return ClampRedstonePower(power) + } + return ClampRedstonePower(source.RedstoneStrongPower(neighbour, tx, face.Opposite())) + } + return 0 +} + +// conductedStrongPower returns strong power conducted through adjacent conductive blocks. +func (e *redstoneEngine) conductedStrongPower(pos cube.Pos, tx *Tx) int { + power := 0 + for _, face := range cube.Faces() { + power = max(power, e.conductedStrongPowerFrom(pos, tx, face)) + } + return power +} + +// conductedStrongPowerFrom returns strong power conducted through the block on face. +func (e *redstoneEngine) conductedStrongPowerFrom(pos cube.Pos, tx *Tx, face cube.Face) int { + conductorPos := pos.Side(face) + if conductorPos.OutOfBounds(tx.Range()) { + return 0 + } + conductor, ok := tx.World().blockLoaded(conductorPos) + if !ok || !redstoneStrongPowerConductor(conductorPos, conductor, tx, face.Opposite()) { + return 0 + } + power := 0 + for _, sourceFace := range cube.Faces() { + power = max(power, e.strongPowerFrom(conductorPos, tx, sourceFace)) + } + return power +} + +// weakBlockPower returns weak power directly applied to a conductive block by sources that weak-power blocks. +func (e *redstoneEngine) weakBlockPower(pos cube.Pos, tx *Tx) int { + power := 0 + for _, face := range cube.Faces() { + power = max(power, e.weakBlockPowerFrom(pos, tx, face)) + } + return power +} + +// weakBlockPowerFrom returns weak power applied to pos from face by a source that weak-powers conductive blocks. +func (e *redstoneEngine) weakBlockPowerFrom(pos cube.Pos, tx *Tx, face cube.Face) int { + sourcePos := pos.Side(face) + if sourcePos.OutOfBounds(tx.Range()) { + return 0 + } + b, ok := tx.World().blockLoaded(sourcePos) + if !ok { + return 0 + } + if source, ok := b.(RedstonePowerSource); ok && e.redstoneWeaklyPowersBlocks(b) { + return ClampRedstonePower(e.redstonePower(source, sourcePos, tx, face.Opposite())) + } + return 0 +} + +// conductedWeakPower returns weak power conducted through adjacent conductive blocks for mechanism activation. +func (e *redstoneEngine) conductedWeakPower(pos cube.Pos, tx *Tx) int { + power := 0 + for _, face := range cube.Faces() { + power = max(power, e.conductedWeakPowerFrom(pos, tx, face)) + } + return power +} + +// conductedWeakPowerFrom returns weak power conducted through the block on face for mechanism activation. It excludes +// strong power and only counts sources that explicitly weak-power conductive blocks, such as redstone dust. +func (e *redstoneEngine) conductedWeakPowerFrom(pos cube.Pos, tx *Tx, face cube.Face) int { + conductorPos := pos.Side(face) + if conductorPos.OutOfBounds(tx.Range()) { + return 0 + } + conductor, ok := tx.World().blockLoaded(conductorPos) + if !ok || !redstoneStrongPowerConductor(conductorPos, conductor, tx, face.Opposite()) { + return 0 + } + return e.weakBlockPower(conductorPos, tx) +} + +// conductedActivationPower returns power that can activate a non-relayer mechanism behind adjacent conductive blocks. +func (e *redstoneEngine) conductedActivationPower(pos cube.Pos, tx *Tx) int { + return max(e.conductedStrongPower(pos, tx), e.conductedWeakPower(pos, tx)) +} + +// conductedActivationPowerFrom returns mechanism activation power conducted through the block on face. +func (e *redstoneEngine) conductedActivationPowerFrom(pos cube.Pos, tx *Tx, face cube.Face) int { + return max(e.conductedStrongPowerFrom(pos, tx, face), e.conductedWeakPowerFrom(pos, tx, face)) +} + +// conductivePowerTo returns power held by pos as a conductive block, excluding direct component activation. +func (e *redstoneEngine) conductivePowerTo(pos cube.Pos, tx *Tx) int { + b, ok := tx.World().blockLoaded(pos) + if !ok || !RedstoneFullPowerConductor(pos, b, tx) { + return 0 + } + return max(e.strongPower(pos, tx), e.weakBlockPower(pos, tx)) +} + +// acceptsDirectSourcePower reports whether direct source output should activate the block at pos. +func (e *redstoneEngine) acceptsDirectSourcePower(pos cube.Pos, tx *Tx) bool { + b, ok := tx.World().blockLoaded(pos) + if !ok { + return true + } + if isRedstoneRelevant(b) { + return true + } + return !RedstoneFullPowerConductor(pos, b, tx) +} + +// acceptsWeakConductedPower reports whether the block at pos may be activated by a weakly powered conductor. +func (e *redstoneEngine) acceptsWeakConductedPower(pos cube.Pos, tx *Tx) bool { + b, ok := tx.World().blockLoaded(pos) + if !ok { + return true + } + _, relayer := b.(RedstonePowerRelayer) + return !relayer +} + +// redstoneWeaklyPowersBlocks reports whether b's weak source output can weak-power adjacent conductive blocks. +func (e *redstoneEngine) redstoneWeaklyPowersBlocks(b Block) bool { + weakBlockPowerer, ok := b.(RedstoneWeakBlockPowerer) + return ok && weakBlockPowerer.RedstoneWeaklyPowersBlocks() +} + +// sourcePower returns the strongest output emitted by a source block. +func (e *redstoneEngine) sourcePower(pos cube.Pos, tx *Tx) int { + b, ok := tx.World().blockLoaded(pos) + if !ok { + return 0 + } + source, ok := b.(RedstonePowerSource) + if !ok { + return 0 + } + power := 0 + for _, face := range cube.Faces() { + power = max(power, ClampRedstonePower(e.redstonePower(source, pos, tx, face))) + } + return power +} + +// graphPower computes propagated graph power for every graph node. +func (e *redstoneEngine) graphPower(tx *Tx, graph redstoneGraph) []int { + powers := make([]int, len(graph.nodes)) + if len(graph.nodes) == 0 { + return powers + } + + index := make(map[cube.Pos]int, len(graph.nodes)) + sources := make([]RedstonePowerSource, len(graph.nodes)) + relayers := make([]RedstonePowerRelayer, len(graph.nodes)) + edges := make([][]redstoneEdge, len(graph.nodes)) + for i, node := range graph.nodes { + index[node.pos] = i + if b, ok := tx.World().blockLoaded(node.pos); ok { + sources[i], _ = b.(RedstonePowerSource) + relayers[i], _ = b.(RedstonePowerRelayer) + } + } + for _, edge := range graph.edges { + edges[edge.from] = append(edges[edge.from], edge) + } + + queue := make([]int, 0, len(graph.nodes)) + push := func(i, power int) { + power = ClampRedstonePower(power) + if power <= powers[i] { + return + } + powers[i] = power + queue = append(queue, i) + } + + for i, node := range graph.nodes { + if node.sink && relayers[i] == nil { + push(i, e.conductedActivationPower(node.pos, tx)) + continue + } + push(i, e.conductedStrongPower(node.pos, tx)) + } + + for i, source := range sources { + // Relayers such as redstone wire store their previous output as RedstonePower. + // They must be recomputed from real sources, not used as seeds themselves. + if source == nil || relayers[i] != nil { + continue + } + pos := graph.nodes[i].pos + for _, face := range cube.Faces() { + j, ok := index[pos.Side(face)] + if !ok { + continue + } + power := ClampRedstonePower(e.redstonePower(source, pos, tx, face)) + push(j, power) + } + } + + for head := 0; head < len(queue); head++ { + i := queue[head] + if relayers[i] == nil { + continue + } + for _, edge := range edges[i] { + push(edge.to, powers[i]-edge.weight) + } + } + return powers +} + +// powerTo returns the strongest redstone power currently reaching pos. +func (e *redstoneEngine) powerTo(pos cube.Pos, tx *Tx) int { + power := 0 + for _, face := range cube.Faces() { + power = max(power, e.powerFrom(pos, tx, face)) + } + if e.acceptsWeakConductedPower(pos, tx) { + power = max(power, e.conductedActivationPower(pos, tx)) + } else { + power = max(power, e.conductedStrongPower(pos, tx)) + } + return ClampRedstonePower(power) +} + +// powerFrom returns redstone power reaching pos through face. +func (e *redstoneEngine) powerFrom(pos cube.Pos, tx *Tx, face cube.Face) int { + power := e.conductedStrongPowerFrom(pos, tx, face) + if e.acceptsWeakConductedPower(pos, tx) { + power = e.conductedActivationPowerFrom(pos, tx, face) + } + type step struct { + pos cube.Pos + from cube.Face + loss int + depth int + } + queue := []step{{pos: pos.Side(face), from: face.Opposite(), loss: 0, depth: 0}} + seen := make(map[cube.Pos]int, 16) + for len(queue) != 0 { + s := queue[0] + queue = queue[1:] + if s.pos.OutOfBounds(tx.Range()) || s.loss >= 15 || s.depth >= 15 { + continue + } + if s.pos == pos { + continue + } + if loss, ok := seen[s.pos]; ok && loss <= s.loss { + continue + } + seen[s.pos] = s.loss + + b, ok := tx.World().blockLoaded(s.pos) + if !ok { + continue + } + relayer, isRelayer := b.(RedstonePowerRelayer) + // See graphPower: relayers carry recomputed power through edges, so their + // stored RedstonePower should not count as an independent source here. + if source, ok := b.(RedstonePowerSource); ok && !isRelayer && e.acceptsDirectSourcePower(pos, tx) { + power = max(power, ClampRedstonePower(e.redstonePower(source, s.pos, tx, s.from)-s.loss)) + } + if !isRelayer { + continue + } + for _, next := range e.redstoneRelayerNeighbourPositions(tx, s.pos, b) { + to := redstoneStepFace(s.pos, next) + if to == s.from { + continue + } + nextBlock, ok := tx.World().blockLoaded(next) + if !ok { + continue + } + loss := s.loss + if _, nextRelayer := nextBlock.(RedstonePowerRelayer); nextRelayer { + loss += max(relayer.RedstoneSignalLoss(s.pos, tx), 1) + } + if loss <= 15 { + queue = append(queue, step{pos: next, from: to.Opposite(), loss: loss, depth: s.depth + 1}) + } + } + } + return ClampRedstonePower(power) +} + +// torchBurnoutStatus returns whether the torch at pos is burned out and whether it can recover at currentTick. +func (e *redstoneEngine) torchBurnoutStatus(pos cube.Pos, currentTick int64) (burnedOut, recoverable bool) { + data, ok := e.pruneTorchBurnout(pos, currentTick) + if !ok { + return false, true + } + if !data.burnedOut { + return false, true + } + return true, len(data.offTicks) < redstoneTorchBurnoutThreshold +} + +// recordTorchTurnOff records a torch being forced off and reports whether that torch should burn out. +func (e *redstoneEngine) recordTorchTurnOff(pos cube.Pos, currentTick int64) bool { + if e.torchBurnout == nil { + e.torchBurnout = make(map[cube.Pos]redstoneTorchBurnout) + } + data, _ := e.pruneTorchBurnout(pos, currentTick) + data.offTicks = append(data.offTicks, currentTick) + if len(data.offTicks) >= redstoneTorchBurnoutThreshold { + data.burnedOut = true + } + e.torchBurnout[pos] = data + return data.burnedOut +} + +// clearTorchBurnout removes transient burnout state for a redstone torch. +func (e *redstoneEngine) clearTorchBurnout(pos cube.Pos) { + if e == nil { + return + } + delete(e.torchBurnout, pos) +} + +// markTorchSelfTriggered records that the next torch turn-off at pos was caused by that torch's own output loop. +func (e *redstoneEngine) markTorchSelfTriggered(pos cube.Pos) { + if e == nil { + return + } + if e.torchBurnout == nil { + e.torchBurnout = make(map[cube.Pos]redstoneTorchBurnout) + } + data := e.torchBurnout[pos] + data.pendingSelfTriggered = true + e.torchBurnout[pos] = data +} + +// consumeTorchSelfTriggered reports and clears whether the next torch turn-off at pos was self-triggered. +func (e *redstoneEngine) consumeTorchSelfTriggered(pos cube.Pos) bool { + if e == nil || e.torchBurnout == nil { + return false + } + data, ok := e.torchBurnout[pos] + if !ok { + return false + } + selfTriggered := data.pendingSelfTriggered + data.pendingSelfTriggered = false + if len(data.offTicks) == 0 && !data.burnedOut { + delete(e.torchBurnout, pos) + } else { + e.torchBurnout[pos] = data + } + return selfTriggered +} + +// pruneTorchBurnout removes expired turn-off entries and returns the remaining burnout data. +func (e *redstoneEngine) pruneTorchBurnout(pos cube.Pos, currentTick int64) (redstoneTorchBurnout, bool) { + if e == nil || e.torchBurnout == nil { + return redstoneTorchBurnout{}, false + } + data, ok := e.torchBurnout[pos] + if !ok { + return redstoneTorchBurnout{}, false + } + data.offTicks = slices.DeleteFunc(data.offTicks, func(tick int64) bool { + return currentTick-tick >= redstoneTorchBurnoutWindowTicks + }) + if len(data.offTicks) == 0 && !data.burnedOut && !data.pendingSelfTriggered { + delete(e.torchBurnout, pos) + return redstoneTorchBurnout{}, false + } + e.torchBurnout[pos] = data + return data, true +} + +// redstonePower reads source power while guarding against recursive source evaluation. +func (e *redstoneEngine) redstonePower(source RedstonePowerSource, pos cube.Pos, tx *Tx, face cube.Face) int { + if power, ok := e.suppressedSources[pos]; ok { + return ClampRedstonePower(power) + } + if _, ok := e.evaluating[pos]; ok { + return 0 + } + e.evaluating[pos] = struct{}{} + defer delete(e.evaluating, pos) + return source.RedstonePower(pos, tx, face) +} + +// redstoneUpdateAllowed dispatches redstone callbacks and reports whether the update was cancelled. +func (e *redstoneEngine) redstoneUpdateAllowed(tx *Tx, update RedstoneUpdate) bool { + ctx := tx.Event() + tx.World().Handler().HandleRedstoneUpdate(ctx, update) + return !ctx.Cancelled() +} + +// redstoneLightDiffuser is the local subset needed to exclude transparent conductors. +type redstoneLightDiffuser interface { + LightDiffusionLevel() uint8 +} + +// redstoneStrongPowerConductor reports whether b can conduct strong power through face. +func redstoneStrongPowerConductor(pos cube.Pos, b Block, tx *Tx, face cube.Face) bool { + if !b.Model().FaceSolid(pos, face, tx) { + return false + } + if _, ok := b.(RedstoneNonConductive); ok { + return false + } + if diffuser, ok := b.(redstoneLightDiffuser); ok && diffuser.LightDiffusionLevel() == 0 { + return false + } + return true +} + +// RedstoneFullPowerConductor reports whether b is a full solid redstone conductor according to the default redstone +// conductivity rules. +func RedstoneFullPowerConductor(pos cube.Pos, b Block, tx *Tx) bool { + for _, face := range cube.Faces() { + if !redstoneStrongPowerConductor(pos, b, tx, face) { + return false + } + } + return true +} + +// compileEdges builds deterministic weighted edges between relayer-connected nodes. +func (e *redstoneEngine) compileEdges(tx *Tx, nodes []redstoneNode) []redstoneEdge { + index := make(map[cube.Pos]int, len(nodes)) + for i, node := range nodes { + index[node.pos] = i + } + edges := make([]redstoneEdge, 0, len(nodes)) + for i, node := range nodes { + b, loaded := tx.World().blockLoaded(node.pos) + if !loaded { + continue + } + relayer, ok := b.(RedstonePowerRelayer) + if !ok { + continue + } + for _, neighbour := range e.redstoneRelayerNeighbourPositions(tx, node.pos, b) { + j, ok := index[neighbour] + if !ok { + continue + } + weight := 0 + if neighbourBlock, ok := tx.World().blockLoaded(neighbour); ok { + if _, neighbourRelayer := neighbourBlock.(RedstonePowerRelayer); neighbourRelayer { + weight = max(relayer.RedstoneSignalLoss(node.pos, tx), 1) + } + } + edges = append(edges, redstoneEdge{from: i, to: j, weight: weight}) + } + } + slices.SortFunc(edges, compareRedstoneEdge) + return edges +} + +// redstoneRelayerNeighbourPositions returns sorted relayer neighbours for b at pos. +func (e *redstoneEngine) redstoneRelayerNeighbourPositions(tx *Tx, pos cube.Pos, b Block) []cube.Pos { + if neighbourer, ok := b.(RedstonePowerRelayerNeighbourer); ok { + neighbours := slices.Clone(neighbourer.RedstoneRelayerNeighbours(pos, tx)) + slices.SortFunc(neighbours, compareBlockPos) + return neighbours + } + neighbours := make([]cube.Pos, 0, len(cube.Faces())) + e.redstoneRelayerNeighbours(tx, pos, func(neighbour cube.Pos) { + neighbours = append(neighbours, neighbour) + }) + slices.SortFunc(neighbours, compareBlockPos) + return neighbours +} + +// redstoneRelayerConnectedPositions returns relayers connected to pos in either direction. Graph membership must be +// weakly connected so a one-way relayer, such as dust climbing glowstone, still compiles with its lower input path. +func (e *redstoneEngine) redstoneRelayerConnectedPositions(tx *Tx, pos cube.Pos, b Block) []cube.Pos { + neighbours := e.redstoneRelayerNeighbourPositions(tx, pos, b) + seen := make(map[cube.Pos]struct{}, len(neighbours)+8) + for _, neighbour := range neighbours { + seen[neighbour] = struct{}{} + } + for _, candidate := range redstoneRelayerIncomingCandidates(pos, tx.Range()) { + if _, ok := seen[candidate]; ok { + continue + } + candidateBlock, ok := tx.World().blockLoaded(candidate) + if !ok { + continue + } + if _, ok := candidateBlock.(RedstonePowerRelayer); !ok { + continue + } + if slices.Contains(e.redstoneRelayerNeighbourPositions(tx, candidate, candidateBlock), pos) { + neighbours = append(neighbours, candidate) + seen[candidate] = struct{}{} + } + } + slices.SortFunc(neighbours, compareBlockPos) + return neighbours +} + +// redstoneRelayerIncomingCandidates returns nearby positions that can point at pos with the built-in relayer geometry. +func redstoneRelayerIncomingCandidates(pos cube.Pos, r cube.Range) []cube.Pos { + candidates := make([]cube.Pos, 0, 26) + for x := pos[0] - 1; x <= pos[0]+1; x++ { + for y := pos[1] - 1; y <= pos[1]+1; y++ { + for z := pos[2] - 1; z <= pos[2]+1; z++ { + candidate := cube.Pos{x, y, z} + if candidate == pos || candidate.OutOfBounds(r) { + continue + } + candidates = append(candidates, candidate) + } + } + } + return candidates +} + +// redstoneRelayerNeighbours visits the six default adjacent relayer neighbours. +func (e *redstoneEngine) redstoneRelayerNeighbours(tx *Tx, pos cube.Pos, f func(cube.Pos)) { + for _, face := range cube.Faces() { + neighbour := pos.Side(face) + if !neighbour.OutOfBounds(tx.Range()) { + f(neighbour) + } + } +} + +// redstoneStepFace returns the dominant face direction from one relayer position to another. +func redstoneStepFace(from, to cube.Pos) cube.Face { + dx, dy, dz := to[0]-from[0], to[1]-from[1], to[2]-from[2] + switch { + case dy > 0: + return cube.FaceUp + case dy < 0: + return cube.FaceDown + case dx > 0: + return cube.FaceEast + case dx < 0: + return cube.FaceWest + case dz > 0: + return cube.FaceSouth + case dz < 0: + return cube.FaceNorth + default: + return cube.FaceUp + } +} + +// classifyRedstoneBlock reports the redstone capabilities implemented by b. +func classifyRedstoneBlock(b Block) (source, consumer, action, relayer bool) { + _, source = b.(RedstonePowerSource) + _, consumer = b.(RedstonePowerConsumer) + _, action = b.(RedstonePowerAction) + if !action { + _, action = b.(RedstonePowerContextAction) + } + _, relayer = b.(RedstonePowerRelayer) + return +} + +// isRedstoneRelevant reports whether b should be included in redstone graph compilation. +func isRedstoneRelevant(b Block) bool { + source, consumer, action, relayer := classifyRedstoneBlock(b) + return source || consumer || action || relayer +} + +// compareBlockPos orders positions deterministically by Y, Z, then X. +func compareBlockPos(a, b cube.Pos) int { + if a[1] != b[1] { + return a[1] - b[1] + } + if a[2] != b[2] { + return a[2] - b[2] + } + return a[0] - b[0] +} + +// compareRedstoneEdge orders edges deterministically by endpoints and weight. +func compareRedstoneEdge(a, b redstoneEdge) int { + if a.from != b.from { + return a.from - b.from + } + if a.to != b.to { + return a.to - b.to + } + return a.weight - b.weight +} + +// ClampRedstonePower clamps power to the vanilla 0-15 redstone range. +func ClampRedstonePower(power int) int { + if power < 0 { + return 0 + } + if power > 15 { + return 15 + } + return power +} diff --git a/server/world/redstone_bench_test.go b/server/world/redstone_bench_test.go new file mode 100644 index 0000000000..ad1cc3b264 --- /dev/null +++ b/server/world/redstone_bench_test.go @@ -0,0 +1,43 @@ +package world + +import ( + "testing" + + "github.com/df-mc/dragonfly/server/block/cube" +) + +var redstoneDirtyTickBenchmarkPower int + +func BenchmarkRedstoneDirtyTickLongLineWithClocks(b *testing.B) { + const lineLength = 96 + + w := Config{Synchronous: true, Blocks: redstoneSignalLossTestRegistry()}.New() + defer w.Close() + + runWorld(w, func(tx *Tx) { + clockA := cube.Pos{-1, 64, 0} + clockB := cube.Pos{lineLength / 2, 64, 1} + line := make([]cube.Pos, lineLength) + for x := range lineLength { + line[x] = cube.Pos{x, 64, 0} + tx.SetBlock(line[x], redstoneLossRelayer{}, nil) + } + tx.SetBlock(clockA, redstoneLossSource{Power: 15}, nil) + tx.SetBlock(clockB, redstoneLossSource{Power: 15}, nil) + tx.SetBlock(cube.Pos{lineLength, 64, 0}, redstoneLossConsumer{}, nil) + tx.World().redstone.tick(tx, 0) + + b.ReportAllocs() + b.ResetTimer() + for tick := range b.N { + tx.World().redstone.invalidateAround(clockA, clockA, RedstoneUpdateCauseBlockUpdate, tx.Range()) + tx.World().redstone.invalidateAround(clockB, clockB, RedstoneUpdateCauseBlockUpdate, tx.Range()) + tx.World().redstone.tick(tx, int64(tick+1)) + } + b.StopTimer() + + if consumer, ok := tx.Block(cube.Pos{lineLength, 64, 0}).(redstoneLossConsumer); ok { + redstoneDirtyTickBenchmarkPower = consumer.Power + } + }) +} diff --git a/server/world/redstone_test.go b/server/world/redstone_test.go new file mode 100644 index 0000000000..161deea7d0 --- /dev/null +++ b/server/world/redstone_test.go @@ -0,0 +1,1115 @@ +package world + +import ( + "context" + "math/rand/v2" + "slices" + "testing" + "time" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl64" +) + +var _ Handler = minimalRedstoneTestHandler{} + +type minimalRedstoneTestHandler struct{} + +func runWorld(w *World, f func(*Tx)) { + w.Do(f).Wait(context.Background()) +} + +func (minimalRedstoneTestHandler) HandleRedstoneUpdate(*Context, RedstoneUpdate) {} +func (minimalRedstoneTestHandler) HandleLiquidFlow(*Context, cube.Pos, cube.Pos, Liquid, Block) {} +func (minimalRedstoneTestHandler) HandleLiquidDecay(*Context, cube.Pos, Liquid, Liquid) {} +func (minimalRedstoneTestHandler) HandleLiquidHarden(*Context, cube.Pos, Block, Block, Block) {} +func (minimalRedstoneTestHandler) HandleSound(*Context, Sound, mgl64.Vec3) {} +func (minimalRedstoneTestHandler) HandleFireSpread(*Context, cube.Pos, cube.Pos) {} +func (minimalRedstoneTestHandler) HandleBlockBurn(*Context, cube.Pos) {} +func (minimalRedstoneTestHandler) HandleCropTrample(*Context, cube.Pos) {} +func (minimalRedstoneTestHandler) HandleLeavesDecay(*Context, cube.Pos) {} +func (minimalRedstoneTestHandler) HandlePortalCreate(*Context, Dimension, []cube.Pos) {} +func (minimalRedstoneTestHandler) HandlePortalActivate(*Context, Dimension, []cube.Pos) {} +func (minimalRedstoneTestHandler) HandleEntitySpawn(*Tx, Entity) {} +func (minimalRedstoneTestHandler) HandleEntityDespawn(*Tx, Entity) {} +func (minimalRedstoneTestHandler) HandleExplosion(*Context, ExplosionSource, *[]Entity, *[]cube.Pos, *float64, *bool) { +} +func (minimalRedstoneTestHandler) HandleClose(*Tx) {} + +func TestClampRedstonePower(t *testing.T) { + tests := []struct { + name string + power int + want int + }{ + {name: "negative", power: -1, want: 0}, + {name: "zero", power: 0, want: 0}, + {name: "middle", power: 8, want: 8}, + {name: "maximum", power: 15, want: 15}, + {name: "over maximum", power: 16, want: 15}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := ClampRedstonePower(test.power); got != test.want { + t.Fatalf("ClampRedstonePower(%d) = %d, want %d", test.power, got, test.want) + } + }) + } +} + +func TestRedstoneStepFace(t *testing.T) { + tests := []struct { + name string + from cube.Pos + to cube.Pos + want cube.Face + }{ + {name: "diagonal step up", from: cube.Pos{0, 64, 0}, to: cube.Pos{1, 65, 0}, want: cube.FaceUp}, + {name: "diagonal step down", from: cube.Pos{0, 64, 0}, to: cube.Pos{-1, 63, 0}, want: cube.FaceDown}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := redstoneStepFace(test.from, test.to); got != test.want { + t.Fatalf("redstoneStepFace(%v, %v) = %v, want %v", test.from, test.to, got, test.want) + } + }) + } +} + +func TestRedstoneTorchBurnoutRecoveryUsesRollingWindow(t *testing.T) { + e := newRedstoneEngine(0) + pos := cube.Pos{1, 64, 1} + for tick := int64(0); tick < redstoneTorchBurnoutThreshold; tick++ { + burnedOut := e.recordTorchTurnOff(pos, tick) + if burnedOut != (tick == redstoneTorchBurnoutThreshold-1) { + t.Fatalf("recordTorchTurnOff after %d turn-offs burnedOut=%t", tick+1, burnedOut) + } + } + + burnedOut, recoverable := e.torchBurnoutStatus(pos, redstoneTorchBurnoutWindowTicks-1) + if !burnedOut || recoverable { + t.Fatalf("burnout status with all eight turn-offs still inside the rolling window = burnedOut:%t recoverable:%t, want burnedOut:true recoverable:false", burnedOut, recoverable) + } + burnedOut, recoverable = e.torchBurnoutStatus(pos, redstoneTorchBurnoutWindowTicks) + if !burnedOut || !recoverable { + t.Fatalf("burnout status after earliest turn-off has aged out of the rolling window = burnedOut:%t recoverable:%t, want burnedOut:true recoverable:true", burnedOut, recoverable) + } +} + +func TestCompareBlockPosSortOrder(t *testing.T) { + positions := []cube.Pos{ + {3, 2, 1}, + {2, 1, 2}, + {1, 1, 1}, + {0, 1, 1}, + {0, 0, 9}, + {9, 0, -1}, + } + want := []cube.Pos{ + {9, 0, -1}, + {0, 0, 9}, + {0, 1, 1}, + {1, 1, 1}, + {2, 1, 2}, + {3, 2, 1}, + } + + slices.SortFunc(positions, compareBlockPos) + if !slices.Equal(positions, want) { + t.Fatalf("sorted positions = %v, want %v", positions, want) + } + if got := compareBlockPos(cube.Pos{1, 2, 3}, cube.Pos{1, 2, 3}); got != 0 { + t.Fatalf("compareBlockPos(equal positions) = %d, want 0", got) + } +} + +func TestRedstoneRelayerNeighbourPositionsAreDeterministic(t *testing.T) { + engine := newRedstoneEngine(0) + pos := cube.Pos{0, 64, 0} + got := engine.redstoneRelayerNeighbourPositions(nil, pos, redstoneNeighbourOrderTestBlock{neighbours: []cube.Pos{ + {1, 64, 0}, + {0, 63, 0}, + {0, 64, -1}, + {-1, 64, 0}, + {0, 65, 0}, + {0, 64, 1}, + }}) + want := []cube.Pos{ + {0, 63, 0}, + {0, 64, -1}, + {-1, 64, 0}, + {1, 64, 0}, + {0, 64, 1}, + {0, 65, 0}, + } + if !slices.Equal(got, want) { + t.Fatalf("redstone relayer neighbours = %v, want %v", got, want) + } +} + +func TestRedstoneStrongPowerConductorExcludesMarkedNonConductors(t *testing.T) { + pos := cube.Pos{0, 64, 0} + tests := []struct { + name string + block Block + want bool + }{ + {name: "solid block", block: redstoneSolidBlock{}, want: true}, + {name: "marked non-conductor", block: redstoneNonConductiveSolidBlock{}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := redstoneStrongPowerConductor(pos, test.block, nil, cube.FaceWest); got != test.want { + t.Fatalf("redstoneStrongPowerConductor(%T) = %t, want %t", test.block, got, test.want) + } + }) + } +} + +func TestRedstoneEngineInvalidateAround(t *testing.T) { + var nilEngine *redstoneEngine + nilEngine.invalidateAround(cube.Pos{0, 0, 0}, cube.Pos{0, 0, 0}, RedstoneUpdateCauseBlockUpdate, cube.Range{0, 0}) + + engine := newRedstoneEngine(42) + pos, changed := cube.Pos{8, 0, 8}, cube.Pos{9, 0, 8} + engine.invalidateAround(pos, changed, RedstoneUpdateCauseBlockUpdate, cube.Range{0, 1}) + + want := map[cube.Pos]redstoneDirty{ + {8, 0, 8}: redstoneDirty{changed: changed, hasChanged: true, source: changed, hasSource: true, cause: RedstoneUpdateCauseBlockUpdate}, + {9, 0, 8}: redstoneDirty{changed: changed, hasChanged: true, source: changed, hasSource: true, cause: RedstoneUpdateCauseBlockUpdate}, + {7, 0, 8}: redstoneDirty{changed: changed, hasChanged: true, source: changed, hasSource: true, cause: RedstoneUpdateCauseBlockUpdate}, + {8, 1, 8}: redstoneDirty{changed: changed, hasChanged: true, source: changed, hasSource: true, cause: RedstoneUpdateCauseBlockUpdate}, + {8, 0, 9}: redstoneDirty{changed: changed, hasChanged: true, source: changed, hasSource: true, cause: RedstoneUpdateCauseBlockUpdate}, + {8, 0, 7}: redstoneDirty{changed: changed, hasChanged: true, source: changed, hasSource: true, cause: RedstoneUpdateCauseBlockUpdate}, + } + if len(engine.dirty) != len(want) { + t.Fatalf("dirty positions = %v, want %v", engine.dirty, want) + } + for pos, dirty := range want { + if got, ok := engine.dirty[pos]; !ok || got != dirty { + t.Fatalf("dirty[%v] = %v, %t; want %v, true", pos, got, ok, dirty) + } + } + + engine.invalidateAround(cube.Pos{0, -1, 0}, changed, RedstoneUpdateCauseScheduledTick, cube.Range{0, 1}) + if len(engine.dirty) != len(want) { + t.Fatalf("out-of-bounds invalidation changed dirty positions to %v, want %v", engine.dirty, want) + } + for pos, dirty := range want { + if got, ok := engine.dirty[pos]; !ok || got != dirty { + t.Fatalf("dirty[%v] after out-of-bounds invalidation = %v, %t; want %v, true", pos, got, ok, dirty) + } + } +} + +func TestRedstoneEngineRemoveChunkKeepsUnchangedDirtyOutsideChunk(t *testing.T) { + engine := newRedstoneEngine(42) + unloadedChunk := chunkPosFromBlockPos(cube.Pos{0, 64, 0}) + dirtyPos := cube.Pos{32, 64, 0} + unchanged := redstoneDirty{cause: RedstoneUpdateCauseCompilerRebuild} + engine.dirty[dirtyPos] = unchanged + + engine.removeChunk(unloadedChunk) + if got, ok := engine.dirty[dirtyPos]; !ok || got != unchanged { + t.Fatalf("dirty[%v] after unrelated chunk removal = %v, %t; want %v, true", dirtyPos, got, ok, unchanged) + } + + changed := redstoneDirty{changed: cube.Pos{0, 64, 0}, hasChanged: true, cause: RedstoneUpdateCauseBlockUpdate} + engine.dirty[dirtyPos] = changed + engine.removeChunk(unloadedChunk) + if _, ok := engine.dirty[dirtyPos]; ok { + t.Fatalf("dirty[%v] with changed position in unloaded chunk was not removed", dirtyPos) + } +} + +func TestRedstoneEngineRemoveChunkClearsTransientStateInChunk(t *testing.T) { + engine := newRedstoneEngine(42) + unloadedPos := cube.Pos{0, 64, 0} + keptPos := cube.Pos{32, 64, 0} + unloadedChunk := chunkPosFromBlockPos(unloadedPos) + + engine.power[unloadedPos] = 15 + engine.output[unloadedPos] = 14 + engine.torchBurnout = map[cube.Pos]redstoneTorchBurnout{unloadedPos: {burnedOut: true}, keptPos: {burnedOut: true}} + + engine.power[keptPos] = 13 + engine.output[keptPos] = 12 + engine.removeChunk(unloadedChunk) + + if _, ok := engine.power[unloadedPos]; ok { + t.Fatalf("power for unloaded position %v was not cleared", unloadedPos) + } + if _, ok := engine.output[unloadedPos]; ok { + t.Fatalf("output for unloaded position %v was not cleared", unloadedPos) + } + if _, ok := engine.torchBurnout[unloadedPos]; ok { + t.Fatalf("torch burnout state for unloaded position %v was not cleared", unloadedPos) + } + if got := engine.power[keptPos]; got != 13 { + t.Fatalf("power for kept position = %d, want 13", got) + } + if got := engine.output[keptPos]; got != 12 { + t.Fatalf("output for kept position = %d, want 12", got) + } + if burnout, ok := engine.torchBurnout[keptPos]; !ok || !burnout.burnedOut { + t.Fatalf("torch burnout state for kept position = %v, %t; want burned out state", burnout, ok) + } +} + +func TestRedstoneCancelledSourceDoesNotPropagate(t *testing.T) { + sourcePos, sinkPos := cube.Pos{0, 64, 0}, cube.Pos{1, 64, 0} + w := Config{Synchronous: true, Blocks: redstoneCancellationTestRegistry()}.New() + defer w.Close() + + w.Handle(&redstoneCancellationHandler{cancel: map[cube.Pos]struct{}{sourcePos: {}}}) + var sinkPowered bool + var sourceOutput int + runWorld(w, func(tx *Tx) { + tx.SetBlock(sourcePos, redstoneCancellationSource{Power: 15}, nil) + tx.SetBlock(sinkPos, redstoneCancellationConsumer{}, nil) + tx.World().redstone.tick(tx, 1) + + sinkPowered = tx.Block(sinkPos).(redstoneCancellationConsumer).Powered + sourceOutput = tx.World().redstone.output[sourcePos] + }) + if sinkPowered { + t.Fatalf("sink powered after cancelling source update") + } + if sourceOutput != 0 { + t.Fatalf("stored source output = %d, want 0", sourceOutput) + } +} + +func TestRedstoneCancelledSourceKeepsPreviousOutputDuringEvaluation(t *testing.T) { + sourcePos := cube.Pos{0, 64, 0} + w := Config{Synchronous: true, Blocks: redstoneCancellationTestRegistry()}.New() + defer w.Close() + + w.Handle(&redstoneCancellationHandler{cancel: map[cube.Pos]struct{}{sourcePos: {}}}) + + var sourceOutput int + runWorld(w, func(tx *Tx) { + tx.SetBlock(sourcePos, redstoneCancellationSource{}, &SetOpts{DisableRedstoneUpdates: true}) + tx.World().redstone.output[sourcePos] = 15 + tx.World().redstone.invalidate(sourcePos, redstoneDirty{changed: sourcePos, hasChanged: true, source: sourcePos, hasSource: true, cause: RedstoneUpdateCauseBlockUpdate}, tx.Range()) + tx.World().redstone.tick(tx, 2) + + sourceOutput = tx.World().redstone.output[sourcePos] + }) + if sourceOutput != 15 { + t.Fatalf("stored source output after cancelled update = %d, want 15", sourceOutput) + } +} + +func TestRedstoneCancelledConsumerDoesNotUpdate(t *testing.T) { + sourcePos, sinkPos := cube.Pos{0, 64, 0}, cube.Pos{1, 64, 0} + w := Config{Synchronous: true, Blocks: redstoneCancellationTestRegistry()}.New() + defer w.Close() + + w.Handle(&redstoneCancellationHandler{cancel: map[cube.Pos]struct{}{sinkPos: {}}}) + var sinkPowered bool + runWorld(w, func(tx *Tx) { + tx.SetBlock(sourcePos, redstoneCancellationSource{Power: 15}, nil) + tx.SetBlock(sinkPos, redstoneCancellationConsumer{}, nil) + tx.World().redstone.tick(tx, 1) + + sinkPowered = tx.Block(sinkPos).(redstoneCancellationConsumer).Powered + }) + if sinkPowered { + t.Fatalf("sink powered after cancelling consumer update") + } +} + +func TestRedstoneUpdateIncludesContextMetadata(t *testing.T) { + sourcePos, sinkPos := cube.Pos{0, 64, 0}, cube.Pos{1, 64, 0} + w := Config{Synchronous: true, Blocks: redstoneCancellationTestRegistry()}.New() + defer w.Close() + + handler := &redstoneRecordingHandler{pos: sinkPos} + w.Handle(handler) + runWorld(w, func(tx *Tx) { + tx.SetBlock(sinkPos, redstoneCancellationConsumer{}, &SetOpts{DisableRedstoneUpdates: true}) + tx.SetBlock(sourcePos, redstoneCancellationSource{Power: 15}, nil) + tx.World().redstone.tick(tx, 7) + }) + if len(handler.updates) == 0 { + t.Fatal("no redstone update recorded for consumer") + } + update := handler.updates[0] + if update.Pos != sinkPos { + t.Fatalf("update Pos = %v, want %v", update.Pos, sinkPos) + } + if !update.HasChangedNeighbour { + t.Fatal("update did not record a changed neighbour") + } + if !update.ChangedRedstoneRelevant { + t.Fatal("update did not mark changed neighbour as redstone relevant") + } + if !update.HasSource || update.Source != sourcePos { + t.Fatalf("update source = %v, %t; want %v, true", update.Source, update.HasSource, sourcePos) + } + if update.OldPower != 0 || update.NewPower != 15 { + t.Fatalf("update power = old:%d new:%d, want old:0 new:15", update.OldPower, update.NewPower) + } + if update.CurrentTick != 7 { + t.Fatalf("update CurrentTick = %d, want 7", update.CurrentTick) + } + if update.Cause != RedstoneUpdateCauseBlockUpdate { + t.Fatalf("update Cause = %d, want RedstoneUpdateCauseBlockUpdate", update.Cause) + } + if _, ok := update.Before.(redstoneCancellationConsumer); !ok { + t.Fatalf("update Before = %T, want redstoneCancellationConsumer", update.Before) + } + if after, ok := update.After.(redstoneCancellationConsumer); !ok || !after.Powered { + t.Fatalf("update After = %T %#v, want powered redstoneCancellationConsumer", update.After, update.After) + } +} + +func TestRedstoneRecursiveSourceEvaluationReturnsZero(t *testing.T) { + sourcePos, targetPos := cube.Pos{0, 64, 0}, cube.Pos{1, 64, 0} + w := Config{Synchronous: true, Blocks: redstoneRecursiveSourceTestRegistry()}.New() + defer w.Close() + + var power int + runWorld(w, func(tx *Tx) { + tx.SetBlock(sourcePos, redstoneRecursiveSource{Target: targetPos}, nil) + power = tx.RedstonePower(targetPos) + }) + if power != 0 { + t.Fatalf("recursive source power = %d, want 0", power) + } +} + +func TestRedstoneCancelledActionDoesNotRun(t *testing.T) { + sourcePos, actionPos := cube.Pos{0, 64, 0}, cube.Pos{1, 64, 0} + w := Config{Synchronous: true, Blocks: redstoneCancellationTestRegistry()}.New() + defer w.Close() + + actions := 0 + redstoneCancellationActions = &actions + t.Cleanup(func() { + redstoneCancellationActions = nil + }) + w.Handle(&redstoneCancellationHandler{cancel: map[cube.Pos]struct{}{actionPos: {}}}) + runWorld(w, func(tx *Tx) { + tx.SetBlock(sourcePos, redstoneCancellationSource{Power: 15}, nil) + tx.SetBlock(actionPos, redstoneCancellationAction{}, nil) + tx.World().redstone.tick(tx, 1) + }) + if actions != 0 { + t.Fatalf("actions = %d, want 0", actions) + } +} + +func TestRedstoneActionOnlyRunsOnPowerChange(t *testing.T) { + sourcePos, actionPos := cube.Pos{0, 64, 0}, cube.Pos{1, 64, 0} + w := Config{Synchronous: true, Blocks: redstoneCancellationTestRegistry()}.New() + defer w.Close() + + actions := 0 + redstoneCancellationActions = &actions + t.Cleanup(func() { + redstoneCancellationActions = nil + }) + runWorld(w, func(tx *Tx) { + tx.SetBlock(sourcePos, redstoneCancellationSource{Power: 15}, nil) + tx.SetBlock(actionPos, redstoneCancellationAction{}, nil) + tx.World().redstone.tick(tx, 1) + tx.World().redstone.invalidate(actionPos, redstoneDirty{cause: RedstoneUpdateCauseBlockUpdate}, tx.Range()) + tx.World().redstone.tick(tx, 2) + }) + if actions != 1 { + t.Fatalf("actions after same-power dirty evaluation = %d, want 1", actions) + } +} + +func TestRedstoneRelayerToSinkDoesNotLosePower(t *testing.T) { + sourcePos, relayerPos, sinkPos := cube.Pos{0, 64, 0}, cube.Pos{1, 64, 0}, cube.Pos{2, 64, 0} + w := Config{Synchronous: true, Blocks: redstoneSignalLossTestRegistry()}.New() + defer w.Close() + + var directPower, sinkPower int + runWorld(w, func(tx *Tx) { + tx.SetBlock(sourcePos, redstoneLossSource{Power: 15}, nil) + tx.SetBlock(relayerPos, redstoneLossRelayer{}, nil) + tx.SetBlock(sinkPos, redstoneLossConsumer{}, nil) + + directPower = tx.RedstonePower(sinkPos) + tx.World().redstone.tick(tx, 1) + if sink, ok := tx.Block(sinkPos).(redstoneLossConsumer); ok { + sinkPower = sink.Power + } + }) + if directPower != 15 { + t.Fatalf("powerFrom through relayer into sink = %d, want 15", directPower) + } + if sinkPower != 15 { + t.Fatalf("graph power through relayer into sink = %d, want 15", sinkPower) + } +} + +func TestRedstoneVerticalRelayerPropagation(t *testing.T) { + tests := []struct { + name string + from cube.Pos + to cube.Pos + block Block + want int + }{ + {name: "glowstone upward", from: cube.Pos{1, 64, 0}, to: cube.Pos{0, 65, 0}, block: redstoneLadderGlowstone{}, want: 14}, + {name: "glowstone downward", from: cube.Pos{0, 65, 0}, to: cube.Pos{1, 64, 0}, block: redstoneLadderGlowstone{}, want: 0}, + {name: "glass downward", from: cube.Pos{0, 65, 0}, to: cube.Pos{1, 64, 0}, block: redstoneLadderGlass{}, want: 14}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + w := Config{Synchronous: true, Blocks: redstoneVerticalRelayerTestRegistry()}.New() + defer w.Close() + + var got int + runWorld(w, func(tx *Tx) { + low, high := cube.Pos{1, 64, 0}, cube.Pos{0, 65, 0} + source := test.from.Side(cube.FaceNorth) + tx.SetBlock(low, redstoneVerticalRelayer{Power: 0}, nil) + tx.SetBlock(high, redstoneVerticalRelayer{Power: 0}, nil) + tx.SetBlock(high.Side(cube.FaceDown), test.block, nil) + tx.SetBlock(source, redstoneVerticalSource{}, nil) + + tx.World().redstone.tick(tx, 1) + got = tx.Block(test.to).(redstoneVerticalRelayer).Power + }) + if got != test.want { + t.Fatalf("propagated power = %d, want %d", got, test.want) + } + }) + } +} + +func TestPlacedRedstoneTorchTurnsOffWhenAttachmentBecomesPowered(t *testing.T) { + w := Config{Synchronous: true, Blocks: redstoneTorchAttachmentTestRegistry()}.New() + defer w.Close() + + torchPos := cube.Pos{1, 64, 0} + attachmentPos := torchPos.Side(cube.FaceWest) + var lit bool + runWorld(w, func(tx *Tx) { + tx.SetBlock(attachmentPos, redstoneSolidBlock{}, nil) + tx.SetBlock(torchPos, redstoneAttachmentTorch{Facing: cube.FaceWest, Lit: true}, nil) + tx.SetBlock(attachmentPos.Side(cube.FaceNorth), redstoneWeakBlockSource{}, nil) + + tx.World().scheduledUpdates.tick(tx, 2) + tx.World().redstone.tick(tx, 2) + lit = tx.Block(torchPos).(redstoneAttachmentTorch).Lit + }) + + if lit { + t.Fatal("torch stayed lit after its attachment block became powered") + } +} + +func TestRedstoneConsumerUpdatesBehindPoweredConductor(t *testing.T) { + w := Config{Synchronous: true, Blocks: redstonePoweredConductorTestRegistry()}.New() + defer w.Close() + + sourcePos, conductorPos, consumerPos := cube.Pos{0, 64, 0}, cube.Pos{1, 64, 0}, cube.Pos{2, 64, 0} + var powered bool + runWorld(w, func(tx *Tx) { + tx.SetBlock(sourcePos, redstoneStrongSource{}, nil) + tx.SetBlock(conductorPos, redstoneSolidBlock{}, nil) + tx.SetBlock(consumerPos, redstoneCancellationConsumer{}, nil) + + tx.World().redstone.tick(tx, 1) + powered = tx.Block(consumerPos).(redstoneCancellationConsumer).Powered + }) + + if !powered { + t.Fatal("consumer behind strongly powered conductor was not powered") + } +} + +func TestWeaklyPoweredConductorActivatesConsumerButNotDust(t *testing.T) { + w := Config{Synchronous: true, Blocks: redstoneWeakConductorTestRegistry()}.New() + defer w.Close() + + sourcePos := cube.Pos{0, 64, 0} + conductorPos := sourcePos.Side(cube.FaceEast) + consumerPos := conductorPos.Side(cube.FaceEast) + dustPos := conductorPos.Side(cube.FaceSouth) + var consumerPowered bool + var dustPower int + runWorld(w, func(tx *Tx) { + tx.SetBlock(sourcePos, redstoneWeakBlockSource{}, nil) + tx.SetBlock(conductorPos, redstoneSolidBlock{}, nil) + tx.SetBlock(consumerPos, redstoneCancellationConsumer{}, nil) + tx.SetBlock(dustPos, redstoneVerticalRelayer{}, nil) + + tx.World().redstone.tick(tx, 1) + consumerPowered = tx.Block(consumerPos).(redstoneCancellationConsumer).Powered + dustPower = tx.Block(dustPos).(redstoneVerticalRelayer).Power + }) + + if !consumerPowered { + t.Fatal("consumer behind weakly powered conductor was not powered") + } + if dustPower != 0 { + t.Fatalf("dust behind weakly powered conductor = %d, want 0", dustPower) + } +} + +func TestDirectSourceDoesNotWeakPowerConductor(t *testing.T) { + w := Config{Synchronous: true, Blocks: redstonePoweredConductorTestRegistry()}.New() + defer w.Close() + + sourcePos, conductorPos, consumerPos := cube.Pos{0, 64, 0}, cube.Pos{1, 64, 0}, cube.Pos{2, 64, 0} + var powered bool + runWorld(w, func(tx *Tx) { + tx.SetBlock(sourcePos, redstoneCancellationSource{Power: 15}, nil) + tx.SetBlock(conductorPos, redstoneSolidBlock{}, nil) + tx.SetBlock(consumerPos, redstoneCancellationConsumer{}, nil) + + tx.World().redstone.tick(tx, 1) + powered = tx.Block(consumerPos).(redstoneCancellationConsumer).Powered + }) + + if powered { + t.Fatal("direct-only source weak-powered a conductor") + } +} + +func TestScheduledTickQueueDuplicateScheduling(t *testing.T) { + tests := []struct { + name string + delays []time.Duration + wantTicks []int64 + }{ + {name: "keeps earlier tick when later tick is scheduled", delays: []time.Duration{time.Second / 20, time.Second / 10}, wantTicks: []int64{101, 102}}, + {name: "ignores earlier tick behind later tick", delays: []time.Duration{time.Second / 10, time.Second / 20}, wantTicks: []int64{102}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + queue := newScheduledTickQueue(100) + pos := cube.Pos{8, 64, 8} + b := scheduledTickTestBlock{} + + for _, delay := range test.delays { + queue.schedule(DefaultBlockRegistry, pos, b, delay) + } + + index := scheduledTickIndex{pos: pos, hash: DefaultBlockRegistry.BlockHash(b)} + if got, want := queue.furthestTicks[index], int64(102); got != want { + t.Fatalf("furthest tick = %d, want %d", got, want) + } + ticks := queue.fromChunk(chunkPosFromBlockPos(pos)) + if len(ticks) != len(test.wantTicks) { + t.Fatalf("active ticks = %v, want %v", ticks, test.wantTicks) + } + for i, want := range test.wantTicks { + if got := ticks[i].t; got != want { + t.Fatalf("active tick %d = %d, want %d; ticks=%v", i, got, want, ticks) + } + } + }) + } +} + +func TestScheduledTickQueueRemoveChunkClearsSchedule(t *testing.T) { + queue := newScheduledTickQueue(100) + pos := cube.Pos{8, 64, 8} + b := scheduledTickTestBlock{} + + queue.schedule(DefaultBlockRegistry, pos, b, time.Second) + queue.removeChunk(chunkPosFromBlockPos(pos)) + + if len(queue.ticks) != 0 { + t.Fatalf("ticks after removeChunk = %v, want empty", queue.ticks) + } + if len(queue.furthestTicks) != 0 { + t.Fatalf("furthest ticks after removeChunk = %v, want empty", queue.furthestTicks) + } +} + +func TestScheduledTickQueueCanRescheduleWhileCurrentTickIsDue(t *testing.T) { + queue := newScheduledTickQueue(100) + pos := cube.Pos{8, 64, 8} + b := scheduledTickTestBlock{} + index := scheduledTickIndex{pos: pos, hash: DefaultBlockRegistry.BlockHash(b)} + queue.furthestTicks[index] = 100 + + queue.schedule(DefaultBlockRegistry, pos, b, time.Second/2) + if got, want := queue.furthestTicks[index], int64(110); got != want { + t.Fatalf("rescheduled tick = %d, want %d", got, want) + } +} + +func TestScheduledTickQueueExecutesEarlierDueTickBeforeLaterTick(t *testing.T) { + registry := scheduledTickTestRegistry() + w := Config{Synchronous: true, Blocks: registry}.New() + defer w.Close() + + queue := newScheduledTickQueue(100) + pos := cube.Pos{8, 64, 8} + b := scheduledTickTestBlock{} + index := scheduledTickIndex{pos: pos, hash: registry.BlockHash(b)} + + ticks := 0 + scheduledTickTestBlockTicks = &ticks + t.Cleanup(func() { + scheduledTickTestBlockTicks = nil + }) + + var ( + ticksAfterFirst, ticksAfterSecond int + activeAfterFirst, activeAfterSecond []scheduledTick + furthestAfterFirst int64 + hasFurthestAfterFirst bool + ) + runWorld(w, func(tx *Tx) { + tx.SetBlock(pos, b, nil) + queue.schedule(registry, pos, b, time.Second/20) + queue.schedule(registry, pos, b, time.Second/10) + + queue.tick(tx, 101) + ticksAfterFirst = ticks + activeAfterFirst = queue.fromChunk(chunkPosFromBlockPos(pos)) + furthestAfterFirst, hasFurthestAfterFirst = queue.furthestTicks[index] + + queue.tick(tx, 102) + ticksAfterSecond = ticks + activeAfterSecond = queue.fromChunk(chunkPosFromBlockPos(pos)) + }) + if ticksAfterFirst != 1 { + t.Fatalf("earlier due tick executed %d time(s), want 1", ticksAfterFirst) + } + if !hasFurthestAfterFirst || furthestAfterFirst != 102 { + t.Fatalf("furthest tick after earlier tick = %d, %t; want 102, true", furthestAfterFirst, hasFurthestAfterFirst) + } + if len(activeAfterFirst) != 1 || activeAfterFirst[0].t != 102 { + t.Fatalf("active ticks after earlier tick = %v, want only tick 102", activeAfterFirst) + } + if ticksAfterSecond != 2 { + t.Fatalf("later scheduled tick execution count = %d, want 2", ticksAfterSecond) + } + if len(activeAfterSecond) != 0 { + t.Fatalf("active ticks after later tick = %v, want empty", activeAfterSecond) + } +} + +type scheduledTickTestBlock struct{} + +// Test counters below are package-level because block instances are registry values; tests using them must stay serial. +var scheduledTickTestBlockTicks *int + +func (scheduledTickTestBlock) ScheduledTick(cube.Pos, *Tx, *rand.Rand) { + if scheduledTickTestBlockTicks != nil { + (*scheduledTickTestBlockTicks)++ + } +} +func (scheduledTickTestBlock) EncodeBlock() (string, map[string]any) { + return "test:scheduled_tick", nil +} +func (scheduledTickTestBlock) Hash() (uint64, uint64) { return 1 << 40, 0 } +func (scheduledTickTestBlock) Model() BlockModel { return nil } + +func scheduledTickTestRegistry() BlockRegistry { + registry := NewBlockRegistry() + registry.RegisterBlockState(BlockState{Name: "test:scheduled_tick", Properties: map[string]any{}}) + registry.RegisterBlock(scheduledTickTestBlock{}) + return registry +} + +type redstoneNeighbourOrderTestBlock struct { + neighbours []cube.Pos +} + +func (b redstoneNeighbourOrderTestBlock) RedstoneRelayerNeighbours(cube.Pos, *Tx) []cube.Pos { + return slices.Clone(b.neighbours) +} +func (redstoneNeighbourOrderTestBlock) EncodeBlock() (string, map[string]any) { + return "test:redstone_neighbour_order", nil +} +func (redstoneNeighbourOrderTestBlock) Hash() (uint64, uint64) { return 1 << 41, 0 } +func (redstoneNeighbourOrderTestBlock) Model() BlockModel { return nil } + +type redstoneCancellationHandler struct { + NopHandler + cancel map[cube.Pos]struct{} +} + +func (h *redstoneCancellationHandler) HandleRedstoneUpdate(ctx *Context, update RedstoneUpdate) { + if _, ok := h.cancel[update.Pos]; ok { + ctx.Cancel() + } +} + +type redstoneRecordingHandler struct { + NopHandler + pos cube.Pos + updates []RedstoneUpdate +} + +func (h *redstoneRecordingHandler) HandleRedstoneUpdate(_ *Context, update RedstoneUpdate) { + if update.Pos == h.pos { + h.updates = append(h.updates, update) + } +} + +var redstoneCancellationActions *int + +func redstoneCancellationTestRegistry() BlockRegistry { + registry := NewBlockRegistry() + for _, power := range []int32{0, 15} { + registry.RegisterBlockState(BlockState{Name: "test:redstone_source", Properties: map[string]any{"power": power}}) + registry.RegisterBlock(redstoneCancellationSource{Power: int(power)}) + } + for _, powered := range []bool{false, true} { + registry.RegisterBlockState(BlockState{Name: "test:redstone_consumer", Properties: map[string]any{"powered": powered}}) + registry.RegisterBlock(redstoneCancellationConsumer{Powered: powered}) + } + registry.RegisterBlockState(BlockState{Name: "test:redstone_action", Properties: map[string]any{}}) + registry.RegisterBlock(redstoneCancellationAction{}) + return registry +} + +type redstoneCancellationSource struct { + Power int +} + +func (b redstoneCancellationSource) RedstonePower(cube.Pos, *Tx, cube.Face) int { + return b.Power +} +func (b redstoneCancellationSource) EncodeBlock() (string, map[string]any) { + return "test:redstone_source", map[string]any{"power": int32(b.Power)} +} +func (b redstoneCancellationSource) Hash() (uint64, uint64) { + return 1 << 42, uint64(b.Power) +} +func (redstoneCancellationSource) Model() BlockModel { return redstoneCancellationModel{} } + +func redstoneRecursiveSourceTestRegistry() BlockRegistry { + registry := NewBlockRegistry() + registry.RegisterBlockState(BlockState{Name: "test:redstone_recursive_source", Properties: map[string]any{}}) + registry.RegisterBlock(redstoneRecursiveSource{}) + return registry +} + +type redstoneRecursiveSource struct { + Target cube.Pos +} + +func (b redstoneRecursiveSource) RedstonePower(_ cube.Pos, tx *Tx, _ cube.Face) int { + return tx.RedstonePower(b.Target) +} +func (redstoneRecursiveSource) EncodeBlock() (string, map[string]any) { + return "test:redstone_recursive_source", nil +} +func (redstoneRecursiveSource) Hash() (uint64, uint64) { return 1 << 57, 0 } +func (redstoneRecursiveSource) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneCancellationConsumer struct { + Powered bool +} + +func (b redstoneCancellationConsumer) RedstonePowerUpdate(_ cube.Pos, _ *Tx, power int) (Block, bool) { + powered := power > 0 + if b.Powered == powered { + return b, false + } + b.Powered = powered + return b, true +} +func (b redstoneCancellationConsumer) EncodeBlock() (string, map[string]any) { + return "test:redstone_consumer", map[string]any{"powered": b.Powered} +} +func (b redstoneCancellationConsumer) Hash() (uint64, uint64) { + if b.Powered { + return 1 << 43, 1 + } + return 1 << 43, 0 +} +func (redstoneCancellationConsumer) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneCancellationAction struct{} + +func (redstoneCancellationAction) RedstonePowerAction(cube.Pos, *Tx, int, int) { + if redstoneCancellationActions != nil { + (*redstoneCancellationActions)++ + } +} +func (redstoneCancellationAction) EncodeBlock() (string, map[string]any) { + return "test:redstone_action", nil +} +func (redstoneCancellationAction) Hash() (uint64, uint64) { return 1 << 44, 0 } +func (redstoneCancellationAction) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneCancellationModel struct{} + +func (redstoneCancellationModel) BBox(cube.Pos, BlockSource) []cube.BBox { return nil } +func (redstoneCancellationModel) FaceSolid(cube.Pos, cube.Face, BlockSource) bool { + return false +} + +func redstoneSignalLossTestRegistry() BlockRegistry { + registry := NewBlockRegistry() + registry.RegisterBlockState(BlockState{Name: "test:redstone_loss_source", Properties: map[string]any{"power": int32(15)}}) + registry.RegisterBlock(redstoneLossSource{Power: 15}) + registry.RegisterBlockState(BlockState{Name: "test:redstone_loss_relayer", Properties: map[string]any{}}) + registry.RegisterBlock(redstoneLossRelayer{}) + for power := int32(0); power <= 15; power++ { + registry.RegisterBlockState(BlockState{Name: "test:redstone_loss_consumer", Properties: map[string]any{"power": power}}) + registry.RegisterBlock(redstoneLossConsumer{Power: int(power)}) + } + return registry +} + +type redstoneLossSource struct { + Power int +} + +func (b redstoneLossSource) RedstonePower(cube.Pos, *Tx, cube.Face) int { + return b.Power +} +func (b redstoneLossSource) EncodeBlock() (string, map[string]any) { + return "test:redstone_loss_source", map[string]any{"power": int32(b.Power)} +} +func (b redstoneLossSource) Hash() (uint64, uint64) { + return 1 << 45, uint64(b.Power) +} +func (redstoneLossSource) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneLossRelayer struct{} + +func (redstoneLossRelayer) RedstoneSignalLoss(cube.Pos, *Tx) int { + return 1 +} +func (redstoneLossRelayer) EncodeBlock() (string, map[string]any) { + return "test:redstone_loss_relayer", nil +} +func (redstoneLossRelayer) Hash() (uint64, uint64) { return 1 << 46, 0 } +func (redstoneLossRelayer) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneLossConsumer struct { + Power int +} + +func (b redstoneLossConsumer) RedstonePowerUpdate(_ cube.Pos, _ *Tx, power int) (Block, bool) { + if b.Power == power { + return b, false + } + b.Power = power + return b, true +} +func (b redstoneLossConsumer) EncodeBlock() (string, map[string]any) { + return "test:redstone_loss_consumer", map[string]any{"power": int32(b.Power)} +} +func (b redstoneLossConsumer) Hash() (uint64, uint64) { + return 1 << 47, uint64(b.Power) +} +func (redstoneLossConsumer) Model() BlockModel { return redstoneCancellationModel{} } + +func redstoneVerticalRelayerTestRegistry() BlockRegistry { + registry := NewBlockRegistry() + registry.RegisterBlockState(BlockState{Name: "test:redstone_vertical_source", Properties: map[string]any{}}) + registry.RegisterBlock(redstoneVerticalSource{}) + for power := int32(0); power <= 15; power++ { + registry.RegisterBlockState(BlockState{Name: "test:redstone_vertical_relayer", Properties: map[string]any{"power": power}}) + registry.RegisterBlock(redstoneVerticalRelayer{Power: int(power)}) + } + registry.RegisterBlockState(BlockState{Name: "test:redstone_ladder_glowstone", Properties: map[string]any{}}) + registry.RegisterBlock(redstoneLadderGlowstone{}) + registry.RegisterBlockState(BlockState{Name: "test:redstone_ladder_glass", Properties: map[string]any{}}) + registry.RegisterBlock(redstoneLadderGlass{}) + return registry +} + +func redstoneTorchAttachmentTestRegistry() BlockRegistry { + registry := redstoneVerticalRelayerTestRegistry() + for _, lit := range []bool{false, true} { + registry.RegisterBlockState(BlockState{Name: "test:redstone_attachment_torch", Properties: map[string]any{"lit": lit}}) + registry.RegisterBlock(redstoneAttachmentTorch{Facing: cube.FaceWest, Lit: lit}) + } + registry.RegisterBlockState(BlockState{Name: "test:redstone_weak_block_source", Properties: map[string]any{}}) + registry.RegisterBlock(redstoneWeakBlockSource{}) + registry.RegisterBlockState(BlockState{Name: "test:solid_block", Properties: map[string]any{}}) + registry.RegisterBlock(redstoneSolidBlock{}) + return registry +} + +func redstonePoweredConductorTestRegistry() BlockRegistry { + registry := redstoneCancellationTestRegistry() + registry.RegisterBlockState(BlockState{Name: "test:redstone_strong_source", Properties: map[string]any{}}) + registry.RegisterBlock(redstoneStrongSource{}) + registry.RegisterBlockState(BlockState{Name: "test:solid_block", Properties: map[string]any{}}) + registry.RegisterBlock(redstoneSolidBlock{}) + return registry +} + +func redstoneWeakConductorTestRegistry() BlockRegistry { + registry := redstoneVerticalRelayerTestRegistry() + for _, powered := range []bool{false, true} { + registry.RegisterBlockState(BlockState{Name: "test:redstone_consumer", Properties: map[string]any{"powered": powered}}) + registry.RegisterBlock(redstoneCancellationConsumer{Powered: powered}) + } + registry.RegisterBlockState(BlockState{Name: "test:redstone_weak_block_source", Properties: map[string]any{}}) + registry.RegisterBlock(redstoneWeakBlockSource{}) + registry.RegisterBlockState(BlockState{Name: "test:solid_block", Properties: map[string]any{}}) + registry.RegisterBlock(redstoneSolidBlock{}) + return registry +} + +type redstoneVerticalSource struct{} + +func (redstoneVerticalSource) RedstonePower(cube.Pos, *Tx, cube.Face) int { return 15 } +func (redstoneVerticalSource) EncodeBlock() (string, map[string]any) { + return "test:redstone_vertical_source", nil +} +func (redstoneVerticalSource) Hash() (uint64, uint64) { return 1 << 52, 0 } +func (redstoneVerticalSource) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneStrongSource struct{} + +func (redstoneStrongSource) RedstonePower(cube.Pos, *Tx, cube.Face) int { return 15 } +func (redstoneStrongSource) RedstoneStrongPower(cube.Pos, *Tx, cube.Face) int { + return 15 +} +func (redstoneStrongSource) EncodeBlock() (string, map[string]any) { + return "test:redstone_strong_source", nil +} +func (redstoneStrongSource) Hash() (uint64, uint64) { return 1 << 54, 0 } +func (redstoneStrongSource) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneWeakBlockSource struct{} + +func (redstoneWeakBlockSource) RedstonePower(cube.Pos, *Tx, cube.Face) int { return 15 } +func (redstoneWeakBlockSource) RedstoneWeaklyPowersBlocks() bool { return true } +func (redstoneWeakBlockSource) EncodeBlock() (string, map[string]any) { + return "test:redstone_weak_block_source", nil +} +func (redstoneWeakBlockSource) Hash() (uint64, uint64) { return 1 << 55, 0 } +func (redstoneWeakBlockSource) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneVerticalRelayer struct { + Power int +} + +func (b redstoneVerticalRelayer) RedstonePower(cube.Pos, *Tx, cube.Face) int { + return b.Power +} +func (redstoneVerticalRelayer) RedstoneSignalLoss(cube.Pos, *Tx) int { + return 1 +} +func (b redstoneVerticalRelayer) RedstonePowerUpdate(_ cube.Pos, _ *Tx, power int) (Block, bool) { + if b.Power == power { + return b, false + } + b.Power = power + return b, true +} +func (b redstoneVerticalRelayer) RedstoneRelayerNeighbours(pos cube.Pos, tx *Tx) []cube.Pos { + neighbours := make([]cube.Pos, 0, 2) + for _, side := range []cube.Pos{pos.Add(cube.Pos{-1, 1, 0}), pos.Add(cube.Pos{1, -1, 0})} { + if side.OutOfBounds(tx.Range()) { + continue + } + if side[1] < pos[1] && !redstoneTestCanTransmitDown(tx, pos) { + continue + } + neighbours = append(neighbours, side) + } + return neighbours +} +func (b redstoneVerticalRelayer) EncodeBlock() (string, map[string]any) { + return "test:redstone_vertical_relayer", map[string]any{"power": int32(b.Power)} +} +func (b redstoneVerticalRelayer) Hash() (uint64, uint64) { + return 1 << 49, uint64(b.Power) +} +func (redstoneVerticalRelayer) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneAttachmentTorch struct { + Facing cube.Face + Lit bool +} + +func (t redstoneAttachmentTorch) RedstonePower(cube.Pos, *Tx, cube.Face) int { + if t.Lit { + return 15 + } + return 0 +} +func (t redstoneAttachmentTorch) RedstonePowerAction(pos cube.Pos, tx *Tx, _, _ int) { + if t.Lit == !t.attachmentPowered(pos, tx) { + return + } + t.Lit = !t.Lit + tx.SetBlock(pos, t, nil) +} +func (t redstoneAttachmentTorch) attachmentPowered(pos cube.Pos, tx *Tx) bool { + attached := pos.Side(t.Facing) + attachedBlock := tx.Block(attached) + if !redstoneStrongPowerConductor(attached, attachedBlock, tx, t.Facing.Opposite()) { + return false + } + return tx.RedstoneConductivePower(attached) > 0 +} +func (t redstoneAttachmentTorch) EncodeBlock() (string, map[string]any) { + return "test:redstone_attachment_torch", map[string]any{"lit": t.Lit} +} +func (t redstoneAttachmentTorch) Hash() (uint64, uint64) { + if t.Lit { + return 1 << 53, 1 + } + return 1 << 53, 0 +} +func (redstoneAttachmentTorch) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneLadderGlowstone struct{} + +func (redstoneLadderGlowstone) RedstoneNonConductive() {} +func (redstoneLadderGlowstone) EncodeBlock() (string, map[string]any) { + return "test:redstone_ladder_glowstone", nil +} +func (redstoneLadderGlowstone) Hash() (uint64, uint64) { return 1 << 50, 0 } +func (redstoneLadderGlowstone) Model() BlockModel { return redstoneSolidModel{} } + +type redstoneLadderGlass struct{} + +func (redstoneLadderGlass) LightDiffusionLevel() uint8 { return 0 } +func (redstoneLadderGlass) EncodeBlock() (string, map[string]any) { + return "test:redstone_ladder_glass", nil +} +func (redstoneLadderGlass) Hash() (uint64, uint64) { return 1 << 51, 0 } +func (redstoneLadderGlass) Model() BlockModel { return redstoneSolidModel{} } + +func redstoneTestCanTransmitDown(tx *Tx, pos cube.Pos) bool { + supportPos := pos.Side(cube.FaceDown) + support, ok := tx.World().blockLoaded(supportPos) + if !ok || !support.Model().FaceSolid(supportPos, cube.FaceUp, tx) { + return false + } + if _, ok := support.(RedstoneNonConductive); ok { + return false + } + return true +} + +type redstoneSolidBlock struct{} + +func (redstoneSolidBlock) EncodeBlock() (string, map[string]any) { return "test:solid_block", nil } +func (redstoneSolidBlock) Hash() (uint64, uint64) { return 1 << 48, 0 } +func (redstoneSolidBlock) Model() BlockModel { return redstoneSolidModel{} } + +type redstoneNonConductiveSolidBlock struct { + redstoneSolidBlock +} + +func (redstoneNonConductiveSolidBlock) RedstoneNonConductive() {} +func (redstoneNonConductiveSolidBlock) EncodeBlock() (string, map[string]any) { + return "test:non_conductive_solid_block", nil +} +func (redstoneNonConductiveSolidBlock) Hash() (uint64, uint64) { return 1 << 56, 0 } + +type redstoneSolidModel struct{} + +func (redstoneSolidModel) BBox(cube.Pos, BlockSource) []cube.BBox { return nil } +func (redstoneSolidModel) FaceSolid(cube.Pos, cube.Face, BlockSource) bool { + return true +} diff --git a/server/world/settings.go b/server/world/settings.go index 085a5c280d..10ccfa4190 100644 --- a/server/world/settings.go +++ b/server/world/settings.go @@ -1,6 +1,7 @@ package world import ( + "math/rand/v2" "sync" "sync/atomic" @@ -54,6 +55,8 @@ func defaultSettings() *Settings { Difficulty: DifficultyNormal, TimeCycle: true, WeatherCycle: true, + RainTime: int64(rand.IntN(8400)+600) * 20, + ThunderTime: int64(rand.IntN(8400)+600) * 20, TickRange: 6, } } diff --git a/server/world/sound/block.go b/server/world/sound/block.go index fc62f59ab1..02cfad5bbc 100644 --- a/server/world/sound/block.go +++ b/server/world/sound/block.go @@ -60,6 +60,12 @@ type BarrelClose struct{ sound } // Deny is a sound played when a block is placed or broken above a 'Deny' block from Education edition. type Deny struct{ sound } +// ShulkerBoxOpen is a sound played when a shulker box is opened. +type ShulkerBoxOpen struct{ sound } + +// ShulkerBoxClose is a sound played when a shulker box is closed. +type ShulkerBoxClose struct{ sound } + // DoorOpen is a sound played when a door is opened. type DoorOpen struct { // Block is the block which is being opened, for which a sound should be played. The sound played depends on the @@ -216,6 +222,12 @@ type DecoratedPotInserted struct { // DecoratedPotInsertFailed is a sound played when an item fails to be inserted into a decorated pot. type DecoratedPotInsertFailed struct{ sound } +// EnderEyePlaced is a sound played when an eye of ender is placed into an end portal frame. +type EnderEyePlaced struct{ sound } + +// EndPortalCreated is a sound played when a complete end portal frame ring is activated. +type EndPortalCreated struct{ sound } + // sound implements the world.Sound interface. type sound struct{} diff --git a/server/world/sound/custom.go b/server/world/sound/custom.go new file mode 100644 index 0000000000..27a1f6c82f --- /dev/null +++ b/server/world/sound/custom.go @@ -0,0 +1,14 @@ +package sound + +// Custom is a sound identified by name. It may be used to play sounds defined +// by a resource pack. +type Custom struct { + // Name is the identifier of the sound. + Name string + // Volume is the volume of the sound. + Volume float64 + // Pitch is the pitch of the sound. + Pitch float64 + + sound +} diff --git a/server/world/task.go b/server/world/task.go new file mode 100644 index 0000000000..e954ccff8c --- /dev/null +++ b/server/world/task.go @@ -0,0 +1,419 @@ +package world + +import ( + "context" + "errors" + "fmt" + "runtime/debug" + "sync" + "sync/atomic" + "time" +) + +var ( + // ErrWorldClosed means the task's world closed before the task could run. + ErrWorldClosed = errors.New("world: world closed") + // ErrEntityClosed means the entity closed before the task could run. + ErrEntityClosed = errors.New("world: entity closed") + // ErrEntityNotInWorld means an entity was not in the transaction's world. + ErrEntityNotInWorld = errors.New("world: entity not in this world") + // ErrTaskCancelled means the task was cancelled before it started. + ErrTaskCancelled = errors.New("world: scheduled task cancelled") + // ErrTaskPanicked means the task's callback panicked; see PanicError. + ErrTaskPanicked = errors.New("world: scheduled task panicked") + // ErrEntityType means the entity no longer had the type expected by a + // typed EntityRef when the task ran. + ErrEntityType = errors.New("world: unexpected entity type") +) + +// PanicError is the Task error for a fire-and-forget callback that panicked. It +// matches errors.Is(err, ErrTaskPanicked) and keeps the original panic value +// and stack. Synchronous Call functions re-panic with Value automatically. +type PanicError struct { + // Value is the recovered panic value. + Value any + // Stack is the stack of the panicking goroutine. + Stack []byte +} + +// Error implements the error interface. +func (e *PanicError) Error() string { + return fmt.Sprintf("world: scheduled task panicked: %v", e.Value) +} + +// Unwrap returns ErrTaskPanicked so errors.Is works. +func (e *PanicError) Unwrap() error { return ErrTaskPanicked } + +// rethrowPanic re-panics with the original panic value if err wraps a +// *PanicError. It does nothing for any other error, including nil. +func rethrowPanic(err error) { + if pe, ok := errors.AsType[*PanicError](err); ok { + panic(pe.Value) + } +} + +// callContext normalises a possibly-nil caller context and reports whether it +// was cancelled before any work was scheduled. +func callContext(ctx context.Context) (context.Context, error) { + if ctx == nil { + ctx = context.Background() + } + return ctx, ctx.Err() +} + +// executeWithRecovery runs f, recovering and logging any panic through w. +func executeWithRecovery(w *World, f func() error) (err error) { + defer func() { + if r := recover(); r != nil { + panicErr := &PanicError{Value: r, Stack: debug.Stack()} + w.conf.Log.Error("scheduled task panicked", "panic", panicErr.Value, "stack", string(panicErr.Stack)) + err = panicErr + } + }() + return f() +} + +// awaitTask waits for a task to complete or for cancellation to stop a pending +// task, returning the result stored by the callback through the result pointer. +// Once a callback starts, awaitTask waits for it to finish so synchronous calls +// preserve their result and panic semantics. +func awaitTask[T any](ctx context.Context, task *Task, result *T) (T, error) { + var zero T + completed := func() (T, error) { + if err := task.Err(); err != nil { + rethrowPanic(err) + return zero, err + } + return *result, nil + } + select { + case <-task.Done(): + return completed() + case <-ctx.Done(): + if task.Cancel() { + return zero, ctx.Err() + } + <-task.Done() + return completed() + } +} + +const ( + taskPending int32 = iota + taskRunning + taskDone + taskCancelled +) + +// Task tracks work scheduled onto a world or entity owner. Tasks are usually +// fire-and-forget: Done, Err and Wait are for code running off the owner, +// such as tests and shutdown paths. A zero-value Task behaves like a cancelled +// task. +type Task struct { + done chan struct{} + state atomic.Int32 + + errMu sync.Mutex + err error + + cancelMu sync.Mutex + onCancel func() +} + +// newTask returns a pending Task with an open done channel. +func newTask() *Task { + return &Task{done: make(chan struct{})} +} + +// NewFinishedTask returns a Task that already completed with err. +func NewFinishedTask(err error) *Task { + t := newTask() + t.failIfPending(err) + return t +} + +// closedDone is the Done channel returned for nil tasks. +var closedDone = func() <-chan struct{} { + c := make(chan struct{}) + close(c) + return c +}() + +// Done returns a channel that closes once the task has run, failed or been +// cancelled. +func (t *Task) Done() <-chan struct{} { + if t == nil || t.done == nil { + return closedDone + } + return t.done +} + +// Err returns the task's error, or nil while the task is still pending or +// after it succeeded. +func (t *Task) Err() error { + if t == nil || t.done == nil { + return ErrTaskCancelled + } + select { + case <-t.done: + t.errMu.Lock() + defer t.errMu.Unlock() + return t.err + default: + return nil + } +} + +// Wait blocks until the task finishes or ctx is cancelled. Never call it from +// a callback running on the same owner: that blocks the owner on itself, the +// deadlock Do exists to avoid. +func (t *Task) Wait(ctx context.Context) error { + if ctx == nil { + ctx = context.Background() + } + if t == nil || t.done == nil { + return ErrTaskCancelled + } + select { + case <-t.done: + return t.Err() + case <-ctx.Done(): + return ctx.Err() + } +} + +// OnDone calls f with the task's error on a fresh goroutine once the task +// completes. Nil and zero-value tasks call f with ErrTaskCancelled. The +// callback always runs on a fresh goroutine, including for those tasks. +func (t *Task) OnDone(f func(err error)) { + if t == nil || t.done == nil { + go f(ErrTaskCancelled) + return + } + go func() { + <-t.done + f(t.Err()) + }() +} + +// Cancel stops a task that has not started yet, reporting whether it did: +// true means the task will never run. +func (t *Task) Cancel() bool { + if t == nil || t.done == nil || !t.state.CompareAndSwap(taskPending, taskCancelled) { + return false + } + t.setErr(ErrTaskCancelled) + close(t.done) + t.runCancel() + return true +} + +// begin moves the task from pending to running, reporting whether it did. +func (t *Task) begin() bool { + return t != nil && t.state.CompareAndSwap(taskPending, taskRunning) +} + +// failIfPending completes a still-pending task with err, reporting whether it +// did. +func (t *Task) failIfPending(err error) bool { + if t == nil || !t.state.CompareAndSwap(taskPending, taskRunning) { + return false + } + t.finish(err) + return true +} + +// finish completes the task, storing err and closing the done channel. +func (t *Task) finish(err error) { + t.setErr(err) + t.state.Store(taskDone) + close(t.done) +} + +func (t *Task) setErr(err error) { + t.errMu.Lock() + t.err = err + t.errMu.Unlock() +} + +func (t *Task) pending() bool { + return t != nil && t.state.Load() == taskPending +} + +// setCancel registers a function to run if the task is cancelled. If the +// task is already cancelled when setCancel is called, f runs immediately. +func (t *Task) setCancel(f func()) { + if t == nil || f == nil { + return + } + t.cancelMu.Lock() + cancelled := t.state.Load() == taskCancelled + if !cancelled { + t.onCancel = f + } + t.cancelMu.Unlock() + if cancelled { + f() + } +} + +// runCancel invokes the registered cancel function, if any. +func (t *Task) runCancel() { + t.cancelMu.Lock() + f := t.onCancel + t.cancelMu.Unlock() + if f != nil { + f() + } +} + +// Do schedules f to run on the world owner and returns immediately; it is +// safe to call from anywhere, including owner callbacks. Tasks usually run in +// submission order, but ordering is not guaranteed between tasks scheduled +// while the queue is saturated. Use one task or Tx.Defer for strictly ordered +// work. On a synchronous World, f runs before Do returns. +func (w *World) Do(f func(tx *Tx)) *Task { + return w.scheduleTask(newTask(), func(tx *Tx) error { + f(tx) + return nil + }) +} + +// DoAfter schedules f to run on the world owner after delay. Cancelling the +// task before delay elapses stops f from being queued at all. +func (w *World) DoAfter(delay time.Duration, f func(tx *Tx)) *Task { + t := newTask() + run := func(tx *Tx) error { + f(tx) + return nil + } + if delay <= 0 { + return w.scheduleTask(t, run) + } + if w == nil || w.queue == nil || w.closed.Load() { + t.failIfPending(ErrWorldClosed) + return t + } + go func() { + timer := time.NewTimer(delay) + defer timer.Stop() + select { + case <-timer.C: + w.scheduleTask(t, run) + case <-t.Done(): + case <-w.closeStarted: + t.failIfPending(ErrWorldClosed) + case <-w.closing: + t.failIfPending(ErrWorldClosed) + case <-w.queueClosing: + t.failIfPending(ErrWorldClosed) + } + }() + return t +} + +// Call runs f on w's owner and waits for its typed result. It is for +// off-owner code such as tests, startup and background goroutines; if you +// already have a *world.Tx, just use it directly. Calling it from the +// owner itself (any scheduled callback or Handler event) deadlocks. If f +// panics, Call re-panics with the original value on the waiting goroutine after +// logging the original stack through the World's Logger. Context cancellation +// stops pending work, but Call waits for a callback that has already started. +func Call[T any](ctx context.Context, w *World, f func(tx *Tx) (T, error)) (T, error) { + var zero T + ctx, err := callContext(ctx) + if err != nil { + return zero, err + } + var result T + task := w.scheduleTask(newTask(), func(tx *Tx) error { + var err error + result, err = f(tx) + return err + }) + return awaitTask(ctx, task, &result) +} + +// CallEntity runs f with the EntityHandle's entity on its current world owner +// and waits for the typed result. Off-owner code only, like Call. If f panics, +// CallEntity re-panics with the original value on the waiting goroutine. +func CallEntity[T any](ctx context.Context, h *EntityHandle, f func(tx *Tx, e Entity) (T, error)) (T, error) { + return CallRef(ctx, NewEntityRef[Entity](h), f) +} + +// scheduleTask enqueues a scheduledTransaction on the world's owner queue, +// handing a full queue off to a helper goroutine rather than blocking. +func (w *World) scheduleTask(task *Task, f func(tx *Tx) error) *Task { + if task == nil { + task = newTask() + } + if w == nil || w.queue == nil || w.closed.Load() { + task.failIfPending(ErrWorldClosed) + return task + } + if !task.pending() { + return task + } + st := scheduledTransaction{task: task, f: f} + w.scheduleMu.Lock() + if w.closed.Load() { + w.scheduleMu.Unlock() + task.failIfPending(ErrWorldClosed) + return task + } + if w.conf.Synchronous { + w.scheduleMu.Unlock() + st.Run(w) + return task + } + select { + case <-w.closing: + task.failIfPending(ErrWorldClosed) + case <-w.queueClosing: + task.failIfPending(ErrWorldClosed) + case w.queue <- st: + default: + w.scheduling.Add(1) + go w.queueScheduled(st) + } + w.scheduleMu.Unlock() + return task +} + +// queueScheduled retries enqueuing st once the queue, full at schedule time, +// has room, failing the task if the world closes first. +func (w *World) queueScheduled(st scheduledTransaction) { + defer w.scheduling.Done() + if w.closed.Load() { + st.task.failIfPending(ErrWorldClosed) + return + } + select { + case <-w.closing: + st.task.failIfPending(ErrWorldClosed) + case <-w.queueClosing: + st.task.failIfPending(ErrWorldClosed) + case <-st.task.Done(): + case w.queue <- st: + } +} + +// scheduledTransaction is a queued task from Do, DoAfter or Tx.Defer: it +// runs the callback with panic recovery, drains deferred work and finishes the +// task. +type scheduledTransaction struct { + task *Task + f func(tx *Tx) error +} + +// Run executes the scheduled callback on the world goroutine. +func (st scheduledTransaction) Run(w *World) { + if !st.task.begin() { + return + } + tx := newTx(w) + err := executeWithRecovery(w, func() error { return st.f(tx) }) + tx.close() + tx.runDeferred() + st.task.finish(err) +} diff --git a/server/world/task_test.go b/server/world/task_test.go new file mode 100644 index 0000000000..44b6b98352 --- /dev/null +++ b/server/world/task_test.go @@ -0,0 +1,394 @@ +package world + +import ( + "context" + "errors" + "io" + "log/slog" + "sync/atomic" + "testing" + "time" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl64" +) + +func TestCallRethrowsPanic(t *testing.T) { + w := Config{Log: slog.New(slog.NewTextHandler(io.Discard, nil))}.New() + t.Cleanup(func() { _ = w.Close() }) + + panicValue := &struct{ message string }{"call panic"} + defer func() { + if recovered := recover(); recovered != panicValue { + t.Fatalf("Call panic = %v, want original value %v", recovered, panicValue) + } + }() + _, _ = Call(context.Background(), w, func(*Tx) (struct{}, error) { + panic(panicValue) + }) +} + +func TestCallRefRethrowsPanic(t *testing.T) { + w := Config{Log: slog.New(slog.NewTextHandler(io.Discard, nil))}.New() + t.Cleanup(func() { _ = w.Close() }) + h := NewEntity(taskTestEntityType{}, taskTestEntityConfig{}) + if err := w.Do(func(tx *Tx) { tx.AddEntity(h) }).Wait(context.Background()); err != nil { + t.Fatalf("add entity: %v", err) + } + + panicValue := &struct{ message string }{"call ref panic"} + defer func() { + if recovered := recover(); recovered != panicValue { + t.Fatalf("CallRef panic = %v, want original value %v", recovered, panicValue) + } + }() + _, _ = CallRef(context.Background(), NewEntityRef[Entity](h), func(*Tx, Entity) (struct{}, error) { + panic(panicValue) + }) +} + +func TestCallRethrowsPanicWhenCancellationLoses(t *testing.T) { + w := Config{Log: slog.New(slog.NewTextHandler(io.Discard, nil))}.New() + t.Cleanup(func() { _ = w.Close() }) + ctx, cancel := context.WithCancel(context.Background()) + started := make(chan struct{}) + release := make(chan struct{}) + panicValue := &struct{ message string }{"call panic after cancellation"} + type outcome struct { + panicValue any + err error + } + result := make(chan outcome, 1) + go func() { + var out outcome + defer func() { + out.panicValue = recover() + result <- out + }() + _, out.err = Call(ctx, w, func(*Tx) (struct{}, error) { + close(started) + <-release + panic(panicValue) + }) + }() + + <-started + cancel() + select { + case out := <-result: + close(release) + t.Fatalf("Call returned before running callback completed: panic = %v, err = %v", out.panicValue, out.err) + case <-time.After(20 * time.Millisecond): + close(release) + } + out := <-result + if out.panicValue != panicValue { + t.Fatalf("Call panic = %v, want original value %v (err = %v)", out.panicValue, panicValue, out.err) + } +} + +func TestDoCapturesPanic(t *testing.T) { + w := Config{Synchronous: true, Log: slog.New(slog.NewTextHandler(io.Discard, nil))}.New() + t.Cleanup(func() { _ = w.Close() }) + + panicValue := &struct{ message string }{"do panic"} + task := w.Do(func(*Tx) { panic(panicValue) }) + var panicErr *PanicError + if err := task.Err(); !errors.As(err, &panicErr) { + t.Fatalf("Do error = %v, want *PanicError", err) + } + if panicErr.Value != panicValue { + t.Fatalf("PanicError.Value = %v, want original value %v", panicErr.Value, panicValue) + } +} + +func TestEntityDoCancelAfterInvalidatedWeakTransactionDoesNotPoisonHandle(t *testing.T) { + w := New() + defer w.Close() + + h := NewEntity(taskTestEntityType{}, taskTestEntityConfig{}) + <-w.exec(func(tx *Tx) { tx.AddEntity(h) }) + + started := make(chan struct{}) + release := make(chan struct{}) + w.exec(func(*Tx) { + close(started) + <-release + }) + <-started + + removeDone := w.exec(func(tx *Tx) { + e, ok := h.Entity(tx) + if !ok { + t.Error("entity missing before remove") + return + } + tx.RemoveEntity(e) + }) + task := h.Do(func(*Tx, Entity) { + t.Error("cancelled task ran") + }) + // The fast path in schedule may queue directly without a weak + // transaction. Either way, the task must still be cancellable while + // pending. + if !task.Cancel() { + t.Fatal("expected pending task to cancel") + } + close(release) + <-removeDone + if err := task.Wait(testContext(t)); !errors.Is(err, ErrTaskCancelled) { + t.Fatalf("expected ErrTaskCancelled, got %v", err) + } + + <-w.exec(func(tx *Tx) { tx.AddEntity(h) }) + task = h.Do(func(*Tx, Entity) {}) + if err := task.Wait(testContext(t)); err != nil { + t.Fatalf("handle poisoned after cancelling invalidated weak transaction: %v", err) + } +} + +func TestDoDoesNotBlockOwnerWhenQueueFull(t *testing.T) { + w := New() + defer w.Close() + + done := make(chan struct{}) + go func() { + <-w.exec(func(tx *Tx) { + for i := 0; i < cap(w.queue)+32; i++ { + w.Do(func(*Tx) {}) + tx.Defer(func(*Tx) {}) + } + }) + close(done) + }() + + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("Do or Context.Defer blocked the world owner") + } +} + +// TestWeakExecDoesNotBlockOwnerWhenQueueFull ensures an off-owner weak +// transaction blocked on a full queue does not hold scheduleMu, which would + +func TestWeakExecDoesNotBlockOwnerWhenQueueFull(t *testing.T) { + w := New() + + h := NewEntity(taskTestEntityType{}, taskTestEntityConfig{}) + <-w.exec(func(tx *Tx) { tx.AddEntity(h) }) + + entered := make(chan struct{}) + proceed := make(chan struct{}) + release := make(chan struct{}) + + // Occupy the world owner goroutine. + w.exec(func(tx *Tx) { + close(entered) + <-proceed + // Owner-side fire-and-forget must not block on scheduleMu. + w.Do(func(tx *Tx) {}) + close(release) + }) + <-entered + + // Fill the queue to capacity while the owner is busy. + for i := 0; i < cap(w.queue); i++ { + w.queue <- normalTransaction{c: make(chan struct{}), f: func(tx *Tx) {}} + } + + // Entity task whose weak transaction ends up in World.weakExec with the + // queue full. + task := h.Do(func(tx *Tx, e Entity) {}) + time.Sleep(200 * time.Millisecond) + + close(proceed) + select { + case <-release: + case <-time.After(3 * time.Second): + t.Fatal("owner-side World.Do deadlocked on scheduleMu held by weakExec blocked on full queue") + } + if err := task.Wait(testContext(t)); err != nil { + t.Fatalf("entity task failed after queue drained: %v", err) + } + _ = w.Close() +} + +func TestDoQueuedBeforeCloseDoesNotRunAfterHandleClose(t *testing.T) { + w := New() + + var closeHandled atomic.Bool + var ranAfterClose atomic.Bool + w.Handle(closeOrderHandler{closed: &closeHandled}) + + started := make(chan struct{}) + release := make(chan struct{}) + w.exec(func(*Tx) { + close(started) + <-release + }) + <-started + + for i := 0; i < cap(w.queue); i++ { + w.exec(func(*Tx) {}) + } + task := w.Do(func(*Tx) { + if closeHandled.Load() { + ranAfterClose.Store(true) + } + }) + + closed := make(chan struct{}) + go func() { + _ = w.Close() + close(closed) + }() + close(release) + + select { + case <-closed: + case <-time.After(5 * time.Second): + t.Fatal("world close did not complete") + } + if err := task.Wait(testContext(t)); err != nil && !errors.Is(err, ErrWorldClosed) { + t.Fatalf("scheduled task failed with unexpected error: %v", err) + } + if ranAfterClose.Load() { + t.Fatal("scheduled task ran after HandleClose") + } +} + +func TestEntityDoScheduledDuringWorldCloseRunsBeforeQueueShutdown(t *testing.T) { + var task *Task + w := New() + h := NewEntity(closeSchedulingEntityType{}, closeSchedulingEntityConfig{onClose: func(h *EntityHandle) { + task = h.Do(func(*Tx, Entity) {}) + }}) + <-w.exec(func(tx *Tx) { tx.AddEntity(h) }) + + if err := w.Close(); err != nil { + t.Fatalf("close world: %v", err) + } + if task == nil { + t.Fatal("entity close did not schedule cleanup task") + } + if err := task.Wait(testContext(t)); err != nil { + t.Fatalf("close-time entity task failed: %v", err) + } +} + +func TestEntityDoBlockedBeforeWorldCloseFailsPromptly(t *testing.T) { + w := New() + h := NewEntity(closeSchedulingEntityType{}, closeSchedulingEntityConfig{}) + <-w.exec(func(tx *Tx) { tx.AddEntity(h) }) + + h.cond.L.Lock() + h.weakTxActive = true + h.cond.L.Unlock() + task := h.Do(func(*Tx, Entity) { + t.Error("entity task ran after world close") + }) + + if err := w.Close(); err != nil { + t.Fatalf("close world: %v", err) + } + h.cond.L.Lock() + h.weakTxActive = false + h.cond.Broadcast() + h.cond.L.Unlock() + + if err := task.Wait(testContext(t)); !errors.Is(err, ErrWorldClosed) { + t.Fatalf("expected ErrWorldClosed, got %v", err) + } +} + +type taskTestEntityConfig struct{} + +type closeOrderHandler struct { + NopHandler + closed *atomic.Bool +} + +func (h closeOrderHandler) HandleClose(*Tx) { h.closed.Store(true) } + +type closeSchedulingEntityConfig struct { + onClose func(*EntityHandle) +} + +func (c closeSchedulingEntityConfig) Apply(data *EntityData) { data.Data = c.onClose } + +type closeSchedulingEntityType struct{} + +func (closeSchedulingEntityType) Open(_ *Tx, handle *EntityHandle, data *EntityData) Entity { + onClose, _ := data.Data.(func(*EntityHandle)) + return closeSchedulingEntity{h: handle, onClose: onClose} +} + +func (closeSchedulingEntityType) EncodeEntity() string { return "dragonfly:close_scheduling_entity" } + +func (closeSchedulingEntityType) BBox(Entity) cube.BBox { return cube.BBox{} } + +func (closeSchedulingEntityType) DecodeNBT(map[string]any, *EntityData) {} + +func (closeSchedulingEntityType) EncodeNBT(*EntityData) map[string]any { return nil } + +type closeSchedulingEntity struct { + h *EntityHandle + onClose func(*EntityHandle) +} + +func (e closeSchedulingEntity) Close() error { + if e.onClose != nil { + e.onClose(e.h) + } + return nil +} + +func (e closeSchedulingEntity) H() *EntityHandle { return e.h } + +func (closeSchedulingEntity) Position() mgl64.Vec3 { return mgl64.Vec3{} } + +func (closeSchedulingEntity) Rotation() cube.Rotation { return cube.Rotation{} } + +func (taskTestEntityConfig) Apply(*EntityData) {} + +type taskTestEntityType struct{} + +func (taskTestEntityType) Open(tx *Tx, handle *EntityHandle, _ *EntityData) Entity { + return taskTestEntity{h: handle, tx: tx} +} + +func (taskTestEntityType) EncodeEntity() string { return "dragonfly:test_entity" } + +func (taskTestEntityType) BBox(Entity) cube.BBox { return cube.BBox{} } + +func (taskTestEntityType) DecodeNBT(map[string]any, *EntityData) {} + +func (taskTestEntityType) EncodeNBT(*EntityData) map[string]any { return nil } + +type taskTestEntity struct { + h *EntityHandle + tx *Tx +} + +func (e taskTestEntity) Close() error { + if e.tx != nil { + if ent, ok := e.h.Entity(e.tx); ok { + e.tx.RemoveEntity(ent) + } + } + return e.h.Close() +} + +func (e taskTestEntity) H() *EntityHandle { return e.h } + +func (taskTestEntity) Position() mgl64.Vec3 { return mgl64.Vec3{} } + +func (taskTestEntity) Rotation() cube.Rotation { return cube.Rotation{} } + +func testContext(t *testing.T) context.Context { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + t.Cleanup(cancel) + return ctx +} diff --git a/server/world/tick.go b/server/world/tick.go index 1088a11565..4f0ead48ec 100644 --- a/server/world/tick.go +++ b/server/world/tick.go @@ -24,7 +24,7 @@ func (t ticker) tickLoop(w *World) { for { select { case <-tc.C: - <-w.Exec(t.tick) + <-w.exec(t.tick) case <-w.closing: // World is being closed: Stop ticking and get rid of a task. w.running.Done() @@ -33,6 +33,14 @@ func (t ticker) tickLoop(w *World) { } } +// AdvanceTick advances the World by a single tick. It is generally only useful +// for Worlds created with Config.Synchronous set: other Worlds tick +// automatically 20 times per second. Synchronous Worlds tick loaded chunks +// even when no viewers are present. +func (w *World) AdvanceTick() { + <-w.exec(ticker{}.tick) +} + // tick performs a tick on the World and updates the time, weather, blocks and // entities that require updates. func (t ticker) tick(tx *Tx) { @@ -43,10 +51,11 @@ func (t ticker) tick(tx *Tx) { if s := w.set.Spawn; s[1] > tx.Range()[1] && w.Dimension() == Overworld { // Vanilla will set the spawn position's Y value to max to indicate that // the player should spawn at the highest position in the world. - w.set.Spawn[1] = w.highestObstructingBlock(s[0], s[2]) + 1 + w.set.Spawn[1] = tx.highestObstructingBlock(s[0], s[2]) + 1 } - if len(viewers) == 0 && w.set.CurrentTick != 0 { - // Don't continue ticking if no viewers are in the world. + if len(viewers) == 0 && w.set.CurrentTick != 0 && !w.conf.Synchronous { + // Don't continue ticking if no viewers are in the world. Synchronous + // worlds only tick on explicit AdvanceTick calls, so they always tick. w.set.Unlock() return } @@ -92,6 +101,7 @@ func (t ticker) tick(tx *Tx) { w.scheduledUpdates.tick(tx, tick) t.tickBlocksRandomly(tx, loaders, tick) t.performNeighbourUpdates(tx) + w.redstone.tick(tx, tick) } // performNeighbourUpdates performs all block updates that came as a result of a neighbouring block being changed. @@ -105,7 +115,7 @@ func (t ticker) performNeighbourUpdates(tx *Tx) { if ticker, ok := tx.Block(pos).(NeighbourUpdateTicker); ok { ticker.NeighbourUpdateTick(pos, changedNeighbour, tx) } - if liquid, ok := tx.World().additionalLiquid(pos); ok { + if liquid, ok := tx.additionalLiquid(pos); ok { if ticker, ok := liquid.(NeighbourUpdateTicker); ok { ticker.NeighbourUpdateTick(pos, changedNeighbour, tx) } @@ -113,8 +123,7 @@ func (t ticker) performNeighbourUpdates(tx *Tx) { } } -// tickBlocksRandomly executes random block ticks in each sub chunk in the world that has at least one viewer -// registered from the viewers passed. +// tickBlocksRandomly executes random block ticks in loaded chunks within range of loaders. func (t ticker) tickBlocksRandomly(tx *Tx, loaders []*Loader, tick int64) { var ( r = int32(tx.World().tickRange()) @@ -128,12 +137,16 @@ func (t ticker) tickBlocksRandomly(tx *Tx, loaders []*Loader, tick int64) { } loaded := make([]ChunkPos, 0, len(loaders)) - for _, loader := range loaders { - loader.mu.RLock() - pos := loader.pos - loader.mu.RUnlock() - - loaded = append(loaded, pos) + if tx.World().conf.Synchronous { + loaded = slices.Collect(maps.Keys(tx.World().chunks)) + } else { + for _, loader := range loaders { + loader.mu.RLock() + pos := loader.pos + loader.mu.RUnlock() + + loaded = append(loaded, pos) + } } for pos, c := range tx.World().chunks { @@ -238,7 +251,7 @@ func (t ticker) tickEntities(tx *Tx, tick int64) { } } - if len(c.viewers) > 0 { + if tx.World().conf.Synchronous || len(c.viewers) > 0 { if te, ok := e.(TickerEntity); ok { te.Tick(tx, tick) } @@ -304,7 +317,7 @@ func (queue *scheduledTickQueue) tick(tx *Tx, tick int64) { b := tx.Block(t.pos) if ticker, ok := b.(ScheduledTicker); ok && w.conf.Blocks.BlockHash(b) == t.bhash { ticker.ScheduledTick(t.pos, tx, w.r) - } else if liquid, ok := tx.World().additionalLiquid(t.pos); ok && w.conf.Blocks.BlockHash(liquid) == t.bhash { + } else if liquid, ok := tx.additionalLiquid(t.pos); ok && w.conf.Blocks.BlockHash(liquid) == t.bhash { if ticker, ok := liquid.(ScheduledTicker); ok { ticker.ScheduledTick(t.pos, tx, w.r) } @@ -327,10 +340,7 @@ func (queue *scheduledTickQueue) tick(tx *Tx, tick int64) { func (queue *scheduledTickQueue) schedule(br BlockRegistry, pos cube.Pos, b Block, delay time.Duration) { resTick := queue.currentTick + int64(max(delay/(time.Second/20), 1)) index := scheduledTickIndex{pos: pos, hash: br.BlockHash(b)} - if t, ok := queue.furthestTicks[index]; ok && t >= resTick { - // Already have a tick scheduled for this position that will occur after - // the delay passed. Block updates can only be scheduled if they are - // after any currently scheduled updates. + if t, ok := queue.furthestTicks[index]; ok && t >= resTick && t > queue.currentTick { return } queue.furthestTicks[index] = resTick @@ -353,6 +363,9 @@ func (queue *scheduledTickQueue) removeChunk(pos ChunkPos) { queue.ticks = slices.DeleteFunc(queue.ticks, func(tick scheduledTick) bool { return chunkPosFromBlockPos(tick.pos) == pos }) + maps.DeleteFunc(queue.furthestTicks, func(index scheduledTickIndex, _ int64) bool { + return chunkPosFromBlockPos(index.pos) == pos + }) } // add adds a slice of scheduled ticks to the queue. It assumes no duplicate @@ -362,10 +375,9 @@ func (queue *scheduledTickQueue) add(ticks []scheduledTick) { for _, t := range ticks { index := scheduledTickIndex{pos: t.pos, hash: t.bhash} if existing, ok := queue.furthestTicks[index]; ok { - // Make sure we find the furthest tick for each of the ticks added. - // Some ticks may have the same block and position, in which case we - // need to set the furthest tick. queue.furthestTicks[index] = max(existing, t.t) + } else { + queue.furthestTicks[index] = t.t } } } diff --git a/server/world/tx.go b/server/world/tx.go index 61655893a6..abb25fc689 100644 --- a/server/world/tx.go +++ b/server/world/tx.go @@ -3,27 +3,66 @@ package world import ( "iter" "sync" - "sync/atomic" "time" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/player/chat" - "github.com/df-mc/dragonfly/server/world/redstone" "github.com/go-gl/mathgl/mgl64" ) -// Tx represents a synchronised transaction performed on a World. Most -// operations on a World can only be called through a transaction. Tx is not -// safe for use by multiple goroutines concurrently. +// Tx is the owner transaction handle passed to world callbacks. It is the +// only way to perform world operations and is valid only during its callback. type Tx struct { - w *World - closed bool + w *World + closed bool + deferred []scheduledTransaction +} + +// Context is a cancellable event scope passed to Handler events. It embeds the +// owner transaction, so world operations are available directly on it. +type Context struct { + *Tx + cancel bool +} + +// newTx returns a fresh transaction on World w. +func newTx(w *World) *Tx { + return &Tx{w: w} +} + +// Event returns a fresh Context for dispatching one Handler event on this +// transaction, so cancelling one event cannot affect another. +func (tx *Tx) Event() *Context { + return &Context{Tx: tx} +} + +// Cancelled returns whether the Context has been cancelled by an event handler. +func (ctx *Context) Cancelled() bool { return ctx.cancel } + +// Cancel cancels the Context. It is used by event handlers to signal that the +// default behaviour of the event should not run. +func (ctx *Context) Cancel() { ctx.cancel = true } + +// Defer schedules f to run on the owner after the current callback completes +// and before the parent Task completes. Deferred callbacks run FIFO in +// registration order, unlike Go defer's LIFO order. +func (tx *Tx) Defer(f func(tx *Tx)) *Task { + return tx.DeferErr(func(tx *Tx) error { + f(tx) + return nil + }) +} + +// DeferErr schedules f to run on the owner after the current callback +// completes, recording any returned error on the Task. +func (tx *Tx) DeferErr(f func(tx *Tx) error) *Task { + return tx.deferTask(f) } // Range returns the lower and upper bounds of the World that the Tx is // operating on. func (tx *Tx) Range() cube.Range { - return tx.w.ra + return tx.World().ra } // SetBlock writes a block to the position passed. If a chunk is not yet loaded @@ -41,21 +80,41 @@ func (tx *Tx) Range() cube.Range { // needing to set a lot of blocks to the world. BuildStructure may be used // instead. func (tx *Tx) SetBlock(pos cube.Pos, b Block, opts *SetOpts) { - tx.World().setBlock(pos, b, opts) + tx.setBlock(pos, b, opts) +} + +// SetBlockEntity updates block entity data without notifying viewers, updating +// neighbouring blocks or invalidating redstone. The chunk is marked modified. +// It falls back to SetBlock if b has no block entity data or its state differs. +func (tx *Tx) SetBlockEntity(pos cube.Pos, b Block) { + tx.setBlockEntity(pos, b) } // Block reads a block from the position passed. If a chunk is not yet loaded // at that position, the chunk is loaded, or generated if it could not be found // in the world save, and the block returned. func (tx *Tx) Block(pos cube.Pos) Block { - return tx.World().block(pos) + return tx.block(pos) +} + +// BlockLoaded returns the block at the position passed if the chunk containing it is already loaded. It returns false +// without loading or generating the chunk when the block is unavailable. +func (tx *Tx) BlockLoaded(pos cube.Pos) (Block, bool) { + return tx.World().blockLoaded(pos) +} + +// BlocksWithin returns an iterator over the positions of blocks matching any of the block states passed, within a +// horizontal square radius around pos. Chunks not in memory are read from the world save; missing chunks are +// skipped, not generated. Only the primary block layer is searched and blocks are matched by their state alone. +func (tx *Tx) BlocksWithin(pos cube.Pos, radius int, blocks ...Block) iter.Seq[cube.Pos] { + return tx.World().blocksWithin(pos, radius, blocks...) } // Liquid attempts to return a Liquid block at the position passed. This // Liquid may be in the foreground or in any other layer. If found, the Liquid // is returned. If not, the bool returned is false. func (tx *Tx) Liquid(pos cube.Pos) (Liquid, bool) { - return tx.World().liquid(pos) + return tx.liquid(pos) } // SetLiquid sets a Liquid at a specific position in the World. Unlike @@ -65,7 +124,7 @@ func (tx *Tx) Liquid(pos cube.Pos) (Liquid, bool) { // overwritten. If nil is passed for the Liquid, any Liquid currently present // will be removed. func (tx *Tx) SetLiquid(pos cube.Pos, b Liquid) { - tx.World().setLiquid(pos, b) + tx.setLiquid(pos, b) } // BuildStructure builds a Structure passed at a specific position in the @@ -76,7 +135,7 @@ func (tx *Tx) SetLiquid(pos cube.Pos, b Liquid) { // method operates on a per-chunk basis, setting all blocks within a single // chunk part of the Structure before moving on to the next chunk. func (tx *Tx) BuildStructure(pos cube.Pos, s Structure) { - tx.World().buildStructure(pos, s) + tx.buildStructure(pos, s) } // ScheduleBlockUpdate schedules a block update at the position passed for the @@ -92,50 +151,52 @@ func (tx *Tx) ScheduleBlockUpdate(pos cube.Pos, b Block, delay time.Duration) { // HighestLightBlocker gets the Y value of the highest fully light blocking // block at the x and z values passed in the World. func (tx *Tx) HighestLightBlocker(x, z int) int { - return tx.World().HighestLightBlocker(x, z) + return tx.highestLightBlocker(x, z) } // HighestBlock looks up the highest non-air block in the World at a specific x // and z. The y value of the highest block is returned, or 0 if no blocks were // present in the column. func (tx *Tx) HighestBlock(x, z int) int { - return tx.World().highestBlock(x, z) + return tx.highestBlock(x, z) } // Light returns the light level at the position passed. This is the highest of // the sky- and block light. The light value returned is a value in the range // 0-15, where 0 means there is no light present, whereas 15 means the block is -// fully lit. +// fully lit. Light does not load chunks: 0 is returned for positions in chunks +// that are not currently loaded. func (tx *Tx) Light(pos cube.Pos) uint8 { - return tx.World().light(pos) + return tx.light(pos) } // SkyLight returns the skylight level at the position passed. This light level // is not influenced by blocks that emit light, such as torches. The light // value, similarly to Light, is a value in the range 0-15, where 0 means no -// light is present. +// light is present. Unlike Light, SkyLight loads or generates the chunk at the +// position if it is not currently loaded. func (tx *Tx) SkyLight(pos cube.Pos) uint8 { - return tx.World().skyLight(pos) + return tx.skyLight(pos) } // SetBiome sets the Biome at the position passed. If a chunk is not yet loaded // at that position, the chunk is first loaded or generated if it could not be // found in the world save. func (tx *Tx) SetBiome(pos cube.Pos, b Biome) { - tx.World().setBiome(pos, b) + tx.setBiome(pos, b) } // Biome reads the Biome at the position passed. If a chunk is not yet loaded // at that position, the chunk is loaded, or generated if it could not be found // in the world save, and the Biome returned. func (tx *Tx) Biome(pos cube.Pos) Biome { - return tx.World().biome(pos) + return tx.biome(pos) } // Temperature returns the temperature in the World at a specific position. // Higher altitudes and different biomes influence the temperature returned. func (tx *Tx) Temperature(pos cube.Pos) float64 { - return tx.World().temperature(pos) + return tx.temperature(pos) } // RainingAt checks if it is raining at a specific cube.Pos in the World. True @@ -143,21 +204,21 @@ func (tx *Tx) Temperature(pos cube.Pos) float64 { // for it not to be snow and if the block is above the top-most obstructing // block. func (tx *Tx) RainingAt(pos cube.Pos) bool { - return tx.World().rainingAt(pos) + return tx.rainingAt(pos) } // SnowingAt checks if it is snowing at a specific cube.Pos in the World. True // is returned if the temperature in the Biome at that position is sufficiently // low, if it is raining and if it's above the top-most obstructing block. func (tx *Tx) SnowingAt(pos cube.Pos) bool { - return tx.World().snowingAt(pos) + return tx.snowingAt(pos) } // ThunderingAt checks if it is thundering at a specific cube.Pos in the World. // True is returned if RainingAt returns true and if it is thundering in the // world. func (tx *Tx) ThunderingAt(pos cube.Pos) bool { - return tx.World().thunderingAt(pos) + return tx.thunderingAt(pos) } // Raining checks if it is raining anywhere in the World. @@ -199,6 +260,13 @@ func (tx *Tx) AddEntity(e *EntityHandle) Entity { return tx.World().addEntity(tx, e) } +// AddEntityAt adds an EntityHandle to a World at the position passed. The Entity will be visible to all viewers of +// the World that have the chunk at the position passed. AddEntityAt panics if the EntityHandle is already in a world. +// AddEntityAt returns the Entity created by the EntityHandle. +func (tx *Tx) AddEntityAt(e *EntityHandle, pos mgl64.Vec3) Entity { + return tx.World().addEntityAt(tx, e, pos) +} + // RemoveEntity removes an Entity from the World that is currently present in // it. Any viewers of the Entity will no longer be able to see it. // RemoveEntity returns the EntityHandle of the Entity. After removing an Entity @@ -278,46 +346,19 @@ func (tx *Tx) BroadcastSleepingReminder(sleeper Sleeper) { } } -// RedstonePower returns the redstone power emitted by the block at pos toward a neighbouring receiver. -// The face argument is relative to the receiving block. -func (tx *Tx) RedstonePower(pos cube.Pos, face cube.Face, accountForDust bool) (power int) { - b := tx.Block(pos) - if c, ok := b.(Conductor); ok { - return c.WeakPower(pos, face, tx, accountForDust) - } - // The wiki states that in the future some blocks may be transparent but still relay redstone. - // If a block implements RedstonePowerRelayer, it should always be prioritised over lightDiffuser. - if r, ok := b.(RedstonePowerRelayer); ok { - if !r.RelaysRedstonePowerThrough() { - return 0 - } - } else if d, ok := b.(lightDiffuser); ok && d.LightDiffusionLevel() != 15 { - return 0 - } - for _, f := range cube.Faces() { - if !b.Model().FaceSolid(pos, f, tx) { - return 0 - } - } - for _, f := range cube.Faces() { - c, ok := tx.Block(pos.Side(f)).(Conductor) - if !ok { - continue - } - sourcePos := pos.Side(f) - power = max(power, c.StrongPower(sourcePos, f, tx, accountForDust)) - if !accountForDust { - continue - } - if weakBlockPowerer, ok := c.(WeakBlockPowerer); ok && weakBlockPowerer.WeaklyPowersBlocks() { - power = max(power, c.WeakPower(sourcePos, f, tx, accountForDust)) - } +func (tx *Tx) deferTask(f func(tx *Tx) error) *Task { + if tx.closed { + panic("world.Tx: use of transaction after transaction finishes is not permitted") } - return power + task := newTask() + tx.deferred = append(tx.deferred, scheduledTransaction{task: task, f: f}) + return task } -// World returns the World of the Tx. It panics if the transaction was already -// marked complete. +// World returns the Tx's World. It panics once the callback has +// completed. Treat the result as the off-owner handle: blocking calls like +// Save and Close deadlock from inside the callback, so do world operations +// through the Tx instead. func (tx *Tx) World() *World { if tx.closed { panic("world.Tx: use of transaction after transaction finishes is not permitted") @@ -333,18 +374,23 @@ func (tx *Tx) CurrentTick() int64 { return w.set.CurrentTick } -// Redstone returns the transient redstone runtime state owned by the transaction's world. -func (tx *Tx) Redstone() *redstone.State { - return &tx.World().redstone -} - // close finishes the Tx, causing any following call on the Tx to panic. func (tx *Tx) close() { tx.closed = true } +func (tx *Tx) runDeferred() { + for len(tx.deferred) > 0 { + deferred := tx.deferred + tx.deferred = nil + for _, st := range deferred { + st.Run(tx.w) + } + } +} + // normalTransaction is added to the transaction queue for transactions created -// using World.Exec(). +// using World.exec(). type normalTransaction struct { c chan struct{} f func(tx *Tx) @@ -353,30 +399,32 @@ type normalTransaction struct { // Run creates a *Tx, calls ntx.f, closes the transaction and finally closes // ntx.c. func (ntx normalTransaction) Run(w *World) { - tx := &Tx{w: w} + tx := newTx(w) ntx.f(tx) tx.close() + tx.runDeferred() close(ntx.c) } -// weakTransaction is a transaction that may be cancelled by setting its invalid -// bool to false before the transaction is run. +// weakTransaction is a transaction that may be cancelled by its validity +// predicate before the transaction is run. type weakTransaction struct { - c chan bool - f func(tx *Tx) - invalid *atomic.Bool - cond *sync.Cond + c chan bool + f func(tx *Tx) + valid func() bool + cond *sync.Cond } -// Run runs the transaction, first checking if its invalid bool is false and -// creating a *Tx if so. Afterwards, a bool indicating if the transaction was -// run is added to wtx.c. Finally, wtx.cond.Broadcast() is called. +// Run runs the transaction, first checking if it is still valid and creating a +// *Tx if so. Afterwards, a bool indicating if the transaction was run is added +// to wtx.c. Finally, wtx.cond.Broadcast() is called. func (wtx weakTransaction) Run(w *World) { - valid := !wtx.invalid.Load() + valid := wtx.valid == nil || wtx.valid() if valid { - tx := &Tx{w: w} + tx := newTx(w) wtx.f(tx) tx.close() + tx.runDeferred() } // We have to acquire a lock on wtx.cond.L here to make sure cond.Wait() // has been called before we call cond.Broadcast(). If not, we might @@ -387,3 +435,12 @@ func (wtx weakTransaction) Run(w *World) { wtx.c <- valid wtx.cond.Broadcast() } + +// fail delivers false to a weak transaction that will never run, using the +// same condition handshake as Run so a waiter in cond.Wait is woken. +func (wtx weakTransaction) fail() { + wtx.cond.L.Lock() + defer wtx.cond.L.Unlock() + wtx.c <- false + wtx.cond.Broadcast() +} diff --git a/server/world/tx_redstone.go b/server/world/tx_redstone.go new file mode 100644 index 0000000000..0bc164d994 --- /dev/null +++ b/server/world/tx_redstone.go @@ -0,0 +1,97 @@ +package world + +import "github.com/df-mc/dragonfly/server/block/cube" + +// Redstone returns a transaction-scoped handle for redstone engine operations. +func (tx *Tx) Redstone() RedstoneTransaction { + return RedstoneTransaction{tx: tx} +} + +// RedstoneTransaction provides access to redstone engine operations within a transaction. +type RedstoneTransaction struct { + tx *Tx +} + +// ScheduleUpdate marks pos and its neighbours for re-evaluation during the next redstone phase. +func (r RedstoneTransaction) ScheduleUpdate(pos cube.Pos) { + r.tx.World().redstone.invalidateAround(pos, pos, RedstoneUpdateCauseScheduledTick, r.tx.Range()) +} + +// Torch returns a transaction-scoped handle for transient redstone torch state at pos. +func (r RedstoneTransaction) Torch(pos cube.Pos) RedstoneTorchTransaction { + return RedstoneTorchTransaction{tx: r.tx, pos: pos} +} + +// RedstoneTorchTransaction provides access to transient redstone torch state within a transaction. +type RedstoneTorchTransaction struct { + tx *Tx + pos cube.Pos +} + +// BurnoutStatus returns the transient burnout state for the redstone torch. +func (t RedstoneTorchTransaction) BurnoutStatus() (burnedOut, recoverable bool) { + return t.tx.World().redstone.torchBurnoutStatus(t.pos, t.tx.CurrentTick()) +} + +// RecordTurnOff records that the redstone torch was forced off. +func (t RedstoneTorchTransaction) RecordTurnOff() (burnsOut bool) { + return t.tx.World().redstone.recordTorchTurnOff(t.pos, t.tx.CurrentTick()) +} + +// MarkSelfTriggered records that the next turn-off was caused by the torch's own output. +func (t RedstoneTorchTransaction) MarkSelfTriggered() { + t.tx.World().redstone.markTorchSelfTriggered(t.pos) +} + +// ConsumeSelfTriggered reports and clears whether the next turn-off was self-triggered. +func (t RedstoneTorchTransaction) ConsumeSelfTriggered() bool { + return t.tx.World().redstone.consumeTorchSelfTriggered(t.pos) +} + +// ClearBurnout removes transient burnout state for the redstone torch. +func (t RedstoneTorchTransaction) ClearBurnout() { + t.tx.World().redstone.clearTorchBurnout(t.pos) +} + +// RedstonePower returns the strongest redstone power currently applied to the position passed. Custom redstone block +// implementations may use this method to query the transaction's current redstone state. +func (tx *Tx) RedstonePower(pos cube.Pos) int { + return tx.World().redstone.powerTo(pos, tx) +} + +// RedstoneDirectPower returns the strongest direct redstone power currently applied to the position passed, excluding +// power conducted through solid blocks. Custom redstone block implementations may use this method to query the +// transaction's current redstone state. +func (tx *Tx) RedstoneDirectPower(pos cube.Pos) int { + return tx.World().redstone.directPower(pos, tx) +} + +// RedstoneStrongPower returns the strongest strong redstone power currently applied to the position passed. Custom +// redstone block implementations may use this method to query the transaction's current redstone state. +func (tx *Tx) RedstoneStrongPower(pos cube.Pos) int { + return tx.World().redstone.strongPower(pos, tx) +} + +// RedstoneConductivePower returns the power held by pos as a conductive block, excluding direct component activation. +// Custom redstone block implementations may use this method to query the transaction's current redstone state. +func (tx *Tx) RedstoneConductivePower(pos cube.Pos) int { + return tx.World().redstone.conductivePowerTo(pos, tx) +} + +// RedstonePowerFrom returns the strongest redstone power reaching pos from the side passed. Custom redstone block +// implementations may use this method to query the transaction's current redstone state. +func (tx *Tx) RedstonePowerFrom(pos cube.Pos, face cube.Face) int { + return tx.World().redstone.powerFrom(pos, tx, face) +} + +// RedstoneDirectPowerFrom returns the strongest direct redstone power reaching pos from the side passed. Custom +// redstone block implementations may use this method to query the transaction's current redstone state. +func (tx *Tx) RedstoneDirectPowerFrom(pos cube.Pos, face cube.Face) int { + return tx.World().redstone.directPowerFrom(pos, tx, face) +} + +// RedstoneStrongPowerFrom returns the strongest strong redstone power reaching pos from the side passed. Custom +// redstone block implementations may use this method to query the transaction's current redstone state. +func (tx *Tx) RedstoneStrongPowerFrom(pos cube.Pos, face cube.Face) int { + return tx.World().redstone.strongPowerFrom(pos, tx, face) +} diff --git a/server/world/vanilla_items.nbt b/server/world/vanilla_items.nbt index 52af4d9e1d6ed874eb13b56f923ffb714097507a..ec858bab04610d414062178116a3b6a7db40918a 100644 GIT binary patch delta 29675 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 0.15 { + if b := tx.biome(pos); b.Rainfall() == 0 || tx.temperature(pos) > 0.15 { return false } - w.w.set.Lock() - raining := w.w.set.Raining - w.w.set.Unlock() - return raining && w.w.highestObstructingBlock(pos[0], pos[2]) < pos[1] + w.set.Lock() + raining := w.set.Raining + w.set.Unlock() + return raining && tx.highestObstructingBlock(pos[0], pos[2]) < pos[1] } // rainingAt checks if it is raining at a specific cube.Pos in the World. True // is returned if it is raining, if the temperature is high enough in the biome // for it not to be snow and if the block is above the top-most obstructing // block. -func (w weather) rainingAt(pos cube.Pos) bool { - if w.w == nil || !w.w.Dimension().WeatherCycle() { +func (tx *Tx) rainingAt(pos cube.Pos) bool { + w := tx.World() + if w == nil || !w.Dimension().WeatherCycle() { return false } - if b := w.w.biome(pos); b.Rainfall() == 0 || w.w.temperature(pos) <= 0.15 { + if b := tx.biome(pos); b.Rainfall() == 0 || tx.temperature(pos) <= 0.15 { return false } - w.w.set.Lock() - a := w.w.set.Raining - w.w.set.Unlock() - return a && w.w.highestObstructingBlock(pos[0], pos[2]) < pos[1] + w.set.Lock() + a := w.set.Raining + w.set.Unlock() + return a && tx.highestObstructingBlock(pos[0], pos[2]) < pos[1] } // thunderingAt checks if it is thundering at a specific cube.Pos in the World. // True is returned if rainingAt returns true and if it is thundering in the // world. -func (w weather) thunderingAt(pos cube.Pos) bool { - raining := w.rainingAt(pos) - w.w.set.Lock() - a := w.w.set.Thundering && raining - w.w.set.Unlock() - return a && w.w.highestObstructingBlock(pos[0], pos[2]) < pos[1] +func (tx *Tx) thunderingAt(pos cube.Pos) bool { + w := tx.World() + raining := tx.rainingAt(pos) + w.set.Lock() + a := w.set.Thundering && raining + w.set.Unlock() + return a && tx.highestObstructingBlock(pos[0], pos[2]) < pos[1] } // raining checks if it is raining anywhere in the World. @@ -207,6 +210,9 @@ func (w weather) tickLightning(tx *Tx) { // near the lightning strike. If there is no rain at the final position // selected, the lightning strike will fail. func (w weather) strikeLightning(tx *Tx, c ChunkPos) { + if w.w.conf.Entities.Config().Lightning == nil { + return + } if pos := w.lightningPosition(tx, c); tx.ThunderingAt(cube.PosFromVec3(pos)) { tx.AddEntity(w.w.conf.Entities.Config().Lightning(EntitySpawnOpts{Position: pos})) } @@ -243,7 +249,7 @@ func (w weather) adjustPositionToEntities(tx *Tx, vec mgl64.Vec3) mgl64.Vec3 { // block at its position is eligible to be struck by lightning. We // first save all entity positions where this is the case. pos := cube.PosFromVec3(e.Position()) - if tx.HighestBlock(pos[0], pos[1]) < pos[2] { + if tx.HighestBlock(pos[0], pos[2]) < pos[1] { list = append(list, e.Position()) } } diff --git a/server/world/world.go b/server/world/world.go index 649186b4af..742130e454 100644 --- a/server/world/world.go +++ b/server/world/world.go @@ -1,6 +1,7 @@ package world import ( + "context" "encoding/binary" "errors" "fmt" @@ -13,10 +14,8 @@ import ( "time" "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/internal/sliceutil" "github.com/df-mc/dragonfly/server/world/chunk" - "github.com/df-mc/dragonfly/server/world/redstone" "github.com/df-mc/goleveldb/leveldb" "github.com/go-gl/mathgl/mgl64" "github.com/google/uuid" @@ -35,6 +34,16 @@ type World struct { queueClosing chan struct{} queueing sync.WaitGroup + // scheduleMu serialises task scheduling against the close transitions + // below. scheduling counts in-flight scheduled work that close must drain. + scheduleMu sync.Mutex + scheduling sync.WaitGroup + // closed flips once close starts; new tasks fail with ErrWorldClosed. + // closeAcceptingEntityTasks is true only during the close transaction, when + // entity Close methods may still schedule final work that close drains. + closed atomic.Bool + closeAcceptingEntityTasks atomic.Bool + // advance is a bool that specifies if this World should advance the current // tick, time and weather saved in the Settings struct held by the World. advance bool @@ -46,12 +55,17 @@ type World struct { weather - closing chan struct{} - running sync.WaitGroup + // closeStarted closes as soon as World.Close begins, before the close + // transaction runs; closing closes once the world stops ticking. + closeStarted chan struct{} + closing chan struct{} + running sync.WaitGroup // chunks holds a cache of chunks currently loaded. These chunks are cleared // from this map after some time of not being used. - chunks map[ChunkPos]*Column + chunks map[ChunkPos]*Column + chunkRequests map[ChunkPos]*chunkRequest + chunkWorkers *chunkWorkerPool // entities holds a map of entities currently loaded and the last ChunkPos // that the Entity was in. These are tracked so that a call to RemoveEntity @@ -65,10 +79,9 @@ type World struct { // tick value passed, the block update will be performed and the entry will // be removed from the map. scheduledUpdates *scheduledTickQueue + redstone *redstoneEngine neighbourUpdates []neighbourUpdate - redstone redstone.State - viewerMu sync.Mutex viewers map[*Loader]Viewer } @@ -116,20 +129,65 @@ func (w *World) BlockRegistry() BlockRegistry { return w.conf.Blocks } -// ExecFunc is a function that performs a synchronised transaction on a World. -type ExecFunc func(tx *Tx) +// execFunc is a function that performs a synchronised transaction on a World. +type execFunc func(tx *Tx) -// Exec performs a synchronised transaction f on a World. Exec returns a channel -// that is closed once the transaction is complete. -func (w *World) Exec(f ExecFunc) <-chan struct{} { +// exec runs f on the World, bypassing the closed check that Do/DoAfter/Call +// apply — reserved for the World's own machinery (ticking, saving, chunk +// unload, the close transaction), which must queue work after close begins. +// The returned channel closes when done; waiting on it from the owner deadlocks. +func (w *World) exec(f execFunc) <-chan struct{} { c := make(chan struct{}) - w.queue <- normalTransaction{c: c, f: f} + ntx := normalTransaction{c: c, f: f} + if w.conf.Synchronous { + ntx.Run(w) + return c + } + w.queue <- ntx return c } -func (w *World) weakExec(invalid *atomic.Bool, cond *sync.Cond, f ExecFunc) <-chan bool { +func (w *World) weakExec(valid func() bool, cond *sync.Cond, f execFunc, allowClosed bool) <-chan bool { c := make(chan bool, 1) - w.queue <- weakTransaction{c: c, f: f, invalid: invalid, cond: cond} + if w.conf.Synchronous { + run := valid == nil || valid() + if run { + // As in weakTransaction.Run, f must not run under cond.L: it may + // relock it, e.g. through RemoveEntity. + cond.L.Unlock() + tx := newTx(w) + f(tx) + tx.close() + tx.runDeferred() + cond.L.Lock() + } + c <- run + return c + } + w.scheduleMu.Lock() + if w.closed.Load() && !w.closeAcceptingEntityTasks.Load() && !allowClosed { + w.scheduleMu.Unlock() + c <- false + return c + } + wtx := weakTransaction{c: c, f: f, valid: valid, cond: cond} + select { + case w.queue <- wtx: + w.scheduleMu.Unlock() + default: + w.scheduling.Add(1) + w.scheduleMu.Unlock() + go func() { + defer w.scheduling.Done() + select { + case w.queue <- wtx: + case <-w.closing: + wtx.fail() + case <-w.queueClosing: + wtx.fail() + } + }() + } return c } @@ -156,8 +214,26 @@ func (w *World) EntityRegistry() EntityRegistry { // block reads a block from the position passed. If a chunk is not yet loaded // at that position, the chunk is loaded, or generated if it could not be found // in the world save, and the block returned. -func (w *World) block(pos cube.Pos) Block { - return w.blockInChunk(w.chunk(chunkPosFromBlockPos(pos)), pos) +func (tx *Tx) block(pos cube.Pos) Block { + return tx.World().blockInChunk(tx.chunk(chunkPosFromBlockPos(pos)), pos) +} + +// blockLoaded reads a block from a position only if its chunk is already loaded. +func (w *World) blockLoaded(pos cube.Pos) (Block, bool) { + if pos.OutOfBounds(w.ra) { + return w.conf.Blocks.Air(), false + } + c, ok := w.chunks[chunkPosFromBlockPos(pos)] + if !ok { + return w.conf.Blocks.Air(), false + } + rid := c.Block(uint8(pos[0]), int16(pos[1]), uint8(pos[2]), 0) + if w.conf.Blocks.NBTBlock(rid) { + if b, ok := c.BlockEntities[pos]; ok { + return b, true + } + } + return w.conf.Blocks.BlockByRuntimeIDOrAir(rid), true } // blockInChunk reads a block from a chunk at the position passed. The block @@ -188,45 +264,56 @@ func (w *World) blockInChunk(c *Column, pos cube.Pos) Block { // biome reads the Biome at the position passed. If a chunk is not yet loaded // at that position, the chunk is loaded, or generated if it could not be found // in the world save, and the Biome returned. -func (w *World) biome(pos cube.Pos) Biome { - if pos.OutOfBounds(w.Range()) { +func (tx *Tx) biome(pos cube.Pos) Biome { + if pos.OutOfBounds(tx.Range()) { // Fast way out. return ocean() } - id := int(w.chunk(chunkPosFromBlockPos(pos)).Biome(uint8(pos[0]), int16(pos[1]), uint8(pos[2]))) - b, ok := w.conf.Biomes.BiomeByID(id) + id := int(tx.chunk(chunkPosFromBlockPos(pos)).Biome(uint8(pos[0]), int16(pos[1]), uint8(pos[2]))) + b, ok := tx.World().conf.Biomes.BiomeByID(id) if !ok { - w.conf.Log.Error("biome not found by ID", "ID", id) + tx.World().conf.Log.Error("biome not found by ID", "ID", id) + return unknownBiome{id: id} } return b } // HighestLightBlocker gets the Y value of the highest fully light blocking -// block at the x and z values passed in the World. +// block at the x and z values passed in the World. It must not be called from +// within a transaction; use Tx.HighestLightBlocker instead. func (w *World) HighestLightBlocker(x, z int) int { - return int(w.chunk(ChunkPos{int32(x >> 4), int32(z >> 4)}).HighestLightBlocker(uint8(x), uint8(z))) + y, _ := Call(context.Background(), w, func(tx *Tx) (int, error) { + return tx.highestLightBlocker(x, z), nil + }) + return y +} + +// highestLightBlocker gets the Y value of the highest fully light blocking +// block at the x and z values passed in the World. +func (tx *Tx) highestLightBlocker(x, z int) int { + return int(tx.chunk(ChunkPos{int32(x >> 4), int32(z >> 4)}).HighestLightBlocker(uint8(x), uint8(z))) } // highestBlock looks up the highest non-air block in the World at a specific x // and z The y value of the highest block is returned, or 0 if no blocks were // present in the column. -func (w *World) highestBlock(x, z int) int { - return int(w.chunk(ChunkPos{int32(x >> 4), int32(z >> 4)}).HighestBlock(uint8(x), uint8(z))) +func (tx *Tx) highestBlock(x, z int) int { + return int(tx.chunk(ChunkPos{int32(x >> 4), int32(z >> 4)}).HighestBlock(uint8(x), uint8(z))) } // highestObstructingBlock returns the highest block in the World at a given x // and z that has at least a solid top or bottom face. -func (w *World) highestObstructingBlock(x, z int) int { - yHigh := w.highestBlock(x, z) - src := worldSource{w: w} - for y := yHigh; y >= w.Range()[0]; y-- { +func (tx *Tx) highestObstructingBlock(x, z int) int { + yHigh := tx.highestBlock(x, z) + src := worldSource{tx: tx} + for y := yHigh; y >= tx.Range()[0]; y-- { pos := cube.Pos{x, y, z} - m := w.block(pos).Model() + m := tx.block(pos).Model() if m.FaceSolid(pos, cube.FaceUp, src) || m.FaceSolid(pos, cube.FaceDown, src) { return y } } - return w.Range()[0] + return tx.Range()[0] } // SetOpts holds several parameters that may be set to disable updates in the @@ -241,6 +328,10 @@ type SetOpts struct { // performance is very important, or where it is known no liquid can be // present anyway. DisableLiquidDisplacement bool + // DisableRedstoneUpdates makes SetBlock not invalidate the redstone engine + // around the changed block. This is used by the redstone engine while + // applying its own block-state updates to avoid duplicate same-tick work. + DisableRedstoneUpdates bool } // setBlock writes a block to the position passed. If a chunk is not yet loaded @@ -257,7 +348,8 @@ type SetOpts struct { // setBlock should be avoided in situations where performance is critical when // needing to set a lot of blocks to the world. BuildStructure may be used // instead. -func (w *World) setBlock(pos cube.Pos, b Block, opts *SetOpts) { +func (tx *Tx) setBlock(pos cube.Pos, b Block, opts *SetOpts) { + w := tx.World() if pos.OutOfBounds(w.Range()) { // Fast way out. return @@ -267,13 +359,30 @@ func (w *World) setBlock(pos cube.Pos, b Block, opts *SetOpts) { } x, y, z := uint8(pos[0]), int16(pos[1]), uint8(pos[2]) - c := w.chunk(chunkPosFromBlockPos(pos)) + c := tx.chunk(chunkPosFromBlockPos(pos)) rid := w.conf.Blocks.BlockRuntimeID(b) + redstoneAfterRelevant := isRedstoneRelevant(b) + needOldBlock := !opts.DisableRedstoneUpdates || !redstoneAfterRelevant + needOldRID := needOldBlock || (rid != w.conf.Blocks.AirRuntimeID() && !opts.DisableLiquidDisplacement) + + var oldRID uint32 + if needOldRID { + oldRID = c.Block(x, y, z, 0) + } + var oldBlock Block + if needOldBlock { + oldBlock = w.conf.Blocks.BlockByRuntimeIDOrAir(oldRID) + if w.conf.Blocks.NBTBlock(oldRID) { + if blockEntity, ok := c.BlockEntities[pos]; ok { + oldBlock = blockEntity + } + } + } var before uint32 if rid != w.conf.Blocks.AirRuntimeID() && !opts.DisableLiquidDisplacement { - before = c.Block(x, y, z, 0) + before = oldRID } c.modified = true @@ -317,6 +426,10 @@ func (w *World) setBlock(pos cube.Pos, b Block, opts *SetOpts) { } } + if redstoneAfterRelevant || (needOldBlock && isRedstoneRelevant(oldBlock)) { + w.redstone.forget(pos) + } + for _, viewer := range viewers { viewer.ViewBlockUpdate(pos, b, 0) } @@ -324,17 +437,38 @@ func (w *World) setBlock(pos cube.Pos, b Block, opts *SetOpts) { if !opts.DisableBlockUpdates { w.doBlockUpdatesAround(pos) } + if !opts.DisableRedstoneUpdates { + w.redstone.invalidateAroundBlockChange(pos, oldBlock, b, RedstoneUpdateCauseBlockUpdate, w.Range()) + } +} + +// setBlockEntity updates block entity data without triggering block updates. +func (tx *Tx) setBlockEntity(pos cube.Pos, b Block) { + w := tx.World() + if pos.OutOfBounds(w.Range()) { + // Fast way out. + return + } + c := tx.chunk(chunkPosFromBlockPos(pos)) + + rid := w.conf.Blocks.BlockRuntimeID(b) + if !w.conf.Blocks.NBTBlock(rid) || c.Block(uint8(pos[0]), int16(pos[1]), uint8(pos[2]), 0) != rid { + tx.setBlock(pos, b, nil) + return + } + c.BlockEntities[pos] = b + c.modified = true } // setBiome sets the Biome at the position passed. If a chunk is not yet loaded // at that position, the chunk is first loaded or generated if it could not be // found in the world save. -func (w *World) setBiome(pos cube.Pos, b Biome) { - if pos.OutOfBounds(w.Range()) { +func (tx *Tx) setBiome(pos cube.Pos, b Biome) { + if pos.OutOfBounds(tx.Range()) { // Fast way out. return } - c := w.chunk(chunkPosFromBlockPos(pos)) + c := tx.chunk(chunkPosFromBlockPos(pos)) c.modified = true c.SetBiome(uint8(pos[0]), int16(pos[1]), uint8(pos[2]), uint32(b.EncodeBiome())) } @@ -346,12 +480,13 @@ func (w *World) setBiome(pos cube.Pos, b Biome) { // will do so within much less time than separate setBlock calls would. The // method operates on a per-chunk basis, setting all blocks within a single // chunk part of the Structure before moving on to the next chunk. -func (w *World) buildStructure(pos cube.Pos, s Structure) { +func (tx *Tx) buildStructure(pos cube.Pos, s Structure) { + w := tx.World() dim := s.Dimensions() width, height, length := dim[0], dim[1], dim[2] maxX, maxY, maxZ := pos[0]+width, pos[1]+height, pos[2]+length f := func(x, y, z int) Block { - return w.block(cube.Pos{pos[0] + x, pos[1] + y, pos[2] + z}) + return tx.block(cube.Pos{pos[0] + x, pos[1] + y, pos[2] + z}) } // We approach this on a per-chunk basis, so that we can keep only one chunk @@ -361,7 +496,7 @@ func (w *World) buildStructure(pos cube.Pos, s Structure) { for chunkX := pos[0] >> 4; chunkX <= maxX>>4; chunkX++ { for chunkZ := pos[2] >> 4; chunkZ <= maxZ>>4; chunkZ++ { chunkPos := ChunkPos{int32(chunkX), int32(chunkZ)} - c := w.chunk(chunkPos) + c := tx.chunk(chunkPos) baseX, baseZ := chunkX<<4, chunkZ<<4 for i, sub := range c.Sub() { @@ -428,12 +563,13 @@ func (w *World) buildStructure(pos cube.Pos, s Structure) { // liquid attempts to return a Liquid block at the position passed. This // Liquid may be in the foreground or in any other layer. If found, the Liquid // is returned. If not, the bool returned is false. -func (w *World) liquid(pos cube.Pos) (Liquid, bool) { +func (tx *Tx) liquid(pos cube.Pos) (Liquid, bool) { + w := tx.World() if pos.OutOfBounds(w.Range()) { // Fast way out. return nil, false } - c := w.chunk(chunkPosFromBlockPos(pos)) + c := tx.chunk(chunkPosFromBlockPos(pos)) x, y, z := uint8(pos[0]), int16(pos[1]), uint8(pos[2]) id := c.Block(x, y, z, 0) @@ -462,16 +598,18 @@ func (w *World) liquid(pos cube.Pos) (Liquid, bool) { // there already is a Liquid at that position, in which case it will be // overwritten. If nil is passed for the Liquid, any Liquid currently present // will be removed. -func (w *World) setLiquid(pos cube.Pos, b Liquid) { +func (tx *Tx) setLiquid(pos cube.Pos, b Liquid) { + w := tx.World() if pos.OutOfBounds(w.Range()) { // Fast way out. return } chunkPos := chunkPosFromBlockPos(pos) - c := w.chunk(chunkPos) + c := tx.chunk(chunkPos) if b == nil { w.removeLiquids(c, pos) w.doBlockUpdatesAround(pos) + w.redstone.invalidateAround(pos, pos, RedstoneUpdateCauseBlockUpdate, w.Range()) return } x, y, z := uint8(pos[0]), int16(pos[1]), uint8(pos[2]) @@ -495,6 +633,7 @@ func (w *World) setLiquid(pos cube.Pos, b Liquid) { c.modified = true w.doBlockUpdatesAround(pos) + w.redstone.invalidateAround(pos, pos, RedstoneUpdateCauseBlockUpdate, w.Range()) } // removeLiquids removes any liquid blocks that may be present at a specific @@ -541,12 +680,13 @@ func (w *World) removeLiquidOnLayer(c *chunk.Chunk, x uint8, y int16, z, layer u // additionalLiquid checks if the block at a position has additional liquid on // another layer and returns the liquid if so. -func (w *World) additionalLiquid(pos cube.Pos) (Liquid, bool) { +func (tx *Tx) additionalLiquid(pos cube.Pos) (Liquid, bool) { + w := tx.World() if pos.OutOfBounds(w.Range()) { // Fast way out. return nil, false } - c := w.chunk(chunkPosFromBlockPos(pos)) + c := tx.chunk(chunkPosFromBlockPos(pos)) id := c.Block(uint8(pos[0]), int16(pos[1]), uint8(pos[2]), 1) b, ok := w.conf.Blocks.BlockByRuntimeID(id) @@ -562,7 +702,8 @@ func (w *World) additionalLiquid(pos cube.Pos) (Liquid, bool) { // the sky and block light. The light value returned is a value in the range // 0-15, where 0 means there is no light present, whereas 15 means the block is // fully lit. -func (w *World) light(pos cube.Pos) uint8 { +func (tx *Tx) light(pos cube.Pos) uint8 { + w := tx.World() if pos[1] < w.ra[0] { // Fast way out. return 0 @@ -571,14 +712,19 @@ func (w *World) light(pos cube.Pos) uint8 { // Above the rest of the world, so full skylight. return 15 } - return w.chunk(chunkPosFromBlockPos(pos)).Light(uint8(pos[0]), int16(pos[1]), uint8(pos[2])) + c, ok := w.loadedChunk(chunkPosFromBlockPos(pos)) + if !ok { + return 0 + } + return c.Light(uint8(pos[0]), int16(pos[1]), uint8(pos[2])) } // skyLight returns the skylight level at the position passed. This light level // is not influenced by blocks that emit light, such as torches. The light // value, similarly to light, is a value in the range 0-15, where 0 means no // light is present. -func (w *World) skyLight(pos cube.Pos) uint8 { +func (tx *Tx) skyLight(pos cube.Pos) uint8 { + w := tx.World() if pos[1] < w.ra[0] { // Fast way out. return 0 @@ -587,7 +733,7 @@ func (w *World) skyLight(pos cube.Pos) uint8 { // Above the rest of the world, so full skylight. return 15 } - return w.chunk(chunkPosFromBlockPos(pos)).SkyLight(uint8(pos[0]), int16(pos[1]), uint8(pos[2])) + return tx.chunk(chunkPosFromBlockPos(pos)).SkyLight(uint8(pos[0]), int16(pos[1]), uint8(pos[2])) } // Time returns the current time of the world. The time is incremented every @@ -657,13 +803,13 @@ func (w *World) enableTimeCycle(v bool) { // temperature returns the temperature in the World at a specific position. // Higher altitudes and different biomes influence the temperature returned. -func (w *World) temperature(pos cube.Pos) float64 { +func (tx *Tx) temperature(pos cube.Pos) float64 { const ( tempDrop = 1.0 / 600 seaLevel = 64 ) diff := max(pos[1]-seaLevel, 0) - return w.biome(pos).Temperature() - float64(diff)*tempDrop + return tx.biome(pos).Temperature() - float64(diff)*tempDrop } // addParticle spawns a Particle at a given position in the World. Viewers that @@ -678,7 +824,7 @@ func (w *World) addParticle(pos mgl64.Vec3, p Particle) { // playSound plays a sound at a specific position in the World. Viewers of that // position will be able to hear the sound if they are close enough. func (w *World) playSound(tx *Tx, pos mgl64.Vec3, s Sound) { - ctx := event.C(tx) + ctx := tx.Event() if w.Handler().HandleSound(ctx, s, pos); ctx.Cancelled() { return } @@ -694,11 +840,16 @@ func (w *World) playSound(tx *Tx, pos mgl64.Vec3, s Sound) { // loaded. addEntity panics if the EntityHandle is already in a world. // addEntity returns the Entity created by the EntityHandle. func (w *World) addEntity(tx *Tx, handle *EntityHandle) Entity { - handle.setAndUnlockWorld(w) - pos := chunkPosFromVec3(handle.data.Pos) - w.entities[handle] = pos + return w.addEntityAt(tx, handle, handle.data.Pos) +} - c := w.chunk(pos) +// addEntityAt adds an EntityHandle to a World at the position passed. +func (w *World) addEntityAt(tx *Tx, handle *EntityHandle, pos mgl64.Vec3) Entity { + handle.setAndUnlockWorldAt(w, pos) + chunkPos := chunkPosFromVec3(handle.data.Pos) + w.entities[handle] = chunkPos + + c := tx.chunk(chunkPos) c.Entities, c.modified = append(c.Entities, handle), true e := handle.mustEntity(tx) @@ -707,6 +858,7 @@ func (w *World) addEntity(tx *Tx, handle *EntityHandle) Entity { showEntity(e, v) } w.Handler().HandleEntitySpawn(tx, e) + handle.markWorldReady(w) return e } @@ -723,7 +875,7 @@ func (w *World) removeEntity(e Entity, tx *Tx) *EntityHandle { } w.Handler().HandleEntityDespawn(tx, e) - c := w.chunk(pos) + c := tx.chunk(pos) c.Entities, c.modified = sliceutil.DeleteVal(c.Entities, handle), true w.removeEntityFromViewLayers(e) @@ -1021,11 +1173,11 @@ func (w *World) PortalDestination(dim Dimension) *World { // Save saves the World to the provider. func (w *World) Save() { - <-w.Exec(w.save(w.saveChunk)) + <-w.exec(w.save(w.saveChunk)) } // save saves all loaded chunks to the World's provider. -func (w *World) save(f func(*Tx, ChunkPos, *Column)) ExecFunc { +func (w *World) save(f func(*Tx, ChunkPos, *Column)) execFunc { return func(tx *Tx) { if w.conf.ReadOnly { return @@ -1055,6 +1207,7 @@ func (w *World) saveChunk(_ *Tx, pos ChunkPos, c *Column) { func (w *World) closeChunk(tx *Tx, pos ChunkPos, c *Column) { w.saveChunk(tx, pos, c) w.scheduledUpdates.removeChunk(pos) + w.redstone.removeChunk(pos) // Note: We close c.Entities here because some entities may remove // themselves from the world in their Close method, which can lead to // unexpected conditions. @@ -1074,16 +1227,31 @@ func (w *World) Close() error { // close stops the World from ticking, saves all chunks to the Provider and // updates the world's settings. func (w *World) close() { - <-w.Exec(func(tx *Tx) { + w.scheduleMu.Lock() + w.closed.Store(true) + close(w.closeStarted) + w.scheduleMu.Unlock() + + w.scheduling.Wait() + w.scheduleMu.Lock() + w.closeAcceptingEntityTasks.Store(true) + w.scheduleMu.Unlock() + <-w.exec(func(tx *Tx) { // Let user code run anything that needs to be finished before closing. w.Handler().HandleClose(tx) + tx.runDeferred() w.Handle(NopHandler{}) w.save(w.closeChunk)(tx) }) + w.scheduleMu.Lock() + w.closeAcceptingEntityTasks.Store(false) + w.scheduleMu.Unlock() + w.scheduling.Wait() close(w.closing) w.running.Wait() + w.chunkWorkers.wg.Wait() close(w.queueClosing) w.queueing.Wait() @@ -1177,48 +1345,106 @@ func showEntity(e Entity, viewer Viewer) { viewer.ViewEntityArmour(e) } +// loadedChunk returns chunk & true only if chunk at position passed is loaded. +func (w *World) loadedChunk(pos ChunkPos) (*Column, bool) { + c, ok := w.chunks[pos] + return c, ok +} + // chunk reads a chunk from the position passed. If a chunk at that position is // not yet loaded, the chunk is loaded from the provider, or generated if it // did not yet exist. Additionally, chunks newly loaded have the light in them // calculated before they are returned. -func (w *World) chunk(pos ChunkPos) *Column { +func (tx *Tx) chunk(pos ChunkPos) *Column { + w := tx.World() c, ok := w.chunks[pos] if ok { return c } - c, err := w.loadChunk(pos) - chunk.LightArea([]*chunk.Chunk{c.Chunk}, int(pos[0]), int(pos[1])).Fill() + c, ok = w.chunkFromAsyncPool(tx, pos) + if ok { + return c + } + col, err := w.loadChunk(pos) if err != nil { w.conf.Log.Error("load chunk: "+err.Error(), "X", pos[0], "Z", pos[1]) - return c } - w.calculateLight(pos) - return c + if col == nil { + return w.emptyColumn() + } + return w.addChunk(pos, col) } -// loadChunk attempts to load a chunk from the provider, or generates a chunk -// if one doesn't currently exist. -func (w *World) loadChunk(pos ChunkPos) (*Column, error) { +// loadChunk loads a chunk from the provider, or generates a chunk if one +// doesn't currently exist, and calculates the light within it. +func (w *World) loadChunk(pos ChunkPos) (*chunk.Column, error) { column, err := w.conf.Provider.LoadColumn(pos, w.conf.Dim) - switch { - case err == nil: - col := w.columnFrom(column, pos) - w.chunks[pos] = col - for _, e := range col.Entities { - w.entities[e] = pos - e.w = w + if err != nil { + if !errors.Is(err, leveldb.ErrNotFound) { + return nil, err } - return col, nil - case errors.Is(err, leveldb.ErrNotFound): - // The provider doesn't have a chunk saved at this position, so we generate a new one. - col := newColumn(chunk.New(w.conf.Blocks, w.Range())) - w.chunks[pos] = col - - w.conf.Generator.GenerateChunk(pos, col.Chunk) - return col, nil - default: - return newColumn(chunk.New(w.conf.Blocks, w.Range())), err + ch := chunk.New(w.conf.Blocks, w.Range()) + w.conf.Generator.GenerateChunk(pos, ch) + column = &chunk.Column{Chunk: ch} + } + chunk.LightArea([]*chunk.Chunk{column.Chunk}, int(pos[0]), int(pos[1])).Fill() + return column, nil +} + +// emptyColumn returns an empty column, used as a stand-in when a chunk could +// not be loaded. +func (w *World) emptyColumn() *Column { + return w.columnFrom(&chunk.Column{Chunk: chunk.New(w.conf.Blocks, w.Range())}, ChunkPos{}) +} + +// loadChunkAsync loads or generates the chunk at pos in the background, +// calling callback once ready. It returns false if it could not be scheduled. +func (w *World) loadChunkAsync(tx *Tx, pos ChunkPos, callback chunkCallback) bool { + if c, ok := w.chunks[pos]; ok { + callback(tx, c) + return true + } + if w.conf.Synchronous { + // Synchronous worlds have no chunk workers; load on the calling goroutine. + callback(tx, tx.chunk(pos)) + return true + } + if req, ok := w.chunkRequests[pos]; ok { + req.callbacks = append(req.callbacks, callback) + return true + } + req := &chunkRequest{pos: pos, done: make(chan struct{}), callbacks: []chunkCallback{callback}} + if !w.chunkWorkers.schedule(req) { + return false } + w.chunkRequests[pos] = req + return true +} + +// addChunk adds a loaded or generated chunk to the world, spawning saved +// entities and spreading light to neighbouring chunks. The chunk passed must +// already have its own light calculated. +func (w *World) addChunk(pos ChunkPos, c *chunk.Column) *Column { + column := w.columnFrom(c, pos) + w.chunks[pos] = column + for _, e := range column.Entities { + w.entities[e] = pos + e.setAndUnlockWorld(w) + e.markWorldReady(w) + } + w.calculateLight(pos) + return column +} + +// chunkFromAsyncPool waits for a pending background load of the chunk at pos, +// returning false if none was underway. +func (w *World) chunkFromAsyncPool(tx *Tx, pos ChunkPos) (*Column, bool) { + req, ok := w.chunkRequests[pos] + if ok { + c := req.doImmediate(tx) + return c, c != nil + } + return nil, false } // calculateLight calculates the light in the chunk passed and spreads the @@ -1271,7 +1497,7 @@ func (w *World) autoSave() { for { select { case <-closeUnused.C: - <-w.Exec(w.closeUnusedChunks) + <-w.exec(w.closeUnusedChunks) case <-save.C: w.Save() case <-w.closing: @@ -1303,11 +1529,6 @@ type Column struct { loaders []*Loader } -// newColumn returns a new Column wrapper around the chunk.Chunk passed. -func newColumn(c *chunk.Chunk) *Column { - return &Column{Chunk: c, BlockEntities: map[cube.Pos]Block{}} -} - // columnTo converts a Column to a chunk.Column so that it can be written to // a provider. func (w *World) columnTo(col *Column, pos ChunkPos) *chunk.Column { diff --git a/server/world/world_test.go b/server/world/world_test.go new file mode 100644 index 0000000000..5ca43933ac --- /dev/null +++ b/server/world/world_test.go @@ -0,0 +1,259 @@ +package world + +import ( + "context" + "sync/atomic" + "testing" + "time" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl64" +) + +// TestSynchronousWorldDo verifies that Do on a synchronous World runs the task +// on the calling goroutine and returns a completed task. +func TestSynchronousWorldDo(t *testing.T) { + w := Config{Synchronous: true}.New() + defer w.Close() + + var ran bool + task := w.Do(func(tx *Tx) { ran = true }) + if !ran { + t.Fatal("expected task to have run when Do returned") + } + select { + case <-task.Done(): + default: + t.Fatal("expected task returned by Do to be done when Do returned") + } +} + +// TestSynchronousWorldAdvanceTick verifies that a synchronous World does not +// tick on its own and that AdvanceTick advances the current tick exactly once +// per call, even without any viewers. +func TestSynchronousWorldAdvanceTick(t *testing.T) { + w := Config{Synchronous: true}.New() + defer w.Close() + + current := func() int64 { + w.set.Lock() + defer w.set.Unlock() + return w.set.CurrentTick + } + start := current() + time.Sleep(time.Second / 10) + if got := current(); got != start { + t.Fatalf("expected no automatic ticking, tick advanced from %v to %v", start, got) + } + for range 5 { + w.AdvanceTick() + } + if got := current(); got != start+5 { + t.Fatalf("expected current tick %v after 5 AdvanceTick calls, got %v", start+5, got) + } +} + +func TestSynchronousEntityDoCanRemoveEntity(t *testing.T) { + w := Config{Synchronous: true}.New() + defer w.Close() + + h := EntitySpawnOpts{Position: mgl64.Vec3{0, 4, 0}}.New(testEntityType{}, testEntityConfig{}) + <-w.exec(func(tx *Tx) { + tx.AddEntity(h) + }) + + task := h.Do(func(tx *Tx, e Entity) { + tx.RemoveEntity(e) + }) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + if err := task.Wait(ctx); err != nil { + t.Fatalf("entity Do self-removal did not complete: %v", err) + } +} + +func TestSynchronousEntityDoWaitsForAddEntityToFinish(t *testing.T) { + w := Config{Synchronous: true}.New() + defer w.Close() + + state := &blockingOpenState{ + firstOpen: make(chan struct{}), + secondOpen: make(chan struct{}), + release: make(chan struct{}), + } + h := EntitySpawnOpts{}.New(blockingOpenType{}, blockingOpenConfig{state: state}) + task := h.Do(func(*Tx, Entity) {}) + added := make(chan struct{}) + go func() { + w.Do(func(tx *Tx) { tx.AddEntity(h) }) + close(added) + }() + <-state.firstOpen + + premature := false + select { + case <-state.secondOpen: + premature = true + case <-time.After(time.Millisecond * 50): + } + close(state.release) + <-added + if err := task.Wait(context.Background()); err != nil { + t.Fatalf("entity Do failed: %v", err) + } + if premature { + t.Fatal("entity callback opened before AddEntity completed") + } +} + +func TestSynchronousAdvanceTickTicksViewerlessEntities(t *testing.T) { + w := Config{Synchronous: true}.New() + defer w.Close() + + h := EntitySpawnOpts{Position: mgl64.Vec3{0, 4, 0}}.New(testEntityType{}, testEntityConfig{}) + <-w.exec(func(tx *Tx) { + tx.AddEntity(h) + }) + + start := h.data.Pos + for range 3 { + w.AdvanceTick() + } + if got := h.data.Pos; got == start { + t.Fatalf("expected entity position to change after ticking, got %v", got) + } +} + +func TestSynchronousAdvanceTickTicksViewerlessBlockEntities(t *testing.T) { + w := Config{Synchronous: true}.New() + defer w.Close() + + pos := cube.Pos{0, 4, 0} + tb := &testTickerBlock{} + <-w.exec(func(tx *Tx) { + col := tx.chunk(chunkPosFromBlockPos(pos)) + chest, ok := tx.World().conf.Blocks.BlockByName("minecraft:chest", map[string]any{"minecraft:cardinal_direction": "north"}) + if !ok { + t.Fatal("expected chest block to be registered") + } + col.SetBlock(uint8(pos[0]), int16(pos[1]), uint8(pos[2]), 0, tx.World().conf.Blocks.BlockRuntimeID(chest)) + col.BlockEntities[pos] = tb + }) + + w.AdvanceTick() + if tb.ticks == 0 { + t.Fatal("expected block entity to tick") + } +} + +type testEntityConfig struct{} + +func (testEntityConfig) Apply(*EntityData) {} + +type testEntityType struct{} + +func (testEntityType) Open(_ *Tx, handle *EntityHandle, data *EntityData) Entity { + return &testEntity{handle: handle, data: data} +} + +func (testEntityType) EncodeEntity() string { + return "dragonfly:test_entity" +} + +func (testEntityType) BBox(Entity) cube.BBox { + return cube.Box(0, 0, 0, 1, 1, 1) +} + +func (testEntityType) DecodeNBT(map[string]any, *EntityData) {} + +func (testEntityType) EncodeNBT(*EntityData) map[string]any { + return nil +} + +type testEntity struct { + handle *EntityHandle + data *EntityData +} + +func (e *testEntity) Close() error { + return nil +} + +func (e *testEntity) H() *EntityHandle { + return e.handle +} + +func (e *testEntity) Position() mgl64.Vec3 { + return e.data.Pos +} + +func (e *testEntity) Rotation() cube.Rotation { + return e.data.Rot +} + +func (e *testEntity) Tick(*Tx, int64) { + e.data.Pos = e.data.Pos.Add(mgl64.Vec3{0, -0.1, 0}) +} + +type testTickerBlock struct { + ticks int +} + +type blockingOpenState struct { + opens atomic.Int32 + firstOpen chan struct{} + secondOpen chan struct{} + release chan struct{} +} + +type blockingOpenConfig struct { + state *blockingOpenState +} + +func (c blockingOpenConfig) Apply(data *EntityData) { data.Data = c.state } + +type blockingOpenType struct{} + +func (blockingOpenType) Open(_ *Tx, handle *EntityHandle, data *EntityData) Entity { + state := data.Data.(*blockingOpenState) + switch state.opens.Add(1) { + case 1: + close(state.firstOpen) + <-state.release + case 2: + close(state.secondOpen) + } + return &testEntity{handle: handle, data: data} +} + +func (blockingOpenType) EncodeEntity() string { return "dragonfly:blocking_open" } + +func (blockingOpenType) BBox(Entity) cube.BBox { return cube.BBox{} } + +func (blockingOpenType) DecodeNBT(map[string]any, *EntityData) {} + +func (blockingOpenType) EncodeNBT(*EntityData) map[string]any { return nil } + +func (*testTickerBlock) EncodeBlock() (string, map[string]any) { + return "dragonfly:test_ticker", nil +} + +func (*testTickerBlock) Hash() (uint64, uint64) { + return 1<<32 - 1, 0 +} + +func (*testTickerBlock) Model() BlockModel { + return unknownModel{} +} + +func (*testTickerBlock) DecodeNBT(map[string]any) any { + return &testTickerBlock{} +} + +func (*testTickerBlock) EncodeNBT() map[string]any { + return nil +} + +func (b *testTickerBlock) Tick(int64, cube.Pos, *Tx) { + b.ticks++ +} From a365ecfbe1f70b65b6716a25e76b9c2ebb14626e Mon Sep 17 00:00:00 2001 From: Zephyr-OP Date: Tue, 18 Aug 2026 23:22:22 -0600 Subject: [PATCH 2/2] 1.26.44 follow-up: recipe item fix, dependency bumps --- go.mod | 42 +++++++-- go.sum | 129 ++++++++++++++++++--------- server/item/recipe/crafting_data.nbt | Bin 900455 -> 1135160 bytes server/item/recipe/item.go | 68 ++++++++------ server/item/recipe/potion_data.nbt | Bin 33442 -> 33442 bytes server/item/recipe/recipe.go | 39 ++++++++ server/item/recipe/vanilla.go | 30 ++++++- server/session/player.go | 19 ++++ 8 files changed, 249 insertions(+), 78 deletions(-) diff --git a/go.mod b/go.mod index da45a453f3..7c54a00ad3 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/go-gl/mathgl v1.2.0 github.com/google/uuid v1.6.0 github.com/pelletier/go-toml v1.9.5 - github.com/sandertv/gophertunnel v1.57.0 + github.com/sandertv/gophertunnel v1.59.0 github.com/segmentio/fasthash v1.0.3 github.com/thomaso-mirodin/intmath v0.0.0-20160323211736-5dc6d854e46e github.com/zaataylor/cartesian v0.0.0-20221028053253-3b3244d82727 @@ -20,15 +20,41 @@ require ( ) require ( + github.com/coder/websocket v1.8.14 // indirect + github.com/coreos/go-oidc/v3 v3.17.0 // indirect + github.com/df-mc/go-nethernet v1.0.20 // indirect + github.com/df-mc/go-playfab/v2 v2.0.2 // indirect + github.com/df-mc/go-xsapi/v2 v2.0.3 // indirect github.com/df-mc/jsonc v1.0.5 // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // indirect - github.com/golang/snappy v1.0.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/klauspost/compress v1.18.4 // indirect - github.com/sandertv/go-raknet v1.15.1-0.20260112202637-beca0b10c217 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/oauth2 v0.29.0 // indirect - golang.org/x/sync v0.13.0 // indirect + github.com/nxadm/tail v1.4.11 // indirect + github.com/pion/datachannel v1.6.2 // indirect + github.com/pion/dtls/v3 v3.1.4 // indirect + github.com/pion/ice/v4 v4.2.7 // indirect + github.com/pion/interceptor v0.1.45 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/mdns/v2 v2.1.0 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.16 // indirect + github.com/pion/rtp v1.10.2 // indirect + github.com/pion/sctp v1.10.2 // indirect + github.com/pion/sdp/v3 v3.0.19 // indirect + github.com/pion/srtp/v3 v3.0.12 // indirect + github.com/pion/stun/v3 v3.1.6 // indirect + github.com/pion/transport/v4 v4.0.2 // indirect + github.com/pion/turn/v5 v5.0.10 // indirect + github.com/pion/webrtc/v4 v4.2.16-0.20260627075746-7a223a6f4d4f // indirect + github.com/sandertv/go-raknet v1.15.2-0.20260705184311-0d1fd09e2cf6 // indirect + github.com/wlynxg/anet v0.0.5 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/time v0.10.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 73a9b99773..b86e309292 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,18 @@ github.com/brentp/intintmap v0.0.0-20251106190759-56907b1f8479 h1:UZbbt19ACBOFO+ github.com/brentp/intintmap v0.0.0-20251106190759-56907b1f8479/go.mod h1:TOk10ahXejq9wkEaym3KPRNeuR/h5Jx+s8QRWIa2oTM= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cooldogedev/gophertunnel v0.0.0-20251216175446-7b35fac7a626 h1:eCA3OnjCPklA+Db+ApkBGiCLsGjOVgfX2U+S+NeHxCg= -github.com/cooldogedev/gophertunnel v0.0.0-20251216175446-7b35fac7a626/go.mod h1:IhLg93aMPY/rKgB7lKxTzOZ6i11OgkL0WinVs+gMvlI= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/df-mc/go-nethernet v1.0.20 h1:H0MO0GkVlAypu9IDQhzg5Pph3mCD7r40+ExiImsXT2M= +github.com/df-mc/go-nethernet v1.0.20/go.mod h1:11MfQo5F7PDKfHg1H0zM5mMkrCv29jRE0lK5B4egW9w= +github.com/df-mc/go-playfab/v2 v2.0.2 h1:JqOsrpe0nt6Ni4V2JrdmOAhbBgTm7LBw1ppGXlmknhs= +github.com/df-mc/go-playfab/v2 v2.0.2/go.mod h1:CS3FV3tJlohvGQiX0IJiaYKOVNfy0p778B84oEVyxlw= +github.com/df-mc/go-xsapi/v2 v2.0.3 h1:fQLw4k15OinFhagWjbSj0BwQXeDUmY8a1J8yquEod3w= +github.com/df-mc/go-xsapi/v2 v2.0.3/go.mod h1:Gi/zQG2DFMJOMt4DIjuBuINTnU4YnZINe+RyFyza8oo= github.com/df-mc/goleveldb v1.1.9 h1:ihdosZyy5jkQKrxucTQmN90jq/2lUwQnJZjIYIC/9YU= github.com/df-mc/goleveldb v1.1.9/go.mod h1:+NHCup03Sci5q84APIA21z3iPZCuk6m6ABtg4nANCSk= github.com/df-mc/jsonc v1.0.5 h1:O7oh07kbS5AYY+l2Fji6l4h0iHcdjKbxCtK5VlZlLMU= @@ -13,73 +21,114 @@ github.com/df-mc/jsonc v1.0.5/go.mod h1:+Q++JuCE9IKiP8v7sWImdf/RjQX0nfXyfX6PdfTT github.com/df-mc/worldupgrader v1.0.21 h1:Qr4/QB8ek7En0vkTuRXYq4FrZM0HHSOXsJOL7Ko4Cjg= github.com/df-mc/worldupgrader v1.0.21/go.mod h1:tsSOLTRm9mpG7VHvYpAjjZrkRHWmSbKZAm9bOLNnlDk= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-gl/mathgl v1.2.0 h1:v2eOj/y1B2afDxF6URV1qCYmo1KW08lAMtTbOn3KXCY= github.com/go-gl/mathgl v1.2.0/go.mod h1:pf9+b5J3LFP7iZ4XXaVzZrCle0Q/vNpB/vDe5+3ulRE= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= -github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pion/datachannel v1.6.2 h1:7EXQ8TH3vTouBUdRWYbcX2edSx9Yj6k5zl5P+qyxEPc= +github.com/pion/datachannel v1.6.2/go.mod h1:pzbdAZvyGtXbcHM1hBbsFaOTf40lZizU/dNlvVOak6E= +github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY= +github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc= +github.com/pion/ice/v4 v4.2.7 h1:zDEbC6MiEdhQpF8TxBOTws+NU6ZgGpveHrQq4Lc1kao= +github.com/pion/ice/v4 v4.2.7/go.mod h1:9SNPaq0c7El/ki8leJzyCkK10zsskprR3zTNbO3monY= +github.com/pion/interceptor v0.1.45 h1:6PUo/5829bIfRFIPPJQzuDn8EjxRTSB/CSD7QVCOaqo= +github.com/pion/interceptor v0.1.45/go.mod h1:gNDYM/uFKcLe/B3gS2/7+aw6z+RDiMy2qKTnF1LO31w= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY= +github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= +github.com/pion/rtp v1.10.2 h1:l+f6tTDcAH6xwepaAoW791ddhuYsJlqRATOzirO04Mo= +github.com/pion/rtp v1.10.2/go.mod h1:Au8fc6cEByy8RLTwKTQTEeQqDB/SJDxwL4mZuxYA5Pk= +github.com/pion/sctp v1.10.2 h1:6aezYsMrHAwpjJ6kUdyCiWPqZwgToT00ponT7seJ6a4= +github.com/pion/sctp v1.10.2/go.mod h1:7KFmTwLcoYgJs/Z+99nJvsWL0qDpuyloSI0RbAqlrz0= +github.com/pion/sdp/v3 v3.0.19 h1:1VMKs3gIkTQV5M3hNKfTAPrDXSNrYtOlmOD8+mSZUGQ= +github.com/pion/sdp/v3 v3.0.19/go.mod h1:dE5WOSlzXrtiE/iuZqe9n+AcEbOjtAd3k5m5NtlV/qU= +github.com/pion/srtp/v3 v3.0.12 h1:U7V17bckl7sI4mb3sepiojByDuBY0wNCqQE+6IlQBbc= +github.com/pion/srtp/v3 v3.0.12/go.mod h1:EeZOi/sd6glM1EXapg051gdNWO9yWT1YSsgQ4SlJkns= +github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8= +github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs= +github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= +github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= +github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk= +github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= +github.com/pion/turn/v5 v5.0.10 h1:mOMZjudflXpte5OsCnXztpUKwNXcpXIAzMBnq9TXOSQ= +github.com/pion/turn/v5 v5.0.10/go.mod h1:u3XjBqy2Z4+NhCUpDoOSsNuQDrPLvKStlCGWk6sTQ1E= +github.com/pion/webrtc/v4 v4.2.16-0.20260627075746-7a223a6f4d4f h1:m39L6AcxVDq1Gwm9/c6QGCiksJ9xoYE4TQyebl9pMu4= +github.com/pion/webrtc/v4 v4.2.16-0.20260627075746-7a223a6f4d4f/go.mod h1:g/C+nTxS7qM2dBr1hRK56OTTD9zFzQDwZi3fMfZxFNM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sandertv/go-raknet v1.15.1-0.20260112202637-beca0b10c217 h1:UZQq2253Q+7co/C9Et62RYPBggzz+L+2yqGlvQhSNM8= -github.com/sandertv/go-raknet v1.15.1-0.20260112202637-beca0b10c217/go.mod h1:/yysjwfCXm2+2OY8mBazLzcxJ3irnylKCyG3FLgUPVU= -github.com/sandertv/gophertunnel v1.57.0 h1:UkgVg1xLCsOSm79rP09WmodGSHgA8M7+l4quL01cIL8= -github.com/sandertv/gophertunnel v1.57.0/go.mod h1:W4VnrX9AIPIVXNDMEIKMIRj1T80EdOgdqXpGbQpyAbE= +github.com/sandertv/go-raknet v1.15.2-0.20260705184311-0d1fd09e2cf6 h1:Oj7QsiwWTOLmCpOjjmW0Qwde4BE8F5+Kjz817qzICyk= +github.com/sandertv/go-raknet v1.15.2-0.20260705184311-0d1fd09e2cf6/go.mod h1:/yysjwfCXm2+2OY8mBazLzcxJ3irnylKCyG3FLgUPVU= +github.com/sandertv/gophertunnel v1.59.0 h1:hIjmLycavwSjrqRlKFDMiV8LrP64tgzjgEUqWOOxvuQ= +github.com/sandertv/gophertunnel v1.59.0/go.mod h1:JMKsf8DmVmn+qEVJ1IA9m7zEo9qHJ3ZN6IEvySEk9bs= 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/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/thomaso-mirodin/intmath v0.0.0-20160323211736-5dc6d854e46e h1:cR8/SYRgyQCt5cNCMniB/ZScMkhI9nk8U5C7SbISXjo= -github.com/thomaso-mirodin/intmath v0.0.0-20160323211736-5dc6d854e46e/go.mod h1:Tu4lItkATkonrYuvtVjG0/rhy15qrNGNTjPdaphtZ/8= -github.com/zaataylor/cartesian v0.0.0-20221028053253-3b3244d82727 h1:eP/MDnwQtFR2hwUv84a5kD5bKgjx3wir69Nhy33Pl4I= -github.com/zaataylor/cartesian v0.0.0-20221028053253-3b3244d82727/go.mod h1:gEXybXQbaNi8PEDSaJ/a9AEG8tiqtnEaPtKFugCAjwU= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588= +golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= -golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/item/recipe/crafting_data.nbt b/server/item/recipe/crafting_data.nbt index 2a229328462414683d00d592eb96080c276e4b07..5ee024e95699261768bd4f499b125b94bd058314 100644 GIT binary patch literal 1135160 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

Eed=SgkWq1clNqqOK8|I;+r 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 delta 49014 zcmb@vcbpT|`ak}e$t0Oc+08C2EbOuyKsqb|kzOLb6Gc#@Y!r|pvJnwG#9j~yjO4)v zu_A&HU1e-Qyn3(Yszk5XiV(YEA+~D+`Mu9Gsesq}`o4aDuuNvoocf%nzMnHs+&AL$ z-+IS)O8P#_bc+38EhFD#$*FrSi^;!Ns%uQjFAKYfIq3+k(o6znjRj2NL zvxFwK)H;#xYgNLT@Y%{9!V?rNnQ>WLD9&6U=+2eFl_;cB#AMAHw zbcU+~X0;Troz-8d)(^Mspr_AhLNgmdY=_%6*Fznu$dOVy>D}A6$0R>aOUmRb{iSwh zPhM*GpdL7}O!v2Fr-#?rD74sqp4r#w9n2EbJKU{%t87$#L~&BuA=z|qM^%5XW3@i9 zV=Fz`(Lw9Kbb^R$tQ4}lxQJcl8-P}r#oHR-g4!MjHeWXf#Qs0te7WL>G&zt>8Z+t{gRCt)0^oRS_ z(B4Z_m0l{5O3AI+ODOekxuu?-YBTBUVO0F-tIigBd`K5PczbT`OR{UP-}Yb+J@mj} zy>zwtu<>BOVVH&o`f@AYBOryEfn|+{2izm^YJ1+gg~#R{w1R)9PiRMp`D#^Tiy`C9 z5(V@yGfNm9K%&(YoQz42w0G(CC$BXp!{34@W9ZP)^m2(*k{iM>Ju#$;JR`s+Qp3kn zN^+Fzo?#d2-dSaOVEAV8o)6Lu40q@|hIQqezL7mC@(wzC#RyfOG{Qs4X)3tXn|#NE znO@3_M{qpUI3wDv)TJ*TnJTQP-X!ntqbnLqMxP_bvXPFgwzkrJV{gKb+o)^U< zVqNX}%3s#&iOo%E>~ofKJu-f?=wlf!#_M|~Z=%$dj+V6l4$Z5_PaCe!ns}KWo;VHv zdf@4!6q1}h*WThIuOvAragJ=)50g_5#w&Htt$p>>(qpd^cXhyXP5<|dRpk3aJB@tL zXjNF$mianp(RyqT-&of~OnMh=AxMp}*i^O4TrDWs+T$^wp?A^sjxH3hbd(qOA$WV4 z?p-|FHr25~=R65k-qFBg1hGHW;n1hhJdA>lCg_|k;nWLqioLUy$ zpcPLxcVg{alWNw^6-4uf9=mjC!Ek*STU?at>>kIIMe)0^7d+Q)$PX>p#Zjeizx2d{ zcf=GG56GM@3Zz{winMi$emCI~e=$%!zPm3)>OF2fd3Lk>po&I~Dpm?X-$o3P(syVB zizm%oUeDyr@)lo+ec*c;?}k_S^CRGSj)XKl6e=}ajf8S@4Hbl+oUF%R!&c+#SHyN0 zL9u;WQwsjzXja^V#^CMUC3BG6-#n~8HI$k2L!#TOQ2GOCl z(Ji{?qLgT<7o|r#+EC(RhhV9NyU(P~?1G*O9{B`>=`c!Yjxt(ts>7;3HNvMSR+Jq@ z^pSsKdc?+wd9iUaTfo-nx%?uD_poPz`vi0Q7SEq4W8P#Eqm8dSno#{^j`pHcjfoZS zNfdt7*@QfGj_wc!qqyB;iKAc#WMtNU@?ky6`-H|-@q5Q1#757xGbBB>u!A1EuDEmo zuj?b`z??KNy!@o)IsUv$n*pw--nPq}u~W{2#O1noX-N@&()#y)exzCh8E0P{P@T{gc zY*Y2bjWZk5TPI6;WJet($6>#F&VeLg;~@S#2sJU>T_yyE9*@tY#6L><=;D2pl4pw*7Qm|=A;7Ja{HerF z8v~`Sxrk>+`-EaZ=~7s~k#G zzvFf;Hv`#z*!6pNUX}gPyfOXv36kKodhA?HKjjH1gL}s4fy9Aq3A;XdR@vIQr(!7c zDL%2(-RS?@63ek2Kt=Z*r(fj#wWKeP)w{eNb<8wPtvz!}!Td54o!KE`gy{#|klJc# zR~}1d=Ipv^sSOI3iT)zOCi>2ig?X?DAS1NA{HE!yw&Z}XfN+sUjJr$<;}4WHnq&s#~b z&IdtJ7KTFl1n9=)P&muizSE47Y0FitaI$R}S?6*_Pq!-My9j3O=yzvh*OujOG>(3E zoKN==2= z!KqLD;Ht)5AFfdK?M>Lk2vWpmv$4UePdwmc%Yp*m+q&q!L+vOQx15NJDdt7wPj zPHsisCem06Pq5vo$5z|K#a2C#u8aO^E7R{xSJAs)SfaZByVJx>!ALl%9fkc8{0Jp- z)96H7A1Li^z55R^42m8n?{vfNDU~&pvD2x)S({PtfTikBO8Z;Fe3*Cu%<==fl}`T+ zo|%Hu%P3qAM%vIqEa#71L>CuPc$3_pIce!3Yj1Ju97^4ZG1h)<*M0w~5cXtE=7*IA zexw9=sr2~h7W&{r&7&@51kY)_656fr(CPyFqmZpJ3BfF(wQJ?>T06y9qs7sF5*?Z54lDX!Sd#K?5VVNiWt@5bQaH=qw)uV5;V^wq!lWh8mF!gsL! zU$*w-tA!`yI9HF?OFo6Z`NDQTi4GYJsa*Q-*;3-sUudZfT)J z+pe^=q3~|>`sT0YZhGoKJCw~3Q+iE?m;!4BF}diWa?oz?sg`DxQqhIY<&w)tc%*C5 z)5TE0@z`Tr{EpIGGL|=yo|MURt*e^5!tDo|!voQ}q9cU{!^7hpY%R~-rWKXaXb@sK zi9+HRCg*^sidr_4+_by7WTolNA%qewtQJ-rBjlI2S4t;g#XIKi z7^rSK#C}Mb8=a$+8>h6C{?6PDdwh+X?yQo87YEW|3^rzsRBhVT=93l_uaUYI-l?SI zI(wDTtybD*H;GxaT-O2J?i*5;=H|9mqU70_pL;t>tz~!;PojjX3=ljZ1}~wLB2mRo zpLLPa>@TE5S5)vFDPw5*9E+QRoupbl@?Ia~wr-MJVrDjJKa7jjb)ax>52-B;dm8eS zos+@g_Ok^|^njdowx>W(X_ol8iaZZFtHc{?DSVw&M*j711I53!uH_+l_=*jVcQYD4oQ3mVAh)(2f zsZ{ByZ=s0_77-aHo%Cmax|^#yW(d}I@N9T555C}Z8uhPq6XT9fBvG?lXt8FJVySGC79DQr0d0>ZWKJE zR*7Xtl$;A!X$%ma$m#tP0{Lzg98*DBL z&6OY%Vz%<)t0~NH=)t+zx9PcVz?G`dk&v_6bl`@& zmsn}fd~AYeFO>R0>VU{@#tzxONE*s~^#AQ=g`Tu~C_K#Rq3>T&T2NvqrYn8A!)o+D zPqNtPl75zIcDT~S3ovkbx)XGLirm^<`_uxdOLVfbi((&v-Ej%A>a_be>?Hi0LcRy3 z*GUu;tNgN9I5(ACikT545gmkLu1nz>%u|Fvgst{hWrt9*&TjhOjQFL}A2!;qXr&H| zW6s>rlDV_bT1si3tv`7ugC*v7e`u@IO-HVhTGFvs?^C>wyPQ%^MWw|zNM{x>gJTPu z6&ORVl`6m|*SKpat|^-S?fU~wM&f-&8bR^55Kst1Dkh;Dq+hbaiWsUON+9#2wiJ3- zt2R2`B-PnWim162Q#0*WOwIJQ(pWt)^hBd%jpQ?xemd%D`pV&yd<13FyCBE9>ynFY z7L9I-NT<-}qtzxXR?OF)TPqDR4&N$4jDi(Fa1hqG<}!6SrG{I41`J4eR99DNretK! zPfaA~yXPHcYc5coV7ILu$^4wkbi(MIWD3iN(g4Ct!Iii}GIt&dZKKp=xtl0N zfpNAK;yv@$j;EwcCByfLAc90+M1|^~kVcVDu^<3*fuc~lyX`8&|BW=c@UhT4^Hp%I zMFIzk3O0PM4N)NaytGhE3jXhT>{CjfW;=&fT3q;!^E~c_J|22e`iMO5OAGUZ33kGe z8|K*_GSFI4ACq58L9@oMrEbTmF|Za52_?Gl!Y?{KoDx1;K^yVcp%#gfxM$u{iW=Ru?4udw8-GoY$>kDfm<-g}_k96fKD`^THFebO^fX)^Q012xdy zQi;(2Np#%#`io8#&0TIb{eoWZ!C=3WREr=j4EfkO%uX~mn=} z`5F7U;b*Yw)gzs~DL5E*dW_g<8|FB%2 zU2->_Cc*oGh49i964bTXZLK{i)mAbip|PJW7^H0NOSpoo=M~nX9JE0OhloD|yDIgq zgDmIDryE|2{C0*@#uUUW?iqO=1@5(>FFT=m2z8!tzZRqdBl>^4fE2n_by1+1Y%^Z8 z$yeE^wnDlZ3bGlOHuk&aSHMsY%BBV{s&!NQG;G=69}xP5t#s$Gqp_+?e$C334EdYG znN^^jb8*2M+6v9Q=w-weahn2_a*)E6a%W0?Yp?G;1!j+3Si+p4SPL1>x9xI! zQ5^CMVfsN@^uC~3Z^RI1Lz4= zzX$n;Dc#}vFg+Fx9po0wlEXh|d>Gq1$ld6hf6FI9M-=i2;c4=Al%dTF|G_gI<(70X z46&AOX@`=SAy&iNNp7LA{gCzaRZx95S)&=H7IuU*(POUz5wH$^x!psd3ND)=w>7V9 zKDmw1Xb{!9L)DCqb#luL4FQjg^X6Nl&>0C3?xTOx||A+D?!7N`-uDNs%71Lc6Y2hh)`WkEyia zo#7N;taQx1-BYYq{^u+TPEx99&x!J6Vf00@GwNWXg7j=samw9(#c5o zmYtFy^bVq6seZu^wML+?{5cbUq1zHqHM;l%j59V$g4JWeMw~SfblDd=PsAuKkJegS z{P{&EN|iv!b?*mX=vM=w^fL^BDcds$^s{2E+MJTltLIXpn{aGg3`9a=gm>+eSkm4w z50axWud4>jRl>q6JKmLnr(T8OBeZKMJiiZ3mwFnWVRCOx416zj*DPRK+ffg27x4WG z1Hvn`rAhrQFH>p~h*~&DL_8iU=S1PTJsC}9f7)nUG^16SQ54kjZwgKXX$#ba5e~>N zsHVc0oGEuQO;m`HYVx0{XcRvayo7g7_UHKGpYM@>8kE%T(=bEt&XmvRt{UDMa+yiN z!UDF>Zdg3$!Uap|?&(lA1TTeP0qv%Pa&z*wE^TGJ9h7@pOvRp|Aq~&LZ*t;#7)NZ> z2|v5xnJa4$J`1s#0<+~aDD{B54f&VIR>Oaee6xjWUQ#BD%F8LTPzE_Hls}Tmx5`yR z!GAb48h@VbGhKS3HbQMD5G>SAzd8u;AU)WgRGH-?j30c@IG9I@uGwUT&>P?xjl>%$qlN z36)hkT2p+K)Pj6%fy6kt5hPkW5qd57S6S|7elItp`ZjQd!aIXl${i>?>lN^guXj74 zrrwtcstF-Sfyc2&vt4Z^rV9Ic3H1GAuRywxUo_+<*+KC;Rk#7Zw|FVs!3t{H0r7+f zravryW#HaQ*<|nn(LwPIY8eelIY$}(Rr2q!4n@Sk`!6h?_ffS4y>b>XO$+Ui5SuY9 zk8p179!g#-myrEiYe&OFag7s_crfdU)Fiq>LA_iT`#qSz@gs8e^z__2yuYCZL;%;=0$%}0;RR+@5z0Q^tn68yj`cd?6#3uP= zi`a}dTD=$pOFt~P5ZynMHZ9V63k}iOvqgRys=(`znXsV)WlB+ZEw=uA$o3u$#|(77MZeE#)lsQwgtg z_5yK9`1y>#|4sfwCVvIo8YN#^N&y6e%R2s%v%MZW(%PiM`Xp8>@eeS6PX}9f@n;A( zMjc*|n=vSE51OVkcG+gqEl`peEqBJO7-A*;q+FUqBGd~CFx%upQ?H%<3# z;t|PX40%(&M#8oXQnJPZ?1l*dY$g03!s<0({@!olR)bf9yyNTPz8LZzCV%^0`6Ree zHHwW>UZv3bl6HcI=1^*!at8a;vPIoAX(Noc*d&aU8CCWH*P+qk?-2}OqcGV^*+QYy z-Cn5k%HziJee!9F>2qR7MEWDyORM`TCz;*|ZZbUAo@Ci=XrIb0?Ls-xXkxh(pl}6n z4KogaxjbGjy+jQkOScI6DOB=J`MLdDxt&Q*fmK*s{H&lZ?<*VVwV&l0N*zX{!SCg( zfFgiM+W)ashfoC6HzsZo=FpUY!d ziW{N-$S2E)cyuu>{O}i8_4C)*1{vLcmFLU6Iap@$gPtv{gQ%gq6#=Eu)&%?N*jx%9 zmQ4!`7q>Hfab3_-x^Anbr|C4wzhZjmii!)qD2kijpyY{CRdj`k0nJBmLeWmPrcv3l z{pjdT=zXc3pcb>Jfk4ki7GWpACYCMB$a99>PT1y7BWbt5a^u;WbvNesmWk?c1ZYnb z+|5KK88N5jT*+L5n#Zu~J=bcjD0H{&MWC|^Y!Rcn!~$t1wyeI<2d-xa!Ol2Ct)%oI zD<&E|*nqax(<)*$v?mCC4BEiQgq_ZXZ{R6|5yrpTh^2)yaGqLWyxYt&CF8!?)6t@? zdqe6^q1{SL-SZ^MwzYgH!XYFc8MoJ1_JdiL~$Xe zqu}2Tb9Bi5y9?a#oXL*Ehaz`eDMohe!(@C z*0lS5CjxJ)!KsAs;Q_vs z<1@iYmiAB~><#St3IEI4#0Ui}^LZ;~x)V4b(mnnT%sv1H0jYA*uG1|Jda4|;!qjAY zFG?BG1;=-^RZ^gnvlW&6$EFz{OtYK}WkyW>kuyQI?wJ-19&anaTlE$o2i}x-i8ZYf zPiv1;&M!=Thwjr6S-=a?y_Pq?C0wrPbW6>DEpDLra7k>h3rq|{s>afp*NtvE8x!j5 zYhPr*tu_|%Ets>EoQ=^Pm!-ah>M>j+%W5ImOYP4{iRTgB$6$Gtd>VvX>CrFS6WeBDqm zWuexIZ2;F_$Z`)QS6Eh&cbNqeVT;>ioO-_H@>XI$zQ}@xJ>`+54jK}%G*ILZSA~JA zmnAcXYeJppLDbZ)wUlmxgg`i6fu@j?Fyz@$;uTMAGLHvNN0j`x=iecV_)){x{L68r)uRcrsKv_){;q3uXGB?7ozl4ZnvIrs=C{) zk#oAURFAFfdi)C0*}QetU%y=L)RVUs07#Mj5uHI8$Rc<$6IyAAECdTgvq2JDEWocJn z;XH-#1-5dxj6TIE2`l-#I4e2A1XtSFXt~mp7C!|_p74~VyDB-BHq4v1Xs-Tjx{@_R zN?##OH7cLBJnS-eNANV*fm>ey^V|Od5;tym$=SjPzG#Vn6kc_XV0A)Su%uxz?R^z+ zxY+j=1b2mW;`vJ1r4fvVh&7%>;s?dvwp_{{n1;8Z;Prh6)KoUj*~uf7FIYPFy!vyN zoORyZ`t!~T(OJ#y<2ejtc;B^D+rYif%5zlVr2?j;g!~6By#Qs-{bBn{&?vrXyEh7`__;=Ah7L;6|&0O0(#1SUmB| z81=PmDfZvy9N^1%Gw%8Vn~~$Q9BIJMDypfFuE~1`8@{x3Vy8(qz7O~eoP+&{0Ue&N zEVJ^iL&nzdLxJC*)P7hMzs$1%|AUuieFbHOwIHF@3D8Z(Gqu#xlA#>r{fD$B>pC<+ zae#6(CI1hWQ%%JMZhO;hfqAhT_1{{~&lr`N8trS}vC+3_OP%l=_RN@S5v(Z&k+Dht z!kU|%6P@zoc0fB-(VmkRxAXnx9z=D*+vFCk$G?K^(51b{RhT*N5qVCr zXI^AOLS(>DaDZxKeB0_8z&GxiZOw+U1cI>m;bZ66`-xEgGto^Y$Fv=9t5nj#yTJ}j zqra_E#xY;hHUI?7LlfF4&7*CUIhlV@+RwCKZG7HF+1cFeFKc5BeHE_(*^Du!=DFZ( zKm^uE4piD?fC|5W&XmDQAK|HCS0{(pptx4K5V`&prY3X6+)v&A3UrheKtLYnVXka* z@@!J7cKDUg6y^^5hCz#-HB!WuM=4_oaY#EYKM58|_z;ARh?2ssC@m)M$tc%#q|)?% zd#jglFHa)R$Z9Qxl8HF7vzqcV1H%;?CkmV z29yfoK%>y$jf#M>&tgi;S#N=ZUp5Jkx2NP`rV$y}OFNQSd*P5ad{dPhWIcU5AQ97) zpBT<6@>S9!6pyign$n<|zD#bXeztP9ekk3~G{5~9D0K!Pj~|-$35Dh;3n+39?BSf9 zDC(@_O&{H_v=ac8$rS4aztP@#3Y^2R73jR?)^?@_j(g342f{WMSTEz|`O14{Y&X2i ziC~HZx6PhaHaAOPo|XZM#i7ifyXc$(8bcF?@Qqzh{%fq0*oF}vN#lh@$~o$B((A-N zhuF=@iX6q{h~l{P^k+{^3X1yS5^Xsg9bpRJD=ReKd$La)r`~ z&F>voD61J4wTSL)3lkI%R+4WE_No-5lp{TanaeWgjEG4Uypqa@47(7kVMVAh4M( zaCwO-*ipL7*iA|b7P{yWt3UA#SZeeLOh<(~HVX~`KI|T)i)ji9_61if%B|j@TwsQI zv(PYu(rw%@g8u+X2{SMmQxMK#1jq(-f8l=1Ao4mPA;6rTwz6x2T@L4NRy;I)2;5mG zS0EI9j0>+NlWTkmh^*#Ouu6_~B7cmYPKyvGg=I}_gYK8y3U(ma zgjfxKOs7SZ_Dg#i9oV9b$}OD8Nu!q@Qul#?| zP8Jyq z=h`IsRs@oiwj#1$rZ${?!Q3Uw;OK8Olo1rFka~(pLMD=floc1PenQz}_?}e$1(q)4 zIeW;07U0mUb}FTLSE3QxsVvr5#aR8OnCF8|_?rHKt&^(P%0=jKf&lqD;aF9c6?inf zFDgkf$ttCzP8$tz*{hkwddH%YS6)<_($HJ&9iz*I$@C0hJE9H)b+olXY1QbrJ>c5I zlgb1calP}CFfhxKAj0MSq!QCs0;bRtI%^U1>4pm~s4tLW!i(yl{ctCX^vUR_U;`ZM zi+!OO6u-Kpni@V;)E#dqG2t`Jprrld;cW1Ig=PNmRX9mJ7h4bz&Ac7_5^;FX=gKtV ztH4{znanK(51}I;;+^kR`EtYmw$d~MIYMHjlOF!b=B94XA(g0S|0bI`Xda7K;Xvey-7q z4-~g}0WqaaO1gQ^aDJnl16(jtrOD&aPM~jZwJ*^pkE=5L2bDv-SS-*ySHYVR`4Li0 zC@rSbJpDU(75wEuux^k#v24o~GZy}+{NZBEZ1f1;>UKn#QOv_uitSTp09dgU%(U-78VJ@k0ds=5p-FHDW*y?_-*yXJj%crU1t6A_yUZBvWf38x#6Z( z_DN8p+PaM5E7Y#E>q(nN-U{o%yt7kH+uAU-@Bdn@r3qHKuRv1RDf9*awW#7;s#^{* zbyst18_pi@(A?S$HuWhKe-B>Ad)Ff@RoIYEK)Zma?Sd%zSGNK4#NlM@YJiohpnfK6 zwXQjSV*nJ7q%?G>9sTjO+KW8hEF(>aN@1O*8HiDAHu>eGf1$QWD~P|r-K;O=BGD0> z;1I_|y?4}Fk7R;W{9>vNWb>eXSsWfzj0+^Qm+bKNwgck!&x78Er@gf#qcsYYFnKy! zp)kzNxU~(qwa?9m#Nb&8S!bBANhlsJBi6-QpLx9dL*QQ#j_gF~Y9LpVK5LttJxTRG z>p7G@(b@xWGwZ43?*-EmS1p7JdIE(%g|GL?a-en*X>pPNQ?(i0@~Jx9fN&U@p)QV6 z@`z$-EG%OKBA;Tl()~>}6GlaE=(GSrz#8uZv#Hhh;F8EB9X}U*Zm3op^;_Ly|MWC~s;|PZ zUxZDCYC(c0qq3LBYd}oY*#|IE2!*Ez-$TLwP_Pbos-r_t%Mu0bn^OpoJkyi{!%#_q zHu55>OANPuN{Jy(Xs*Mp3(Y}Thdj(!55wwfl2}C&Dh->a|j+n zBq%rrp_|kznAOl&t7c3bW!+@qY(xr-w=T$K!I=PO&v>i|CqxM^Iet0FGY+cgjaMP` zDxM{x_6TRp2j0Ux#t*9y8sn{g2K}4Dl2zaE5_>tt|A3rl&{23aSWf<4YN8bhra&V_ zuYlHEeBZ#sHJO~KKkvg+f7wY+)~Cp6)>(N))d)|q-e@!R zpkf^@mQXGo3Ai1B`~a(V>3i=*%JFJU0e+om8b8eJOgI2a01l;-=ISUw8D+ zq+L7Bw6P9CY4X`Mx5*tvA`rY{<~eFMsf+2$ z5kPrlpH4P&Q~%`WZ0=IniM-_@H^MMrjR`;9^t4mUOjYt)_`@UTgFyh*ccS4t-@2DU zW5luhRu^GC1j2@OTLhL%jg=Q!dl%vluD{4yBD8TXVxY))fUJ;=#WqI7Q=(AhTJT8t z@qeQ9nI!;`WFy5Unu3eOt_NgE0OgC`Vw^nS%fEu7!OCcx%_%vXBDX-(k6oVmDbs4BVseWgxGgU2ts)_<(_M?**J07ro8urA~+8C5PXV zJRv(Dp)oLfDntI(ZM^kD6%b#m*Mf8#+JQSI$`OkaF!ljj4XqQoW zpY<0wdUpUDRi||mfL5n5ZHx69U;rcyiGJc#l=qu~m=bZTm3D2k0nWZ1^28svo?)tC z*`M<%@~9P-nZRvquhiPI42b>$mGj_ME7I)5``sS{CyQ@|7#2Sb{M-V}?`8y0cNy5= zwkI&w*%i_d^0&40Fm8Inda@3+$Ix% z`*%f#bWaN1s(L6e908C4Z-N{C&03MQ(Ovq$Hyz@Rm0nq<4v$VoP$C$C4USYx1F4qF z5X=+@X4GhU$J!3Y))%ZZPB3j$p3ehcVLpM%InfAxZ9Q9}*g@;R*xKY1dhqnRy!$-- zB7(f3dZ5#a9g7_q>sul!l@S64RJ94#oU2s1U~$7mbY(AP zun}ov+YM(90P6+->%eyxB@845NPnrwD?eEL;30lRVB8}Ho{qHT@$wWo(YCKRW34+# z9qB9n!M6LQB4muX4NcAlAY-Nn1L1w{VH;Kqhv(P-q+DTgKtVZib`~W&*~SQWbtfhq z^3Sn7`)5G+z--$iM}gZ5YRmR+g>;b(}sdr_^ZMFaxO< z-xk6b{`f*_Yv4?w{)i!FpGxOGZY!ZxSIPYh??tw^GSQ~sC-9O6FNEE<;UZfn{nTb1$>)&DjPRv4 zBya)$d7BB;4}Jn8OeAX)uFM2maRuKl6fk4p)wW%5MG3&rtZQxASY2VFNG{pozg+k^ zjJNfATN8b8x{h!Wgin6BG4lr7k&FW(a~M=5L$r|J0;d{>=Z0Jfo7B6;rWpHgvR%y{ ztQxvtm2C#SDcOe`-dk)du5I*v(P27}HD{YW;nM4-@7f6m|P$AoL z8&8C{Xg?Z%8vAx;Vi3W=M#&1%!*aX8jHtwXhx>ZP4Z zu_tYq;S3gA9GqstO4#Gfd2)fLY`>UP)8T0tg%>?-T;ZIvr%S;?{ z%@N^nuj$|kzba2<1rfXJQqFudN_W|w&p=Sv&}Gk0vJ`~!<8E7xaC-JMA(!HOFM918 zr4J3c%V{+>CTv&XBocAlo{%EZ7tpoXAnlH0!Rn-HSGfq5D6+J-%GX% z;rs1mBFY|9?8eHMY#-X#j|oZWq$`pzrlE@iHw6fi0qEFNScph`2fPX?XNm1-*#E9= z3WZJrgf025Z6dY2!r`W`_kuX!tsY3hbr8&=93N7cPT4LoF;hrnXtraYZFt71m%JJM zO#G@&G3{w_IE{hAgUlcl?B#Ud$ z)ekqVR5w|(;mEd(>2AF8xvdh8D4{2^8~b3L4A0*e*nZtl1HKIon&x?nQY1Fy!V$)S zFKwT&dcsFS5}+e?;eQ${LX3~&@ErT)#>Ve#Ejon1?TFFa_o87qIYEkk~C zF2t9phswO~4;%6lvmri$n&Da(vFkaP4cmbpkF5_O%WFRQ%Ha|jBH61rqEZ|Nj7|dE z##mg!0|6CW3>OfZpok|g!g*ZKP+~UK*lMvW8PAkJSm7R=+KN4QDx^;wFhD7*eW=J@ zGU8TyFW{~Oa+s|~p3m28cAGfUrV|PHu;BDQp|?H-Zv(VrhTm;b?W2lM3LMY5H{l$^ve>U5{fBb^^48@+xr*{t9&;6Q+b=fR z`ig%#k*x4DH*?6aU=mwQRslp?Je_>gp|#OB&)w6 z-r%dWf3Yr(J)HeDm?G77;KI+j9}f`!mIFZ3;T<#Kmrbr zB=eSb#9d!dW)qZXn5i}P5fs)?C9diUX384}h(*9Qjaaq4!Zf^zVi$t1@^=J+Qk2P_>R4HL2xHfr14{(laQ=;m!j;m>1(@UF_?RBs+>1|j z<@}E5xpwn3v)sjwbL}`ECvP4wCr_>hJI|i{gvb*Pg{8CwY4NHqfO_7fT(y%I#p$x`MQ|O{dNKA! zOpF6R%gB8R5_0$mT*nej$NyNwKrK4~{jKnn(ImIlkr#Q(a#T?3h8Rtr?5Pp=Tk!Ew zIbRPF057wr$Tu8st-c%!8@R$=$*=x;IpE6T{4rc?6=7?jjyxC>4_Khqsdk0^3{k>B zE6B-bam48Xv)W@T;watB>@4hy!b4;Pgj-!{$8iSYRIWeyS;B`faf}otRQ;53HRQKS zDuu7KH_hYyahMG<-LJGO`e#+cDZLf^aep_9N<*%~L<-k1F^GJ3X9%}%I|BsR`NPsC zxgzVZz*V&Kek|+i`&B!=Rtdj0dS52)orDv25d0Ta##fuPk!4&} z{YOZ?@?M9X@WJQkUbYoLu`Un>#YZ?^Ry;LkTyF=KfayP?!WRbmhb!R(t4a|Ke_p)- zfHa;5HrTnyA2)!i#L;19z%KL#!WWa?Q0>OV8|{N&yI*UE%HG|IRGI7G6Tp>9SKMST z?pyV7dIW^PN_!`sHTTV6pWG}v98xWyMg?z1v!Pw6?WJm;o`!=r+qau^ z!(r5Ro=9bn6b;%?f-)3h&yMc3?KSljw zH*t%Zv$hmE_yL69ZUt6b!mpk{p)YVeN$^5Uep;Nd6WeE3j7#^~zmb^_B0wF zN2{yVa@zWxJxb%M)%j+c*seR=(+$rL_7zs)4?r$dwc1Psa1Bh&W;h>&d68Zwx%Cg< ztmO)f7k{&Vrm@cPtfE5eM{87WRhOD7ZsZsH)f9ZtT|omLaE~R_TuygA;J%En43Ayy zW-TbllM}lPGBk8CyqBC^8QSUwHF~iCzOC-*;tB!at?tQ`@Idovf+0+L*gcVd@k|)+ zl&F*7gcb@mx1KrxP|~hyb)sBw6L7r#Vp&I1e2q3k^hPz7dsXDwz+1wpD*3SsvKR(b zA_QglUw|3h-&%do#M+Gg#y-Uy)Bd(9tcqOVpQ#4k7Z+V#VRP+hrzRB&U*hb>%Ph_m z*F#&a)T*=kE6-e_H|h?ofJY(LOKoAy@1nxd1K(-65^7jHkG|W4!xY478B@Bc=fP8A z<`N-JgOisyyw2FrJL-^3z2-YmXf@ARmF~vwh$f3tMvYIMZq5l3la8*8dS)s! zKJBUE%(P?MWaMM8ms(-g_GB;hT$x8R=4z<5|15`YGNF$OExiZmZ(Sc%q+ND~@Y&Z_ zZ5ADfge>uZ8v3a#%=@nXD)3LD@U;HwOh7MjEIf0k$UuIf549eo_7DyRXu^Y3fMSrF zEDlbm#}VR=T0sfc!Rn>_9`ohMV6~Mnpi#`gt+DI%BQlky$01rhei8bY`c_4{ z;u!TpioA-B*Nsvu4fiPZZJ7(BrdoPN72q;aj*(hOHp5v2V-Y52EhL+J2F3jv4QGdq zK@6GUDe;F;Uqbvm)K8mEo~HPTh<+0~I7glK7lqNGE~em1>F|p!n54e^7mW+(qR6Og zW}G)sU1-i9dXGrJwV*CO-sj zcHSO%ztm~`j@55h4!4J5@2%DP6V9w#gaVq${DuL?R?ZV?B=LIvrBsqu}03+lhM ziX+B^rj1v(;2gS4V}!=0<{kjUE51H)x7xR$nRlKOVK{HnNu=*b{MF(IOl8I^*aGo|yf_gOcJ1l8%qe#N8K z`g(K9Dv%D>yPK80S3H($DLG%g_UP$iB`~|*tbVmJ_Rmu*O)5laup^WeCrmtnPf z-m6wd-@^;g4f>F0f2l{DKIfuU1kE$PO#Q;lC&VFh3FN@39gN7u>UgG7d@#YQ0S9}r z+T6I|Qgyv7cIjp=ghU_p5s~xxp!rT{g*u8|f6*Q*bkI|Jv1w9cMu@-U@e2k(owg+VI-XDD@o zQjo$r>35*ccs~`s)sps|1Uo9ZO8u?)ebGw(VQ+JCxOwi9ILCzcu2#>@zh%0hvJYNl z-eG#LIwBQ(KtGYDAecH<+7+#}b~49?@)~{F21ah_3~(p!ALtF=>$6Y2qxd`%V3yXZ zBXb9s0Ioek>tIaM)mL*z2D69T%V&k?p{izr<6TQ)A^z->FWZ|V?*+c^GI1CQ(zq6* z4-8Dnok(KfM3P^m0$M>VfRAG~k0c=+Nz#l~UxcGna;!KWke{XG?LyRwLk<|G$){jM z&d~;#B$Pe!qPcl6NcTHo`MWOA-~t&68a;G5~|s+1683> z;AU)DW>NK>VQ=9spS6(svYxx)Bt>g$cn)O zI5MXJV3Q}0KYs&Ni^EgA=Kh`ZsJi+%^=b5~p+bGk9srn6bLTC=uzfg2T9|P$_(Hy; zz)ond!6&f?4jllSIjRW!BdRg!DfQo$0=b&F0`tm)N$&=kWU@<|>D#*^sO-fWvn&x> zuJ~3EU4OOE&xEdk;`2;H&U-a7g%CCj7+CTH5|gojlpy)TK?AN=G72p3Af#|$wZ*|J zoKP}L1H%ge&QR>!edEyI)f#w0+riBTygf4a=W9Te{S$%{P+<)3WpySK zKg%WDZ=fS>;6W(D=GU1o2w4?<6{HELcLtszG&#O?Xt`bw}F!sN$Hpu2m zB{ZOMnS5DVh8h^M?=6~_7e23C_?D|U4IcxoUyLB}woBBOM#DR5W9HN##z8XxdHDxwG|xVY?iWZq zHW>UVXtA}uAu;l2OR!+h$!=4xK)Ln>x+WWDKr=U^=(enq%bd{PwG4Svb$Af}P<_+n z8_-m$UqRxA!l4P%XJeTIpNR@A5c%p=zj59oX8gZ zT3tlG_kb&oltC+IoVs@p26dFe75hg0)SM|cDuHuuN>BE&4l%_i7xo-fPtOWDFzif; ztlfIDHqfNsL_atZ_iP3A@frrc<0DuKO`&|#Od3B$>Nz=NWu$#l-oLK79P9X6G?QpS0k#THOy00e;ZCtA_KlL}3wW6+vw$A<}K9`m5B^ z=ugmS_}1lPp?0yo5IUsJ6#~Q5^U3EE6L45PJELo?gggA(!{|GUhU`-S6%V1DZxUE0 zuMBbz_$SbkldQu969OVFgAqTG#JAGe4f=S6)`qPJYzq-`0bJZh7vG=;3dF*(ubIlE zX~iT?!;wSD(>16~A`G;9nH@(2UtLnFzx?i8p$m4%g@;ZB%*9=6q!i7e2nXeoB;s^ z2vc62Vsjp_-!|snWwh`7n?Zv)M*!bivo?$25CSKw44Y8G*3Gs2E8-)8C~&#G0>!8DHpuqFtgZ-MY+7in zxB*-|H0!@WDj_to?0f8qFYbRHa%|6A1>%Iw93J_2n-@44!J#4Ez?#Ie1S+*Uwi^xD zhd7v(9jtyy>w6sJZ$?LDtpV>%E7*Cg%Wm5S7-YB+1O$JFIRK%3GF90cbT> z@F=eV1EWjtN1mLiz9K%{Ny91YSn5^4l}rK7SU$%ez>{cF&_=EbQ3l% z6uvQtNt%aDX1|UpJhrB}AS|XVdW?@o&N$da4k7?$P4XQzb+OtytvO;8!a;njgQFQE zNQ7_0fX`!{g;#>NK^B-7Fx6a-Ppjj-&-Vix!Muez#sridAp`kx!~<8fu631~%Cns{iYvh&BwQ*kG-a_WuncB&%XyRG=3e?{?kp##BNzpHKFQ}v5%SoSR^vDy+`p7m<6#xmw3Aas(MBX~ih!UfN-{gncPoZK&fm!ESgri0B`PR0jB zPa}4W6jj2|zGfy9762ehQ+y zS>TkB%b{K5*ck`a{P)83fKym~qU?EsY2=Y;>?vS|9Jo(zZ+>G%9UgHsAn$L&;KY^0!S^`F>^49o~MPs4=kG5ZgKe|v$28dN+A<#?uVaq z&>Zt8j@%Xwyj)p|t-!!>_^HkC;SDn$mv6!UPc=lBYe*YP&(&au0B$kc1P7jJ!_{nXo$KaA)Ry=}`~(&A_8_58)my zHLw(=2!jeAET2AGYQT|{$kviA%*eeG^2`89;tdnXXMl>fnG2eK6}tl3fH>5@-6?#hU zO?U#%p7YJJQWClhC@Y@EY`NJcE6<;yJLF95BJy3PjiXzx&^|L2Vj)AJwUyKP)tb)$ z8sR4RPYRC!&&14F7sF$OL2;xsJdp5zR^I}0%&XNL=@HckP(zo$h^S`k>&-z@LXeb9 zr8E0(VcauYxC@Kwzl?6SEvV^F1Dp9eSv%oyV(y-Sh+M72)fgY#s9DXG#ynqBQ1wxv ztlY~iNeJoRWmM7u(57z}WE5V^btdu#O2LsK|HZ)sn8X=7BHHUf(UKINg zIQ+;44F?uq>L?2CbKpx^q5}RRFE^B`IJR(RJrPZO#ZgTw8nwy1jDg3s2eNZ} zjae<$0@~CH2V_}s|9NUfAIw4U=au0faEO!1v`nASS$x;o?d(x-k1IUtUciOEM@}r5 zflLK>Fe^9E(G)Co4l^et^NnM~qh3WejOgE{;A=P*7&O>ROf614d_lV;_w8}=J)zKX0)+$VI&pU|ez;IAEs+*%wESbxJj?@6EpfBQE0ckN;FuW}4M>Mh?S$HF`f z8>kYeu!S$}Y4(9fo$pd^26Imlqt0_oL`&^qv$&X;OGwtxViG7E)E#lr0<^bX|$61p3)dAgQF9}>$>t*eDZHf8Hv}I2HgO2u@ntUbB>?5%Q#OJj!p5t}{@!l)3F#GR7Mv^=1=oZoDV8M588SOnR3F5Jw@Dy+hE*<<` z6O)o*q1jt05)#Lp+*xEHv1_DB8XhBoZ3#!6_%Zw7C@eSi4T}w@u_d*YQA> zzd=6!NgYi7*y7`;dmHc*Zbis^w1R}dDUL;g?M`b8Q2-i*>=0a&{AYnV@Ystw1$0IfIyFyydi<=@OwQxkqn)A0>Y?e2 z-5|zhU~H{f*qGHOCykDFbabWp`?UZ(2cQhz8KQn{vbjR4GAo%tfjxrk&xYDo+(zhX z#|+qi>(DxTQFtR}mQ~XDJ(#>~wI&Y^p@%#?a8-aB6B@q3 za2@oHdvN?0tH}A5m`>19gkak{uQg{US6QCdW*QJg#f1~MYey9}2ZYZW7EwBn2Ezs0 z7K~SP%%rVsEelu?GFgUrn436^e6y+DrA~KTXllifN146AdX)E3xLffJ(K%R_E)*K? zxXh$;RzC~x^Glt$C1__evx-3(qzfo8r!ld(Lu*S5>Ju7Pw!YC~)8mg&bI#e1sqkEq z)dA7k+=4zrx@R zQhcG~H*ytBscsU{c9Jrx>9ku{7x`$yLH z$l1&|j0++bw(>6-5tUV-4cvo$3eBM#!BmCs@$`~T%=fpAgLs4P2e|F?Ft!s*97thp zbRa<>BYR4ZGJ#Rz7hy7Il)qeUM(jcdETY&x_-c4~`2CAuHk~s+f7E6p!P#*GD|NZo z2*sW!tQA~8pF28=4*Zg_It6n%wp?c%G>0k6KVa^Xov1klGtFW(_K*kUc&p<`Zb0P} zdB`)sz=;ITj9!<168sxwKBUOM(EHGrT1RX#+>75Rm|PQ3Fjpg5Vx|lJe+zltjuxh} z$s#`g0+YP?!fK3-wQg8Y;MJlpxk|vf*Rwoe8xxvWL?CXt7146e9x*G=wVV}S_vDO-w)i-sV@sx(c$Lj2SR=qT!TkbN z8;*jV9>mfF+s9->YHfB*H7#V{0;n%v9D?75Rhm5CsNRGk;Vq6=^ZGL0T9Pvj*+O7- z0is&?7`RTR>#Qh&PAJqQ@5|s>2VU_YlOKE14(MvSKDMrqOY_X~m9r+`(L!Un9@6SU=`VdHcDr z?45#g0gn@1;kkwoOB!byjutQ*gjOvY2e+PUO428^TcfXQW|u_WNQ-{Paa&%R=3Q9g zJyQ{2u~_UrF#E=u2|oq@hhs??$mj^rtUL>1XJ>x1Zpl8H;2Vzfb2_@I`IuZ2(zXK} z7>;w05e(hY`;}u|&V2=oKKFo?h{#PC8LIldz@>$sQ%pX~TPxiJ4Q0vgj)Eq^9y4^v z{)VwHqZZ~^EL5=jk7d0~4ojRuIue z(g>qGV^~05j%l8YJj41x1_oQ2+sS4^uN zzl5rSLkTw$vIbMRDmz1_hke@gFrYjl#%ckE>41|Zup$tbQ#s2;6mREqg-S?L<2>^$ zLyj(wR81F|rfCOysQ2159pcTyr%LM7#$Jv5d|6^TR>c&%NyRb*lwHS$=gh*qMdyzn zq*Yordxts9;V}GUi53|k#Xfqa`UdALD?)eaaavtvPC{b`dEUxN|MyP=&Zg6-x`ilJ zvG$@`LDB20YFdergo)fM1{2KKXf;*~LWl_G+6_K0wpVl(;08XM%}6`6C7Maql>MC6 zG@D0PHJ!yLjxOoUtpy5h${lUYkLwEyL0DXaZja;o(+S3lY8}i2kPlDhX9Avv-%hjP z#`Qeixafnz$qseFPeU2^WjV=0kcoxm09C}R7qNxw*8SbY7f3KL73xj+h}B5_*jF@C zryI%+D)f{HA#e-Qw(t$6bK9!$>lPfM(ZoJ@v%qt$nj7&@aOFyLsa_(+sAyw( zKHbgoW#8ko&@FNo8);=ei;s}03e5SpMuF>}{ z?PH8?h!RTx&G{`HheOpe+dhAb*ajc1EA(=-7s7wPFj&BlGy-3elb17t_TV969ZuVS z7JXGBrlH)UJzT%O9U1yuQ0<;!vAcnaEfwt)YTBy7Lc)uk{P|B;iks}RVFIyp*`ccT z4WQ~LcVgW)Mi6M_*kDA9i6UAKrCY1`gX~aeVeRg&ixiY4DW+yBXb03)+TxAP)7@^VjjBFg{{>SG#0_D9P}=KsZU;*oT1$16?!X-vSQB6mGK?}kZk+D>7sciy)HsTY2*%4@tJy0daA_3 zc(YJjh6FpIn0)&yb1<;ge)L9{fpiqsU6kXY0 zEx8&oR&pPMLnXVl@z2rg1f1b+#p1XFa*9Z`a~X(|-|f`X!$yzx4S~tWh+Vc;1i&q$ zHVW9YiBZ@C96wt!5^Dq)1&=qReQu*zt%Cq_z+NO9$x$y(5#3a%P;{ff+bUTu^piw9 zrpJmX0{-d|DH|XA4ylb?X8T%IK4Z3-0s$-Sdqrj8d7$Lr8i6qK69jzY?$1TW@>Pf! z6?_aaUnwrF!dUQF*^DO3e+JqoK*>Ya-m+Ot^>(B*ZDVP%_MZ}>98D;9BZndel=m>- zXpw-~S{d?$D~P*$?5EiOd4XrfBvu$?PL{xB1hQTBytvdt`#n!QC@l4P5MdQThyk62 zlXGlNJ|spfUVc}ayQ^W}?xYqG?X=lR^C6uLk9c(St_lFVk0FSMZ;;p1f z7lj<)uxK0+D-et#fPl$c5TK%mL^!U{C&oZkA0FS|5OIu3Xmz72VTxOPbNbrCTa zDC@U9_iYi!=MHCpfU5iub09m!5)E3E;`qOHRFrG?xKs3w;TWb3zl)7I_Lcalrakj| zu%Y_lU9pVYdedIGfUQfkg3~LnHgF7ZR<0zeycGU;KODmXpj~CFrI$1FIM@MG0z9r7 zRlI`@j(tRChm-Ja+4Cd9k^Rr_#S>67951co5CNf!0O)8g;u=}mh*wkl0g_?xOjV`G zjKVp(1IGkjM~sJ3MM=W7e1fa6PG4k@o1F8av&<p?T0ZFjzo+dK#^_Tmi?r z{a}{MYy=NwtReZZZr55xk3rTY&SRg^4Pn$Sh`TC=15=e}#j^|~2Y*P;M;Jc_61#&S z@4ttJfK0Q`a|`xetGOO~RrE1-roQb-dSxmONGa5Nd%%1NI2D!<{ z@4(aU_z^KkzgFY$Zyn#lxgWeBTH8bZEVlCllsbDgbS^vz&;1<|j)z_jjfgCQNG^L> z6q_6yxX$GPLr4KLK%Mzg?4|W)+@yYUUIf+aUt!LwKZ<@fW;(=^_?QY_luh*-n4wta zB;Q4{kux8O`w+QP``5v1vT-B(%fE`n+CQmYx4NMkrI(W8zck@`G*L@WL63h6!Ub|4 zP?67@0OBr$P@^d4^rfa$XZ=z2Fl~|5X1~0Thc)X$Siz!TIwM&8d5C2>IJcZWs znBu(#6aNCm6OZF@>dc3UTD}nghO{sN3;y)%gGfT(XVY^37b`Sv{}(gPS*X#-7Gt*J)O|(Y=IsSvD#Xjb_3dA ztYDsOq5>V|*Y;CwCA70Mtz;x4i^sOnU_8{MZnB}u=!CZG3uRcX?__{1?(9sR9Abui zwTo#M9Auj1-vDrY!>UEy41DFc8TkHiqghuYJ$J7{c_4!qme&J~O1jIF>dPLc4VHbt3oo+G*e7lv|wTB&xVygR676A zxyy}J#vs|A5X)l+Dey49bl)s-VX3v?=G78v^#0!H{!KU8TRj!VYI^12pDumm7pg3b zzCd^%Roqv8m^%35M~-Cb!GZnT-Em)4-*%HBJrK4appNyE=X`@>3!ugHmw!@`uq>iA zocGldHtnf{WIh6B(A;PZe(Eoyl+pod9BG~F2Sa_X?2qll7QsEBzwBY>43Q%;b>X;M z|%@KuNOMY_~sVhN+-D2e>MmjG*Qq`Y3mj^HZgHL1EQ zD?eUxKfY0ok_bI~wPwgbmbY=@?f8HoPi&kqivL$tTX1`f#pM_~7?BIE0Y~H?G8&JK z%2A~w<#QEJYM@@CB-n28&Up1=&8an;pvSk$uYq(_kmX~HY-8vKXcTwU?uhq zzi!s)@M*E??c%X$Gu87E zrxmXf%{!-!O6!4-@egtacdwY`^j1}Iq~{AgqQP9FptcRfYVmw!w*1PCqZqH=lcQ$c z3%xZ|Dt9ABpoL*(OWa$Kc~+z1(gImx)5hbbGRYcF)h}19mNU7XR{H2}V$g=zl;46N zXgY5{xf9-LWm>8>A}6Zqh4RgKVlG5Bt+S+z*E#T7gY|H+WTW#ie40xEkg-FfBD{t( zzAC$4PPD_1$a`RweOzv)^m(~RL4>!P)my-uwJRhda?iJ*gRH`F!%I&o+n`EjS#Z2x zC0{ao52`M;aEn?cPitgw2O53!8xf0F0(czfcn(p*80kMv_O*kX6lG zT2X_hMHL}PdZ>>GTJ@=fgWGFzpSNKoBuLaFmce1#7Lt=6NmzjXAq&{F1d~mk8ht>{ zikDR7BlwuShw9eq0Xc*@_{G0yPeT6WHTkwaSa{C#gM}w?2CAKF6{_S83gY}Ubj`pk zfDyZeQ-C9Hwnm7^%55M!9@4PT3?C$c=DE#OB5Fpq3Djwe>X^(sm*Y%GC?@EG0d!tioJ3yAHPLM zWWpolx@yA%${ew%dxkfS;9#n*Jp_EzUXt_8NV`;R%$lsYTnkvVFsuHH+u0GSsKa8B zM~sjenqzfQHHYL8Pmg&0sN7YL9)Z!zRWyO{l7S?Y)UwVN_PNlSPQ#OpI1NR5cn(`! zJ=|)lP8>o6#=vYm7A~|Fx_xi}b<(e~tFqOdt*o|n#vP#vf$8YP3g`t@r=W?knKP1< zl81co6**Lc20?Kn0$bxb#I;%(+M_6(ci_txHXUhV5hu_kIh5@Kt~JGh=;77;O7>OO zKs!+f><;>*!#HQtML_+SV4erm_3TZXx_Bd{kLFxyFv+JLy068HZm329oOP7V3XZzx zB|f3~skca1gwLUZ6#&!jMLz6&-RPk6HFmXJ=y|OxE=WXVH;I)Y9kbQ0&vA}xvUMT} zx}uylIy299regjDAo5waV||DPKEqE(r4JC|al7nGIWrSk)(4WFN%Qa3dXE`sJ8%$1 z@p1%Vg&r1L2N4d@v57p(5Eiy0$nHn5gq%NMhktRdz_y`#OnCNRBpN@d}iA zh=7>kMJlkN-4AR)#V}$)nd2h?(!xwb$;qM6%;vd#0>-IFT6N_3Sd4o_Nx3zW;NxpW z%*Kx5qYs9i?wYDwX%Wp<>Y%hL4d^(6$Z%bfX+fuG4*q4j^*Dsnt)cc_ZDu#J91r0$ zEIbwAOGv#Ny_N8hcK918!DR&yOH7tEt+BJ9Z}3Th z#T&f=PwTB1pFwGZm>sA48xBTlUq6~~;y^^9o=7-l5J#QjSq(a7*l3De7?}21CJO4h z&m21`-4j|QgC*7j98_mB#0u*z(^NT;N*HEX5p4K4E&h*PJ z>23XyZ!ic_GENF3UU28W)#~NjbJADRPa03w#KOR~!|%6iae^Ym!MxvEXh-{7Qym7W zZwlwCPNGJk>8@+aEI>7uH27hzv|L7z-a_Z{Oz!T{z7bd4q8FXXQEKs8Jh2&{6XqdE zeK`iBDK%Sgds+!HVPNm823W{UHyqE3T#lfe3NQcPNNKBrMb^t6KDZa_;QBA<*wkXT zEB`1e&pYl$(+@_1ZoLhM9=A5cK9RuC7hIN!_|?_bjuk7vTD})q5sw;3ALemb|Dq8z z74TnlnWa(Jso*%Ph`FB-EQo_tXoB@z>Z18z^HrL|%`398^TSCAgY$)N96X|X+R~vj9DtK-hPMXR*!*K783}#&q+4-HF{3D@ zgeb(h1gDhKNoXcmpdyb+c0DT4=SfYebvPYyD8utwgq@2A=I5)*hb+9QC9?>(Wg4lc z1q%so;m8MkRwadsi(K`!)OsIevqY6FuwK&V2G@IJQ_skZlDg#vFa8ULxm7izY17xh zN)?ymNa#Tvrbavt^EsTm5UdMV03x=z1lmD;=%8(!j7vCO;afoXLjInAvv#E7D^33& zI2kemt@e7n6Ja7r&wwbojZidoIV)MSUNoBDd#=+OgT zd)&_M!NhVpW#lJGr`H^$rv#60FjCw%riQ{OoOLP~)5NgpaJy`qTJfaExTzH@NYq0; aH?X_hoZbvPJ;TM+B73N^Zs?lvPyY)}XkZWk diff --git a/server/item/recipe/item.go b/server/item/recipe/item.go index d4f2e264cd..805cdd3bfb 100644 --- a/server/item/recipe/item.go +++ b/server/item/recipe/item.go @@ -7,7 +7,7 @@ import ( "github.com/df-mc/dragonfly/server/world" ) -// Item represents an item that can be used as either the input or output of an item. These do not +// Item represents an item that can be used as either the input or output of a recipe. It does not // necessarily resolve to an actual item, but can be just as simple as a tag etc. type Item interface { // Count returns the amount of items that is present on the stack. The count is guaranteed never to be @@ -17,25 +17,29 @@ type Item interface { Empty() bool } -// inputItem is a type representing an input item, with a helper function to convert it to an Item. +// blockState is the exact encoded state of a block, included with input and output items that are blocks. +type blockState struct { + Name string `nbt:"name"` + Properties map[string]any `nbt:"states"` + Version int32 `nbt:"version"` +} + +// inputItem is an input item as present in the recipe data, convertible to an [Item]. type inputItem struct { // Name is the name of the item being inputted. Name string `nbt:"name"` - // Meta is the meta of the item. This can change the item almost completely, or act as durability. + // Meta may change the item almost completely, or act as durability. A value of math.MaxInt16 means any + // meta matches. Meta int32 `nbt:"meta"` // Count is the amount of the item. Count int32 `nbt:"count"` - // State is included if the output is a block. If it's not included, the meta can be discarded and the output item can be incorrect. - State struct { - Name string `nbt:"name"` - Properties map[string]interface{} `nbt:"states"` - Version int32 `nbt:"version"` - } `nbt:"block"` - // Tag is included if the input item is defined by a tag instead of a specific item. + // State is included if the input is a block. + State blockState `nbt:"block"` + // Tag is set if the input is defined by an item tag instead of a specific item. Tag string `nbt:"tag"` } -// Item converts an input item to a recipe item. +// Item converts an input item to a recipe [Item]. func (i inputItem) Item() (Item, bool) { if i.Tag != "" { return NewItemTag(i.Tag, int(i.Count)), true @@ -53,10 +57,10 @@ func (i inputItem) Item() (Item, bool) { return st, true } -// inputItems is a type representing a list of input items, with a helper function to convert it to an Item. +// inputItems is a list of input items, where each is convertible to an [Item]. type inputItems []inputItem -// Items converts input items to recipe items. +// Items converts each input item to an [Item]. func (d inputItems) Items() ([]Item, bool) { s := make([]Item, 0, len(d)) for _, i := range d { @@ -69,41 +73,49 @@ func (d inputItems) Items() ([]Item, bool) { return s, true } -// outputItem is an output item. +// outputItem is an output item as present in the recipe data, convertible to an [item.Stack]. type outputItem struct { // Name is the name of the item being output. Name string `nbt:"name"` - // Meta is the meta of the item. This can change the item almost completely, or act as durability. + // Meta may change the item almost completely, or act as durability. Meta int32 `nbt:"meta"` // Count is the amount of the item. Count int16 `nbt:"count"` - // State is included if the output is a block. If it's not included, the meta can be discarded and the output item can be incorrect. - State struct { - Name string `nbt:"name"` - Properties map[string]interface{} `nbt:"states"` - Version int32 `nbt:"version"` - } `nbt:"block"` - // NBTData contains extra NBTData which may modify the item in other, more discreet ways. - NBTData map[string]interface{} `nbt:"data"` + // State holds the exact block state of the output if it is a block. + State blockState `nbt:"block"` + // NBTData contains extra NBT which may modify the item in other, more discreet ways. + NBTData map[string]any `nbt:"data"` } -// Stack converts an output item to an item stack. +// Stack converts an output item to an [item.Stack]. func (o outputItem) Stack() (item.Stack, bool) { - it, ok := world.ItemByName(o.Name, int16(o.Meta)) + it, ok := o.item() if !ok { return item.Stack{}, false } - if n, ok := it.(world.NBTer); ok { + if n, ok := it.(world.NBTer); ok && len(o.NBTData) > 0 { it = n.DecodeNBT(o.NBTData).(world.Item) } return item.NewStack(it, int(o.Count)), true } -// outputItems is an array of output items. +// item resolves the [world.Item] of an output item. +func (o outputItem) item() (world.Item, bool) { + if o.State.Name != "" { + if b, ok := world.BlockByName(o.State.Name, o.State.Properties); ok { + if it, ok := b.(world.Item); ok { + return it, true + } + } + } + return world.ItemByName(o.Name, int16(o.Meta)) +} + +// outputItems is a list of output items, where each is convertible to an [item.Stack]. type outputItems []outputItem -// Stacks converts output items to item stacks. +// Stacks converts each output item to an [item.Stack]. func (d outputItems) Stacks() ([]item.Stack, bool) { s := make([]item.Stack, 0, len(d)) for _, o := range d { diff --git a/server/item/recipe/potion_data.nbt b/server/item/recipe/potion_data.nbt index fdd42726f771f1bd888d28bf317d18a7db9fd34b..465959b198e4be3e998d8003e3c854f035197e60 100644 GIT binary patch delta 188 zcmZ3~%CxAJX~QR`$p%a|lNE&ICi^fKP5#ZKJozpc=j4rihCsgLWCcC($pXTXlLb_G zCKs?=oqUB&W-=c$h|RLOkTsbRsF`hY0*A@u3tHlnU$IM0(w3VXATBxCmLnWw=H^%I z(yTy30adI99$d;JN3hb;P8#c!ZeovlUP%1N7u}o&NA+P%6 Z_A<#y71opQl}W*c3t9Cxx0gxt0037nJ!Aj? delta 215 zcmZ3~%CxAJX~QR`$*!y#%sH94lM7h&C;KoLO*UY%nQWk?2xLo6R?rilEFdg7SwMwn zaskWL$p?6&HqU1@Wdv%HnS4foZ?Yk;`s7#al9ROMCI^U1PPXL;pR6DxxA_&jG%HZC z&EyN^2~f@cAc@HV0zPm)$dUt9PGC(Sbx_SfHJLz1uuV?j2nEV(PgY=O1vz4Ktl;-# fu*APIr^y$z#3#3xNlvP;o_wz?32bS5nKTaoGe}2e diff --git a/server/item/recipe/recipe.go b/server/item/recipe/recipe.go index 63d5b3fab6..053d99cde9 100644 --- a/server/item/recipe/recipe.go +++ b/server/item/recipe/recipe.go @@ -3,6 +3,7 @@ package recipe import ( "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" + "github.com/google/uuid" ) // Recipe is implemented by all recipe types. @@ -44,6 +45,44 @@ func NewShapeless(input []Item, output item.Stack, block string) Shapeless { }} } +// UserDataShapeless is a shapeless recipe crafting items that must retain their user data, such as their +// contents or colour, when crafted. Vanilla uses these recipes for dyeing items such as shulker boxes, +// bundles and harnesses. +type UserDataShapeless struct { + recipe +} + +// NewUserDataShapeless creates a new user data shapeless recipe and returns it. The recipe can only be +// crafted on the block passed in the parameters. If the block given is a crafting table, the recipe can +// also be crafted in the 2x2 crafting grid in the player's inventory. +func NewUserDataShapeless(input []Item, output item.Stack, block string) UserDataShapeless { + return UserDataShapeless{recipe: recipe{ + input: input, + output: []item.Stack{output}, + block: block, + }} +} + +// Multi is a special recipe with behaviour hardcoded in the vanilla client, such as map cloning/extending, +// book cloning, banner duplication and firework crafting. It has no inputs or outputs: It is identified only +// by its UUID, and sending it enables the behaviour of that recipe client-side. +type Multi struct { + recipe + uuid uuid.UUID +} + +// NewMulti creates a new multi recipe from the UUID passed and returns it. The UUID must be one of the UUIDs +// hardcoded in the vanilla client for the recipe to have any effect. +func NewMulti(id uuid.UUID) Multi { + return Multi{uuid: id, recipe: recipe{block: "crafting_table"}} +} + +// UUID returns the UUID of the recipe, which identifies the hardcoded behaviour of the recipe in the vanilla +// client. +func (m Multi) UUID() uuid.UUID { + return m.uuid +} + // SmithingTransform represents a recipe only craftable on a smithing table. type SmithingTransform struct { recipe diff --git a/server/item/recipe/vanilla.go b/server/item/recipe/vanilla.go index 0b1af9f3df..a088833dee 100644 --- a/server/item/recipe/vanilla.go +++ b/server/item/recipe/vanilla.go @@ -5,6 +5,7 @@ import ( "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" + "github.com/google/uuid" "github.com/sandertv/gophertunnel/minecraft/nbt" ) @@ -57,13 +58,38 @@ type potionContainerChangeRecipe struct { //lint:ignore U1000 Function is used through compiler directives. func registerVanilla() { var craftingRecipes struct { - Shaped []shapedRecipe `nbt:"shaped"` - Shapeless []shapelessRecipe `nbt:"shapeless"` + Shaped []shapedRecipe `nbt:"shaped"` + Shapeless []shapelessRecipe `nbt:"shapeless"` + UserDataShapeless []shapelessRecipe `nbt:"shulker_box"` + Multi []string `nbt:"multi"` } if err := nbt.Unmarshal(vanillaCraftingData, &craftingRecipes); err != nil { panic(err) } + for _, id := range craftingRecipes.Multi { + u, err := uuid.Parse(id) + if err != nil { + continue + } + Register(NewMulti(u)) + } + + for _, s := range craftingRecipes.UserDataShapeless { + input, ok := s.Input.Items() + output, okTwo := s.Output.Stacks() + if !ok || !okTwo { + // This can be expected to happen, as some recipes contain blocks or items that aren't currently implemented. + continue + } + Register(UserDataShapeless{recipe{ + input: input, + output: output, + block: s.Block, + priority: uint32(s.Priority), + }}) + } + for _, s := range craftingRecipes.Shapeless { input, ok := s.Input.Items() output, okTwo := s.Output.Stacks() diff --git a/server/session/player.go b/server/session/player.go index 67f63cbb75..e8bdce05e7 100644 --- a/server/session/player.go +++ b/server/session/player.go @@ -115,6 +115,8 @@ func (s *Session) sendRecipes() { var ( shapedRecipes []protocol.ShapedRecipe shapelessRecipes []protocol.ShapelessRecipe + userDataShapelessRecipes []protocol.UserDataShapelessRecipe + multiRecipes []protocol.MultiRecipe smithingTransformRecipes []protocol.SmithingTransformRecipe smithingTrimRecipes []protocol.SmithingTrimRecipe potionRecipes []protocol.PotionRecipe @@ -135,6 +137,20 @@ func (s *Session) sendRecipes() { Block: i.Block(), RecipeNetworkID: networkID, }) + case recipe.UserDataShapeless: + userDataShapelessRecipes = append(userDataShapelessRecipes, protocol.UserDataShapelessRecipe{ShapelessRecipe: protocol.ShapelessRecipe{ + RecipeID: uuid.New().String(), + Priority: int32(i.Priority()), + Input: stacksToIngredientItems(s.br, i.Input()), + Output: stacksToRecipeStacks(s.br, i.Output()), + Block: i.Block(), + RecipeNetworkID: networkID, + }}) + case recipe.Multi: + multiRecipes = append(multiRecipes, protocol.MultiRecipe{ + UUID: i.UUID(), + RecipeNetworkID: networkID, + }) case recipe.Shaped: shapedRecipes = append(shapedRecipes, protocol.ShapedRecipe{ RecipeID: uuid.New().String(), @@ -144,6 +160,7 @@ func (s *Session) sendRecipes() { Input: stacksToIngredientItems(s.br, i.Input()), Output: stacksToRecipeStacks(s.br, i.Output()), Block: i.Block(), + AssumeSymmetry: true, RecipeNetworkID: networkID, }) case recipe.SmithingTransform: @@ -196,6 +213,8 @@ func (s *Session) sendRecipes() { s.writePacket(&packet.CraftingData{ ShapedRecipes: shapedRecipes, ShapelessRecipes: shapelessRecipes, + MultiRecipes: multiRecipes, + UserDataShapelessRecipes: userDataShapelessRecipes, SmithingTransformRecipes: smithingTransformRecipes, SmithingTrimRecipes: smithingTrimRecipes, PotionRecipes: potionRecipes,