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 " 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 /hesaplar 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 = "
Henüz mesaj yok...
"; } else { data.logs.forEach(log => { chatHtml += `
[${log.time}] ${log.text}
`; }); } // 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 = "-"; 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 `${label}`; }).join(""); } rowsHtml += ` ${username} ${data.status}
Reconnect: ${data.reconnectCount || 0} ${stepsHtml}
${chatHtml}
${data.logs.length} mesaj (yukarı kaydırarak geçmişi gör)
`; } const fullHtml = ` Bot Kontrol Paneli

Minecraft Bot Yönetim Paneli

Anlık durumu görmek için sayfayı yenileyebilir veya aşağıdaki butona basabilirsin.

${rowsHtml || ''}
Hesap Adı Durum Ada Kurulum Adımları Chat Geçmişi (kaydırılabilir)
Botlar henüz yükleniyor...
`; 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}`); });