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
+5 -3
View File
@@ -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
+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.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')
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+30
View File
@@ -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
View File
@@ -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)
+4 -1
View File
@@ -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,
+132
View File
@@ -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()