Port loiter and harden 26.3 bed updates

This commit is contained in:
Tanner
2026-11-05 01:05:52 +00:00
parent b9d43c7c37
commit efa2140a91
12 changed files with 664 additions and 65 deletions
+2 -2
View File
@@ -66,9 +66,9 @@ class Commands:
command = 'nosquarebrackets'
unsupported = {
'gather', 'loiter', 'trade', 'gapple', 'fill',
'gather', 'trade', 'gapple', 'fill',
'drop', 'dump', 'break', 'open', 'click', 'use',
'interact', 'respawn',
'interact',
}
if (command in unsupported or
(command == 'farm' and not data.lower().startswith('crop'))):
+11 -3
View File
@@ -24,7 +24,7 @@ from mosfet.protocol.packets import (
from mosfet.protocol.types import Slot
from mosfet.protocol.modern import (
PlayerCommandPacket, UpdateTimePacket, UseItemPacket,
ClientCommandPacket, PlayerCommandPacket, UpdateTimePacket, UseItemPacket,
)
from mosfet import utils
@@ -711,6 +711,12 @@ class Game(LegacyGame):
self.break_deadline = time.monotonic() + max(0.05, hardness_time)
return True
def respawn(self):
self.g.respawn_requested_at = time.monotonic()
self.g.connection.write_packet(
ClientCommandPacket(action_id=0), force=True)
self.g.connection.write_packet(ClientTickEnd(), force=True)
def leave_bed(self):
self._send_action(PlayerCommandPacket(
entity_id=self.g.eid, action_id=0, jump_boost=0))
@@ -854,11 +860,13 @@ class Game(LegacyGame):
self.g.connection.write_packet(ClientTickEnd(), force=True)
return True
def place_block(self, location, face, hand=0):
def place_block(self, location, face, hand=0, cursor_y=None):
location = tuple(int(value) for value in location)
if cursor_y is None:
cursor_y = 0.99 if int(face) == 1 else 0.5
packet = UseItemOnBlockPacket(
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=cursor_y,
cursor_z=0.5, inside_block=False, world_border_hit=False,
sequence=self._next_sequence())
self._send_action(packet)
+11 -1
View File
@@ -265,7 +265,7 @@ class LegacyJobStates:
class JobStates(LegacyJobStates):
"""26.3 job facade; crop gathering is the currently migrated job."""
"""26.3 facade for crop, inventory, sleeping, and loiter workflows."""
def __init__(self, global_state):
self.g = global_state
self.gather_crop_states = gather_crop.GatherCropStates(global_state)
@@ -273,6 +273,8 @@ class JobStates(LegacyJobStates):
self.sleep_with_bed_states = sleep_with_bed.SleepWithBedStates(
global_state)
self.eat_food_states = eat_food.EatFoodStates(global_state)
self.check_threats_states = check_threats.CheckThreatsStates(
global_state)
self.state = self.idle
def farm_crop(self):
@@ -295,6 +297,11 @@ class JobStates(LegacyJobStates):
self.sleep_with_bed_states.silent = False
return [self.sleep_with_bed_states]
def loiter(self):
self.sleep_with_bed_states.silent = True
return [self.check_threats_states, self.sleep_with_bed_states,
self.eat_food_states]
def stop(self):
if self.g.get('breaking'):
self.g.game.cancel_break()
@@ -309,5 +316,8 @@ class JobStates(LegacyJobStates):
self.sleep_with_bed_states.state = self.sleep_with_bed_states.idle
self.sleep_with_bed_states.force_new_bed = False
self.eat_food_states.state = self.eat_food_states.idle
self.check_threats_states.state = self.check_threats_states.idle
self.check_threats_states.safety = None
self.check_threats_states.external_safety = False
self.gather_crop_states.active_section_anchor = None
self.state = self.idle
+46 -28
View File
@@ -17,6 +17,10 @@ from mosfet.info import mcdata
from mosfet.info import mobs
class CheckThreatsStates:
THREAT_DISTANCE = 20
CLEAR_DISTANCE = 30
CHECK_INTERVAL = 2.0
def idle(self):
return None
@@ -28,51 +32,63 @@ class CheckThreatsStates:
w = self.g.world
p = utils.pint(self.g.pos)
threats = w.find_threats(p, 40)
threats = w.find_threats(p, self.THREAT_DISTANCE)
if threats:
print('Found', len(threats), 'threats, fleeing:')
print('Found', len(threats), 'threats, seeking safety:')
print(threats)
self.threats = threats
self.state = self.find_safety
else:
print('Aborting, no threats')
self.state = self.cleanup
self.wait_time = self.CHECK_INTERVAL
self.state = self.wait
def find_safety(self):
w = self.g.world
p = utils.pint(self.g.pos)
safety = w.find_blocks_indexed(p, [blocks.EMERALD_BLOCK])
if not safety:
print('No emerald blocks found, aborting')
self.state = self.cleanup
return
safety.sort(key=lambda s: utils.phyp(p, s))
print('Found emerald blocks:', safety)
for s in safety:
s = utils.padd(s, path.BLOCK_ABOVE)
navpath = w.path_to_place(p, s)
if navpath:
safety_blocks = w.find_blocks_indexed(
p, [blocks.EMERALD_BLOCK], self.THREAT_DISTANCE * 4)
for block in safety_blocks:
spot = utils.padd(block, path.BLOCK_ABOVE)
if w.find_threats(spot, self.CLEAR_DISTANCE):
continue
navpath = w.path_to_place(p, spot)
if navpath is not None:
self.safety = spot
self.external_safety = True
self.g.path = navpath
self.state = self.going_to_safety
self.safety = s
print('Going to safety', self.safety)
print('Going to emerald safety point', self.safety)
return
else:
print('Cant get to safety', self.safety)
time.sleep(0.1)
print('Cant get to safety, aborting')
self.state = self.cleanup
# No configured emerald safe point was reachable. Retreat to a clear,
# supported location that the modern navigator can actually reach.
for spot in w.find_bed_areas(p, self.CLEAR_DISTANCE * 2):
if w.find_threats(spot, self.CLEAR_DISTANCE):
continue
navpath = w.path_to_place(p, spot)
if navpath is not None:
self.safety = spot
self.external_safety = False
self.g.path = navpath
self.state = self.going_to_safety
print('Fleeing to clear area', self.safety)
return
print('No reachable safe area found')
self.wait_time = self.CHECK_INTERVAL
self.state = self.wait
def going_to_safety(self):
if utils.pint(self.g.pos) == self.safety:
print('At safety spot, waiting to be moved')
self.state = self.wait_for_move
if self.external_safety:
print('At emerald safety point, waiting for server move')
self.state = self.wait_for_move
else:
print('At clear retreat spot')
self.wait_time = 3
self.state = self.wait
def wait_for_move(self):
# wait for the server to move the bot when it's safe
@@ -103,6 +119,8 @@ class CheckThreatsStates:
self.safety = None
self.wait_time = 0
self.threats = []
self.external_safety = False
def run(self):
self.state()
+106 -15
View File
@@ -24,7 +24,11 @@ class SleepWithBedStates:
self.my_bed = False
self.area = None
self.opening = None
self.bed_head = None
self.beds = []
self.sleep_use_attempts = 0
self.waiting_for_night = False
self.waiting_for_time_update = False
current_time = self.g.get('time')
if current_time is None or current_time < 12000:
@@ -39,16 +43,55 @@ class SleepWithBedStates:
self.state = self.find_beds
@staticmethod
def _bed_facing(state_id):
props = blocks.PROPS.get(state_id, {})
return {
'north': (0, 0, -1), 'south': (0, 0, 1),
'east': (1, 0, 0), 'west': (-1, 0, 0),
}.get(props.get('facing'))
def _bed_foot(self, position):
w = self.g.world
state_id = w.block_at(*position)
props = blocks.PROPS.get(state_id, {})
if props.get('part') != 'head':
return position
facing = self._bed_facing(state_id)
if facing is None:
return position
foot = utils.psub(position, facing)
foot_state = w.block_at(*foot)
if (foot_state in blocks.BED_IDS and
blocks.PROPS.get(foot_state, {}).get('part') == 'foot'):
return foot
return position
def _bed_head(self, foot):
state_id = self.g.world.block_at(*foot)
facing = self._bed_facing(state_id)
return utils.padd(foot, facing) if facing is not None else foot
def find_beds(self):
print('Finding beds...')
w = self.g.world
p = utils.pint(self.g.pos)
result = ([] if self.force_new_bed else
w.find_blocks_indexed(p, blocks.BED_IDS, 80))
if self.force_new_bed:
result = []
print('Skipping existing beds; placing a new bed as requested')
else:
indexed = w.find_blocks_indexed(p, blocks.BED_IDS, 80)
# Keep a tiny direct scan as a guard against stale/missing chunk
# index entries, especially around respawn and chunk-cache changes.
nearby = list(w.find_blocks_3d(
p, blocks.BED_IDS, distance=8, y_limit=8))
result = list(dict.fromkeys(nearby + indexed))
result.sort(key=lambda bed: utils.phyp(p, bed))
if nearby and not indexed:
print('Found beds by local scan; chunk index had none')
result = list(dict.fromkeys(self._bed_foot(bed) for bed in result))
self.beds = []
for bed in result:
if bed in self.bad_beds:
@@ -78,8 +121,11 @@ class SleepWithBedStates:
continue
self.area = bed
self.bed_head = self._bed_head(bed)
self.opening = navpath[-2] if len(navpath) > 1 else p
self.g.path = navpath[:-1]
self.opening_deadline = time.monotonic() + max(
8.0, len(navpath) * 0.75)
self.g.look_at = self.area
self.state = self.going_to_bed
return
@@ -87,13 +133,23 @@ class SleepWithBedStates:
print('No reachable beds; trying to place one')
self.state = self.select_bed
def _at_opening(self):
position = utils.pint(self.g.pos)
return utils.phyp(position, self.opening) <= 1.25
def going_to_bed(self):
if utils.pint(self.g.pos) == self.opening:
if self._at_opening():
print('At existing bed')
self.g.path = []
self.my_bed = False
self.g.look_at = self.area
self.wait_time = 0.5
self.state = self.turn_to_bed
elif time.monotonic() >= self.opening_deadline:
print('Could not reach bed opening', self.opening,
'; trying another bed')
self.bad_beds.append(self.area)
self.state = self.choose_bed
def turn_to_bed(self):
self.g.look_at = self.area
@@ -151,15 +207,22 @@ class SleepWithBedStates:
return
self.g.path = navpath
self.opening_deadline = time.monotonic() + max(
8.0, len(navpath) * 0.75)
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:
if self._at_opening():
print('At bed area')
self.g.path = []
self.g.look_at = self.area
self.wait_time = 0.5
self.state = self.turn_to_bed_area
elif time.monotonic() >= self.opening_deadline:
print('Could not reach bed placement opening', self.opening)
self.bad_areas.append(self.area)
self.state = self.cleanup
def turn_to_bed_area(self):
self.g.look_at = self.area
@@ -174,28 +237,38 @@ class SleepWithBedStates:
support, BlockFace.TOP,
hand=self.g.get('holding_hand', 0))
self.my_bed = True
self.wait_time = 0.5
self.bed_place_deadline = time.monotonic() + 5.0
self.state = self.wait_use
def wait_use(self):
if self.wait_time > 0:
self.wait_time -= utils.TICK
return
if self.g.world.block_at(*self.area) not in blocks.BED_IDS:
# Block updates can arrive after the use packet; poll authoritative
# chunk state for several seconds instead of giving up after 0.5s.
if self.g.world.block_at(*self.area) in blocks.BED_IDS:
self.bed_head = self._bed_head(self.area)
self.state = self.use_bed
elif time.monotonic() >= self.bed_place_deadline:
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 current_time is None or current_time < 12550:
if current_time is None:
if not getattr(self, 'waiting_for_time_update', False):
print('Waiting for world time update before using bed')
self.waiting_for_time_update = True
return
if current_time < 12550:
if not getattr(self, 'waiting_for_night', False):
print('Bed reached; waiting until sleep time (time={})'.format(
current_time))
self.waiting_for_night = True
return
self.waiting_for_night = False
self.waiting_for_time_update = False
if w.block_at(*self.area) not in blocks.BED_IDS:
print('Bed disappeared before use:', self.area)
@@ -208,8 +281,13 @@ class SleepWithBedStates:
return
print('Using bed')
self.g.game.place_block(self.area, BlockFace.TOP)
# Beds are only 9/16 block tall. Aim inside their collision shape;
# the near-top cursor used for placing a new block misses them.
self.g.game.place_block(
self.area, BlockFace.TOP, cursor_y=0.5)
self.sleep_deadline = time.monotonic() + 5.0
self.sleep_retry_at = time.monotonic() + 1.5
self.sleep_use_attempts = 1
self.sleep_confirmed = False
if not self.silent:
self.g.chat.send('zzz')
@@ -236,9 +314,19 @@ class SleepWithBedStates:
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
self.sleep_use_attempts < 2 and
time.monotonic() >= self.sleep_retry_at):
print('Bed use not confirmed on foot; trying head block',
self.bed_head)
self.g.game.place_block(
self.bed_head, BlockFace.TOP, cursor_y=0.5)
self.sleep_use_attempts += 1
self.sleep_retry_at = time.monotonic() + 1.5
elif (not self.sleep_confirmed and
time.monotonic() >= self.sleep_deadline):
print('Bed use was not confirmed')
print('Bed use was not confirmed; blacklisting', self.area)
self.bad_beds.append(self.area)
self.state = self.cleanup
def break_bed(self):
@@ -299,8 +387,11 @@ class SleepWithBedStates:
self.bad_beds = []
self.area = None
self.opening = None
self.bed_head = None
self.bad_areas = []
self.wait_time = 0
self.sleep_use_attempts = 0
self.sleep_retry_at = 0
self.force_new_bed = False
self.sleep_confirmed = False
self.sleep_deadline = 0
+7
View File
@@ -48,6 +48,13 @@ class UseItemPacket(Packet):
]
class ClientCommandPacket(Packet):
"""Client command; action 0 asks the server to respawn this player."""
id = 0x0C
packet_name = 'client command'
definition = [{'action_id': VarInt}]
def parse_incoming_private_message(message):
"""Return (sender_name, body) for common Minecraft incoming-DM forms."""
patterns = (
+8 -3
View File
@@ -13,6 +13,7 @@ from mosfet.info import mobs
class World:
def __init__(self, global_state):
self.g = global_state
self._bed_occupancy_states = {}
def block_at(self, x, y, z):
return self.g.chunks.get_block_at(x, y, z)
@@ -316,11 +317,15 @@ class World:
return result
def check_bed_occupied(self, bed):
"""Return whether a loaded bed block currently has occupied=true."""
"""Return occupancy, logging only the first observation or a change."""
bed = tuple(bed)
bid = self.g.chunks.get_block_at(*bed)
occupied = blocks.PROPS.get(bid, {}).get('occupied') == 'true'
print('Checking bed occupancy:', bed, '->',
'occupied' if occupied else 'free')
previous = self._bed_occupancy_states.get(bed)
if previous is None or previous != occupied:
print('Checking bed occupancy:', bed, '->',
'occupied' if occupied else 'free')
self._bed_occupancy_states[bed] = occupied
return occupied
def find_cache_openings(self, area):