From d43533bcfee3d36913af2a34c3ea4963966feb2d Mon Sep 17 00:00:00 2001 From: Tanner Date: Tue, 3 Nov 2026 19:20:06 +0000 Subject: [PATCH] Add minimal connection example for 26.3 and AuthMe Plugin --- README.md | 19 ++++++++ mosfet/connect.py | 112 ++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 +- run_connect.sh | 19 ++++++++ 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 mosfet/connect.py create mode 100755 run_connect.sh diff --git a/README.md b/README.md index 2390e22..655895f 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,25 @@ Or, if you have your own alt account for the bot: $ EMAIL=you@domain.com PASSWORD=supersecret SERVER=minecraft.example.com ./run_linux.sh ``` +### Current offline servers (Minecraft 26.3) + +The legacy automation in `main.py` uses 1.16-era world/chunk packet handling. +For an offline-mode server running Minecraft 26.3, use the minimal current- +protocol connection mode instead. It joins as `mosfet1` by default, answers +AuthMe registration/login prompts using `GAME_PASSWORD`, and stays connected; +world automation is not enabled in this mode. + +The locally updated pyCraft checkout is expected at `../pyCraft`: + +``` +$ ./run_connect.sh +``` + +You can override `SERVER`, `PORT`, `PLAYER_NAME`, or `GAME_PASSWORD` through +environment variables. The current AuthMe default is `mosfetbot`. pyCraft is +being extended locally for protocol 777; no Microsoft authentication is used +in this mode. + ## Usage [How to Cache Items](docs/cache_items.md) diff --git a/mosfet/connect.py b/mosfet/connect.py new file mode 100644 index 0000000..274114d --- /dev/null +++ b/mosfet/connect.py @@ -0,0 +1,112 @@ +"""Minimal current-protocol connection mode for AuthMe / offline servers. + +Run with SERVER=... PLAYER_NAME=mosfet1 GAME_PASSWORD=... python -m mosfet.connect +""" +import os +from minecraft.networking.connection import Connection +from minecraft.networking.modern import ( + CancelPostjoinDialog, ChatCommand, PlayShowDialog, + SubmitPostjoinLoginDialog, SystemChat, TeleportPosition, +) + + +def component_text(value): + """Best-effort flattening of modern anonymous-NBT chat components.""" + if value is None: + return '' + if isinstance(value, (str, int, float)): + return str(value) + if isinstance(value, (list, tuple)): + return ''.join(component_text(item) for item in value) + raw = getattr(value, 'value', None) + if isinstance(raw, (str, int, float)): + return str(raw) + if raw is not None and raw is not value and hasattr(raw, 'items'): + value = raw + tags = getattr(value, 'tags', None) + if tags is not None: + value = tags + if hasattr(value, 'items'): + fields = dict(value.items()) + if 'text' in fields: + return component_text(fields['text']) + component_text(fields.get('extra', [])) + if 'translate' in fields: + return component_text(fields['translate']) + component_text(fields.get('with', [])) + return ''.join(component_text(v) for v in fields.values()) + if isinstance(value, (list, tuple)): + return ''.join(component_text(item) for item in value) + return str(value) + + +def main(): + server = os.environ.get('SERVER', 'games.protospace.ca') + 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']) + connection.authme_password = password + sent_auth_commands = set() + + def send_auth_command(action): + if action in sent_auth_commands: + return + sent_auth_commands.add(action) + command = '/register {0} {0}'.format(password) if action == 'register' \ + else '/login ' + password + print('[AuthMe] sending ' + command.split()[0], flush=True) + connection.write_packet(ChatCommand(command=command.lstrip('/')), + force=True) + + @connection.listener(TeleportPosition) + def on_spawn(packet): + if getattr(connection, 'authme_mode', 'register') == 'register': + send_auth_command('register') + + @connection.listener(PlayShowDialog) + def on_authme_dialog(packet): + is_login = b'login' in packet.payload.lower() + print('[AuthMe] dismissing the post-join dialog to use /commands.', + flush=True) + if is_login: + connection.write_packet(SubmitPostjoinLoginDialog( + password=password), force=True) + else: + connection.write_packet(CancelPostjoinDialog(), force=True) + send_auth_command('register') + + @connection.listener(SystemChat) + def on_system_chat(packet): + message = component_text(packet.content) + print('[server] ' + message, flush=True) + lowered = message.lower() + if 'already registered' in lowered: + send_auth_command('login') + elif 'register' in lowered and any(word in lowered for word in + ('password', 'account', '/register')): + send_auth_command('register') + elif any(word in lowered for word in + ('please login', 'please log in', 'log in', '/login', + 'authenticate', 'authentication required')): + send_auth_command('login') + + connection.connect() + print('Connected as {} to {}:{} using offline protocol 777.'.format( + username, server, port), flush=True) + try: + while connection.connected: + thread = connection.networking_thread + if thread is None: + break + thread.join(timeout=0.5) + if connection.exception: + raise connection.exception + print('Connection ended.', flush=True) + except KeyboardInterrupt: + print('Disconnecting...', flush=True) + connection.disconnect() + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt index 6c09de4..79c0a1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ pep517==0.10.0 pkg-resources==0.0.0 progress==1.5 pycparser==2.20 -git+https://github.com/ammaraskar/pyCraft.git@2813d02ae7fb8182c3e5227a73de2240b09878d9 +-e ../pyCraft PyNBT==3.1.0 pyparsing==2.4.7 pytoml==0.1.21 diff --git a/run_connect.sh b/run_connect.sh new file mode 100755 index 0000000..51bbfea --- /dev/null +++ b/run_connect.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -euo pipefail +cd "$(dirname "$0")" + +if ! command -v uv >/dev/null 2>&1; then + echo "Install uv to create the local Python environment." >&2 + exit 1 +fi + +if [ ! -x .venv/bin/python ]; then + uv venv .venv +fi +uv pip install --python .venv/bin/python -e ../pyCraft + +export SERVER="${SERVER:-games.protospace.ca}" +export PORT="${PORT:-25565}" +export PLAYER_NAME="${PLAYER_NAME:-mosfet1}" +export GAME_PASSWORD="${GAME_PASSWORD:-mosfetbot}" +exec .venv/bin/python -m mosfet.connect