Port wood gathering and fix 26.3 swing animation
This commit is contained in:
+3
-2
@@ -66,10 +66,11 @@ class Commands:
|
|||||||
command = 'nosquarebrackets'
|
command = 'nosquarebrackets'
|
||||||
|
|
||||||
unsupported = {
|
unsupported = {
|
||||||
'gather', 'trade', 'gapple', 'fill',
|
'trade', 'gapple', 'fill',
|
||||||
'break', 'open', 'click', 'use', 'interact',
|
'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'))):
|
(command == 'farm' and not data.lower().startswith('crop'))):
|
||||||
reply = 'not migrated to Minecraft 26.3 yet'
|
reply = 'not migrated to Minecraft 26.3 yet'
|
||||||
print('Reply:', reply)
|
print('Reply:', reply)
|
||||||
|
|||||||
+6
-5
@@ -47,11 +47,12 @@ def component_text(value):
|
|||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
def main(global_state=None):
|
def main(global_state=None, *, server=None, port=None, username=None,
|
||||||
server = os.environ.get('SERVER', '127.0.0.1')
|
password=None):
|
||||||
port = int(os.environ.get('PORT', '25565'))
|
server = server or os.environ.get('SERVER', '127.0.0.1')
|
||||||
username = os.environ.get('PLAYER_NAME', 'mosfet1')
|
port = int(port or os.environ.get('PORT', '25565'))
|
||||||
password = os.environ.get('GAME_PASSWORD', 'mosfetbot')
|
username = username or os.environ.get('PLAYER_NAME', 'mosfet1')
|
||||||
|
password = password or os.environ.get('GAME_PASSWORD', 'mosfetbot')
|
||||||
|
|
||||||
connection = Connection(server, port, username=username,
|
connection = Connection(server, port, username=username,
|
||||||
allowed_versions=['26.3'],
|
allowed_versions=['26.3'],
|
||||||
|
|||||||
+20
-3
@@ -8,7 +8,8 @@ from minecraft.networking.packets import Packet, clientbound, serverbound
|
|||||||
from minecraft.networking.types import BlockFace, Byte, Short, VarInt
|
from minecraft.networking.types import BlockFace, Byte, Short, VarInt
|
||||||
from minecraft.networking.modern import (
|
from minecraft.networking.modern import (
|
||||||
ClientTickEnd, ContainerClickSwapPacket, PlayerActionPacket,
|
ClientTickEnd, ContainerClickSwapPacket, PlayerActionPacket,
|
||||||
SetCarriedItemPacket, SetHeldSlotPacket, UseItemOnBlockPacket,
|
PunchPacket, SetCarriedItemPacket, SetHeldSlotPacket,
|
||||||
|
UseItemOnBlockPacket,
|
||||||
)
|
)
|
||||||
|
|
||||||
from mosfet.protocol.packets import (
|
from mosfet.protocol.packets import (
|
||||||
@@ -651,11 +652,14 @@ class LegacyGame:
|
|||||||
# Active game facade. The constructor registers no legacy packet listeners;
|
# Active game facade. The constructor registers no legacy packet listeners;
|
||||||
# migrated actions use the 26.3 packet classes imported above.
|
# migrated actions use the 26.3 packet classes imported above.
|
||||||
class Game(LegacyGame):
|
class Game(LegacyGame):
|
||||||
|
ARM_SWING_INTERVAL = 0.1
|
||||||
|
|
||||||
def __init__(self, global_state):
|
def __init__(self, global_state):
|
||||||
self.g = global_state
|
self.g = global_state
|
||||||
self.sequence = 0
|
self.sequence = 0
|
||||||
self.break_deadline = None
|
self.break_deadline = None
|
||||||
self.breaking_state = None
|
self.breaking_state = None
|
||||||
|
self.next_swing_at = 0.0
|
||||||
|
|
||||||
from mosfet.protocol.packets import SetHealthPacket
|
from mosfet.protocol.packets import SetHealthPacket
|
||||||
# Health is a protocol-777 packet, not the legacy UpdateHealthPacket.
|
# Health is a protocol-777 packet, not the legacy UpdateHealthPacket.
|
||||||
@@ -708,6 +712,10 @@ class Game(LegacyGame):
|
|||||||
self.sequence += 1
|
self.sequence += 1
|
||||||
return self.sequence
|
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):
|
def break_block(self, location):
|
||||||
location = tuple(int(value) for value in location)
|
location = tuple(int(value) for value in location)
|
||||||
block_id = self.g.chunks.get_block_at(*location)
|
block_id = self.g.chunks.get_block_at(*location)
|
||||||
@@ -720,7 +728,10 @@ class Game(LegacyGame):
|
|||||||
self.g.breaking = location
|
self.g.breaking = location
|
||||||
self.breaking_state = block_id
|
self.breaking_state = block_id
|
||||||
hardness_time = utils.break_time(block_id, self.g.get('holding', 0))
|
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
|
return True
|
||||||
|
|
||||||
def respawn(self):
|
def respawn(self):
|
||||||
@@ -922,7 +933,11 @@ class Game(LegacyGame):
|
|||||||
self._tick_drain()
|
self._tick_drain()
|
||||||
if not self.g.get('breaking') or self.break_deadline is None:
|
if not self.g.get('breaking') or self.break_deadline is None:
|
||||||
return
|
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
|
return
|
||||||
packet = PlayerActionPacket(
|
packet = PlayerActionPacket(
|
||||||
action=3, location=self.g.breaking, face=1,
|
action=3, location=self.g.breaking, face=1,
|
||||||
@@ -937,6 +952,7 @@ class Game(LegacyGame):
|
|||||||
self.g.breaking = None
|
self.g.breaking = None
|
||||||
self.break_deadline = None
|
self.break_deadline = None
|
||||||
self.breaking_state = None
|
self.breaking_state = None
|
||||||
|
self.next_swing_at = 0.0
|
||||||
|
|
||||||
def cancel_break(self):
|
def cancel_break(self):
|
||||||
if not self.g.get('breaking'):
|
if not self.g.get('breaking'):
|
||||||
@@ -948,6 +964,7 @@ class Game(LegacyGame):
|
|||||||
self.g.breaking = None
|
self.g.breaking = None
|
||||||
self.break_deadline = None
|
self.break_deadline = None
|
||||||
self.breaking_state = None
|
self.breaking_state = None
|
||||||
|
self.next_swing_at = 0.0
|
||||||
|
|
||||||
def select_item(self, item_ids):
|
def select_item(self, item_ids):
|
||||||
item_ids = set(item_ids)
|
item_ids = set(item_ids)
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ SAPLINGS = sorted(name for name in MCD_BLOCKS
|
|||||||
BEDS = sorted(name for name in MCD_BLOCKS if name.endswith('_bed'))
|
BEDS = sorted(name for name in MCD_BLOCKS if name.endswith('_bed'))
|
||||||
INDEXED = ['chest', 'trapped_chest', 'emerald_block', 'barrel'] + BEDS + [
|
INDEXED = ['chest', 'trapped_chest', 'emerald_block', 'barrel'] + BEDS + [
|
||||||
'wheat', 'potatoes', 'carrots', 'beetroots',
|
'wheat', 'potatoes', 'carrots', 'beetroots',
|
||||||
]
|
] + LOGS
|
||||||
|
|
||||||
LOG_IDS = get_set(LOGS)
|
LOG_IDS = get_set(LOGS)
|
||||||
LEAF_IDS = get_set(LEAVES)
|
LEAF_IDS = get_set(LEAVES)
|
||||||
|
|||||||
+3
-1
@@ -265,9 +265,10 @@ class LegacyJobStates:
|
|||||||
|
|
||||||
|
|
||||||
class JobStates(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):
|
def __init__(self, global_state):
|
||||||
self.g = 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.gather_crop_states = gather_crop.GatherCropStates(global_state)
|
||||||
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(
|
||||||
@@ -311,6 +312,7 @@ class JobStates(LegacyJobStates):
|
|||||||
self.g.look_at = None
|
self.g.look_at = None
|
||||||
if self.sleep_with_bed_states.state == self.sleep_with_bed_states.sleep_bed:
|
if self.sleep_with_bed_states.state == self.sleep_with_bed_states.sleep_bed:
|
||||||
self.g.game.leave_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.gather_crop_states.state = self.gather_crop_states.idle
|
||||||
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
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import re
|
"""Gather naturally generated trees using the active 26.3 world/game APIs."""
|
||||||
import time
|
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 utils
|
||||||
from mosfet import path
|
from mosfet import path
|
||||||
from mosfet.info import blocks
|
from mosfet.info import blocks
|
||||||
from mosfet.info import items
|
from mosfet.info import items
|
||||||
from mosfet.info import mcdata
|
|
||||||
from mosfet.info import mobs
|
|
||||||
|
|
||||||
class GatherWoodStates:
|
class GatherWoodStates:
|
||||||
def bair(self, p):
|
def bair(self, p):
|
||||||
|
|||||||
+13
-10
@@ -81,9 +81,14 @@ class World:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def find_trees(self, center, distance):
|
def find_trees(self, center, distance):
|
||||||
found_trees = []
|
found_trees = set()
|
||||||
for log in self.find_blocks_3d(center, blocks.LOG_IDS, distance, 15):
|
# 26.3 block-state caches keep an index of tree logs. Searching a
|
||||||
# crawl to the bottom log
|
# 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:
|
while self.block_at(*utils.padd(log, path.BLOCK_BELOW)) in blocks.LOG_IDS:
|
||||||
log = utils.padd(log, path.BLOCK_BELOW)
|
log = utils.padd(log, path.BLOCK_BELOW)
|
||||||
base = log
|
base = log
|
||||||
@@ -91,27 +96,25 @@ class World:
|
|||||||
if base in found_trees:
|
if base in found_trees:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# make sure we are on the ground
|
# Only classify trees when their supporting block is known.
|
||||||
if self.block_at(*utils.padd(base, path.BLOCK_BELOW)) in blocks.NON_SOLID_IDS:
|
support = self.block_at(*utils.padd(base, path.BLOCK_BELOW))
|
||||||
|
if support is None or support in blocks.NON_SOLID_IDS:
|
||||||
continue
|
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
|
log_count = 1
|
||||||
good_leaves = False
|
good_leaves = False
|
||||||
while self.block_at(*utils.padd(log, path.BLOCK_ABOVE)) in blocks.LOG_IDS:
|
while self.block_at(*utils.padd(log, path.BLOCK_ABOVE)) in blocks.LOG_IDS:
|
||||||
log = utils.padd(log, path.BLOCK_ABOVE)
|
log = utils.padd(log, path.BLOCK_ABOVE)
|
||||||
log_count += 1
|
log_count += 1
|
||||||
|
|
||||||
for offset in path.CHECK_DIRECTIONS:
|
for offset in path.CHECK_DIRECTIONS:
|
||||||
if self.block_at(*utils.padd(log, offset)) in blocks.LEAF_IDS:
|
if self.block_at(*utils.padd(log, offset)) in blocks.LEAF_IDS:
|
||||||
good_leaves = True
|
good_leaves = True
|
||||||
|
|
||||||
# make sure it's a good tree
|
|
||||||
if not good_leaves or log_count < 3:
|
if not good_leaves or log_count < 3:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
found_trees.append(base)
|
found_trees.add(base)
|
||||||
|
|
||||||
yield base
|
yield base
|
||||||
|
|
||||||
def find_tree_openings(self, tree):
|
def find_tree_openings(self, tree):
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user