Implement 26.3 food consumption

This commit is contained in:
Tanner
2026-11-04 20:06:02 +00:00
parent 1ecec547ae
commit 7d4f045100
8 changed files with 200 additions and 25 deletions
+4 -4
View File
@@ -51,8 +51,8 @@ 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), 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
also run by crop farming at night), food use (`1eat`, also run as needed by
crop farming), 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
@@ -78,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`, `1sleep`,
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`, `1eat`,
`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
+5
View File
@@ -404,6 +404,11 @@ class Commands:
self.g.job.state = self.g.job.sleep
reply = 'ok'
## 1eat - eat one food item when hunger/health requires it
if command == 'eat':
self.g.job.state = self.g.job.eat
reply = 'ok'
## 1gather wood - gathers wood from the world
## 1gather sand - gathers sand from the world
if command == 'gather' and data:
+9 -1
View File
@@ -23,7 +23,9 @@ from mosfet.protocol.packets import (
)
from mosfet.protocol.types import Slot
from mosfet.protocol.modern import PlayerCommandPacket, UpdateTimePacket
from mosfet.protocol.modern import (
PlayerCommandPacket, UpdateTimePacket, UseItemPacket,
)
from mosfet import utils
from mosfet import path
@@ -713,6 +715,12 @@ class Game(LegacyGame):
self._send_action(PlayerCommandPacket(
entity_id=self.g.eid, action_id=0, jump_boost=0))
def use_item(self, hand=0):
self._send_action(UseItemPacket(
hand=hand, sequence=self._next_sequence(),
yaw=float(self.g.get('yaw', 0.0)),
pitch=float(self.g.get('pitch', 0.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)]
+6 -1
View File
@@ -272,6 +272,7 @@ class JobStates(LegacyJobStates):
self.cache_items_states = cache_items.CacheItemsStates(global_state)
self.sleep_with_bed_states = sleep_with_bed.SleepWithBedStates(
global_state)
self.eat_food_states = eat_food.EatFoodStates(global_state)
self.state = self.idle
def farm_crop(self):
@@ -285,7 +286,10 @@ class JobStates(LegacyJobStates):
items.BEETROOT_SEEDS_ID,
})
return [self.gather_crop_states, self.sleep_with_bed_states,
self.cache_items_states]
self.eat_food_states, self.cache_items_states]
def eat(self):
return [self.eat_food_states]
def sleep(self):
self.sleep_with_bed_states.silent = False
@@ -304,5 +308,6 @@ class JobStates(LegacyJobStates):
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.eat_food_states.state = self.eat_food_states.idle
self.gather_crop_states.active_section_anchor = None
self.state = self.idle
+63 -18
View File
@@ -21,41 +21,86 @@ class EatFoodStates:
return None
def init(self):
if self.g.food < 12:
food = self.g.get('food')
health = self.g.get('health')
if food is None:
print('No authoritative hunger status yet; not eating')
self.state = self.cleanup
return
if food < 12:
print('Hungry, eating')
self.state = self.select_food
return
if self.g.health < 20 and self.g.food < 18:
elif health is not None and health < 20 and food < 18:
print('Low health, eating')
self.state = self.select_food
else:
print('Don\'t need to eat, aborting')
self.state = self.cleanup
return
print('Don\'t need to eat, aborting')
self.state = self.cleanup
self.food_before = food
self.saturation_before = self.g.get('food_saturation', 0.0) or 0.0
self.health_before = health
self.selected_item_id = None
self.selected_slot = None
self.selected_count = 0
self.state = self.select_food
def select_food(self):
if self.g.game.select_item(items.FOOD_IDS):
self.state = self.eat_food
else:
if not self.g.game.select_item(items.FOOD_IDS):
print('No food, aborting')
self.state = self.cleanup
return
self.selected_item_id = self.g.holding
self.selected_slot = (
45 if self.g.get('holding_hand', 0) == 1 else
36 + self.g.get('holding_slot', 0))
self.select_deadline = time.monotonic() + 2.0
self.state = self.wait_for_selection
def wait_for_selection(self):
selected = self.g.inv.get(self.selected_slot)
if (selected and selected.present and
selected.item_id == self.selected_item_id):
self.selected_count = selected.item_count
self.state = self.eat_food
elif time.monotonic() >= self.select_deadline:
print('Food did not reach the selected hand; aborting')
self.state = self.cleanup
def eat_food(self):
self.g.game.use_item(0)
print('Eating food')
self.wait_time = 3
hand = self.g.get('holding_hand', 0)
print('Eating food from {} hand'.format(
'off' if hand else 'main'))
self.g.game.use_item(hand)
self.eat_deadline = time.monotonic() + 6.0
self.state = self.wait
def wait(self):
if self.wait_time > 0:
self.wait_time -= utils.TICK
else:
selected = self.g.inv.get(self.selected_slot)
consumed = (
not selected or not selected.present or
selected.item_id != self.selected_item_id or
selected.item_count < self.selected_count)
stats_improved = (
(self.g.get('food') is not None and
self.g.food > self.food_before) or
(self.g.get('food_saturation') is not None and
self.g.food_saturation > self.saturation_before) or
(self.health_before is not None and self.g.get('health') is not None
and self.g.health > self.health_before))
if consumed or stats_improved:
print('Eating confirmed by server state')
self.state = self.cleanup
elif time.monotonic() >= self.eat_deadline:
print('Food use was not confirmed by server state')
self.state = self.cleanup
def cleanup(self):
self.state = self.done
if (self.g.get('job') and
getattr(self.g.job.state, '__name__', None) == 'eat'):
self.g.job.state = self.g.job.idle
def done(self):
# never gets ran, placeholder
+10
View File
@@ -38,6 +38,16 @@ class PlayerCommandPacket(Packet):
]
class UseItemPacket(Packet):
"""Use the selected hand, including the 26.3 interaction sequence."""
id = 0x43
packet_name = 'use item'
definition = [
{'hand': VarInt}, {'sequence': VarInt},
{'yaw': Float}, {'pitch': Float},
]
def parse_incoming_private_message(message):
"""Return (sender_name, body) for common Minecraft incoming-DM forms."""
patterns = (
+4 -1
View File
@@ -12,14 +12,17 @@ class TestCropCaching(unittest.TestCase):
crop_state = object()
cache_state = Munch(silent=False)
sleep_state = Munch(silent=False)
eat_state = object()
jobs = JobStates.__new__(JobStates)
jobs.gather_crop_states = crop_state
jobs.cache_items_states = cache_state
jobs.sleep_with_bed_states = sleep_state
jobs.eat_food_states = eat_state
try:
machines = JobStates.farm_crop(jobs)
self.assertEqual(machines, [crop_state, sleep_state, cache_state])
self.assertEqual(
machines, [crop_state, sleep_state, eat_state, cache_state])
self.assertTrue(cache_state.silent)
self.assertTrue(sleep_state.silent)
self.assertTrue({
+99
View File
@@ -0,0 +1,99 @@
import unittest
from munch import Munch
from minecraft.networking.modern import ClientTickEnd
from minecraft.networking.packets import PacketBuffer
from mosfet.game import Game
from mosfet.info import items
from mosfet.jobs.eat_food import EatFoodStates
from mosfet.protocol.modern import UseItemPacket
from mosfet.protocol.types import Slot
class FakeConnection:
def __init__(self):
self.packets = []
def write_packet(self, packet, force=False):
self.packets.append((packet, force))
class FakeGame:
def __init__(self, state):
self.state = state
self.used_hands = []
def select_item(self, item_ids):
for slot, item in self.state.g.inv.items():
if item.present and item.item_id in item_ids:
self.state.g.holding = item.item_id
self.state.g.holding_hand = 0
self.state.g.holding_slot = slot - 36
return True
return False
def use_item(self, hand=0):
self.used_hands.append(hand)
class TestEatFood(unittest.TestCase):
def test_use_item_packet_encodes_hand_sequence_and_rotation(self):
packet = UseItemPacket(hand=1, sequence=300, yaw=90.0, pitch=-30.0)
buffer = PacketBuffer()
packet.write_fields(buffer)
self.assertEqual(packet.id, 0x43)
self.assertEqual(buffer.get_writable(), bytes.fromhex(
'01ac0242b40000c1f00000'))
def test_game_use_item_sends_packet_and_tick_end(self):
connection = FakeConnection()
game = Game.__new__(Game)
game.g = Munch(connection=connection, yaw=45.0, pitch=-5.0)
game.sequence = 0
game.use_item(1)
packet, forced = connection.packets[0]
self.assertIsInstance(packet, UseItemPacket)
self.assertEqual((packet.hand, packet.sequence,
packet.yaw, packet.pitch), (1, 1, 45.0, -5.0))
self.assertTrue(forced)
self.assertIsInstance(connection.packets[1][0], ClientTickEnd)
def test_eat_uses_authoritative_hunger_and_waits_for_confirmation(self):
food_id = next(iter(items.FOOD_IDS))
state = EatFoodStates(Munch(
food=8, health=20.0, food_saturation=0.0,
inv={36: Slot(True, food_id, 3)}, holding_hand=0,
holding_slot=0, job=None))
fake_game = FakeGame(state)
state.g.game = fake_game
state.init()
self.assertEqual(state.state, state.select_food)
state.select_food()
self.assertEqual(state.state, state.wait_for_selection)
state.wait_for_selection()
self.assertEqual(state.state, state.eat_food)
state.eat_food()
self.assertEqual(fake_game.used_hands, [0])
self.assertEqual(state.state, state.wait)
# Don't declare success until the server sends a hunger/saturation
# update or confirms that it consumed the selected stack.
state.wait()
self.assertEqual(state.state, state.wait)
state.g.food = 16
state.g.food_saturation = 4.0
state.wait()
self.assertEqual(state.state, state.cleanup)
def test_no_hunger_packet_does_not_eat(self):
state = EatFoodStates(Munch(food=None, health=None))
state.init()
self.assertEqual(state.state, state.cleanup)
if __name__ == '__main__':
unittest.main()