Add state machine for caching items into chests

This commit is contained in:
Tanner Collin 2020-09-20 19:08:23 -06:00
parent b8cbd9a5f0
commit f8d44e7e38
12 changed files with 326 additions and 59 deletions

View File

@ -1,11 +1,12 @@
import minecraft_data
import json import json
import importlib
mcd = minecraft_data('1.16.2') import data
importlib.reload(data)
MCD_BLOCKS = {} MCD_BLOCKS = {}
for data in mcd.blocks.values(): for d in data.mcd.blocks.values():
MCD_BLOCKS[data['name']] = data MCD_BLOCKS[d['name']] = d
with open('mcdata/blocks.json') as f: with open('mcdata/blocks.json') as f:
JSON_BLOCKS = json.load(f) JSON_BLOCKS = json.load(f)
@ -18,9 +19,11 @@ for name, data in JSON_BLOCKS.items():
AIR = 0 AIR = 0
SAND = 66 SAND = 66
SINGLE_SNOW = 3921 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 = [ AVOID = [

9
bot.py
View File

@ -10,6 +10,7 @@ from math import floor, ceil
USERNAME = os.environ['USERNAME'] USERNAME = os.environ['USERNAME']
PASSWORD = os.environ['PASSWORD'] PASSWORD = os.environ['PASSWORD']
SERVER = os.environ['SERVER'] SERVER = os.environ['SERVER']
PORT = int(os.environ.get('PORT', 25565))
import monkey_patch # must be before any possible pyCraft imports import monkey_patch # must be before any possible pyCraft imports
@ -154,7 +155,9 @@ def init(global_state):
g.break_time = 0 g.break_time = 0
g.dumping = None g.dumping = None
g.dump_lock = False g.item_lock = False
g.window = None
g.job = jobs.JobStates(g) g.job = jobs.JobStates(g)
@ -172,7 +175,7 @@ def bot(global_state):
print(e) print(e)
sys.exit() sys.exit()
print("Logged in as %s..." % auth_token.username) 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) g.chunks = ChunksManager(g.mcdata)
@ -198,8 +201,6 @@ def bot(global_state):
init(g) init(g)
print('Initialized.') print('Initialized.')
print(blocks.mcd.windows)
while g.running: while g.running:
tick(g) tick(g)

26
data.py Normal file
View File

@ -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,
),
}

68
game.py
View File

@ -3,13 +3,14 @@ import time
import importlib import importlib
from math import hypot from math import hypot
from itertools import count from itertools import count
from bunch import Bunch
from panda3d.core import LPoint3f from panda3d.core import LPoint3f
from minecraft.networking.packets import Packet, clientbound, serverbound from minecraft.networking.packets import Packet, clientbound, serverbound
from minecraft.networking.types import BlockFace 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 import utils
importlib.reload(utils) importlib.reload(utils)
@ -19,6 +20,8 @@ import blocks
importlib.reload(blocks) importlib.reload(blocks)
import items import items
importlib.reload(items) importlib.reload(items)
import data
importlib.reload(data)
class MCWorld: class MCWorld:
def __init__(self, global_state): def __init__(self, global_state):
@ -122,6 +125,9 @@ class MCWorld:
areas.sort(key=lambda x: utils.phyp(center, x)) areas.sort(key=lambda x: utils.phyp(center, x))
return areas return areas
def find_cache_areas(self, center, distance):
return self.find_bed_areas(center, distance)
def sand_adjacent_safe(self, sand): def sand_adjacent_safe(self, sand):
for direction in path.CHECK_DIRECTIONS: for direction in path.CHECK_DIRECTIONS:
if self.block_at(*utils.padd(sand, direction)) in blocks.AVOID_IDS: if self.block_at(*utils.padd(sand, direction)) in blocks.AVOID_IDS:
@ -163,6 +169,9 @@ class MCWorld:
result.append(utils.padd(area, direction)) result.append(utils.padd(area, direction))
return result return result
def find_cache_openings(self, area):
return self.find_bed_openings(area)
class Game: class Game:
def __init__(self, global_state): def __init__(self, global_state):
@ -176,6 +185,7 @@ class Game:
register(self.handle_set_slot, SetSlotPacket) register(self.handle_set_slot, SetSlotPacket)
register(self.handle_break_animation, BlockBreakAnimationPacket) register(self.handle_break_animation, BlockBreakAnimationPacket)
register(self.handle_break_ack, AcknowledgePlayerDiggingPacket) register(self.handle_break_ack, AcknowledgePlayerDiggingPacket)
register(self.handle_window, OpenWindowPacket)
self.g.chat.set_handler(self.handle_chat) self.g.chat.set_handler(self.handle_chat)
@ -314,7 +324,22 @@ class Game:
reply = 'not found' reply = 'not found'
if command == 'open': 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: if reply:
print(reply) print(reply)
@ -324,12 +349,15 @@ class Game:
self.g.time = packet.time_of_day % 24000 self.g.time = packet.time_of_day % 24000
def handle_set_slot(self, packet): def handle_set_slot(self, packet):
g = self.g
print(packet) print(packet)
if packet.window_id == 0: 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: if not packet.slot_data.present:
self.g.dump_lock = False g.item_lock = False
def break_block(self, location): def break_block(self, location):
bid = self.g.chunks.get_block_at(*location) bid = self.g.chunks.get_block_at(*location)
@ -416,6 +444,32 @@ class Game:
packet.face = 1 packet.face = 1
self.g.connection.write_packet(packet) 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): def tick(self):
if self.g.breaking: if self.g.breaking:
self.animate() self.animate()
@ -423,10 +477,10 @@ class Game:
if time.time() >= self.g.break_time - 2*utils.TICK: if time.time() >= self.g.break_time - 2*utils.TICK:
self.break_finish() 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]): if self.select_item([self.g.dumping]):
self.drop_stack() self.drop_stack()
self.g.dump_lock = True self.g.item_lock = True
else: else:
self.g.dumping = None self.g.dumping = None

View File

@ -29,3 +29,7 @@ for item_name in BEDS:
ITEM_NAMES = {} ITEM_NAMES = {}
for item_name, item in ITEMS.items(): for item_name, item in ITEMS.items():
ITEM_NAMES[ITEMS[item_name]['protocol_id']] = item_name.replace('minecraft:', '') 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

179
jobs.py
View File

@ -310,7 +310,7 @@ class SleepWithBedStates:
self.state = self.select_bed self.state = self.select_bed
def select_bed(self): 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.g.look_at = utils.padd(self.area, path.BLOCK_BELOW)
self.state = self.place_bed self.state = self.place_bed
else: else:
@ -372,32 +372,148 @@ class SleepWithBedStates:
self.state() self.state()
class JobStates: class CacheItemsStates:
def idle(self): def idle(self):
return None return None
def sleep_with_bed(self): def init(self):
s = self.sleep_with_bed_states #if len(self.g.inv) >= 27:
if s.state == s.idle: if len(self.g.inv) >= 2:
s.state = s.init self.state = self.find_chest_spot
elif s.state == s.done: else:
s.state = s.init print('Aborting caching, not full')
# check time, etc self.state = self.cleanup
if self.prev_state: def find_cache_spot(self):
print('Reverting to prev state') print('Finding a chest spot...')
self.state = self.prev_state w = self.g.world
return 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): def gather_sand(self):
s = self.gather_sand_states s = self.gather_sand_states
if s.state == s.idle: if s.state == s.idle:
s.state = s.init s.state = s.init
elif s.state == s.done: elif s.state == s.done:
s.state = s.init #s.state = s.init
# check time, etc
self.prev_state = self.gather_sand self.prev_state = self.gather_sand
self.state = self.sleep_with_bed self.state = self.sleep_with_bed
@ -406,23 +522,35 @@ class JobStates:
s.run() s.run()
def lumberjack(self): def lumberjack(self):
s = self.lumberjack_states s1 = self.lumberjack_states
if s.state == s.idle: s2 = self.sleep_with_bed_states
s.state = s.init s3 = self.cache_items_states
elif s.state == s.done:
s.state = s.init
# check time, etc
self.prev_state = self.lumberjack if s1.state == s1.idle:
self.state = self.sleep_with_bed 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
if s3.state != s3.done:
s3.run()
return
s1.state = s1.init
s2.state = s2.init
s3.state = s3.init
return return
s.run() s1.run()
def stop(self): def stop(self):
self.lumberjack_states = LumberjackStates(self.g) self.lumberjack_states = LumberjackStates(self.g)
self.gather_sand_states = GatherSandStates(self.g) self.gather_sand_states = GatherSandStates(self.g)
self.sleep_with_bed_states = SleepWithBedStates(self.g) self.sleep_with_bed_states = SleepWithBedStates(self.g)
self.cache_items_states = CacheItemsStates(self.g)
self.state = self.idle self.state = self.idle
def __init__(self, global_state): def __init__(self, global_state):
@ -433,6 +561,7 @@ class JobStates:
self.lumberjack_states = LumberjackStates(self.g) self.lumberjack_states = LumberjackStates(self.g)
self.gather_sand_states = GatherSandStates(self.g) self.gather_sand_states = GatherSandStates(self.g)
self.sleep_with_bed_states = SleepWithBedStates(self.g) self.sleep_with_bed_states = SleepWithBedStates(self.g)
self.cache_items_states = CacheItemsStates(self.g)
def tick(self): def tick(self):
self.state() self.state()

12
main.py
View File

@ -2,6 +2,7 @@ import importlib
import threading import threading
import time import time
import traceback import traceback
import json
from flask import Flask from flask import Flask
app = Flask(__name__) app = Flask(__name__)
@ -21,9 +22,14 @@ g.inv = {}
@app.route('/') @app.route('/')
def hello_world(): def hello_world():
#print(chunks.chunks) data = json.dumps(g, default=lambda o: str(o), indent=4)
return str(g.chunks.get_block_at(84,62,54))
#return 'ok' response = app.response_class(
response=data,
status=200,
mimetype='application/json'
)
return response
reload_timeout = time.time() reload_timeout = time.time()

View File

@ -15,6 +15,9 @@ def get_packets(old_get_packets):
mc_packets.add(packets.PlayerDiggingPacket) mc_packets.add(packets.PlayerDiggingPacket)
mc_packets.add(packets.PickItemPacket) mc_packets.add(packets.PickItemPacket)
mc_packets.add(packets.HeldItemChangePacket) mc_packets.add(packets.HeldItemChangePacket)
mc_packets.add(packets.OpenWindowPacket)
mc_packets.add(packets.CloseWindowPacket)
mc_packets.add(packets.ClickWindowPacket)
return mc_packets return mc_packets
return lambda x: wrapper(old_get_packets, x) return lambda x: wrapper(old_get_packets, x)

View File

@ -112,6 +112,8 @@ class ChatManager:
return ''.join([self.translate_chat(x) for x in data['extra']]) return ''.join([self.translate_chat(x) for x in data['extra']])
elif 'text' in data: elif 'text' in data:
return data['text'] return data['text']
elif 'with' in data:
return '<{}> {}'.format(*[self.translate_chat(x) for x in data['with']])
elif 'translate' in data: elif 'translate' in data:
return data['translate'] return data['translate']
else: else:

View File

@ -4,7 +4,7 @@ from minecraft.networking.packets import Packet, PacketBuffer
from minecraft.networking.types import ( from minecraft.networking.types import (
VarInt, Integer, UnsignedByte, Position, Vector, MutableRecord, VarInt, Integer, UnsignedByte, Position, Vector, MutableRecord,
attribute_alias, multi_attribute_alias, Long, Boolean, VarLong, attribute_alias, multi_attribute_alias, Long, Boolean, VarLong,
Short, UnsignedLong, Byte, BlockFace, Short, UnsignedLong, Byte, BlockFace, String
) )
from protocol.types import Nbt, Slot from protocol.types import Nbt, Slot
@ -217,3 +217,45 @@ class HeldItemChangePacket(Packet):
definition = [ definition = [
{'slot': Short}, {'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},
]

View File

@ -120,14 +120,10 @@ class Slot(Type):
item_count = Byte.read(file_object) if present else None item_count = Byte.read(file_object) if present else None
nbt = TrailingByteArray.read(file_object) if present else None nbt = TrailingByteArray.read(file_object) if present else None
return Slot(present, item_id, item_count, nbt) 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 @staticmethod
def send(value, socket): def send(value, socket):
# TODO Boolean.send(value.present, socket)
pass VarInt.send(value.item_id, socket)
Byte.send(value.item_count, socket)
TrailingByteArray.send(value.nbt, socket)

View File

@ -3,6 +3,7 @@ from math import floor, ceil, sqrt, hypot
import blocks import blocks
importlib.reload(blocks) importlib.reload(blocks)
import data
TICK = 0.05 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={}): def break_time(block_id, held_item=0, in_water=False, on_ground=True, enchantments=[], effects={}):
# from PrismarineJS/prismarine-block # 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'] can_harvest = 'harvestTools' not in block_data or str(held_item) in block_data['harvestTools']
material = data.get('material', 'n/a') material = block_data.get('material', 'n/a')
tool_multipliers = blocks.mcd.materials.get(material, []) tool_multipliers = data.mcd.materials.get(material, [])
is_best_tool = held_item in tool_multipliers is_best_tool = held_item in tool_multipliers
time = data['hardness'] time = block_data['hardness']
if can_harvest: if can_harvest:
time *= 1.5 time *= 1.5