Consolidate bot runtime on Minecraft 26.3

This commit is contained in:
Tanner
2026-11-04 03:05:23 +00:00
parent b6f7ce9948
commit f7f58f5d07
15 changed files with 706 additions and 537 deletions
+14 -11
View File
@@ -1,11 +1,13 @@
# Mosfet Minecraft Bot
A general-purpose Minecraft 1.16 bot written in Python that uses an actual
Minecraft account to play.
A Minecraft 26.3 bot written in Python that uses an offline-mode account to
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
netherwart, and trade with villagers to get emeralds. He can eat, sleep, and
flee from threats.
The original bot included wood/sand/crop farming, villager trading, eating,
sleeping, and threat avoidance. On the current 26.3 runtime, path following and
crop harvest/replant are migrated; other legacy jobs remain unavailable until
ported to protocol 777.
## 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,
modern block/item/entity data, entity tracking, read-only inventory/component
sync, and path-following movement (`1goto x y z`). Inventory actions, block
breaking/placing, and farming/trading jobs remain disabled until their 26.3
packets are migrated. The default server is
sync, path-following movement (`1goto x y z`), and the crop harvest/replant loop
(`1farm crops`). General inventory/window actions and the other farming/trading
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
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
not migrated yet. Verified commands are `!ping`, `!pos`, `!loaded`, `!info x y
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;
inventory actions, block interaction, and farming/trading commands are not yet
available on 26.3. Common item components (names, damage, enchantments, book
`1goto x y z`, and `1farm crops`. The command catalog below is retained from
the legacy bot; other inventory actions, arbitrary block interaction, and the
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
marked partial rather than silently discarded.
+15 -5
View File
@@ -3,6 +3,7 @@ import json
import threading
import time
import traceback
import sys
import logging
log = logging.getLogger('werkzeug')
@@ -64,13 +65,22 @@ def root():
def main():
"""Connect with offline/AuthMe and enable only the migrated 26.3 features.
Current gameplay support is read-only world/entity/inventory state plus
path following; inventory actions, block interactions, and legacy job
packet handlers remain disabled until migrated.
Current gameplay support includes path following and a crop-only harvest /
replant job. General inventory actions, other block interactions, and
legacy jobs remain disabled until migrated.
"""
reload_timeout = time.time()
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):
nonlocal reload_timeout, reload_requested
if time.time() - reload_timeout > 2.0:
@@ -94,7 +104,7 @@ def main():
if not reload_requested:
break
reload_requested = False
importlib.reload(connect)
reload_modules()
except KeyboardInterrupt:
break
except BaseException:
@@ -104,7 +114,7 @@ def main():
while g.running:
time.sleep(1)
reload_requested = False
importlib.reload(connect)
reload_modules()
finally:
g.running = False
observer.stop()
-77
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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):
-102
View File
@@ -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]
-55
View File
@@ -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)
+305
View File
@@ -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)
-3
View File
@@ -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:
-220
View File
@@ -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