diff --git a/README.md b/README.md index 655895f..ec0325e 100644 --- a/README.md +++ b/README.md @@ -7,61 +7,34 @@ 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. -## Requirements +## Requirements and setup -- Python >= 3.6 -- pip, virtualenv, git, wget, unzip +- Python 3.11+ +- The locally updated pyCraft checkout at `../pyCraft` -## Linux Setup - -Assuming Debian / Ubuntu based distro: +From the repository root, create and activate a virtual environment, then +install the connection-mode dependencies: ``` -$ sudo apt update -$ sudo apt install build-essential python3 python3-dev python3-pip virtualenv git wget unzip - -$ git clone https://git.tannercollin.com/tanner/minecraft-bot.git -$ cd minecraft-bot/ +$ python3 -m venv .venv +$ source .venv/bin/activate +$ python -m pip install flask flask-cors munch watchdog -e ../pyCraft ``` ## Running -If you want to use the built-in burner account (Minecraft name `mattstack`): +Run the bot directly with Python. For the Protospace offline-mode server: ``` -$ SERVER=minecraft.example.com ./run_linux.sh +$ SERVER=games.protospace.ca python mosfet.py ``` -Or, use `PORT` to specify a custom port to connect to: +`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. -``` -$ SERVER=localhost PORT=12345 ./run_linux.sh -``` - -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. +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. ## Usage diff --git a/main.py b/mosfet.py similarity index 66% rename from main.py rename to mosfet.py index 2453329..4138e87 100644 --- a/main.py +++ b/mosfet.py @@ -1,8 +1,8 @@ import importlib +import json import threading import time import traceback -import json import logging log = logging.getLogger('werkzeug') @@ -18,11 +18,15 @@ from munch import Munch from watchdog.observers import Observer from watchdog.events import PatternMatchingEventHandler -from mosfet import bot +from mosfet import connect global_state = Munch() g = global_state g.connection = False +g.connected = False +g.connection_status = 'disconnected' +g.player_position = None +g.last_chat = None g.name = None g.mcdata = False g.pos = False @@ -57,20 +61,27 @@ def api_global(): def root(): return app.send_static_file('index.html') -reload_timeout = time.time() - def main(): + """Connect using the current offline/AuthMe protocol bootstrap. + + The legacy automation loop is deliberately not started yet: its packet + handlers still target Minecraft 1.16 and will be migrated incrementally. + """ + reload_timeout = time.time() + reload_requested = False + def reload_bot(event): - global reload_timeout + nonlocal reload_timeout, reload_requested if time.time() - reload_timeout > 2.0: reload_timeout = time.time() + reload_requested = True print('Reloading...') g.running = False - event_handler = PatternMatchingEventHandler(patterns=['*.py'], ignore_patterns=['./main.py']) - event_handler.on_any_event = reload_bot - observer = Observer() + event_handler = PatternMatchingEventHandler( + patterns=['*.py'], ignore_patterns=['./mosfet.py']) + event_handler.on_any_event = reload_bot observer.schedule(event_handler, 'mosfet', recursive=True) observer.start() @@ -78,18 +89,25 @@ def main(): while True: try: g.running = True - bot.bot(global_state) - importlib.reload(bot) - except BaseException as e: + connect.main(global_state) + if not reload_requested: + break + reload_requested = False + importlib.reload(connect) + except KeyboardInterrupt: + break + except BaseException: g.running = True traceback.print_exc() print('Locking...') while g.running: time.sleep(1) - importlib.reload(bot) - except KeyboardInterrupt: + reload_requested = False + importlib.reload(connect) + finally: + g.running = False observer.stop() - observer.join() + observer.join() def run_api(): host = '0.0.0.0' @@ -106,7 +124,5 @@ def run_api(): port += 1 if __name__ == '__main__': - threading.Thread(target=run_api).start() - time.sleep(1) - + threading.Thread(target=run_api, daemon=True).start() main() diff --git a/mosfet/bot.py b/mosfet/bot.py index 410634e..c911884 100644 --- a/mosfet/bot.py +++ b/mosfet/bot.py @@ -1,5 +1,5 @@ if __name__ == '__main__': - print('Run main.py instead.') + print('Run mosfet.py instead.') exit(1) import os @@ -251,11 +251,8 @@ def bot(global_state): if not SERVER: print() print('You must specify a server to connect to. For example:') - print('SERVER=minecraft.example.com ./run_linux.sh') - print('SERVER=localhost PORT=12345 ./run_linux.sh') - print() - print('If you want to use your own account:') - print('EMAIL=you@domain.com PASSWORD=supersecret SERVER=minecraft.example.com ./run_linux.sh') + print('SERVER=minecraft.example.com python mosfet.py') + print('SERVER=localhost PORT=12345 python mosfet.py') os._exit(0) elif not g.connection: if EMAIL and PASSWORD: @@ -274,7 +271,7 @@ def bot(global_state): print('') print('Did you forget to specify an email?') print('If you want to use your own account:') - print('EMAIL=you@domain.com PASSWORD=supersecret SERVER=minecraft.example.com ./run_linux.sh') + print('Set SERVER and run python mosfet.py.') os._exit(0) else: print('No username or password provided, using burner minecraft account...') diff --git a/mosfet/connect.py b/mosfet/connect.py index 274114d..c0a6513 100644 --- a/mosfet/connect.py +++ b/mosfet/connect.py @@ -6,7 +6,7 @@ import os from minecraft.networking.connection import Connection from minecraft.networking.modern import ( CancelPostjoinDialog, ChatCommand, PlayShowDialog, - SubmitPostjoinLoginDialog, SystemChat, TeleportPosition, + PlayLogin, SubmitPostjoinLoginDialog, SystemChat, TeleportPosition, ) @@ -38,7 +38,7 @@ def component_text(value): return str(value) -def main(): +def main(global_state=None): server = os.environ.get('SERVER', 'games.protospace.ca') port = int(os.environ.get('PORT', '25565')) username = os.environ.get('PLAYER_NAME', 'mosfet1') @@ -48,6 +48,19 @@ def main(): allowed_versions=['26.3']) connection.authme_password = password sent_auth_commands = set() + if global_state is not None: + global_state.connection = connection + global_state.name = username + global_state.connected = False + global_state.connection_status = 'connecting' + global_state.player_position = None + + @connection.listener(PlayLogin) + def on_play_login(packet): + if global_state is not None: + global_state.connected = True + global_state.connection_status = 'in_game' + def send_auth_command(action): if action in sent_auth_commands: @@ -61,6 +74,8 @@ def main(): @connection.listener(TeleportPosition) def on_spawn(packet): + if global_state is not None: + global_state.player_position = (packet.x, packet.y, packet.z) if getattr(connection, 'authme_mode', 'register') == 'register': send_auth_command('register') @@ -79,6 +94,8 @@ def main(): @connection.listener(SystemChat) def on_system_chat(packet): message = component_text(packet.content) + if global_state is not None: + global_state.last_chat = message print('[server] ' + message, flush=True) lowered = message.lower() if 'already registered' in lowered: @@ -96,16 +113,27 @@ def main(): username, server, port), flush=True) try: while connection.connected: + if global_state is not None and not global_state.get('running', True): + break thread = connection.networking_thread if thread is None: break thread.join(timeout=0.5) + if connection.connected: + connection.disconnect() if connection.exception: raise connection.exception print('Connection ended.', flush=True) except KeyboardInterrupt: print('Disconnecting...', flush=True) connection.disconnect() + finally: + if connection.connected: + connection.disconnect() + if global_state is not None: + global_state.connection = False + global_state.connected = False + global_state.connection_status = 'disconnected' if __name__ == '__main__': diff --git a/run_connect.sh b/run_connect.sh deleted file mode 100755 index 51bbfea..0000000 --- a/run_connect.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/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 diff --git a/run_linux.sh b/run_linux.sh deleted file mode 100755 index c4c8424..0000000 --- a/run_linux.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash - -set -eu - -if [ ! -d "mosfet" ] -then - echo "You must run this from the mosfet-minecraft-bot directory." - exit -fi - -if ! command -v virtualenv &> /dev/null -then - echo "virtualenv could not be found, please install with:" - echo "sudo apt install virtualenv" - exit -fi - -if ! command -v wget &> /dev/null -then - echo "wget could not be found, please install with:" - echo "sudo apt install wget" - exit -fi - -if ! command -v unzip &> /dev/null -then - echo "unzip could not be found, please install with:" - echo "sudo apt install unzip" - exit -fi - -if ! command -v python3 &> /dev/null -then - echo "python3 could not be found, please install with:" - echo "sudo apt install python3" - exit -fi - -if ! command -v pip3 &> /dev/null -then - echo "pip3 could not be found, please install with:" - echo "sudo apt install pip3" - exit -fi - - -# download minecraft data - -if [ ! -d "minecraft_data" ] -then - echo "Grabbing minecraft data..." - - VERSION="1.16.4" - - wget -Omcdata.zip "https://apimon.de/mcdata/$VERSION/$VERSION.zip" - mkdir minecraft_data - unzip mcdata.zip -d minecraft_data - rm mcdata.zip -fi - -# https://github.com/pypa/virtualenv/issues/1029 -PS1=${PS1:-} - -# create virtual environment - -if [ ! -d "env" ] -then - echo "Installing Python requirements..." - - virtualenv -p python3 env - source env/bin/activate - pip install -r requirements.txt -else - source env/bin/activate -fi - -echo "Running bot..." - -python main.py diff --git a/update_linux.sh b/update_linux.sh deleted file mode 100755 index 6f63886..0000000 --- a/update_linux.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/bin/bash - -set -eu - -if [ ! -d "mosfet" ] -then - echo "You must run this from the mosfet-minecraft-bot directory." - exit -fi - -if ! command -v virtualenv &> /dev/null -then - echo "virtualenv could not be found, please install with:" - echo "sudo apt install virtualenv" - exit -fi - -if ! command -v wget &> /dev/null -then - echo "wget could not be found, please install with:" - echo "sudo apt install wget" - exit -fi - -if ! command -v unzip &> /dev/null -then - echo "unzip could not be found, please install with:" - echo "sudo apt install unzip" - exit -fi - -if ! command -v python3 &> /dev/null -then - echo "python3 could not be found, please install with:" - echo "sudo apt install python3" - exit -fi - -if ! command -v pip3 &> /dev/null -then - echo "pip3 could not be found, please install with:" - echo "sudo apt install pip3" - exit -fi - -if ! command -v git &> /dev/null -then - echo "git could not be found, please install with:" - echo "sudo apt install pip3" - exit -fi - - -# download minecraft data - -echo "Grabbing minecraft data..." - -VERSION="1.16.4" - -wget -Omcdata.zip "https://apimon.de/mcdata/$VERSION/$VERSION.zip" -rm -r minecraft_data || true -mkdir minecraft_data -unzip mcdata.zip -d minecraft_data -rm mcdata.zip - - -# update code - -git stash -git pull --rebase -git stash pop || true - - -# https://github.com/pypa/virtualenv/issues/1029 -PS1=${PS1:-} - -# create virtual environment - -echo "Installing Python requirements..." - -rm -r env || true -rm -r __pycache__ || true -virtualenv -p python3 env -source env/bin/activate -pip install -r requirements.txt - - -echo "Done."