Begin port to 26.3: movement

This commit is contained in:
Tanner
2026-11-03 22:48:15 +00:00
parent 891f14b988
commit 59c76471f2
23 changed files with 30692 additions and 478 deletions
+14 -9
View File
@@ -18,7 +18,7 @@ install the connection-mode dependencies:
```
$ python3 -m venv .venv
$ source .venv/bin/activate
$ python -m pip install flask flask-cors munch watchdog -e ../pyCraft
$ python -m pip install astar flask flask-cors munch watchdog -e ../pyCraft
```
## Running
@@ -26,15 +26,20 @@ $ python -m pip install flask flask-cors munch watchdog -e ../pyCraft
Run the bot directly with Python. For the Protospace offline-mode server:
```
$ SERVER=games.protospace.ca python mosfet.py
$ SERVER=127.0.0.1 python mosfet.py
```
`mosfet.py` defaults to that server, player name `mosfet1`, and the current
AuthMe password. Override `SERVER`, `PORT`, `PLAYER_NAME`, or `GAME_PASSWORD`
through environment variables as needed. Microsoft authentication is not used.
The current entry point connects and handles AuthMe, but world automation is
not enabled yet: those modules still depend on 1.16-era packet and world data.
The 26.3 client now handles AuthMe, chat commands, chunk/world-state caching,
modern block/item/entity data, basic entity tracking, and path-following movement
(`1goto x y z`).
Inventory, entity tracking, block breaking/placing, and the 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.
## Usage
@@ -54,11 +59,11 @@ would be ran like `1farm wood` or `1pos`.
The exception are the below public commands, they can optionally be prefixed with `!`
and all bots will run the command.
Commands can be issued in public chat or private message like `/msg mosfet1
1farm wood`. The bot will reply the same way the command was issued.
Use `echo` to make Mosfet run Minecraft commands. For example, `1echo /sethome
home` will cause him to issue the `/sethome` 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`, and addressed path movement such as `1goto x y z`. The command catalog below
is retained from the legacy bot; inventory, interaction, and farming/trading
commands are not yet available on 26.3.
### Public Commands
+4 -3
View File
@@ -62,10 +62,11 @@ def root():
return app.send_static_file('index.html')
def main():
"""Connect using the current offline/AuthMe protocol bootstrap.
"""Connect with offline/AuthMe and enable only the migrated 26.3 features.
The legacy automation loop is deliberately not started yet: its packet
handlers still target Minecraft 1.16 and will be migrated incrementally.
Current gameplay support is read-only world/entity state plus path
following; inventory, interaction, and legacy job packet handlers remain
disabled until migrated.
"""
reload_timeout = time.time()
reload_requested = False
+77
View File
@@ -0,0 +1,77 @@
"""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
+31 -3
View File
@@ -65,12 +65,23 @@ def tick(global_state):
target = None
# make sure current chunks are loaded for physics
if not g.chunks.check_loaded(g.info.render_distance):
required_distance = getattr(g, 'physics_render_distance',
g.info.render_distance)
if not g.chunks.check_loaded(required_distance):
if not g.chunks.loading:
print('Loading chunks', end='', flush=True)
g.chunks.loading = time.time()
packet = serverbound.play.PositionAndLookPacket(x=p.x, feet_y=p.y, z=p.z, pitch=0, yaw=0, on_ground=True)
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)
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)
return
else:
if g.chunks.loading:
@@ -197,10 +208,27 @@ def tick(global_state):
target_pitch_d = 0 - g.pitch
g.pitch += utils.cap(target_pitch_d, 10)
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 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)
if g.job:
g.job.tick()
if g.game:
g.game.tick() # order important for correction_count
+7 -2
View File
@@ -39,7 +39,9 @@ class Commands:
text = text.replace('zzz', '!zzz')
match = re.match(r'(.*\W+)\s+(['+bot_num+'|!])(\w+) ?(.*)', text)
match = re.match(
r'(.*\W+)\s+([{}!])(\w+) ?(.*)'.format(re.escape(bot_num)),
text)
if match:
meta, prefix, command, data = match.groups()
else:
@@ -155,7 +157,10 @@ class Commands:
if command == 'loaded':
l = self.g.chunks.get_loaded_area()
reply = '{}, {}, {} to {}, {}, {}'.format(*l[0], *l[1])
reply += ' - ' + str(len(self.g.chunks.chunks)//16) + ' chunks'
chunk_count = getattr(
self.g.chunks, 'loaded_chunk_count',
len(self.g.chunks.chunks) // 16)
reply += ' - ' + str(chunk_count) + ' chunks'
## !players - prints the current players
## !players clear - clears the current player list
+49 -6
View File
@@ -3,11 +3,14 @@
Run with SERVER=... PLAYER_NAME=mosfet1 GAME_PASSWORD=... python -m mosfet.connect
"""
import os
import threading
from minecraft.networking.connection import Connection
from minecraft.networking.modern import (
CancelPostjoinDialog, ChatCommand, PlayShowDialog,
PlayLogin, SubmitPostjoinLoginDialog, SystemChat, TeleportPosition,
)
from mosfet.world_26_3 import WorldState
from mosfet.vector import Point3D
def component_text(value):
@@ -39,14 +42,17 @@ def component_text(value):
def main(global_state=None):
server = os.environ.get('SERVER', 'games.protospace.ca')
server = os.environ.get('SERVER', '127.0.0.1')
port = int(os.environ.get('PORT', '25565'))
username = os.environ.get('PLAYER_NAME', 'mosfet1')
password = os.environ.get('GAME_PASSWORD', 'mosfetbot')
connection = Connection(server, port, username=username,
allowed_versions=['26.3'])
allowed_versions=['26.3'],
handle_exception=False)
connection.authme_password = password
connection.world_state = WorldState()
connection.dimension_types_by_id = []
sent_auth_commands = set()
if global_state is not None:
global_state.connection = connection
@@ -54,12 +60,32 @@ def main(global_state=None):
global_state.connected = False
global_state.connection_status = 'connecting'
global_state.player_position = None
global_state.world_state = connection.world_state
global_state.chunks = connection.world_state
from mosfet.protocol.managers import DataManager
global_state.mcdata = DataManager()
@connection.listener(PlayLogin)
def on_play_login(packet):
if global_state is not None:
global_state.connected = True
global_state.connection_status = 'in_game'
global_state.player_id = packet.player_id
global_state.dimension = packet.dimension.removeprefix('minecraft:')
packet.render_distance = packet.chunk_radius
global_state.info = packet
global_state.eid = packet.player_id
global_state.command_lock = False
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.automation_26_3 import start as start_modern_movement
global_state.world = World(global_state)
global_state.chat = ModernChatManager(global_state)
global_state.entities = ModernEntityManager(global_state)
global_state.commands = Commands(global_state)
start_modern_movement(global_state)
def send_auth_command(action):
@@ -75,7 +101,15 @@ def main(global_state=None):
@connection.listener(TeleportPosition)
def on_spawn(packet):
if global_state is not None:
global_state.player_position = (packet.x, packet.y, packet.z)
position = getattr(connection, 'player_position', None)
if position is None:
position = (packet.x, packet.y, packet.z,
packet.yaw, packet.pitch)
x, y, z, yaw, pitch = position
global_state.player_position = (x, y, z)
global_state.pos = Point3D(global_state.player_position)
global_state.yaw = yaw
global_state.pitch = pitch
if getattr(connection, 'authme_mode', 'register') == 'register':
send_auth_command('register')
@@ -96,6 +130,9 @@ def main(global_state=None):
message = component_text(packet.content)
if global_state is not None:
global_state.last_chat = message
chat = global_state.get('chat')
if chat is not None:
chat.handle_system_chat(packet, message)
print('[server] ' + message, flush=True)
lowered = message.lower()
if 'already registered' in lowered:
@@ -111,26 +148,32 @@ def main(global_state=None):
connection.connect()
print('Connected as {} to {}:{} using offline protocol 777.'.format(
username, server, port), flush=True)
shutdown_requested = False
try:
while connection.connected:
if global_state is not None and not global_state.get('running', True):
shutdown_requested = True
break
thread = connection.networking_thread
if thread is None:
break
thread.join(timeout=0.5)
if connection.connected:
connection.disconnect()
if connection.exception:
connection.disconnect(immediate=True)
if connection.exception and not shutdown_requested:
raise connection.exception
print('Connection ended.', flush=True)
except KeyboardInterrupt:
shutdown_requested = True
print('Disconnecting...', flush=True)
connection.disconnect()
finally:
if connection.connected:
connection.disconnect()
connection.disconnect(immediate=True)
if global_state is not None:
worker = global_state.get('movement_thread')
if worker and worker is not threading.current_thread():
worker.join(timeout=1.0)
global_state.connection = False
global_state.connected = False
global_state.connection_status = 'disconnected'
+84 -273
View File
@@ -1,280 +1,84 @@
import json
import importlib
"""Minecraft 26.3 block-state metadata and legacy automation selectors."""
from itertools import product
from mosfet.info import mcdata
MCD_BLOCKS = {}
for d in mcdata.mcd.blocks.values():
MCD_BLOCKS[d['name']] = d
with open('minecraft_data/blocks.json') as f:
JSON_BLOCKS = json.load(f)
MCD_BLOCKS = mcdata.mcd.blocks
JSON_BLOCKS = {'minecraft:' + name: data
for name, data in MCD_BLOCKS.items()}
BLOCKS = {}
for name, data in JSON_BLOCKS.items():
for state in data['states']:
BLOCKS[state['id']] = name.replace('minecraft:', '')
PROPS = {}
for name, data in JSON_BLOCKS.items():
for state in data['states']:
PROPS[state['id']] = state.get('properties', {})
COLLISION_BOXES = {}
for name, data in MCD_BLOCKS.items():
first = data['minStateId']
last = data['maxStateId']
properties = data.get('states', [])
property_values = [prop.get('values', []) for prop in properties]
combinations = list(product(*property_values)) if property_values else [()]
for offset, state_id in enumerate(range(first, last + 1)):
BLOCKS[state_id] = name
if offset < len(combinations):
PROPS[state_id] = {
prop['name']: value
for prop, value in zip(properties, combinations[offset])
}
else:
PROPS[state_id] = {}
shape_ref = mcdata.mcd.collision_shapes['blocks'].get(name, 1)
refs = shape_ref if isinstance(shape_ref, list) else [shape_ref] * (last-first+1)
for offset, state_id in enumerate(range(first, last + 1)):
ref = refs[offset] if offset < len(refs) else refs[-1]
COLLISION_BOXES[state_id] = mcdata.mcd.collision_shapes['shapes'].get(
str(ref), [])
BREAK_DISTANCE = 6
AIR = 0
STONE = 1
SAND = 66
SINGLE_SNOW = 3921
SOUL_TORCH = 4008
EMERALD_BLOCK = 5407
AIR = MCD_BLOCKS['air']['minStateId']
STONE = MCD_BLOCKS['stone']['minStateId']
SAND = MCD_BLOCKS['sand']['minStateId']
SOUL_TORCH = MCD_BLOCKS['soul_torch']['minStateId']
EMERALD_BLOCK = MCD_BLOCKS['emerald_block']['minStateId']
SINGLE_SNOW = MCD_BLOCKS.get('snow', MCD_BLOCKS.get('snow_layer', {})).get(
'minStateId', AIR)
TEST_BLOCK = (616, 78, 496)
AVOID = [
'lava',
'water',
'fire',
'magma_block',
'oak_fence',
'oak_fence_gate',
'nether_brick_fence',
'spruce_fence_gate',
'birch_fence_gate',
'jungle_fence_gate',
'acacia_fence_gate',
'dark_oak_fence_gate',
'spruce_fence',
'birch_fence',
'jungle_fence',
'acacia_fence',
'dark_oak_fence',
'sweet_berry_bush',
'nether_portal',
'end_portal',
'cobblestone_wall',
'mossy_cobblestone_wall',
'brick_wall',
'prismarine_wall',
'red_sandstone_wall',
'mossy_stone_brick_wall',
'granite_wall',
'stone_brick_wall',
'nether_brick_wall',
'andesite_wall',
'red_nether_brick_wall',
'sandstone_wall',
'end_stone_brick_wall',
'diorite_wall',
'oak_sapling', # saplings can grow up and hurt
'spruce_sapling',
'birch_sapling',
'jungle_sapling',
'acacia_sapling',
'dark_oak_sapling',
]
NON_SOLID = [
'air',
'powered_rail',
'detector_rail',
'grass',
'fern',
'dead_bush',
'seagrass',
'tall_seagrass',
'dandelion',
'poppy',
'blue_orchid',
'allium',
'azure_bluet',
'red_tulip',
'orange_tulip',
'white_tulip',
'pink_tulip',
'oxeye_daisy',
'cornflower',
'wither_rose',
'lily_of_the_valley',
'brown_mushroom',
'red_mushroom',
'torch',
'wall_torch',
'redstone_wire',
'wheat',
'oak_sign',
'spruce_sign',
'birch_sign',
'acacia_sign',
'jungle_sign',
'dark_oak_sign',
'rail',
'oak_wall_sign',
'spruce_wall_sign',
'birch_wall_sign',
'acacia_wall_sign',
'jungle_wall_sign',
'dark_oak_wall_sign',
'lever',
'stone_pressure_plate',
'oak_pressure_plate',
'spruce_pressure_plate',
'birch_pressure_plate',
'jungle_pressure_plate',
'acacia_pressure_plate',
'dark_oak_pressure_plate',
'redstone_torch',
'redstone_wall_torch',
'stone_button',
'sugar_cane',
'repeater',
'attached_pumpkin_stem',
'attached_melon_stem',
'pumpkin_stem',
'melon_stem',
'nether_wart',
'tripwire_hook',
'tripwire',
'carrots',
'potatoes',
'oak_button',
'spruce_button',
'birch_button',
'jungle_button',
'acacia_button',
'dark_oak_button',
'light_weighted_pressure_plate',
'heavy_weighted_pressure_plate',
'comparator',
'activator_rail',
'white_carpet',
'orange_carpet',
'magenta_carpet',
'light_blue_carpet',
'yellow_carpet',
'lime_carpet',
'pink_carpet',
'gray_carpet',
'light_gray_carpet',
'cyan_carpet',
'purple_carpet',
'blue_carpet',
'brown_carpet',
'green_carpet',
'red_carpet',
'black_carpet',
'sunflower',
'lilac',
'rose_bush',
'peony',
'tall_grass',
'large_fern',
'white_banner',
'orange_banner',
'magenta_banner',
'light_blue_banner',
'yellow_banner',
'lime_banner',
'pink_banner',
'gray_banner',
'light_gray_banner',
'cyan_banner',
'purple_banner',
'blue_banner',
'brown_banner',
'green_banner',
'red_banner',
'black_banner',
'white_wall_banner',
'orange_wall_banner',
'magenta_wall_banner',
'light_blue_wall_banner',
'yellow_wall_banner',
'lime_wall_banner',
'pink_wall_banner',
'gray_wall_banner',
'light_gray_wall_banner',
'cyan_wall_banner',
'purple_wall_banner',
'blue_wall_banner',
'brown_wall_banner',
'green_wall_banner',
'red_wall_banner',
'black_wall_banner',
'beetroots',
'bamboo_sapling',
'void_air',
'cave_air',
'lantern',
'soul_torch',
]
LOGS = [
'oak_log',
'spruce_log',
'birch_log',
'jungle_log',
'acacia_log',
'dark_oak_log',
]
LEAVES = [
'oak_leaves',
'spruce_leaves',
'birch_leaves',
'jungle_leaves',
'acacia_leaves',
'dark_oak_leaves',
]
SAPLINGS = [
'oak_sapling',
'spruce_sapling',
'birch_sapling',
'jungle_sapling',
'acacia_sapling',
'dark_oak_sapling',
]
BEDS = [
'white_bed',
'orange_bed',
'magenta_bed',
'light_blue_bed',
'yellow_bed',
'lime_bed',
'pink_bed',
'gray_bed',
'light_gray_bed',
'cyan_bed',
'purple_bed',
'blue_bed',
'brown_bed',
'green_bed',
'red_bed',
'black_bed',
]
INDEXED = [
'chest',
'trapped_chest',
'emerald_block',
'barrel',
] + BEDS
def get_set(ids):
def get_set(names):
"""Return every state ID belonging to the named blocks; ignore removed names."""
result = set()
for block_name in ids:
for state in JSON_BLOCKS['minecraft:' + block_name]['states']:
result.add(state['id'])
for name in names:
name = name.removeprefix('minecraft:')
data = MCD_BLOCKS.get(name)
if data is not None:
result.update(range(data['minStateId'], data['maxStateId'] + 1))
return result
NON_SOLID_IDS = get_set(NON_SOLID)
NON_SOLID_IDS.add(SINGLE_SNOW)
# Collision geometry is safer for navigation than the old hard-coded 1.16 list.
NON_SOLID_IDS = {
state_id for state_id, boxes in COLLISION_BOXES.items() if not boxes
}
AVOID = {
'lava', 'water', 'fire', 'magma_block', 'sweet_berry_bush',
'nether_portal', 'end_portal', 'cactus', 'powder_snow',
}
AVOID_IDS = get_set(AVOID)
AVOID_IDS.update(
state_id for state_id, name in BLOCKS.items()
if name.endswith(('_fence', '_fence_gate', '_wall'))
)
WATER_IDS = get_set(['water'])
LOGS = sorted(name for name in MCD_BLOCKS
if name.endswith(('_log', '_wood', '_stem', '_hyphae')))
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
LOG_IDS = get_set(LOGS)
LEAF_IDS = get_set(LEAVES)
CHEST_IDS = get_set(['chest'])
@@ -287,21 +91,28 @@ CARROT_IDS = get_set(['carrots'])
BEETROOT_IDS = get_set(['beetroots'])
SAPLING_IDS = get_set(SAPLINGS)
BED_IDS = get_set(BEDS)
INDEXED_IDS = set()
for block_name in INDEXED:
for state in JSON_BLOCKS['minecraft:' + block_name]['states']:
INDEXED_IDS.add(state['id'])
INDEXED_IDS = get_set(INDEXED)
MATURE_WHEAT_ID = max(WHEAT_IDS)
MATURE_POTATO_ID = max(POTATO_IDS)
MATURE_CARROT_ID = max(CARROT_IDS)
MATURE_BEETROOT_ID = max(BEETROOT_IDS)
def _mature_state(name, property_name, mature_value):
data = MCD_BLOCKS.get(name)
if data is None:
return None
for state_id in range(data['minStateId'], data['maxStateId'] + 1):
if PROPS[state_id].get(property_name) == str(mature_value):
return state_id
return data['maxStateId']
MATURE_WHEAT_ID = _mature_state('wheat', 'age', 7)
MATURE_POTATO_ID = _mature_state('potatoes', 'age', 7)
MATURE_CARROT_ID = _mature_state('carrots', 'age', 7)
MATURE_BEETROOT_ID = _mature_state('beetroots', 'age', 3)
def get(block_state_id):
return MCD_BLOCKS[BLOCKS[block_state_id]]
def get(bid):
name = BLOCKS[bid]
return MCD_BLOCKS[name]
def find(name):
return MCD_BLOCKS[name]
return MCD_BLOCKS[name.removeprefix('minecraft:')]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 grepsedawk <grepsedawk@users.noreply.github.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+7
View File
@@ -0,0 +1,7 @@
Minecraft 26.3 data files
Source: RosegoldMC/minecraft-data.cr, data/26.3
https://github.com/RosegoldMC/minecraft-data.cr/tree/main/data/26.3
The JSON files in this directory are redistributed under the MIT License;
see LICENSE. They are generated data, not Mojang source code.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
[
{"id":0,"name":"aqua_affinity"},
{"id":1,"name":"bane_of_arthropods"},
{"id":2,"name":"binding_curse"},
{"id":3,"name":"blast_protection"},
{"id":4,"name":"breach"},
{"id":5,"name":"channeling"},
{"id":6,"name":"density"},
{"id":7,"name":"depth_strider"},
{"id":8,"name":"efficiency"},
{"id":9,"name":"feather_falling"},
{"id":10,"name":"fire_aspect"},
{"id":11,"name":"fire_protection"},
{"id":12,"name":"flame"},
{"id":13,"name":"fortune"},
{"id":14,"name":"frost_walker"},
{"id":15,"name":"impaling"},
{"id":16,"name":"infinity"},
{"id":17,"name":"knockback"},
{"id":18,"name":"looting"},
{"id":19,"name":"loyalty"},
{"id":20,"name":"luck_of_the_sea"},
{"id":21,"name":"lunge"},
{"id":22,"name":"lure"},
{"id":23,"name":"mending"},
{"id":24,"name":"multishot"},
{"id":25,"name":"piercing"},
{"id":26,"name":"power"},
{"id":27,"name":"projectile_protection"},
{"id":28,"name":"protection"},
{"id":29,"name":"punch"},
{"id":30,"name":"quick_charge"},
{"id":31,"name":"respiration"},
{"id":32,"name":"riptide"},
{"id":33,"name":"sharpness"},
{"id":34,"name":"silk_touch"},
{"id":35,"name":"smite"},
{"id":36,"name":"soul_speed"},
{"id":37,"name":"sweeping_edge"},
{"id":38,"name":"swift_sneak"},
{"id":39,"name":"thorns"},
{"id":40,"name":"unbreaking"},
{"id":41,"name":"vanishing_curse"},
{"id":42,"name":"wind_burst"}
]
+163
View File
@@ -0,0 +1,163 @@
[
{"id":0,"name":"acacia_boat","width":1.375,"height":0.5625},
{"id":1,"name":"acacia_chest_boat","width":1.375,"height":0.5625},
{"id":2,"name":"allay","width":0.35,"height":0.6},
{"id":3,"name":"area_effect_cloud","width":6,"height":0.5},
{"id":4,"name":"armadillo","width":0.7,"height":0.65},
{"id":5,"name":"armor_stand","width":0.5,"height":1.975},
{"id":6,"name":"arrow","width":0.5,"height":0.5},
{"id":7,"name":"axolotl","width":0.75,"height":0.42},
{"id":8,"name":"bamboo_chest_raft","width":1.375,"height":0.5625},
{"id":9,"name":"bamboo_raft","width":1.375,"height":0.5625},
{"id":10,"name":"bat","width":0.5,"height":0.9},
{"id":11,"name":"bee","width":0.7,"height":0.6},
{"id":12,"name":"birch_boat","width":1.375,"height":0.5625},
{"id":13,"name":"birch_chest_boat","width":1.375,"height":0.5625},
{"id":14,"name":"blaze","width":0.6,"height":1.8},
{"id":15,"name":"block_display","width":0,"height":0},
{"id":16,"name":"bogged","width":0.6,"height":1.99},
{"id":17,"name":"breeze","width":0.6,"height":1.77},
{"id":18,"name":"breeze_wind_charge","width":0.3125,"height":0.3125},
{"id":19,"name":"camel","width":1.7,"height":2.375},
{"id":20,"name":"camel_husk","width":1.7,"height":2.375},
{"id":21,"name":"cat","width":0.6,"height":0.7},
{"id":22,"name":"cave_spider","width":0.7,"height":0.5},
{"id":23,"name":"cherry_boat","width":1.375,"height":0.5625},
{"id":24,"name":"cherry_chest_boat","width":1.375,"height":0.5625},
{"id":25,"name":"chest_minecart","width":0.98,"height":0.7},
{"id":26,"name":"chicken","width":0.4,"height":0.7},
{"id":27,"name":"cod","width":0.5,"height":0.3},
{"id":28,"name":"copper_golem","width":0.49,"height":0.98},
{"id":29,"name":"command_block_minecart","width":0.98,"height":0.7},
{"id":30,"name":"cow","width":0.9,"height":1.4},
{"id":31,"name":"creaking","width":0.9,"height":2.7},
{"id":32,"name":"creeper","width":0.6,"height":1.7},
{"id":33,"name":"cushion","width":1.0,"height":0.25,"type":"other","category":"Other"},
{"id":34,"name":"dark_oak_boat","width":1.375,"height":0.5625},
{"id":35,"name":"dark_oak_chest_boat","width":1.375,"height":0.5625},
{"id":36,"name":"dolphin","width":0.9,"height":0.6},
{"id":37,"name":"donkey","width":1.3964844,"height":1.5},
{"id":38,"name":"dragon_fireball","width":1,"height":1},
{"id":39,"name":"drowned","width":0.6,"height":1.95},
{"id":40,"name":"egg","width":0.25,"height":0.25},
{"id":41,"name":"elder_guardian","width":1.9975,"height":1.9975},
{"id":42,"name":"enderman","width":0.6,"height":2.9},
{"id":43,"name":"endermite","width":0.4,"height":0.3},
{"id":44,"name":"ender_dragon","width":16,"height":8},
{"id":45,"name":"ender_pearl","width":0.25,"height":0.25},
{"id":46,"name":"end_crystal","width":2,"height":2},
{"id":47,"name":"evoker","width":0.6,"height":1.95},
{"id":48,"name":"evoker_fangs","width":0.5,"height":0.8},
{"id":49,"name":"experience_bottle","width":0.25,"height":0.25},
{"id":50,"name":"experience_orb","width":0.5,"height":0.5},
{"id":51,"name":"eye_of_ender","width":0.25,"height":0.25},
{"id":52,"name":"falling_block","width":0.98,"height":0.98},
{"id":53,"name":"fireball","width":1,"height":1},
{"id":54,"name":"firework_rocket","width":0.25,"height":0.25},
{"id":55,"name":"fox","width":0.6,"height":0.7},
{"id":56,"name":"frog","width":0.5,"height":0.5},
{"id":57,"name":"furnace_minecart","width":0.98,"height":0.7},
{"id":58,"name":"ghast","width":4,"height":4},
{"id":59,"name":"happy_ghast","width":4,"height":4},
{"id":60,"name":"giant","width":3.6,"height":12},
{"id":61,"name":"glow_item_frame","width":0.5,"height":0.5},
{"id":62,"name":"glow_squid","width":0.8,"height":0.8},
{"id":63,"name":"goat","width":0.9,"height":1.3},
{"id":64,"name":"guardian","width":0.85,"height":0.85},
{"id":65,"name":"hoglin","width":1.3964844,"height":1.4},
{"id":66,"name":"hopper_minecart","width":0.98,"height":0.7},
{"id":67,"name":"horse","width":1.3964844,"height":1.6},
{"id":68,"name":"husk","width":0.6,"height":1.95},
{"id":69,"name":"illusioner","width":0.6,"height":1.95},
{"id":70,"name":"interaction","width":0,"height":0},
{"id":71,"name":"iron_golem","width":1.4,"height":2.7},
{"id":72,"name":"item","width":0.25,"height":0.25},
{"id":73,"name":"item_display","width":0,"height":0},
{"id":74,"name":"item_frame","width":0.5,"height":0.5},
{"id":75,"name":"jungle_boat","width":1.375,"height":0.5625},
{"id":76,"name":"jungle_chest_boat","width":1.375,"height":0.5625},
{"id":77,"name":"leash_knot","width":0.375,"height":0.5},
{"id":78,"name":"lightning_bolt","width":0,"height":0},
{"id":79,"name":"llama","width":0.9,"height":1.87},
{"id":80,"name":"llama_spit","width":0.25,"height":0.25},
{"id":81,"name":"magma_cube","width":0.52,"height":0.52},
{"id":82,"name":"mangrove_boat","width":1.375,"height":0.5625},
{"id":83,"name":"mangrove_chest_boat","width":1.375,"height":0.5625},
{"id":84,"name":"mannequin","width":0.6,"height":1.8},
{"id":85,"name":"marker","width":0,"height":0},
{"id":86,"name":"minecart","width":0.98,"height":0.7},
{"id":87,"name":"mooshroom","width":0.9,"height":1.4},
{"id":88,"name":"mule","width":1.3964844,"height":1.6},
{"id":89,"name":"nautilus","width":0.875,"height":0.95},
{"id":90,"name":"oak_boat","width":1.375,"height":0.5625},
{"id":91,"name":"oak_chest_boat","width":1.375,"height":0.5625},
{"id":92,"name":"ocelot","width":0.6,"height":0.7},
{"id":93,"name":"ominous_item_spawner","width":0.25,"height":0.25},
{"id":94,"name":"painting","width":0.5,"height":0.5},
{"id":95,"name":"pale_oak_boat","width":1.375,"height":0.5625},
{"id":96,"name":"pale_oak_chest_boat","width":1.375,"height":0.5625},
{"id":97,"name":"panda","width":1.3,"height":1.25},
{"id":98,"name":"parched","width":0.6,"height":1.99},
{"id":99,"name":"parrot","width":0.5,"height":0.9},
{"id":100,"name":"phantom","width":0.9,"height":0.5},
{"id":101,"name":"pig","width":0.9,"height":0.9},
{"id":102,"name":"piglin","width":0.6,"height":1.95},
{"id":103,"name":"piglin_brute","width":0.6,"height":1.95},
{"id":104,"name":"pillager","width":0.6,"height":1.95},
{"id":105,"name":"polar_bear","width":1.4,"height":1.4},
{"id":106,"name":"poplar_boat","width":1.375,"height":0.5625,"type":"other","category":"Vehicles"},
{"id":107,"name":"poplar_chest_boat","width":1.375,"height":0.5625,"type":"other","category":"Vehicles"},
{"id":108,"name":"splash_potion","width":0.25,"height":0.25},
{"id":109,"name":"lingering_potion","width":0.25,"height":0.25},
{"id":110,"name":"pufferfish","width":0.7,"height":0.7},
{"id":111,"name":"rabbit","width":0.4,"height":0.5},
{"id":112,"name":"ravager","width":1.95,"height":2.2},
{"id":113,"name":"salmon","width":0.7,"height":0.4},
{"id":114,"name":"sheep","width":0.9,"height":1.3},
{"id":115,"name":"shulker","width":1,"height":1},
{"id":116,"name":"shulker_bullet","width":0.3125,"height":0.3125},
{"id":117,"name":"silverfish","width":0.4,"height":0.3},
{"id":118,"name":"skeleton","width":0.6,"height":1.99},
{"id":119,"name":"skeleton_horse","width":1.3964844,"height":1.6},
{"id":120,"name":"slime","width":0.52,"height":0.52},
{"id":121,"name":"small_fireball","width":0.3125,"height":0.3125},
{"id":122,"name":"sniffer","width":1.9,"height":1.75},
{"id":123,"name":"snowball","width":0.25,"height":0.25},
{"id":124,"name":"snow_golem","width":0.7,"height":1.9},
{"id":125,"name":"spawner_minecart","width":0.98,"height":0.7},
{"id":126,"name":"spectral_arrow","width":0.5,"height":0.5},
{"id":127,"name":"spider","width":1.4,"height":0.9},
{"id":128,"name":"spruce_boat","width":1.375,"height":0.5625},
{"id":129,"name":"spruce_chest_boat","width":1.375,"height":0.5625},
{"id":130,"name":"squid","width":0.8,"height":0.8},
{"id":131,"name":"stray","width":0.6,"height":1.99},
{"id":132,"name":"strider","width":0.9,"height":1.7},
{"id":133,"name":"sulfur_cube","width":0.49,"height":0.49},
{"id":134,"name":"tadpole","width":0.4,"height":0.3},
{"id":135,"name":"text_display","width":0,"height":0},
{"id":136,"name":"tnt","width":0.98,"height":0.98},
{"id":137,"name":"tnt_minecart","width":0.98,"height":0.7},
{"id":138,"name":"trader_llama","width":0.9,"height":1.87},
{"id":139,"name":"trident","width":0.5,"height":0.5},
{"id":140,"name":"tropical_fish","width":0.5,"height":0.4},
{"id":141,"name":"turtle","width":1.2,"height":0.4},
{"id":142,"name":"vex","width":0.4,"height":0.8},
{"id":143,"name":"villager","width":0.6,"height":1.95},
{"id":144,"name":"vindicator","width":0.6,"height":1.95},
{"id":145,"name":"wandering_trader","width":0.6,"height":1.95},
{"id":146,"name":"warden","width":0.9,"height":2.9},
{"id":147,"name":"wind_charge","width":0.3125,"height":0.3125},
{"id":148,"name":"witch","width":0.6,"height":1.95},
{"id":149,"name":"wither","width":0.9,"height":3.5},
{"id":150,"name":"wither_skeleton","width":0.7,"height":2.4},
{"id":151,"name":"wither_skull","width":0.3125,"height":0.3125},
{"id":152,"name":"wolf","width":0.6,"height":0.85},
{"id":153,"name":"zoglin","width":1.3964844,"height":1.4},
{"id":154,"name":"zombie","width":0.6,"height":1.95},
{"id":155,"name":"zombie_horse","width":1.3964844,"height":1.6},
{"id":156,"name":"zombie_nautilus","width":0.875,"height":0.95},
{"id":157,"name":"zombie_villager","width":0.6,"height":1.95},
{"id":158,"name":"zombified_piglin","width":0.6,"height":1.95},
{"id":159,"name":"player","width":0.6,"height":1.8},
{"id":160,"name":"fishing_bobber","width":0.25,"height":0.25}
]
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
{
"default":{},
"sword_instantly_mines":{"1025":2.0,"1030":1.0,"1035":4.0,"1040":12.0,"1045":6.0,"1050":8.0,"1055":9.0},
"sword_efficient":{"1025":2.0,"1030":1.0,"1035":4.0,"1040":12.0,"1045":6.0,"1050":8.0,"1055":9.0},
"incorrect_for_wooden_tool":{"1026":2.0,"1027":2.0,"1028":2.0,"1029":2.0},
"mineable/shovel":{"1026":2.0,"1031":1.0,"1036":4.0,"1041":12.0,"1046":6.0,"1051":8.0,"1056":9.0},
"mineable/pickaxe":{"1027":2.0,"1032":1.0,"1037":4.0,"1042":12.0,"1047":6.0,"1052":8.0,"1057":9.0},
"mineable/axe":{"1028":2.0,"1033":1.0,"1038":4.0,"1043":12.0,"1048":6.0,"1053":8.0,"1058":9.0},
"mineable/hoe":{"1029":2.0,"1034":1.0,"1039":4.0,"1044":12.0,"1049":6.0,"1054":8.0,"1059":9.0},
"incorrect_for_copper_tool":{"1031":1.0,"1032":1.0,"1033":1.0,"1034":1.0},
"incorrect_for_stone_tool":{"1036":4.0,"1037":4.0,"1038":4.0,"1039":4.0},
"incorrect_for_gold_tool":{"1041":12.0,"1042":12.0,"1043":12.0,"1044":12.0},
"incorrect_for_iron_tool":{"1046":6.0,"1047":6.0,"1048":6.0,"1049":6.0},
"incorrect_for_diamond_tool":{"1051":8.0,"1052":8.0,"1053":8.0,"1054":8.0},
"incorrect_for_netherite_tool":{"1056":9.0,"1057":9.0,"1058":9.0,"1059":9.0},
"shears_extreme_breaking_speed":{"1255":1.0},
"shears_major_breaking_speed":{"1255":1.0},
"shears_minor_breaking_speed":{"1255":1.0},
"leaves":{"1255":15.0,"1025":1.5,"1030":1.5,"1035":1.5,"1040":1.5,"1045":1.5,"1050":1.5,"1055":1.5},
"coweb":{"1255":15.0,"1025":15.0,"1030":15.0,"1035":15.0,"1040":15.0,"1045":15.0,"1050":15.0,"1055":15.0},
"vine_or_glow_lichen":{"1255":2.0},
"wool":{"1255":5.0},
"plant":{"1025":1.5,"1030":1.5,"1035":1.5,"1040":1.5,"1045":1.5,"1050":1.5,"1055":1.5},
"gourd":{"1025":1.5,"1030":1.5,"1035":1.5,"1040":1.5,"1045":1.5,"1050":1.5,"1055":1.5},
"plant;mineable/axe":{"1025":1.5,"1030":1.5,"1035":1.5,"1040":1.5,"1045":1.5,"1050":1.5,"1055":1.5,"1028":2.0,"1033":1.0,"1038":4.0,"1043":12.0,"1048":6.0,"1053":8.0,"1058":9.0},
"gourd;mineable/axe":{"1025":1.5,"1030":1.5,"1035":1.5,"1040":1.5,"1045":1.5,"1050":1.5,"1055":1.5,"1028":2.0,"1033":1.0,"1038":4.0,"1043":12.0,"1048":6.0,"1053":8.0,"1058":9.0},
"leaves;mineable/hoe":{"1255":15.0,"1025":1.5,"1030":1.5,"1035":1.5,"1040":1.5,"1045":1.5,"1050":1.5,"1055":1.5,"1029":2.0,"1034":1.0,"1039":4.0,"1044":12.0,"1049":6.0,"1054":8.0,"1059":9.0},
"leaves;mineable/axe;mineable/hoe":{
"1255":15.0,
"1025":1.5,
"1030":1.5,
"1035":1.5,
"1040":1.5,
"1045":1.5,
"1050":1.5,
"1055":1.5,
"1028":2.0,
"1033":1.0,
"1038":4.0,
"1043":12.0,
"1048":6.0,
"1053":8.0,
"1058":9.0,
"1029":2.0,
"1034":1.0,
"1039":4.0,
"1044":12.0,
"1049":6.0,
"1054":8.0,
"1059":9.0
},
"vine_or_glow_lichen;plant;mineable/axe":{"1255":2.0,"1025":1.5,"1030":1.5,"1035":1.5,"1040":1.5,"1045":1.5,"1050":1.5,"1055":1.5,"1028":2.0,"1033":1.0,"1038":4.0,"1043":12.0,"1048":6.0,"1053":8.0,"1058":9.0}
}
+32 -69
View File
@@ -1,78 +1,45 @@
import json
"""Minecraft 26.3 item registry data and automation selectors."""
from mosfet.info import mcdata
with open('minecraft_data/registries.json') as f:
ITEMS = json.load(f)['minecraft:item']['entries']
ITEMS = {
'minecraft:' + name: dict(item, protocol_id=item['id'])
for name, item in mcdata.mcd.items.items()
}
BEDS = [
'white_bed',
'orange_bed',
'magenta_bed',
'light_blue_bed',
'yellow_bed',
'lime_bed',
'pink_bed',
'gray_bed',
'light_gray_bed',
'cyan_bed',
'purple_bed',
'blue_bed',
'brown_bed',
'green_bed',
'red_bed',
'black_bed',
'white_bed', 'orange_bed', 'magenta_bed', 'light_blue_bed',
'yellow_bed', 'lime_bed', 'pink_bed', 'gray_bed', 'light_gray_bed',
'cyan_bed', 'purple_bed', 'blue_bed', 'brown_bed', 'green_bed',
'red_bed', 'black_bed',
]
SHOVELS = [
'wooden_shovel',
'stone_shovel',
'golden_shovel',
'iron_shovel',
'diamond_shovel',
'netherite_shovel',
'wooden_shovel', 'stone_shovel', 'golden_shovel', 'iron_shovel',
'diamond_shovel', 'netherite_shovel',
]
AXES = [
'wooden_axe',
'stone_axe',
'golden_axe',
'iron_axe',
'diamond_axe',
'netherite_axe',
'wooden_axe', 'stone_axe', 'golden_axe', 'iron_axe',
'diamond_axe', 'netherite_axe',
]
SAPLINGS = [
'oak_sapling',
'spruce_sapling',
'birch_sapling',
'jungle_sapling',
'acacia_sapling',
'dark_oak_sapling',
name for name in mcdata.mcd.blocks
if name.endswith('_sapling') or name.endswith('_propagule')
]
FOOD = [
'cooked_porkchop',
'cooked_beef',
'bread',
'cooked_chicken',
'cooked_cod',
'cooked_salmon',
'cooked_porkchop', 'cooked_beef', 'bread', 'cooked_chicken',
'cooked_cod', 'cooked_salmon',
]
LOGS = [
'oak_log',
'spruce_log',
'birch_log',
'jungle_log',
'acacia_log',
'dark_oak_log',
name for name in mcdata.mcd.blocks
if name.endswith(('_log', '_wood', '_stem', '_hyphae'))
]
def get_set(ids):
result = set()
for item_name in ids:
result.add(ITEMS['minecraft:'+item_name]['protocol_id'])
return result
def get_set(names):
return {
ITEMS['minecraft:' + name]['protocol_id']
for name in names if 'minecraft:' + name in ITEMS
}
BED_IDS = get_set(BEDS)
SHOVEL_IDS = get_set(SHOVELS)
@@ -80,23 +47,22 @@ AXE_IDS = get_set(AXES)
FOOD_IDS = get_set(FOOD)
SAPLING_IDS = get_set(SAPLINGS)
LOG_IDS = get_set(LOGS)
ITEM_NAMES = {}
for item_name, item in ITEMS.items():
ITEM_NAMES[ITEMS[item_name]['protocol_id']] = item_name.replace('minecraft:', '')
ITEM_NAMES = {item['protocol_id']: name.removeprefix('minecraft:')
for name, item in ITEMS.items()}
def get_id(name):
return ITEMS['minecraft:' + name]['protocol_id']
def get_name(idx):
return ITEM_NAMES[idx]
CHEST_ID = get_id('chest')
GAPPLE_ID = get_id('enchanted_golden_apple')
SAND_ID = get_id('sand')
NETHERWART_ID = get_id('nether_wart')
CARROT_ID = get_id('carrot')
POTATO_ID = get_id('potato')
WHEAT_ID = get_id('wheat')
@@ -104,25 +70,22 @@ WHEAT_SEEDS_ID = get_id('wheat_seeds')
BEETROOT_SEEDS_ID = get_id('beetroot_seeds')
PUMPKIN_ID = get_id('pumpkin')
BEETROOT_ID = get_id('beetroot')
EMERALD_ID = get_id('emerald')
BERRIES_ID = get_id('sweet_berries')
IRON_INGOT_ID = get_id('iron_ingot')
INIT_NEEDED_ITEMS = BED_IDS | FOOD_IDS
INIT_NEEDED_ITEMS.add(CHEST_ID)
NEEDED_ITEMS = INIT_NEEDED_ITEMS
INIT_WANTED_ITEMS = set()
WANTED_ITEMS = INIT_WANTED_ITEMS
def set_needed(items):
global NEEDED_ITEMS
NEEDED_ITEMS = INIT_NEEDED_ITEMS | items
def set_wanted(items):
global WANTED_ITEMS
WANTED_ITEMS = INIT_WANTED_ITEMS | items
+30 -26
View File
@@ -1,32 +1,36 @@
"""Minecraft 26.3 client data used by the automation layer.
The generated data is vendored under ``mosfet/info/data/26.3`` from
RosegoldMC/minecraft-data.cr (MIT); see that directory's LICENSE.
"""
import json
from munch import Munch
from pathlib import Path
from types import SimpleNamespace
import minecraft_data
DATA_VERSION = '26.3'
DATA_DIR = Path(__file__).resolve().parent / 'data' / DATA_VERSION
mcd = minecraft_data('1.16.4')
with open('minecraft_data/registries.json') as f:
DATA = json.load(f)
def _load(name):
with (DATA_DIR / name).open(encoding='utf-8') as source:
return json.load(source)
SINGLE_CHEST = 2
DOUBLE_CHEST = 5
VILLAGER_TRADE = 18
WINDOWS = {
SINGLE_CHEST: Munch(
container=list(range(0, 27)),
inventory=list(range(27, 63)),
slot_diff=18,
),
DOUBLE_CHEST: Munch(
container=list(range(0, 54)),
inventory=list(range(54, 90)),
slot_diff=45,
),
VILLAGER_TRADE: Munch(
input1=0,
input2=1,
output=2,
inventory=list(range(3, 38)),
),
}
block_list = _load('blocks.json')
item_list = _load('items.json')
entity_list = _load('entities.json')
materials = _load('materials.json')
collision_shapes = _load('blockCollisionShapes.json')
blocks = {block['name']: block for block in block_list}
items = {item['name']: item for item in item_list}
entities = {entity['name']: entity for entity in entity_list}
mcd = SimpleNamespace(
version=DATA_VERSION,
blocks=blocks,
items=items,
entities=entities,
materials=materials,
collision_shapes=collision_shapes,
)
+22 -49
View File
@@ -1,10 +1,13 @@
from mosfet.info import items
import json
"""Minecraft 26.3 entity registry data and legacy mob selectors."""
from mosfet.info import items, mcdata
with open('minecraft_data/registries.json') as f:
MOBS = json.load(f)['minecraft:entity_type']['entries']
MOBS = {
'minecraft:' + name: dict(entity, protocol_id=entity['id'])
for name, entity in mcdata.mcd.entities.items()
}
VILLAGER = MOBS['minecraft:villager']['protocol_id']
VILLAGER = 93
# Villager profession enum values remain the protocol's 0-based profession IDs.
ARMORER = 1
BUTCHER = 2
CARTOGRAPHER = 3
@@ -23,53 +26,23 @@ WEAPONSMITH = 14
TRADES = {
ARMORER: [items.IRON_INGOT_ID],
BUTCHER: [items.BERRIES_ID],
FARMER: [items.PUMPKIN_ID, items.WHEAT_ID, items.POTATO_ID, items.CARROT_ID, items.BEETROOT_ID],
FARMER: [items.PUMPKIN_ID, items.WHEAT_ID, items.POTATO_ID,
items.CARROT_ID, items.BEETROOT_ID],
TOOLSMITH: [items.IRON_INGOT_ID],
WEAPONSMITH: [items.IRON_INGOT_ID],
}
EVIL = [
'blaze',
'cave_spider',
'creeper',
'drowned',
'elder_guardian',
'ender_dragon',
'enderman',
'endermite',
'evoker',
'ghast',
'giant',
'guardian',
'hoglin',
'husk',
'illusioner',
'magma_cube',
'phantom',
'piglin',
'piglin_brute',
'pillager',
'ravager',
'shulker',
'silverfish',
'skeleton',
'skeleton_horse',
'slime',
'spider',
'stray',
'vex',
'vindicator',
'witch',
'wither',
'zoglin',
'zombie',
'zombie_villager',
'blaze', 'cave_spider', 'creeper', 'drowned', 'elder_guardian',
'ender_dragon', 'enderman', 'endermite', 'evoker', 'ghast', 'giant',
'guardian', 'hoglin', 'husk', 'illusioner', 'magma_cube', 'phantom',
'piglin', 'piglin_brute', 'pillager', 'ravager', 'shulker', 'silverfish',
'skeleton', 'skeleton_horse', 'slime', 'spider', 'stray', 'vex',
'vindicator', 'witch', 'wither', 'zoglin', 'zombie', 'zombie_villager',
]
EVIL_IDS = set()
for mob_name in EVIL:
EVIL_IDS.add(MOBS['minecraft:'+mob_name]['protocol_id'])
MOB_NAMES = {}
for mob_name, mob in MOBS.items():
MOB_NAMES[MOBS[mob_name]['protocol_id']] = mob_name.replace('minecraft:', '')
EVIL_IDS = {
MOBS['minecraft:' + name]['protocol_id']
for name in EVIL if 'minecraft:' + name in MOBS
}
MOB_NAMES = {entity['protocol_id']: name.removeprefix('minecraft:')
for name, entity in MOBS.items()}
+4 -5
View File
@@ -121,7 +121,7 @@ class Pathfinder(AStar):
def __init__(self, g):
self.g = g
self.chunks = g.chunks
self.start_time = time.time()
self.start_time = time.monotonic()
@functools.lru_cache(maxsize=BLOCK_CACHE_SIZE)
def bair(self, p):
@@ -267,6 +267,9 @@ class Pathfinder(AStar):
return True
def neighbors(self, node):
if time.monotonic() - self.start_time > 2.0:
raise AStarTimeout
results = []
if self.g.crawling:
@@ -296,10 +299,6 @@ class Pathfinder(AStar):
if self.check_parkour(node, offset):
results.append(utils.padd(node, offset))
if not results:
if time.time() - self.start_time > 2.0:
raise(AStarTimeout)
return results
def distance_between(self, n1, n2):
+102
View File
@@ -0,0 +1,102 @@
"""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]
+11 -30
View File
@@ -1,42 +1,23 @@
import os
from math import floor
import json
import time
from math import floor
from minecraft.networking.packets import clientbound, serverbound
from mosfet.protocol import packets
from mosfet.info import blocks, mcdata, mobs
from mosfet import utils
from mosfet.info import blocks
class DataManager:
def __init__(self, directory):
self.blocks = {}
self.blocks_states = {}
self.blocks_properties = {}
"""Compatibility facade over the checked-in Minecraft 26.3 data."""
def __init__(self, directory=None):
self.blocks = mcdata.mcd.blocks
self.blocks_states = dict(blocks.BLOCKS)
self.blocks_properties = dict(blocks.PROPS)
self.registries = {}
self.biomes = {}
self.entity_type = {}
if not os.path.isdir(directory):
raise FileNotFoundError("%s is not a valid directory")
if not os.path.isfile("%s/registries.json"%(directory)):
raise FileNotFoundError("%s is not a valid minecraft data directory")
with open("%s/blocks.json"%(directory)) as f:
blocks = json.loads(f.read())
for x in blocks:
for s in blocks[x]['states']:
self.blocks_states[s['id']] = x
self.blocks_properties[s['id']] = s.get('properties', {})
with open("%s/registries.json"%(directory)) as f:
registries = json.loads(f.read())
#for x in registries["minecraft:biome"]["entries"]:
# self.biomes[registries["minecraft:biome"]["entries"][x]["protocol_id"]] = x
for x in registries["minecraft:entity_type"]["entries"]:
self.entity_type[registries["minecraft:entity_type"]["entries"][x]["protocol_id"]] = x
self.entity_type = {
data['protocol_id']: name
for name, data in mobs.MOBS.items()
}
class ChunksManager:
+39
View File
@@ -0,0 +1,39 @@
"""Chat adapter for the Minecraft 26.3 play protocol."""
import time
from minecraft.networking.modern import (
ChatCommand, ClientboundPlayerChatPacket, ServerboundChatPacket, SystemChat,
)
class ModernChatManager:
def __init__(self, global_state):
self.g = global_state
self.handler = None
self.g.connection.register_packet_listener(
self.handle_player_chat, ClientboundPlayerChatPacket)
def set_handler(self, callback):
self.handler = callback
def handle_player_chat(self, packet):
sender = str(packet.sender)
text = '{}: {}'.format(sender, packet.message)
print('[chat] {}: {}'.format(sender, packet.message), flush=True)
if self.handler:
self.handler(('CHAT', sender, text))
def handle_system_chat(self, packet, message):
if self.handler:
self.handler(('SYSTEM', None, message))
def send(self, message):
if not message:
return
if message.startswith('/'):
self.g.connection.write_packet(
ChatCommand(command=message[1:]), force=True)
else:
packet = ServerboundChatPacket(
message=message, timestamp=int(time.time() * 1000))
self.g.connection.write_packet(packet, force=True)
+220
View File
@@ -0,0 +1,220 @@
"""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