From 683de943c030306759ac3fd34bbcf2780546eb1f Mon Sep 17 00:00:00 2001 From: Alex Piechowski Date: Fri, 28 Jul 2023 12:50:04 -0500 Subject: [PATCH] Rewrite client to use a global tick loop Only digging, use hand, change hotbar slot, and physics are part of this loops sofar Speed up the specs greatly using event emitter callbacks Add apple to eating Fix a ton of random anti-cheat issues Fix a few inventory desync issues Fixes the occasional eating problems Resolves #98 Resolves #97 --- spec/integration/attack_spec.cr | 39 +--- .../{dig_spec.cr => interactions_spec.cr} | 35 +++- spec/integration/inventory_spec.cr | 64 +++--- spec/integration/movement_spec.cr | 25 ++- src/rosegold/bot.cr | 41 ++-- src/rosegold/client.cr | 24 ++- src/rosegold/control/interactions.cr | 191 ++++++++++++------ src/rosegold/control/inventory.cr | 8 +- src/rosegold/control/physics.cr | 24 +-- src/rosegold/events/event.cr | 1 + src/rosegold/events/tick.cr | 14 ++ src/rosegold/models/block.cr | 16 +- src/rosegold/models/event_emitter.cr | 2 - .../packets/clientbound/destroy_entities.cr | 1 - .../packets/clientbound/entity_position.cr | 1 - src/rosegold/packets/clientbound/set_slot.cr | 1 - .../packets/clientbound/spawn_entity.cr | 1 - .../clientbound/spawn_living_entity.cr | 3 +- src/rosegold/world/dimension.cr | 2 + src/rosegold/world/entity.cr | 5 +- src/rosegold/world/slot.cr | 13 ++ 21 files changed, 314 insertions(+), 197 deletions(-) rename spec/integration/{dig_spec.cr => interactions_spec.cr} (56%) create mode 100644 src/rosegold/events/event.cr create mode 100644 src/rosegold/events/tick.cr diff --git a/spec/integration/attack_spec.cr b/spec/integration/attack_spec.cr index 76cc4d1a..677bcc49 100644 --- a/spec/integration/attack_spec.cr +++ b/spec/integration/attack_spec.cr @@ -6,33 +6,8 @@ Spectator.describe Rosegold::Bot do Rosegold::Bot.new(client).try do |bot| bot.chat "/kill @e[type=!minecraft:player]" bot.chat "/fill -10 -60 -10 10 0 10 minecraft:air" - sleep 1 - end - end - end - - it "should be able to attack" do - client.join_game do |client| - Rosegold::Bot.new(client).try do |bot| - bot.chat "/tp @p -9 -60 9" - bot.chat "/time set 13000" - bot.chat "/kill @e[type=!minecraft:player]" - bot.chat "/clear" - sleep 1 - bot.chat "/give #{bot.username} minecraft:diamond_sword" - bot.chat "/summon minecraft:zombie -9 -60 8 {NoAI:1}" - sleep 1 - bot.inventory.pick! "diamond_sword" - bot.yaw = 180 - bot.pitch = 0 - 20.times do - break if client.dimension.entities.select { |_, e| e.entity_type == 107 }.empty? - bot.chat "attack!" - bot.attack - bot.wait_ticks 20 - end - # no zombies left - expect(client.dimension.entities.select { |_, e| e.entity_type == 107 }).to be_empty + bot.chat "/fill -10 -61 -10 10 -61 10 minecraft:bedrock" + bot.wait_tick end end end @@ -44,11 +19,11 @@ Spectator.describe Rosegold::Bot do bot.chat "/time set 13000" bot.chat "/kill @e[type=!minecraft:player]" bot.chat "/clear" - bot.wait_ticks 5 + bot.wait_for Rosegold::Clientbound::SetSlot + bot.chat "/give #{bot.username} minecraft:diamond_sword" - bot.wait_tick + bot.wait_for Rosegold::Clientbound::SetSlot bot.chat "/fill -10 -60 8 0 -58 6 minecraft:dirt" - bot.wait_tick bot.chat "/fill -9 -60 7 0 -58 7 minecraft:air" bot.wait_tick bot.chat "/fill -6 -60 7 -6 -60 7 minecraft:water" @@ -56,7 +31,7 @@ Spectator.describe Rosegold::Bot do bot.chat "/fill -9 -59 8 -9 -59 8 minecraft:air" bot.wait_tick bot.chat "/summon minecraft:zombie -7 -60 7" - sleep 1 + bot.inventory.pick! "diamond_sword" bot.yaw = 180 bot.pitch = 0 @@ -64,7 +39,7 @@ Spectator.describe Rosegold::Bot do break if client.dimension.entities.select { |_, e| e.entity_type == 107 }.empty? bot.chat "attack!" bot.attack - bot.wait_ticks 20 + bot.wait_ticks 13 end # no zombies left expect(client.dimension.entities.select { |_, e| e.entity_type == 107 }).to be_empty diff --git a/spec/integration/dig_spec.cr b/spec/integration/interactions_spec.cr similarity index 56% rename from spec/integration/dig_spec.cr rename to spec/integration/interactions_spec.cr index 60a3a891..83d893fb 100644 --- a/spec/integration/dig_spec.cr +++ b/spec/integration/interactions_spec.cr @@ -1,13 +1,21 @@ require "../spec_helper" Spectator.describe Rosegold::Bot do + before_all do + client.join_game do |client| + Rosegold::Bot.new(client).try do |bot| + bot.chat "/fill 8 -60 8 8 0 8 minecraft:air" + bot.wait_tick + end + end + end + it "should be able to dig" do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| bot.chat "/fill 8 -60 8 8 -57 8 minecraft:dirt" - sleep 2 # load chunks bot.chat "/tp 8 -56 8" - sleep 1 # teleport + bot.wait_tick bot.look &.down bot.start_digging @@ -26,9 +34,8 @@ Spectator.describe Rosegold::Bot do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| bot.chat "/fill 8 -60 8 8 -57 8 minecraft:dirt" - sleep 2 # load chunks bot.chat "/tp 8 -56 8" - sleep 1 # teleport + bot.wait_tick bot.look &.down bot.start_digging @@ -40,4 +47,24 @@ Spectator.describe Rosegold::Bot do end end end + + it "should be able to place blocks" do + client.join_game do |client| + Rosegold::Bot.new(client).try do |bot| + bot.chat "/give #{bot.username} dirt 64" + bot.wait_for Rosegold::Clientbound::SetSlot + starting_y = bot.feet.y + + bot.pitch = 90 + bot.inventory.pick! "dirt" + bot.start_using_hand + 2.times do + bot.start_jump + bot.wait_ticks 20 + end + + expect(starting_y).to be < bot.feet.y + end + end + end end diff --git a/spec/integration/inventory_spec.cr b/spec/integration/inventory_spec.cr index b681866b..74a32fd1 100644 --- a/spec/integration/inventory_spec.cr +++ b/spec/integration/inventory_spec.cr @@ -7,7 +7,7 @@ Spectator.describe Rosegold::Bot do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| bot.chat "/clear" - sleep 1 + bot.wait_for Rosegold::Clientbound::SetSlot expect(bot.inventory.count("bucket")).to eq 0 end @@ -20,7 +20,7 @@ Spectator.describe Rosegold::Bot do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| bot.chat "/clear" - sleep 1 + bot.wait_for Rosegold::Clientbound::SetSlot bot.chat "/give #{bot.username} minecraft:bucket 16" bot.chat "/give #{bot.username} minecraft:bucket 1" @@ -38,10 +38,10 @@ Spectator.describe Rosegold::Bot do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| bot.chat "/clear" - sleep 1 + bot.wait_for Rosegold::Clientbound::SetSlot bot.chat "/give #{bot.username} minecraft:bucket 513" - sleep 1 + bot.wait_ticks 5 expect(bot.inventory.count("bucket")).to eq 513 end @@ -56,8 +56,7 @@ Spectator.describe Rosegold::Bot do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| bot.chat "/clear" - - sleep 1 + bot.wait_for Rosegold::Clientbound::SetSlot expect(bot.inventory.pick("diamond_pickaxe")).to eq false expect(bot.inventory.pick("stone")).to eq false @@ -72,9 +71,11 @@ Spectator.describe Rosegold::Bot do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| bot.chat "/clear" + bot.wait_for Rosegold::Clientbound::SetSlot bot.chat "/give #{bot.username} minecraft:stone 42" + bot.wait_for Rosegold::Clientbound::SetSlot bot.chat "/give #{bot.username} minecraft:grass_block 43" - sleep 1 + bot.wait_for Rosegold::Clientbound::SetSlot expect(bot.inventory.pick("stone")).to eq true expect(bot.inventory.main_hand.item_id).to eq "stone" @@ -90,9 +91,11 @@ Spectator.describe Rosegold::Bot do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| bot.chat "/clear" + bot.wait_for Rosegold::Clientbound::SetSlot bot.chat "/give #{bot.username} minecraft:stone #{64*9}" + bot.wait_for Rosegold::Clientbound::SetSlot bot.chat "/give #{bot.username} minecraft:grass_block 1" - sleep 1 + bot.wait_for Rosegold::Clientbound::SetSlot expect(bot.inventory.pick("grass_block")).to eq true expect(bot.inventory.main_hand.item_id).to eq "grass_block" @@ -108,7 +111,7 @@ Spectator.describe Rosegold::Bot do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| bot.chat "/clear" - sleep 1 + bot.wait_for Rosegold::Clientbound::SetSlot expect { bot.inventory.pick!("diamond_pickaxe") }.to raise_error(Rosegold::Inventory::ItemNotFoundError) end end @@ -120,7 +123,9 @@ Spectator.describe Rosegold::Bot do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| bot.chat "/clear" + bot.wait_for Rosegold::Clientbound::SetSlot bot.chat "/give #{bot.username} minecraft:diamond_pickaxe{Damage:1550,Enchantments:[{id:efficiency,lvl:1}]} 1" + bot.wait_for Rosegold::Clientbound::SetSlot expect { bot.inventory.pick!("diamond_pickaxe") }.to raise_error(Rosegold::Inventory::ItemNotFoundError) end end @@ -133,17 +138,17 @@ Spectator.describe Rosegold::Bot do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| bot.chat "/fill ~ ~ ~ ~ ~ ~ minecraft:air" - sleep 1 + bot.wait_tick bot.chat "/setblock ~ ~ ~ minecraft:chest{Items:[{Slot:7b, id: \"minecraft:diamond_sword\",Count:1b}]}" bot.chat "/clear" - sleep 1 + bot.wait_for Rosegold::Clientbound::SetSlot + bot.wait_tick bot.pitch = 90 bot.use_hand - sleep 1 - expect(bot.inventory.withdraw_at_least(1, "diamond_sword")).to eq 1 + bot.wait_for Rosegold::Clientbound::WindowItems - sleep 1 + expect(bot.inventory.withdraw_at_least(1, "diamond_sword")).to eq 1 local_inventory = bot.inventory.inventory.map &.dup local_hotbar = bot.inventory.hotbar.map &.dup @@ -152,9 +157,9 @@ Spectator.describe Rosegold::Bot do expect((local_inventory + local_hotbar).map(&.item_id)).to contain "diamond_sword" expect(local_content.map(&.item_id)).not_to contain "diamond_sword" - sleep 1 bot.use_hand - sleep 1 + bot.wait_for Rosegold::Clientbound::WindowItems + bot.wait_tick expect(local_inventory.map(&.item_id)).to match_array bot.inventory.inventory.map(&.item_id) expect(local_hotbar.map(&.item_id)).to match_array bot.inventory.hotbar.map(&.item_id) @@ -172,16 +177,16 @@ Spectator.describe Rosegold::Bot do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| bot.chat "/fill ~ ~ ~ ~ ~ ~ minecraft:air" - sleep 1 + bot.wait_tick bot.chat "/setblock ~ ~ ~ minecraft:chest{Items:[]}" bot.chat "/clear" - sleep 1 + bot.wait_for Rosegold::Clientbound::SetSlot bot.chat "/give #{bot.username} minecraft:diamond_sword 1" - sleep 1 + bot.wait_for Rosegold::Clientbound::SetSlot bot.pitch = 90 bot.use_hand - sleep 1 + bot.wait_for Rosegold::Clientbound::WindowItems expect(bot.inventory.deposit_at_least(1, "diamond_sword")).to eq 1 @@ -192,9 +197,8 @@ Spectator.describe Rosegold::Bot do expect((local_inventory + local_hotbar).map(&.item_id)).not_to contain "diamond_sword" expect(local_content.map(&.item_id)).to contain "diamond_sword" - sleep 1 bot.use_hand - sleep 1 + bot.wait_for Rosegold::Clientbound::WindowItems expect(local_inventory.map(&.item_id)).to match_array bot.inventory.inventory.map(&.item_id) expect(local_hotbar.map(&.item_id)).to match_array bot.inventory.hotbar.map(&.item_id) @@ -213,21 +217,18 @@ Spectator.describe Rosegold::Bot do Rosegold::Bot.new(client).try do |bot| bot.chat "/tp #{bot.username} -10 -60 -10" bot.chat "/fill ~ ~ ~ ~ ~ ~ minecraft:air" - sleep 1 + bot.wait_tick bot.chat "/setblock ~ ~ ~ minecraft:chest{Items:[{Slot:7b, id: \"minecraft:diamond_sword\",Count:1b}]}" bot.chat "/clear" - sleep 1 bot.pitch = 90 bot.use_hand - sleep 1 + bot.wait_for Rosegold::Clientbound::WindowItems bot.inventory.withdraw_at_least(1, "diamond_sword") - sleep 1 - bot.inventory.close - sleep 1 + bot.wait_tick slots_before_reload = bot.inventory.slots @@ -251,14 +252,17 @@ Spectator.describe Rosegold::Bot do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| bot.chat "/clear" + bot.wait_for Rosegold::Clientbound::SetSlot bot.chat "/give #{bot.username} minecraft:diamond_sword 4" + bot.wait_for Rosegold::Clientbound::SetSlot bot.chat "/give #{bot.username} minecraft:stone 200" - sleep 1 + bot.wait_for Rosegold::Clientbound::SetSlot + bot.look = Rosegold::Look.new 0, 0 expect(bot.inventory.throw_all_of "diamond_sword").to eq 4 expect(bot.inventory.throw_all_of "stone").to eq 200 - sleep 1 + bot.wait_tick expect(bot.inventory.inventory.map(&.item_id)).not_to contain "diamond_sword" expect(bot.inventory.hotbar.map(&.item_id)).not_to contain "diamond_sword" diff --git a/spec/integration/movement_spec.cr b/spec/integration/movement_spec.cr index 95ab48df..4ae4cc69 100644 --- a/spec/integration/movement_spec.cr +++ b/spec/integration/movement_spec.cr @@ -1,12 +1,22 @@ require "../spec_helper" Spectator.describe Rosegold::Bot do + before_all do + client.join_game do |client| + Rosegold::Bot.new(client).try do |bot| + bot.chat "/kill @e[type=!minecraft:player]" + bot.chat "/fill -10 -60 -10 10 0 10 minecraft:air" + bot.chat "/fill -10 -61 -10 10 -61 10 minecraft:bedrock" + bot.wait_tick + end + end + end + it "should fall due to gravity" do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| - sleep 2 # load chunks bot.chat "/tp 1 -58 1" - sleep 1 # teleport + bot.wait_tick until client.player.on_ground? bot.wait_tick end @@ -18,9 +28,8 @@ Spectator.describe Rosegold::Bot do it "can move to location successfully" do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| - sleep 2 # load chunks bot.chat "/tp 1 -60 1" - sleep 1 # teleport + bot.wait_tick bot.move_to 2, 2 expect(bot.feet).to eq(Rosegold::Vec3d.new(2.5, -60, 2.5)) @@ -52,9 +61,8 @@ Spectator.describe Rosegold::Bot do it "should jump and fall" do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| - sleep 2 # load chunks bot.chat "/tp 1 -60 1" - sleep 1 # teleport + bot.wait_tick initial_feet = bot.feet @@ -77,17 +85,16 @@ Spectator.describe Rosegold::Bot do it "throws Physics::MovementStuck" do client.join_game do |client| Rosegold::Bot.new(client).try do |bot| - sleep 2 # load chunks bot.chat "/fill 1 -60 2 1 -60 2 minecraft:stone" bot.chat "/tp 1 -60 1" - sleep 1 # teleport + bot.wait_tick expect { bot.move_to 1, 2 }.to raise_error(Rosegold::Physics::MovementStuck) bot.chat "/fill 1 -60 2 1 -60 2 minecraft:air" - sleep 1 + bot.wait_tick end end end diff --git a/src/rosegold/bot.cr b/src/rosegold/bot.cr index 60adc2df..8f139965 100644 --- a/src/rosegold/bot.cr +++ b/src/rosegold/bot.cr @@ -8,8 +8,15 @@ class Rosegold::Bot < Rosegold::EventEmitter def initialize(@client) @inventory = Inventory.new client - @interact = Interactions.new client - client.on(Rosegold::Clientbound::ChatMessage) do |packet| + + subscribe Rosegold::Clientbound::ChatMessage + subscribe Event::Tick + subscribe Rosegold::Clientbound::WindowItems + subscribe Rosegold::Clientbound::SetSlot + end + + def subscribe(event_class : Class) + client.on event_class do |packet| emit_event packet end end @@ -28,7 +35,7 @@ class Rosegold::Bot < Rosegold::EventEmitter delegate uuid, username, feet, eyes, health, food, saturation, gamemode, sneaking?, sprinting?, to: client.player delegate sneak, sprint, to: client.physics delegate main_hand, to: inventory - delegate stop_using_hand, stop_digging, to: @interact + delegate stop_using_hand, stop_digging, to: client.interactions delegate x, y, z, to: feet def disconnect_reason @@ -56,13 +63,14 @@ class Rosegold::Bot < Rosegold::EventEmitter client.queue_packet Serverbound::ChatMessage.new message end - # Is adjusted to server TPS. def wait_ticks(ticks : Int32) - sleep ticks / 20 # TODO adjust to server TPS, changing over time + ticks.times do + wait_tick + end end def wait_tick - wait_ticks 1 + wait_for Event::Tick, timeout: 1.second end # Direction the player is looking. @@ -200,7 +208,6 @@ class Rosegold::Bot < Rosegold::EventEmitter # Selects the active (main hand) hotbar slot number (1-9). def hotbar_selection=(index : UInt8) # TODO check range - client.queue_packet Serverbound::HeldItemChange.new index - 1 client.player.hotbar_selection = index - 1 end @@ -227,7 +234,7 @@ class Rosegold::Bot < Rosegold::EventEmitter # Activates the "use" button. def start_using_hand(hand : Hand = :main_hand) # can't delegate this because it wouldn't pick up the symbol as a Hand value - @interact.start_using_hand hand + client.interactions.start_using_hand hand end # Looks in the direction of `target`, then @@ -248,21 +255,23 @@ class Rosegold::Bot < Rosegold::EventEmitter return if food >= 15 && full_health? return if food >= 18 # above healing threshold - Log.debug { "Eating because food is #{food} and health is #{health}" } + Log.info { "Eating because food is #{food} and health is #{health}" } - inventory.pick("baked_potato") || + inventory.pick("apple") || + inventory.pick("baked_potato") || inventory.pick("bread") || inventory.pick("carrot") || raise "Bot food not found" - 10.times do - client.send_packet! Serverbound::UseItem.new :main_hand + start_using_hand + + until food >= 18 wait_ticks 33 - client.send_packet! Serverbound::PlayerDigging.new :finish_using_hand - break if food >= 18 end - Log.debug { "Eating finished, food is #{food} and health is #{health}" } + stop_using_hand + + Log.info { "Eating finished, food is #{food} and health is #{health}" } end def full_health? @@ -273,7 +282,7 @@ class Rosegold::Bot < Rosegold::EventEmitter def start_digging(target : Vec3d? | Look? = nil) look_at target if target.is_a? Vec3d look target if target.is_a? Look - @interact.start_digging + client.interactions.start_digging end # Looks in the direction of target, then diff --git a/src/rosegold/client.cr b/src/rosegold/client.cr index 1f38646b..eb484d37 100644 --- a/src/rosegold/client.cr +++ b/src/rosegold/client.cr @@ -4,10 +4,9 @@ require "../minecraft/auth" require "./control/*" require "./models/*" require "./packets/*" +require "./events/*" require "./world/*" -abstract class Rosegold::Event; end # defined elsewhere, but otherwise it would be a module - class Rosegold::Event::RawPacket < Rosegold::Event getter bytes : Bytes @@ -29,6 +28,7 @@ class Rosegold::Client < Rosegold::EventEmitter access_token : String = "", dimension : Dimension = Dimension.new, physics : Physics, + interactions : Interactions, inventory : PlayerWindow, window : Window, offline : NamedTuple(uuid: String, username: String)? = nil @@ -39,9 +39,11 @@ class Rosegold::Client < Rosegold::EventEmitter @port = port_str.to_i end @physics = uninitialized Physics + @interactions = uninitialized Interactions @inventory = uninitialized PlayerWindow @window = uninitialized Window @physics = Physics.new self + @interactions = Interactions.new self @inventory = PlayerWindow.new self @window = @inventory end @@ -79,6 +81,8 @@ class Rosegold::Client < Rosegold::EventEmitter raise NotConnected.new "Took too long to join the game" if timeout_ticks <= 0 end + start_ticker + self end @@ -90,6 +94,22 @@ class Rosegold::Client < Rosegold::EventEmitter self end + def start_ticker + spawn do + loop do + sleep 1.tick + + break unless connected? + + spawn do + interactions.tick + physics.tick + emit_event Event::Tick.new + end + end + end + end + def connect raise NotConnected.new "Already connected" if connected? diff --git a/src/rosegold/control/interactions.cr b/src/rosegold/control/interactions.cr index 84dcf15a..d395d0aa 100644 --- a/src/rosegold/control/interactions.cr +++ b/src/rosegold/control/interactions.cr @@ -10,104 +10,133 @@ class Rosegold::Interactions def initialize(@intercept, @block, @face); end end - # not exposed, for rules compliance - @using_hand = false + @using_hand = nil + @queue_using_hand = nil + @using_hand_delay = 0_i32 @digging_block : ReachedBlock? @dig_hand_swing_countdown = 0_i8 + @attack_queued = false + @digging = false + @block_damage_progress = 0_f32 + @last_tick_held_item : Slot = Slot.new + @sent_held_item_index : UInt8? getter client : Client property? digging : Bool = false def initialize(@client) - client.on Event::PhysicsTick do - on_physics_tick - end end # Activates the "use" button. - def start_using_hand(hand : Hand = :main_hand) - reached = reach_block_or_entity - if reached.is_a? ReachedBlock - place_block hand, reached - else - @using_hand = true - send_packet Serverbound::UseItem.new hand - send_packet Serverbound::SwingArm.new - end + def start_using_hand(hand : Hand = :main_hand) # TODO: Auto select hand each tick + @using_hand = hand + @queue_using_hand = hand end # Deactivates the "use" button. def stop_using_hand return unless @using_hand - @using_hand = false + + @using_hand = nil + # TODO: seems to be only for eating + # move to tick loop send_packet Serverbound::PlayerDigging.new :finish_using_hand end # Activates the "attack" button. def start_digging return if digging? + self.digging = true + @attack_queued = true + end - attack - - spawn do - while digging? && client.connected? - cancel = false - - case reached = wait_for_reached_block - when ReachedBlock - start_digging reached - - client.dimension.block_state(reached.block).try do |block_state| - block = Block.from_block_state_id block_state - next sleep 1.tick if block.id_str == "air" - - block.break_time(inventory.main_hand, client.player).to_i.times do - sleep 1.tick - reached = reach_block_or_entity - if reached.is_a?(Entity) || reached.try &.block != @digging_block.try &.block - cancel_digging - cancel = true - break - end - end - end - - finish_digging if digging? && !cancel - else - next sleep 1.tick - end - end - end + # Deactivates the "attack" button. + def stop_digging + return unless digging? + + self.digging = false end - def wait_for_reached_block - until reached = reach_block_or_entity - sleep 1.tick - end + def tick + tick_held_item + tick_attack + tick_digging + tick_using_hand + @last_tick_held_item = inventory.main_hand + end - reached + private def inventory + Inventory.new client + end + + private def tick_held_item + if @sent_held_item_index != client.player.hotbar_selection + @sent_held_item_index = client.player.hotbar_selection + send_packet Serverbound::HeldItemChange.new client.player.hotbar_selection + end end - private def attack - if reached = reach_block_or_entity - return if reached.is_a? ReachedBlock + private def tick_attack + return unless @attack_queued + @attack_queued = false + + case reached = reach_block_or_entity + when Entity send_packet Serverbound::InteractEntity.new reached.entity_id, :attack send_packet Serverbound::SwingArm.new + when ReachedBlock + start_digging reached end end - # Deactivates the "attack" button. - def stop_digging - return unless digging? - self.digging = false - cancel_digging + private def tick_digging + reached = reach_block_or_entity + if @digging_block + if !digging? + cancel_digging + return + end + + case reached + when Entity + # do nothing, but retain @block_damage_progress like vanilla client + when ReachedBlock + tick_digging_block reached + when nil + cancel_digging + end + else + return unless digging? + + if reached.is_a? ReachedBlock + start_digging reached + end + end end - private def on_physics_tick - # TODO if buttons are being held, keep placing more blocks or throwing/eating more items - if @digging_block + private def tick_digging_block(reached) + if digging_block = @digging_block + if reached.block != digging_block.block + cancel_digging + end + + if @last_tick_held_item != inventory.main_hand + cancel_digging + return + end + + client.dimension.block_state(digging_block.block).try do |block_state| + block = Block.from_block_state_id block_state + @block_damage_progress += block.break_damage inventory.main_hand, client.player + end + + if @block_damage_progress >= 1.0 + finish_digging + @block_damage_progress = 0.0 + end + @dig_hand_swing_countdown -= 1 if @dig_hand_swing_countdown <= 0 @dig_hand_swing_countdown = 6 @@ -116,8 +145,38 @@ class Rosegold::Interactions end end + private def tick_using_hand + @using_hand_delay -= 1 if @using_hand_delay > 0 + return if @using_hand_delay > 0 + + if using_hand = @using_hand || @queue_using_hand + @using_hand_delay = using_hand_delay_for inventory.main_hand + @queue_using_hand = nil + case reached = reach_block_or_entity + when Entity + Log.warn { "Rosegold does not support using items on entities yet" } + when ReachedBlock + puts "place block" + place_block using_hand, reached + send_packet Serverbound::UseItem.new using_hand + else + puts "use item" + send_packet Serverbound::UseItem.new using_hand + end + end + end + + # need a method to increase using_hand_delay if the item being used is a food item + def using_hand_delay_for(slot) + if slot.edible? + 50 + else + 4 + end + end + private def send_packet(packet) - client.queue_packet packet + client.send_packet! packet end private def place_block(hand : Hand, reached : ReachedBlock) @@ -137,7 +196,7 @@ class Rosegold::Interactions send_packet Serverbound::SwingArm.new end - def finish_digging + private def finish_digging reached = @digging_block return unless reached @digging_block = nil @@ -145,11 +204,11 @@ class Rosegold::Interactions :finish, reached.block, reached.face end - # TODO decide finish/cancel based on block dig time - def cancel_digging + private def cancel_digging reached = @digging_block return unless reached @digging_block = nil + @block_damage_progress = 0.0 send_packet Serverbound::PlayerDigging.new \ :cancel, reached.block, reached.face end diff --git a/src/rosegold/control/inventory.cr b/src/rosegold/control/inventory.cr index 4d684ec0..9f58db3d 100644 --- a/src/rosegold/control/inventory.cr +++ b/src/rosegold/control/inventory.cr @@ -33,7 +33,6 @@ class Rosegold::Inventory hotbar.each_with_index do |slot, index| if slot.matches?(spec) && !slot.needs_repair? - client.send_packet! Serverbound::HeldItemChange.new index.to_u8 client.player.hotbar_selection = index.to_u8 return true end @@ -83,10 +82,15 @@ class Rosegold::Inventory deposit_at_least(count, spec) end + # Finds an empty slot in the source + # In order to match vanilla: + # When source is the container, prioritize first empty #container slot + # When source is the player inventory, prioritize rightmost empty #hotbar slot + # then rightmost empty #inventory slot private def find_empty_slot(source) empty_slot = nil - source.each do |slot| + source.sort { |a, b| b.slot_number <=> a.slot_number }.each do |slot| if slot.empty? empty_slot = slot break diff --git a/src/rosegold/control/physics.cr b/src/rosegold/control/physics.cr index c9e147ca..b31411f9 100644 --- a/src/rosegold/control/physics.cr +++ b/src/rosegold/control/physics.cr @@ -1,18 +1,7 @@ require "../client" require "./action" require "./raytrace" - -struct Int32 - def ticks - self / 20 - end - - def tick - ticks - end -end - -abstract class Rosegold::Event; end # defined elsewhere, but otherwise it would be a module +require "../events/**" class Rosegold::Event::PhysicsTick < Rosegold::Event getter movement : Vec3d @@ -34,7 +23,6 @@ class Rosegold::Physics VERY_CLOSE = 0.00001 # consider arrived at target if squared distance is closer than this private getter client : Rosegold::Client - private property ticker : Fiber? property? paused : Bool = true property? jump_queued : Bool = false private getter movement_action : Action(Vec3d)? @@ -58,16 +46,6 @@ class Rosegold::Physics def handle_reset @paused = false player.velocity = Vec3d::ORIGIN - - ticker.try do |t| - return unless t.dead? - end - self.ticker = spawn do - while client.connected? - tick - sleep 1/20 - end - end end def handle_disconnect diff --git a/src/rosegold/events/event.cr b/src/rosegold/events/event.cr new file mode 100644 index 00000000..2a91da13 --- /dev/null +++ b/src/rosegold/events/event.cr @@ -0,0 +1 @@ +abstract class Rosegold::Event; end diff --git a/src/rosegold/events/tick.cr b/src/rosegold/events/tick.cr new file mode 100644 index 00000000..c193a28e --- /dev/null +++ b/src/rosegold/events/tick.cr @@ -0,0 +1,14 @@ +require "./event" + +struct Int32 + def ticks + 1.second / 20 + end + + def tick + ticks + end +end + +class Rosegold::Event::Tick < Rosegold::Event +end diff --git a/src/rosegold/models/block.cr b/src/rosegold/models/block.cr index 903fb1ed..bad30593 100644 --- a/src/rosegold/models/block.cr +++ b/src/rosegold/models/block.cr @@ -46,8 +46,8 @@ class Rosegold::Block best_tool?(slot) || harvest_tools.try &.keys.includes? slot.item_id_int.to_s end - def break_time(main_hand : Slot, player : Player, creative : Bool = false) : Int32 - return 0 if creative + def break_damage(main_hand : Slot, player : Player, creative : Bool = false) : Float64 + return 0_f64 if creative speed_multiplier = 1.0 if best_tool?(main_hand) @@ -68,8 +68,16 @@ class Rosegold::Block damage = speed_multiplier / hardness damage /= can_harvest?(main_hand) ? 30 : 100 - return 0 if damage > 1 + return 0_f64 if damage > 1 + + damage + end + + def break_time(main_hand : Slot, player : Player, creative : Bool = false) : Int32 + break_damage = break_damage(main_hand, player, creative) + + return 0 if break_damage.zero? - (1.0 / damage).ceil.to_i + (1.0 / break_damage).ceil.to_i end end diff --git a/src/rosegold/models/event_emitter.cr b/src/rosegold/models/event_emitter.cr index 4a85d822..913a1203 100644 --- a/src/rosegold/models/event_emitter.cr +++ b/src/rosegold/models/event_emitter.cr @@ -1,5 +1,3 @@ -abstract class Rosegold::Event; end - require "uuid" class Rosegold::EventEmitter diff --git a/src/rosegold/packets/clientbound/destroy_entities.cr b/src/rosegold/packets/clientbound/destroy_entities.cr index 2f8b7b07..e1c6e7f2 100644 --- a/src/rosegold/packets/clientbound/destroy_entities.cr +++ b/src/rosegold/packets/clientbound/destroy_entities.cr @@ -23,7 +23,6 @@ class Rosegold::Clientbound::DestroyEntities < Rosegold::Clientbound::Packet end def callback(client) - Log.debug { "Received destroy entities packet for #{entity_ids.size} entities" } entity_ids.each { |entity_id| client.dimension.entities.delete(entity_id) } end end diff --git a/src/rosegold/packets/clientbound/entity_position.cr b/src/rosegold/packets/clientbound/entity_position.cr index cd5dd391..7d09c4ba 100644 --- a/src/rosegold/packets/clientbound/entity_position.cr +++ b/src/rosegold/packets/clientbound/entity_position.cr @@ -35,7 +35,6 @@ class Rosegold::Clientbound::EntityPosition < Rosegold::Clientbound::Packet end def callback(client) - Log.debug { "Received entity position packet for entity ID #{entity_id}, delta X #{delta_x}, delta Y #{delta_y}, delta Z #{delta_z}" } entity = client.dimension.entities[entity_id]? if entity.nil? diff --git a/src/rosegold/packets/clientbound/set_slot.cr b/src/rosegold/packets/clientbound/set_slot.cr index 60c6f218..f0ef32d4 100644 --- a/src/rosegold/packets/clientbound/set_slot.cr +++ b/src/rosegold/packets/clientbound/set_slot.cr @@ -29,7 +29,6 @@ class Rosegold::Clientbound::SetSlot < Rosegold::Clientbound::Packet end def callback(client) - Log.debug { "Server set slot #{slot}" } if window_id == -1 && slot.slot_number == -1 client.window.cursor = slot elsif window_id == 0 diff --git a/src/rosegold/packets/clientbound/spawn_entity.cr b/src/rosegold/packets/clientbound/spawn_entity.cr index ea3aa13b..df450f05 100644 --- a/src/rosegold/packets/clientbound/spawn_entity.cr +++ b/src/rosegold/packets/clientbound/spawn_entity.cr @@ -54,7 +54,6 @@ class Rosegold::Clientbound::SpawnEntity < Rosegold::Clientbound::Packet end def callback(client) - Log.debug { "Received spawn entity packet for entity ID #{entity_id}, UUID #{uuid}, type #{entity_type}" } client.dimension.entities[entity_id] = Rosegold::Entity.new \ entity_id, uuid, diff --git a/src/rosegold/packets/clientbound/spawn_living_entity.cr b/src/rosegold/packets/clientbound/spawn_living_entity.cr index 8fbe3c73..2cef94df 100644 --- a/src/rosegold/packets/clientbound/spawn_living_entity.cr +++ b/src/rosegold/packets/clientbound/spawn_living_entity.cr @@ -60,6 +60,7 @@ class Rosegold::Clientbound::SpawnLivingEntity < Rosegold::Clientbound::Packet pitch, yaw, head_yaw, - Vec3d.new(velocity_x, velocity_y, velocity_z) + Vec3d.new(velocity_x, velocity_y, velocity_z), + living: true end end diff --git a/src/rosegold/world/dimension.cr b/src/rosegold/world/dimension.cr index 6f9c7faa..af139d55 100644 --- a/src/rosegold/world/dimension.cr +++ b/src/rosegold/world/dimension.cr @@ -64,6 +64,8 @@ class Rosegold::Dimension ray_end = start + look * max_distance entities.each_value do |entity| + next unless entity.living? + bounding_box = entity.bounding_box distance = bounding_box.ray_intersection(start, ray_end) next if distance.nil? diff --git a/src/rosegold/world/entity.cr b/src/rosegold/world/entity.cr index fcc6d801..463f5946 100644 --- a/src/rosegold/world/entity.cr +++ b/src/rosegold/world/entity.cr @@ -29,9 +29,10 @@ class Rosegold::Entity effects : Array(EntityEffect) = [] of EntityEffect property? \ - on_ground : Bool = true + on_ground : Bool = true, + living : Bool = false - def initialize(@entity_id, @uuid, @entity_type, @position, @pitch, @yaw, @head_yaw, @velocity, @on_ground = true) + def initialize(@entity_id, @uuid, @entity_type, @position, @pitch, @yaw, @head_yaw, @velocity, @on_ground = true, @living = false) end def metadata diff --git a/src/rosegold/world/slot.cr b/src/rosegold/world/slot.cr index e49a14d5..49702c26 100644 --- a/src/rosegold/world/slot.cr +++ b/src/rosegold/world/slot.cr @@ -135,6 +135,19 @@ class Rosegold::Slot other.nbt = tmp end + def edible? : Bool + [ + "apple", "baked_potato", "beef", "beetroot", "beetroot_soup", "bread", "carrot", + "chicken", "chorus_fruit", "cod", "cooked_beef", "cooked_chicken", "cooked_cod", + "cooked_mutton", "cooked_porkchop", "cooked_rabbit", "cooked_salmon", "cookie", + "dried_kelp", "enchanted_golden_apple", "golden_apple", "golden_carrot", + "honey_bottle", "melon_slice", "mushroom_stew", "mutton", "poisonous_potato", + "porkchop", "potato", "pufferfish", "pumpkin_pie", "rabbit", "rabbit_stew", + "rotten_flesh", "salmon", "spider_eye", "suspicious_stew", "sweet_berries", + "glow_berries", "tropical_fish", + ].includes? item_id + end + def to_s(io) inspect io end