/** * DougBot Bridge - Entry Point * * Connects to Minecraft Bedrock servers (offline, online, or Realm) * and exposes a WebSocket API for the Python controller. * * Usage: * node dist/index.js --host --port --username [--ws-port ] [--offline] * node dist/index.js --email --host --port [--ws-port ] * node dist/index.js --email --realm [--realm-id ] [--ws-port ] */ import { BedrockClient, ClientOptions } from './client'; import { BridgeWSServer } from './ws_server'; import { ChatAction } from './actions/chat'; import { MovementAction } from './actions/movement'; import { EntityTracker } from './world/entity_tracker'; import { buildAuthOptions, buildRealmOptions, listRealms } from './auth'; import { createLogger, setLogLevel, LogLevel } from './utils/logger'; const log = createLogger('main'); // ============================================================ // Parse command line arguments // ============================================================ function parseArgs(): Record { const args = process.argv.slice(2); const parsed: Record = {}; for (let i = 0; i < args.length; i++) { if (args[i].startsWith('--')) { const key = args[i].substring(2); if (i + 1 < args.length && !args[i + 1].startsWith('--')) { parsed[key] = args[i + 1]; i++; } else { parsed[key] = 'true'; } } } return parsed; } // ============================================================ // Main // ============================================================ async function main(): Promise { const args = parseArgs(); if (args['debug'] === 'true') { setLogLevel(LogLevel.DEBUG); } const wsPort = parseInt(args['ws-port'] || '8765', 10); const connType = args['conn-type'] || (args['offline'] === 'true' ? 'offline' : (args['realm'] === 'true' ? 'realm' : 'online')); log.info('DougBot Bridge starting', { connType, host: args['host'] || '(realm)', port: args['port'] || '(realm)', username: args['username'] || args['email'] || 'Doug', wsPort, }); // Create the WebSocket server const wsServer = new BridgeWSServer(wsPort); // Build client options based on connection type let clientOptions: ClientOptions; let bedrockClient: BedrockClient; if (connType === 'offline') { // ── Offline: Direct connection, no auth ── clientOptions = { host: args['host'] || '127.0.0.1', port: parseInt(args['port'] || '19132', 10), username: args['username'] || 'Doug', offline: true, }; } else if (connType === 'realm') { // ── Realm: Xbox Live auth + Realm connection ── const email = args['email']; if (!email) { log.error('Realm connection requires --email'); process.exit(1); } const authOpts = buildRealmOptions( { email, onDeviceCode: (code, url) => { // Emit device code as an event so Python GUI can display it wsServer.emitEvent('auth_device_code', { code, url }); }, }, args['realm-id'] || undefined, ); clientOptions = { host: '', // Will be resolved by Realm auth port: 0, username: email, offline: false, extraOptions: authOpts, }; } else { // ── Online: Xbox Live auth + direct server ── const email = args['email']; if (!email) { log.error('Online connection requires --email'); process.exit(1); } const authOpts = buildAuthOptions({ email, onDeviceCode: (code, url) => { wsServer.emitEvent('auth_device_code', { code, url }); }, }); clientOptions = { host: args['host'] || '127.0.0.1', port: parseInt(args['port'] || '19132', 10), username: email, offline: false, extraOptions: authOpts, }; } bedrockClient = new BedrockClient(clientOptions); const chatAction = new ChatAction(bedrockClient); const movementAction = new MovementAction(bedrockClient); movementAction.setOnArrival(() => { wsServer.emitEvent('movement_complete', {}); }); const entityTracker = new EntityTracker(); // Wire up entity tracking from bedrock events bedrockClient.on('entity_spawned', (data: any) => { entityTracker.addEntity(data.entityId, data.type, data.position, data.name); }); bedrockClient.on('entity_removed', (data: any) => { entityTracker.removeEntity(data.entityId); }); // ============================================================ // Wire up bridge action handler // ============================================================ wsServer.setActionHandler(async (action, params) => { switch (action) { case 'status': return { status: 'success', data: { connected: bedrockClient.state.connected, spawned: bedrockClient.state.spawned, position: bedrockClient.state.position, health: bedrockClient.state.health, food: bedrockClient.state.food, gameTime: bedrockClient.state.gameTime, dayTime: bedrockClient.state.dayTime, }, }; case 'connect': try { await bedrockClient.connect(); return { status: 'success', data: { message: 'Connecting to server...' } }; } catch (err: any) { return { status: 'error', error: err.message }; } case 'disconnect': bedrockClient.disconnect(); return { status: 'success', data: { message: 'Disconnected' } }; case 'send_chat': const chatResult = chatAction.sendChat(params.message || ''); return { status: chatResult.success ? 'success' : 'error', data: chatResult.success ? {} : undefined, error: chatResult.error, }; case 'get_position': return { status: 'success', data: { position: bedrockClient.state.position }, }; case 'get_health': return { status: 'success', data: { health: bedrockClient.state.health, food: bedrockClient.state.food }, }; case 'get_time': return { status: 'success', data: { gameTime: bedrockClient.state.gameTime, dayTime: bedrockClient.state.dayTime }, }; case 'move_to': // Use /tp for reliable movement (works on all server types) try { bedrockClient.teleportTo(params.x || 0, params.y || 0, params.z || 0); return { status: 'success' }; } catch (err: any) { return { status: 'error', error: err.message }; } case 'move_relative': try { bedrockClient.teleportRelative(params.dx || 0, params.dy || 0, params.dz || 0); return { status: 'success' }; } catch (err: any) { return { status: 'error', error: err.message }; } case 'send_command': try { bedrockClient.sendCommand(params.command || ''); return { status: 'success' }; } catch (err: any) { return { status: 'error', error: err.message }; } case 'stop': movementAction.stop(); return { status: 'success' }; case 'look_at': movementAction.lookAt({ x: params.x || 0, y: params.y || 0, z: params.z || 0 }); return { status: 'success' }; case 'get_nearby_entities': const nearby = entityTracker.getNearbyEntities( bedrockClient.state.position, params.radius || 32, ); return { status: 'success', data: { entities: nearby } }; case 'get_nearby_hostiles': const hostiles = entityTracker.getNearbyHostiles( bedrockClient.state.position, params.radius || 16, ); return { status: 'success', data: { hostiles } }; case 'get_players': return { status: 'success', data: { players: entityTracker.getPlayerNames() }, }; case 'list_realms': try { const email = params.email; if (!email) { return { status: 'error', error: 'Email is required to list Realms' }; } const realms = await listRealms({ email, onDeviceCode: (code, url) => { wsServer.emitEvent('auth_device_code', { code, url }); }, }); return { status: 'success', data: { realms } }; } catch (err: any) { return { status: 'error', error: err.message }; } default: log.warn(`Unhandled action: ${action}`); return { status: 'error', error: `Unknown action: ${action}`, }; } }); // ============================================================ // Forward Bedrock events to WebSocket // ============================================================ const forwardEvents = [ 'chat_message', 'health_changed', 'entity_spawned', 'entity_removed', 'player_joined', 'player_left', 'damage_taken', 'death', 'time_update', 'spawn_complete', 'connected', 'disconnected', 'error', 'respawn', ] as const; for (const event of forwardEvents) { bedrockClient.on(event, (data: any) => { wsServer.emitEvent(event as any, data || {}); }); } // ============================================================ // Start // ============================================================ try { await wsServer.start(); await bedrockClient.connect(); log.info('Bridge is running. Waiting for Python controller...'); } catch (err: any) { log.error('Failed to start bridge', { error: err.message }); process.exit(1); } // Graceful shutdown const shutdown = () => { log.info('Shutting down...'); bedrockClient.disconnect(); wsServer.stop(); process.exit(0); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); process.on('unhandledRejection', (reason: any) => { log.error('Unhandled promise rejection', { error: reason?.message || String(reason), stack: reason?.stack || '' }); }); process.on('uncaughtException', (err) => { log.error('Uncaught exception', { error: err.message, stack: err.stack }); shutdown(); }); } main().catch((err) => { log.error('Fatal error', { error: err.message }); process.exit(1); });