ro3 / bot.js
Suvarii's picture
Update bot.js
f1411bc verified
Raw
History Blame Contribute Delete
41.3 kB
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}`);
});