Implement 26.3 food consumption
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
return
|
||||
|
||||
else:
|
||||
print('Don\'t need to eat, aborting')
|
||||
self.state = self.cleanup
|
||||
return
|
||||
|
||||
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
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user