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`),
|
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), bed sleeping (`1sleep`, or `1sleep new` to place a bed;
|
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
|
also run by crop farming at night), food use (`1eat`, also run as needed by
|
||||||
(`1farm crops`). Other
|
crop farming), 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
|
||||||
@@ -78,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`, `1sleep`,
|
`!info x y z`, `!inv` (basic slot/count data), `1cache`, `1drain`, `1eat`,
|
||||||
addressed path movement such as `1goto x y z`, and `1farm crops`. The command catalog below is retained from
|
`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
|
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
|
||||||
|
|||||||
@@ -404,6 +404,11 @@ class Commands:
|
|||||||
self.g.job.state = self.g.job.sleep
|
self.g.job.state = self.g.job.sleep
|
||||||
reply = 'ok'
|
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 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:
|
||||||
|
|||||||
+9
-1
@@ -23,7 +23,9 @@ 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.protocol.modern import (
|
||||||
|
PlayerCommandPacket, UpdateTimePacket, UseItemPacket,
|
||||||
|
)
|
||||||
|
|
||||||
from mosfet import utils
|
from mosfet import utils
|
||||||
from mosfet import path
|
from mosfet import path
|
||||||
@@ -713,6 +715,12 @@ class Game(LegacyGame):
|
|||||||
self._send_action(PlayerCommandPacket(
|
self._send_action(PlayerCommandPacket(
|
||||||
entity_id=self.g.eid, action_id=0, jump_boost=0))
|
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):
|
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)]
|
||||||
|
|||||||
+6
-1
@@ -272,6 +272,7 @@ class JobStates(LegacyJobStates):
|
|||||||
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(
|
self.sleep_with_bed_states = sleep_with_bed.SleepWithBedStates(
|
||||||
global_state)
|
global_state)
|
||||||
|
self.eat_food_states = eat_food.EatFoodStates(global_state)
|
||||||
self.state = self.idle
|
self.state = self.idle
|
||||||
|
|
||||||
def farm_crop(self):
|
def farm_crop(self):
|
||||||
@@ -285,7 +286,10 @@ class JobStates(LegacyJobStates):
|
|||||||
items.BEETROOT_SEEDS_ID,
|
items.BEETROOT_SEEDS_ID,
|
||||||
})
|
})
|
||||||
return [self.gather_crop_states, self.sleep_with_bed_states,
|
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):
|
def sleep(self):
|
||||||
self.sleep_with_bed_states.silent = False
|
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.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.state = self.sleep_with_bed_states.idle
|
||||||
self.sleep_with_bed_states.force_new_bed = False
|
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.gather_crop_states.active_section_anchor = None
|
||||||
self.state = self.idle
|
self.state = self.idle
|
||||||
|
|||||||
+63
-18
@@ -21,41 +21,86 @@ class EatFoodStates:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def init(self):
|
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')
|
print('Hungry, eating')
|
||||||
self.state = self.select_food
|
elif health is not None and health < 20 and food < 18:
|
||||||
return
|
|
||||||
|
|
||||||
if self.g.health < 20 and self.g.food < 18:
|
|
||||||
print('Low health, eating')
|
print('Low health, eating')
|
||||||
self.state = self.select_food
|
else:
|
||||||
|
print('Don\'t need to eat, aborting')
|
||||||
|
self.state = self.cleanup
|
||||||
return
|
return
|
||||||
|
|
||||||
print('Don\'t need to eat, aborting')
|
self.food_before = food
|
||||||
self.state = self.cleanup
|
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):
|
def select_food(self):
|
||||||
if self.g.game.select_item(items.FOOD_IDS):
|
if not self.g.game.select_item(items.FOOD_IDS):
|
||||||
self.state = self.eat_food
|
|
||||||
else:
|
|
||||||
print('No food, aborting')
|
print('No food, aborting')
|
||||||
self.state = self.cleanup
|
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):
|
def eat_food(self):
|
||||||
self.g.game.use_item(0)
|
hand = self.g.get('holding_hand', 0)
|
||||||
|
print('Eating food from {} hand'.format(
|
||||||
print('Eating food')
|
'off' if hand else 'main'))
|
||||||
self.wait_time = 3
|
self.g.game.use_item(hand)
|
||||||
|
self.eat_deadline = time.monotonic() + 6.0
|
||||||
self.state = self.wait
|
self.state = self.wait
|
||||||
|
|
||||||
def wait(self):
|
def wait(self):
|
||||||
if self.wait_time > 0:
|
selected = self.g.inv.get(self.selected_slot)
|
||||||
self.wait_time -= utils.TICK
|
consumed = (
|
||||||
else:
|
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
|
self.state = self.cleanup
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
self.state = self.done
|
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):
|
def done(self):
|
||||||
# never gets ran, placeholder
|
# 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):
|
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 = (
|
||||||
|
|||||||
@@ -12,14 +12,17 @@ class TestCropCaching(unittest.TestCase):
|
|||||||
crop_state = object()
|
crop_state = object()
|
||||||
cache_state = Munch(silent=False)
|
cache_state = Munch(silent=False)
|
||||||
sleep_state = Munch(silent=False)
|
sleep_state = Munch(silent=False)
|
||||||
|
eat_state = object()
|
||||||
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
|
jobs.sleep_with_bed_states = sleep_state
|
||||||
|
jobs.eat_food_states = eat_state
|
||||||
|
|
||||||
try:
|
try:
|
||||||
machines = JobStates.farm_crop(jobs)
|
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(cache_state.silent)
|
||||||
self.assertTrue(sleep_state.silent)
|
self.assertTrue(sleep_state.silent)
|
||||||
self.assertTrue({
|
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