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,
path-following movement (`1goto x y z`), inventory draining (`1drain`),
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
26.3 packets are migrated.
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
in the same channel. Verified commands are `!ping`, `!pos`, `!loaded`,
`!info x y z`, `!inv` (basic slot/count data), `1cache`, `1drain`, addressed
path movement such as `1goto x y z`, and `1farm crops`. The command catalog below is retained from
`!info x y z`, `!inv` (basic slot/count data), `1cache`, `1drain`, `1sleep`,
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
other farming/trading commands are not yet available on 26.3. Common item
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.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)
+4 -1
View File
@@ -11,14 +11,17 @@ class TestCropCaching(unittest.TestCase):
old_wanted = items.WANTED_ITEMS
crop_state = object()
cache_state = Munch(silent=False)
sleep_state = Munch(silent=False)
jobs = JobStates.__new__(JobStates)
jobs.gather_crop_states = crop_state
jobs.cache_items_states = cache_state
jobs.sleep_with_bed_states = sleep_state
try:
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(sleep_state.silent)
self.assertTrue({
items.CARROT_ID, items.POTATO_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()