Implement 26.3 bed sleeping

This commit is contained in:
Tanner
2026-11-04 19:20:15 +00:00
parent 9fd565c3c9
commit 1ecec547ae
10 changed files with 404 additions and 75 deletions
+3
View File
@@ -407,6 +407,7 @@ def start_automation(global_state, initialize=True):
g.yaw = float(g.get('yaw', 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_hand = 0
g.chunks.loading = False
# Readiness below is based on nearby chunks, not unrelated view holes.
g.movement_paused = False
@@ -429,6 +430,8 @@ def start_automation(global_state, initialize=True):
g.setdefault('item_lock', False)
g.setdefault('minimum_cache_slots', 27)
g.setdefault('holding_slot', 0)
g.setdefault('holding_hand', 0)
g.setdefault('time', 0.0)
old_stop = g.get('movement_stop_event')
old_worker = g.get('movement_thread')
+7
View File
@@ -397,6 +397,13 @@ class Commands:
self.g.game.respawn()
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 sand - gathers sand from the world
if command == 'gather' and data:
+42 -3
View File
@@ -23,6 +23,7 @@ from mosfet.protocol.packets import (
)
from mosfet.protocol.types import Slot
from mosfet.protocol.modern import PlayerCommandPacket, UpdateTimePacket
from mosfet import utils
from mosfet import path
@@ -164,7 +165,32 @@ class LegacyGame:
self.g.path = new_path
def handle_time_update(self, packet):
self.g.time = packet.time_of_day % 24000
if hasattr(packet, 'time_of_day'):
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):
g = self.g
@@ -632,8 +658,11 @@ class Game(LegacyGame):
for packet in reactor.__class__.get_clientbound_packets(context)
}
reactor.clientbound_packets[SetHealthPacket.id] = SetHealthPacket
reactor.clientbound_packets[UpdateTimePacket.id] = UpdateTimePacket
self.g.connection.register_packet_listener(
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):
self.g.health = None
@@ -680,6 +709,10 @@ class Game(LegacyGame):
self.break_deadline = time.monotonic() + max(0.05, hardness_time)
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):
slots = [slot for slot, item in sorted(self.g.inv.items())
if 9 <= slot <= 45 and getattr(item, 'present', False)]
@@ -783,6 +816,12 @@ class Game(LegacyGame):
slot, item = next(
((slot, item) for slot, item in matches if 36 <= slot <= 44),
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:
hotbar_slot = slot - 36
elif 9 <= slot <= 35:
@@ -807,10 +846,10 @@ class Game(LegacyGame):
self.g.connection.write_packet(ClientTickEnd(), force=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)
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_z=0.5, inside_block=False, world_border_hit=False,
sequence=self._next_sequence())
+14 -1
View File
@@ -270,17 +270,26 @@ class JobStates(LegacyJobStates):
self.g = global_state
self.gather_crop_states = gather_crop.GatherCropStates(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
def farm_crop(self):
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.CARROT_ID,
items.POTATO_ID,
items.WHEAT_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):
if self.g.get('breaking'):
@@ -289,7 +298,11 @@ class JobStates(LegacyJobStates):
self.g.game.close_window()
self.g.path = []
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.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.state = self.idle
+121 -41
View File
@@ -21,12 +21,18 @@ class SleepWithBedStates:
return None
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')
self.state = self.cleanup
return
if self.g.dimension != 'overworld':
if self.g.get('dimension') != 'overworld':
print('Aborting sleep, not in overworld')
self.state = self.cleanup
return
@@ -38,7 +44,10 @@ class SleepWithBedStates:
w = self.g.world
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 = []
for bed in result:
@@ -57,38 +66,39 @@ class SleepWithBedStates:
print('Choosing a bed...')
w = self.g.world
p = utils.pint(self.g.pos)
c = self.g.chunks
if not len(self.beds):
print('No beds')
self.state = self.select_bed
return
while self.beds:
bed = self.beds.pop(0)
print('Chose:', bed)
navpath = w.path_to_place_faked(p, bed)
print('navpath:', navpath)
if navpath is None:
self.bad_beds.append(bed)
print('Cant get to bed, blacklisting')
continue
bed = self.beds[0]
print('Chose:', bed)
navpath = w.path_to_place_faked(p, bed)
print('navpath:', navpath)
if navpath:
self.g.path = navpath[:-1]
self.opening = self.g.path[-1]
self.g.look_at = self.opening
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
else:
self.beds.pop(0)
self.bad_beds.append(bed)
print('Cant get to bed, blacklisting')
time.sleep(0.1)
self.state = self.select_bed
print('No reachable beds; trying to place one')
self.state = self.select_bed
def going_to_bed(self):
if utils.pint(self.g.pos) == self.opening:
print('At existing bed')
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
def select_bed(self):
@@ -116,9 +126,24 @@ class SleepWithBedStates:
openings = w.find_bed_openings(self.area)
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)
self.opening = o
if navpath: break
if navpath is not None:
break
else: # for
print('Unable to get to bed area', self.area)
self.bad_areas.append(self.area)
@@ -126,35 +151,57 @@ class SleepWithBedStates:
return
self.g.path = navpath
self.g.look_at = self.opening
self.g.look_at = self.area
self.state = self.going_to_area
def going_to_area(self):
if utils.pint(self.g.pos) == self.opening:
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
def place_bed(self):
print('Placing bed')
self.g.game.place_block(self.area, BlockFace.TOP)
support = utils.padd(self.area, path.BLOCK_BELOW)
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.wait_time = 0.5
self.state = self.wait_use
def wait_use(self):
# wait to use the bed
if self.wait_time > 0:
self.wait_time -= utils.TICK
else:
self.state = self.use_bed
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
def use_bed(self):
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
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):
print('Bed occupied, aborting.')
self.state = self.cleanup
@@ -162,6 +209,8 @@ class SleepWithBedStates:
print('Using bed')
self.g.game.place_block(self.area, BlockFace.TOP)
self.sleep_deadline = time.monotonic() + 5.0
self.sleep_confirmed = False
if not self.silent:
self.g.chat.send('zzz')
self.state = self.sleep_bed
@@ -169,43 +218,71 @@ class SleepWithBedStates:
def sleep_bed(self):
w = self.g.world
p = utils.pint(self.g.pos)
current_time = self.g.get('time')
threats = w.find_threats(p, 10)
if w.check_bed_occupied(self.area):
self.sleep_confirmed = True
if threats:
print('Waking up due to threats:')
print(threats)
self.g.game.leave_bed()
if self.sleep_confirmed:
self.g.game.leave_bed()
self.state = self.cleanup
elif self.g.time < 100:
elif current_time is not None and current_time < 100:
print('Woke up time')
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')
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):
if self.my_bed:
self.g.game.break_block(self.area)
self.state = self.collect_bed
if self.g.game.break_block(self.area):
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:
self.state = self.cleanup
def collect_bed(self):
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.collect_deadline = time.monotonic() + 8
self.state = self.wait
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:
self.wait_time -= utils.TICK
else:
self.state = self.cleanup
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.path = []
self.my_bed = False
self.force_new_bed = False
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):
# never gets ran, placeholder
@@ -224,6 +301,9 @@ class SleepWithBedStates:
self.opening = None
self.bad_areas = []
self.wait_time = 0
self.force_new_bed = False
self.sleep_confirmed = False
self.sleep_deadline = 0
def run(self):
self.state()
+30
View File
@@ -3,11 +3,41 @@ import re
import time
from contextlib import nullcontext
from minecraft.networking.packets import Packet
from minecraft.networking.types import Float, Long, VarInt, VarLong
from minecraft.networking.modern import (
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):
"""Return (sender_name, body) for common Minecraft incoming-DM forms."""
patterns = (
+46 -26
View File
@@ -158,28 +158,50 @@ class World:
return navpath
def find_bed_areas(self, center, distance):
bed_clearance = 9 # 5x5 area
clear_distance = 2
for a in self.find_blocks_3d(center, [0], distance, 50):
# check for air around the area
if len(self.find_blocks(a, clear_distance, [0], bed_clearance)) < bed_clearance:
"""Yield nearby two-block bed footprints on solid, safe ground."""
# Only search loaded-neighborhood distance. The old 3-D flood fill
# explored millions of air blocks and used overly broad 5x5 clearance.
distance = min(distance, 32)
headroom = path.BLOCK_ABOVE
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
# check for ground around the area
if len(self.find_blocks(utils.padd(a, path.BLOCK_BELOW), clear_distance, blocks.NON_SOLID_IDS, bed_clearance)):
continue
footprint_found = False
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
# check for air above the area
if len(self.find_blocks(utils.padd(a, path.BLOCK_ABOVE), clear_distance, [0], bed_clearance)) < bed_clearance:
continue
# 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
# Avoid placing a bed within range of nearby monsters.
if self.find_monsters(area, 20):
continue
yield area
def find_cache_areas(self, center, distance):
"""Yield nearby, supported air blocks suitable for placing a chest."""
@@ -294,14 +316,12 @@ class World:
return result
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)
if blocks.PROPS[bid]['occupied'] == 'true':
print('Checking bed occupancy:', bed, '-> occupied')
return True
else:
print('Checking bed occupancy:', bed, '-> free')
return False
occupied = blocks.PROPS.get(bid, {}).get('occupied') == 'true'
print('Checking bed occupancy:', bed, '->',
'occupied' if occupied else 'free')
return occupied
def find_cache_openings(self, area):
return self.find_bed_openings(area)