Consolidate bot runtime on Minecraft 26.3
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
# Mosfet Minecraft Bot
|
# Mosfet Minecraft Bot
|
||||||
|
|
||||||
A general-purpose Minecraft 1.16 bot written in Python that uses an actual
|
A Minecraft 26.3 bot written in Python that uses an offline-mode account to
|
||||||
Minecraft account to play.
|
play. The connection path targets protocol 777; older Minecraft protocol
|
||||||
|
versions are not supported.
|
||||||
|
|
||||||
Mosfet is able to farm wood by cutting trees, farm crops, gather sand, farm
|
The original bot included wood/sand/crop farming, villager trading, eating,
|
||||||
netherwart, and trade with villagers to get emeralds. He can eat, sleep, and
|
sleeping, and threat avoidance. On the current 26.3 runtime, path following and
|
||||||
flee from threats.
|
crop harvest/replant are migrated; other legacy jobs remain unavailable until
|
||||||
|
ported to protocol 777.
|
||||||
|
|
||||||
## Requirements and setup
|
## Requirements and setup
|
||||||
|
|
||||||
@@ -35,9 +37,9 @@ through environment variables as needed. Microsoft authentication is not used.
|
|||||||
|
|
||||||
The 26.3 client now handles AuthMe, chat commands, chunk/world-state caching,
|
The 26.3 client now handles AuthMe, chat commands, chunk/world-state caching,
|
||||||
modern block/item/entity data, entity tracking, read-only inventory/component
|
modern block/item/entity data, entity tracking, read-only inventory/component
|
||||||
sync, and path-following movement (`1goto x y z`). Inventory actions, block
|
sync, path-following movement (`1goto x y z`), and the crop harvest/replant loop
|
||||||
breaking/placing, and farming/trading jobs remain disabled until their 26.3
|
(`1farm crops`). General inventory/window actions and the other farming/trading
|
||||||
packets are migrated. The default server is
|
jobs remain disabled until their 26.3 packets are migrated. The default server is
|
||||||
`127.0.0.1`; set `SERVER` explicitly only when you intentionally want another
|
`127.0.0.1`; set `SERVER` explicitly only when you intentionally want another
|
||||||
server.
|
server.
|
||||||
|
|
||||||
@@ -62,9 +64,10 @@ and all bots will run the command.
|
|||||||
The current 26.3 command path handles public chat. Private-message routing is
|
The current 26.3 command path handles public chat. Private-message routing is
|
||||||
not migrated yet. Verified commands are `!ping`, `!pos`, `!loaded`, `!info x y
|
not migrated yet. Verified commands are `!ping`, `!pos`, `!loaded`, `!info x y
|
||||||
z`, `!inv` (basic slot/count data), and addressed path movement such as
|
z`, `!inv` (basic slot/count data), and addressed path movement such as
|
||||||
`1goto x y z`. The command catalog below is retained from the legacy bot;
|
`1goto x y z`, and `1farm crops`. The command catalog below is retained from
|
||||||
inventory actions, block interaction, and farming/trading commands are not yet
|
the legacy bot; other inventory actions, arbitrary block interaction, and the
|
||||||
available on 26.3. Common item components (names, damage, enchantments, book
|
other farming/trading commands are not yet available on 26.3. Common item
|
||||||
|
components (names, damage, enchantments, book
|
||||||
contents, and shulker contents) are decoded; unrecognized component types are
|
contents, and shulker contents) are decoded; unrecognized component types are
|
||||||
marked partial rather than silently discarded.
|
marked partial rather than silently discarded.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import json
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
import sys
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
log = logging.getLogger('werkzeug')
|
log = logging.getLogger('werkzeug')
|
||||||
@@ -64,13 +65,22 @@ def root():
|
|||||||
def main():
|
def main():
|
||||||
"""Connect with offline/AuthMe and enable only the migrated 26.3 features.
|
"""Connect with offline/AuthMe and enable only the migrated 26.3 features.
|
||||||
|
|
||||||
Current gameplay support is read-only world/entity/inventory state plus
|
Current gameplay support includes path following and a crop-only harvest /
|
||||||
path following; inventory actions, block interactions, and legacy job
|
replant job. General inventory actions, other block interactions, and
|
||||||
packet handlers remain disabled until migrated.
|
legacy jobs remain disabled until migrated.
|
||||||
"""
|
"""
|
||||||
reload_timeout = time.time()
|
reload_timeout = time.time()
|
||||||
reload_requested = False
|
reload_requested = False
|
||||||
|
|
||||||
|
def reload_modules():
|
||||||
|
manager_module = sys.modules.get('mosfet.protocol.managers')
|
||||||
|
if manager_module is not None:
|
||||||
|
importlib.reload(manager_module)
|
||||||
|
bot_module = sys.modules.get('mosfet.bot')
|
||||||
|
if bot_module is not None:
|
||||||
|
importlib.reload(bot_module)
|
||||||
|
importlib.reload(connect)
|
||||||
|
|
||||||
def reload_bot(event):
|
def reload_bot(event):
|
||||||
nonlocal reload_timeout, reload_requested
|
nonlocal reload_timeout, reload_requested
|
||||||
if time.time() - reload_timeout > 2.0:
|
if time.time() - reload_timeout > 2.0:
|
||||||
@@ -94,7 +104,7 @@ def main():
|
|||||||
if not reload_requested:
|
if not reload_requested:
|
||||||
break
|
break
|
||||||
reload_requested = False
|
reload_requested = False
|
||||||
importlib.reload(connect)
|
reload_modules()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
break
|
break
|
||||||
except BaseException:
|
except BaseException:
|
||||||
@@ -104,7 +114,7 @@ def main():
|
|||||||
while g.running:
|
while g.running:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
reload_requested = False
|
reload_requested = False
|
||||||
importlib.reload(connect)
|
reload_modules()
|
||||||
finally:
|
finally:
|
||||||
g.running = False
|
g.running = False
|
||||||
observer.stop()
|
observer.stop()
|
||||||
|
|||||||
@@ -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 sys
|
||||||
import time
|
import time
|
||||||
import importlib
|
import importlib
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
from math import floor, ceil
|
from math import floor, ceil
|
||||||
from copy import copy
|
from copy import copy
|
||||||
|
|
||||||
@@ -68,26 +70,51 @@ def tick(global_state):
|
|||||||
required_distance = getattr(g, 'physics_render_distance',
|
required_distance = getattr(g, 'physics_render_distance',
|
||||||
g.info.render_distance)
|
g.info.render_distance)
|
||||||
if not g.chunks.check_loaded(required_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:
|
if not g.chunks.loading:
|
||||||
print('Loading chunks', end='', flush=True)
|
print('Loading chunks', end='', flush=True)
|
||||||
g.chunks.loading = time.time()
|
g.chunks.loading = time.time()
|
||||||
if getattr(g, 'protocol_version', 0) == 777:
|
now = time.time()
|
||||||
from minecraft.networking.modern import PlayerPositionRotation
|
if now - g.get('chunk_wait_report_at', 0) >= 5:
|
||||||
packet = PlayerPositionRotation(
|
center = g.chunks.center
|
||||||
x=p.x, y=p.y, z=p.z, pitch=0, yaw=0, on_ground=True)
|
if center is None:
|
||||||
else:
|
missing = 'cache center not received'
|
||||||
packet = serverbound.play.PositionAndLookPacket(
|
else:
|
||||||
x=p.x, feet_y=p.y, z=p.z, pitch=0, yaw=0, on_ground=True)
|
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)
|
g.connection.write_packet(packet, force=True)
|
||||||
if getattr(g, 'protocol_version', 0) == 777:
|
g.connection.write_packet(ClientTickEnd(), force=True)
|
||||||
from minecraft.networking.modern import ClientTickEnd
|
|
||||||
g.connection.write_packet(ClientTickEnd(), force=True)
|
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
if g.chunks.loading:
|
if g.chunks.loading:
|
||||||
print()
|
print()
|
||||||
print('Chunks loaded in', round(time.time() - g.chunks.loading, 2), 's')
|
print('Chunks loaded in', round(time.time() - g.chunks.loading, 2), 's')
|
||||||
g.chunks.loading = False
|
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 ##########
|
########## object physics ##########
|
||||||
# note: it's possible the chunk data is out of date when this runs
|
# 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
|
target_pitch_d = 0 - g.pitch
|
||||||
g.pitch += utils.cap(target_pitch_d, 10)
|
g.pitch += utils.cap(target_pitch_d, 10)
|
||||||
|
|
||||||
if getattr(g, 'protocol_version', 0) == 777:
|
from minecraft.networking.modern import (
|
||||||
from minecraft.networking.modern import PlayerPositionRotation
|
ClientTickEnd, PlayerPositionRotation,
|
||||||
packet = PlayerPositionRotation(
|
)
|
||||||
x=p.x, y=p.y, z=p.z, pitch=g.pitch, yaw=g.yaw,
|
packet = PlayerPositionRotation(
|
||||||
on_ground=(not in_air))
|
x=p.x, y=p.y, z=p.z, pitch=g.pitch, yaw=g.yaw,
|
||||||
else:
|
on_ground=(not in_air))
|
||||||
packet = serverbound.play.PositionAndLookPacket(
|
# Keep the client-tick boundary after its movement packet on the wire.
|
||||||
x=p.x, feet_y=p.y, z=p.z, pitch=g.pitch, yaw=g.yaw,
|
g.connection.write_packet(packet, force=True)
|
||||||
on_ground=(not in_air))
|
g.connection.write_packet(ClientTickEnd(), force=True)
|
||||||
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)
|
|
||||||
|
|
||||||
if g.job:
|
if g.job:
|
||||||
g.job.tick()
|
g.job.tick()
|
||||||
@@ -352,3 +371,90 @@ def bot(global_state):
|
|||||||
g.connection.early_outgoing_packet_listeners = []
|
g.connection.early_outgoing_packet_listeners = []
|
||||||
|
|
||||||
print('Bot module loaded.')
|
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 print_help
|
||||||
from mosfet import utils
|
from mosfet import utils
|
||||||
from mosfet import path
|
from mosfet import path
|
||||||
from mosfet import bot
|
|
||||||
from mosfet.info import blocks
|
from mosfet.info import blocks
|
||||||
from mosfet.info import items
|
from mosfet.info import items
|
||||||
from mosfet.info import mobs
|
from mosfet.info import mobs
|
||||||
@@ -56,6 +55,20 @@ class Commands:
|
|||||||
if data.startswith('[') and data.endswith(']'):
|
if data.startswith('[') and data.endswith(']'):
|
||||||
command = 'nosquarebrackets'
|
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:
|
try:
|
||||||
|
|
||||||
@@ -406,15 +419,12 @@ class Commands:
|
|||||||
self.g.job.state = self.g.job.farm_wart
|
self.g.job.state = self.g.job.farm_wart
|
||||||
reply = 'ok'
|
reply = 'ok'
|
||||||
elif data.startswith('crop'):
|
elif data.startswith('crop'):
|
||||||
self.g.job.state = self.g.job.farm_crop
|
mode = self.g.get('game_mode')
|
||||||
reply = 'ok'
|
if mode is not None and mode != 0:
|
||||||
|
reply = 'crop farming needs Survival mode'
|
||||||
if reply and self.g.dimension == 'overworld':
|
|
||||||
for i in self.g.inv.values():
|
|
||||||
if i.item_id in items.BED_IDS:
|
|
||||||
break
|
|
||||||
else:
|
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
|
## 1loiter - stands still but eats, sleeps, and flees
|
||||||
if command == 'loiter':
|
if command == 'loiter':
|
||||||
@@ -428,8 +438,8 @@ class Commands:
|
|||||||
|
|
||||||
## 1stop - stops the current job and resets bot
|
## 1stop - stops the current job and resets bot
|
||||||
if command == 'stop':
|
if command == 'stop':
|
||||||
|
self.g.job.stop()
|
||||||
self.g.game.close_window()
|
self.g.game.close_window()
|
||||||
bot.init(self.g)
|
|
||||||
reply = 'ok'
|
reply = 'ok'
|
||||||
|
|
||||||
## 1drop - drops the current stack its holding
|
## 1drop - drops the current stack its holding
|
||||||
@@ -515,14 +525,12 @@ class Commands:
|
|||||||
navpath = self.g.world.path_to_place(pos, goal)
|
navpath = self.g.world.path_to_place(pos, goal)
|
||||||
|
|
||||||
if navpath:
|
if navpath:
|
||||||
self.g.path = navpath
|
|
||||||
if self.g.job:
|
if self.g.job:
|
||||||
self.g.job.stop()
|
self.g.job.stop()
|
||||||
|
self.g.path = navpath
|
||||||
print(len(navpath))
|
print(len(navpath))
|
||||||
print(navpath)
|
print(navpath)
|
||||||
print(round(time.time() - start, 3), 'seconds')
|
print(round(time.time() - start, 3), 'seconds')
|
||||||
if self.g.job:
|
|
||||||
self.g.job.stop()
|
|
||||||
self.g.look_at = None
|
self.g.look_at = None
|
||||||
reply = 'ok'
|
reply = 'ok'
|
||||||
else:
|
else:
|
||||||
@@ -543,14 +551,12 @@ class Commands:
|
|||||||
navpath = self.g.world.path_to_place(pos, goal)
|
navpath = self.g.world.path_to_place(pos, goal)
|
||||||
|
|
||||||
if navpath:
|
if navpath:
|
||||||
self.g.path = navpath
|
|
||||||
if self.g.job:
|
if self.g.job:
|
||||||
self.g.job.stop()
|
self.g.job.stop()
|
||||||
|
self.g.path = navpath
|
||||||
print(len(navpath))
|
print(len(navpath))
|
||||||
print(navpath)
|
print(navpath)
|
||||||
print(round(time.time() - start, 3), 'seconds')
|
print(round(time.time() - start, 3), 'seconds')
|
||||||
if self.g.job:
|
|
||||||
self.g.job.stop()
|
|
||||||
self.g.look_at = None
|
self.g.look_at = None
|
||||||
reply = 'ok'
|
reply = 'ok'
|
||||||
else:
|
else:
|
||||||
|
|||||||
+13
-8
@@ -6,10 +6,10 @@ import os
|
|||||||
import threading
|
import threading
|
||||||
from minecraft.networking.connection import Connection
|
from minecraft.networking.connection import Connection
|
||||||
from minecraft.networking.modern import (
|
from minecraft.networking.modern import (
|
||||||
CancelPostjoinDialog, ChatCommand, PlayShowDialog,
|
CancelPostjoinDialog, ChatCommand, PlayGameEventPacket, PlayShowDialog,
|
||||||
PlayLogin, SubmitPostjoinLoginDialog, SystemChat, TeleportPosition,
|
PlayLogin, SubmitPostjoinLoginDialog, SystemChat, TeleportPosition,
|
||||||
)
|
)
|
||||||
from mosfet.world_26_3 import WorldState
|
from mosfet.protocol.managers import WorldState
|
||||||
from mosfet.vector import Point3D
|
from mosfet.vector import Point3D
|
||||||
|
|
||||||
|
|
||||||
@@ -71,6 +71,7 @@ def main(global_state=None):
|
|||||||
global_state.connected = True
|
global_state.connected = True
|
||||||
global_state.connection_status = 'in_game'
|
global_state.connection_status = 'in_game'
|
||||||
global_state.player_id = packet.player_id
|
global_state.player_id = packet.player_id
|
||||||
|
global_state.game_mode = packet.game_mode
|
||||||
global_state.dimension = packet.dimension.removeprefix('minecraft:')
|
global_state.dimension = packet.dimension.removeprefix('minecraft:')
|
||||||
packet.render_distance = packet.chunk_radius
|
packet.render_distance = packet.chunk_radius
|
||||||
global_state.info = packet
|
global_state.info = packet
|
||||||
@@ -79,15 +80,14 @@ def main(global_state=None):
|
|||||||
from mosfet.world import World
|
from mosfet.world import World
|
||||||
from mosfet.commands import Commands
|
from mosfet.commands import Commands
|
||||||
from mosfet.protocol.modern import ModernChatManager
|
from mosfet.protocol.modern import ModernChatManager
|
||||||
from mosfet.protocol.entities_26_3 import ModernEntityManager
|
from mosfet.protocol.managers import EntityManager, InventoryManager
|
||||||
from mosfet.protocol.inventory_26_3 import ModernInventoryManager
|
from mosfet.bot import start_automation
|
||||||
from mosfet.automation_26_3 import start as start_modern_movement
|
|
||||||
global_state.world = World(global_state)
|
global_state.world = World(global_state)
|
||||||
global_state.chat = ModernChatManager(global_state)
|
global_state.chat = ModernChatManager(global_state)
|
||||||
global_state.entities = ModernEntityManager(global_state)
|
global_state.entities = EntityManager(global_state)
|
||||||
global_state.inventory = ModernInventoryManager(global_state)
|
global_state.inventory = InventoryManager(global_state)
|
||||||
global_state.commands = Commands(global_state)
|
global_state.commands = Commands(global_state)
|
||||||
start_modern_movement(global_state)
|
start_automation(global_state)
|
||||||
|
|
||||||
|
|
||||||
def send_auth_command(action):
|
def send_auth_command(action):
|
||||||
@@ -100,6 +100,11 @@ def main(global_state=None):
|
|||||||
connection.write_packet(ChatCommand(command=command.lstrip('/')),
|
connection.write_packet(ChatCommand(command=command.lstrip('/')),
|
||||||
force=True)
|
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)
|
@connection.listener(TeleportPosition)
|
||||||
def on_spawn(packet):
|
def on_spawn(packet):
|
||||||
if global_state is not None:
|
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.packets import Packet, clientbound, serverbound
|
||||||
from minecraft.networking.types import BlockFace
|
from minecraft.networking.types import BlockFace
|
||||||
|
from minecraft.networking.modern import (
|
||||||
|
ClientTickEnd, ContainerClickSwapPacket, PlayerActionPacket,
|
||||||
|
SetCarriedItemPacket, UseItemOnBlockPacket,
|
||||||
|
)
|
||||||
|
|
||||||
from mosfet.protocol.packets import (
|
from mosfet.protocol.packets import (
|
||||||
SetSlotPacket, PlayerDiggingPacket,
|
SetSlotPacket, PlayerDiggingPacket,
|
||||||
@@ -29,7 +33,7 @@ from mosfet.info import items
|
|||||||
from mosfet.info import mcdata
|
from mosfet.info import mcdata
|
||||||
from mosfet.info import mobs
|
from mosfet.info import mobs
|
||||||
|
|
||||||
class Game:
|
class LegacyGame:
|
||||||
def __init__(self, global_state):
|
def __init__(self, global_state):
|
||||||
self.g = global_state
|
self.g = global_state
|
||||||
|
|
||||||
@@ -568,3 +572,115 @@ class Game:
|
|||||||
|
|
||||||
if not self.g.path:
|
if not self.g.path:
|
||||||
self.g.correction_count = 0
|
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
|
SAPLINGS = sorted(name for name in MCD_BLOCKS
|
||||||
if name.endswith('_sapling') or name.endswith('_propagule'))
|
if name.endswith('_sapling') or name.endswith('_propagule'))
|
||||||
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',
|
||||||
|
]
|
||||||
|
|
||||||
LOG_IDS = get_set(LOGS)
|
LOG_IDS = get_set(LOGS)
|
||||||
LEAF_IDS = get_set(LEAVES)
|
LEAF_IDS = get_set(LEAVES)
|
||||||
|
|||||||
+20
-1
@@ -55,7 +55,7 @@ for module in [
|
|||||||
importlib.reload(module)
|
importlib.reload(module)
|
||||||
|
|
||||||
|
|
||||||
class JobStates:
|
class LegacyJobStates:
|
||||||
def idle(self):
|
def idle(self):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -262,3 +262,22 @@ class JobStates:
|
|||||||
|
|
||||||
def tick(self):
|
def tick(self):
|
||||||
self.run_machines(self.state())
|
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,
|
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:
|
if crop not in self.bad_crops:
|
||||||
break
|
break
|
||||||
else: # for
|
else: # for
|
||||||
print('No good crops left, aborting.')
|
print('No good crops left; retrying after a short wait.')
|
||||||
self.state = self.cleanup
|
self.retry_after = time.monotonic() + 15.0
|
||||||
|
self.state = self.wait_for_crops
|
||||||
return
|
return
|
||||||
|
|
||||||
print('Found crop:', crop)
|
print('Found crop:', crop)
|
||||||
@@ -48,6 +51,10 @@ class GatherCropStates:
|
|||||||
self.type_id = w.block_at(*crop)
|
self.type_id = w.block_at(*crop)
|
||||||
self.state = self.nav_to_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):
|
def nav_to_crop(self):
|
||||||
w = self.g.world
|
w = self.g.world
|
||||||
p = utils.pint(self.g.pos)
|
p = utils.pint(self.g.pos)
|
||||||
@@ -71,8 +78,34 @@ class GatherCropStates:
|
|||||||
self.state = self.break_crop
|
self.state = self.break_crop
|
||||||
|
|
||||||
def break_crop(self):
|
def break_crop(self):
|
||||||
self.g.game.break_block(self.crop)
|
print('Requesting crop break at {}: block={}, mode={}, held={}'.format(
|
||||||
self.state = self.select_seed
|
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):
|
def select_seed(self):
|
||||||
p = utils.pint(self.g.pos)
|
p = utils.pint(self.g.pos)
|
||||||
@@ -86,9 +119,19 @@ class GatherCropStates:
|
|||||||
self.target_seed = crop_seeds[self.type_id]
|
self.target_seed = crop_seeds[self.type_id]
|
||||||
|
|
||||||
if self.g.game.select_item([self.target_seed]):
|
if self.g.game.select_item([self.target_seed]):
|
||||||
|
self.seed_deadline = None
|
||||||
self.state = self.wait_select
|
self.state = self.wait_select
|
||||||
else:
|
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
|
return
|
||||||
|
|
||||||
def wait_select(self):
|
def wait_select(self):
|
||||||
@@ -98,17 +141,25 @@ class GatherCropStates:
|
|||||||
self.state = self.place_crop
|
self.state = self.place_crop
|
||||||
|
|
||||||
def place_crop(self):
|
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)
|
self.g.game.place_block(p, BlockFace.TOP)
|
||||||
print('Placed crop')
|
print('Placed crop')
|
||||||
self.state = self.wait_place
|
self.state = self.wait_place
|
||||||
|
|
||||||
def wait_place(self):
|
def wait_place(self):
|
||||||
w = self.g.world
|
w = self.g.world
|
||||||
if w.block_at(*self.crop) == blocks.AIR:
|
current_state = w.block_at(*self.crop)
|
||||||
return
|
crop_name = blocks.BLOCKS.get(self.type_id)
|
||||||
else:
|
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
|
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):
|
def cleanup(self):
|
||||||
self.g.look_at = None
|
self.g.look_at = None
|
||||||
@@ -125,6 +176,9 @@ class GatherCropStates:
|
|||||||
self.crop = None
|
self.crop = None
|
||||||
self.type_id = None
|
self.type_id = None
|
||||||
self.target_seed = None
|
self.target_seed = None
|
||||||
|
self.seed_deadline = None
|
||||||
|
self.seed_wait_logged = False
|
||||||
|
self.retry_after = 0.0
|
||||||
self.bad_crops = []
|
self.bad_crops = []
|
||||||
|
|
||||||
def run(self):
|
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
|
import json
|
||||||
from math import floor
|
from math import floor
|
||||||
|
from munch import Munch
|
||||||
|
|
||||||
from minecraft.networking.packets import clientbound, serverbound
|
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 import packets
|
||||||
|
from mosfet.protocol.types import Slot
|
||||||
from mosfet.info import blocks, mcdata, mobs
|
from mosfet.info import blocks, mcdata, mobs
|
||||||
|
|
||||||
|
|
||||||
@@ -172,3 +182,298 @@ class ChatManager:
|
|||||||
packet = serverbound.play.ChatPacket()
|
packet = serverbound.play.ChatPacket()
|
||||||
packet.message = text
|
packet.message = text
|
||||||
self.g.connection.write_packet(packet)
|
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
|
yield check
|
||||||
|
|
||||||
def find_blocks_indexed(self, center, block_ids, distance=0):
|
def find_blocks_indexed(self, center, block_ids, distance=0):
|
||||||
print('finding', block_ids)
|
|
||||||
index = []
|
index = []
|
||||||
for bid in block_ids:
|
for bid in block_ids:
|
||||||
index.extend(self.g.chunks.index.get(bid, []))
|
index.extend(self.g.chunks.index.get(bid, []))
|
||||||
|
|
||||||
print('index', index)
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for block in index:
|
for block in index:
|
||||||
if self.block_at(*block) not in block_ids:
|
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