Port wood gathering and fix 26.3 swing animation

This commit is contained in:
Tanner
2026-11-05 15:26:55 +00:00
parent 659fa49ea1
commit b71774b9aa
9 changed files with 208 additions and 33 deletions
+3 -2
View File
@@ -66,10 +66,11 @@ class Commands:
command = 'nosquarebrackets'
unsupported = {
'gather', 'trade', 'gapple', 'fill',
'trade', 'gapple', 'fill',
'break', 'open', 'click', 'use', 'interact',
}
if (command in unsupported or
if ((command == 'gather' and data.lower().strip() != 'wood') or
command in unsupported or
(command == 'farm' and not data.lower().startswith('crop'))):
reply = 'not migrated to Minecraft 26.3 yet'
print('Reply:', reply)
+6 -5
View File
@@ -47,11 +47,12 @@ def component_text(value):
return str(value)
def main(global_state=None):
server = os.environ.get('SERVER', '127.0.0.1')
port = int(os.environ.get('PORT', '25565'))
username = os.environ.get('PLAYER_NAME', 'mosfet1')
password = os.environ.get('GAME_PASSWORD', 'mosfetbot')
def main(global_state=None, *, server=None, port=None, username=None,
password=None):
server = server or os.environ.get('SERVER', '127.0.0.1')
port = int(port or os.environ.get('PORT', '25565'))
username = username or os.environ.get('PLAYER_NAME', 'mosfet1')
password = password or os.environ.get('GAME_PASSWORD', 'mosfetbot')
connection = Connection(server, port, username=username,
allowed_versions=['26.3'],
+20 -3
View File
@@ -8,7 +8,8 @@ from minecraft.networking.packets import Packet, clientbound, serverbound
from minecraft.networking.types import BlockFace, Byte, Short, VarInt
from minecraft.networking.modern import (
ClientTickEnd, ContainerClickSwapPacket, PlayerActionPacket,
SetCarriedItemPacket, SetHeldSlotPacket, UseItemOnBlockPacket,
PunchPacket, SetCarriedItemPacket, SetHeldSlotPacket,
UseItemOnBlockPacket,
)
from mosfet.protocol.packets import (
@@ -651,11 +652,14 @@ class LegacyGame:
# Active game facade. The constructor registers no legacy packet listeners;
# migrated actions use the 26.3 packet classes imported above.
class Game(LegacyGame):
ARM_SWING_INTERVAL = 0.1
def __init__(self, global_state):
self.g = global_state
self.sequence = 0
self.break_deadline = None
self.breaking_state = None
self.next_swing_at = 0.0
from mosfet.protocol.packets import SetHealthPacket
# Health is a protocol-777 packet, not the legacy UpdateHealthPacket.
@@ -708,6 +712,10 @@ class Game(LegacyGame):
self.sequence += 1
return self.sequence
def animate(self):
"""Ask the 26.3 server to broadcast a main-hand punch animation."""
self._send_action(PunchPacket())
def break_block(self, location):
location = tuple(int(value) for value in location)
block_id = self.g.chunks.get_block_at(*location)
@@ -720,7 +728,10 @@ class Game(LegacyGame):
self.g.breaking = location
self.breaking_state = block_id
hardness_time = utils.break_time(block_id, self.g.get('holding', 0))
self.break_deadline = time.monotonic() + max(0.05, hardness_time)
now = time.monotonic()
self.break_deadline = now + max(0.05, hardness_time)
self.animate()
self.next_swing_at = now + self.ARM_SWING_INTERVAL
return True
def respawn(self):
@@ -922,7 +933,11 @@ class Game(LegacyGame):
self._tick_drain()
if not self.g.get('breaking') or self.break_deadline is None:
return
if time.monotonic() < self.break_deadline:
now = time.monotonic()
if now < self.break_deadline:
if now >= self.next_swing_at:
self.animate()
self.next_swing_at = now + self.ARM_SWING_INTERVAL
return
packet = PlayerActionPacket(
action=3, location=self.g.breaking, face=1,
@@ -937,6 +952,7 @@ class Game(LegacyGame):
self.g.breaking = None
self.break_deadline = None
self.breaking_state = None
self.next_swing_at = 0.0
def cancel_break(self):
if not self.g.get('breaking'):
@@ -948,6 +964,7 @@ class Game(LegacyGame):
self.g.breaking = None
self.break_deadline = None
self.breaking_state = None
self.next_swing_at = 0.0
def select_item(self, item_ids):
item_ids = set(item_ids)
+1 -1
View File
@@ -79,7 +79,7 @@ SAPLINGS = sorted(name for name in MCD_BLOCKS
BEDS = sorted(name for name in MCD_BLOCKS if name.endswith('_bed'))
INDEXED = ['chest', 'trapped_chest', 'emerald_block', 'barrel'] + BEDS + [
'wheat', 'potatoes', 'carrots', 'beetroots',
]
] + LOGS
LOG_IDS = get_set(LOGS)
LEAF_IDS = get_set(LEAVES)
+3 -1
View File
@@ -265,9 +265,10 @@ class LegacyJobStates:
class JobStates(LegacyJobStates):
"""26.3 facade for crop, inventory, sleeping, and loiter workflows."""
"""26.3 facade for migrated gather-wood and crop workflows."""
def __init__(self, global_state):
self.g = global_state
self.gather_wood_states = gather_wood.GatherWoodStates(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(
@@ -311,6 +312,7 @@ class JobStates(LegacyJobStates):
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_wood_states.state = self.gather_wood_states.idle
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
+2 -11
View File
@@ -1,20 +1,11 @@
import re
"""Gather naturally generated trees using the active 26.3 world/game APIs."""
import time
import importlib
import random
from itertools import count
from math import floor
from minecraft.networking.types import BlockFace
from mosfet.protocol.managers import ChunkNotLoadedException
from mosfet import utils
from mosfet import path
from mosfet.info import blocks
from mosfet.info import items
from mosfet.info import mcdata
from mosfet.info import mobs
class GatherWoodStates:
def bair(self, p):
+13 -10
View File
@@ -81,9 +81,14 @@ class World:
return result
def find_trees(self, center, distance):
found_trees = []
for log in self.find_blocks_3d(center, blocks.LOG_IDS, distance, 15):
# crawl to the bottom log
found_trees = set()
# 26.3 block-state caches keep an index of tree logs. Searching a
# three-dimensional radius voxel-by-voxel is prohibitively expensive
# and can walk through unloaded space, so use loaded indexed states.
for candidate in self.find_blocks_indexed(
center, blocks.LOG_IDS, distance):
# Crawl to the bottom log of this trunk.
log = candidate
while self.block_at(*utils.padd(log, path.BLOCK_BELOW)) in blocks.LOG_IDS:
log = utils.padd(log, path.BLOCK_BELOW)
base = log
@@ -91,27 +96,25 @@ class World:
if base in found_trees:
continue
# make sure we are on the ground
if self.block_at(*utils.padd(base, path.BLOCK_BELOW)) in blocks.NON_SOLID_IDS:
# Only classify trees when their supporting block is known.
support = self.block_at(*utils.padd(base, path.BLOCK_BELOW))
if support is None or support in blocks.NON_SOLID_IDS:
continue
# crawl to the top log to count and check leaves
# Crawl to the top log to count trunk blocks and check for leaves.
log_count = 1
good_leaves = False
while self.block_at(*utils.padd(log, path.BLOCK_ABOVE)) in blocks.LOG_IDS:
log = utils.padd(log, path.BLOCK_ABOVE)
log_count += 1
for offset in path.CHECK_DIRECTIONS:
if self.block_at(*utils.padd(log, offset)) in blocks.LEAF_IDS:
good_leaves = True
# make sure it's a good tree
if not good_leaves or log_count < 3:
continue
found_trees.append(base)
found_trees.add(base)
yield base
def find_tree_openings(self, tree):
+78
View File
@@ -0,0 +1,78 @@
import io
import time
import unittest
from munch import Munch
from minecraft.networking.modern import (
ClientTickEnd, EntityAnimationPacket, EntitySwingAnimationPacket,
PunchPacket,
)
from mosfet.game import Game
class RecordingConnection:
def __init__(self):
self.packets = []
def write_packet(self, packet, force=False):
self.packets.append((packet, force))
class BlockBreakAnimationTests(unittest.TestCase):
def make_game(self):
connection = RecordingConnection()
game = object.__new__(Game)
game.g = Munch(connection=connection, inv={}, inventory_state_ids={})
game.sequence = 0
game.break_deadline = None
game.breaking_state = None
game.next_swing_at = 0.0
return game, connection
def test_26_3_punch_packet_is_the_empty_serverbound_packet(self):
self.assertEqual(PunchPacket.id, 0x2E)
self.assertEqual(PunchPacket.definition, [])
def test_observer_packet_decodes_entity_id_and_swing_kind(self):
packet = EntityAnimationPacket()
packet.read(io.BytesIO(bytes((0x85, 0x01, 0))))
self.assertEqual(packet.entity_id, 133)
self.assertEqual(packet.animation, 0)
def test_26_3_observer_swing_packet_decodes_animation_details(self):
packet = EntitySwingAnimationPacket()
packet.read(io.BytesIO(bytes((0x85, 0x01, 0, 1, 6))))
self.assertEqual(EntitySwingAnimationPacket.id, 0x7B)
self.assertEqual((packet.entity_id, packet.hand,
packet.animation_type, packet.duration),
(133, 0, 1, 6))
def test_animate_sends_punch_before_client_tick_end(self):
game, connection = self.make_game()
game.animate()
self.assertEqual([type(packet) for packet, _ in connection.packets],
[PunchPacket, ClientTickEnd])
self.assertTrue(all(force for _, force in connection.packets))
def test_breaking_sends_initial_and_periodic_punches(self):
game, connection = self.make_game()
game.g.breaking = (1, 2, 3)
game.break_deadline = time.monotonic() + 5
game.breaking_state = 10
game.next_swing_at = 0
game.tick()
self.assertEqual(sum(isinstance(packet, PunchPacket)
for packet, _ in connection.packets), 1)
sent = len(connection.packets)
game.tick()
self.assertEqual(len(connection.packets), sent)
game.next_swing_at = 0
game.tick()
self.assertEqual(sum(isinstance(packet, PunchPacket)
for packet, _ in connection.packets), 2)
if __name__ == '__main__':
unittest.main()
+82
View File
@@ -0,0 +1,82 @@
import unittest
from munch import Munch
from mosfet.commands import Commands
from mosfet.info import blocks, items
from mosfet.job import JobStates
from mosfet.world import World
class FakeChunks:
def __init__(self, states):
self.states = states
self.index = {}
def get_block_at(self, x, y, z):
return self.states.get((x, y, z), blocks.AIR)
class GatherWoodPortTests(unittest.TestCase):
def test_log_states_are_indexed_for_modern_chunk_search(self):
self.assertTrue(blocks.LOG_IDS)
self.assertTrue(blocks.LOG_IDS.issubset(blocks.INDEXED_IDS))
def test_tree_search_finds_indexed_26_3_log_states(self):
oak_log = blocks.MCD_BLOCKS['oak_log']['minStateId']
oak_leaves = blocks.MCD_BLOCKS['oak_leaves']['minStateId']
stone = blocks.MCD_BLOCKS['stone']['minStateId']
states = {
(0, 63, 0): stone,
(0, 64, 0): oak_log,
(0, 65, 0): oak_log,
(0, 66, 0): oak_log,
(1, 66, 0): oak_leaves,
}
chunks = FakeChunks(states)
chunks.index[oak_log] = {
(0, 64, 0), (0, 65, 0), (0, 66, 0),
}
world = World(Munch(chunks=chunks))
self.assertEqual(list(world.find_trees((0, 64, 0), 10)),
[(0, 64, 0)])
def test_gather_wood_job_keeps_existing_machine_sequence(self):
job = JobStates(Munch())
self.assertIsNotNone(job.gather_wood_states)
self.assertEqual(job.gather_wood(), [
job.gather_wood_states,
job.sleep_with_bed_states,
job.eat_food_states,
job.cache_items_states,
])
job.gather_wood_states.state = job.gather_wood_states.find_new_tree
job.stop()
self.assertIs(job.gather_wood_states.state.__func__,
job.gather_wood_states.idle.__func__)
def test_gather_wood_command_is_enabled_but_farm_wood_is_not(self):
class FakeChat:
def __init__(self):
self.sent = []
def set_handler(self, handler):
self.handler = handler
def send(self, message):
self.sent.append(message)
job = JobStates(Munch())
chat = FakeChat()
g = Munch(name='mosfet1', chat=chat, job=job,
inv={0: Munch(item_id=next(iter(items.BED_IDS)))})
Commands(g).handle_chat(('CHAT', 'tester', 'tester: 1gather wood'))
self.assertEqual(chat.sent, ['ok'])
self.assertIs(job.state.__func__, job.gather_wood.__func__)
chat.sent.clear()
Commands(g).handle_chat(('CHAT', 'tester', 'tester: 1farm wood'))
self.assertEqual(chat.sent, ['not migrated to Minecraft 26.3 yet'])
if __name__ == '__main__':
unittest.main()