File size: 41,347 Bytes
ba3eee4
71a610e
 
ba3eee4
 
5c82e3f
71a610e
 
 
 
 
 
 
 
3d6b4ba
71a610e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0cc361
ba3eee4
 
50e917b
 
71a610e
 
89d1903
71a610e
f0cc361
71a610e
50e917b
 
 
 
 
 
71a610e
 
 
 
f0cc361
71a610e
 
 
f0cc361
89d1903
71a610e
 
89d1903
71a610e
 
 
3d6b4ba
 
71a610e
3d6b4ba
 
 
 
 
 
 
 
71a610e
 
 
64b8acc
 
 
 
 
 
 
 
 
 
 
 
71a610e
 
 
 
 
 
 
 
 
 
64b8acc
 
7a20c38
 
 
 
 
 
 
 
 
64b8acc
7a20c38
64b8acc
 
 
 
89d1903
 
64b8acc
71a610e
 
 
 
 
 
 
 
03eafbf
64b8acc
 
 
 
89d1903
 
f1411bc
03eafbf
 
71a610e
03eafbf
 
64b8acc
 
 
 
 
 
 
 
 
 
 
 
 
 
89d1903
64b8acc
 
89d1903
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7a20c38
 
 
 
 
 
 
89d1903
 
 
 
 
 
 
2b59478
89d1903
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82dafb3
89d1903
 
82dafb3
 
 
 
71a610e
 
 
 
 
89d1903
71a610e
 
 
 
 
 
 
 
50e917b
82dafb3
89d1903
71a610e
 
 
 
 
 
 
 
 
 
89d1903
 
 
 
 
 
 
 
 
 
 
 
 
71a610e
 
89d1903
 
71a610e
 
89d1903
71a610e
89d1903
 
 
 
 
 
 
 
 
 
71a610e
 
89d1903
71a610e
89d1903
 
 
 
71a610e
89d1903
71a610e
50e917b
 
64b8acc
89d1903
64b8acc
 
89d1903
 
 
64b8acc
 
 
89d1903
 
64b8acc
 
 
 
 
 
 
 
 
 
 
 
 
3d6b4ba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89d1903
 
 
 
 
 
 
3d6b4ba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89d1903
3d6b4ba
 
 
 
89d1903
 
 
 
 
 
 
 
 
 
 
71a610e
89d1903
71a610e
 
50e917b
 
89d1903
71a610e
89d1903
71a610e
 
03eafbf
 
71a610e
 
 
 
 
89d1903
 
71a610e
 
 
89d1903
71a610e
 
 
fb8a6d2
5c82e3f
71a610e
 
 
03eafbf
 
89d1903
71a610e
2d1b6e1
89d1903
71a610e
89d1903
 
 
 
7a20c38
 
 
 
 
 
 
2d1b6e1
89d1903
2d1b6e1
71a610e
89d1903
71a610e
89d1903
71a610e
89d1903
7518602
71a610e
89d1903
36e8b2f
71a610e
89d1903
71a610e
89d1903
 
7a20c38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89d1903
7a20c38
89d1903
 
7a20c38
89d1903
2b59478
71a610e
89d1903
 
7a20c38
 
 
 
 
89d1903
 
50e917b
71a610e
 
89d1903
 
7a20c38
 
 
 
 
 
89d1903
 
82dafb3
71a610e
89d1903
71a610e
89d1903
1b0827e
71a610e
64b8acc
 
 
 
 
 
89d1903
64b8acc
 
 
 
 
 
89d1903
64b8acc
 
 
 
 
 
89d1903
64b8acc
 
 
 
 
 
89d1903
64b8acc
 
 
 
 
 
89d1903
64b8acc
 
 
 
 
9b375f6
89d1903
 
2b59478
89d1903
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82dafb3
71a610e
89d1903
50e917b
89d1903
 
 
 
50e917b
71a610e
 
 
 
 
 
50e917b
f1411bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89d1903
71a610e
 
 
89d1903
501ed65
 
3d6b4ba
 
 
 
501ed65
 
 
 
 
 
3d6b4ba
501ed65
 
 
50e917b
71a610e
 
89d1903
50e917b
71a610e
89d1903
71a610e
 
89d1903
 
3d6b4ba
 
 
 
 
89d1903
 
3d6b4ba
89d1903
 
50e917b
71a610e
 
 
 
 
9b375f6
 
1b0827e
82dafb3
71a610e
 
204a4dc
 
d5c9020
 
 
ba3eee4
2b59478
 
71a610e
82dafb3
89d1903
71a610e
 
03eafbf
82dafb3
89d1903
71a610e
 
 
 
 
 
 
 
7a20c38
 
 
 
 
 
 
 
 
 
71a610e
 
 
 
3d6b4ba
 
 
 
 
 
 
7a20c38
 
 
 
 
 
 
 
3d6b4ba
f1411bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71a610e
50e917b
71a610e
03eafbf
 
 
82dafb3
89d1903
 
71a610e
 
 
 
 
50e917b
 
64b8acc
 
 
 
 
 
 
 
 
03eafbf
 
 
71a610e
 
 
 
50e917b
71a610e
 
50e917b
71a610e
64b8acc
 
 
71a610e
 
50e917b
71a610e
 
 
 
 
 
50e917b
 
64b8acc
 
 
 
 
 
 
 
 
 
 
89d1903
7a20c38
 
 
 
71a610e
 
7a20c38
 
71a610e
 
 
f1411bc
71a610e
 
50e917b
71a610e
 
 
 
ba3eee4
 
89d1903
71a610e
 
 
50e917b
71a610e
 
50e917b
71a610e
 
 
 
50e917b
71a610e
 
 
 
 
50e917b
71a610e
ba3eee4
 
50e917b
71a610e
50e917b
71a610e
 
50e917b
71a610e
 
 
 
50e917b
71a610e
 
 
 
 
 
 
 
 
50e917b
 
64b8acc
 
 
 
 
 
 
7a20c38
64b8acc
89d1903
 
64b8acc
89d1903
 
64b8acc
 
 
 
 
 
 
71a610e
 
 
 
 
 
64b8acc
 
 
 
 
 
 
71a610e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64b8acc
 
 
 
 
 
 
 
 
 
 
 
71a610e
 
 
 
 
 
 
 
 
 
89d1903
 
 
64b8acc
71a610e
 
 
64b8acc
71a610e
 
64b8acc
 
 
 
 
 
 
71a610e
 
50e917b
 
71a610e
50e917b
 
71a610e
 
50e917b
 
89d1903
 
 
 
 
 
 
1985b85
 
 
 
 
 
 
 
89d1903
50e917b
 
 
1985b85
50e917b
 
 
 
89d1903
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
const mineflayer = require("mineflayer");
const mcData = require('minecraft-data')('1.21.11');
const ChatMessage = require('prismarine-chat')(mcData);
const Movements = require("mineflayer-pathfinder").Movements;
const pathfinder = require("mineflayer-pathfinder").pathfinder;
const util = require("util");
const ConvertAnsi = require("ansi-to-html");
const { SocksClient } = require("socks");

const convert = new ConvertAnsi({ newline: true, escapeXML: true });

const config = {
  "bot-accounts": [
    {
      "usernames": "OrangeLion",
      "type": "offline"
    }
  ],
  "server": {
    "ip": "play.reborncraft.pw",
    "port": 25565,
    "version": "1.21.11"
  },
  "utils": {
    "coopNick": "NexoHD",
    "auto-auth": {
      "enabled": true,
      "password": "Qweasd123"
    },
    "auto-reconnect": true,
    "auto-reconnect-delay": 5000
  }
};

const loggers = require("./logging.js");
const logger = loggers.logger;
const https = require('https');

// --- BOTLARIN DURUMUNU VE CHAT GEÇMİŞİNİ TUTACAK MERKEZİ DEPO ---
const botStatuses = {};
// Botların birbirine referans verebilmesi için (main <-> helper haberleşmesi)
const botInstances = {};

// Dış IP kontrolü
https.get('https://api.ipify.org?format=json', (res) => {
  let data = '';
  res.on('data', chunk => { data += chunk; });
  res.on('end', () => {
    try {
      const ipInfo = JSON.parse(data);
      console.log(`Dış IP adresin: ${ipInfo.ip}`);
    } catch (err) {
      console.error('JSON parse hatası:', err);
    }
  });
}).on('error', (err) => {
  console.error('İstek hatası:', err);
});

// --- REGEX TANIMLARI (sunucunun gerçek chat mesajlarına göre) ---
// "... adlı oyuncunun adasına davet edildiniz. Kabul etmek için 5 dakika var. KABUL ET | REDDET"
const INVITE_REGEX = /adasına davet edildiniz/i;
// "Teali1 Adanıza katıldı!"
const JOINED_ISLAND_REGEX = /Adanıza katıldı/i;
// "Bilgi: Artık bir ada sahibisiniz." (yeni oluşturma) veya
// "Bilgi: Adanıza ışınlandınız." (ada zaten mevcutsa) -> ikisi de gerçek
// ışınlanma kanıtı, ikisinden biri gelirse yeterli. SADECE "main" rolü için
// kullanılır - main'in kendi adasını oluşturması normaldir.
const ISLAND_CONFIRMED_REGEX = /Artık bir ada sahibisiniz|Adanıza ışınlandınız/i;

// HELPER için: SADECE var olan bir adaya (partnerin adasına) ışınlanma
// mesajı geçerli onaydır. "Artık bir ada sahibisiniz" helper'da ASLA kabul
// edilmemeli çünkü bu, helper'ın kendi yeni adasını oluşturduğu, yani
// partnerin adasına HENÜZ katılmadığı anlamına gelir.
const ISLAND_TELEPORTED_REGEX = /Adanıza ışınlandınız/i;
const ISLAND_CREATED_REGEX = /Artık bir ada sahibisiniz/i;

// "Bu oyuncu çevrim içi değil!" -> invite başarısız oldu demek, tekrar denenmeli
const OFFLINE_ERROR_REGEX = /çevrim içi değil/i;

function coopGrantedRegex(nick) {
  return new RegExp(`${nick} ile işbirliği yaptınız`, "i");
}
function coopRevokedRegex(nick) {
  return new RegExp(`${nick} adlı oyuncunun kooperatif`, "i");
}
function whitelistAddedRegex(nameToAdd) {
  return new RegExp(`Beyaz listeye ${nameToAdd} adlı oyuncuyu ekledin`, "i");
}
const ISLAND_LOCKED_REGEX = /ziyaretçilere kapatıldı/i;
const WHITELIST_ON_REGEX = /Beyaz liste aktif/i;

function createBot(username, authType, role, partnerName) {
  // Depoda bu bota ait alanı ilklendir (SADECE ilk seferde - reconnect'lerde
  // sıfırlanmaz, böylece "islandSetupComplete" gibi kalıcı bayraklar korunur)
  if (!botStatuses[username]) {
    botStatuses[username] = {
      status: "Bağlanıyor...",
      logs: [],
      skyblockDone: false,
      invited: false,
      stageComplete: false,        // helper: /isaccept -> spawn -> is go akışı bitti mi
      islandSetupComplete: false,  // main: TÜM kurulum adımları (coop/whitelist/lock/chest) bitti mi
      reconnectCount: 0,
      // ÖNEMLİ: /is go adımları burada YOK ve OLMAMALI. Her reconnect'te bot
      // /skyblock ile hub'a düşüyor, adada değil - "daha önce onaylandı" diye
      // /is go'yu atlamak, bot adada değilken sonraki komutları (setspawn,
      // coop, whitelist, lock) çalıştırmaya kalkışmasına yol açar. Bu yüzden
      // /is go her session'da koşulsuz tekrar denenir - ensureIslandGo()
      // zaten kendi içinde onay bekliyor, ekstra bayrağa gerek yok.
      //
      // Burada SADECE ada seviyesinde kalıcı olan, sunucu tarafında bir kez
      // yapılınca kalan ayarlar tutuluyor.
      steps: {
        partnerJoined: false,        // partner adaya gerçekten katıldı mı (bir kere olur, kalıcı)
        coopDone: false,
        whitelistCoopDone: false,
        whitelistPartnerDone: false,
        lockDone: false,
        whitelistOnDone: false,
        chestDone: false
      }
    };
  }

  const bot = mineflayer.createBot({
    username: username,
    auth: authType,
    host: config.server.ip,
    port: config.server.port,
    version: config.server.version,
    // 1.21+ sunucularda bağlantı/config aşamasında fizik paketleri gönderilmesi
    // bazı sunucularda "internal error" tipi anlık kicklere yol açabiliyor.
    // Fiziği spawn olana kadar kapalı tutup spawn'da açıyoruz.
    physicsEnabled: false,
    // Paket hacmini azaltıp bazı olaylara maruz kalma ihtimalini biraz
    // düşürebilir (garanti bir çözüm değil, sadece maruziyeti azaltır).
    viewDistance: "tiny"
  });

  botInstances[username] = bot;
  bot.loadPlugin(pathfinder);

  // --- 1.21.11 BİLİNEN SORUN: mineflayer'ın yüksek seviye resourcePack/
  // acceptResourcePack() mekanizması bu sürümde sunucudan gelen kaynak paketi
  // isteğini doğru işleyemiyor ve bot kick yiyor. Düşük seviye protokol paketini
  // doğrudan yazarak bu sorunu atlatıyoruz (topluluk tarafından doğrulanmış çözüm).
  bot._client.on('add_resource_pack', (data) => {
    try {
      bot._client.write('resource_pack_receive', { uuid: data.uuid, result: 3 });
      bot._client.write('resource_pack_receive', { uuid: data.uuid, result: 0 });
      logger.info(`[${username}] Kaynak paketi isteği düşük seviye paketle yanıtlandı.`);
    } catch (err) {
      logger.warn(`[${username}] Kaynak paketi yanıtlanamadı: ${err.message}`);
    }
  });
  bot.on('resourcePack', () => {
    try { bot.acceptResourcePack(); } catch {}
  });

  // Bot bazında 'error' event'i mineflayer'ın process'e throw etmesini engeller
  bot.on('error', (err) => {
    logger.error(`[${username}] Hata: ${err && err.message ? err.message : err}`);
  });

  // ============================================================
  // SESSION ABORT MEKANİZMASI
  // Her createBot çağrısı (= her yeniden bağlantı) kendi izole session'ına
  // sahip. Bağlantı koptuğunda abortSession() çağrılır ve TÜM bekleyen
  // wait/waitForChat/waitForWindow çağrıları anında sonlanır; akışlar bir
  // sonraki "if (sessionAborted) return" noktasında kendini durdurur.
  // Böylece kick yiyen eski bağlantı hiçbir zaman ölü soket üzerinden komut
  // göndermeye çalışmaz ve kalıcı "steps" bayrakları bozulmaz.
  // ============================================================
  let sessionAborted = false;
  let pendingAborts = [];

  // Helper'ın invite mesajına tekrar tetiklenmesini SADECE bu session içinde
  // engeller. botStatuses[username].invited KALICI olduğu için onu guard
  // olarak kullanmıyoruz - aksi halde helper kick yiyip reconnect olunca ve
  // main tekrar davet gönderdiğinde (bkz. steps.partnerJoined döngüsü) yeni
  // mesaj gelse bile hiç tetiklenmez, sonsuza kadar beklerdi.
  let helperTriggeredThisSession = false;

  function abortSession() {
    if (sessionAborted) return;
    sessionAborted = true;
    const toResolve = pendingAborts;
    pendingAborts = [];
    toResolve.forEach((fn) => { try { fn(); } catch {} });
  }

  function wait(ms) {
    return new Promise((resolve) => {
      if (sessionAborted) return resolve();
      let settled = false;
      const t = setTimeout(() => {
        if (settled) return;
        settled = true;
        pendingAborts = pendingAborts.filter((f) => f !== onAbort);
        resolve();
      }, ms);
      const onAbort = () => {
        if (settled) return;
        settled = true;
        clearTimeout(t);
        resolve();
      };
      pendingAborts.push(onAbort);
    });
  }

  function safeChat(message) {
    if (sessionAborted) {
      logger.warn(`[${username}] Session aborted, komut gönderilmedi: "${message}"`);
      return false;
    }
    try {
      bot.chat(message);
      return true;
    } catch (err) {
      logger.warn(`[${username}] "${message}" gönderilemedi (bağlantı kopmuş olabilir): ${err.message}`);
      return false;
    }
  }

  let activeTimeouts = [];
  function setSafeTimeout(fn, delay) {
    if (sessionAborted) return null;
    const t = setTimeout(() => { if (!sessionAborted) fn(); }, delay);
    activeTimeouts.push(t);
    return t;
  }

  async function antiBotMove() {
    const moves = ['forward', 'left', 'right', 'jump'];
    const moveCount = Math.floor(Math.random() * 4) + 2; // 2-5 hareket

    for (let i = 0; i < moveCount; i++) {
      if (sessionAborted) return;
      const move = moves[Math.floor(Math.random() * moves.length)];
      const duration = 150 + Math.floor(Math.random() * 500);

      bot.setControlState(move, true);
      await wait(duration);
      bot.setControlState(move, false);

      await wait(100 + Math.random() * 300);
    }

    if (sessionAborted) return;
    try {
      await bot.look(
        bot.entity.yaw + ((Math.random() - 0.5) * 1.5),
        bot.entity.pitch + ((Math.random() - 0.5) * 0.3),
        true
      );
    } catch {}
  }

  function waitForWindow() {
    return new Promise((resolve) => {
      if (sessionAborted) return resolve(null);
      const handler = (w) => {
        pendingAborts = pendingAborts.filter((f) => f !== onAbort);
        resolve(w);
      };
      const onAbort = () => {
        bot.removeListener('windowOpen', handler);
        resolve(null);
      };
      bot.once('windowOpen', handler);
      pendingAborts.push(onAbort);
    });
  }

  // Belirli bir regex'e uyan chat mesajı gelene kadar bekler (timeout ve
  // session-abort ile güvenceli).
  function waitForChat(regex, timeoutMs = 30000) {
    return new Promise((resolve) => {
      if (sessionAborted) return resolve(null);
      let done = false;

      const finish = (result) => {
        if (done) return;
        done = true;
        bot.removeListener('message', handler);
        clearTimeout(timer);
        pendingAborts = pendingAborts.filter((f) => f !== onAbort);
        resolve(result);
      };

      const handler = (jsonMsg) => {
        const text = new ChatMessage(jsonMsg).toString();
        if (regex.test(text)) finish(text);
      };

      const onAbort = () => finish(null);
      const timer = setTimeout(() => finish(null), timeoutMs);

      bot.on('message', handler);
      pendingAborts.push(onAbort);
    });
  }

  // Bir komutu gönderir, belirtilen onay mesajını bekler. Onay gelmezse
  // komutu tekrar gönderir (maxRetries kadar).
  async function sendUntilConfirmed(command, confirmRegex, { timeoutMs = 8000, maxRetries = 5, retryDelay = 2000 } = {}) {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      if (sessionAborted) return false;

      safeChat(command);
      logger.info(`[${username}] Komut gönderildi (deneme ${attempt}/${maxRetries}): ${command}`);

      const confirmed = await waitForChat(confirmRegex, timeoutMs);
      if (sessionAborted) return false;

      if (confirmed) {
        logger.info(`[${username}] Onaylandı: "${command}" -> "${confirmed}"`);
        return true;
      }

      logger.warn(`[${username}] Onay gelmedi: "${command}", ${retryDelay}ms sonra tekrar denenecek.`);
      await wait(retryDelay);
    }

    logger.error(`[${username}] ${maxRetries} denemede onaylanamadı: "${command}"`);
    return false;
  }

  // "/is go" yaz, onay gelene kadar tekrar dener. Rol bazlı davranış:
  //
  // - main: "Artık bir ada sahibisiniz" (yeni ada) ya da "Adanıza
  //   ışınlandınız" (var olan ada) - ikisi de geçerli onaydır.
  //
  // - helper: SADECE "Adanıza ışınlandınız" geçerli onaydır. Eğer sunucudan
  //   "Artık bir ada sahibisiniz" gelirse, bu KRİTİK bir hata durumudur:
  //   helper partnerin adasına henüz katılmamışken kendi adasını açmış
  //   demektir. Bu durumda /is go TEKRARLANMAZ, fonksiyon false döner ve
  //   çağıran taraf akışı durdurmalıdır - aksi halde helper'ın kendi adasında
  //   dolaşırken partnerin adasına ait komutlar (coop/whitelist/lock)
  //   yanlışlıkla tetiklenmiş olabilir.
  //
  // Onay gelmeden bu fonksiyondan false dönerse, adaya bağlı HİÇBİR komut
  // (setspawn, coop, whitelist, lock, chest vb.) çalıştırılmamalı - çağıran
  // taraf akışı durdurur.
  async function ensureIslandGo({ timeoutMs = 15000, maxRetries = 6, retryDelay = 3000 } = {}) {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      if (sessionAborted) return false;

      safeChat("/is go");
      logger.info(`[${username}] /is go gönderildi (deneme ${attempt}/${maxRetries}), onay bekleniyor...`);

      if (role === "helper") {
        // İki olası mesajı aynı anda dinle: doğru onay (teleport) ya da
        // yanlış/kritik durum (kendi adası oluşturuldu).
        const raced = await Promise.race([
          waitForChat(ISLAND_TELEPORTED_REGEX, timeoutMs).then((r) => ({ type: "ok", msg: r })),
          waitForChat(ISLAND_CREATED_REGEX, timeoutMs).then((r) => ({ type: "wrong", msg: r }))
        ]);
        if (sessionAborted) return false;

        if (raced.type === "ok" && raced.msg) {
          logger.info(`[${username}] Adaya ışınlanma onaylandı: "${raced.msg}"`);
          return true;
        }
        if (raced.type === "wrong" && raced.msg) {
          logger.error(`[${username}] KRİTİK: Helper kendi adasını oluşturdu! "${raced.msg}" - partnerin adasına katılım henüz gerçekleşmemiş. /is go tekrarlanmıyor, akış durduruluyor.`);
          return false;
        }
        // İkisi de gelmediyse (timeout) tekrar denenecek.
      } else {
        const confirmed = await waitForChat(ISLAND_CONFIRMED_REGEX, timeoutMs);
        if (sessionAborted) return false;

        if (confirmed) {
          logger.info(`[${username}] Adaya ışınlanma onaylandı: "${confirmed}"`);
          return true;
        }
      }

      logger.warn(`[${username}] /is go onayı gelmedi, ${retryDelay}ms sonra tekrar denenecek.`);
      await wait(retryDelay);
    }

    logger.error(`[${username}] ${maxRetries} denemede /is go onaylanamadı.`);
    return false;
  }

  // Main, helper'ın aşamalarının bitmesini bu fonksiyonla bekler
  async function waitForPartnerStage() {
    while (!sessionAborted && (!botStatuses[partnerName] || !botStatuses[partnerName].stageComplete)) {
      await wait(1000);
    }
  }

  // Main, davet göndermeden önce helper'ın skyblock'a geçtiğinden emin olur
  async function waitForPartnerSkyblock() {
    while (!sessionAborted && (!botStatuses[partnerName] || !botStatuses[partnerName].skyblockDone)) {
      await wait(1000);
    }
  }

  // "/is invite <isim>" gönderir. 5sn içinde "Bu oyuncu çevrim içi değil!" hatası
  // gelirse davet başarısız olmuştur; 30sn bekleyip tekrar dener. Hata gelmezse
  // (5sn içinde sessizlik) davetin gittiğini varsayıp fonksiyondan çıkar.
  async function inviteUntilOnline(targetName) {
    while (true) {
      if (sessionAborted) return;
      safeChat(`/is invite ${targetName}`);
      logger.info(`[${username}] /is invite ${targetName} gönderildi, kontrol ediliyor...`);

      const offlineError = await waitForChat(OFFLINE_ERROR_REGEX, 5000);
      if (sessionAborted) return;
      if (!offlineError) {
        logger.info(`[${username}] Davet başarılı görünüyor (çevrimdışı hatası görülmedi).`);
        return;
      }

      logger.warn(`[${username}] ${targetName} çevrim içi değil, 30sn sonra tekrar denenecek.`);
      await wait(30000);
    }
  }

  // --- ANA HESAP AKIŞI ---
  async function runMainSequence() {
    try {
      const steps = botStatuses[username].steps;

      // Skyblock'a ışınlandıktan hemen sonra /is go yazmamak için tampon bekleme
      await wait(7000);
      if (sessionAborted) return;

      // 1. /is go - HER SESSION'DA koşulsuz denenir (reconnect sonrası bot
      // hub'da oluyor, adada değil - bu yüzden "zaten onaylandı" diye
      // atlanamaz). Onaylanmadan hiçbir sonraki adım çalıştırılmaz.
      const firstGoOk = await ensureIslandGo();
      if (!firstGoOk) {
        logger.error(`[${username}] İlk /is go onaylanamadı, akış durduruluyor. Reconnect'te tekrar denenecek.`);
        return;
      }
      if (sessionAborted) return;

      await wait(2000);
      if (sessionAborted) return;

      safeChat("/is setspawn all");
      await wait(1000);
      if (sessionAborted) return;

      await antiBotMove();
      if (sessionAborted) return;

      await wait(5000);
      // Helper skyblock'a geçmeden davet gönderilemiyor, bu yüzden bekliyoruz
      await waitForPartnerSkyblock();
      if (sessionAborted) return;

      // Partner adaya GERÇEKTEN katılana kadar (JOINED_ISLAND_REGEX gelene
      // kadar) davet göndermeye devam ediyoruz. Bu, "helper daveti kaçırdı /
      // kick yedi / invite süresi doldu (5dk)" gibi durumlarda kilitlenmeyi
      // önlüyor - tek seferlik "davet gönderildi" bayrağı YOK, çünkü davet
      // gönderilmiş olması partnerin katıldığı anlamına gelmiyor.
      if (!steps.partnerJoined) {
        while (!sessionAborted && !steps.partnerJoined) {
          await inviteUntilOnline(partnerName);
          if (sessionAborted) return;

          const joinedMsg = await waitForChat(JOINED_ISLAND_REGEX, 90000);
          if (sessionAborted) return;

          if (joinedMsg) {
            steps.partnerJoined = true;
            logger.info(`[${username}] ${partnerName} adaya katıldı: "${joinedMsg}"`);
          } else {
            logger.warn(`[${username}] ${partnerName} 90sn içinde katılmadı, davet tekrar gönderilecek.`);
          }
        }
      } else {
        logger.info(`[${username}] ${partnerName} zaten katılmıştı, davet aşaması atlanıyor.`);
      }
      if (sessionAborted) return;

      safeChat("/spawn");

      await wait(5000);
      if (sessionAborted) return;

      // 2. /is go - HER SESSION'DA koşulsuz denenir
      const secondGoOk = await ensureIslandGo();
      if (!secondGoOk) {
        logger.error(`[${username}] İkinci /is go onaylanamadı, akış durduruluyor.`);
        return;
      }
      if (sessionAborted) return;

      await wait(5000);
      await antiBotMove();
      if (sessionAborted) return;

      // 3. /is go - coop öncesi son garanti, HER SESSION'DA koşulsuz denenir
      await wait(3000);
      const thirdGoOk = await ensureIslandGo();
      if (!thirdGoOk) {
        logger.error(`[${username}] Üçüncü /is go onaylanamadı, coop aşaması atlanıyor.`);
        return;
      }
      if (sessionAborted) return;

      await wait(1500);
      // Helper'ın kendi aşamalarını (isaccept, spawn, is go) bitirmesini bekliyoruz
      await waitForPartnerStage();
      if (sessionAborted) return;

      const coopNick = config.utils.coopNick;

      if (!steps.coopDone) {
        steps.coopDone = await sendUntilConfirmed(`/is coop ${coopNick}`, coopGrantedRegex(coopNick));
      } else {
        logger.info(`[${username}] Coop zaten onaylanmıştı, atlanıyor: ${coopNick}`);
      }
      if (sessionAborted) return;

      if (!steps.whitelistCoopDone) {
        steps.whitelistCoopDone = await sendUntilConfirmed(`/is whitelist add ${coopNick}`, whitelistAddedRegex(coopNick));
      } else {
        logger.info(`[${username}] Whitelist (${coopNick}) zaten onaylanmıştı, atlanıyor.`);
      }
      if (sessionAborted) return;

      if (!steps.whitelistPartnerDone) {
        steps.whitelistPartnerDone = await sendUntilConfirmed(`/is whitelist add ${partnerName}`, whitelistAddedRegex(partnerName));
      } else {
        logger.info(`[${username}] Whitelist (${partnerName}) zaten onaylanmıştı, atlanıyor.`);
      }
      if (sessionAborted) return;

      if (!steps.lockDone) {
        steps.lockDone = await sendUntilConfirmed("/is lock", ISLAND_LOCKED_REGEX);
      } else {
        logger.info(`[${username}] Ada kilitleme zaten onaylanmıştı, atlanıyor.`);
      }
      if (sessionAborted) return;

      if (!steps.whitelistOnDone) {
        steps.whitelistOnDone = await sendUntilConfirmed("/is whitelist on", WHITELIST_ON_REGEX);
      } else {
        logger.info(`[${username}] Whitelist aktifleştirme zaten onaylanmıştı, atlanıyor.`);
      }
      if (sessionAborted) return;

      if (!steps.coopDone || !steps.whitelistCoopDone || !steps.whitelistPartnerDone || !steps.lockDone || !steps.whitelistOnDone) {
        logger.error(`[${username}] Bazı ada kurulum adımları onaylanamadı, chest aşamasına geçilmiyor. Bir sonraki bağlantıda kaldığı yerden devam edilecek.`);
        return;
      }

      if (!steps.chestDone) {
        safeChat("/is settings");

        // 1. chest (ayarlar chesti)
        const window1 = await waitForWindow();
        if (sessionAborted || !window1) return;
        await wait(1500);
        bot.clickWindow(6, 0, 0); // mineflayer'a göre 6. slot (bize göre 7.)

        // Yeni chest açılıyor
        const window2 = await waitForWindow();
        if (sessionAborted || !window2) return;

        const slotSequence = [1, 0, 4, 1, 0, 3, 1, 0];
        for (const slot of slotSequence) {
          if (sessionAborted) return;
          await wait(1500);
          bot.clickWindow(slot, 0, 0);
        }

        await wait(1500);
        try { bot.closeWindow(window2); } catch {}

        steps.chestDone = true;
      } else {
        logger.info(`[${username}] Chest aşaması zaten tamamlanmıştı, atlanıyor.`);
      }

      botStatuses[username].islandSetupComplete = true;
      logger.info(`[${username}] Ana hesap ada akışı tamamlandı.`);
    } catch (err) {
      logger.error(`[${username}] Ana akış hatası: ${err.message}`);
    }
  }

  // Helper akışını güvenli biçimde başlatır. hem gerçek bir "davet edildiniz"
  // chat mesajı görüldüğünde, hem de reconnect sonrası "acaba bekleyen bir
  // davetim var mı?" kör denemesinde kullanılır.
  //
  // ÖNEMLİ (reconnect senaryosu): Sunucu, davet mesajını SADECE daveti
  // gönderdiği anda o anki bağlantıya yazar. Helper davet geldikten sonra
  // ama kabul etmeden bağlantısı koparsa, yeniden bağlandığında sunucu bu
  // mesajı TEKRAR YAZMAZ - davet server tarafında hâlâ (5 dk boyunca)
  // geçerli olsa bile chat'te görünmez. Bu yüzden salt "mesaj gelirse
  // tetikle" mantığı bu durumda sonsuza dek beklemeye yol açıyordu.
  // Bunu gidermek için reconnect sonrası bir kez kör /isaccept deneniyor;
  // bekleyen davet varsa kabul edilir, yoksa deneme başarısız olur ve
  // bayrak sıfırlanarak asıl mesaj dinleyicisi (yeni bir invite metni
  // geldiğinde) tekrar devreye girebilir hale getirilir.
  async function tryStartHelperSequence(reason) {
    if (sessionAborted || botStatuses[username].stageComplete || helperTriggeredThisSession) return;
    helperTriggeredThisSession = true;
    botStatuses[username].invited = true; // panelde göstermek için
    logger.info(`[${username}] Helper akışı deneniyor (${reason})...`);
    await runHelperSequence();
    if (!sessionAborted && !botStatuses[username].stageComplete) {
      // Deneme başarısız oldu (ör. bekleyen davet yoktu) - bayrağı sıfırla
      // ki gerçek bir invite mesajı geldiğinde ya da bir sonraki reconnect'te
      // tekrar denenebilsin.
      helperTriggeredThisSession = false;
      logger.info(`[${username}] Helper akışı bu denemede tamamlanamadı, bir sonraki tetikleyiciye (mesaj/reconnect) hazır bekleniyor.`);
    }
  }

  // --- YARDIMCI HESAP AKIŞI ---
  async function runHelperSequence() {
    try {
      await wait(3000);
      if (sessionAborted) return;

      // /isaccept gönderilir ve adaya GERÇEKTEN katıldığımıza dair onay
      // (JOINED_ISLAND_REGEX: "Adanıza katıldı") gelene kadar tekrar
      // denenir. Onay gelmeden /spawn veya /is go asla çalıştırılmaz -
      // aksi halde helper daveti henüz işlenmemişken adaya gitmeye
      // çalışıp kendi adasını oluşturabilir.
      const acceptOk = await sendUntilConfirmed("/isaccept", JOINED_ISLAND_REGEX, {
        timeoutMs: 10000,
        maxRetries: 5,
        retryDelay: 3000
      });
      if (!acceptOk) {
        logger.error(`[${username}] /isaccept onaylanamadı (adaya katılma teyidi gelmedi), akış durduruluyor. Reconnect'te tekrar denenecek.`);
        return;
      }
      if (sessionAborted) return;

      await wait(5000);
      await antiBotMove();
      if (sessionAborted) return;

      await wait(5000);
      safeChat("/spawn");

      await wait(5000);
      if (sessionAborted) return;

      // "/is go" onaylanana kadar tekrar dener - ensureIslandGo() artık
      // role === "helper" için SADECE "Adanıza ışınlandınız" mesajını kabul
      // ediyor; "Artık bir ada sahibisiniz" gelirse kritik hata sayıp false
      // döner, böylece helper yanlışlıkla kendi adasını açmış olsa bile akış
      // burada durur ve coop/whitelist/lock gibi sonraki adımlara geçilmez.
      const goOk = await ensureIslandGo();
      if (!goOk) {
        logger.error(`[${username}] Yardımcı hesap /is go onaylanamadı (ya da kendi adasını açtı), stageComplete işaretlenmiyor. Reconnect'te tekrar denenecek.`);
        return;
      }

      botStatuses[username].stageComplete = true;
      logger.info(`[${username}] Yardımcı hesap akışı tamamlandı.`);
    } catch (err) {
      logger.error(`[${username}] Yardımcı akış hatası: ${err.message}`);
    }
  }

  bot.once("spawn", async () => {
    bot.pathfinder.setMovements(new Movements(bot));
    logger.info(`[${username}] Sunucuya giriş yaptı.`);
    botStatuses[username].status = "Aktif (Oyunda)";

    // Config/login aşaması bitti, fiziği güvenle açabiliriz
    setSafeTimeout(() => {
      bot.physicsEnabled = true;
    }, 1000);

    if (config.utils && config.utils["auto-auth"] && config.utils["auto-auth"].enabled) {
      const password = config.utils["auto-auth"].password;

      setSafeTimeout(() => {
        safeChat(`/l ${password}`);
        logger.info(`[${username}] Login komutu kullanıldı.`);
      }, 5000);

      setSafeTimeout(() => {
        safeChat("/skyblock");
        botStatuses[username].skyblockDone = true;
        logger.info(`[${username}] Skyblock'a ışınlanma komutu kullanıldı.`);

        if (role === "main") {
          if (!botStatuses[username].islandSetupComplete) {
            // Kurulum ilk kez yapılıyor (ya da önceki denemede yarım kalmış) -> tam akışı çalıştır
            runMainSequence();
          } else {
            // Kurulum daha önce tamamlanmıştı, sadece adaya geri dönmemiz
            // yeterli - ama yine de onay bekleyerek deniyoruz (tek atımlık
            // bot.chat yerine), zira kick anında tek deneme kaybolabilir.
            setSafeTimeout(async () => {
              const ok = await ensureIslandGo();
              if (ok) {
                logger.info(`[${username}] Kurulum zaten tamamlanmıştı, adaya dönüldü.`);
              } else {
                logger.error(`[${username}] Kurulum tamamlanmıştı ama adaya dönüş onaylanamadı, bir sonraki reconnect'te tekrar denenecek.`);
              }
            }, 7000);
          }
        } else if (role === "helper") {
          if (botStatuses[username].stageComplete) {
            // Yardımcı hesap DAHA ÖNCE bu bota özgü olarak partnerin
            // adasına GERÇEKTEN katıldığını (JOINED_ISLAND_REGEX ile
            // onaylanmış /isaccept + ISLAND_TELEPORTED_REGEX ile onaylanmış
            // /is go) kanıtlamıştı. Bu durumda tekrar /is go ile adaya
            // dönmek güvenlidir çünkü helper zaten var olan bir adaya üye;
            // ensureIslandGo() yine de "kendi ada oluşturuldu" mesajı
            // gelirse bunu kritik hata sayıp false dönecektir.
            setSafeTimeout(async () => {
              const ok = await ensureIslandGo();
              if (ok) {
                logger.info(`[${username}] Yardımcı hesap zaten katılmıştı, adaya dönüldü.`);
              } else {
                logger.error(`[${username}] Yardımcı hesap adaya dönüş onaylanamadı, bir sonraki reconnect'te tekrar denenecek.`);
              }
            }, 7000);
          } else {
            // ÖNEMLİ: /is go burada ASLA denenmez (probu dahil) - çünkü
            // helper henüz /isaccept ile onaylanmış bir katılım kanıtlamadıysa
            // /is go, helper'ın KENDİ adasını oluşturmasına yol açar.
            //
            // AMA: reconnect sonrası, sunucunun daha önce gönderdiği bir
            // davet mesajını bu yeni bağlantıda ASLA tekrar görmeyeceğimizi
            // biliyoruz (sunucu invite mesajını sadece gönderildiği anki
            // bağlantıya yazar). Bu yüzden sadece pasif beklemek yeterli
            // değil - bekleyen bir davet olup olmadığını anlamak için bir
            // kez KÖR /isaccept denemesi yapılır. Bekleyen davet varsa kabul
            // edilir ve akış devam eder; yoksa deneme başarısız olur, bayrak
            // sıfırlanır ve normal mesaj dinleyicisi (yeni bir invite metni
            // için) aktif kalmaya devam eder.
            setSafeTimeout(() => {
              tryStartHelperSequence("reconnect sonrası bekleyen davet kontrolü");
            }, 4000);
          }
        }
      }, 15000);
    }
  });

  bot.on("end", () => {
    abortSession(); // ÖNCE bunu çağır - askıdaki tüm wait/waitForChat/waitForWindow anında serbest kalır

    logger.warn(`[${username}] Bağlantı koptu. Zamanlayıcılar temizleniyor...`);
    botStatuses[username].status = "Bağlantı Koptu";
    botStatuses[username].reconnectCount += 1;
    activeTimeouts.forEach((t) => clearTimeout(t));
    activeTimeouts = [];

    if (config.utils && config.utils["auto-reconnect"]) {
      const baseDelay = config.utils["auto-reconnect-delay"];
      // Rastgele +0-3000ms jitter: art arda kick/reconnect döngülerinde
      // tüm botların tam olarak aynı anda tekrar bağlanmaya çalışmasını
      // (ve bunun bir anti-bot sistemi tarafından şüpheli görülmesini) engeller.
      const jitter = Math.floor(Math.random() * 3000);
      const delay = baseDelay + jitter;

      logger.info(`[${username}] ${delay}ms sonra tekrar bağlanılacak (deneme #${botStatuses[username].reconnectCount}).`);
      setTimeout(() => createBot(username, authType, role, partnerName), delay);
    }
  });

  bot.on('message', (jsonMsg) => {
    const chatMessage = new ChatMessage(jsonMsg);
    const coloredAnsi = chatMessage.toAnsi();
    console.log(`[${username}] ${coloredAnsi}`);

    const htmlMessage = convert.toHtml(coloredAnsi);
    const timeStr = new Date().toLocaleTimeString('tr-TR');

    botStatuses[username].logs.push({ time: timeStr, text: htmlMessage });
    // Panelde kaydırarak daha eski mesajları da görebilmek için limiti yükselttik.
    const MAX_STORED_LOGS = 200;
    if (botStatuses[username].logs.length > MAX_STORED_LOGS) {
      botStatuses[username].logs.shift();
    }

    const plainText = chatMessage.toString();

    if (plainText.includes("Lütfen sohbeti kullanmadan önce hareket edin")) {
      logger.warn(`[${username}] AntiBot mesajı algılandı.`);
      antiBotMove();
      return;
    }

    // Coop yetkisi geri alınırsa (elle ya da başka sebeple), bunu logla ve
    // bayrağı sıfırla ki bir sonraki kurulum turunda coop tekrar verilsin.
    if (role === "main" && botStatuses[username].steps) {
      const coopNick = config.utils.coopNick;
      if (coopRevokedRegex(coopNick).test(plainText)) {
        logger.warn(`[${username}] Coop yetkisi geri alınmış görünüyor (${coopNick}). Bayrak sıfırlanıyor, tekrar verilecek.`);
        botStatuses[username].steps.coopDone = false;
        botStatuses[username].islandSetupComplete = false;
      }
    }

    // Yardımcı hesap: davet mesajını yakala.
    // Skyblock'a henüz geçmediyse tetiklenmez. stageComplete zaten "tamamen
    // bitti" kalıcı sinyali olduğu için asıl guard bu - session-local
    // helperTriggeredThisSession ise aynı session içinde birden fazla
    // tetiklenmeyi (aynı invite mesajını iki kez işlemeyi) önlüyor.
    if (
      role === "helper" &&
      !helperTriggeredThisSession &&
      !botStatuses[username].stageComplete &&
      botStatuses[username].skyblockDone &&
      INVITE_REGEX.test(plainText)
    ) {
      tryStartHelperSequence("chat'te davet mesajı görüldü");
    }
  });

  bot.on("kicked", (reason) => {
    botStatuses[username].status = "Kicked (Atıldı)";
    logger.warn(`[${username}] Sunucudan atıldı: ${util.inspect(reason)}`);
  });
}

// --- BOTLARI BAŞLAT (her hesap için main ve helper çifti oluşturuluyor) ---
const accounts = config["bot-accounts"] || [];
accounts.forEach((account, index) => {
  if (!account.usernames) return;

  const mainName = account.usernames;
  const helperName = `${mainName}1`;

  setTimeout(() => {
    logger.info(`${mainName} için bot işlemi başlatılıyor... (Mod: ${account.type})`);
    createBot(mainName, account.type, "main", helperName);
  }, index * 6000);

  setTimeout(() => {
    logger.info(`${helperName} için bot işlemi başlatılıyor... (Mod: ${account.type})`);
    createBot(helperName, account.type, "helper", mainName);
  }, index * 6000 + 3000);
});

// --- EXPRESS SUNUCUSU VE PANEL ROTASI ---
const express = require("express");
const app = express();

app.get("/", (req, res) => res.send("Botlar Aktif. Durumu görmek için <a href='/hesaplar'>/hesaplar</a> sayfasına gidin."));

app.get("/hesaplar", (req, res) => {
  let rowsHtml = "";

  for (const [username, data] of Object.entries(botStatuses)) {
    let statusBadgeColor = "#f39c12";
    if (data.status.includes("Aktif")) statusBadgeColor = "#2ecc71";
    if (data.status.includes("Koptu") || data.status.includes("Atıldı")) statusBadgeColor = "#e74c3c";

    let chatHtml = "";
    if (data.logs.length === 0) {
      chatHtml = "<div style='color: #7f8c8d; font-style: italic;'>Henüz mesaj yok...</div>";
    } else {
      data.logs.forEach(log => {
        chatHtml += `<div style="margin-bottom: 4px; font-family: 'Courier New', monospace;">
          <span style="color: #95a5a6; font-size: 12px;">[${log.time}]</span> ${log.text}
        </div>`;
      });
    }

    // Panel'de kullanılacak güvenli bir DOM id'si (kullanıcı adı özel karakter içerebilir)
    const safeId = username.replace(/[^a-zA-Z0-9_-]/g, "_");

    // Ada kurulum adımlarının durumu (sadece "steps" alanı olan/main hesaplarda görünür)
    let stepsHtml = "<span style='color: #7f8c8d;'>-</span>";
    if (data.steps) {
      const stepLabels = [
        ["Partner Katıldı", data.steps.partnerJoined],
        ["Coop", data.steps.coopDone],
        ["WL:Coop", data.steps.whitelistCoopDone],
        ["WL:Partner", data.steps.whitelistPartnerDone],
        ["Lock", data.steps.lockDone],
        ["WL Açık", data.steps.whitelistOnDone],
        ["Chest", data.steps.chestDone]
      ];
      stepsHtml = stepLabels.map(([label, done]) => {
        const color = done ? "#2ecc71" : "#555";
        return `<span style="background-color:${color}; color:#fff; padding:2px 6px; border-radius:3px; font-size:11px; margin-right:4px; display:inline-block; margin-bottom:3px;">${label}</span>`;
      }).join("");
    }

    rowsHtml += `
      <tr style="border-bottom: 1px solid #333;">
        <td style="padding: 15px; font-weight: bold; color: #fff;">${username}</td>
        <td style="padding: 15px;">
          <span style="background-color: ${statusBadgeColor}; color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold;">
            ${data.status}
          </span><br/>
          <span style="color:#7f8c8d; font-size: 11px;">Reconnect: ${data.reconnectCount || 0}</span>
        </td>
        <td style="padding: 15px;">${stepsHtml}</td>
        <td style="padding: 15px;">
          <div id="chatbox-${safeId}" class="chat-box">${chatHtml}</div>
          <div style="font-size: 11px; color: #7f8c8d; margin-top: 4px;">${data.logs.length} mesaj (yukarı kaydırarak geçmişi gör)</div>
        </td>
      </tr>
    `;
  }

  const fullHtml = `
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <title>Bot Kontrol Paneli</title>
      <style>
        body { background-color: #1e1e1e; color: #f1f1f1; font-family: Arial, sans-serif; margin: 30px; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; background-color: #252526; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.3); }
        th { background-color: #333337; color: #3498db; text-align: left; padding: 15px; font-size: 14px; text-transform: uppercase; }
        h2 { color: #3498db; margin-bottom: 5px; }
        .refresh-btn { background-color: #3498db; color: white; border: none; padding: 10px 15px; text-decoration: none; border-radius: 4px; font-weight: bold; cursor: pointer; display: inline-block; margin-top: 10px; }
        .refresh-btn:hover { background-color: #2980b9; }
        .chat-box {
          background: #111;
          border-radius: 4px;
          padding: 10px;
          max-height: 320px;
          overflow-y: auto;
          scrollbar-width: thin;
          scrollbar-color: #3498db #111;
        }
        .chat-box::-webkit-scrollbar { width: 8px; }
        .chat-box::-webkit-scrollbar-track { background: #111; }
        .chat-box::-webkit-scrollbar-thumb { background-color: #3498db; border-radius: 4px; }
      </style>
    </head>
    <body>
      <h2>Minecraft Bot Yönetim Paneli</h2>
      <p>Anlık durumu görmek için sayfayı yenileyebilir veya aşağıdaki butona basabilirsin.</p>
      <button class="refresh-btn" onclick="location.reload()">Sayfayı Yenile (F5)</button>

      <table>
        <thead>
          <tr>
            <th style="width: 12%;">Hesap Adı</th>
            <th style="width: 13%;">Durum</th>
            <th style="width: 25%;">Ada Kurulum Adımları</th>
            <th style="width: 50%;">Chat Geçmişi (kaydırılabilir)</th>
          </tr>
        </thead>
        <tbody>
          ${rowsHtml || '<tr><td colspan="4" style="padding: 20px; text-align: center; color: #7f8c8d;">Botlar henüz yükleniyor...</td></tr>'}
        </tbody>
      </table>
      <script>
        // Sayfa açıldığında her chat kutusunu en alta (en son mesaja) kaydır.
        // Kullanıcı isterse yukarı kaydırıp eski mesajları görebilir.
        document.querySelectorAll('.chat-box').forEach((box) => {
          box.scrollTop = box.scrollHeight;
        });
      </script>
    </body>
    </html>
  `;

  res.send(fullHtml);
});

app.listen(process.env.PORT || 7860, () => {
  logger.info("Express sunucusu 7860 portunda aktif.");
});

// ============================================================
// STDERR GÜRÜLTÜ FİLTRESİ
// 1.21.11'in bilinen protodef/paket-parse hataları (örn. world_particles)
// process seviyesinde stderr'e doğrudan stack trace basıyor, logger'ı
// bypass ediyor. Bilinen zararsız kalıpları burada yutuyoruz; başka bir
// hata görürsen tam metnini NOISY_ERROR_PATTERNS listesine ekle.
// ============================================================
const NOISY_ERROR_PATTERNS = [
  "Ignoring block entities as chunk failed to load",
  "PartialReadError",                 // protodef paket okuma hataları (örn. world_particles)
  "packet_world_particles",
  "Read error for undefined",
  "at CompiledProtodef.read",
  "at eval (eval at compile"          // protodef'in derlenmiş (eval'lenmiş) okuyucu fonksiyonlarının stack izleri
];

const originalStderrWrite = process.stderr.write;
process.stderr.write = function (chunk, encoding, callback) {
  const message = chunk.toString();
  if (NOISY_ERROR_PATTERNS.some((pattern) => message.includes(pattern))) {
    if (typeof callback === 'function') callback();
    return true;
  }
  return originalStderrWrite.apply(process.stderr, arguments);
};

// process seviyesinde yakalanmamış hata/red olursa da process çökmesin,
// sadece logla (mineflayer/protodef bazen bunları process'e kadar sızdırabiliyor)
process.on('uncaughtException', (err) => {
  logger.error(`[uncaughtException] ${err && err.stack ? err.stack : err}`);
});
process.on('unhandledRejection', (reason) => {
  logger.error(`[unhandledRejection] ${reason && reason.stack ? reason.stack : reason}`);
});