Implement 26.3 bed sleeping
This commit is contained in:
@@ -50,7 +50,9 @@ The 26.3 client now handles AuthMe, chat commands, chunk/world-state caching,
|
|||||||
modern block/item/entity data, entity tracking, inventory/component sync,
|
modern block/item/entity data, entity tracking, inventory/component sync,
|
||||||
path-following movement (`1goto x y z`), inventory draining (`1drain`),
|
path-following movement (`1goto x y z`), inventory draining (`1drain`),
|
||||||
normal/trapped-chest caching (`1cache`, also run by crop farming at the
|
normal/trapped-chest caching (`1cache`, also run by crop farming at the
|
||||||
inventory threshold), and the crop harvest/replant loop (`1farm crops`). Other
|
inventory threshold), bed sleeping (`1sleep`, or `1sleep new` to place a bed;
|
||||||
|
also run by crop farming at night), and the crop harvest/replant loop
|
||||||
|
(`1farm crops`). Other
|
||||||
inventory/window actions and farming/trading jobs remain disabled until their
|
inventory/window actions and farming/trading jobs remain disabled until their
|
||||||
26.3 packets are migrated.
|
26.3 packets are migrated.
|
||||||
The default server is `127.0.0.1`; set `SERVER` explicitly only when you
|
The default server is `127.0.0.1`; set `SERVER` explicitly only when you
|
||||||
@@ -76,8 +78,8 @@ and all bots will run the command.
|
|||||||
|
|
||||||
The current 26.3 command path handles public chat and direct messages, replying
|
The current 26.3 command path handles public chat and direct messages, replying
|
||||||
in the same channel. Verified commands are `!ping`, `!pos`, `!loaded`,
|
in the same channel. Verified commands are `!ping`, `!pos`, `!loaded`,
|
||||||
`!info x y z`, `!inv` (basic slot/count data), `1cache`, `1drain`, addressed
|
`!info x y z`, `!inv` (basic slot/count data), `1cache`, `1drain`, `1sleep`,
|
||||||
path movement such as `1goto x y z`, and `1farm crops`. The command catalog below is retained from
|
addressed path movement such as `1goto x y z`, and `1farm crops`. The command catalog below is retained from
|
||||||
the legacy bot; other inventory actions, arbitrary block interaction, and the
|
the legacy bot; other inventory actions, arbitrary block interaction, and the
|
||||||
other farming/trading commands are not yet available on 26.3. Common item
|
other farming/trading commands are not yet available on 26.3. Common item
|
||||||
components (names, damage, enchantments, book
|
components (names, damage, enchantments, book
|
||||||
|
|||||||
@@ -407,6 +407,7 @@ def start_automation(global_state, initialize=True):
|
|||||||
g.yaw = float(g.get('yaw', 0.0) or 0.0)
|
g.yaw = float(g.get('yaw', 0.0) or 0.0)
|
||||||
g.pitch = float(g.get('pitch', 0.0) or 0.0)
|
g.pitch = float(g.get('pitch', 0.0) or 0.0)
|
||||||
g.holding_slot = g.get('holding_slot', 0)
|
g.holding_slot = g.get('holding_slot', 0)
|
||||||
|
g.holding_hand = 0
|
||||||
g.chunks.loading = False
|
g.chunks.loading = False
|
||||||
# Readiness below is based on nearby chunks, not unrelated view holes.
|
# Readiness below is based on nearby chunks, not unrelated view holes.
|
||||||
g.movement_paused = False
|
g.movement_paused = False
|
||||||
@@ -429,6 +430,8 @@ def start_automation(global_state, initialize=True):
|
|||||||
g.setdefault('item_lock', False)
|
g.setdefault('item_lock', False)
|
||||||
g.setdefault('minimum_cache_slots', 27)
|
g.setdefault('minimum_cache_slots', 27)
|
||||||
g.setdefault('holding_slot', 0)
|
g.setdefault('holding_slot', 0)
|
||||||
|
g.setdefault('holding_hand', 0)
|
||||||
|
g.setdefault('time', 0.0)
|
||||||
|
|
||||||
old_stop = g.get('movement_stop_event')
|
old_stop = g.get('movement_stop_event')
|
||||||
old_worker = g.get('movement_thread')
|
old_worker = g.get('movement_thread')
|
||||||
|
|||||||
@@ -397,6 +397,13 @@ class Commands:
|
|||||||
self.g.game.respawn()
|
self.g.game.respawn()
|
||||||
reply = 'ok'
|
reply = 'ok'
|
||||||
|
|
||||||
|
## 1sleep [new] - use a bed; `new` forces bed placement
|
||||||
|
if command == 'sleep':
|
||||||
|
self.g.job.sleep_with_bed_states.force_new_bed = (
|
||||||
|
data.lower() in ('new', 'place'))
|
||||||
|
self.g.job.state = self.g.job.sleep
|
||||||
|
reply = 'ok'
|
||||||
|
|
||||||
## 1gather wood - gathers wood from the world
|
## 1gather wood - gathers wood from the world
|
||||||
## 1gather sand - gathers sand from the world
|
## 1gather sand - gathers sand from the world
|
||||||
if command == 'gather' and data:
|
if command == 'gather' and data:
|
||||||
|
|||||||
+41
-2
@@ -23,6 +23,7 @@ from mosfet.protocol.packets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from mosfet.protocol.types import Slot
|
from mosfet.protocol.types import Slot
|
||||||
|
from mosfet.protocol.modern import PlayerCommandPacket, UpdateTimePacket
|
||||||
|
|
||||||
from mosfet import utils
|
from mosfet import utils
|
||||||
from mosfet import path
|
from mosfet import path
|
||||||
@@ -164,7 +165,32 @@ class LegacyGame:
|
|||||||
self.g.path = new_path
|
self.g.path = new_path
|
||||||
|
|
||||||
def handle_time_update(self, packet):
|
def handle_time_update(self, packet):
|
||||||
|
if hasattr(packet, 'time_of_day'):
|
||||||
self.g.time = packet.time_of_day % 24000
|
self.g.time = packet.time_of_day % 24000
|
||||||
|
return
|
||||||
|
|
||||||
|
updates = getattr(packet, 'clock_updates', ())
|
||||||
|
if not updates:
|
||||||
|
return
|
||||||
|
|
||||||
|
registries = getattr(self.g.connection, 'registry_ids', {})
|
||||||
|
clock_names = {
|
||||||
|
index: name
|
||||||
|
for name, index in registries.get(
|
||||||
|
'minecraft:world_clock', {}).items()
|
||||||
|
}
|
||||||
|
dimension = self.g.get('dimension', 'overworld')
|
||||||
|
preferred = [
|
||||||
|
update for update in updates
|
||||||
|
if clock_names.get(update['clock_id'], '').removeprefix(
|
||||||
|
'minecraft:') in (dimension, 'overworld')
|
||||||
|
]
|
||||||
|
update = preferred[0] if preferred else updates[0]
|
||||||
|
self.g.time = update['total_ticks'] % 24000
|
||||||
|
self.g.clock_id = update['clock_id']
|
||||||
|
self.g.clock_name = clock_names.get(update['clock_id'])
|
||||||
|
self.g.clock_rate = update['rate']
|
||||||
|
self.g.world_age = packet.age
|
||||||
|
|
||||||
def handle_set_slot(self, packet):
|
def handle_set_slot(self, packet):
|
||||||
g = self.g
|
g = self.g
|
||||||
@@ -632,8 +658,11 @@ class Game(LegacyGame):
|
|||||||
for packet in reactor.__class__.get_clientbound_packets(context)
|
for packet in reactor.__class__.get_clientbound_packets(context)
|
||||||
}
|
}
|
||||||
reactor.clientbound_packets[SetHealthPacket.id] = SetHealthPacket
|
reactor.clientbound_packets[SetHealthPacket.id] = SetHealthPacket
|
||||||
|
reactor.clientbound_packets[UpdateTimePacket.id] = UpdateTimePacket
|
||||||
self.g.connection.register_packet_listener(
|
self.g.connection.register_packet_listener(
|
||||||
self.handle_set_health, SetHealthPacket)
|
self.handle_set_health, SetHealthPacket)
|
||||||
|
self.g.connection.register_packet_listener(
|
||||||
|
self.handle_time_update, UpdateTimePacket)
|
||||||
|
|
||||||
if not self.g.get('health_update_received', False):
|
if not self.g.get('health_update_received', False):
|
||||||
self.g.health = None
|
self.g.health = None
|
||||||
@@ -680,6 +709,10 @@ class Game(LegacyGame):
|
|||||||
self.break_deadline = time.monotonic() + max(0.05, hardness_time)
|
self.break_deadline = time.monotonic() + max(0.05, hardness_time)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def leave_bed(self):
|
||||||
|
self._send_action(PlayerCommandPacket(
|
||||||
|
entity_id=self.g.eid, action_id=0, jump_boost=0))
|
||||||
|
|
||||||
def start_drain(self):
|
def start_drain(self):
|
||||||
slots = [slot for slot, item in sorted(self.g.inv.items())
|
slots = [slot for slot, item in sorted(self.g.inv.items())
|
||||||
if 9 <= slot <= 45 and getattr(item, 'present', False)]
|
if 9 <= slot <= 45 and getattr(item, 'present', False)]
|
||||||
@@ -783,6 +816,12 @@ class Game(LegacyGame):
|
|||||||
slot, item = next(
|
slot, item = next(
|
||||||
((slot, item) for slot, item in matches if 36 <= slot <= 44),
|
((slot, item) for slot, item in matches if 36 <= slot <= 44),
|
||||||
matches[0])
|
matches[0])
|
||||||
|
if slot == 45:
|
||||||
|
self.g.holding = item.item_id
|
||||||
|
self.g.holding_hand = 1
|
||||||
|
self.g.connection.write_packet(ClientTickEnd(), force=True)
|
||||||
|
return True
|
||||||
|
self.g.holding_hand = 0
|
||||||
if 36 <= slot <= 44:
|
if 36 <= slot <= 44:
|
||||||
hotbar_slot = slot - 36
|
hotbar_slot = slot - 36
|
||||||
elif 9 <= slot <= 35:
|
elif 9 <= slot <= 35:
|
||||||
@@ -807,10 +846,10 @@ class Game(LegacyGame):
|
|||||||
self.g.connection.write_packet(ClientTickEnd(), force=True)
|
self.g.connection.write_packet(ClientTickEnd(), force=True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def place_block(self, location, face):
|
def place_block(self, location, face, hand=0):
|
||||||
location = tuple(int(value) for value in location)
|
location = tuple(int(value) for value in location)
|
||||||
packet = UseItemOnBlockPacket(
|
packet = UseItemOnBlockPacket(
|
||||||
hand=0, location=location, face=int(face),
|
hand=hand, location=location, face=int(face),
|
||||||
cursor_x=0.5, cursor_y=0.99 if int(face) == 1 else 0.5,
|
cursor_x=0.5, cursor_y=0.99 if int(face) == 1 else 0.5,
|
||||||
cursor_z=0.5, inside_block=False, world_border_hit=False,
|
cursor_z=0.5, inside_block=False, world_border_hit=False,
|
||||||
sequence=self._next_sequence())
|
sequence=self._next_sequence())
|
||||||
|
|||||||
+14
-1
@@ -270,17 +270,26 @@ class JobStates(LegacyJobStates):
|
|||||||
self.g = global_state
|
self.g = global_state
|
||||||
self.gather_crop_states = gather_crop.GatherCropStates(global_state)
|
self.gather_crop_states = gather_crop.GatherCropStates(global_state)
|
||||||
self.cache_items_states = cache_items.CacheItemsStates(global_state)
|
self.cache_items_states = cache_items.CacheItemsStates(global_state)
|
||||||
|
self.sleep_with_bed_states = sleep_with_bed.SleepWithBedStates(
|
||||||
|
global_state)
|
||||||
self.state = self.idle
|
self.state = self.idle
|
||||||
|
|
||||||
def farm_crop(self):
|
def farm_crop(self):
|
||||||
self.cache_items_states.silent = True
|
self.cache_items_states.silent = True
|
||||||
|
self.sleep_with_bed_states.silent = True
|
||||||
|
self.sleep_with_bed_states.force_new_bed = False
|
||||||
items.set_wanted({
|
items.set_wanted({
|
||||||
items.CARROT_ID,
|
items.CARROT_ID,
|
||||||
items.POTATO_ID,
|
items.POTATO_ID,
|
||||||
items.WHEAT_SEEDS_ID,
|
items.WHEAT_SEEDS_ID,
|
||||||
items.BEETROOT_SEEDS_ID,
|
items.BEETROOT_SEEDS_ID,
|
||||||
})
|
})
|
||||||
return [self.gather_crop_states, self.cache_items_states]
|
return [self.gather_crop_states, self.sleep_with_bed_states,
|
||||||
|
self.cache_items_states]
|
||||||
|
|
||||||
|
def sleep(self):
|
||||||
|
self.sleep_with_bed_states.silent = False
|
||||||
|
return [self.sleep_with_bed_states]
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
if self.g.get('breaking'):
|
if self.g.get('breaking'):
|
||||||
@@ -289,7 +298,11 @@ class JobStates(LegacyJobStates):
|
|||||||
self.g.game.close_window()
|
self.g.game.close_window()
|
||||||
self.g.path = []
|
self.g.path = []
|
||||||
self.g.look_at = None
|
self.g.look_at = None
|
||||||
|
if self.sleep_with_bed_states.state == self.sleep_with_bed_states.sleep_bed:
|
||||||
|
self.g.game.leave_bed()
|
||||||
self.gather_crop_states.state = self.gather_crop_states.idle
|
self.gather_crop_states.state = self.gather_crop_states.idle
|
||||||
self.cache_items_states.state = self.cache_items_states.idle
|
self.cache_items_states.state = self.cache_items_states.idle
|
||||||
|
self.sleep_with_bed_states.state = self.sleep_with_bed_states.idle
|
||||||
|
self.sleep_with_bed_states.force_new_bed = False
|
||||||
self.gather_crop_states.active_section_anchor = None
|
self.gather_crop_states.active_section_anchor = None
|
||||||
self.state = self.idle
|
self.state = self.idle
|
||||||
|
|||||||
+116
-36
@@ -21,12 +21,18 @@ class SleepWithBedStates:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
if self.g.time < 12000:
|
self.my_bed = False
|
||||||
|
self.area = None
|
||||||
|
self.opening = None
|
||||||
|
self.beds = []
|
||||||
|
|
||||||
|
current_time = self.g.get('time')
|
||||||
|
if current_time is None or current_time < 12000:
|
||||||
print('Aborting sleep, not night')
|
print('Aborting sleep, not night')
|
||||||
self.state = self.cleanup
|
self.state = self.cleanup
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.g.dimension != 'overworld':
|
if self.g.get('dimension') != 'overworld':
|
||||||
print('Aborting sleep, not in overworld')
|
print('Aborting sleep, not in overworld')
|
||||||
self.state = self.cleanup
|
self.state = self.cleanup
|
||||||
return
|
return
|
||||||
@@ -38,7 +44,10 @@ class SleepWithBedStates:
|
|||||||
w = self.g.world
|
w = self.g.world
|
||||||
p = utils.pint(self.g.pos)
|
p = utils.pint(self.g.pos)
|
||||||
|
|
||||||
result = w.find_blocks_indexed(p, blocks.BED_IDS, 80)
|
result = ([] if self.force_new_bed else
|
||||||
|
w.find_blocks_indexed(p, blocks.BED_IDS, 80))
|
||||||
|
if self.force_new_bed:
|
||||||
|
print('Skipping existing beds; placing a new bed as requested')
|
||||||
|
|
||||||
self.beds = []
|
self.beds = []
|
||||||
for bed in result:
|
for bed in result:
|
||||||
@@ -57,38 +66,39 @@ class SleepWithBedStates:
|
|||||||
print('Choosing a bed...')
|
print('Choosing a bed...')
|
||||||
w = self.g.world
|
w = self.g.world
|
||||||
p = utils.pint(self.g.pos)
|
p = utils.pint(self.g.pos)
|
||||||
c = self.g.chunks
|
|
||||||
|
|
||||||
if not len(self.beds):
|
while self.beds:
|
||||||
print('No beds')
|
bed = self.beds.pop(0)
|
||||||
self.state = self.select_bed
|
|
||||||
return
|
|
||||||
|
|
||||||
bed = self.beds[0]
|
|
||||||
print('Chose:', bed)
|
print('Chose:', bed)
|
||||||
|
|
||||||
navpath = w.path_to_place_faked(p, bed)
|
navpath = w.path_to_place_faked(p, bed)
|
||||||
print('navpath:', navpath)
|
print('navpath:', navpath)
|
||||||
|
if navpath is None:
|
||||||
if navpath:
|
|
||||||
self.g.path = navpath[:-1]
|
|
||||||
self.opening = self.g.path[-1]
|
|
||||||
self.g.look_at = self.opening
|
|
||||||
self.area = bed
|
|
||||||
self.state = self.going_to_bed
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
self.beds.pop(0)
|
|
||||||
self.bad_beds.append(bed)
|
self.bad_beds.append(bed)
|
||||||
print('Cant get to bed, blacklisting')
|
print('Cant get to bed, blacklisting')
|
||||||
time.sleep(0.1)
|
continue
|
||||||
|
|
||||||
|
self.area = bed
|
||||||
|
self.opening = navpath[-2] if len(navpath) > 1 else p
|
||||||
|
self.g.path = navpath[:-1]
|
||||||
|
self.g.look_at = self.area
|
||||||
|
self.state = self.going_to_bed
|
||||||
|
return
|
||||||
|
|
||||||
|
print('No reachable beds; trying to place one')
|
||||||
self.state = self.select_bed
|
self.state = self.select_bed
|
||||||
|
|
||||||
def going_to_bed(self):
|
def going_to_bed(self):
|
||||||
if utils.pint(self.g.pos) == self.opening:
|
if utils.pint(self.g.pos) == self.opening:
|
||||||
print('At existing bed')
|
print('At existing bed')
|
||||||
self.my_bed = False
|
self.my_bed = False
|
||||||
self.g.look_at = utils.padd(self.area, path.BLOCK_BELOW)
|
self.g.look_at = self.area
|
||||||
|
self.wait_time = 0.5
|
||||||
|
self.state = self.turn_to_bed
|
||||||
|
|
||||||
|
def turn_to_bed(self):
|
||||||
|
self.g.look_at = self.area
|
||||||
|
self.wait_time -= utils.TICK
|
||||||
|
if self.wait_time <= 0:
|
||||||
self.state = self.use_bed
|
self.state = self.use_bed
|
||||||
|
|
||||||
def select_bed(self):
|
def select_bed(self):
|
||||||
@@ -116,9 +126,24 @@ class SleepWithBedStates:
|
|||||||
openings = w.find_bed_openings(self.area)
|
openings = w.find_bed_openings(self.area)
|
||||||
|
|
||||||
for o in openings:
|
for o in openings:
|
||||||
|
toward_player = (o[0] - self.area[0], 0,
|
||||||
|
o[2] - self.area[2])
|
||||||
|
# The bed head points away from the player when placing from this
|
||||||
|
# opening, so require that footprint and its support to be clear.
|
||||||
|
head = (self.area[0] - toward_player[0], self.area[1],
|
||||||
|
self.area[2] - toward_player[2])
|
||||||
|
if (w.block_at(*head) != blocks.AIR or
|
||||||
|
w.block_at(*utils.padd(head, path.BLOCK_ABOVE)) != blocks.AIR):
|
||||||
|
continue
|
||||||
|
head_support = w.block_at(*utils.padd(head, path.BLOCK_BELOW))
|
||||||
|
if (head_support is None or
|
||||||
|
head_support in blocks.NON_SOLID_IDS or
|
||||||
|
head_support in blocks.AVOID_IDS):
|
||||||
|
continue
|
||||||
navpath = w.path_to_place(p, o)
|
navpath = w.path_to_place(p, o)
|
||||||
self.opening = o
|
self.opening = o
|
||||||
if navpath: break
|
if navpath is not None:
|
||||||
|
break
|
||||||
else: # for
|
else: # for
|
||||||
print('Unable to get to bed area', self.area)
|
print('Unable to get to bed area', self.area)
|
||||||
self.bad_areas.append(self.area)
|
self.bad_areas.append(self.area)
|
||||||
@@ -126,35 +151,57 @@ class SleepWithBedStates:
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.g.path = navpath
|
self.g.path = navpath
|
||||||
self.g.look_at = self.opening
|
self.g.look_at = self.area
|
||||||
self.state = self.going_to_area
|
self.state = self.going_to_area
|
||||||
|
|
||||||
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:
|
||||||
print('At bed area')
|
print('At bed area')
|
||||||
self.g.look_at = utils.padd(self.area, path.BLOCK_BELOW)
|
self.g.look_at = self.area
|
||||||
|
self.wait_time = 0.5
|
||||||
|
self.state = self.turn_to_bed_area
|
||||||
|
|
||||||
|
def turn_to_bed_area(self):
|
||||||
|
self.g.look_at = self.area
|
||||||
|
self.wait_time -= utils.TICK
|
||||||
|
if self.wait_time <= 0:
|
||||||
self.state = self.place_bed
|
self.state = self.place_bed
|
||||||
|
|
||||||
def place_bed(self):
|
def place_bed(self):
|
||||||
print('Placing bed')
|
support = utils.padd(self.area, path.BLOCK_BELOW)
|
||||||
self.g.game.place_block(self.area, BlockFace.TOP)
|
print('Placing bed at', self.area, 'on', support)
|
||||||
|
self.g.game.place_block(
|
||||||
|
support, BlockFace.TOP,
|
||||||
|
hand=self.g.get('holding_hand', 0))
|
||||||
self.my_bed = True
|
self.my_bed = True
|
||||||
self.wait_time = 0.5
|
self.wait_time = 0.5
|
||||||
self.state = self.wait_use
|
self.state = self.wait_use
|
||||||
|
|
||||||
def wait_use(self):
|
def wait_use(self):
|
||||||
# wait to use the bed
|
|
||||||
if self.wait_time > 0:
|
if self.wait_time > 0:
|
||||||
self.wait_time -= utils.TICK
|
self.wait_time -= utils.TICK
|
||||||
else:
|
return
|
||||||
|
|
||||||
|
if self.g.world.block_at(*self.area) not in blocks.BED_IDS:
|
||||||
|
print('Bed placement was not confirmed at', self.area)
|
||||||
|
self.bad_areas.append(self.area)
|
||||||
|
self.my_bed = False
|
||||||
|
self.state = self.cleanup
|
||||||
|
return
|
||||||
self.state = self.use_bed
|
self.state = self.use_bed
|
||||||
|
|
||||||
def use_bed(self):
|
def use_bed(self):
|
||||||
w = self.g.world
|
w = self.g.world
|
||||||
|
current_time = self.g.get('time')
|
||||||
|
|
||||||
if self.g.time < 12550:
|
if current_time is None or current_time < 12550:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if w.block_at(*self.area) not in blocks.BED_IDS:
|
||||||
|
print('Bed disappeared before use:', self.area)
|
||||||
|
self.my_bed = False
|
||||||
|
self.state = self.cleanup
|
||||||
|
return
|
||||||
if w.check_bed_occupied(self.area):
|
if w.check_bed_occupied(self.area):
|
||||||
print('Bed occupied, aborting.')
|
print('Bed occupied, aborting.')
|
||||||
self.state = self.cleanup
|
self.state = self.cleanup
|
||||||
@@ -162,6 +209,8 @@ class SleepWithBedStates:
|
|||||||
|
|
||||||
print('Using bed')
|
print('Using bed')
|
||||||
self.g.game.place_block(self.area, BlockFace.TOP)
|
self.g.game.place_block(self.area, BlockFace.TOP)
|
||||||
|
self.sleep_deadline = time.monotonic() + 5.0
|
||||||
|
self.sleep_confirmed = False
|
||||||
if not self.silent:
|
if not self.silent:
|
||||||
self.g.chat.send('zzz')
|
self.g.chat.send('zzz')
|
||||||
self.state = self.sleep_bed
|
self.state = self.sleep_bed
|
||||||
@@ -169,43 +218,71 @@ class SleepWithBedStates:
|
|||||||
def sleep_bed(self):
|
def sleep_bed(self):
|
||||||
w = self.g.world
|
w = self.g.world
|
||||||
p = utils.pint(self.g.pos)
|
p = utils.pint(self.g.pos)
|
||||||
|
current_time = self.g.get('time')
|
||||||
threats = w.find_threats(p, 10)
|
threats = w.find_threats(p, 10)
|
||||||
|
|
||||||
|
if w.check_bed_occupied(self.area):
|
||||||
|
self.sleep_confirmed = True
|
||||||
|
|
||||||
if threats:
|
if threats:
|
||||||
print('Waking up due to threats:')
|
print('Waking up due to threats:')
|
||||||
print(threats)
|
print(threats)
|
||||||
|
if self.sleep_confirmed:
|
||||||
self.g.game.leave_bed()
|
self.g.game.leave_bed()
|
||||||
self.state = self.cleanup
|
self.state = self.cleanup
|
||||||
elif self.g.time < 100:
|
elif current_time is not None and current_time < 100:
|
||||||
print('Woke up time')
|
print('Woke up time')
|
||||||
self.state = self.break_bed
|
self.state = self.break_bed
|
||||||
elif self.g.correction_count:
|
elif self.g.get('correction_count', 0) and self.sleep_confirmed:
|
||||||
print('Woke up by movement')
|
print('Woke up by movement')
|
||||||
self.state = self.break_bed
|
self.state = self.break_bed
|
||||||
|
elif (not self.sleep_confirmed and
|
||||||
|
time.monotonic() >= self.sleep_deadline):
|
||||||
|
print('Bed use was not confirmed')
|
||||||
|
self.state = self.cleanup
|
||||||
|
|
||||||
def break_bed(self):
|
def break_bed(self):
|
||||||
if self.my_bed:
|
if self.my_bed:
|
||||||
self.g.game.break_block(self.area)
|
if self.g.game.break_block(self.area):
|
||||||
self.state = self.collect_bed
|
self.state = self.collect_bed
|
||||||
|
else:
|
||||||
|
print('Could not break the placed bed at', self.area)
|
||||||
|
self.my_bed = False
|
||||||
|
self.state = self.cleanup
|
||||||
else:
|
else:
|
||||||
self.state = self.cleanup
|
self.state = self.cleanup
|
||||||
|
|
||||||
def collect_bed(self):
|
def collect_bed(self):
|
||||||
if not self.g.breaking:
|
if not self.g.breaking:
|
||||||
self.g.path = [utils.padd(self.area, utils.spiral(n)) for n in range(9)]
|
self.g.path = [utils.padd(self.area, utils.spiral(n))
|
||||||
|
for n in range(9)]
|
||||||
self.wait_time = 2
|
self.wait_time = 2
|
||||||
|
self.collect_deadline = time.monotonic() + 8
|
||||||
self.state = self.wait
|
self.state = self.wait
|
||||||
|
|
||||||
def wait(self):
|
def wait(self):
|
||||||
# wait to pick up bed
|
# Finish the short pickup sweep, but don't let it block forever.
|
||||||
|
if (self.g.path and
|
||||||
|
time.monotonic() < self.collect_deadline):
|
||||||
|
return
|
||||||
if self.wait_time > 0:
|
if self.wait_time > 0:
|
||||||
self.wait_time -= utils.TICK
|
self.wait_time -= utils.TICK
|
||||||
else:
|
else:
|
||||||
self.state = self.cleanup
|
self.state = self.cleanup
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
|
if (self.my_bed and self.area and
|
||||||
|
self.g.world.block_at(*self.area) in blocks.BED_IDS):
|
||||||
|
self.state = self.break_bed
|
||||||
|
return
|
||||||
self.g.look_at = None
|
self.g.look_at = None
|
||||||
|
self.g.path = []
|
||||||
|
self.my_bed = False
|
||||||
|
self.force_new_bed = False
|
||||||
self.state = self.done
|
self.state = self.done
|
||||||
|
if (self.g.get('job') and
|
||||||
|
getattr(self.g.job.state, '__name__', None) == 'sleep'):
|
||||||
|
self.g.job.state = self.g.job.idle
|
||||||
|
|
||||||
def done(self):
|
def done(self):
|
||||||
# never gets ran, placeholder
|
# never gets ran, placeholder
|
||||||
@@ -224,6 +301,9 @@ class SleepWithBedStates:
|
|||||||
self.opening = None
|
self.opening = None
|
||||||
self.bad_areas = []
|
self.bad_areas = []
|
||||||
self.wait_time = 0
|
self.wait_time = 0
|
||||||
|
self.force_new_bed = False
|
||||||
|
self.sleep_confirmed = False
|
||||||
|
self.sleep_deadline = 0
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.state()
|
self.state()
|
||||||
|
|||||||
@@ -3,11 +3,41 @@ import re
|
|||||||
import time
|
import time
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
|
|
||||||
|
from minecraft.networking.packets import Packet
|
||||||
|
from minecraft.networking.types import Float, Long, VarInt, VarLong
|
||||||
from minecraft.networking.modern import (
|
from minecraft.networking.modern import (
|
||||||
ChatCommand, ClientboundPlayerChatPacket, ServerboundChatPacket, SystemChat,
|
ChatCommand, ClientboundPlayerChatPacket, ServerboundChatPacket, SystemChat,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateTimePacket(Packet):
|
||||||
|
"""26.3 update-time packet with registry-indexed world clocks."""
|
||||||
|
# The 26.1 Prismarine schema defines this payload; protocol 777's 26.3
|
||||||
|
# clientbound table places it at 0x73 (0x71 is a different short packet).
|
||||||
|
id = 0x73
|
||||||
|
packet_name = 'update time'
|
||||||
|
|
||||||
|
def read(self, file_object):
|
||||||
|
self.age = Long.read(file_object)
|
||||||
|
self.clock_updates = []
|
||||||
|
for _ in range(VarInt.read(file_object)):
|
||||||
|
self.clock_updates.append({
|
||||||
|
'clock_id': VarInt.read(file_object),
|
||||||
|
'total_ticks': VarLong.read(file_object),
|
||||||
|
'partial_tick': Float.read(file_object),
|
||||||
|
'rate': Float.read(file_object),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerCommandPacket(Packet):
|
||||||
|
"""Client player-command packet; action 0 stops sleeping (protocol 777)."""
|
||||||
|
id = 0x2A
|
||||||
|
packet_name = 'player command'
|
||||||
|
definition = [
|
||||||
|
{'entity_id': VarInt}, {'action_id': VarInt}, {'jump_boost': VarInt},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def parse_incoming_private_message(message):
|
def parse_incoming_private_message(message):
|
||||||
"""Return (sender_name, body) for common Minecraft incoming-DM forms."""
|
"""Return (sender_name, body) for common Minecraft incoming-DM forms."""
|
||||||
patterns = (
|
patterns = (
|
||||||
|
|||||||
+44
-24
@@ -158,28 +158,50 @@ class World:
|
|||||||
return navpath
|
return navpath
|
||||||
|
|
||||||
def find_bed_areas(self, center, distance):
|
def find_bed_areas(self, center, distance):
|
||||||
bed_clearance = 9 # 5x5 area
|
"""Yield nearby two-block bed footprints on solid, safe ground."""
|
||||||
clear_distance = 2
|
# Only search loaded-neighborhood distance. The old 3-D flood fill
|
||||||
|
# explored millions of air blocks and used overly broad 5x5 clearance.
|
||||||
for a in self.find_blocks_3d(center, [0], distance, 50):
|
distance = min(distance, 32)
|
||||||
# check for air around the area
|
headroom = path.BLOCK_ABOVE
|
||||||
if len(self.find_blocks(a, clear_distance, [0], bed_clearance)) < bed_clearance:
|
support = path.BLOCK_BELOW
|
||||||
|
for offset in utils.search_2d(distance):
|
||||||
|
if utils.hypot(offset[0], offset[2]) > distance:
|
||||||
|
continue
|
||||||
|
for dy in (0, 1, -1, 2, -2):
|
||||||
|
area = (center[0] + offset[0], center[1] + dy,
|
||||||
|
center[2] + offset[2])
|
||||||
|
if area == center:
|
||||||
|
continue
|
||||||
|
if self.block_at(*area) != blocks.AIR:
|
||||||
|
continue
|
||||||
|
if self.block_at(*utils.padd(area, headroom)) != blocks.AIR:
|
||||||
|
continue
|
||||||
|
support_block = self.block_at(*utils.padd(area, support))
|
||||||
|
if (support_block is None or
|
||||||
|
support_block in blocks.NON_SOLID_IDS or
|
||||||
|
support_block in blocks.AVOID_IDS):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# check for ground around the area
|
footprint_found = False
|
||||||
if len(self.find_blocks(utils.padd(a, path.BLOCK_BELOW), clear_distance, blocks.NON_SOLID_IDS, bed_clearance)):
|
for direction in path.CHECK_DIRECTIONS:
|
||||||
|
head = utils.padd(area, direction)
|
||||||
|
if (self.block_at(*head) != blocks.AIR or
|
||||||
|
self.block_at(*utils.padd(head, headroom)) != blocks.AIR):
|
||||||
|
continue
|
||||||
|
head_support = self.block_at(*utils.padd(head, support))
|
||||||
|
if (head_support is None or
|
||||||
|
head_support in blocks.NON_SOLID_IDS or
|
||||||
|
head_support in blocks.AVOID_IDS):
|
||||||
|
continue
|
||||||
|
footprint_found = True
|
||||||
|
break
|
||||||
|
if not footprint_found:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# check for air above the area
|
# Avoid placing a bed within range of nearby monsters.
|
||||||
if len(self.find_blocks(utils.padd(a, path.BLOCK_ABOVE), clear_distance, [0], bed_clearance)) < bed_clearance:
|
if self.find_monsters(area, 20):
|
||||||
continue
|
continue
|
||||||
|
yield area
|
||||||
# ensure there's no monsters within 20 blocks
|
|
||||||
# can't sleep if they are within 10, good to have a buffer
|
|
||||||
if self.find_monsters(a, 20):
|
|
||||||
continue
|
|
||||||
|
|
||||||
yield a
|
|
||||||
|
|
||||||
def find_cache_areas(self, center, distance):
|
def find_cache_areas(self, center, distance):
|
||||||
"""Yield nearby, supported air blocks suitable for placing a chest."""
|
"""Yield nearby, supported air blocks suitable for placing a chest."""
|
||||||
@@ -294,14 +316,12 @@ class World:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def check_bed_occupied(self, bed):
|
def check_bed_occupied(self, bed):
|
||||||
# returns true if the bed is occupied by a player
|
"""Return whether a loaded bed block currently has occupied=true."""
|
||||||
bid = self.g.chunks.get_block_at(*bed)
|
bid = self.g.chunks.get_block_at(*bed)
|
||||||
if blocks.PROPS[bid]['occupied'] == 'true':
|
occupied = blocks.PROPS.get(bid, {}).get('occupied') == 'true'
|
||||||
print('Checking bed occupancy:', bed, '-> occupied')
|
print('Checking bed occupancy:', bed, '->',
|
||||||
return True
|
'occupied' if occupied else 'free')
|
||||||
else:
|
return occupied
|
||||||
print('Checking bed occupancy:', bed, '-> free')
|
|
||||||
return False
|
|
||||||
|
|
||||||
def find_cache_openings(self, area):
|
def find_cache_openings(self, area):
|
||||||
return self.find_bed_openings(area)
|
return self.find_bed_openings(area)
|
||||||
|
|||||||
@@ -11,14 +11,17 @@ class TestCropCaching(unittest.TestCase):
|
|||||||
old_wanted = items.WANTED_ITEMS
|
old_wanted = items.WANTED_ITEMS
|
||||||
crop_state = object()
|
crop_state = object()
|
||||||
cache_state = Munch(silent=False)
|
cache_state = Munch(silent=False)
|
||||||
|
sleep_state = Munch(silent=False)
|
||||||
jobs = JobStates.__new__(JobStates)
|
jobs = JobStates.__new__(JobStates)
|
||||||
jobs.gather_crop_states = crop_state
|
jobs.gather_crop_states = crop_state
|
||||||
jobs.cache_items_states = cache_state
|
jobs.cache_items_states = cache_state
|
||||||
|
jobs.sleep_with_bed_states = sleep_state
|
||||||
|
|
||||||
try:
|
try:
|
||||||
machines = JobStates.farm_crop(jobs)
|
machines = JobStates.farm_crop(jobs)
|
||||||
self.assertEqual(machines, [crop_state, cache_state])
|
self.assertEqual(machines, [crop_state, sleep_state, cache_state])
|
||||||
self.assertTrue(cache_state.silent)
|
self.assertTrue(cache_state.silent)
|
||||||
|
self.assertTrue(sleep_state.silent)
|
||||||
self.assertTrue({
|
self.assertTrue({
|
||||||
items.CARROT_ID, items.POTATO_ID,
|
items.CARROT_ID, items.POTATO_ID,
|
||||||
items.WHEAT_SEEDS_ID, items.BEETROOT_SEEDS_ID,
|
items.WHEAT_SEEDS_ID, items.BEETROOT_SEEDS_ID,
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import io
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from munch import Munch
|
||||||
|
from minecraft.networking.packets import PacketBuffer
|
||||||
|
from minecraft.networking.types import Float, Long, VarInt, VarLong
|
||||||
|
|
||||||
|
from mosfet.game import Game
|
||||||
|
from mosfet.info import blocks, items
|
||||||
|
from mosfet.protocol.types import Slot
|
||||||
|
from mosfet.jobs.sleep_with_bed import SleepWithBedStates
|
||||||
|
from mosfet.protocol.modern import PlayerCommandPacket, UpdateTimePacket
|
||||||
|
from minecraft.networking.modern import ClientTickEnd
|
||||||
|
|
||||||
|
|
||||||
|
class FakeConnection:
|
||||||
|
def __init__(self):
|
||||||
|
self.packets = []
|
||||||
|
|
||||||
|
def write_packet(self, packet, force=False):
|
||||||
|
self.packets.append((packet, force))
|
||||||
|
|
||||||
|
|
||||||
|
class FakeWorld:
|
||||||
|
def __init__(self, block=blocks.AIR):
|
||||||
|
self.block = block
|
||||||
|
|
||||||
|
def block_at(self, *position):
|
||||||
|
return self.block
|
||||||
|
|
||||||
|
|
||||||
|
class FakeGame:
|
||||||
|
def __init__(self):
|
||||||
|
self.placements = []
|
||||||
|
|
||||||
|
def place_block(self, location, face, hand=0):
|
||||||
|
self.placements.append((location, face, hand))
|
||||||
|
|
||||||
|
|
||||||
|
class TestSleepWithBed(unittest.TestCase):
|
||||||
|
def test_update_time_packet_decodes_clock_updates(self):
|
||||||
|
payload = PacketBuffer()
|
||||||
|
Long.send(123456, payload)
|
||||||
|
VarInt.send(1, payload)
|
||||||
|
VarInt.send(0, payload)
|
||||||
|
VarLong.send(13000, payload)
|
||||||
|
Float.send(0.5, payload)
|
||||||
|
Float.send(1.0, payload)
|
||||||
|
|
||||||
|
packet = UpdateTimePacket()
|
||||||
|
packet.read(io.BytesIO(payload.get_writable()))
|
||||||
|
self.assertEqual(packet.id, 0x73)
|
||||||
|
self.assertEqual(packet.age, 123456)
|
||||||
|
self.assertEqual(packet.clock_updates, [{
|
||||||
|
'clock_id': 0, 'total_ticks': 13000,
|
||||||
|
'partial_tick': 0.5, 'rate': 1.0,
|
||||||
|
}])
|
||||||
|
|
||||||
|
def test_player_command_encodes_stop_sleeping(self):
|
||||||
|
packet = PlayerCommandPacket(
|
||||||
|
entity_id=7, action_id=0, jump_boost=0)
|
||||||
|
payload = PacketBuffer()
|
||||||
|
packet.write_fields(payload)
|
||||||
|
self.assertEqual(packet.id, 0x2A)
|
||||||
|
self.assertEqual(payload.get_writable(), b'\x07\x00\x00')
|
||||||
|
|
||||||
|
def test_game_leave_bed_sends_protocol_777_stop_sleeping(self):
|
||||||
|
connection = FakeConnection()
|
||||||
|
game = Game.__new__(Game)
|
||||||
|
game.g = Munch(connection=connection, eid=42)
|
||||||
|
|
||||||
|
game.leave_bed()
|
||||||
|
|
||||||
|
packet, force = connection.packets[0]
|
||||||
|
self.assertIsInstance(packet, PlayerCommandPacket)
|
||||||
|
self.assertEqual((packet.entity_id, packet.action_id,
|
||||||
|
packet.jump_boost), (42, 0, 0))
|
||||||
|
self.assertTrue(force)
|
||||||
|
self.assertIsInstance(connection.packets[1][0], ClientTickEnd)
|
||||||
|
|
||||||
|
def test_bed_in_offhand_is_selectable_without_swapping_inventory(self):
|
||||||
|
connection = FakeConnection()
|
||||||
|
game = Game.__new__(Game)
|
||||||
|
bed_id = next(iter(items.BED_IDS))
|
||||||
|
game.g = Munch(
|
||||||
|
connection=connection, inv={45: Slot(True, bed_id, 1)},
|
||||||
|
holding_hand=0)
|
||||||
|
|
||||||
|
self.assertTrue(game.select_item(items.BED_IDS))
|
||||||
|
self.assertEqual(game.g.holding, bed_id)
|
||||||
|
self.assertEqual(game.g.holding_hand, 1)
|
||||||
|
self.assertIsInstance(connection.packets[0][0], ClientTickEnd)
|
||||||
|
|
||||||
|
def test_daytime_sleep_aborts_cleanly(self):
|
||||||
|
state = SleepWithBedStates(Munch(time=1000, dimension='overworld'))
|
||||||
|
state.init()
|
||||||
|
self.assertEqual(state.state, state.cleanup)
|
||||||
|
|
||||||
|
def test_placing_bed_uses_support_block_face_top(self):
|
||||||
|
game = FakeGame()
|
||||||
|
state = SleepWithBedStates(Munch(
|
||||||
|
game=game, world=FakeWorld(), path=[], look_at=None,
|
||||||
|
holding_hand=1))
|
||||||
|
state.area = (4, 70, 5)
|
||||||
|
|
||||||
|
state.place_bed()
|
||||||
|
|
||||||
|
self.assertEqual(len(game.placements), 1)
|
||||||
|
location, face, hand = game.placements[0]
|
||||||
|
self.assertEqual(location, (4, 69, 5))
|
||||||
|
self.assertEqual(int(face), 1)
|
||||||
|
self.assertEqual(hand, 1)
|
||||||
|
self.assertTrue(state.my_bed)
|
||||||
|
self.assertEqual(state.state, state.wait_use)
|
||||||
|
|
||||||
|
def test_wait_use_aborts_if_server_rejects_bed_placement(self):
|
||||||
|
state = SleepWithBedStates(Munch(
|
||||||
|
game=FakeGame(), world=FakeWorld(block=blocks.AIR),
|
||||||
|
path=[], look_at=None))
|
||||||
|
state.area = (4, 70, 5)
|
||||||
|
state.my_bed = True
|
||||||
|
state.wait_time = 0
|
||||||
|
|
||||||
|
state.wait_use()
|
||||||
|
|
||||||
|
self.assertIn(state.area, state.bad_areas)
|
||||||
|
self.assertFalse(state.my_bed)
|
||||||
|
self.assertEqual(state.state, state.cleanup)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user