|
|
|
|
|
import os |
|
|
import sys |
|
|
import time |
|
|
import json |
|
|
import threading |
|
|
import subprocess |
|
|
import signal |
|
|
import socket |
|
|
import struct |
|
|
from datetime import datetime |
|
|
from flask import Flask, jsonify, request |
|
|
from flask_cors import CORS |
|
|
import logging |
|
|
|
|
|
|
|
|
log = logging.getLogger('werkzeug') |
|
|
log.setLevel(logging.ERROR) |
|
|
|
|
|
app = Flask(__name__) |
|
|
CORS(app) |
|
|
|
|
|
|
|
|
SERVER_HOST = "orbitmc.progamer.me" |
|
|
SERVER_PORT = 40675 |
|
|
SERVER_VERSION = "1.21.8" |
|
|
BOT_NAMES = ["Alfha_lag", "Bigboybloom", "Kitcat"] |
|
|
ROTATION_DURATION = 3600 |
|
|
|
|
|
|
|
|
bots = {} |
|
|
bot_processes = {} |
|
|
current_bot_index = 0 |
|
|
rotation_start_time = None |
|
|
server_status = { |
|
|
"online": False, |
|
|
"players": "0/0", |
|
|
"latency": 0, |
|
|
"last_check": None, |
|
|
"motd": "" |
|
|
} |
|
|
|
|
|
|
|
|
BOT_SCRIPT = """ |
|
|
const mineflayer = require('mineflayer'); |
|
|
const { Vec3 } = require('vec3'); |
|
|
|
|
|
const botName = process.argv[2]; |
|
|
const host = process.argv[3]; |
|
|
const port = parseInt(process.argv[4]) || 25565; |
|
|
const version = process.argv[5] || false; |
|
|
|
|
|
let isConnected = false; |
|
|
let circleInterval = null; |
|
|
let chatInterval = null; |
|
|
let angle = 0; |
|
|
let centerPos = null; |
|
|
let reconnectTimeout = null; |
|
|
|
|
|
console.log(JSON.stringify({event:'starting', name:botName, time:Date.now()})); |
|
|
|
|
|
function createBot() { |
|
|
const bot = mineflayer.createBot({ |
|
|
host: host, |
|
|
port: port, |
|
|
username: botName, |
|
|
auth: 'offline', |
|
|
version: version === 'false' ? false : version, |
|
|
hideErrors: false, |
|
|
checkTimeoutInterval: 30000, |
|
|
keepAlive: true, |
|
|
skipValidation: true |
|
|
}); |
|
|
|
|
|
function startCircularMovement() { |
|
|
if (circleInterval) clearInterval(circleInterval); |
|
|
|
|
|
setTimeout(() => { |
|
|
if (!bot.entity || !isConnected) return; |
|
|
|
|
|
centerPos = bot.entity.position.clone(); |
|
|
angle = 0; |
|
|
|
|
|
console.log(JSON.stringify({ |
|
|
event:'movement_started', |
|
|
name:botName, |
|
|
center: { |
|
|
x: Math.floor(centerPos.x), |
|
|
y: Math.floor(centerPos.y), |
|
|
z: Math.floor(centerPos.z) |
|
|
} |
|
|
})); |
|
|
|
|
|
circleInterval = setInterval(() => { |
|
|
if (!bot.entity || !isConnected || !centerPos) return; |
|
|
|
|
|
try { |
|
|
const radius = 4; |
|
|
angle += 0.03; |
|
|
|
|
|
const targetX = centerPos.x + Math.cos(angle) * radius; |
|
|
const targetZ = centerPos.z + Math.sin(angle) * radius; |
|
|
|
|
|
const dx = targetX - bot.entity.position.x; |
|
|
const dz = targetZ - bot.entity.position.z; |
|
|
const yaw = Math.atan2(-dx, -dz); |
|
|
|
|
|
bot.look(yaw, 0, true); |
|
|
bot.setControlState('forward', true); |
|
|
|
|
|
// Send position every 2 seconds |
|
|
if (Math.floor(angle * 100) % 200 === 0) { |
|
|
const pos = bot.entity.position; |
|
|
console.log(JSON.stringify({ |
|
|
event:'position', |
|
|
name:botName, |
|
|
x: Math.floor(pos.x), |
|
|
y: Math.floor(pos.y), |
|
|
z: Math.floor(pos.z), |
|
|
health: bot.health, |
|
|
food: bot.food |
|
|
})); |
|
|
} |
|
|
} catch(err) { |
|
|
console.log(JSON.stringify({event:'movement_error', name:botName, error:err.message})); |
|
|
} |
|
|
}, 100); |
|
|
}, 3000); |
|
|
} |
|
|
|
|
|
function stopMovement() { |
|
|
if (circleInterval) { |
|
|
clearInterval(circleInterval); |
|
|
circleInterval = null; |
|
|
} |
|
|
if (chatInterval) { |
|
|
clearInterval(chatInterval); |
|
|
chatInterval = null; |
|
|
} |
|
|
try { |
|
|
bot.setControlState('forward', false); |
|
|
} catch(err) {} |
|
|
} |
|
|
|
|
|
bot.once('spawn', () => { |
|
|
console.log(JSON.stringify({ |
|
|
event:'connected', |
|
|
name:botName, |
|
|
time:Date.now(), |
|
|
gamemode: bot.game.gameMode, |
|
|
dimension: bot.game.dimension |
|
|
})); |
|
|
isConnected = true; |
|
|
startCircularMovement(); |
|
|
|
|
|
// Random chat |
|
|
chatInterval = setInterval(() => { |
|
|
if (isConnected && Math.random() > 0.8) { |
|
|
try { |
|
|
const messages = ['Hello!', 'Im monitoring', 'All good here', ':)', 'Server looking good']; |
|
|
bot.chat(messages[Math.floor(Math.random() * messages.length)]); |
|
|
} catch(err) {} |
|
|
} |
|
|
}, 180000); // Every 3 minutes |
|
|
}); |
|
|
|
|
|
bot.on('respawn', () => { |
|
|
console.log(JSON.stringify({event:'respawned', name:botName, time:Date.now()})); |
|
|
stopMovement(); |
|
|
centerPos = null; |
|
|
startCircularMovement(); |
|
|
}); |
|
|
|
|
|
bot.on('death', () => { |
|
|
console.log(JSON.stringify({event:'death', name:botName, time:Date.now()})); |
|
|
stopMovement(); |
|
|
centerPos = null; |
|
|
}); |
|
|
|
|
|
bot.on('health', () => { |
|
|
if (bot.health <= 0) { |
|
|
console.log(JSON.stringify({event:'died', name:botName})); |
|
|
} |
|
|
}); |
|
|
|
|
|
bot.on('kicked', (reason) => { |
|
|
console.log(JSON.stringify({ |
|
|
event:'kicked', |
|
|
name:botName, |
|
|
reason: JSON.stringify(reason), |
|
|
time:Date.now() |
|
|
})); |
|
|
isConnected = false; |
|
|
stopMovement(); |
|
|
}); |
|
|
|
|
|
bot.on('error', (err) => { |
|
|
console.log(JSON.stringify({ |
|
|
event:'error', |
|
|
name:botName, |
|
|
error:err.message, |
|
|
time:Date.now() |
|
|
})); |
|
|
}); |
|
|
|
|
|
bot.on('end', (reason) => { |
|
|
console.log(JSON.stringify({ |
|
|
event:'disconnected', |
|
|
name:botName, |
|
|
reason: reason || 'unknown', |
|
|
time:Date.now() |
|
|
})); |
|
|
isConnected = false; |
|
|
stopMovement(); |
|
|
|
|
|
// Auto reconnect after 5 seconds |
|
|
if (reconnectTimeout) clearTimeout(reconnectTimeout); |
|
|
reconnectTimeout = setTimeout(() => { |
|
|
console.log(JSON.stringify({event:'reconnecting', name:botName})); |
|
|
createBot(); |
|
|
}, 5000); |
|
|
}); |
|
|
|
|
|
bot.on('message', (message) => { |
|
|
const msg = message.toString(); |
|
|
if (msg.includes(botName)) { |
|
|
console.log(JSON.stringify({event:'mentioned', name:botName, message:msg})); |
|
|
} |
|
|
}); |
|
|
|
|
|
// Heartbeat |
|
|
setInterval(() => { |
|
|
if (isConnected && bot.entity) { |
|
|
console.log(JSON.stringify({ |
|
|
event:'heartbeat', |
|
|
name:botName, |
|
|
health: bot.health || 0, |
|
|
food: bot.food || 0, |
|
|
time:Date.now() |
|
|
})); |
|
|
} |
|
|
}, 30000); |
|
|
|
|
|
process.on('SIGTERM', () => { |
|
|
stopMovement(); |
|
|
bot.quit(); |
|
|
process.exit(0); |
|
|
}); |
|
|
|
|
|
process.on('SIGINT', () => { |
|
|
stopMovement(); |
|
|
bot.quit(); |
|
|
process.exit(0); |
|
|
}); |
|
|
} |
|
|
|
|
|
createBot(); |
|
|
""" |
|
|
|
|
|
def write_bot_script(): |
|
|
"""Write bot.js to disk""" |
|
|
try: |
|
|
with open('/tmp/bot.js', 'w') as f: |
|
|
f.write(BOT_SCRIPT) |
|
|
print("✅ Bot script written to /tmp/bot.js") |
|
|
return True |
|
|
except Exception as e: |
|
|
print(f"❌ Failed to write bot script: {e}") |
|
|
return False |
|
|
|
|
|
def ping_minecraft_server(): |
|
|
"""Ping Minecraft server and get real status""" |
|
|
global server_status |
|
|
|
|
|
try: |
|
|
start_time = time.time() |
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
|
sock.settimeout(5) |
|
|
|
|
|
|
|
|
sock.connect((SERVER_HOST, SERVER_PORT)) |
|
|
|
|
|
|
|
|
handshake = b'\x00\x00' + len(SERVER_HOST).to_bytes(1, 'big') + SERVER_HOST.encode('utf-8') |
|
|
handshake += SERVER_PORT.to_bytes(2, 'big') + b'\x01' |
|
|
handshake = len(handshake).to_bytes(1, 'big') + handshake |
|
|
|
|
|
|
|
|
status_request = b'\x01\x00' |
|
|
|
|
|
sock.send(handshake) |
|
|
sock.send(status_request) |
|
|
|
|
|
|
|
|
response_length = sock.recv(1024) |
|
|
response_data = sock.recv(4096) |
|
|
|
|
|
latency = int((time.time() - start_time) * 1000) |
|
|
|
|
|
sock.close() |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
json_start = response_data.find(b'{') |
|
|
if json_start != -1: |
|
|
json_data = response_data[json_start:].decode('utf-8', errors='ignore') |
|
|
server_info = json.loads(json_data) |
|
|
|
|
|
players = server_info.get('players', {}) |
|
|
online = players.get('online', 0) |
|
|
max_players = players.get('max', 0) |
|
|
|
|
|
server_status.update({ |
|
|
"online": True, |
|
|
"players": f"{online}/{max_players}", |
|
|
"latency": latency, |
|
|
"motd": server_info.get('description', {}).get('text', 'Minecraft Server'), |
|
|
"last_check": datetime.now().isoformat() |
|
|
}) |
|
|
else: |
|
|
server_status.update({ |
|
|
"online": True, |
|
|
"players": "?/?", |
|
|
"latency": latency, |
|
|
"last_check": datetime.now().isoformat() |
|
|
}) |
|
|
except: |
|
|
server_status.update({ |
|
|
"online": True, |
|
|
"players": "?/?", |
|
|
"latency": latency, |
|
|
"last_check": datetime.now().isoformat() |
|
|
}) |
|
|
|
|
|
except Exception as e: |
|
|
server_status.update({ |
|
|
"online": False, |
|
|
"players": "0/0", |
|
|
"latency": 0, |
|
|
"motd": str(e), |
|
|
"last_check": datetime.now().isoformat() |
|
|
}) |
|
|
|
|
|
def start_bot(name): |
|
|
"""Start a bot process""" |
|
|
if name in bot_processes: |
|
|
try: |
|
|
proc = bot_processes[name] |
|
|
proc.terminate() |
|
|
proc.wait(timeout=3) |
|
|
except: |
|
|
pass |
|
|
|
|
|
try: |
|
|
proc = subprocess.Popen( |
|
|
['node', '/tmp/bot.js', name, SERVER_HOST, str(SERVER_PORT), SERVER_VERSION], |
|
|
stdout=subprocess.PIPE, |
|
|
stderr=subprocess.STDOUT, |
|
|
text=True, |
|
|
bufsize=1 |
|
|
) |
|
|
|
|
|
bot_processes[name] = proc |
|
|
|
|
|
if name not in bots: |
|
|
bots[name] = { |
|
|
'status': 'connecting', |
|
|
'start_time': time.time(), |
|
|
'deaths': 0, |
|
|
'reconnects': 0, |
|
|
'position': {'x': 0, 'y': 0, 'z': 0}, |
|
|
'health': 20, |
|
|
'food': 20 |
|
|
} |
|
|
else: |
|
|
bots[name]['status'] = 'connecting' |
|
|
bots[name]['start_time'] = time.time() |
|
|
|
|
|
threading.Thread(target=monitor_bot, args=(name,), daemon=True).start() |
|
|
|
|
|
print(f"🚀 Started bot: {name}") |
|
|
return True |
|
|
except Exception as e: |
|
|
print(f"❌ Error starting bot {name}: {e}") |
|
|
return False |
|
|
|
|
|
def stop_bot(name): |
|
|
"""Stop a bot""" |
|
|
if name in bot_processes: |
|
|
try: |
|
|
proc = bot_processes[name] |
|
|
proc.terminate() |
|
|
proc.wait(timeout=3) |
|
|
del bot_processes[name] |
|
|
if name in bots: |
|
|
bots[name]['status'] = 'offline' |
|
|
print(f"⏹️ Stopped bot: {name}") |
|
|
except Exception as e: |
|
|
print(f"⚠️ Error stopping bot {name}: {e}") |
|
|
|
|
|
def monitor_bot(name): |
|
|
"""Monitor bot output""" |
|
|
if name not in bot_processes: |
|
|
return |
|
|
|
|
|
proc = bot_processes[name] |
|
|
|
|
|
try: |
|
|
while proc.poll() is None: |
|
|
line = proc.stdout.readline() |
|
|
if not line: |
|
|
continue |
|
|
|
|
|
try: |
|
|
data = json.loads(line.strip()) |
|
|
event = data.get('event') |
|
|
|
|
|
if name in bots: |
|
|
if event in ['connected', 'heartbeat']: |
|
|
bots[name]['status'] = 'online' |
|
|
bots[name]['health'] = data.get('health', 20) |
|
|
bots[name]['food'] = data.get('food', 20) |
|
|
elif event == 'position': |
|
|
bots[name]['position'] = { |
|
|
'x': data.get('x', 0), |
|
|
'y': data.get('y', 0), |
|
|
'z': data.get('z', 0) |
|
|
} |
|
|
bots[name]['health'] = data.get('health', 20) |
|
|
bots[name]['food'] = data.get('food', 20) |
|
|
elif event in ['death', 'died']: |
|
|
bots[name]['deaths'] += 1 |
|
|
elif event == 'reconnecting': |
|
|
bots[name]['reconnects'] += 1 |
|
|
bots[name]['status'] = 'connecting' |
|
|
elif event in ['disconnected', 'error', 'kicked']: |
|
|
bots[name]['status'] = 'offline' |
|
|
|
|
|
if event in ['connected', 'disconnected', 'kicked', 'error', 'death']: |
|
|
print(f"[{name}] {event}") |
|
|
|
|
|
except json.JSONDecodeError: |
|
|
pass |
|
|
except: |
|
|
pass |
|
|
|
|
|
if name in bots: |
|
|
bots[name]['status'] = 'offline' |
|
|
|
|
|
def rotation_manager(): |
|
|
"""Manage bot rotation""" |
|
|
global current_bot_index, rotation_start_time |
|
|
|
|
|
while True: |
|
|
try: |
|
|
current_bot = BOT_NAMES[current_bot_index] |
|
|
|
|
|
if rotation_start_time is None or time.time() - rotation_start_time >= ROTATION_DURATION: |
|
|
|
|
|
for name in BOT_NAMES: |
|
|
stop_bot(name) |
|
|
|
|
|
time.sleep(3) |
|
|
|
|
|
|
|
|
start_bot(current_bot) |
|
|
rotation_start_time = time.time() |
|
|
print(f"🔄 Rotation: {current_bot} is now active") |
|
|
|
|
|
|
|
|
current_bot_index = (current_bot_index + 1) % len(BOT_NAMES) |
|
|
|
|
|
time.sleep(5) |
|
|
except Exception as e: |
|
|
print(f"⚠️ Rotation error: {e}") |
|
|
time.sleep(10) |
|
|
|
|
|
def server_ping_loop(): |
|
|
"""Continuously ping server""" |
|
|
while True: |
|
|
try: |
|
|
ping_minecraft_server() |
|
|
time.sleep(1) |
|
|
except Exception as e: |
|
|
print(f"⚠️ Server ping error: {e}") |
|
|
time.sleep(2) |
|
|
|
|
|
def initialize(): |
|
|
"""Initialize bots""" |
|
|
for name in BOT_NAMES: |
|
|
bots[name] = { |
|
|
'status': 'offline', |
|
|
'start_time': 0, |
|
|
'deaths': 0, |
|
|
'reconnects': 0, |
|
|
'position': {'x': 0, 'y': 0, 'z': 0}, |
|
|
'health': 20, |
|
|
'food': 20 |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@app.route('/') |
|
|
def index(): |
|
|
"""Serve HTML""" |
|
|
return open('index.html').read() |
|
|
|
|
|
@app.route('/api/status') |
|
|
def api_status(): |
|
|
"""Get full status""" |
|
|
current_bot = BOT_NAMES[(current_bot_index - 1) % len(BOT_NAMES)] if rotation_start_time else BOT_NAMES[current_bot_index] |
|
|
next_bot = BOT_NAMES[current_bot_index] |
|
|
|
|
|
elapsed = int(time.time() - rotation_start_time) if rotation_start_time else 0 |
|
|
remaining = max(0, ROTATION_DURATION - elapsed) |
|
|
|
|
|
bot_list = [] |
|
|
for name in BOT_NAMES: |
|
|
bot = bots.get(name, {}) |
|
|
uptime = int(time.time() - bot.get('start_time', time.time())) if bot.get('status') == 'online' else 0 |
|
|
|
|
|
bot_list.append({ |
|
|
'name': name, |
|
|
'status': bot.get('status', 'offline'), |
|
|
'is_active': name == current_bot, |
|
|
'uptime': uptime, |
|
|
'deaths': bot.get('deaths', 0), |
|
|
'reconnects': bot.get('reconnects', 0), |
|
|
'position': bot.get('position', {'x': 0, 'y': 0, 'z': 0}), |
|
|
'health': bot.get('health', 20), |
|
|
'food': bot.get('food', 20) |
|
|
}) |
|
|
|
|
|
return jsonify({ |
|
|
'server_info': { |
|
|
'host': SERVER_HOST, |
|
|
'port': SERVER_PORT, |
|
|
'version': SERVER_VERSION |
|
|
}, |
|
|
'server_status': server_status, |
|
|
'rotation': { |
|
|
'current_bot': current_bot, |
|
|
'next_bot': next_bot, |
|
|
'elapsed': elapsed, |
|
|
'remaining': remaining, |
|
|
'queue': BOT_NAMES |
|
|
}, |
|
|
'bots': bot_list |
|
|
}) |
|
|
|
|
|
@app.route('/api/next_rotation', methods=['POST']) |
|
|
def api_next_rotation(): |
|
|
"""Force next rotation""" |
|
|
global rotation_start_time |
|
|
rotation_start_time = 0 |
|
|
return jsonify({'success': True}) |
|
|
|
|
|
def cleanup(sig=None, frame=None): |
|
|
"""Clean shutdown""" |
|
|
print("\n🛑 Shutting down...") |
|
|
for name in BOT_NAMES: |
|
|
stop_bot(name) |
|
|
sys.exit(0) |
|
|
|
|
|
if __name__ == '__main__': |
|
|
signal.signal(signal.SIGINT, cleanup) |
|
|
signal.signal(signal.SIGTERM, cleanup) |
|
|
|
|
|
print("=" * 60) |
|
|
print("🎮 MINECRAFT BOT MANAGER") |
|
|
print("=" * 60) |
|
|
|
|
|
if not write_bot_script(): |
|
|
print("❌ Failed to write bot script!") |
|
|
sys.exit(1) |
|
|
|
|
|
print(f"📡 Server: {SERVER_HOST}:{SERVER_PORT}") |
|
|
print(f"🤖 Bots: {', '.join(BOT_NAMES)}") |
|
|
print(f"⏱️ Rotation: {ROTATION_DURATION//60} minutes per bot") |
|
|
print("=" * 60) |
|
|
|
|
|
initialize() |
|
|
|
|
|
|
|
|
threading.Thread(target=rotation_manager, daemon=True).start() |
|
|
threading.Thread(target=server_ping_loop, daemon=True).start() |
|
|
|
|
|
print("🌐 Dashboard: http://localhost:7860") |
|
|
print("=" * 60) |
|
|
|
|
|
app.run(host='0.0.0.0', port=7860, debug=False, use_reloader=False) |