diff --git a/blocks.py b/blocks.py index 1937048..8a19a11 100644 --- a/blocks.py +++ b/blocks.py @@ -194,6 +194,12 @@ NON_SOLID = [ 'cave_air', 'lantern', 'soul_torch', + #'oak_sapling', # saplings can grow up and hurt + #'spruce_sapling', + #'birch_sapling', + #'jungle_sapling', + #'acacia_sapling', + #'dark_oak_sapling', ] LOGS = [ @@ -214,6 +220,15 @@ LEAVES = [ 'dark_oak_leaves', ] +SAPLINGS = [ + 'oak_sapling', + 'spruce_sapling', + 'birch_sapling', + 'jungle_sapling', + 'acacia_sapling', + 'dark_oak_sapling', +] + CHESTS = [ 'chest', ] @@ -253,6 +268,11 @@ for block_name in INDEXED: for state in JSON_BLOCKS['minecraft:' + block_name]['states']: INDEXED_IDS.add(state['id']) +SAPLING_IDS = set() +for block_name in SAPLINGS: + for state in JSON_BLOCKS['minecraft:' + block_name]['states']: + SAPLING_IDS.add(state['id']) + def get(bid): name = BLOCKS[bid] diff --git a/bot.py b/bot.py index 95cb976..0859db7 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ from minecraft.networking.packets import Packet, clientbound, serverbound from protocol.managers import DataManager, ChunksManager, ChatManager, ChunkNotLoadedException -from bunch import Bunch +from munch import Munch from panda3d.core import LPoint3f, LVector3f import game @@ -59,6 +59,37 @@ def tick(global_state): g.chunks.unload_chunks(p) + ########## object physics ########## + + for eid, obj in g.objects.items(): + start_x = obj.x + + if obj.velocity_x: + obj.x += obj.velocity_x / 8000 + if obj.velocity_y: + obj.y += obj.velocity_y / 8000 + if obj.velocity_z: + obj.z += obj.velocity_z / 8000 + + block_below = g.chunks.get_block_at(floor(obj.x), int(obj.y-0.20), floor(obj.z)) + in_air = block_below in blocks.NON_SOLID_IDS + + if in_air: + obj.velocity_x *= 0.988 + obj.velocity_y -= 390 + obj.velocity_z *= 0.988 + else: + obj.y = int(obj.y-0.20)+1 + obj.velocity_x *= 0.5 + obj.velocity_y = 0 + obj.velocity_z *= 0.5 + + if abs(obj.velocity_x) < 1: obj.velocity_x = 0 + if abs(obj.velocity_z) < 1: obj.velocity_z = 0 + + + ########## player physics ########## + if g.path and len(g.path): target = LPoint3f(g.path[0]) target.x += 0.5 @@ -162,6 +193,7 @@ def init(global_state): g.window = None g.job = jobs.JobStates(g) + g.chopped_tree = False def bot(global_state): g = global_state diff --git a/game.py b/game.py index e143ed1..ddebf0a 100644 --- a/game.py +++ b/game.py @@ -1,9 +1,10 @@ import re import time import importlib +import random from math import hypot from itertools import count -from bunch import Bunch +from munch import Munch from panda3d.core import LPoint3f @@ -17,8 +18,7 @@ from protocol.packets import ( ClickWindowPacket, CloseWindowPacket, ServerWindowConfirmationPacket, ClientWindowConfirmationPacket, EntityMetadataPacket, SpawnLivingEntityPacket, EntityPositionPacket, - EntityPositionRotationPacket, - + EntityPositionRotationPacket, DestroyEntitiesPacket, EntityVelocityPacket, ) from protocol.types import Slot @@ -93,7 +93,7 @@ class MCWorld: result = [] # TODO: make sure only non-solid and leaves between - # make sure traversable too + # make sure traversable too and non-avoid for distance in range(5): for direction in path.CHECK_DIRECTIONS: @@ -203,6 +203,8 @@ class Game: register(self.handle_spawn_living, SpawnLivingEntityPacket) register(self.handle_entity_position, EntityPositionPacket) register(self.handle_entity_position_rotation, EntityPositionRotationPacket) + register(self.handle_destroy_entities, DestroyEntitiesPacket) + register(self.handle_entity_velocity, EntityVelocityPacket) #register(self.handle_packet, Packet, early=True) @@ -224,8 +226,8 @@ class Game: solution = path.Pathfinder(self.g.chunks).astar(utils.pint(self.g.pos), utils.pint(self.g.goal)) if solution: solution = list(solution) - #self.g.path = solution - #self.g.job.state = self.g.job.stop + self.g.path = solution + self.g.job.state = self.g.job.stop print(len(solution)) print(solution) print(round(time.time() - start, 3), 'seconds') @@ -320,6 +322,19 @@ class Game: else: reply += ', I need a bed' + if command == 'farm' and data: + if data == 'wood': + self.g.job.state = self.g.job.farm_wood + reply = 'ok' + + if reply: + for i in self.g.inv.values(): + print(i.item_id) + if i.item_id in items.BED_IDS: + break + else: + reply += ', I need a bed' + if command == 'stop': self.g.job.state = self.g.job.stop reply = 'ok' @@ -386,6 +401,11 @@ class Game: self.g.job.find_gapple_states.count = int(data) reply = 'ok' + if command == 'objects': + for k, v in self.g.objects.items(): + if data and v.item_id != int(data): continue + print(str(k) + ':', v) + if reply: print(reply) self.g.chat.send(reply) @@ -483,6 +503,23 @@ class Game: else: #for return False + def select_random_item(self, items): + # select a random match from items of inv + # this is random per item type + # example: 5 stacks wood, 1 stack glass + # -> still 50/50 chance between them + + matches = set() + for slot, item in self.g.inv.items(): + if item.item_id in items: + matches.add(item.item_id) + + if matches: + return self.select_item([random.choice(list(matches))]) + else: + return False + + def drop_stack(self): packet = PlayerDiggingPacket() packet.status = 3 @@ -497,7 +534,7 @@ class Game: def handle_window(self, packet): print(packet) - self.g.window = Bunch(data=packet, contents=dict(), count=0) + self.g.window = Munch(data=packet, contents=dict(), count=0) def click_window(self, slot, button, mode, item): w = self.g.window @@ -529,41 +566,76 @@ class Game: self.g.connection.write_packet(packet2) def handle_spawn_object(self, packet): - return + #return if packet.type_id != 37: return print(packet) - - def handle_entity_metadata(self, packet): - if not packet.metadata: - return - + self.g.objects[packet.entity_id] = Munch( + x=packet.x, + y=packet.y, + z=packet.z, + velocity_x=packet.velocity_x, + velocity_y=packet.velocity_y, + velocity_z=packet.velocity_z, + ) + + def check_gapple(self, packet): if not self.g.job: return - current_chest = self.g.job.find_gapple_states.current_chest - if not current_chest: - return + current_gapple_chest = self.g.job.find_gapple_states.current_chest + if current_gapple_chest: + for entry in packet.metadata: + if entry.type != 6: + continue - for entry in packet.metadata: - if entry.type != 6: - continue + if entry.value.item_id in items.GAPPLE_ID: + self.g.chat.send('gapple found: ' + str(current_gapple_chest)[1:-1]) + print('gapple found:', str(current_gapple_chest)[1:-1]) - if entry.value.item_id in items.GAPPLE_ID: - self.g.chat.send('gapple found: ' + str(current_chest)[1:-1]) - print('gapple found:', str(current_chest)[1:-1]) + def handle_entity_metadata(self, packet): + if not packet.metadata: + return + + self.check_gapple(packet) + obj = self.g.objects.get(packet.entity_id, None) + if obj: + for entry in packet.metadata: + if entry.type != 6: + continue + obj.item_id = entry.value.item_id + obj.item_count = entry.value.item_count def handle_spawn_living(self, packet): return print(packet) def handle_entity_position(self, packet): - return - print(packet) + obj = self.g.objects.get(packet.entity_id, None) + if obj: + pass + #obj.x += packet.delta_x + #obj.y += packet.delta_y + #obj.z += packet.delta_z def handle_entity_position_rotation(self, packet): - return - print(packet) + obj = self.g.objects.get(packet.entity_id, None) + if obj: + print('object rotation found:', packet) + raise + + def handle_entity_velocity(self, packet): + obj = self.g.objects.get(packet.entity_id, None) + if obj: + print(packet) + #obj.velocity_x = packet.velocity_x + #obj.velocity_y = packet.velocity_y + #obj.velocity_z = packet.velocity_z + + def handle_destroy_entities(self, packet): + for eid in packet.entity_ids: + if eid in self.g.objects: + del self.g.objects[eid] def tick(self): if self.g.breaking: diff --git a/items.py b/items.py index db56616..9a22134 100644 --- a/items.py +++ b/items.py @@ -22,10 +22,23 @@ BEDS = [ 'black_bed', ] +SAPLINGS = [ + 'oak_sapling', + 'spruce_sapling', + 'birch_sapling', + 'jungle_sapling', + 'acacia_sapling', + 'dark_oak_sapling', +] + BED_IDS = set() for item_name in BEDS: BED_IDS.add(ITEMS['minecraft:'+item_name]['protocol_id']) +SAPLING_IDS = set() +for item_name in SAPLINGS: + SAPLING_IDS.add(ITEMS['minecraft:'+item_name]['protocol_id']) + ITEM_NAMES = {} for item_name, item in ITEMS.items(): ITEM_NAMES[ITEMS[item_name]['protocol_id']] = item_name.replace('minecraft:', '') diff --git a/jobs.py b/jobs.py index a2dc095..99f1eae 100644 --- a/jobs.py +++ b/jobs.py @@ -131,6 +131,7 @@ class GatherWoodStates: return None def init(self): + self.g.chopped_tree = False self.state = self.find_new_tree def find_new_tree(self): @@ -141,9 +142,14 @@ class GatherWoodStates: trees = w.find_trees(p, 100) print('Found trees:', trees) - while trees[0] in self.bad_trees: - trees.pop(0) - self.tree = trees[0] + try: + while trees[0] in self.bad_trees: + trees.pop(0) + self.tree = trees[0] + except IndexError: + print('No good tress left, aborting.') + self.state = self.cleanup + return self.openings = w.find_tree_openings(self.tree) self.state = self.choose_opening @@ -241,6 +247,7 @@ class GatherWoodStates: if self.wait_time > 0: self.wait_time -= utils.TICK else: + self.g.chopped_tree = True self.state = self.cleanup def cleanup(self): @@ -428,7 +435,8 @@ class SleepWithBedStates: if self.g.time >= 12542: print('Sleeping') self.g.game.place_block(self.area, BlockFace.TOP) - self.g.chat.send('zzz') + if not self.silent: + self.g.chat.send('zzz') self.state = self.sleep_bed def sleep_bed(self): @@ -465,6 +473,8 @@ class SleepWithBedStates: self.g = global_state self.state = self.idle + self.silent = False + self.area = None self.opening = None self.bad_areas = [] @@ -507,7 +517,7 @@ class CacheItemsStates: self.state = self.cleanup return - openings = w.find_bed_openings(self.area) + openings = w.find_cache_openings(self.area) for o in openings: navpath = w.path_to_place(p, o) @@ -612,6 +622,115 @@ class CacheItemsStates: self.state() +class PlantTreeStates: + def idle(self): + return None + + # TODO: maybe add a "plant deficit" so we know when to plant or not + + def init(self): + if self.g.chopped_tree: + self.state = self.check_feet + else: + print('Aborting planting, did not plant') + self.state = self.cleanup + + def check_feet(self): + p = utils.pint(self.g.pos) + + # check for air at feet + if self.g.chunks.get_block_at(*p) in [0]: + self.state = self.select_sapling + else: + print('Aborting planting, feet not air') + self.state = self.cleanup + + def select_sapling(self): + p = utils.pint(self.g.pos) + + if self.g.game.select_random_item(items.SAPLING_IDS): + self.g.look_at = utils.padd(p, path.BLOCK_BELOW) + self.state = self.wait_select + self.wait_time = 1 + else: + print('Aborting planting, no saplings') + self.state = self.cleanup + + def wait_select(self): + # wait a bit to look down + if self.wait_time > 0: + self.wait_time -= utils.TICK + else: + self.state = self.place_sapling + + def place_sapling(self): + p = utils.pint(self.g.pos) + self.g.game.place_block(p, BlockFace.TOP) + print('Placed sapling') + self.state = self.wait_place + self.wait_time = 1 + + def wait_place(self): + # wait a bit for chunk data to update + if self.wait_time > 0: + self.wait_time -= utils.TICK + else: + self.state = self.find_open_spot + + def find_open_spot(self): + print('Finding an open spot to stand...') + w = self.g.world + p = utils.pint(self.g.pos) + + areas = w.find_cache_areas(p, 20) + print('Found areas:', areas) + + try: + while areas[0] in self.bad_areas: + areas.pop(0) + self.area = areas[0] + except IndexError: + print('No good areas left, aborting.') + self.bad_areas = [] + self.state = self.cleanup + return + + navpath = w.path_to_place(p, self.area) + + if not navpath: + print('Unable to get to open area', self.area) + self.bad_areas.append(self.area) + self.state = self.cleanup + return + + self.g.path = navpath + self.state = self.going_to_area + print('Going to area', self.area) + + def going_to_area(self): + if utils.pint(self.g.pos) == self.area: + self.state = self.cleanup + + def cleanup(self): + self.g.look_at = None + self.state = self.done + + def done(self): + # never gets ran, placeholder + return None + + def __init__(self, global_state): + self.g = global_state + self.state = self.idle + + self.wait_time = 0 + self.area = None + self.bad_areas = [] + + def run(self): + self.state() + + class JobStates: def idle(self): return None @@ -676,12 +795,47 @@ class JobStates: s1.run() + def farm_wood(self): + self.sleep_with_bed_states.silent = True + + s1 = self.gather_wood_states + s2 = self.plant_tree_states + s3 = self.sleep_with_bed_states + s4 = self.cache_items_states + + if s1.state == s1.idle: + s1.state = s1.init + s2.state = s2.init + s3.state = s3.init + s4.state = s4.init + elif s1.state == s1.done: + if s2.state != s2.done: + s2.run() + return + + if s3.state != s3.done: + s3.run() + return + + if s4.state != s4.done: + s4.run() + return + + s1.state = s1.init + s2.state = s2.init + s3.state = s3.init + s4.state = s4.init + return + + s1.run() + def stop(self): self.gather_wood_states = GatherWoodStates(self.g) self.gather_sand_states = GatherSandStates(self.g) self.sleep_with_bed_states = SleepWithBedStates(self.g) self.cache_items_states = CacheItemsStates(self.g) self.find_gapple_states = FindGappleStates(self.g) + self.plant_tree_states = PlantTreeStates(self.g) self.state = self.idle def __init__(self, global_state): @@ -694,6 +848,7 @@ class JobStates: self.sleep_with_bed_states = SleepWithBedStates(self.g) self.cache_items_states = CacheItemsStates(self.g) self.find_gapple_states = FindGappleStates(self.g) + self.plant_tree_states = PlantTreeStates(self.g) def tick(self): self.state() diff --git a/main.py b/main.py index ecb8bfb..af8ebd6 100644 --- a/main.py +++ b/main.py @@ -7,18 +7,19 @@ import json from flask import Flask app = Flask(__name__) -from bunch import Bunch +from munch import Munch from watchdog.observers import Observer from watchdog.events import PatternMatchingEventHandler import bot -global_state = Bunch() +global_state = Munch() g = global_state g.connection = False g.mcdata = False g.pos = False g.inv = {} +g.objects = {} g.window = None g.job = None diff --git a/monkey_patch.py b/monkey_patch.py index ed06cbb..e5a3f91 100644 --- a/monkey_patch.py +++ b/monkey_patch.py @@ -24,6 +24,8 @@ def get_packets(old_get_packets): mc_packets.add(packets.SpawnLivingEntityPacket) mc_packets.add(packets.EntityPositionPacket) mc_packets.add(packets.EntityPositionRotationPacket) + mc_packets.add(packets.DestroyEntitiesPacket) + mc_packets.add(packets.EntityVelocityPacket) return mc_packets return lambda x: wrapper(old_get_packets, x) diff --git a/protocol/packets.py b/protocol/packets.py index 986e5e1..694ddad 100644 --- a/protocol/packets.py +++ b/protocol/packets.py @@ -334,9 +334,9 @@ class SpawnLivingEntityPacket(Packet): {'yaw': Angle}, {'pitch': Angle}, {'head_pitch': Angle}, - {'x_velocity': Short}, - {'y_velocity': Short}, - {'z_velocity': Short}, + {'velocity_x': Short}, + {'velocity_y': Short}, + {'velocity_z': Short}, ] class EntityPositionPacket(Packet): @@ -370,3 +370,32 @@ class EntityPositionRotationPacket(Packet): {'pitch': Angle}, {'on_ground': Boolean}, ] + +class DestroyEntitiesPacket(Packet): + # Sent by the server when a list of entities is to be destroyed on the client + # https://wiki.vg/Protocol#Destroy_Entities + + id = 0x36 + packet_name = 'destroy entities' + fields = 'count', 'entity_ids' + + def read(self, file_object): + self.count = VarInt.read(file_object) + self.entity_ids = [] + for _ in range(self.count): + eid = VarInt.read(file_object) + self.entity_ids.append(eid) + +class EntityVelocityPacket(Packet): + # Sent to update entity's velocity + # https://wiki.vg/Protocol#Entity_Velocity + + id = 0x46 + packet_name = 'entity velocity' + + definition = [ + {'entity_id': VarInt}, + {'velocity_x': Short}, + {'velocity_y': Short}, + {'velocity_z': Short}, + ] diff --git a/requirements.txt b/requirements.txt index d9933bb..e886e3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ astar==0.92 -bunch==1.0.1 certifi==2020.6.20 cffi==1.14.2 chardet==3.0.4