Spaces:
Paused
Paused
| 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}`); | |
| }); | |