Files
minecraft-bot/mosfet.py
T
2026-11-04 15:42:40 +00:00

276 lines
9.1 KiB
Python

import importlib
import json
import queue
import threading
import time
import traceback
import sys
from pathlib import Path
import logging
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
from flask import Flask
from flask_cors import CORS
build_folder = 'web_interface/build'
app = Flask(__name__, template_folder=build_folder, static_folder=build_folder, static_url_path='')
CORS(app)
from munch import Munch
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
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
g.dimension = None
g.item_lock = False
g.inv = {}
g.objects = {}
g.mobs = {}
g.players = {}
g.player_names = {}
g.window = None
g.job = None
g.correction_count = 0
g.holding = 0
g.afk = False
g.health = 20
g.food = 20
g.sand_origin = None
g.reload_lock = threading.RLock()
def _api_json_default(value):
"""Serialize live protocol objects without invoking fragile __repr__ code."""
from minecraft.networking.packets import Packet
if isinstance(value, Packet):
fields = {key: item for key, item in vars(value).items()
if key != 'context'}
return {'__type__': type(value).__name__,
'packet_name': getattr(value, 'packet_name', None),
'fields': fields}
if isinstance(value, (bytes, bytearray)):
return value.hex()
try:
return str(value)
except Exception:
return object.__repr__(value)
@app.route('/api/global')
def api_global():
data = json.dumps(g, default=_api_json_default, indent=4)
response = app.response_class(
response=data,
status=200,
mimetype='application/json'
)
return response
@app.route('/')
def root():
return app.send_static_file('index.html')
def main():
"""Run the 26.3 client with in-place development hot reload.
Application modules are refreshed while the current socket and player
session remain alive. Connection/bootstrap and packet-schema edits are
reported as deferred because their listeners cannot safely be replaced
without reconnecting.
"""
g.running = True
changes = queue.Queue()
observer = Observer()
source_root = Path(__file__).resolve().parent / 'mosfet'
def enqueue_change(event):
if event.is_directory:
return
path = getattr(event, 'dest_path', None) if event.event_type == 'moved' else event.src_path
if not path or Path(path).suffix != '.py' or '__pycache__' in path:
return
changes.put(str(Path(path).resolve()))
def module_name(path):
source = Path(path)
try:
relative = source.relative_to(source_root)
except ValueError:
return None
parts = list(relative.with_suffix('').parts)
if parts and parts[-1] == '__init__':
parts.pop()
return '.'.join(('mosfet', *parts))
def reload_changed(paths):
paths = sorted(set(path for path in paths if Path(path).is_file()))
if not paths:
return
# Don't execute a half-written editor save. If any file is syntactically
# invalid, keep the currently running code untouched.
try:
for path in paths:
source = Path(path)
compile(source.read_text(encoding='utf-8'), str(source), 'exec')
except (OSError, SyntaxError, UnicodeError):
print('[hot reload] waiting for a valid save', flush=True)
return
deferred = []
modules = []
for path in paths:
name = module_name(path)
if not name:
continue
if name in ('mosfet.connect', 'mosfet') or name == 'mosfet.monkey_patch':
deferred.append(name)
else:
modules.append(name)
if deferred:
print('[hot reload] {} changed; connection/bootstrap changes are '
'deferred until the next start (no disconnect performed)'.format(
', '.join(sorted(set(deferred)))), flush=True)
if not modules:
return
if not g.get('connected') or not g.get('commands'):
deadline = time.monotonic() + 15.0
while (g.running and time.monotonic() < deadline and
(not g.get('connected') or not g.get('commands'))):
time.sleep(0.1)
if not g.get('connected') or not g.get('commands'):
print('[hot reload] runtime not ready; change left for next save',
flush=True)
return
managers_changed = bool(
{'mosfet.protocol.managers', 'mosfet.protocol.types'} & set(modules))
core_modules = {
'mosfet.bot', 'mosfet.commands', 'mosfet.game', 'mosfet.job',
'mosfet.path', 'mosfet.print_help', 'mosfet.utils',
'mosfet.vector', 'mosfet.world', 'mosfet.info.blocks',
'mosfet.info.items', 'mosfet.info.mcdata', 'mosfet.info.mobs',
}
# bot.py reloads the legacy gameplay modules and job state classes as
# one coherent group. Reload protocol adapters first so rebinding sees
# their latest definitions.
direct_modules = [name for name in modules
if name not in core_modules and
not name.startswith('mosfet.jobs.')]
if managers_changed and 'mosfet.protocol.managers' not in direct_modules:
direct_modules.append('mosfet.protocol.managers')
priority = {
'mosfet.protocol.types': 0,
'mosfet.protocol.packets': 1,
'mosfet.protocol.managers': 2,
'mosfet.protocol.modern': 3,
}
direct_modules.sort(key=lambda name: (priority.get(name, 10), name))
connection = g.connection
packet_lock = getattr(connection, 'packet_listener_lock', None)
try:
if packet_lock is None:
with g.reload_lock:
apply_reload(direct_modules, managers_changed)
else:
# Match packet dispatch's lock order (listener lock, then the
# runtime lock) so callbacks cannot race listener replacement.
with packet_lock:
with g.reload_lock:
apply_reload(direct_modules, managers_changed)
except Exception:
traceback.print_exc()
print('[hot reload] failed; connection left running', flush=True)
def apply_reload(direct_modules, managers_changed):
for name in direct_modules:
module = sys.modules.get(name)
if module is not None:
importlib.reload(module)
bot_module = sys.modules.get('mosfet.bot')
if bot_module is None:
bot_module = importlib.import_module('mosfet.bot')
bot_module = importlib.reload(bot_module)
bot_module.refresh_runtime(global_state,
reload_managers=managers_changed)
print('[hot reload] refreshed {} (session preserved)'.format(
', '.join(direct_modules) if direct_modules else 'bot runtime'),
flush=True)
def reload_worker():
while True:
path = changes.get()
if path is None:
return
batch = {path}
stopping = False
# Coalesce the burst of modified/moved events from editor saves.
while True:
try:
candidate = changes.get(timeout=0.4)
except queue.Empty:
break
if candidate is None:
stopping = True
break
batch.add(candidate)
time.sleep(0.15)
reload_changed(batch)
if stopping:
return
event_handler = FileSystemEventHandler()
event_handler.on_modified = enqueue_change
event_handler.on_created = enqueue_change
event_handler.on_moved = enqueue_change
observer.schedule(event_handler, str(source_root), recursive=True)
observer.start()
worker = threading.Thread(target=reload_worker,
name='mosfet-hot-reloader', daemon=True)
worker.start()
try:
connect.main(global_state)
except KeyboardInterrupt:
pass
except BaseException:
traceback.print_exc()
finally:
g.running = False
changes.put(None)
observer.stop()
observer.join()
worker.join(timeout=2.0)
def run_api():
host = '0.0.0.0'
port = 3300
while True:
print('Trying to run web interface on port:', port)
print('If it works, go to http://localhost:' + str(port))
try:
app.run(host=host, port=port)
except OSError:
print()
print('Error: Port already taken.')
port += 1
if __name__ == '__main__':
threading.Thread(target=run_api, daemon=True).start()
main()