Detect monsters and flee to safety

This commit is contained in:
Tanner Collin 2020-12-04 02:49:22 +00:00
parent 5a7a2f0113
commit b8952db742
7 changed files with 475 additions and 196 deletions

View File

@ -16,12 +16,13 @@ for name, data in JSON_BLOCKS.items():
for state in data['states']: for state in data['states']:
BLOCKS[state['id']] = name.replace('minecraft:', '') BLOCKS[state['id']] = name.replace('minecraft:', '')
BREAK_DISTANCE = 5 BREAK_DISTANCE = 6
AIR = 0 AIR = 0
SAND = 66 SAND = 66
SINGLE_SNOW = 3921 SINGLE_SNOW = 3921
SOUL_TORCH = 4008 SOUL_TORCH = 4008
EMERALD_BLOCK = 5407
TEST_BLOCK = (616, 78, 496) TEST_BLOCK = (616, 78, 496)
@ -246,6 +247,7 @@ TRAPPED_CHESTS = [
INDEXED = [ INDEXED = [
'chest', 'chest',
'trapped_chest', 'trapped_chest',
'emerald_block',
] ]

2
bot.py
View File

@ -203,6 +203,8 @@ def init(global_state):
g.job = jobs.JobStates(g) g.job = jobs.JobStates(g)
g.chopped_tree = False g.chopped_tree = False
g.queue_afk = False
def bot(global_state): def bot(global_state):
g = global_state g = global_state

162
game.py
View File

@ -2,6 +2,7 @@ import re
import time import time
import importlib import importlib
import random import random
import functools
from math import hypot from math import hypot
from itertools import count from itertools import count
from munch import Munch from munch import Munch
@ -19,6 +20,7 @@ from protocol.packets import (
ClickWindowPacket, CloseWindowPacket, ServerWindowConfirmationPacket, ClickWindowPacket, CloseWindowPacket, ServerWindowConfirmationPacket,
ClientWindowConfirmationPacket, EntityMetadataPacket, ClientWindowConfirmationPacket, EntityMetadataPacket,
SpawnLivingEntityPacket, EntityPositionRotationPacket, DestroyEntitiesPacket, SpawnLivingEntityPacket, EntityPositionRotationPacket, DestroyEntitiesPacket,
EntityActionPacket,
) )
from protocol.types import Slot from protocol.types import Slot
@ -33,6 +35,8 @@ import items
importlib.reload(items) importlib.reload(items)
import data import data
importlib.reload(data) importlib.reload(data)
import mobs
importlib.reload(mobs)
class MCWorld: class MCWorld:
def __init__(self, global_state): def __init__(self, global_state):
@ -41,6 +45,13 @@ class MCWorld:
def block_at(self, x, y, z): def block_at(self, x, y, z):
return self.g.chunks.get_block_at(x, y, z) return self.g.chunks.get_block_at(x, y, z)
def check_air_column(self, pos, distance):
for i in range(distance):
check = utils.padd(pos, (0, i, 0))
if self.block_at(*check) not in blocks.NON_SOLID_IDS:
return False
return True
def find_blocks_3d(self, center, block_ids, distance=0, y_limit=0): def find_blocks_3d(self, center, block_ids, distance=0, y_limit=0):
for offset in utils.search_3d(distance, y_limit): for offset in utils.search_3d(distance, y_limit):
check = utils.padd(center, offset) check = utils.padd(center, offset)
@ -189,10 +200,9 @@ class MCWorld:
return safe_sand return safe_sand
def check_sand_slice(self, center): def check_sand_slice(self, center):
# checks if a 5x5x1 slice has diggable sand in it # checks if a 5x5x1 slice has sand in it
for i in range(9): for i in range(9):
s = utils.padd(center, utils.spiral(i)) s = utils.padd(center, utils.spiral(i))
if self.block_at(*s) != blocks.SAND: if self.block_at(*s) != blocks.SAND:
continue continue
# make sure it has solid below # make sure it has solid below
@ -206,18 +216,17 @@ class MCWorld:
continue continue
if not self.sand_adjacent_safe(s): if not self.sand_adjacent_safe(s):
continue continue
return True return True
return False return False
def find_sand_slice(self, center, distance, bad_slices=[]): def find_sand_slice(self, center, distance, y_limit=0, bad_slices=[], prev_layer=0):
# returns the centre coord of the next 5x5x1 slice that still has # returns the centre coord of the next 5x5x1 slice that still has
# diggable sand in it. lower slices are only valid if there's an # diggable sand in it. lower slices are only valid if there's an
# adjacent slice farther at the same level. this should ensure an # adjacent slice farther at the same level. this should ensure an
# upside down pyramid gets excavated so the edges are still climbable # upside down pyramid gets excavated so the edges are still climbable
for v in count(): for v in count(prev_layer):
peak = utils.padd(center, (0, 20-v, 0)) peak = utils.padd(center, (0, 10-v, 0))
slices = [] slices = []
layer = 0 layer = 0
@ -228,14 +237,16 @@ class MCWorld:
check = utils.padd(peak, offset) check = utils.padd(peak, offset)
check = utils.padd(check, (0, layer, 0)) check = utils.padd(check, (0, layer, 0))
if utils.phyp(center, check) >= distance: if y_limit and check[1] - center[1] > y_limit:
break
if utils.phyp_king(center, check) > distance:
break break
if self.check_sand_slice(check) and check not in bad_slices: if self.check_sand_slice(check) and check not in bad_slices:
slices.append(check) slices.append(check)
if len(slices): if len(slices):
return slices[-1] return v, slices[-1]
elif v > 40: elif v > 40:
return None, None return None, None
@ -262,6 +273,31 @@ class MCWorld:
for a in self.find_blocks_3d(center, blocks.LEAF_IDS, distance, 10): for a in self.find_blocks_3d(center, blocks.LEAF_IDS, distance, 10):
yield a yield a
def find_monsters(self, center, distance):
# finds monsters within distance
result = []
for eid, mob in self.g.mobs.items():
if mob.type not in mobs.EVIL_IDS:
continue
pos = utils.pint((mob.x, mob.y, mob.z))
if utils.phyp(center, pos) > distance:
continue
result.append(mob)
return result
def find_threats(self, center, distance):
# finds monsters on the surface within distance
monsters = self.find_monsters(center, distance)
result = []
for mob in monsters:
pos = utils.pint((mob.x, mob.y, mob.z))
# check distance number of blocks above, close enough?
if not self.check_air_column(pos, distance):
continue
result.append(mob)
return result
class Game: class Game:
def __init__(self, global_state): def __init__(self, global_state):
@ -365,7 +401,7 @@ class Game:
else: else:
return return
if text == 'zzz': if text.startswith('zzz'):
text = '!zzz' text = '!zzz'
if text.startswith('! '): if text.startswith('! '):
@ -382,6 +418,7 @@ class Game:
command = text command = text
data = None data = None
try:
if command == 'ping': if command == 'ping':
reply = 'pong' reply = 'pong'
@ -428,10 +465,12 @@ class Game:
if data == 'wood': if data == 'wood':
self.g.job.state = self.g.job.farm_wood self.g.job.state = self.g.job.farm_wood
reply = 'ok' reply = 'ok'
elif data == 'sand':
self.g.job.state = self.g.job.farm_sand
reply = 'ok'
if reply: if reply:
for i in self.g.inv.values(): for i in self.g.inv.values():
print(i.item_id)
if i.item_id in items.BED_IDS: if i.item_id in items.BED_IDS:
break break
else: else:
@ -508,9 +547,43 @@ class Game:
reply = 'ok' reply = 'ok'
if command == 'objects': if command == 'objects':
if data == 'clear':
self.g.objects = {}
reply = 'ok'
else:
for k, v in self.g.objects.items(): for k, v in self.g.objects.items():
if data and v.item_id != int(data): continue if data and v.item_id != int(data): continue
print(str(k) + ':', v) print(str(k) + ':', v, items.ITEM_NAMES[v.item_id])
if command == 'mobs':
if data == 'clear':
self.g.mobs = {}
reply = 'ok'
else:
all_mobs = sorted(list(self.g.mobs.items()), key=lambda x: x[1].type)
for k, v in all_mobs:
if data and v.type != int(data): continue
print(str(k) + ':', v, mobs.MOB_NAMES[v.type])
reply = str(len(all_mobs)) + ' mobs'
if command == 'monsters':
monsters = sorted(list(self.g.mobs.items()), key=lambda x: x[1].type)
count = 0
for k, v in monsters:
if v.type not in mobs.EVIL_IDS: continue
if data and v.type != int(data): continue
count += 1
print(str(k) + ':', v, mobs.MOB_NAMES[v.type])
reply = str(count) + ' monsters'
if command == 'threats':
distance = int(data) if data else 20
p = utils.pint(self.g.pos)
threats = self.g.world.find_threats(p, distance)
for t in threats:
print(str(t.entity_id) + ':', t, mobs.MOB_NAMES[t.type])
reply = str(len(threats)) + ' threats'
if command == 'cache': if command == 'cache':
self.g.job.state = self.g.job.cache_items self.g.job.state = self.g.job.cache_items
@ -523,25 +596,27 @@ class Game:
print(utils.spiral(i)) print(utils.spiral(i))
if command == 'sand_slice': if command == 'sand_slice':
try:
result = self.g.world.find_sand_slice(utils.pint(self.g.pos), 50) result = self.g.world.find_sand_slice(utils.pint(self.g.pos), 50)
reply = str(result) reply = str(result)
except:
import traceback
print(traceback.format_exc())
reply = 'error'
if command == 'zzz': if command == 'zzz':
if not self.g.afk: if not self.g.afk:
if self.g.path:
travel_time = int(len(self.g.path) * 0.4) + 2
reply = 'give me ' + str(travel_time) + ' secs, moving'
self.g.queue_afk = True
else:
reply = '/afk' reply = '/afk'
if command == 'print': if command == 'print' and sender == 'tanner6':
try: data = data.replace('^', '.')
reply = str(eval(data)) reply = str(eval(data))
except BaseException as e: except BaseException as e:
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
reply = 'Error: {} - {}\n'.format(e.__class__.__name__, e) reply = 'Error: {} - {}\n'.format(e.__class__.__name__, e)
pass
if reply: if reply:
print(reply) print(reply)
@ -567,8 +642,8 @@ class Game:
def break_block(self, location): def break_block(self, location):
p = utils.pint(self.g.pos) p = utils.pint(self.g.pos)
if utils.phyp(p, location) > blocks.BREAK_DISTANCE: #if utils.phyp(p, location) > blocks.BREAK_DISTANCE + 1:
return False # return False
bid = self.g.chunks.get_block_at(*location) bid = self.g.chunks.get_block_at(*location)
if bid == 0: if bid == 0:
@ -596,6 +671,7 @@ class Game:
self.g.breaking = None self.g.breaking = None
def handle_break_animation(self, packet): def handle_break_animation(self, packet):
return
print(packet) print(packet)
def handle_break_ack(self, packet): def handle_break_ack(self, packet):
@ -761,22 +837,28 @@ class Game:
obj.item_count = entry.value.item_count obj.item_count = entry.value.item_count
def handle_spawn_living(self, packet): def handle_spawn_living(self, packet):
print(packet) self.g.mobs[packet.entity_id] = Munch(
return entity_id=packet.entity_id,
entity_uuid=packet.entity_uuid,
type=packet.type,
x=packet.x,
y=packet.y,
z=packet.z,
)
def handle_entity_position(self, packet): def handle_entity_position(self, packet):
obj = self.g.objects.get(packet.entity_id, None) mob = self.g.mobs.get(packet.entity_id, None)
if obj: if mob:
pass mob.x += packet.delta_x / 4096.0
#obj.x += packet.delta_x mob.y += packet.delta_y / 4096.0
#obj.y += packet.delta_y mob.z += packet.delta_z / 4096.0
#obj.z += packet.delta_z
def handle_entity_position_rotation(self, packet): def handle_entity_position_rotation(self, packet):
obj = self.g.objects.get(packet.entity_id, None) mob = self.g.mobs.get(packet.entity_id, None)
if obj: if mob:
print('object rotation found:', packet) mob.x += packet.delta_x / 4096.0
raise mob.y += packet.delta_y / 4096.0
mob.z += packet.delta_z / 4096.0
def handle_entity_velocity(self, packet): def handle_entity_velocity(self, packet):
obj = self.g.objects.get(packet.entity_id, None) obj = self.g.objects.get(packet.entity_id, None)
@ -790,6 +872,16 @@ class Game:
for eid in packet.entity_ids: for eid in packet.entity_ids:
if eid in self.g.objects: if eid in self.g.objects:
del self.g.objects[eid] del self.g.objects[eid]
if eid in self.g.mobs:
del self.g.mobs[eid]
def leave_bed(self):
packet = EntityActionPacket()
packet.entity_id = self.g.eid
packet.action_id = 2
packet.jump_boost = 0
self.g.connection.write_packet(packet)
def tick(self): def tick(self):
if self.g.breaking: if self.g.breaking:
@ -805,6 +897,10 @@ class Game:
else: else:
self.g.dumping = None self.g.dumping = None
if not len(self.g.path): if not self.g.path:
self.g.correction_count = 0 self.g.correction_count = 0
if self.g.queue_afk:
self.g.chat.send('/afk')
self.g.queue_afk = False

151
jobs.py
View File

@ -20,6 +20,8 @@ import items
importlib.reload(items) importlib.reload(items)
import data import data
importlib.reload(data) importlib.reload(data)
import mobs
importlib.reload(mobs)
class FindGappleStates: class FindGappleStates:
@ -308,12 +310,13 @@ class GatherSandStates:
w = self.g.world w = self.g.world
print('using origin', self.origin) print('using origin', self.origin)
s = w.find_sand_slice(self.origin, 50, self.bad_slices) start = time.time()
print('Found slice:', s) self.prev_layer, s = w.find_sand_slice(self.origin, 200, 10, self.bad_slices, self.prev_layer)
print('Found slice:', s, 'in', time.time() - start, 'seconds')
if s: if s:
self.slice = s self.slice = s
#self.bad_slices.append(s) self.bad_slices.append(s)
self.state = self.find_new_sand self.state = self.find_new_sand
else: else:
print('No slices remaining.') print('No slices remaining.')
@ -323,24 +326,23 @@ class GatherSandStates:
print('Finding new sand...') print('Finding new sand...')
w = self.g.world w = self.g.world
p = utils.pint(self.g.pos) p = utils.pint(self.g.pos)
head = utils.padd(p, path.BLOCK_ABOVE)
sand = w.find_sand(self.slice, 2, p) for sand in w.find_sand(self.slice, 2, p):
if sand not in self.bad_sand:
print('Found sand:', sand) print('Found sand:', sand)
break
if not len(sand): else: # for
print('No good sands left, aborting.')
self.state = self.cleanup self.state = self.cleanup
return return
for check in sand: self.sand = sand
if check in self.bad_sand:
continue
self.sand = check
break
if utils.phyp(p, self.sand) > blocks.BREAK_DISTANCE: if utils.phyp(head, self.sand) < blocks.BREAK_DISTANCE:
self.state = self.nav_to_sand
else:
self.state = self.dig_sand self.state = self.dig_sand
else:
self.state = self.nav_to_sand
def nav_to_sand(self): def nav_to_sand(self):
w = self.g.world w = self.g.world
@ -356,6 +358,7 @@ class GatherSandStates:
self.g.path = navpath[:-1] self.g.path = navpath[:-1]
self.state = self.going_to_sand self.state = self.going_to_sand
else: else:
print('Cant get to that sand')
self.bad_sand.append(self.sand) self.bad_sand.append(self.sand)
self.state = self.find_new_sand self.state = self.find_new_sand
@ -387,8 +390,10 @@ class GatherSandStates:
self.state = self.idle self.state = self.idle
self.origin = utils.pint(self.g.pos) self.origin = utils.pint(self.g.pos)
self.origin = (2019, 64, 238)
self.slice = None self.slice = None
self.bad_slices = [] self.bad_slices = []
self.prev_layer = 0
self.sand = None self.sand = None
self.bad_sand = [] self.bad_sand = []
self.wait_time = 0 self.wait_time = 0
@ -519,7 +524,7 @@ class SleepWithBedStates:
def going_to_area(self): def going_to_area(self):
if utils.pint(self.g.pos) == self.opening: if utils.pint(self.g.pos) == self.opening:
self.g.look_at = self.area self.g.look_at = utils.padd(self.area, path.BLOCK_BELOW)
self.state = self.place_bed self.state = self.place_bed
def place_bed(self): def place_bed(self):
@ -535,8 +540,16 @@ class SleepWithBedStates:
self.state = self.sleep_bed self.state = self.sleep_bed
def sleep_bed(self): def sleep_bed(self):
if self.g.time < 100: w = self.g.world
print('Woke up') p = utils.pint(self.g.pos)
threats = w.find_threats(p, 30)
if threats:
print('Waking up due to threats')
self.g.game.leave_bed()
self.state = self.break_bed
elif self.g.time < 100:
print('Woke up time')
self.state = self.break_bed self.state = self.break_bed
def break_bed(self): def break_bed(self):
@ -1004,6 +1017,96 @@ class GrabSaplingStates:
self.state() self.state()
class CheckThreatsStates:
def idle(self):
return None
def init(self):
self.state = self.find_threats
print('Checking for threats')
def find_threats(self):
w = self.g.world
p = utils.pint(self.g.pos)
threats = w.find_threats(p, 40)
if threats:
print('Found', len(threats), 'threats, fleeing')
self.state = self.find_safety
else:
print('Aborting, no threats')
self.state = self.cleanup
def find_safety(self):
w = self.g.world
p = utils.pint(self.g.pos)
safety = w.find_blocks_indexed(p, [blocks.EMERALD_BLOCK])
if not safety:
print('No emerald blocks found, aborting')
self.state = self.cleanup
return
safety.sort(key=lambda s: utils.phyp(p, s))
print('Found emerald blocks:', safety)
for s in safety:
s = utils.padd(s, path.BLOCK_ABOVE)
navpath = w.path_to_place(p, s)
if navpath:
self.g.path = navpath
self.state = self.going_to_safety
self.safety = s
print('Going to safety', self.safety)
return
else:
print('Cant get to safety', self.safety)
print('Cant get to safety, aborting')
self.state = self.cleanup
def going_to_safety(self):
if utils.pint(self.g.pos) == self.safety:
print('At safety spot, waiting to be moved')
self.state = self.wait_for_move
def wait_for_move(self):
# wait for the server to move the bot when it's safe
# ie. a piston + daylight sensor
if utils.pint(self.g.pos) != self.safety:
print('Moved, resuming job')
self.state = self.wait
self.wait_time = 3
def wait(self):
# wait to land, etc
if self.wait_time > 0:
self.wait_time -= utils.TICK
else:
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.safety = None
self.wait_time = 0
def run(self):
self.state()
class JobStates: class JobStates:
def idle(self): def idle(self):
return [] return []
@ -1018,6 +1121,7 @@ class JobStates:
self.clear_leaves_states = ClearLeavesStates(self.g) self.clear_leaves_states = ClearLeavesStates(self.g)
self.grab_sapling_states = GrabSaplingStates(self.g) self.grab_sapling_states = GrabSaplingStates(self.g)
self.grab_sand_states = GrabSandStates(self.g) self.grab_sand_states = GrabSandStates(self.g)
self.check_threats_states = CheckThreatsStates(self.g)
def run_machines(self, machines): def run_machines(self, machines):
for m in machines: for m in machines:
@ -1040,6 +1144,18 @@ class JobStates:
] ]
return machines return machines
def farm_sand(self):
machines = [
self.check_threats_states,
self.gather_sand_states,
self.grab_sand_states,
self.cache_items_states,
self.sleep_with_bed_states,
]
self.sleep_with_bed_states.silent = True
self.cache_items_states.silent = True
return machines
def cache_items(self): def cache_items(self):
machines = [ machines = [
self.cache_items_states, self.cache_items_states,
@ -1070,7 +1186,6 @@ class JobStates:
self.sleep_with_bed_states, self.sleep_with_bed_states,
self.cache_items_states, self.cache_items_states,
] ]
self.sleep_with_bed_states.silent = True self.sleep_with_bed_states.silent = True
self.cache_items_states.silent = True self.cache_items_states.silent = True
return machines return machines

50
mobs.py Normal file
View File

@ -0,0 +1,50 @@
import json
with open('mcdata/registries.json') as f:
MOBS = json.load(f)['minecraft:entity_type']['entries']
EVIL = [
'blaze',
'cave_spider',
'creeper',
'drowned',
'elder_guardian',
'ender_dragon',
'enderman',
'endermite',
'evoker',
'ghast',
'giant',
'guardian',
'hoglin',
'husk',
'illusioner',
'magma_cube',
'phantom',
'piglin',
'piglin_brute',
'pillager',
'ravager',
'shulker',
'silverfish',
'skeleton',
'skeleton_horse',
'slime',
'spider',
'stray',
'vex',
'vindicator',
'witch',
'wither',
'zoglin',
'zombie',
'zombie_villager',
]
EVIL_IDS = set()
for mob_name in EVIL:
EVIL_IDS.add(MOBS['minecraft:'+mob_name]['protocol_id'])
MOB_NAMES = {}
for mob_name, mob in MOBS.items():
MOB_NAMES[MOBS[mob_name]['protocol_id']] = mob_name.replace('minecraft:', '')

View File

@ -23,6 +23,7 @@ def get_packets(old_get_packets):
mc_packets.add(packets.SpawnLivingEntityPacket) mc_packets.add(packets.SpawnLivingEntityPacket)
mc_packets.add(packets.EntityPositionRotationPacket) mc_packets.add(packets.EntityPositionRotationPacket)
mc_packets.add(packets.DestroyEntitiesPacket) mc_packets.add(packets.DestroyEntitiesPacket)
mc_packets.add(packets.EntityActionPacket)
return mc_packets return mc_packets
return lambda x: wrapper(old_get_packets, x) return lambda x: wrapper(old_get_packets, x)

View File

@ -361,3 +361,16 @@ class DestroyEntitiesPacket(Packet):
for _ in range(self.count): for _ in range(self.count):
eid = VarInt.read(file_object) eid = VarInt.read(file_object)
self.entity_ids.append(eid) self.entity_ids.append(eid)
class EntityActionPacket(Packet):
# Sent by the client when it performs an action
# https://wiki.vg/Protocol#Entity_Action
id = 0x1C
packet_name = 'entity action'
definition = [
{'entity_id': VarInt},
{'action_id': VarInt},
{'jump_boost': VarInt},
]