From f8d44e7e3818c0396711a55e7639433a0321e2ab Mon Sep 17 00:00:00 2001 From: Tanner Collin Date: Sun, 20 Sep 2020 19:08:23 -0600 Subject: [PATCH] Add state machine for caching items into chests --- blocks.py | 15 ++-- bot.py | 9 ++- data.py | 26 +++++++ game.py | 68 ++++++++++++++-- items.py | 4 + jobs.py | 179 +++++++++++++++++++++++++++++++++++++------ main.py | 12 ++- monkey_patch.py | 3 + protocol/managers.py | 2 + protocol/packets.py | 44 ++++++++++- protocol/types.py | 12 +-- utils.py | 11 +-- 12 files changed, 326 insertions(+), 59 deletions(-) create mode 100644 data.py diff --git a/blocks.py b/blocks.py index 2121d61..00f98d8 100644 --- a/blocks.py +++ b/blocks.py @@ -1,11 +1,12 @@ -import minecraft_data import json +import importlib -mcd = minecraft_data('1.16.2') +import data +importlib.reload(data) MCD_BLOCKS = {} -for data in mcd.blocks.values(): - MCD_BLOCKS[data['name']] = data +for d in data.mcd.blocks.values(): + MCD_BLOCKS[d['name']] = d with open('mcdata/blocks.json') as f: JSON_BLOCKS = json.load(f) @@ -18,9 +19,11 @@ for name, data in JSON_BLOCKS.items(): AIR = 0 SAND = 66 SINGLE_SNOW = 3921 -SOUL_TORCH = 4008 +#SOUL_TORCH = 4008 +SOUL_TORCH = 1435 -TEST_BLOCK = (616, 78, 496) +#TEST_BLOCK = (616, 78, 496) +TEST_BLOCK = (-100, 64, -167) AVOID = [ diff --git a/bot.py b/bot.py index 0d28f00..25ebf88 100644 --- a/bot.py +++ b/bot.py @@ -10,6 +10,7 @@ from math import floor, ceil USERNAME = os.environ['USERNAME'] PASSWORD = os.environ['PASSWORD'] SERVER = os.environ['SERVER'] +PORT = int(os.environ.get('PORT', 25565)) import monkey_patch # must be before any possible pyCraft imports @@ -154,7 +155,9 @@ def init(global_state): g.break_time = 0 g.dumping = None - g.dump_lock = False + g.item_lock = False + + g.window = None g.job = jobs.JobStates(g) @@ -172,7 +175,7 @@ def bot(global_state): print(e) sys.exit() print("Logged in as %s..." % auth_token.username) - g.connection = Connection(SERVER, 25565, auth_token=auth_token) + g.connection = Connection(SERVER, PORT, auth_token=auth_token) g.chunks = ChunksManager(g.mcdata) @@ -198,8 +201,6 @@ def bot(global_state): init(g) print('Initialized.') - print(blocks.mcd.windows) - while g.running: tick(g) diff --git a/data.py b/data.py new file mode 100644 index 0000000..4aff005 --- /dev/null +++ b/data.py @@ -0,0 +1,26 @@ +import json +from bunch import Bunch + +import minecraft_data + +mcd = minecraft_data('1.16.2') + +with open('mcdata/registries.json') as f: + DATA = json.load(f) + +SINGLE_CHEST = 2 +DOUBLE_CHEST = 5 + +WINDOWS = { + SINGLE_CHEST: Bunch( + container=list(range(0, 27)), + inventory=list(range(27, 63)), + slot_diff=18, + ), + DOUBLE_CHEST: Bunch( + container=list(range(0, 54)), + inventory=list(range(54, 90)), + slot_diff=45, + ), +} + diff --git a/game.py b/game.py index e837354..76157f6 100644 --- a/game.py +++ b/game.py @@ -3,13 +3,14 @@ import time import importlib from math import hypot from itertools import count +from bunch import Bunch from panda3d.core import LPoint3f from minecraft.networking.packets import Packet, clientbound, serverbound from minecraft.networking.types import BlockFace -from protocol.packets import TimeUpdatePacket, SetSlotPacket, PlayerDiggingPacket, BlockBreakAnimationPacket, AcknowledgePlayerDiggingPacket, HeldItemChangePacket, PickItemPacket +from protocol.packets import TimeUpdatePacket, SetSlotPacket, PlayerDiggingPacket, BlockBreakAnimationPacket, AcknowledgePlayerDiggingPacket, HeldItemChangePacket, PickItemPacket, OpenWindowPacket, ClickWindowPacket, CloseWindowPacket import utils importlib.reload(utils) @@ -19,6 +20,8 @@ import blocks importlib.reload(blocks) import items importlib.reload(items) +import data +importlib.reload(data) class MCWorld: def __init__(self, global_state): @@ -122,6 +125,9 @@ class MCWorld: areas.sort(key=lambda x: utils.phyp(center, x)) return areas + def find_cache_areas(self, center, distance): + return self.find_bed_areas(center, distance) + def sand_adjacent_safe(self, sand): for direction in path.CHECK_DIRECTIONS: if self.block_at(*utils.padd(sand, direction)) in blocks.AVOID_IDS: @@ -163,6 +169,9 @@ class MCWorld: result.append(utils.padd(area, direction)) return result + def find_cache_openings(self, area): + return self.find_bed_openings(area) + class Game: def __init__(self, global_state): @@ -176,6 +185,7 @@ class Game: register(self.handle_set_slot, SetSlotPacket) register(self.handle_break_animation, BlockBreakAnimationPacket) register(self.handle_break_ack, AcknowledgePlayerDiggingPacket) + register(self.handle_window, OpenWindowPacket) self.g.chat.set_handler(self.handle_chat) @@ -314,7 +324,22 @@ class Game: reply = 'not found' if command == 'open': - self.place_block(blocks.TEST_BLOCK, BlockFace.TOP) + self.open_container(blocks.TEST_BLOCK) + + if command == 'close': + if self.g.window: + self.close_window() + else: + reply = 'nothing open' + + if command == 'click' and data: + if self.g.window: + window_id, slot, button, mode = [int(x) for x in data.split(' ')] + item = self.g.window.contents[slot] + print(item) + self.click_window(window_id, slot, button, mode, item) + else: + reply = 'nothing open' if reply: print(reply) @@ -324,12 +349,15 @@ class Game: self.g.time = packet.time_of_day % 24000 def handle_set_slot(self, packet): + g = self.g print(packet) if packet.window_id == 0: - self.g.inv[packet.slot] = packet.slot_data + g.inv[packet.slot] = packet.slot_data + elif g.window: + g.window.contents[packet.slot] = packet.slot_data - if not packet.slot_data.present: - self.g.dump_lock = False + if not packet.slot_data.present: + g.item_lock = False def break_block(self, location): bid = self.g.chunks.get_block_at(*location) @@ -416,6 +444,32 @@ class Game: packet.face = 1 self.g.connection.write_packet(packet) + def open_container(self, location): + bid = self.g.chunks.get_block_at(*location) + # TODO: check if block is a chest?? + self.place_block(location, BlockFace.TOP) + + def handle_window(self, packet): + print(packet) + self.g.window = Bunch(data=packet, contents=dict()) + + def click_window(self, window_id, slot, button, mode, item): + packet = ClickWindowPacket() + packet.window_id = window_id + packet.slot = slot + packet.button = button + packet.action_number = 1 + packet.mode = mode + packet.clicked_item = item + self.g.connection.write_packet(packet) + + def close_window(self): + packet = CloseWindowPacket() + packet.window_id = self.g.window.data.window_id + self.g.connection.write_packet(packet) + self.g.window = None + + def tick(self): if self.g.breaking: self.animate() @@ -423,10 +477,10 @@ class Game: if time.time() >= self.g.break_time - 2*utils.TICK: self.break_finish() - if self.g.dumping and not self.g.dump_lock: + if self.g.dumping and not self.g.item_lock: if self.select_item([self.g.dumping]): self.drop_stack() - self.g.dump_lock = True + self.g.item_lock = True else: self.g.dumping = None diff --git a/items.py b/items.py index 4aac846..c02a818 100644 --- a/items.py +++ b/items.py @@ -29,3 +29,7 @@ for item_name in BEDS: ITEM_NAMES = {} for item_name, item in ITEMS.items(): ITEM_NAMES[ITEMS[item_name]['protocol_id']] = item_name.replace('minecraft:', '') + +CHEST_ID = set([ITEMS['minecraft:chest']['protocol_id']]) + +USEFUL_ITEMS = BED_IDS | CHEST_ID diff --git a/jobs.py b/jobs.py index fac564c..b81e89e 100644 --- a/jobs.py +++ b/jobs.py @@ -310,7 +310,7 @@ class SleepWithBedStates: self.state = self.select_bed def select_bed(self): - if self.game.select_item(items.BED_IDS): + if self.g.game.select_item(items.BED_IDS): self.g.look_at = utils.padd(self.area, path.BLOCK_BELOW) self.state = self.place_bed else: @@ -372,32 +372,148 @@ class SleepWithBedStates: self.state() -class JobStates: +class CacheItemsStates: def idle(self): return None - def sleep_with_bed(self): - s = self.sleep_with_bed_states - if s.state == s.idle: - s.state = s.init - elif s.state == s.done: - s.state = s.init - # check time, etc + def init(self): + #if len(self.g.inv) >= 27: + if len(self.g.inv) >= 2: + self.state = self.find_chest_spot + else: + print('Aborting caching, not full') + self.state = self.cleanup - if self.prev_state: - print('Reverting to prev state') - self.state = self.prev_state - return + def find_cache_spot(self): + print('Finding a chest spot...') + w = self.g.world + p = utils.pint(self.g.pos) - s.run() + areas = w.find_cache_areas(p, 100) + print('Found areas:', areas) + + if len(areas): + while areas[0] in self.bad_areas: + areas.pop(0) + self.area = areas[0] + elif self.last_area: + self.area = self.last_area + else: + print('Unable to find area, and no last area') + self.state = self.cleanup + return + + openings = w.find_bed_openings(self.area) + + for o in openings: + navpath = w.path_to_place(p, o) + self.opening = o + if navpath: break + else: # for + print('Unable to get to cache area', self.area) + self.bad_areas.append(self.area) + self.state = self.cleanup + return + + self.g.path = navpath + self.state = self.going_to_area + self.last_area = self.area + + def going_to_area(self): + if utils.pint(self.g.pos) == self.opening: + self.g.look_at = self.area + self.state = self.select_chest + + def select_chest(self): + if self.g.game.select_item(items.CHEST_ID): + self.g.look_at = utils.padd(self.area, path.BLOCK_BELOW) + self.state = self.place_chest + else: + self.g.chat.send('I need a chest') + self.state = self.cleanup + + def place_chest(self): + self.g.game.place_block(self.area, BlockFace.TOP) + self.state = self.open_chest + + def open_chest(self): + print('Opening chest') + self.g.game.open_container(self.area) + self.wait_time = 1 + self.state = self.wait + + def wait(self): + # wait for server to send us chest contents + if self.wait_time > 0: + self.wait_time -= utils.TICK + else: + self.state = self.move_items + + def move_items(self): + if self.g.item_lock: return + + w = self.g.window + w_data = data.WINDOWS[w.window_type] + w_inventory_slots = w_data.inventory + + for slot_num in w_inventory_slots: + if slot_num not in w.contents: + continue + + slot = w.contents[slot_num] + + if slot.item_id in items.USEFUL_ITEMS: + continue + + print('moving', slot) + + inv_slot_num = slot_num - w_data.slot_diff + self.g.inv.pop(inv_slot_num, None) + + self.g.item_lock = True + self.g.game.click_window(w.window_id, slot_num, 0, 1, slot) + return + + print('nothing left to move') + self.state = close_chest + + def close_chest(self): + print('closing chest') + self.g.game.close_window() + 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.area = None + self.opening = None + self.bad_areas = [] + self.last_area = None + self.wait_time = 0 + + def run(self): + self.state() + + +class JobStates: + def idle(self): + return None def gather_sand(self): s = self.gather_sand_states if s.state == s.idle: s.state = s.init elif s.state == s.done: - s.state = s.init - # check time, etc + #s.state = s.init self.prev_state = self.gather_sand self.state = self.sleep_with_bed @@ -406,23 +522,35 @@ class JobStates: s.run() def lumberjack(self): - s = self.lumberjack_states - if s.state == s.idle: - s.state = s.init - elif s.state == s.done: - s.state = s.init - # check time, etc + s1 = self.lumberjack_states + s2 = self.sleep_with_bed_states + s3 = self.cache_items_states + + if s1.state == s1.idle: + s1.state = s1.init + s2.state = s2.init + s3.state = s3.init + elif s1.state == s1.done: + if s2.state != s2.done: + s2.run() + return - self.prev_state = self.lumberjack - self.state = self.sleep_with_bed + if s3.state != s3.done: + s3.run() + return + + s1.state = s1.init + s2.state = s2.init + s3.state = s3.init return - s.run() + s1.run() def stop(self): self.lumberjack_states = LumberjackStates(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.state = self.idle def __init__(self, global_state): @@ -433,6 +561,7 @@ class JobStates: self.lumberjack_states = LumberjackStates(self.g) self.gather_sand_states = GatherSandStates(self.g) self.sleep_with_bed_states = SleepWithBedStates(self.g) + self.cache_items_states = CacheItemsStates(self.g) def tick(self): self.state() diff --git a/main.py b/main.py index d6982e8..07a7df5 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import importlib import threading import time import traceback +import json from flask import Flask app = Flask(__name__) @@ -21,9 +22,14 @@ g.inv = {} @app.route('/') def hello_world(): - #print(chunks.chunks) - return str(g.chunks.get_block_at(84,62,54)) - #return 'ok' + data = json.dumps(g, default=lambda o: str(o), indent=4) + + response = app.response_class( + response=data, + status=200, + mimetype='application/json' + ) + return response reload_timeout = time.time() diff --git a/monkey_patch.py b/monkey_patch.py index 79f0521..99b1378 100644 --- a/monkey_patch.py +++ b/monkey_patch.py @@ -15,6 +15,9 @@ def get_packets(old_get_packets): mc_packets.add(packets.PlayerDiggingPacket) mc_packets.add(packets.PickItemPacket) mc_packets.add(packets.HeldItemChangePacket) + mc_packets.add(packets.OpenWindowPacket) + mc_packets.add(packets.CloseWindowPacket) + mc_packets.add(packets.ClickWindowPacket) return mc_packets return lambda x: wrapper(old_get_packets, x) diff --git a/protocol/managers.py b/protocol/managers.py index 33db934..69ea208 100644 --- a/protocol/managers.py +++ b/protocol/managers.py @@ -112,6 +112,8 @@ class ChatManager: return ''.join([self.translate_chat(x) for x in data['extra']]) elif 'text' in data: return data['text'] + elif 'with' in data: + return '<{}> {}'.format(*[self.translate_chat(x) for x in data['with']]) elif 'translate' in data: return data['translate'] else: diff --git a/protocol/packets.py b/protocol/packets.py index 073f259..5f069d2 100644 --- a/protocol/packets.py +++ b/protocol/packets.py @@ -4,7 +4,7 @@ from minecraft.networking.packets import Packet, PacketBuffer from minecraft.networking.types import ( VarInt, Integer, UnsignedByte, Position, Vector, MutableRecord, attribute_alias, multi_attribute_alias, Long, Boolean, VarLong, - Short, UnsignedLong, Byte, BlockFace, + Short, UnsignedLong, Byte, BlockFace, String ) from protocol.types import Nbt, Slot @@ -217,3 +217,45 @@ class HeldItemChangePacket(Packet): definition = [ {'slot': Short}, ] + + +class OpenWindowPacket(Packet): + # Sent to the client when it should open an inventory, such as a chest, workbench, or furnace + # https://wiki.vg/Protocol#Open_Window + + id = 0x2D + packet_name = 'open window' + + definition = [ + {'window_id': VarInt}, + {'window_type': VarInt}, + {'window_title': String}, + ] + +class CloseWindowPacket(Packet): + # Sent by the client when closing a window + # https://wiki.vg/Protocol#Close_Window_.28serverbound.29 + + id = 0x0A + packet_name = 'close window' + + definition = [ + {'window_id': UnsignedByte}, + ] + + +class ClickWindowPacket(Packet): + # Sent by the player when it clicks on a slot in a window + # https://wiki.vg/Protocol#Click_Window + + id = 0x09 + packet_name = 'click window' + + definition = [ + {'window_id': UnsignedByte}, + {'slot': Short}, + {'button': Byte}, + {'action_number': Short}, + {'mode': VarInt}, + {'clicked_item': Slot}, + ] diff --git a/protocol/types.py b/protocol/types.py index bc02790..fd2de9e 100644 --- a/protocol/types.py +++ b/protocol/types.py @@ -120,14 +120,10 @@ class Slot(Type): item_count = Byte.read(file_object) if present else None nbt = TrailingByteArray.read(file_object) if present else None return Slot(present, item_id, item_count, nbt) - #a = {} - #a['present'] = Boolean.read(file_object) - #a['item_id'] = VarInt.read(file_object) if a['present'] else None - #a['item_count'] = Byte.read(file_object) if a['present'] else None - #a['nbt'] = TrailingByteArray.read(file_object) if a['present'] else None - #return a @staticmethod def send(value, socket): - # TODO - pass + Boolean.send(value.present, socket) + VarInt.send(value.item_id, socket) + Byte.send(value.item_count, socket) + TrailingByteArray.send(value.nbt, socket) diff --git a/utils.py b/utils.py index 0f4d547..166de3f 100644 --- a/utils.py +++ b/utils.py @@ -3,6 +3,7 @@ from math import floor, ceil, sqrt, hypot import blocks importlib.reload(blocks) +import data TICK = 0.05 @@ -65,13 +66,13 @@ def diffrange(n): def break_time(block_id, held_item=0, in_water=False, on_ground=True, enchantments=[], effects={}): # from PrismarineJS/prismarine-block - data = blocks.get(block_id) + block_data = blocks.get(block_id) - can_harvest = 'harvestTools' not in data or str(held_item) in data['harvestTools'] - material = data.get('material', 'n/a') - tool_multipliers = blocks.mcd.materials.get(material, []) + can_harvest = 'harvestTools' not in block_data or str(held_item) in block_data['harvestTools'] + material = block_data.get('material', 'n/a') + tool_multipliers = data.mcd.materials.get(material, []) is_best_tool = held_item in tool_multipliers - time = data['hardness'] + time = block_data['hardness'] if can_harvest: time *= 1.5