OrbitMC commited on
Commit
8afe353
·
verified ·
1 Parent(s): 45ea1eb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +389 -339
app.py CHANGED
@@ -5,7 +5,7 @@ import json
5
  import threading
6
  import subprocess
7
  import signal
8
- import requests
9
  from datetime import datetime, timedelta
10
  from flask import Flask, render_template_string, jsonify, request
11
  import logging
@@ -16,15 +16,18 @@ log.setLevel(logging.ERROR)
16
  app = Flask(__name__)
17
 
18
  # Configuration
19
-
20
- SHEET_ID = os.environ.get("SHEET_ID")
21
- SHEET_URL = f"https://docs.google.com/spreadsheets/d/{SHEET_ID}/export?format=csv"
 
 
22
 
23
  # Storage
24
  bots = {}
25
  bot_processes = {}
26
- last_rejoin = {}
27
- server_locks = {}
 
28
 
29
  # HTML Interface
30
  HTML = """<!DOCTYPE html>
@@ -38,140 +41,214 @@ HTML = """<!DOCTYPE html>
38
  body{font-family:Arial,sans-serif;background:#1a1a1a;color:#fff;padding:10px}
39
  .header{background:#2a2a2a;padding:15px;border-radius:8px;margin-bottom:15px}
40
  h1{font-size:24px;margin-bottom:10px}
 
 
 
 
 
 
 
41
  .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-bottom:15px}
42
  .stat{background:#333;padding:12px;border-radius:5px;text-align:center}
 
43
  .stat span{display:block;font-size:24px;font-weight:bold;color:#4CAF50;margin-top:5px}
44
- .controls{margin-bottom:15px}
 
 
 
 
45
  button{background:#4CAF50;border:none;color:white;padding:10px 20px;margin:5px;border-radius:5px;cursor:pointer;font-size:14px}
46
  button:hover{background:#45a049}
47
- button:disabled{background:#666;cursor:not-allowed}
48
- .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}
 
49
  .bot{background:#2a2a2a;padding:15px;border-radius:8px;border-left:4px solid #666}
50
  .bot.online{border-color:#4CAF50}
51
- .bot.offline{border-color:#f44336}
52
  .bot.connecting{border-color:#ff9800}
 
53
  .bot-name{font-weight:bold;font-size:16px;margin-bottom:8px}
54
  .bot-info{color:#999;font-size:13px;margin:4px 0}
55
  .bot-status{display:inline-block;padding:4px 10px;border-radius:4px;font-size:12px;margin-top:8px;font-weight:bold}
56
  .online .bot-status{background:#4CAF50;color:white}
57
- .offline .bot-status{background:#f44336;color:white}
58
  .connecting .bot-status{background:#ff9800;color:white}
 
59
  .msg{background:#2196F3;color:white;padding:10px;border-radius:5px;margin:10px 0}
60
  .error{background:#f44336}
 
61
  </style>
62
  </head>
63
  <body>
64
  <div class="header">
65
  <h1>🎮 Minecraft Bot Manager</h1>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  <div class="stats">
67
- <div class="stat">Total<span id="total">0</span></div>
68
- <div class="stat">Online<span id="online">0</span></div>
69
- <div class="stat">Offline<span id="offline">0</span></div>
70
- <div class="stat">Connecting<span id="connecting">0</span></div>
 
 
 
71
  </div>
 
 
 
 
 
 
 
 
 
 
72
  <div class="controls">
73
- <button onclick="reloadSheet()">📋 Reload Sheet</button>
74
  <button onclick="refreshStatus()">🔄 Refresh</button>
 
75
  </div>
76
  <div id="msg"></div>
77
  </div>
 
78
  <div class="grid" id="grid"></div>
 
79
  <script>
80
  let data={};
81
  let updateTimer;
82
 
83
  async function fetchBots(){
84
  try{
85
- const r=await fetch('/api/bots');
86
  data=await r.json();
87
- renderBots();
88
  }catch(e){
89
- showMsg('Failed to fetch bots','error');
90
  }
91
  }
92
 
93
- async function rejoinBot(name){
94
- if(!confirm(`Rejoin bot ${name}?`))return;
95
- try{
96
- const r=await fetch('/api/rejoin',{
97
- method:'POST',
98
- headers:{'Content-Type':'application/json'},
99
- body:JSON.stringify({name:name})
100
- });
101
- const res=await r.json();
102
- if(res.error){
103
- showMsg(res.error,'error');
104
- }else{
105
- showMsg(`Rejoining ${name}...`);
106
- fetchBots();
 
 
 
 
 
 
 
 
 
 
 
107
  }
108
- }catch(e){
109
- showMsg('Failed to rejoin bot','error');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  }
 
 
 
 
 
 
 
 
111
  }
112
 
113
- async function reloadSheet(){
 
114
  try{
115
- showMsg('Reloading from sheet...');
116
- await fetch('/api/reload',{method:'POST'});
117
- setTimeout(fetchBots,1000);
 
 
 
 
 
118
  }catch(e){
119
- showMsg('Failed to reload','error');
120
  }
121
  }
122
 
123
- async function refreshStatus(){
124
  fetchBots();
 
125
  }
126
 
127
  function showMsg(text,type=''){
128
  const el=document.getElementById('msg');
129
- el.className=type;
130
  el.textContent=text;
131
- if(text)setTimeout(()=>el.textContent='',5000);
132
- }
133
-
134
- function renderBots(){
135
- const g=document.getElementById('grid');
136
- g.innerHTML='';
137
- let total=0,online=0,offline=0,connecting=0;
138
-
139
-
140
- for(const[name,bot]of Object.entries(data)){
141
- total++;
142
- if(bot.status==='online')online++;
143
- else if(bot.status==='offline')offline++;
144
- else connecting++;
145
-
146
- const div=document.createElement('div');
147
- div.className='bot '+bot.status;
148
- let btnHtml='';
149
- if(bot.status==='offline'){
150
- if(bot.can_rejoin){
151
- btnHtml=`<button onclick="rejoinBot('${name.replace(/'/g,"\\'")}')">🔄 Rejoin</button>`;
152
- }else if(bot.cooldown>0){
153
- const m=Math.floor(bot.cooldown/60);
154
- const s=bot.cooldown%60;
155
- btnHtml=`<button disabled>⏳ ${m}:${s.toString().padStart(2,'0')}</button>`;
156
- }
157
- }
158
- div.innerHTML=`
159
- <div class="bot-name">${name}</div>
160
- <div class="bot-info">Version: ${bot.version}</div>
161
- <div class="bot-info">Server: ${bot.server||'Hidden'}</div>
162
- <div class="bot-status">${bot.status.toUpperCase()}</div>
163
- ${btnHtml}`;
164
- g.appendChild(div);
165
- }
166
- document.getElementById('total').textContent=total;
167
- document.getElementById('online').textContent=online;
168
- document.getElementById('offline').textContent=offline;
169
- document.getElementById('connecting').textContent=connecting;
170
  }
171
 
172
  function startAutoUpdate(){
173
  if(updateTimer)clearInterval(updateTimer);
174
- updateTimer=setInterval(fetchBots,5000);
175
  }
176
 
177
  fetchBots();
@@ -189,111 +266,106 @@ const host = process.argv[3];
189
  const port = parseInt(process.argv[4]) || 25565;
190
  const version = process.argv[5] || false;
191
 
192
- console.log(JSON.stringify({event:'starting',name:botName,host:host,port:port,version:version}));
193
-
194
- const bot = mineflayer.createBot({
195
- host: host,
196
- port: port,
197
- username: botName,
198
- auth: 'offline',
199
- version: version === 'false' ? false : version,
200
- hideErrors: false,
201
- checkTimeoutInterval: 30000,
202
- keepAlive: true,
203
- skipValidation: true
204
- });
205
 
 
206
  let isConnected = false;
207
  let afkInterval = null;
208
 
209
- function startAfk() {
210
- if (afkInterval) clearInterval(afkInterval);
211
-
212
- // Anti-AFK movement
213
- afkInterval = setInterval(() => {
214
- if (!bot.entity || !isConnected) return;
215
-
216
- // Random movement
217
- const actions = ['forward', 'back', 'left', 'right', 'jump', 'sneak'];
218
- const action = actions[Math.floor(Math.random() * actions.length)];
219
-
220
- bot.setControlState(action, true);
221
- setTimeout(() => {
222
- bot.setControlState(action, false);
223
- }, 200);
224
 
225
- // Random look
226
- if (Math.random() > 0.5) {
227
- const yaw = bot.entity.yaw + (Math.random() - 0.5);
228
- const pitch = bot.entity.pitch + (Math.random() - 0.5) * 0.5;
229
- bot.look(yaw, pitch, true);
230
- }
231
- }, 15000); // Move every 15 seconds
232
- }
 
 
 
 
 
 
 
 
 
 
233
 
234
- bot.once('spawn', () => {
235
- console.log(JSON.stringify({event:'connected',name:botName}));
236
- isConnected = true;
237
- startAfk();
238
-
239
- // Handle resource packs
240
- bot._client.on('resource_pack_send', (packet) => {
241
- bot._client.write('resource_pack_receive', { result: 0 });
242
  });
243
-
244
- // Random chat to stay active
245
- setInterval(() => {
246
- if (isConnected && Math.random() > 0.95) {
247
- const messages = ['hi', 'hello', 'hey', ':)', 'o/', 'test'];
248
- bot.chat(messages[Math.floor(Math.random() * messages.length)]);
249
- }
250
- }, 300000); // Every 5 minutes maybe chat
251
- });
252
 
253
- bot.on('respawn', () => {
254
- console.log(JSON.stringify({event:'respawned',name:botName}));
255
- startAfk();
256
- });
257
 
258
- bot.on('death', () => {
259
- console.log(JSON.stringify({event:'died',name:botName}));
260
- });
261
 
262
- bot.on('kicked', (reason) => {
263
- console.log(JSON.stringify({event:'kicked',name:botName,reason:reason}));
264
- isConnected = false;
265
- });
266
 
267
- bot.on('error', (err) => {
268
- console.log(JSON.stringify({event:'error',name:botName,error:err.message}));
269
- });
270
 
271
- bot.on('end', (reason) => {
272
- console.log(JSON.stringify({event:'disconnected',name:botName,reason:reason}));
273
- isConnected = false;
274
- if (afkInterval) clearInterval(afkInterval);
275
- process.exit();
276
- });
 
 
 
 
 
 
277
 
278
- // Keep alive signal
279
- setInterval(() => {
280
- if (isConnected) {
281
- console.log(JSON.stringify({event:'alive',name:botName}));
282
- }
283
- }, 30000);
284
 
285
- // Graceful shutdown
286
- process.on('SIGTERM', () => {
287
- if (afkInterval) clearInterval(afkInterval);
288
- bot.quit();
289
- process.exit();
290
- });
291
 
292
- process.on('SIGINT', () => {
293
- if (afkInterval) clearInterval(afkInterval);
294
- bot.quit();
295
- process.exit();
296
- });
 
 
 
297
  """
298
 
299
  def write_bot_script():
@@ -307,47 +379,27 @@ def write_bot_script():
307
  print(f"Error writing bot script: {e}")
308
  return False
309
 
310
- def fetch_sheet_data():
311
- """Fetch bot configuration from Google Sheets"""
 
312
  try:
313
- resp = requests.get(SHEET_URL, timeout=10)
314
- if resp.status_code != 200:
315
- return []
 
316
 
317
- lines = resp.text.strip().split('\n')
318
- data = []
319
-
320
- for i, line in enumerate(lines[1:], 1): # Skip header
321
- parts = [p.strip().strip('"') for p in line.split(',')]
322
- if len(parts) >= 3:
323
- name = parts[0]
324
- ip = parts[1]
325
- port = parts[2]
326
- version = parts[3] if len(parts) > 3 else "false"
327
-
328
- if name and ip and port and port.isdigit():
329
- data.append({
330
- 'name': name,
331
- 'ip': ip,
332
- 'port': port,
333
- 'version': version if version else "false"
334
- })
335
 
336
- print(f"Loaded {len(data)} bots from sheet")
337
- return data
338
  except Exception as e:
339
- print(f"Error fetching sheet: {e}")
340
- return []
 
341
 
342
- def start_bot(config):
343
  """Start a bot process"""
344
- name = config['name']
345
- server_key = f"{config['ip']}:{config['port']}"
346
-
347
- # Check if server already has a bot
348
- if server_key in server_locks and server_locks[server_key] != name:
349
- return False, "Server already has another bot"
350
-
351
  # Kill existing process if any
352
  if name in bot_processes:
353
  try:
@@ -358,9 +410,8 @@ def start_bot(config):
358
  pass
359
 
360
  try:
361
- # Start new bot process
362
  proc = subprocess.Popen(
363
- ['node', '/tmp/bot.js', name, config['ip'], config['port'], config['version']],
364
  stdout=subprocess.PIPE,
365
  stderr=subprocess.STDOUT,
366
  text=True,
@@ -368,23 +419,41 @@ def start_bot(config):
368
  )
369
 
370
  bot_processes[name] = proc
371
- server_locks[server_key] = name
372
 
373
- bots[name] = {
374
- 'status': 'connecting',
375
- 'ip': config['ip'],
376
- 'port': config['port'],
377
- 'version': config['version'],
378
- 'server': server_key,
379
- 'start_time': time.time()
380
- }
 
 
 
381
 
382
  # Start monitoring thread
383
  threading.Thread(target=monitor_bot, args=(name,), daemon=True).start()
384
 
385
- return True, "Bot started"
 
386
  except Exception as e:
387
- return False, str(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
 
389
  def monitor_bot(name):
390
  """Monitor bot process output"""
@@ -403,136 +472,122 @@ def monitor_bot(name):
403
  data = json.loads(line.strip())
404
  event = data.get('event')
405
 
406
- if event == 'connected' or event == 'alive':
407
- if name in bots:
408
  bots[name]['status'] = 'online'
409
- elif event in ['disconnected', 'kicked', 'error']:
410
- if name in bots:
 
 
 
 
411
  bots[name]['status'] = 'offline'
412
 
413
- # Only log important events
414
- if event in ['connected', 'disconnected', 'kicked', 'error']:
415
- print(f"[{name}] {event}: {data.get('reason', '')}")
416
 
417
  except json.JSONDecodeError:
418
- # Not JSON, ignore
419
  pass
420
  except:
421
  pass
422
 
423
- # Process ended
424
  if name in bots:
425
  bots[name]['status'] = 'offline'
426
- server = bots[name].get('server')
427
- if server in server_locks and server_locks[server] == name:
428
- del server_locks[server]
429
-
430
- def sync_bots():
431
- """Sync bots with sheet data"""
432
- data = fetch_sheet_data()
433
- names = {d['name'] for d in data}
434
-
435
- # Remove deleted bots
436
- for name in list(bots.keys()):
437
- if name not in names:
438
- if name in bot_processes:
439
- try:
440
- bot_processes[name].terminate()
441
- except:
442
- pass
443
- if name in bots:
444
- server = bots[name].get('server')
445
- if server in server_locks and server_locks[server] == name:
446
- del server_locks[server]
447
- del bots[name]
448
- print(f"Removed bot: {name}")
449
 
450
- # Add new bots
451
- for config in data:
452
- if config['name'] not in bots:
453
- success, msg = start_bot(config)
454
- if success:
455
- print(f"Added bot: {config['name']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
 
457
  @app.route('/')
458
  def index():
459
  return HTML
460
 
461
- @app.route('/api/bots')
462
- def api_bots():
463
- """Get bot status"""
464
- result = {}
465
- now = datetime.now()
466
 
467
- for name, bot in bots.items():
468
- can_rejoin = True
469
- cooldown = 0
470
-
471
- if name in last_rejoin:
472
- elapsed = now - last_rejoin[name]
473
- if elapsed < timedelta(hours=1):
474
- can_rejoin = False
475
- cooldown = int((timedelta(hours=1) - elapsed).total_seconds())
476
-
477
- result[name] = {
478
- 'status': bot['status'],
479
- 'version': bot['version'],
480
- 'server': 'Hidden', # Hide server info
481
- 'can_rejoin': can_rejoin,
482
- 'cooldown': cooldown
483
- }
484
 
485
- return jsonify(result)
486
-
487
- @app.route('/api/rejoin', methods=['POST'])
488
- def api_rejoin():
489
- """Rejoin a bot"""
490
- name = request.json.get('name')
491
-
492
- if name not in bots:
493
- return jsonify({'error': 'Bot not found'}), 404
494
-
495
- now = datetime.now()
496
-
497
- # Check cooldown
498
- if name in last_rejoin:
499
- elapsed = now - last_rejoin[name]
500
- if elapsed < timedelta(hours=1):
501
- mins = int((timedelta(hours=1) - elapsed).total_seconds() / 60)
502
- return jsonify({'error': f'Please wait {mins} minutes before rejoining'}), 429
503
-
504
- if bots[name]['status'] == 'online':
505
- return jsonify({'error': 'Bot is already online'}), 400
506
-
507
- config = {
508
- 'name': name,
509
- 'ip': bots[name]['ip'],
510
- 'port': bots[name]['port'],
511
- 'version': bots[name]['version']
512
- }
513
-
514
- success, msg = start_bot(config)
515
 
516
- if success:
517
- last_rejoin[name] = now
518
- return jsonify({'success': True, 'message': 'Bot rejoining'})
519
- else:
520
- return jsonify({'error': msg}), 500
521
-
522
- @app.route('/api/reload', methods=['POST'])
523
- def api_reload():
524
- """Reload bots from sheet"""
525
- sync_bots()
526
- return jsonify({'success': True, 'message': 'Reloaded from sheet'})
 
 
 
 
 
 
 
 
 
 
527
 
528
  def cleanup(sig=None, frame=None):
529
  """Clean shutdown"""
530
  print("\nShutting down...")
531
- for name, proc in bot_processes.items():
532
- try:
533
- proc.terminate()
534
- except:
535
- pass
536
  sys.exit(0)
537
 
538
  if __name__ == '__main__':
@@ -545,21 +600,16 @@ if __name__ == '__main__':
545
  sys.exit(1)
546
 
547
  print("Bot Manager starting...")
 
 
 
548
 
549
- # Initial sync
550
- sync_bots()
551
-
552
- # Periodic sync with sheet
553
- def auto_sync():
554
- while True:
555
- time.sleep(60) # Check every minute
556
- try:
557
- sync_bots()
558
- except Exception as e:
559
- print(f"Auto sync error: {e}")
560
 
561
- threading.Thread(target=auto_sync, daemon=True).start()
 
562
 
563
  # Start Flask server
564
- print("Server starting on port 7860...")
565
  app.run(host='0.0.0.0', port=7860, debug=False, use_reloader=False)
 
5
  import threading
6
  import subprocess
7
  import signal
8
+ import socket
9
  from datetime import datetime, timedelta
10
  from flask import Flask, render_template_string, jsonify, request
11
  import logging
 
16
  app = Flask(__name__)
17
 
18
  # Configuration
19
+ SERVER_HOST = "orbitmc.progamer.me"
20
+ SERVER_PORT = 40675
21
+ SERVER_VERSION = "1.21.1"
22
+ BOT_NAMES = ["moderator_1", "moderator_2", "moderator_3", "moderator_4", "moderator_5"]
23
+ ROTATION_DURATION = 3600 # 1 hour in seconds
24
 
25
  # Storage
26
  bots = {}
27
  bot_processes = {}
28
+ current_bot_index = 0
29
+ rotation_start_time = None
30
+ server_status = {"online": False, "players": "?", "last_check": None}
31
 
32
  # HTML Interface
33
  HTML = """<!DOCTYPE html>
 
41
  body{font-family:Arial,sans-serif;background:#1a1a1a;color:#fff;padding:10px}
42
  .header{background:#2a2a2a;padding:15px;border-radius:8px;margin-bottom:15px}
43
  h1{font-size:24px;margin-bottom:10px}
44
+ .server-status{background:#333;padding:15px;border-radius:8px;margin-bottom:15px;border-left:4px solid #666}
45
+ .server-status.online{border-color:#4CAF50}
46
+ .server-status.offline{border-color:#f44336}
47
+ .server-info{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:10px}
48
+ .server-item{background:#2a2a2a;padding:10px;border-radius:5px}
49
+ .server-item label{display:block;color:#999;font-size:12px;margin-bottom:3px}
50
+ .server-item span{font-size:16px;font-weight:bold;color:#4CAF50}
51
  .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-bottom:15px}
52
  .stat{background:#333;padding:12px;border-radius:5px;text-align:center}
53
+ .stat label{display:block;color:#999;font-size:12px;margin-bottom:5px}
54
  .stat span{display:block;font-size:24px;font-weight:bold;color:#4CAF50;margin-top:5px}
55
+ .rotation-info{background:#1976D2;padding:15px;border-radius:8px;margin-bottom:15px;text-align:center}
56
+ .rotation-info h2{font-size:18px;margin-bottom:8px}
57
+ .rotation-info .current-bot{font-size:32px;font-weight:bold;margin:10px 0}
58
+ .rotation-info .timer{font-size:24px;color:#FFF59D}
59
+ .controls{margin-bottom:15px;text-align:center}
60
  button{background:#4CAF50;border:none;color:white;padding:10px 20px;margin:5px;border-radius:5px;cursor:pointer;font-size:14px}
61
  button:hover{background:#45a049}
62
+ button.secondary{background:#2196F3}
63
+ button.secondary:hover{background:#1976D2}
64
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}
65
  .bot{background:#2a2a2a;padding:15px;border-radius:8px;border-left:4px solid #666}
66
  .bot.online{border-color:#4CAF50}
67
+ .bot.offline{border-color:#666}
68
  .bot.connecting{border-color:#ff9800}
69
+ .bot.active{background:#1565C0;border-color:#2196F3}
70
  .bot-name{font-weight:bold;font-size:16px;margin-bottom:8px}
71
  .bot-info{color:#999;font-size:13px;margin:4px 0}
72
  .bot-status{display:inline-block;padding:4px 10px;border-radius:4px;font-size:12px;margin-top:8px;font-weight:bold}
73
  .online .bot-status{background:#4CAF50;color:white}
74
+ .offline .bot-status{background:#666;color:white}
75
  .connecting .bot-status{background:#ff9800;color:white}
76
+ .active .bot-status{background:#2196F3;color:white}
77
  .msg{background:#2196F3;color:white;padding:10px;border-radius:5px;margin:10px 0}
78
  .error{background:#f44336}
79
+ .badge{display:inline-block;background:#FFC107;color:#000;padding:3px 8px;border-radius:3px;font-size:11px;font-weight:bold;margin-left:8px}
80
  </style>
81
  </head>
82
  <body>
83
  <div class="header">
84
  <h1>🎮 Minecraft Bot Manager</h1>
85
+
86
+ <div class="server-status" id="server-status">
87
+ <h2>📡 Server Status</h2>
88
+ <div class="server-info">
89
+ <div class="server-item">
90
+ <label>Address</label>
91
+ <span id="server-address">-</span>
92
+ </div>
93
+ <div class="server-item">
94
+ <label>Status</label>
95
+ <span id="server-online">-</span>
96
+ </div>
97
+ <div class="server-item">
98
+ <label>Players</label>
99
+ <span id="server-players">-</span>
100
+ </div>
101
+ <div class="server-item">
102
+ <label>Version</label>
103
+ <span id="server-version">-</span>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <div class="rotation-info">
109
+ <h2>🔄 Current Rotation</h2>
110
+ <div class="current-bot" id="current-bot">-</div>
111
+ <div class="timer" id="rotation-timer">⏱️ 00:00</div>
112
+ <small>Next rotation in <span id="next-rotation">-</span></small>
113
+ </div>
114
+
115
  <div class="stats">
116
+ <div class="stat">
117
+ <label>Total Bots</label>
118
+ <span id="total">0</span>
119
+ </div>
120
+ <div class="stat">
121
+ <label>Active</label>
122
+ <span id="active">0</span>
123
  </div>
124
+ <div class="stat">
125
+ <label>Online</label>
126
+ <span id="online">0</span>
127
+ </div>
128
+ <div class="stat">
129
+ <label>Offline</label>
130
+ <span id="offline">0</span>
131
+ </div>
132
+ </div>
133
+
134
  <div class="controls">
 
135
  <button onclick="refreshStatus()">🔄 Refresh</button>
136
+ <button class="secondary" onclick="forceRotation()">⏭️ Next Bot</button>
137
  </div>
138
  <div id="msg"></div>
139
  </div>
140
+
141
  <div class="grid" id="grid"></div>
142
+
143
  <script>
144
  let data={};
145
  let updateTimer;
146
 
147
  async function fetchBots(){
148
  try{
149
+ const r=await fetch('/api/status');
150
  data=await r.json();
151
+ renderStatus();
152
  }catch(e){
153
+ showMsg('Failed to fetch status','error');
154
  }
155
  }
156
 
157
+ function renderStatus(){
158
+ // Server Status
159
+ const serverStatus = data.server_status;
160
+ const statusEl = document.getElementById('server-status');
161
+ statusEl.className = 'server-status ' + (serverStatus.online ? 'online' : 'offline');
162
+
163
+ document.getElementById('server-address').textContent = data.server_info.host + ':' + data.server_info.port;
164
+ document.getElementById('server-online').textContent = serverStatus.online ? '✅ Online' : '❌ Offline';
165
+ document.getElementById('server-players').textContent = serverStatus.players;
166
+ document.getElementById('server-version').textContent = data.server_info.version;
167
+
168
+ // Rotation Info
169
+ document.getElementById('current-bot').textContent = data.rotation.current_bot || '-';
170
+ document.getElementById('rotation-timer').textContent = '⏱️ ' + formatTime(data.rotation.elapsed);
171
+ document.getElementById('next-rotation').textContent = formatTime(data.rotation.remaining);
172
+
173
+ // Stats
174
+ const bots = data.bots;
175
+ let total = 0, active = 0, online = 0, offline = 0;
176
+
177
+ for(const bot of bots){
178
+ total++;
179
+ if(bot.is_active) active++;
180
+ if(bot.status === 'online') online++;
181
+ else if(bot.status === 'offline') offline++;
182
  }
183
+
184
+ document.getElementById('total').textContent = total;
185
+ document.getElementById('active').textContent = active;
186
+ document.getElementById('online').textContent = online;
187
+ document.getElementById('offline').textContent = offline;
188
+
189
+ // Bot Grid
190
+ const grid = document.getElementById('grid');
191
+ grid.innerHTML = '';
192
+
193
+ bots.forEach(bot => {
194
+ const div = document.createElement('div');
195
+ let className = 'bot ' + bot.status;
196
+ if(bot.is_active) className += ' active';
197
+ div.className = className;
198
+
199
+ const badge = bot.is_active ? '<span class="badge">ACTIVE</span>' : '';
200
+
201
+ div.innerHTML = `
202
+ <div class="bot-name">${bot.name}${badge}</div>
203
+ <div class="bot-info">Status: ${bot.status.toUpperCase()}</div>
204
+ <div class="bot-info">Uptime: ${formatTime(bot.uptime)}</div>
205
+ <div class="bot-info">Deaths: ${bot.deaths}</div>
206
+ <div class="bot-info">Reconnects: ${bot.reconnects}</div>
207
+ `;
208
+ grid.appendChild(div);
209
+ });
210
  }
211
+
212
+ function formatTime(seconds){
213
+ if(!seconds || seconds < 0) return '0:00';
214
+ const h = Math.floor(seconds / 3600);
215
+ const m = Math.floor((seconds % 3600) / 60);
216
+ const s = seconds % 60;
217
+ if(h > 0) return `${h}:${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`;
218
+ return `${m}:${s.toString().padStart(2,'0')}`;
219
  }
220
 
221
+ async function forceRotation(){
222
+ if(!confirm('Force switch to next bot?')) return;
223
  try{
224
+ const r = await fetch('/api/next_rotation', {method: 'POST'});
225
+ const res = await r.json();
226
+ if(res.success){
227
+ showMsg('Switching to next bot...');
228
+ setTimeout(fetchBots, 1000);
229
+ }else{
230
+ showMsg(res.error || 'Failed to rotate', 'error');
231
+ }
232
  }catch(e){
233
+ showMsg('Failed to rotate bot','error');
234
  }
235
  }
236
 
237
+ function refreshStatus(){
238
  fetchBots();
239
+ showMsg('Refreshing...');
240
  }
241
 
242
  function showMsg(text,type=''){
243
  const el=document.getElementById('msg');
244
+ el.className='msg '+(type||'');
245
  el.textContent=text;
246
+ if(text)setTimeout(()=>el.textContent='',3000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  }
248
 
249
  function startAutoUpdate(){
250
  if(updateTimer)clearInterval(updateTimer);
251
+ updateTimer=setInterval(fetchBots,2000);
252
  }
253
 
254
  fetchBots();
 
266
  const port = parseInt(process.argv[4]) || 25565;
267
  const version = process.argv[5] || false;
268
 
269
+ console.log(JSON.stringify({event:'starting',name:botName}));
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
+ let reconnectAttempts = 0;
272
  let isConnected = false;
273
  let afkInterval = null;
274
 
275
+ function createBot() {
276
+ const bot = mineflayer.createBot({
277
+ host: host,
278
+ port: port,
279
+ username: botName,
280
+ auth: 'offline',
281
+ version: version === 'false' ? false : version,
282
+ hideErrors: false,
283
+ checkTimeoutInterval: 30000,
284
+ keepAlive: true,
285
+ skipValidation: true
286
+ });
287
+
288
+ function startAfk() {
289
+ if (afkInterval) clearInterval(afkInterval);
290
 
291
+ afkInterval = setInterval(() => {
292
+ if (!bot.entity || !isConnected) return;
293
+
294
+ const actions = ['forward', 'back', 'left', 'right'];
295
+ const action = actions[Math.floor(Math.random() * actions.length)];
296
+
297
+ bot.setControlState(action, true);
298
+ setTimeout(() => {
299
+ bot.setControlState(action, false);
300
+ }, 100);
301
+
302
+ if (Math.random() > 0.5) {
303
+ const yaw = bot.entity.yaw + (Math.random() - 0.5);
304
+ const pitch = bot.entity.pitch + (Math.random() - 0.5) * 0.5;
305
+ bot.look(yaw, pitch, true);
306
+ }
307
+ }, 10000);
308
+ }
309
 
310
+ bot.once('spawn', () => {
311
+ console.log(JSON.stringify({event:'connected',name:botName}));
312
+ isConnected = true;
313
+ reconnectAttempts = 0;
314
+ startAfk();
 
 
 
315
  });
 
 
 
 
 
 
 
 
 
316
 
317
+ bot.on('respawn', () => {
318
+ console.log(JSON.stringify({event:'respawned',name:botName}));
319
+ startAfk();
320
+ });
321
 
322
+ bot.on('death', () => {
323
+ console.log(JSON.stringify({event:'death',name:botName}));
324
+ });
325
 
326
+ bot.on('kicked', (reason) => {
327
+ console.log(JSON.stringify({event:'kicked',name:botName,reason:reason}));
328
+ isConnected = false;
329
+ });
330
 
331
+ bot.on('error', (err) => {
332
+ console.log(JSON.stringify({event:'error',name:botName,error:err.message}));
333
+ });
334
 
335
+ bot.on('end', (reason) => {
336
+ console.log(JSON.stringify({event:'disconnected',name:botName}));
337
+ isConnected = false;
338
+ if (afkInterval) clearInterval(afkInterval);
339
+
340
+ // Auto reconnect
341
+ reconnectAttempts++;
342
+ console.log(JSON.stringify({event:'reconnecting',name:botName,attempt:reconnectAttempts}));
343
+ setTimeout(() => {
344
+ createBot();
345
+ }, 5000);
346
+ });
347
 
348
+ // Heartbeat
349
+ setInterval(() => {
350
+ if (isConnected) {
351
+ console.log(JSON.stringify({event:'alive',name:botName}));
352
+ }
353
+ }, 30000);
354
 
355
+ process.on('SIGTERM', () => {
356
+ if (afkInterval) clearInterval(afkInterval);
357
+ bot.quit();
358
+ process.exit();
359
+ });
 
360
 
361
+ process.on('SIGINT', () => {
362
+ if (afkInterval) clearInterval(afkInterval);
363
+ bot.quit();
364
+ process.exit();
365
+ });
366
+ }
367
+
368
+ createBot();
369
  """
370
 
371
  def write_bot_script():
 
379
  print(f"Error writing bot script: {e}")
380
  return False
381
 
382
+ def check_server_status():
383
+ """Check if Minecraft server is online"""
384
+ global server_status
385
  try:
386
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
387
+ sock.settimeout(3)
388
+ result = sock.connect_ex((SERVER_HOST, SERVER_PORT))
389
+ sock.close()
390
 
391
+ server_status["online"] = result == 0
392
+ server_status["last_check"] = datetime.now()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
 
394
+ if not server_status["online"]:
395
+ server_status["players"] = "N/A"
396
  except Exception as e:
397
+ server_status["online"] = False
398
+ server_status["players"] = "N/A"
399
+ server_status["last_check"] = datetime.now()
400
 
401
+ def start_bot(name):
402
  """Start a bot process"""
 
 
 
 
 
 
 
403
  # Kill existing process if any
404
  if name in bot_processes:
405
  try:
 
410
  pass
411
 
412
  try:
 
413
  proc = subprocess.Popen(
414
+ ['node', '/tmp/bot.js', name, SERVER_HOST, str(SERVER_PORT), SERVER_VERSION],
415
  stdout=subprocess.PIPE,
416
  stderr=subprocess.STDOUT,
417
  text=True,
 
419
  )
420
 
421
  bot_processes[name] = proc
 
422
 
423
+ if name not in bots:
424
+ bots[name] = {
425
+ 'status': 'connecting',
426
+ 'start_time': time.time(),
427
+ 'deaths': 0,
428
+ 'reconnects': 0,
429
+ 'uptime': 0
430
+ }
431
+ else:
432
+ bots[name]['status'] = 'connecting'
433
+ bots[name]['start_time'] = time.time()
434
 
435
  # Start monitoring thread
436
  threading.Thread(target=monitor_bot, args=(name,), daemon=True).start()
437
 
438
+ print(f"Started bot: {name}")
439
+ return True
440
  except Exception as e:
441
+ print(f"Error starting bot {name}: {e}")
442
+ return False
443
+
444
+ def stop_bot(name):
445
+ """Stop a bot process"""
446
+ if name in bot_processes:
447
+ try:
448
+ proc = bot_processes[name]
449
+ proc.terminate()
450
+ proc.wait(timeout=3)
451
+ del bot_processes[name]
452
+ if name in bots:
453
+ bots[name]['status'] = 'offline'
454
+ print(f"Stopped bot: {name}")
455
+ except Exception as e:
456
+ print(f"Error stopping bot {name}: {e}")
457
 
458
  def monitor_bot(name):
459
  """Monitor bot process output"""
 
472
  data = json.loads(line.strip())
473
  event = data.get('event')
474
 
475
+ if name in bots:
476
+ if event == 'connected' or event == 'alive':
477
  bots[name]['status'] = 'online'
478
+ elif event == 'death':
479
+ bots[name]['deaths'] += 1
480
+ elif event == 'reconnecting':
481
+ bots[name]['reconnects'] += 1
482
+ bots[name]['status'] = 'connecting'
483
+ elif event in ['disconnected', 'error']:
484
  bots[name]['status'] = 'offline'
485
 
486
+ if event in ['connected', 'disconnected', 'kicked', 'error', 'death']:
487
+ print(f"[{name}] {event}")
 
488
 
489
  except json.JSONDecodeError:
 
490
  pass
491
  except:
492
  pass
493
 
 
494
  if name in bots:
495
  bots[name]['status'] = 'offline'
496
+
497
+ def rotation_manager():
498
+ """Manage bot rotation"""
499
+ global current_bot_index, rotation_start_time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
 
501
+ while True:
502
+ try:
503
+ current_bot = BOT_NAMES[current_bot_index]
504
+
505
+ # Start current bot
506
+ if rotation_start_time is None or time.time() - rotation_start_time >= ROTATION_DURATION:
507
+ # Stop all bots
508
+ for name in BOT_NAMES:
509
+ stop_bot(name)
510
+
511
+ time.sleep(2)
512
+
513
+ # Start new bot
514
+ start_bot(current_bot)
515
+ rotation_start_time = time.time()
516
+ print(f"Rotation: Now active -> {current_bot}")
517
+
518
+ # Move to next bot for next rotation
519
+ current_bot_index = (current_bot_index + 1) % len(BOT_NAMES)
520
+
521
+ time.sleep(5)
522
+ except Exception as e:
523
+ print(f"Rotation error: {e}")
524
+ time.sleep(10)
525
+
526
+ def initialize_bots():
527
+ """Initialize all bots in storage"""
528
+ for name in BOT_NAMES:
529
+ bots[name] = {
530
+ 'status': 'offline',
531
+ 'start_time': 0,
532
+ 'deaths': 0,
533
+ 'reconnects': 0,
534
+ 'uptime': 0
535
+ }
536
 
537
  @app.route('/')
538
  def index():
539
  return HTML
540
 
541
+ @app.route('/api/status')
542
+ def api_status():
543
+ """Get complete status"""
544
+ check_server_status()
 
545
 
546
+ current_bot = BOT_NAMES[current_bot_index - 1 if rotation_start_time else current_bot_index]
547
+ elapsed = int(time.time() - rotation_start_time) if rotation_start_time else 0
548
+ remaining = max(0, ROTATION_DURATION - elapsed)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
 
550
+ bot_list = []
551
+ for name in BOT_NAMES:
552
+ bot = bots.get(name, {})
553
+ uptime = int(time.time() - bot.get('start_time', time.time())) if bot.get('status') == 'online' else 0
554
+
555
+ bot_list.append({
556
+ 'name': name,
557
+ 'status': bot.get('status', 'offline'),
558
+ 'is_active': name == current_bot,
559
+ 'uptime': uptime,
560
+ 'deaths': bot.get('deaths', 0),
561
+ 'reconnects': bot.get('reconnects', 0)
562
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
563
 
564
+ return jsonify({
565
+ 'server_info': {
566
+ 'host': SERVER_HOST,
567
+ 'port': SERVER_PORT,
568
+ 'version': SERVER_VERSION
569
+ },
570
+ 'server_status': server_status,
571
+ 'rotation': {
572
+ 'current_bot': current_bot,
573
+ 'elapsed': elapsed,
574
+ 'remaining': remaining
575
+ },
576
+ 'bots': bot_list
577
+ })
578
+
579
+ @app.route('/api/next_rotation', methods=['POST'])
580
+ def api_next_rotation():
581
+ """Force next rotation"""
582
+ global rotation_start_time
583
+ rotation_start_time = 0 # Force rotation on next cycle
584
+ return jsonify({'success': True})
585
 
586
  def cleanup(sig=None, frame=None):
587
  """Clean shutdown"""
588
  print("\nShutting down...")
589
+ for name in BOT_NAMES:
590
+ stop_bot(name)
 
 
 
591
  sys.exit(0)
592
 
593
  if __name__ == '__main__':
 
600
  sys.exit(1)
601
 
602
  print("Bot Manager starting...")
603
+ print(f"Server: {SERVER_HOST}:{SERVER_PORT}")
604
+ print(f"Bots: {', '.join(BOT_NAMES)}")
605
+ print(f"Rotation: {ROTATION_DURATION//60} minutes per bot")
606
 
607
+ # Initialize
608
+ initialize_bots()
 
 
 
 
 
 
 
 
 
609
 
610
+ # Start rotation manager
611
+ threading.Thread(target=rotation_manager, daemon=True).start()
612
 
613
  # Start Flask server
614
+ print("Dashboard starting on port 7860...")
615
  app.run(host='0.0.0.0', port=7860, debug=False, use_reloader=False)