Consolidate bot runtime on Minecraft 26.3
This commit is contained in:
@@ -1,77 +0,0 @@
|
||||
"""Safe, movement-only bridge from legacy navigation to protocol 777."""
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
|
||||
def start(global_state):
|
||||
"""Start the legacy path-following physics, without legacy job/game packets."""
|
||||
g = global_state
|
||||
g.protocol_version = 777
|
||||
g.time = 0.0
|
||||
g.path = []
|
||||
g.look_at = None
|
||||
g.y_v = 0.0
|
||||
g.y_a = 0.0
|
||||
g.crawling = False
|
||||
g.afk_timeout = 0.0
|
||||
g.objects = g.get('objects') or {}
|
||||
g.players = g.get('players') or {}
|
||||
g.breaking = None
|
||||
g.break_time = 0.0
|
||||
g.correction_count = g.get('correction_count', 0)
|
||||
g.yaw = float(g.get('yaw', 0.0) or 0.0)
|
||||
g.pitch = float(g.get('pitch', 0.0) or 0.0)
|
||||
g.chunks.loading = False
|
||||
# Local navigation only needs nearby chunks; waiting for the entire view
|
||||
# radius can stall on servers that stream chunks gradually.
|
||||
g.physics_render_distance = 2
|
||||
g.movement_paused = False
|
||||
g.movement_status = 'idle'
|
||||
g.movement_error = None
|
||||
# Inventory, interaction, and job packets remain disabled until migrated.
|
||||
g.job = None
|
||||
g.game = None
|
||||
|
||||
def run():
|
||||
from mosfet import bot, utils
|
||||
loaded_since = None
|
||||
|
||||
while (g.get('running', True) and g.get('connection') and
|
||||
g.connection.connected):
|
||||
started = time.monotonic()
|
||||
now = time.monotonic()
|
||||
if g.chunks.check_loaded(g.physics_render_distance):
|
||||
if loaded_since is None:
|
||||
loaded_since = now
|
||||
else:
|
||||
loaded_since = None
|
||||
# Let the initial position correction and first chunk batch settle
|
||||
# before allowing any client-originated movement.
|
||||
movement_ready = loaded_since is not None and now - loaded_since >= 0.5
|
||||
if g.get('movement_paused'):
|
||||
g.movement_status = 'paused'
|
||||
elif g.path and not movement_ready:
|
||||
g.movement_status = 'waiting_for_chunks'
|
||||
elif g.path and movement_ready:
|
||||
g.movement_status = 'following_path'
|
||||
try:
|
||||
bot.tick(g)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
g.movement_error = traceback.format_exc()
|
||||
g.path = []
|
||||
g.movement_status = 'error'
|
||||
print('[26.3 movement] stopped after navigation error',
|
||||
flush=True)
|
||||
return
|
||||
elif not g.path and not g.get('movement_paused'):
|
||||
g.movement_status = 'idle'
|
||||
delay = max(0.0, utils.TICK - (time.monotonic() - started))
|
||||
time.sleep(delay)
|
||||
|
||||
worker = threading.Thread(target=run, name='mosfet-26.3-movement',
|
||||
daemon=True)
|
||||
g.movement_thread = worker
|
||||
worker.start()
|
||||
return worker
|
||||
+133
-27
@@ -6,6 +6,8 @@ import os
|
||||
import sys
|
||||
import time
|
||||
import importlib
|
||||
import threading
|
||||
import traceback
|
||||
from math import floor, ceil
|
||||
from copy import copy
|
||||
|
||||
@@ -68,26 +70,51 @@ def tick(global_state):
|
||||
required_distance = getattr(g, 'physics_render_distance',
|
||||
g.info.render_distance)
|
||||
if not g.chunks.check_loaded(required_distance):
|
||||
if g.get('movement_status') != 'waiting_for_chunks':
|
||||
print('[movement] status=waiting_for_chunks near {}'.format(
|
||||
utils.pint(p)), flush=True)
|
||||
g.movement_status = 'waiting_for_chunks'
|
||||
if not g.chunks.loading:
|
||||
print('Loading chunks', end='', flush=True)
|
||||
g.chunks.loading = time.time()
|
||||
if getattr(g, 'protocol_version', 0) == 777:
|
||||
from minecraft.networking.modern import PlayerPositionRotation
|
||||
packet = PlayerPositionRotation(
|
||||
x=p.x, y=p.y, z=p.z, pitch=0, yaw=0, on_ground=True)
|
||||
else:
|
||||
packet = serverbound.play.PositionAndLookPacket(
|
||||
x=p.x, feet_y=p.y, z=p.z, pitch=0, yaw=0, on_ground=True)
|
||||
now = time.time()
|
||||
if now - g.get('chunk_wait_report_at', 0) >= 5:
|
||||
center = g.chunks.center
|
||||
if center is None:
|
||||
missing = 'cache center not received'
|
||||
else:
|
||||
cx, cz = center
|
||||
missing = [
|
||||
(x, z)
|
||||
for x in range(cx - required_distance,
|
||||
cx + required_distance + 1)
|
||||
for z in range(cz - required_distance,
|
||||
cz + required_distance + 1)
|
||||
if (x, z) not in g.chunks.chunks
|
||||
]
|
||||
print('\n[movement] chunk wait: pos={}, center={}, '
|
||||
'radius={}, loaded={}, missing={}'.format(
|
||||
utils.pint(p), center, g.chunks.radius,
|
||||
len(g.chunks.chunks), missing), flush=True)
|
||||
g.chunk_wait_report_at = now
|
||||
from minecraft.networking.modern import (
|
||||
ClientTickEnd, PlayerPositionRotation,
|
||||
)
|
||||
packet = PlayerPositionRotation(
|
||||
x=p.x, y=p.y, z=p.z, pitch=0, yaw=0, on_ground=True)
|
||||
g.connection.write_packet(packet, force=True)
|
||||
if getattr(g, 'protocol_version', 0) == 777:
|
||||
from minecraft.networking.modern import ClientTickEnd
|
||||
g.connection.write_packet(ClientTickEnd(), force=True)
|
||||
g.connection.write_packet(ClientTickEnd(), force=True)
|
||||
return
|
||||
else:
|
||||
if g.chunks.loading:
|
||||
print()
|
||||
print('Chunks loaded in', round(time.time() - g.chunks.loading, 2), 's')
|
||||
g.chunks.loading = False
|
||||
g.chunk_wait_report_at = 0
|
||||
status = 'following_path' if g.path else 'running_job'
|
||||
if g.get('movement_status') != status:
|
||||
print('[movement] status={}'.format(status), flush=True)
|
||||
g.movement_status = status
|
||||
|
||||
########## object physics ##########
|
||||
# note: it's possible the chunk data is out of date when this runs
|
||||
@@ -208,23 +235,15 @@ def tick(global_state):
|
||||
target_pitch_d = 0 - g.pitch
|
||||
g.pitch += utils.cap(target_pitch_d, 10)
|
||||
|
||||
if getattr(g, 'protocol_version', 0) == 777:
|
||||
from minecraft.networking.modern import PlayerPositionRotation
|
||||
packet = PlayerPositionRotation(
|
||||
x=p.x, y=p.y, z=p.z, pitch=g.pitch, yaw=g.yaw,
|
||||
on_ground=(not in_air))
|
||||
else:
|
||||
packet = serverbound.play.PositionAndLookPacket(
|
||||
x=p.x, feet_y=p.y, z=p.z, pitch=g.pitch, yaw=g.yaw,
|
||||
on_ground=(not in_air))
|
||||
if getattr(g, 'protocol_version', 0) == 777:
|
||||
from minecraft.networking.modern import ClientTickEnd
|
||||
# Keep the client-tick boundary after its movement packet on the wire;
|
||||
# force=True on only the latter could overtake the queued position.
|
||||
g.connection.write_packet(packet, force=True)
|
||||
g.connection.write_packet(ClientTickEnd(), force=True)
|
||||
else:
|
||||
g.connection.write_packet(packet)
|
||||
from minecraft.networking.modern import (
|
||||
ClientTickEnd, PlayerPositionRotation,
|
||||
)
|
||||
packet = PlayerPositionRotation(
|
||||
x=p.x, y=p.y, z=p.z, pitch=g.pitch, yaw=g.yaw,
|
||||
on_ground=(not in_air))
|
||||
# Keep the client-tick boundary after its movement packet on the wire.
|
||||
g.connection.write_packet(packet, force=True)
|
||||
g.connection.write_packet(ClientTickEnd(), force=True)
|
||||
|
||||
if g.job:
|
||||
g.job.tick()
|
||||
@@ -352,3 +371,90 @@ def bot(global_state):
|
||||
g.connection.early_outgoing_packet_listeners = []
|
||||
|
||||
print('Bot module loaded.')
|
||||
|
||||
|
||||
def start_automation(global_state):
|
||||
"""Start migrated path-following and crop automation only."""
|
||||
g = global_state
|
||||
g.protocol_version = 777
|
||||
g.time = 0.0
|
||||
g.path = []
|
||||
g.look_at = None
|
||||
g.y_v = 0.0
|
||||
g.y_a = 0.0
|
||||
g.crawling = False
|
||||
g.afk_timeout = 0.0
|
||||
g.objects = g.get('objects') or {}
|
||||
g.players = g.get('players') or {}
|
||||
g.breaking = None
|
||||
g.break_time = 0.0
|
||||
g.correction_count = g.get('correction_count', 0)
|
||||
g.yaw = float(g.get('yaw', 0.0) or 0.0)
|
||||
g.pitch = float(g.get('pitch', 0.0) or 0.0)
|
||||
g.chunks.loading = False
|
||||
# Local navigation only needs nearby chunks; waiting for the entire view
|
||||
# radius can stall on servers that stream chunks gradually.
|
||||
g.physics_render_distance = 2
|
||||
g.movement_paused = False
|
||||
g.movement_status = 'idle'
|
||||
g.movement_error = None
|
||||
# Keep the port constrained to crop harvesting; other legacy jobs and
|
||||
# general inventory/window mutations are not exposed through this adapter.
|
||||
from mosfet.game import Game
|
||||
from mosfet.job import JobStates
|
||||
g.game = Game(g)
|
||||
g.job = JobStates(g)
|
||||
|
||||
def run():
|
||||
from mosfet import bot, utils
|
||||
loaded_since = None
|
||||
initial_world_ready = False
|
||||
|
||||
def set_status(status):
|
||||
if g.movement_status != status:
|
||||
print('[movement] status={}'.format(status), flush=True)
|
||||
g.movement_status = status
|
||||
|
||||
while (g.get('running', True) and g.get('connection') and
|
||||
g.connection.connected):
|
||||
started = time.monotonic()
|
||||
now = time.monotonic()
|
||||
if not initial_world_ready:
|
||||
if g.chunks.check_loaded(g.physics_render_distance):
|
||||
if loaded_since is None:
|
||||
loaded_since = now
|
||||
elif now - loaded_since >= 0.5:
|
||||
initial_world_ready = True
|
||||
else:
|
||||
loaded_since = None
|
||||
# Wait once for the initial position/chunk batch. After that,
|
||||
# bot.tick() keeps requesting chunks at cache boundaries instead
|
||||
# of gating itself forever on a momentary missing column.
|
||||
movement_ready = initial_world_ready
|
||||
job_active = (g.job is not None and g.job.state != g.job.idle)
|
||||
has_work = bool(g.path) or job_active or bool(g.breaking)
|
||||
if g.get('movement_paused'):
|
||||
set_status('paused')
|
||||
elif has_work and not movement_ready:
|
||||
set_status('waiting_for_chunks')
|
||||
elif has_work and movement_ready:
|
||||
try:
|
||||
bot.tick(g)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
g.movement_error = traceback.format_exc()
|
||||
g.path = []
|
||||
g.movement_status = 'error'
|
||||
print('[movement] stopped after navigation error',
|
||||
flush=True)
|
||||
return
|
||||
elif not has_work and not g.get('movement_paused'):
|
||||
set_status('idle')
|
||||
delay = max(0.0, utils.TICK - (time.monotonic() - started))
|
||||
time.sleep(delay)
|
||||
|
||||
worker = threading.Thread(target=run, name='mosfet-movement',
|
||||
daemon=True)
|
||||
g.movement_thread = worker
|
||||
worker.start()
|
||||
return worker
|
||||
|
||||
+22
-16
@@ -10,7 +10,6 @@ from mosfet.protocol.types import Slot
|
||||
from mosfet import print_help
|
||||
from mosfet import utils
|
||||
from mosfet import path
|
||||
from mosfet import bot
|
||||
from mosfet.info import blocks
|
||||
from mosfet.info import items
|
||||
from mosfet.info import mobs
|
||||
@@ -56,6 +55,20 @@ class Commands:
|
||||
if data.startswith('[') and data.endswith(']'):
|
||||
command = 'nosquarebrackets'
|
||||
|
||||
unsupported = {
|
||||
'gather', 'loiter', 'trade', 'gapple', 'cache', 'fill',
|
||||
'drop', 'dump', 'drain', 'break', 'open', 'click', 'use',
|
||||
'interact', 'respawn',
|
||||
}
|
||||
if (command in unsupported or
|
||||
(command == 'farm' and not data.lower().startswith('crop'))):
|
||||
reply = 'not migrated to Minecraft 26.3 yet'
|
||||
print('Reply:', reply)
|
||||
if private:
|
||||
self.g.chat.send('/r ' + reply)
|
||||
else:
|
||||
self.g.chat.send(reply)
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
@@ -406,15 +419,12 @@ class Commands:
|
||||
self.g.job.state = self.g.job.farm_wart
|
||||
reply = 'ok'
|
||||
elif data.startswith('crop'):
|
||||
self.g.job.state = self.g.job.farm_crop
|
||||
reply = 'ok'
|
||||
|
||||
if reply and self.g.dimension == 'overworld':
|
||||
for i in self.g.inv.values():
|
||||
if i.item_id in items.BED_IDS:
|
||||
break
|
||||
mode = self.g.get('game_mode')
|
||||
if mode is not None and mode != 0:
|
||||
reply = 'crop farming needs Survival mode'
|
||||
else:
|
||||
reply += ', I need a bed'
|
||||
self.g.job.state = self.g.job.farm_crop
|
||||
reply = 'ok'
|
||||
|
||||
## 1loiter - stands still but eats, sleeps, and flees
|
||||
if command == 'loiter':
|
||||
@@ -428,8 +438,8 @@ class Commands:
|
||||
|
||||
## 1stop - stops the current job and resets bot
|
||||
if command == 'stop':
|
||||
self.g.job.stop()
|
||||
self.g.game.close_window()
|
||||
bot.init(self.g)
|
||||
reply = 'ok'
|
||||
|
||||
## 1drop - drops the current stack its holding
|
||||
@@ -515,14 +525,12 @@ class Commands:
|
||||
navpath = self.g.world.path_to_place(pos, goal)
|
||||
|
||||
if navpath:
|
||||
self.g.path = navpath
|
||||
if self.g.job:
|
||||
self.g.job.stop()
|
||||
self.g.path = navpath
|
||||
print(len(navpath))
|
||||
print(navpath)
|
||||
print(round(time.time() - start, 3), 'seconds')
|
||||
if self.g.job:
|
||||
self.g.job.stop()
|
||||
self.g.look_at = None
|
||||
reply = 'ok'
|
||||
else:
|
||||
@@ -543,14 +551,12 @@ class Commands:
|
||||
navpath = self.g.world.path_to_place(pos, goal)
|
||||
|
||||
if navpath:
|
||||
self.g.path = navpath
|
||||
if self.g.job:
|
||||
self.g.job.stop()
|
||||
self.g.path = navpath
|
||||
print(len(navpath))
|
||||
print(navpath)
|
||||
print(round(time.time() - start, 3), 'seconds')
|
||||
if self.g.job:
|
||||
self.g.job.stop()
|
||||
self.g.look_at = None
|
||||
reply = 'ok'
|
||||
else:
|
||||
|
||||
+13
-8
@@ -6,10 +6,10 @@ import os
|
||||
import threading
|
||||
from minecraft.networking.connection import Connection
|
||||
from minecraft.networking.modern import (
|
||||
CancelPostjoinDialog, ChatCommand, PlayShowDialog,
|
||||
CancelPostjoinDialog, ChatCommand, PlayGameEventPacket, PlayShowDialog,
|
||||
PlayLogin, SubmitPostjoinLoginDialog, SystemChat, TeleportPosition,
|
||||
)
|
||||
from mosfet.world_26_3 import WorldState
|
||||
from mosfet.protocol.managers import WorldState
|
||||
from mosfet.vector import Point3D
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ def main(global_state=None):
|
||||
global_state.connected = True
|
||||
global_state.connection_status = 'in_game'
|
||||
global_state.player_id = packet.player_id
|
||||
global_state.game_mode = packet.game_mode
|
||||
global_state.dimension = packet.dimension.removeprefix('minecraft:')
|
||||
packet.render_distance = packet.chunk_radius
|
||||
global_state.info = packet
|
||||
@@ -79,15 +80,14 @@ def main(global_state=None):
|
||||
from mosfet.world import World
|
||||
from mosfet.commands import Commands
|
||||
from mosfet.protocol.modern import ModernChatManager
|
||||
from mosfet.protocol.entities_26_3 import ModernEntityManager
|
||||
from mosfet.protocol.inventory_26_3 import ModernInventoryManager
|
||||
from mosfet.automation_26_3 import start as start_modern_movement
|
||||
from mosfet.protocol.managers import EntityManager, InventoryManager
|
||||
from mosfet.bot import start_automation
|
||||
global_state.world = World(global_state)
|
||||
global_state.chat = ModernChatManager(global_state)
|
||||
global_state.entities = ModernEntityManager(global_state)
|
||||
global_state.inventory = ModernInventoryManager(global_state)
|
||||
global_state.entities = EntityManager(global_state)
|
||||
global_state.inventory = InventoryManager(global_state)
|
||||
global_state.commands = Commands(global_state)
|
||||
start_modern_movement(global_state)
|
||||
start_automation(global_state)
|
||||
|
||||
|
||||
def send_auth_command(action):
|
||||
@@ -100,6 +100,11 @@ def main(global_state=None):
|
||||
connection.write_packet(ChatCommand(command=command.lstrip('/')),
|
||||
force=True)
|
||||
|
||||
@connection.listener(PlayGameEventPacket)
|
||||
def on_game_event(packet):
|
||||
if global_state is not None and packet.event == 3:
|
||||
global_state.game_mode = int(packet.value)
|
||||
|
||||
@connection.listener(TeleportPosition)
|
||||
def on_spawn(packet):
|
||||
if global_state is not None:
|
||||
|
||||
+117
-1
@@ -6,6 +6,10 @@ from munch import Munch
|
||||
|
||||
from minecraft.networking.packets import Packet, clientbound, serverbound
|
||||
from minecraft.networking.types import BlockFace
|
||||
from minecraft.networking.modern import (
|
||||
ClientTickEnd, ContainerClickSwapPacket, PlayerActionPacket,
|
||||
SetCarriedItemPacket, UseItemOnBlockPacket,
|
||||
)
|
||||
|
||||
from mosfet.protocol.packets import (
|
||||
SetSlotPacket, PlayerDiggingPacket,
|
||||
@@ -29,7 +33,7 @@ from mosfet.info import items
|
||||
from mosfet.info import mcdata
|
||||
from mosfet.info import mobs
|
||||
|
||||
class Game:
|
||||
class LegacyGame:
|
||||
def __init__(self, global_state):
|
||||
self.g = global_state
|
||||
|
||||
@@ -568,3 +572,115 @@ class Game:
|
||||
|
||||
if not self.g.path:
|
||||
self.g.correction_count = 0
|
||||
|
||||
|
||||
# Active game facade. The constructor registers no legacy packet listeners;
|
||||
# migrated actions use the 26.3 packet classes imported above.
|
||||
class Game(LegacyGame):
|
||||
def __init__(self, global_state):
|
||||
self.g = global_state
|
||||
self.sequence = 0
|
||||
self.break_deadline = None
|
||||
self.breaking_state = None
|
||||
|
||||
def _send_action(self, packet):
|
||||
self.g.connection.write_packet(packet, force=True)
|
||||
self.g.connection.write_packet(ClientTickEnd(), force=True)
|
||||
|
||||
def _next_sequence(self):
|
||||
self.sequence += 1
|
||||
return self.sequence
|
||||
|
||||
def break_block(self, location):
|
||||
location = tuple(int(value) for value in location)
|
||||
block_id = self.g.chunks.get_block_at(*location)
|
||||
if block_id is None or block_id == blocks.AIR:
|
||||
return False
|
||||
packet = PlayerActionPacket(
|
||||
action=0, location=location, face=1,
|
||||
sequence=self._next_sequence())
|
||||
self._send_action(packet)
|
||||
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)
|
||||
return True
|
||||
|
||||
def tick(self):
|
||||
if not self.g.get('breaking') or self.break_deadline is None:
|
||||
return
|
||||
if time.monotonic() < self.break_deadline:
|
||||
return
|
||||
packet = PlayerActionPacket(
|
||||
action=3, location=self.g.breaking, face=1,
|
||||
sequence=self._next_sequence())
|
||||
self._send_action(packet)
|
||||
location = self.g.breaking
|
||||
if self.g.chunks.get_block_at(*location) == self.breaking_state:
|
||||
# Predict the normal client-side block removal. The server's later
|
||||
# block update is authoritative and will correct the cache if the
|
||||
# action was denied or the block changed differently.
|
||||
self.g.chunks.set_block_at(*location, blocks.AIR)
|
||||
self.g.breaking = None
|
||||
self.break_deadline = None
|
||||
self.breaking_state = None
|
||||
|
||||
def cancel_break(self):
|
||||
if not self.g.get('breaking'):
|
||||
return
|
||||
packet = PlayerActionPacket(
|
||||
action=2, location=self.g.breaking, face=1,
|
||||
sequence=self._next_sequence())
|
||||
self._send_action(packet)
|
||||
self.g.breaking = None
|
||||
self.break_deadline = None
|
||||
self.breaking_state = None
|
||||
|
||||
def select_item(self, item_ids):
|
||||
item_ids = set(item_ids)
|
||||
matches = [(slot, item) for slot, item in self.g.inv.items()
|
||||
if item.present and item.item_id in item_ids]
|
||||
if not matches:
|
||||
return False
|
||||
|
||||
# Prefer an already-hotbar stack; otherwise swap the requested stack
|
||||
# into an empty hotbar slot using the modern container SWAP input.
|
||||
slot, item = next(
|
||||
((slot, item) for slot, item in matches if 36 <= slot <= 44),
|
||||
matches[0])
|
||||
if 36 <= slot <= 44:
|
||||
hotbar_slot = slot - 36
|
||||
elif 9 <= slot <= 35:
|
||||
empty_hotbar = next(
|
||||
(candidate - 36 for candidate in range(36, 45)
|
||||
if candidate not in self.g.inv or
|
||||
not self.g.inv[candidate].present), None)
|
||||
hotbar_slot = (empty_hotbar if empty_hotbar is not None else
|
||||
self.g.get('holding_slot', 0) or 0)
|
||||
self.g.connection.write_packet(ContainerClickSwapPacket(
|
||||
container_id=0,
|
||||
state_id=self.g.inventory_state_ids.get(0, 0),
|
||||
slot=slot, button=hotbar_slot), force=True)
|
||||
else:
|
||||
return False
|
||||
|
||||
if self.g.get('holding_slot') != hotbar_slot:
|
||||
self.g.connection.write_packet(SetCarriedItemPacket(
|
||||
slot=hotbar_slot), force=True)
|
||||
self.g.holding_slot = hotbar_slot
|
||||
self.g.holding = item.item_id
|
||||
self.g.connection.write_packet(ClientTickEnd(), force=True)
|
||||
return True
|
||||
|
||||
def place_block(self, location, face):
|
||||
location = tuple(int(value) for value in location)
|
||||
packet = UseItemOnBlockPacket(
|
||||
hand=0, location=location, face=int(face),
|
||||
cursor_x=0.5, cursor_y=0.99 if int(face) == 1 else 0.5,
|
||||
cursor_z=0.5, inside_block=False, world_border_hit=False,
|
||||
sequence=self._next_sequence())
|
||||
self._send_action(packet)
|
||||
|
||||
def close_window(self):
|
||||
# Container open/close packet migration is not part of the crop path.
|
||||
return None
|
||||
|
||||
@@ -77,7 +77,9 @@ LEAVES = sorted(name for name in MCD_BLOCKS if name.endswith('_leaves'))
|
||||
SAPLINGS = sorted(name for name in MCD_BLOCKS
|
||||
if name.endswith('_sapling') or name.endswith('_propagule'))
|
||||
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',
|
||||
]
|
||||
|
||||
LOG_IDS = get_set(LOGS)
|
||||
LEAF_IDS = get_set(LEAVES)
|
||||
|
||||
+20
-1
@@ -55,7 +55,7 @@ for module in [
|
||||
importlib.reload(module)
|
||||
|
||||
|
||||
class JobStates:
|
||||
class LegacyJobStates:
|
||||
def idle(self):
|
||||
return []
|
||||
|
||||
@@ -262,3 +262,22 @@ class JobStates:
|
||||
|
||||
def tick(self):
|
||||
self.run_machines(self.state())
|
||||
|
||||
|
||||
class JobStates(LegacyJobStates):
|
||||
"""26.3 job facade; crop gathering is the currently migrated job."""
|
||||
def __init__(self, global_state):
|
||||
self.g = global_state
|
||||
self.gather_crop_states = gather_crop.GatherCropStates(global_state)
|
||||
self.state = self.idle
|
||||
|
||||
def farm_crop(self):
|
||||
return [self.gather_crop_states]
|
||||
|
||||
def stop(self):
|
||||
if self.g.get('breaking'):
|
||||
self.g.game.cancel_break()
|
||||
self.g.path = []
|
||||
self.g.look_at = None
|
||||
self.gather_crop_states.state = self.gather_crop_states.idle
|
||||
self.state = self.idle
|
||||
|
||||
+64
-10
@@ -35,12 +35,15 @@ class GatherCropStates:
|
||||
blocks.MATURE_BEETROOT_ID,
|
||||
]
|
||||
|
||||
for crop in w.find_blocks_3d(p, mature_crops, 50, 20, True):
|
||||
candidates = w.find_blocks_indexed(p, mature_crops, 50)
|
||||
|
||||
for crop in candidates:
|
||||
if crop not in self.bad_crops:
|
||||
break
|
||||
else: # for
|
||||
print('No good crops left, aborting.')
|
||||
self.state = self.cleanup
|
||||
print('No good crops left; retrying after a short wait.')
|
||||
self.retry_after = time.monotonic() + 15.0
|
||||
self.state = self.wait_for_crops
|
||||
return
|
||||
|
||||
print('Found crop:', crop)
|
||||
@@ -48,6 +51,10 @@ class GatherCropStates:
|
||||
self.type_id = w.block_at(*crop)
|
||||
self.state = self.nav_to_crop
|
||||
|
||||
def wait_for_crops(self):
|
||||
if time.monotonic() >= self.retry_after:
|
||||
self.state = self.find_new_crop
|
||||
|
||||
def nav_to_crop(self):
|
||||
w = self.g.world
|
||||
p = utils.pint(self.g.pos)
|
||||
@@ -71,8 +78,34 @@ class GatherCropStates:
|
||||
self.state = self.break_crop
|
||||
|
||||
def break_crop(self):
|
||||
self.g.game.break_block(self.crop)
|
||||
self.state = self.select_seed
|
||||
print('Requesting crop break at {}: block={}, mode={}, held={}'.format(
|
||||
self.crop, self.g.world.block_at(*self.crop),
|
||||
self.g.get('game_mode'), self.g.get('holding')))
|
||||
if self.g.game.break_block(self.crop):
|
||||
print('Started breaking crop {} (state {})'.format(
|
||||
self.crop, self.type_id))
|
||||
self.break_deadline = time.time() + 5.0
|
||||
self.state = self.wait_break
|
||||
else:
|
||||
print('Break request skipped; current block is',
|
||||
self.g.world.block_at(*self.crop))
|
||||
self.bad_crops.append(self.crop)
|
||||
self.state = self.find_new_crop
|
||||
|
||||
def wait_break(self):
|
||||
current_state = self.g.world.block_at(*self.crop)
|
||||
if current_state != self.type_id:
|
||||
print('Crop break confirmed at {}: {} -> {}'.format(
|
||||
self.crop, self.type_id, current_state))
|
||||
self.seed_deadline = time.monotonic() + 8.0
|
||||
self.seed_wait_logged = False
|
||||
self.state = self.select_seed
|
||||
elif time.time() > self.break_deadline:
|
||||
print('Crop break timed out at {}: expected state {}, server still has {}'.format(
|
||||
self.crop, self.type_id, current_state))
|
||||
print('Crop did not break; blacklisting', self.crop)
|
||||
self.bad_crops.append(self.crop)
|
||||
self.state = self.find_new_crop
|
||||
|
||||
def select_seed(self):
|
||||
p = utils.pint(self.g.pos)
|
||||
@@ -86,9 +119,19 @@ class GatherCropStates:
|
||||
self.target_seed = crop_seeds[self.type_id]
|
||||
|
||||
if self.g.game.select_item([self.target_seed]):
|
||||
self.seed_deadline = None
|
||||
self.state = self.wait_select
|
||||
else:
|
||||
print('Havent picked up seed yet')
|
||||
if (self.seed_deadline is not None and
|
||||
time.monotonic() > self.seed_deadline):
|
||||
print('No seed collected for crop; skipping', self.crop)
|
||||
self.bad_crops.append(self.crop)
|
||||
self.seed_deadline = None
|
||||
self.state = self.find_new_crop
|
||||
elif not getattr(self, 'seed_wait_logged', False):
|
||||
print('Waiting for crop seed {} in inventory/hotbar'.format(
|
||||
self.target_seed))
|
||||
self.seed_wait_logged = True
|
||||
return
|
||||
|
||||
def wait_select(self):
|
||||
@@ -98,17 +141,25 @@ class GatherCropStates:
|
||||
self.state = self.place_crop
|
||||
|
||||
def place_crop(self):
|
||||
p = utils.pint(self.g.pos)
|
||||
p = utils.padd(self.crop, path.BLOCK_BELOW)
|
||||
self.place_deadline = time.time() + 3.0
|
||||
self.g.game.place_block(p, BlockFace.TOP)
|
||||
print('Placed crop')
|
||||
self.state = self.wait_place
|
||||
|
||||
def wait_place(self):
|
||||
w = self.g.world
|
||||
if w.block_at(*self.crop) == blocks.AIR:
|
||||
return
|
||||
else:
|
||||
current_state = w.block_at(*self.crop)
|
||||
crop_name = blocks.BLOCKS.get(self.type_id)
|
||||
replanted = (current_state not in (None, blocks.AIR) and
|
||||
blocks.BLOCKS.get(current_state) == crop_name and
|
||||
current_state != self.type_id)
|
||||
if replanted:
|
||||
self.state = self.cleanup
|
||||
elif time.time() > getattr(self, 'place_deadline', 0):
|
||||
print('Crop did not replant; blacklisting', self.crop)
|
||||
self.bad_crops.append(self.crop)
|
||||
self.state = self.find_new_crop
|
||||
|
||||
def cleanup(self):
|
||||
self.g.look_at = None
|
||||
@@ -125,6 +176,9 @@ class GatherCropStates:
|
||||
self.crop = None
|
||||
self.type_id = None
|
||||
self.target_seed = None
|
||||
self.seed_deadline = None
|
||||
self.seed_wait_logged = False
|
||||
self.retry_after = 0.0
|
||||
self.bad_crops = []
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
"""Entity and player-list updates for the 26.3 play protocol."""
|
||||
from munch import Munch
|
||||
|
||||
from minecraft.networking.modern import (
|
||||
AddEntityPacket, EntityPositionSyncPacket, MoveEntityPositionPacket,
|
||||
MoveEntityPositionRotationPacket, MoveEntityRotationPacket,
|
||||
PlayerInfoRemovePacket, PlayerInfoUpdatePacket, RemoveEntitiesPacket,
|
||||
)
|
||||
from mosfet.info import mobs
|
||||
|
||||
|
||||
class ModernEntityManager:
|
||||
def __init__(self, global_state):
|
||||
self.g = global_state
|
||||
self.g.mobs = self.g.get('mobs') or {}
|
||||
self.g.players = self.g.get('players') or {}
|
||||
self.g.objects = self.g.get('objects') or {}
|
||||
self.g.player_names = self.g.get('player_names') or {}
|
||||
register = self.g.connection.register_packet_listener
|
||||
for packet_type, handler in (
|
||||
(AddEntityPacket, self.handle_add_entity),
|
||||
(RemoveEntitiesPacket, self.handle_remove_entities),
|
||||
(MoveEntityPositionPacket, self.handle_move),
|
||||
(MoveEntityPositionRotationPacket, self.handle_move),
|
||||
(MoveEntityRotationPacket, self.handle_rotation),
|
||||
(EntityPositionSyncPacket, self.handle_position_sync),
|
||||
(PlayerInfoUpdatePacket, self.handle_player_info),
|
||||
(PlayerInfoRemovePacket, self.handle_player_info_remove),
|
||||
):
|
||||
register(handler, packet_type)
|
||||
|
||||
def handle_add_entity(self, packet):
|
||||
name = mobs.MOB_NAMES.get(packet.entity_type_id, 'unknown')
|
||||
common = dict(entity_id=packet.entity_id, x=packet.x,
|
||||
y=packet.y, z=packet.z)
|
||||
if name == 'player':
|
||||
player_uuid = str(packet.entity_uuid)
|
||||
self.g.players[packet.entity_id] = Munch(
|
||||
**common, player_uuid=player_uuid,
|
||||
yaw=packet.yaw, pitch=packet.pitch)
|
||||
elif name == 'item':
|
||||
self.g.objects[packet.entity_id] = Munch(
|
||||
**common, entity_uuid=str(packet.entity_uuid),
|
||||
velocity_x=packet.velocity_x * 8000,
|
||||
velocity_y=packet.velocity_y * 8000,
|
||||
velocity_z=packet.velocity_z * 8000)
|
||||
else:
|
||||
self.g.mobs[packet.entity_id] = Munch(
|
||||
**common, entity_uuid=str(packet.entity_uuid),
|
||||
type=packet.entity_type_id, yaw=packet.yaw,
|
||||
pitch=packet.pitch)
|
||||
|
||||
def _entity(self, entity_id):
|
||||
for collection in (self.g.players, self.g.mobs, self.g.objects):
|
||||
entity = collection.get(entity_id)
|
||||
if entity is not None:
|
||||
return entity
|
||||
return None
|
||||
|
||||
def handle_move(self, packet):
|
||||
entity = self._entity(packet.entity_id)
|
||||
if entity is None:
|
||||
return
|
||||
entity.x += packet.delta_x
|
||||
entity.y += packet.delta_y
|
||||
entity.z += packet.delta_z
|
||||
if hasattr(packet, 'yaw'):
|
||||
entity.yaw = packet.yaw
|
||||
entity.pitch = packet.pitch
|
||||
|
||||
def handle_rotation(self, packet):
|
||||
entity = self._entity(packet.entity_id)
|
||||
if entity is not None:
|
||||
entity.yaw = packet.yaw
|
||||
entity.pitch = packet.pitch
|
||||
|
||||
def handle_position_sync(self, packet):
|
||||
entity = self._entity(packet.entity_id)
|
||||
if entity is not None:
|
||||
entity.x, entity.y, entity.z = packet.x, packet.y, packet.z
|
||||
entity.yaw = packet.yaw
|
||||
entity.pitch = packet.pitch
|
||||
|
||||
def handle_remove_entities(self, packet):
|
||||
for entity_id in packet.entity_ids:
|
||||
self.g.players.pop(entity_id, None)
|
||||
self.g.mobs.pop(entity_id, None)
|
||||
self.g.objects.pop(entity_id, None)
|
||||
|
||||
def handle_player_info(self, packet):
|
||||
for profile_uuid, name in packet.entries:
|
||||
if name:
|
||||
uuid_text = str(profile_uuid)
|
||||
self.g.player_names[uuid_text] = name
|
||||
self.g.player_names[name] = profile_uuid
|
||||
self.g.player_names[name.lower()] = profile_uuid
|
||||
|
||||
def handle_player_info_remove(self, packet):
|
||||
removed = {str(uuid) for uuid in packet.uuids}
|
||||
for key, value in list(self.g.player_names.items()):
|
||||
if str(value) in removed or str(key) in removed:
|
||||
del self.g.player_names[key]
|
||||
@@ -1,55 +0,0 @@
|
||||
"""Minimal 26.3 inventory synchronization for legacy read-only commands."""
|
||||
from mosfet.protocol.types import Slot
|
||||
from minecraft.networking.modern import (
|
||||
ContainerSetContentPacket, ContainerSetSlotPacket, SetCursorItemPacket,
|
||||
)
|
||||
|
||||
|
||||
class ModernInventoryManager:
|
||||
def __init__(self, global_state):
|
||||
self.g = global_state
|
||||
self.g.inv = self.g.get('inv') or {}
|
||||
self.g.inventory_state_ids = self.g.get('inventory_state_ids') or {}
|
||||
self.g.inventory_partial = False
|
||||
self.g.cursor = Slot(present=False)
|
||||
register = self.g.connection.register_packet_listener
|
||||
register(self.handle_content, ContainerSetContentPacket)
|
||||
register(self.handle_slot, ContainerSetSlotPacket)
|
||||
register(self.handle_cursor, SetCursorItemPacket)
|
||||
|
||||
@staticmethod
|
||||
def to_slot(item):
|
||||
return Slot(item.present, item.item_id, item.item_count,
|
||||
components=item.components,
|
||||
component_ids=item.component_ids,
|
||||
removed_components=item.removed_components,
|
||||
components_unknown=item.components_unknown)
|
||||
|
||||
def handle_content(self, packet):
|
||||
slots = {index: self.to_slot(item)
|
||||
for index, item in enumerate(packet.items)}
|
||||
self.g.inventory_state_ids[packet.container_id] = packet.state_id
|
||||
if packet.container_id == 0:
|
||||
self.g.inv = slots
|
||||
self.g.inventory_partial = packet.partial
|
||||
self.g.cursor = self.to_slot(packet.carried_item)
|
||||
elif self.g.get('window'):
|
||||
self.g.window.contents = slots
|
||||
self.g.cursor = self.to_slot(packet.carried_item)
|
||||
|
||||
def handle_slot(self, packet):
|
||||
self.g.inventory_state_ids[packet.container_id] = packet.state_id
|
||||
item = self.to_slot(packet.item)
|
||||
if packet.item.components_unknown:
|
||||
self.g.inventory_partial = True
|
||||
if packet.container_id == 0:
|
||||
self.g.inv[packet.slot] = item
|
||||
elif packet.container_id == -1 and packet.slot == -1:
|
||||
self.g.cursor = item
|
||||
elif self.g.get('window'):
|
||||
self.g.window.contents[packet.slot] = item
|
||||
|
||||
def handle_cursor(self, packet):
|
||||
if packet.item.components_unknown:
|
||||
self.g.inventory_partial = True
|
||||
self.g.cursor = self.to_slot(packet.item)
|
||||
@@ -1,8 +1,18 @@
|
||||
import json
|
||||
from math import floor
|
||||
from munch import Munch
|
||||
|
||||
from minecraft.networking.packets import clientbound, serverbound
|
||||
from minecraft.networking.modern import (
|
||||
AddEntityPacket, ContainerSetContentPacket, ContainerSetSlotPacket,
|
||||
PaletteSection,
|
||||
EntityPositionSyncPacket, MoveEntityPositionPacket,
|
||||
MoveEntityPositionRotationPacket, MoveEntityRotationPacket,
|
||||
PlayerInfoRemovePacket, PlayerInfoUpdatePacket, RemoveEntitiesPacket,
|
||||
SetCursorItemPacket,
|
||||
)
|
||||
from mosfet.protocol import packets
|
||||
from mosfet.protocol.types import Slot
|
||||
from mosfet.info import blocks, mcdata, mobs
|
||||
|
||||
|
||||
@@ -172,3 +182,298 @@ class ChatManager:
|
||||
packet = serverbound.play.ChatPacket()
|
||||
packet.message = text
|
||||
self.g.connection.write_packet(packet)
|
||||
|
||||
|
||||
class ChunkColumn:
|
||||
def __init__(self, x, z, sections, min_section):
|
||||
self.x = x
|
||||
self.z = z
|
||||
self.sections = {
|
||||
min_section + offset: section
|
||||
for offset, section in enumerate(sections)
|
||||
}
|
||||
|
||||
def get_block_at(self, x, y, z):
|
||||
section = self.sections.get(y // 16)
|
||||
if section is None:
|
||||
return None
|
||||
index = ((y & 15) * 16 + (z & 15)) * 16 + (x & 15)
|
||||
return section['blocks'].get(index)
|
||||
|
||||
def set_block_at(self, x, y, z, block_state_id):
|
||||
section = self.sections.get(y // 16)
|
||||
if section is None:
|
||||
return None
|
||||
index = ((y & 15) * 16 + (z & 15)) * 16 + (x & 15)
|
||||
previous = section['blocks'].get(index)
|
||||
section['blocks'].set(index, block_state_id)
|
||||
return previous
|
||||
|
||||
|
||||
class WorldState:
|
||||
"""Sparse 26.3 chunk/block-state cache used by world and navigation."""
|
||||
def __init__(self, min_y=-64):
|
||||
self.min_y = min_y
|
||||
self.min_section = min_y // 16
|
||||
self.chunks = {}
|
||||
self.index = {}
|
||||
from mosfet.info import blocks
|
||||
self.indexed_ids = blocks.INDEXED_IDS
|
||||
self.center = None
|
||||
self.radius = None
|
||||
self.dimension = 'minecraft:overworld'
|
||||
self.player_id = None
|
||||
|
||||
def set_dimension(self, dimension, dimension_type=None):
|
||||
if dimension:
|
||||
self.dimension = dimension
|
||||
if dimension_type:
|
||||
min_y = dimension_type.get('min_y', self.min_y)
|
||||
if isinstance(min_y, int):
|
||||
self.min_y = min_y
|
||||
self.min_section = min_y // 16
|
||||
|
||||
def load_chunk(self, packet):
|
||||
# Refresh selectors in case block metadata was reloaded before login.
|
||||
from mosfet.info import blocks
|
||||
self.indexed_ids = blocks.INDEXED_IDS
|
||||
self.unload_chunk(packet.x, packet.z)
|
||||
chunk = ChunkColumn(packet.x, packet.z, packet.sections,
|
||||
self.min_section)
|
||||
self.chunks[(packet.x, packet.z)] = chunk
|
||||
for section_y, section in chunk.sections.items():
|
||||
palette = section['blocks']
|
||||
if not self.indexed_ids.intersection(
|
||||
palette.palette or [palette.get(i) for i in range(palette.size)]):
|
||||
continue
|
||||
for index in range(palette.size):
|
||||
state_id = palette.get(index)
|
||||
if state_id not in self.indexed_ids:
|
||||
continue
|
||||
local_x = index & 0xF
|
||||
local_z = (index >> 4) & 0xF
|
||||
local_y = (index >> 8) & 0xF
|
||||
pos = (packet.x * 16 + local_x,
|
||||
section_y * 16 + local_y,
|
||||
packet.z * 16 + local_z)
|
||||
self.index.setdefault(state_id, set()).add(pos)
|
||||
|
||||
def handle_block_update(self, packet):
|
||||
self.set_block_at(packet.x, packet.y, packet.z, packet.block_state_id)
|
||||
|
||||
def handle_section_update(self, packet):
|
||||
for x, y, z, state_id in packet.changes:
|
||||
self.set_block_at(x, y, z, state_id)
|
||||
|
||||
def get_chunk(self, x, z):
|
||||
return self.chunks.get((x, z))
|
||||
|
||||
def get_block_at(self, x, y, z):
|
||||
chunk = self.get_chunk(floor(x / 16), floor(z / 16))
|
||||
if chunk is None:
|
||||
return None
|
||||
return chunk.get_block_at(floor(x), floor(y), floor(z))
|
||||
|
||||
def set_block_at(self, x, y, z, block_state_id):
|
||||
x, y, z = floor(x), floor(y), floor(z)
|
||||
chunk = self.get_chunk(floor(x / 16), floor(z / 16))
|
||||
if chunk is None:
|
||||
return False
|
||||
previous = chunk.set_block_at(x, y, z, block_state_id)
|
||||
pos = (x, y, z)
|
||||
if previous in self.indexed_ids:
|
||||
self.index.get(previous, set()).discard(pos)
|
||||
if block_state_id in self.indexed_ids:
|
||||
self.index.setdefault(block_state_id, set()).add(pos)
|
||||
return True
|
||||
|
||||
def unload_chunk(self, x, z):
|
||||
chunk = self.chunks.pop((x, z), None)
|
||||
if chunk is None:
|
||||
return
|
||||
x0, z0 = x * 16, z * 16
|
||||
for state_id, locations in list(self.index.items()):
|
||||
self.index[state_id] = {
|
||||
pos for pos in locations
|
||||
if not (x0 <= pos[0] < x0 + 16 and z0 <= pos[2] < z0 + 16)
|
||||
}
|
||||
if not self.index[state_id]:
|
||||
del self.index[state_id]
|
||||
|
||||
def unload_all_chunks(self):
|
||||
self.chunks.clear()
|
||||
self.index.clear()
|
||||
|
||||
def get_loaded_area(self, ignore_empty=False):
|
||||
if not self.chunks:
|
||||
return None
|
||||
xs = [pos[0] for pos in self.chunks]
|
||||
zs = [pos[1] for pos in self.chunks]
|
||||
ys = [section_y for chunk in self.chunks.values()
|
||||
for section_y in chunk.sections]
|
||||
return ((min(xs), min(ys), min(zs)),
|
||||
(max(xs), max(ys), max(zs)))
|
||||
|
||||
def check_loaded(self, chunk_distance=None):
|
||||
if self.center is None:
|
||||
return bool(self.chunks)
|
||||
radius = self.radius if chunk_distance is None else chunk_distance
|
||||
if radius is None:
|
||||
return bool(self.chunks)
|
||||
cx, cz = self.center
|
||||
return all((x, z) in self.chunks
|
||||
for x in range(cx - radius, cx + radius + 1)
|
||||
for z in range(cz - radius, cz + radius + 1))
|
||||
|
||||
@property
|
||||
def loaded_chunk_count(self):
|
||||
return len(self.chunks)
|
||||
|
||||
def __repr__(self):
|
||||
return ('WorldState(dimension={!r}, chunks={}, min_y={}, center={!r}, '
|
||||
'radius={!r})').format(
|
||||
self.dimension, len(self.chunks), self.min_y,
|
||||
self.center, self.radius)
|
||||
|
||||
|
||||
class EntityManager:
|
||||
def __init__(self, global_state):
|
||||
self.g = global_state
|
||||
self.g.mobs = self.g.get('mobs') or {}
|
||||
self.g.players = self.g.get('players') or {}
|
||||
self.g.objects = self.g.get('objects') or {}
|
||||
self.g.player_names = self.g.get('player_names') or {}
|
||||
register = self.g.connection.register_packet_listener
|
||||
for packet_type, handler in (
|
||||
(AddEntityPacket, self.handle_add_entity),
|
||||
(RemoveEntitiesPacket, self.handle_remove_entities),
|
||||
(MoveEntityPositionPacket, self.handle_move),
|
||||
(MoveEntityPositionRotationPacket, self.handle_move),
|
||||
(MoveEntityRotationPacket, self.handle_rotation),
|
||||
(EntityPositionSyncPacket, self.handle_position_sync),
|
||||
(PlayerInfoUpdatePacket, self.handle_player_info),
|
||||
(PlayerInfoRemovePacket, self.handle_player_info_remove),
|
||||
):
|
||||
register(handler, packet_type)
|
||||
|
||||
def handle_add_entity(self, packet):
|
||||
name = mobs.MOB_NAMES.get(packet.entity_type_id, 'unknown')
|
||||
common = dict(entity_id=packet.entity_id, x=packet.x,
|
||||
y=packet.y, z=packet.z)
|
||||
if name == 'player':
|
||||
player_uuid = str(packet.entity_uuid)
|
||||
self.g.players[packet.entity_id] = Munch(
|
||||
**common, player_uuid=player_uuid,
|
||||
yaw=packet.yaw, pitch=packet.pitch)
|
||||
elif name == 'item':
|
||||
self.g.objects[packet.entity_id] = Munch(
|
||||
**common, entity_uuid=str(packet.entity_uuid),
|
||||
velocity_x=packet.velocity_x * 8000,
|
||||
velocity_y=packet.velocity_y * 8000,
|
||||
velocity_z=packet.velocity_z * 8000)
|
||||
else:
|
||||
self.g.mobs[packet.entity_id] = Munch(
|
||||
**common, entity_uuid=str(packet.entity_uuid),
|
||||
type=packet.entity_type_id, yaw=packet.yaw,
|
||||
pitch=packet.pitch)
|
||||
|
||||
def _entity(self, entity_id):
|
||||
for collection in (self.g.players, self.g.mobs, self.g.objects):
|
||||
entity = collection.get(entity_id)
|
||||
if entity is not None:
|
||||
return entity
|
||||
return None
|
||||
|
||||
def handle_move(self, packet):
|
||||
entity = self._entity(packet.entity_id)
|
||||
if entity is None:
|
||||
return
|
||||
entity.x += packet.delta_x
|
||||
entity.y += packet.delta_y
|
||||
entity.z += packet.delta_z
|
||||
if hasattr(packet, 'yaw'):
|
||||
entity.yaw = packet.yaw
|
||||
entity.pitch = packet.pitch
|
||||
|
||||
def handle_rotation(self, packet):
|
||||
entity = self._entity(packet.entity_id)
|
||||
if entity is not None:
|
||||
entity.yaw = packet.yaw
|
||||
entity.pitch = packet.pitch
|
||||
|
||||
def handle_position_sync(self, packet):
|
||||
entity = self._entity(packet.entity_id)
|
||||
if entity is not None:
|
||||
entity.x, entity.y, entity.z = packet.x, packet.y, packet.z
|
||||
entity.yaw = packet.yaw
|
||||
entity.pitch = packet.pitch
|
||||
|
||||
def handle_remove_entities(self, packet):
|
||||
for entity_id in packet.entity_ids:
|
||||
self.g.players.pop(entity_id, None)
|
||||
self.g.mobs.pop(entity_id, None)
|
||||
self.g.objects.pop(entity_id, None)
|
||||
|
||||
def handle_player_info(self, packet):
|
||||
for profile_uuid, name in packet.entries:
|
||||
if name:
|
||||
uuid_text = str(profile_uuid)
|
||||
self.g.player_names[uuid_text] = name
|
||||
self.g.player_names[name] = profile_uuid
|
||||
self.g.player_names[name.lower()] = profile_uuid
|
||||
|
||||
def handle_player_info_remove(self, packet):
|
||||
removed = {str(uuid) for uuid in packet.uuids}
|
||||
for key, value in list(self.g.player_names.items()):
|
||||
if str(value) in removed or str(key) in removed:
|
||||
del self.g.player_names[key]
|
||||
|
||||
|
||||
class InventoryManager:
|
||||
def __init__(self, global_state):
|
||||
self.g = global_state
|
||||
self.g.inv = self.g.get('inv') or {}
|
||||
self.g.inventory_state_ids = self.g.get('inventory_state_ids') or {}
|
||||
self.g.inventory_partial = False
|
||||
self.g.cursor = Slot(present=False)
|
||||
register = self.g.connection.register_packet_listener
|
||||
register(self.handle_content, ContainerSetContentPacket)
|
||||
register(self.handle_slot, ContainerSetSlotPacket)
|
||||
register(self.handle_cursor, SetCursorItemPacket)
|
||||
|
||||
@staticmethod
|
||||
def to_slot(item):
|
||||
return Slot(item.present, item.item_id, item.item_count,
|
||||
components=item.components,
|
||||
component_ids=item.component_ids,
|
||||
removed_components=item.removed_components,
|
||||
components_unknown=item.components_unknown)
|
||||
|
||||
def handle_content(self, packet):
|
||||
slots = {index: self.to_slot(item)
|
||||
for index, item in enumerate(packet.items)}
|
||||
self.g.inventory_state_ids[packet.container_id] = packet.state_id
|
||||
if packet.container_id == 0:
|
||||
self.g.inv = slots
|
||||
self.g.inventory_partial = packet.partial
|
||||
self.g.cursor = self.to_slot(packet.carried_item)
|
||||
elif self.g.get('window'):
|
||||
self.g.window.contents = slots
|
||||
self.g.cursor = self.to_slot(packet.carried_item)
|
||||
|
||||
def handle_slot(self, packet):
|
||||
self.g.inventory_state_ids[packet.container_id] = packet.state_id
|
||||
item = self.to_slot(packet.item)
|
||||
if packet.item.components_unknown:
|
||||
self.g.inventory_partial = True
|
||||
if packet.container_id == 0:
|
||||
self.g.inv[packet.slot] = item
|
||||
elif packet.container_id == -1 and packet.slot == -1:
|
||||
self.g.cursor = item
|
||||
elif self.g.get('window'):
|
||||
self.g.window.contents[packet.slot] = item
|
||||
|
||||
def handle_cursor(self, packet):
|
||||
if packet.item.components_unknown:
|
||||
self.g.inventory_partial = True
|
||||
self.g.cursor = self.to_slot(packet.item)
|
||||
|
||||
@@ -49,13 +49,10 @@ class World:
|
||||
yield check
|
||||
|
||||
def find_blocks_indexed(self, center, block_ids, distance=0):
|
||||
print('finding', block_ids)
|
||||
index = []
|
||||
for bid in block_ids:
|
||||
index.extend(self.g.chunks.index.get(bid, []))
|
||||
|
||||
print('index', index)
|
||||
|
||||
result = []
|
||||
for block in index:
|
||||
if self.block_at(*block) not in block_ids:
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
"""Small 26.3 world cache used while the legacy automation is being ported.
|
||||
|
||||
It retains numeric block-state IDs from chunk palettes and block updates. It
|
||||
intentionally does not assign 1.16 block meanings to modern state IDs.
|
||||
"""
|
||||
from math import floor
|
||||
|
||||
|
||||
class PaletteSection:
|
||||
def __init__(self, bits, palette, words, size):
|
||||
self.bits = bits
|
||||
self.palette = palette
|
||||
self.words = words
|
||||
self.size = size
|
||||
self.overrides = {}
|
||||
self.mask = (1 << bits) - 1 if bits else 0
|
||||
self.per_word = 64 // bits if bits else 0
|
||||
|
||||
def get(self, index):
|
||||
if index in self.overrides:
|
||||
return self.overrides[index]
|
||||
if self.bits == 0:
|
||||
return self.palette[0]
|
||||
word = self.words[index // self.per_word]
|
||||
palette_index = (word >> ((index % self.per_word) * self.bits)) & self.mask
|
||||
return self.palette[palette_index] if self.palette is not None else palette_index
|
||||
|
||||
def set(self, index, block_state_id):
|
||||
self.overrides[index] = block_state_id
|
||||
|
||||
|
||||
class ChunkColumn:
|
||||
def __init__(self, x, z, sections, min_section):
|
||||
self.x = x
|
||||
self.z = z
|
||||
self.sections = {
|
||||
min_section + offset: section
|
||||
for offset, section in enumerate(sections)
|
||||
}
|
||||
|
||||
def get_block_at(self, x, y, z):
|
||||
section = self.sections.get(y // 16)
|
||||
if section is None:
|
||||
return None
|
||||
index = ((y & 15) * 16 + (z & 15)) * 16 + (x & 15)
|
||||
return section['blocks'].get(index)
|
||||
|
||||
def set_block_at(self, x, y, z, block_state_id):
|
||||
section = self.sections.get(y // 16)
|
||||
if section is None:
|
||||
return None
|
||||
index = ((y & 15) * 16 + (z & 15)) * 16 + (x & 15)
|
||||
previous = section['blocks'].get(index)
|
||||
section['blocks'].set(index, block_state_id)
|
||||
return previous
|
||||
|
||||
|
||||
class WorldState:
|
||||
"""Sparse loaded-chunk/block-state cache with the old manager's read API."""
|
||||
def __init__(self, min_y=-64):
|
||||
self.min_y = min_y
|
||||
self.min_section = min_y // 16
|
||||
self.chunks = {}
|
||||
self.index = {}
|
||||
from mosfet.info import blocks
|
||||
self.indexed_ids = blocks.INDEXED_IDS
|
||||
self.center = None
|
||||
self.radius = None
|
||||
self.dimension = 'minecraft:overworld'
|
||||
self.player_id = None
|
||||
|
||||
def set_dimension(self, dimension, dimension_type=None):
|
||||
if dimension:
|
||||
self.dimension = dimension
|
||||
if dimension_type:
|
||||
min_y = dimension_type.get('min_y', self.min_y)
|
||||
if isinstance(min_y, int):
|
||||
self.min_y = min_y
|
||||
self.min_section = min_y // 16
|
||||
|
||||
def load_chunk(self, packet):
|
||||
self.unload_chunk(packet.x, packet.z)
|
||||
chunk = ChunkColumn(packet.x, packet.z, packet.sections,
|
||||
self.min_section)
|
||||
self.chunks[(packet.x, packet.z)] = chunk
|
||||
for section_y, section in chunk.sections.items():
|
||||
palette = section['blocks']
|
||||
if not self.indexed_ids.intersection(
|
||||
palette.palette or [palette.get(i) for i in range(palette.size)]):
|
||||
continue
|
||||
for index in range(palette.size):
|
||||
state_id = palette.get(index)
|
||||
if state_id not in self.indexed_ids:
|
||||
continue
|
||||
local_x = index & 0xF
|
||||
local_z = (index >> 4) & 0xF
|
||||
local_y = (index >> 8) & 0xF
|
||||
pos = (packet.x * 16 + local_x,
|
||||
section_y * 16 + local_y,
|
||||
packet.z * 16 + local_z)
|
||||
self.index.setdefault(state_id, set()).add(pos)
|
||||
|
||||
def handle_block_update(self, packet):
|
||||
self.set_block_at(packet.x, packet.y, packet.z, packet.block_state_id)
|
||||
|
||||
def handle_section_update(self, packet):
|
||||
for x, y, z, state_id in packet.changes:
|
||||
self.set_block_at(x, y, z, state_id)
|
||||
|
||||
def get_chunk(self, x, z):
|
||||
return self.chunks.get((x, z))
|
||||
|
||||
def get_block_at(self, x, y, z):
|
||||
chunk = self.get_chunk(floor(x / 16), floor(z / 16))
|
||||
if chunk is None:
|
||||
return None
|
||||
return chunk.get_block_at(floor(x), floor(y), floor(z))
|
||||
|
||||
def set_block_at(self, x, y, z, block_state_id):
|
||||
x, y, z = floor(x), floor(y), floor(z)
|
||||
chunk = self.get_chunk(floor(x / 16), floor(z / 16))
|
||||
if chunk is None:
|
||||
return False
|
||||
previous = chunk.set_block_at(x, y, z, block_state_id)
|
||||
pos = (x, y, z)
|
||||
if previous in self.indexed_ids:
|
||||
self.index.get(previous, set()).discard(pos)
|
||||
if block_state_id in self.indexed_ids:
|
||||
self.index.setdefault(block_state_id, set()).add(pos)
|
||||
return True
|
||||
|
||||
def unload_chunk(self, x, z):
|
||||
chunk = self.chunks.pop((x, z), None)
|
||||
if chunk is None:
|
||||
return
|
||||
x0, z0 = x * 16, z * 16
|
||||
for state_id, locations in list(self.index.items()):
|
||||
self.index[state_id] = {
|
||||
pos for pos in locations
|
||||
if not (x0 <= pos[0] < x0 + 16 and z0 <= pos[2] < z0 + 16)
|
||||
}
|
||||
if not self.index[state_id]:
|
||||
del self.index[state_id]
|
||||
|
||||
def unload_all_chunks(self):
|
||||
self.chunks.clear()
|
||||
self.index.clear()
|
||||
|
||||
def get_loaded_area(self, ignore_empty=False):
|
||||
if not self.chunks:
|
||||
return None
|
||||
xs = [pos[0] for pos in self.chunks]
|
||||
zs = [pos[1] for pos in self.chunks]
|
||||
ys = [section_y for chunk in self.chunks.values()
|
||||
for section_y in chunk.sections]
|
||||
return ((min(xs), min(ys), min(zs)),
|
||||
(max(xs), max(ys), max(zs)))
|
||||
|
||||
def check_loaded(self, chunk_distance=None):
|
||||
if self.center is None:
|
||||
return bool(self.chunks)
|
||||
radius = self.radius if chunk_distance is None else chunk_distance
|
||||
if radius is None:
|
||||
return bool(self.chunks)
|
||||
cx, cz = self.center
|
||||
return all((x, z) in self.chunks
|
||||
for x in range(cx - radius, cx + radius + 1)
|
||||
for z in range(cz - radius, cz + radius + 1))
|
||||
|
||||
@property
|
||||
def loaded_chunk_count(self):
|
||||
return len(self.chunks)
|
||||
|
||||
def __repr__(self):
|
||||
return ('WorldState(dimension={!r}, chunks={}, min_y={}, center={!r}, '
|
||||
'radius={!r})').format(
|
||||
self.dimension, len(self.chunks), self.min_y,
|
||||
self.center, self.radius)
|
||||
|
||||
|
||||
def read_palette_container(stream, *, size, local_bits_max):
|
||||
from minecraft.networking.types import Byte, UnsignedLong, VarInt
|
||||
|
||||
bits = Byte.read(stream)
|
||||
if bits == 0:
|
||||
palette = [VarInt.read(stream)]
|
||||
elif bits <= local_bits_max:
|
||||
palette_size = VarInt.read(stream)
|
||||
palette = [VarInt.read(stream) for _ in range(palette_size)]
|
||||
else:
|
||||
palette = None
|
||||
|
||||
word_count = ((size + (64 // bits) - 1) // (64 // bits)) if bits else 0
|
||||
words = [UnsignedLong.read(stream) for _ in range(word_count)]
|
||||
return PaletteSection(bits, palette, words, size)
|
||||
|
||||
|
||||
def decode_chunk_sections(data):
|
||||
from io import BytesIO
|
||||
from minecraft.networking.types import Short
|
||||
|
||||
stream = BytesIO(data)
|
||||
sections = []
|
||||
while stream.tell() < len(data):
|
||||
start = stream.tell()
|
||||
if len(data) - start < 4:
|
||||
raise ValueError('truncated chunk section header at byte {}'.format(start))
|
||||
non_empty_blocks = Short.read(stream)
|
||||
fluid_count = Short.read(stream)
|
||||
blocks = read_palette_container(stream, size=4096, local_bits_max=8)
|
||||
biomes = read_palette_container(stream, size=64, local_bits_max=3)
|
||||
sections.append({
|
||||
'non_empty_blocks': non_empty_blocks,
|
||||
'fluid_count': fluid_count,
|
||||
'blocks': blocks,
|
||||
'biomes': biomes,
|
||||
})
|
||||
if stream.tell() <= start:
|
||||
raise ValueError('chunk section decoder made no progress')
|
||||
return sections
|
||||
Reference in New Issue
Block a user