Download bot.js from Suvarii/Hurtdown: direct link, hf CLI and curl.
- Browser
- Download file 77.3 kB
-
https://huggingface.co/spaces/Suvarii/Hurtdown/resolve/0a05687d94deafc2da69bbf4bfcbc661fd02ca4c/bot.js
- Command line
-
hf download hf://spaces/Suvarii/Hurtdown@0a05687d94deafc2da69bbf4bfcbc661fd02ca4c/bot.js
-
curl -L -o bot.js https://huggingface.co/spaces/Suvarii/Hurtdown/resolve/0a05687d94deafc2da69bbf4bfcbc661fd02ca4c/bot.js
77.3 kB
| const mineflayer = require("mineflayer"); | |
| const mineflayerViewer = require('prismarine-viewer').mineflayer; | |
| const util = require("util"); | |
| const { SocksClient } = require('socks'); | |
| const { Client } = require('ssh2'); | |
| const dns = require('dns').promises; | |
| const config = require("./settings.json"); | |
| const loggers = require("./logging.js"); | |
| const logger = loggers.logger; | |
| const getRecentLogs = loggers.getRecentLogs; | |
| const https = require('https'); | |
| // --- KÜRESEL GÜVENLİK AĞI --- | |
| process.on('unhandledRejection', (reason) => { | |
| logger.error(`[Küresel Güvenlik Ağı] Yakalanmamış Promise hatası (unhandledRejection): ${util.inspect(reason)}`); | |
| }); | |
| process.on('uncaughtException', (err) => { | |
| logger.error(`[Küresel Güvenlik Ağı] Yakalanmamış istisna (uncaughtException): ${err && err.stack ? err.stack : err}`); | |
| }); | |
| // --- AYARLAR --- | |
| const HOME_ARRIVAL_DELAY_MS = 4000; | |
| const WINDOW_OPEN_TIMEOUT_MS = 6000; | |
| const DEFAULT_CHECK_INTERVAL_MS = 10 * 60 * 1000; // Varsayılan 10 dk | |
| const FULL_FEED_COOLDOWN_MS = 50 * 60 * 1000; // Besleme sonrası sabit 50 dk | |
| const GO_ISLAND_COMMAND = "/is go"; | |
| const BESLEYICI_NAMES_RAW = ["Besleyici", "ʙᴇsʟᴇʏiᴄi"]; | |
| const BESLEYICI_NAMES = Array.isArray(BESLEYICI_NAMES_RAW) ? BESLEYICI_NAMES_RAW : [BESLEYICI_NAMES_RAW]; | |
| const KAZICI_NAMES_RAW = (config.utils && (config.utils["kazici-names"] || config.utils["kazici-name"])) || ["Kazıcı", "ᴋᴀᴢıᴄı", "ᴋᴀᴢɪᴄɪ"]; | |
| const KAZICI_NAMES = Array.isArray(KAZICI_NAMES_RAW) ? KAZICI_NAMES_RAW : [KAZICI_NAMES_RAW]; | |
| const KAZICI_STORAGE_TRANSFER_SLOT = 17; | |
| const KAZICI_BETWEEN_DELAY_MS = 5 * 1000; | |
| const KAZICI_ACTION_SETTLE_DELAY_MS = 1200; | |
| // --- ÇİFTÇİ (HEPSİNİ SAT) MENÜSÜ İÇİN AYARLAR --- | |
| const CIFTCI_NAMES_RAW = (config.utils && (config.utils["ciftci-names"] || config.utils["ciftci-name"])) || ["Çiftçi", "çiꜰᴛçi"]; | |
| const CIFTCI_NAMES = Array.isArray(CIFTCI_NAMES_RAW) ? CIFTCI_NAMES_RAW : [CIFTCI_NAMES_RAW]; | |
| const CIFTCI_SELL_ALL_SLOT = 45; | |
| const CIFTCI_COMMAND = "/çiftçi menü"; | |
| const CIFTCI_WINDOW_TIMEOUT_MS = 6000; | |
| const CIFTCI_LORE_FALLBACK_KEYWORDS = ["hepsini sat"]; | |
| // Menü açıldıktan sonra sunucunun içeriği (lore/slot) tam yerleştirmesi için | |
| // beklenecek süre, tıklamadan önce. Ardından art arda birkaç kez tıklanır. | |
| const CIFTCI_CLICK_DELAY_AFTER_OPEN_MS = 1000; | |
| const CIFTCI_CLICK_REPEAT_COUNT = 3; | |
| const CIFTCI_CLICK_REPEAT_GAP_MS = 350; | |
| // --- KAZICI PERİYODİK DÖNGÜ AYARLARI (bağımsız, 5 dakikada bir) --- | |
| const KAZICI_CYCLE_INTERVAL_MS = 5 * 60 * 1000; // 5 dakika | |
| // --- ÇİFTÇİ PERİYODİK DÖNGÜ AYARLARI (bağımsız, 1 dakikada bir) --- | |
| const CIFTCI_CYCLE_INTERVAL_MS = 60 * 1000; // 1 dakika | |
| let viewerInstance = null; | |
| let viewerTimeout = null; | |
| let bot = null; | |
| let botIsReady = false; | |
| let baglantiTuru = "Yok"; | |
| let activeMessages = []; | |
| let messagesTimeout = null; | |
| let isListeningChat = false; | |
| const ansiToHtmlConvert = new (require('ansi-to-html'))({ newline: true }); | |
| // --- GÜVENLİ BAĞLANTI DEĞİŞKENLERİ --- | |
| let isConnecting = false; | |
| let isDestroying = false; | |
| let reconnectTimeout = null; | |
| let currentSshClient = null; | |
| let currentStream = null; | |
| let currentSocket = null; | |
| let windowOpenResolvers = []; | |
| // --- PANEL METRİKLERİ --- | |
| let botUptimeStart = null; | |
| let lastFullFeedTimeStr = "Henüz besleme yapılmadı"; | |
| let lastCheckTimeStr = "Henüz kontrol yapılmadı"; | |
| let nextScheduledCheckTimeStr = "Botun spawn olması bekleniyor..."; | |
| // --- KAZICI PANEL METRİKLERİ (bağımsız) --- | |
| let lastKaziciCheckTimeStr = "Henüz kazıcı kontrolü yapılmadı"; | |
| let nextKaziciTimeStr = "Botun spawn olması bekleniyor..."; | |
| let nextKaziciAtMs = null; | |
| // --- ÇİFTÇİ PANEL METRİKLERİ (bağımsız) --- | |
| let lastCiftciSellTimeStr = "Henüz çiftçi satışı yapılmadı"; | |
| let nextCiftciTimeStr = "Botun spawn olması bekleniyor..."; | |
| let nextCiftciAtMs = null; | |
| // --- ÇİFTÇİ/KAZICI PERİYODİK DÖNGÜ KONTROLÜ --- | |
| let lastCheckAtMs = null; // Besleyici için (panel bilgisi amaçlı, artık kazıcı/çiftçiyi etkilemiyor) | |
| let nextScheduledCheckAtMs = null; // Besleyici için | |
| let kaziciIntervalHandle = null; // Kazıcı bağımsız döngü zamanlayıcısı (5 dk) | |
| let ciftciIntervalHandle = null; // Çiftçi bağımsız döngü zamanlayıcısı (1 dk) | |
| let isMainCycleRunning = false; // Besleyici (goToIsland->besle) ana döngüsü sürüyor mu | |
| // --- DİNAMİK IP VE KONUM BİLGİLERİ --- | |
| let activeConnectionIp = "Bilinmiyor"; | |
| let activeConnectionLocation = "Bilinmiyor"; | |
| let localExternalIp = "Bilinmiyor"; | |
| // --- DİNAMİK ZAMANLAYICI KONTROLÜ --- | |
| let periodicTimeoutHandle = null; | |
| let activeTimeouts = []; | |
| function setSafeTimeout(fn, delay) { | |
| const t = setTimeout(fn, delay); | |
| activeTimeouts.push(t); | |
| return t; | |
| } | |
| // --- ÖNCELİKLİ GUI KİLİDİ (MUTEX) --- | |
| // Besleyici, kazıcı ve çiftçi menüleri hepsi aynı windowOpen/tıklama mekanizmasını | |
| // paylaştığı için ASLA aynı anda çalışmamalılar. Bu kilit ikisi arasında ayrım yapar: | |
| // - tryAcquire(): periyodik/otomatik döngüler için. Kilit doluysa BEKLEMEZ, hemen | |
| // false döner ve o tur sessizce atlanır (bir sonraki periyotta tekrar dener). | |
| // - acquire(): Express panelinden (manuel) gelen istekler için. Kilit doluysa | |
| // KUYRUĞA GİRER ve önceliklidir — mevcut işlem bitince, kuyrukta bekleyen varsa | |
| // bile (ki otomatik döngüler hiç kuyruğa girmediği için pratikte tek bekleyen | |
| // genelde manuel istek olur) ilk manuel istek işi devralır. Böylece kullanıcı | |
| // panelden bir istek yaptığında, o sırada süren otomatik bir işlem varsa önce | |
| // o tamamlanır, hemen ardından kullanıcının isteği en öncelikli şekilde işlenir. | |
| class PriorityGuiMutex { | |
| constructor() { | |
| this._locked = false; | |
| this._waiters = []; // manuel (öncelikli) bekleyenler, FIFO | |
| } | |
| isLocked() { return this._locked; } | |
| tryAcquire() { | |
| if (this._locked) return false; | |
| this._locked = true; | |
| return true; | |
| } | |
| acquire() { | |
| if (!this._locked) { | |
| this._locked = true; | |
| return Promise.resolve(); | |
| } | |
| return new Promise((resolve) => { | |
| this._waiters.push(resolve); | |
| }); | |
| } | |
| release() { | |
| if (this._waiters.length > 0) { | |
| const next = this._waiters.shift(); | |
| // Kilit devrediliyor (locked=true kalır), bir sonraki bekleyen devralır. | |
| next(); | |
| } else { | |
| this._locked = false; | |
| } | |
| } | |
| } | |
| const guiMutex = new PriorityGuiMutex(); | |
| // Express panelinden diğer bot fonksiyonlarına erişim için (createBot() closure'ı | |
| // dışından çağrılabilmeleri için köprü). Bot her (yeniden) bağlandığında doldurulur. | |
| let botActions = { | |
| scanAndFeedManual: null, | |
| scanAndCollectMinersManual: null, | |
| openCiftciAndSellAllManual: null, | |
| goToIslandManual: null, | |
| sendSkyblockManual: null, | |
| }; | |
| // --- ORTAK SÜRE FORMATLAMA FONKSİYONU --- | |
| // Tüm loglardaki "X dk sonra", "Y saat sonra" gibi elle yazılmış ifadelerin yerine | |
| // bu fonksiyon kullanılır. Böylece yukarıdaki ayar sabitleri (KAZICI_CYCLE_INTERVAL_MS, | |
| // CIFTCI_CYCLE_INTERVAL_MS, DEFAULT_CHECK_INTERVAL_MS, vb.) değiştirildiğinde, | |
| // loglara yansıyan süre ifadeleri OTOMATİK olarak güncellenir; elle metin | |
| // değiştirmeye gerek kalmaz. | |
| function formatDurationMs(ms) { | |
| if (ms == null || isNaN(ms) || ms < 0) return "0 saniye"; | |
| let totalSeconds = Math.round(ms / 1000); | |
| const days = Math.floor(totalSeconds / 86400); | |
| totalSeconds -= days * 86400; | |
| const hours = Math.floor(totalSeconds / 3600); | |
| totalSeconds -= hours * 3600; | |
| const minutes = Math.floor(totalSeconds / 60); | |
| const seconds = totalSeconds - minutes * 60; | |
| const parts = []; | |
| if (days > 0) parts.push(`${days} gün`); | |
| if (hours > 0) parts.push(`${hours} saat`); | |
| if (minutes > 0) parts.push(`${minutes} dakika`); | |
| if (seconds > 0 || parts.length === 0) parts.push(`${seconds} saniye`); | |
| return parts.join(" "); | |
| } | |
| function stopPeriodicHomeChecks() { | |
| if (periodicTimeoutHandle) { | |
| clearTimeout(periodicTimeoutHandle); | |
| periodicTimeoutHandle = null; | |
| } | |
| activeTimeouts.forEach(t => clearTimeout(t)); | |
| activeTimeouts = []; | |
| if (kaziciIntervalHandle) { | |
| clearInterval(kaziciIntervalHandle); | |
| kaziciIntervalHandle = null; | |
| logger.info("[Kazıcı Periyodik] Döngü zamanlayıcısı durduruldu."); | |
| } | |
| if (ciftciIntervalHandle) { | |
| clearInterval(ciftciIntervalHandle); | |
| ciftciIntervalHandle = null; | |
| logger.info("[Çiftçi Periyodik] Döngü zamanlayıcısı durduruldu."); | |
| } | |
| nextScheduledCheckAtMs = null; | |
| nextKaziciAtMs = null; | |
| nextCiftciAtMs = null; | |
| nextKaziciTimeStr = "Zamanlama durduruldu."; | |
| nextCiftciTimeStr = "Zamanlama durduruldu."; | |
| } | |
| // --- MANUEL DURDURMA/BAŞLATMA KONTROLÜ --- | |
| let manualStopRequested = false; | |
| async function getIpLocation(ip) { | |
| if (!ip || ip === "Bilinmiyor" || ip.startsWith("127.")) return "Bilinmiyor"; | |
| const services = [ | |
| `https://ip-api.com/json/${ip}?fields=status,country,city`, | |
| `https://ipapi.co/${ip}/json/` | |
| ]; | |
| for (const url of services) { | |
| try { | |
| const location = await new Promise((resolve) => { | |
| https.get(url, (res) => { | |
| let data = ''; | |
| res.on('data', chunk => data += chunk); | |
| res.on('end', () => { | |
| try { | |
| const geo = JSON.parse(data); | |
| if (geo.country && geo.status !== "fail") { | |
| resolve(`${geo.country} / ${geo.city || geo.region || ""}`); | |
| } else { | |
| resolve(null); | |
| } | |
| } catch (e) { resolve(null); } | |
| }); | |
| }).on('error', () => resolve(null)); | |
| }); | |
| if (location) return location; | |
| } catch (e) { continue; } | |
| } | |
| return "Konum Alınamadı"; | |
| } | |
| 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); | |
| localExternalIp = ipInfo.ip; | |
| console.log(`Dış IP adresin: ${localExternalIp}`); | |
| } catch (err) {} | |
| }); | |
| }).on('error', () => {}); | |
| async function createBot() { | |
| if (isConnecting) { | |
| logger.warn("[Sistem] Zaten aktif bir bağlantı süreci yürütülüyor. Mükerrer istek engellendi."); | |
| return; | |
| } | |
| isConnecting = true; | |
| isDestroying = false; | |
| try { | |
| const net = require('net'); | |
| const sshClient = new Client(); | |
| currentSshClient = sshClient; | |
| const targetServerIp = config.server.ip; | |
| const targetServerPort = config.server.port; | |
| const currentBotUsername = config["bot-account"]["username"]; | |
| bot = mineflayer.createBot({ | |
| username: currentBotUsername, | |
| auth: config["bot-account"]["type"], | |
| host: targetServerIp, | |
| port: targetServerPort, | |
| version: config.server.version, | |
| connect: async (client) => { | |
| let fallbackTriggered = false; | |
| let isFinalFailureHandled = false; | |
| const handleFinalFailure = (reason) => { | |
| if (isFinalFailureHandled || isDestroying) return; | |
| isFinalFailureHandled = true; | |
| logger.error(`[Kritik Hata] ${reason}. Bağlantı sağlanamadı! Sistem en başa döndürülüyor...`); | |
| baglantiTuru = "Yok"; | |
| botIsReady = false; | |
| botUptimeStart = null; | |
| activeConnectionIp = "Bilinmiyor"; | |
| activeConnectionLocation = "Bilinmiyor"; | |
| stopPeriodicHomeChecks(); | |
| lastCheckAtMs = null; | |
| safeDestroyAllConnections(); | |
| if (!manualStopRequested && config.utils && config.utils["auto-reconnect"]) { | |
| triggerSafeReconnect(); | |
| } | |
| }; | |
| const connectDirectly = async (reason) => { | |
| if (fallbackTriggered || isDestroying) return; | |
| fallbackTriggered = true; | |
| logger.warn(`[Bağlantı Yedekleme] Doğrudan IP moduna geçiliyor (Sebep: ${reason}). Kendi (Yurt dışı) IP'miz üzerinden GERÇEK TCP bağlantısı kuruluyor...`); | |
| if (sshClient) { | |
| try { | |
| sshClient.removeAllListeners('ready'); | |
| sshClient.removeAllListeners('error'); | |
| sshClient.destroy(); | |
| } catch (e) {} | |
| } | |
| activeConnectionIp = localExternalIp !== "Bilinmiyor" ? localExternalIp : "Kendi Dış IP'niz"; | |
| activeConnectionLocation = await getIpLocation(localExternalIp); | |
| const socket = net.connect({ | |
| host: targetServerIp, | |
| port: parseInt(targetServerPort) || 25565 | |
| }); | |
| currentSocket = socket; | |
| let isDirectHandled = false; | |
| const directTimeout = setTimeout(() => { | |
| if (isDirectHandled) return; | |
| isDirectHandled = true; | |
| socket.destroy(); | |
| handleFinalFailure("Kendi IP bağlantısı 1 dakikalık süre içinde zaman aşımına uğradı (Sunucu yanıt vermedi)"); | |
| }, 60000); | |
| socket.on('connect', () => { | |
| if (isDirectHandled) return; | |
| isDirectHandled = true; | |
| clearTimeout(directTimeout); | |
| logger.info("[Bağlantı Yedekleme] Kendi IP'miz üzerinden GERÇEK TCP bağlantısı başarıyla kuruldu!"); | |
| baglantiTuru = "Normal"; | |
| if (client && typeof client.setSocket === 'function') { | |
| client.setSocket(socket); | |
| client.emit('connect'); | |
| } | |
| }); | |
| socket.on('error', (err) => { | |
| if (isDirectHandled) return; | |
| isDirectHandled = true; | |
| clearTimeout(directTimeout); | |
| logger.error(`[Bağlantı Yedekleme Hatası] Sunucu bağlantıyı reddetti veya olumsuz yanıt verdi: ${err.message}`); | |
| handleFinalFailure(`Kendi IP hatası: ${err.message}`); | |
| }); | |
| socket.on('close', () => { | |
| if (isDirectHandled) return; | |
| isDirectHandled = true; | |
| clearTimeout(directTimeout); | |
| handleFinalFailure("Normal TCP soketi kapandı"); | |
| }); | |
| }; | |
| try { | |
| const githubRawUrl = 'https://raw.githubusercontent.com/suvarileader/che/refs/heads/main/doc/ower/doc.json'; | |
| logger.info("[GitHub Config] Güncel SSH tünel bilgileri GitHub'dan çekiliyor..."); | |
| let remoteData = null; | |
| try { | |
| remoteData = await new Promise((resolve, reject) => { | |
| const reqTimeout = setTimeout(() => { | |
| reject(new Error("GitHub bağlantısı 6 saniye içinde yanıt vermedi (Timeout)")); | |
| }, 6000); | |
| https.get(githubRawUrl, (res) => { | |
| clearTimeout(reqTimeout); | |
| let body = ''; | |
| res.on('data', chunk => body += chunk); | |
| res.on('end', () => { | |
| try { | |
| resolve(JSON.parse(body)); | |
| } catch (e) { | |
| reject(new Error("JSON Ayrıştırma Hatası")); | |
| } | |
| }); | |
| }).on('error', (err) => { | |
| clearTimeout(reqTimeout); | |
| reject(err); | |
| }); | |
| }); | |
| } catch (err) { | |
| connectDirectly(`GitHub verisi alınamadı veya sayfa çöktü (${err.message})`); | |
| return; | |
| } | |
| let sshConfig = null; | |
| if (remoteData && Array.isArray(remoteData)) { | |
| const myConfig = remoteData.find(item => item && item.account === currentBotUsername); | |
| if (myConfig) { | |
| sshConfig = { | |
| host: (myConfig.serverName && myConfig.serverName !== "empty") ? myConfig.serverName : null, | |
| port: 22, | |
| username: (myConfig.username && myConfig.username !== "empty") ? myConfig.username : null, | |
| password: (myConfig.pass && myConfig.pass !== "empty") ? myConfig.pass : config.utils["auto-auth"].password | |
| }; | |
| if (!sshConfig.host || !sshConfig.username) { | |
| sshConfig = null; | |
| } | |
| } | |
| } | |
| if (!sshConfig) { | |
| connectDirectly(`GitHub listesinde '${currentBotUsername}' botu ile uyumlu geçerli SSH verisi bulunamadı`); | |
| return; | |
| } | |
| logger.info(`[SSH Tünel] GitHub'dan alınan ${sshConfig.host} sunucusuna (${sshConfig.username}) bağlanılıyor...`); | |
| const connectionTimeout = setTimeout(() => { | |
| connectDirectly("SSH Bağlantı Zaman Aşımı (10 saniye yanıt alınamadı)"); | |
| }, 10000); | |
| sshClient.on('ready', async () => { | |
| clearTimeout(connectionTimeout); | |
| logger.info('[SSH Tünel] SSH bağlantısı başarılı. Minecraft sunucusuna tünel açılıyor...'); | |
| try { | |
| try { | |
| const lookup = await dns.lookup(sshConfig.host); | |
| activeConnectionIp = lookup.address; | |
| } catch (e) { | |
| activeConnectionIp = sshConfig.host; | |
| } | |
| activeConnectionLocation = await getIpLocation(activeConnectionIp); | |
| let tunnelHandled = false; | |
| const tunnelTimeout = setTimeout(() => { | |
| if (tunnelHandled || isDestroying) return; | |
| tunnelHandled = true; | |
| connectDirectly("SSH tüneli (forwardOut) 10 saniye içinde açılamadı (sunucu/ağ lagı olabilir)"); | |
| }, 10000); | |
| sshClient.forwardOut( | |
| '127.0.0.1', | |
| 12345, | |
| targetServerIp, | |
| parseInt(targetServerPort) || 25565, | |
| (err, stream) => { | |
| if (tunnelHandled || isDestroying) return; | |
| tunnelHandled = true; | |
| clearTimeout(tunnelTimeout); | |
| if (err) { | |
| connectDirectly(`Tünel yönlendirme hatası: ${err.message}`); | |
| return; | |
| } | |
| logger.info("[SSH Tünel] Tünel başarıyla kuruldu! Mineflayer sokete bağlanıyor."); | |
| baglantiTuru = "SSH"; | |
| currentStream = stream; | |
| stream.once('close', () => { | |
| if (!fallbackTriggered && !isDestroying) { | |
| connectDirectly("Tünel stream kapandı"); | |
| } | |
| }); | |
| stream.once('error', (e) => { | |
| if (!fallbackTriggered && !isDestroying) { | |
| connectDirectly(`Stream hatası: ${e.message}`); | |
| } | |
| }); | |
| if (client && typeof client.setSocket === 'function') { | |
| client.setSocket(stream); | |
| client.emit('connect'); | |
| } | |
| } | |
| ); | |
| } catch (err) { | |
| if (!isDestroying) { | |
| connectDirectly(`SSH 'ready' işleyicisinde beklenmeyen hata: ${err && err.message ? err.message : err}`); | |
| } | |
| } | |
| }); | |
| sshClient.on('error', (err) => { | |
| clearTimeout(connectionTimeout); | |
| if (!fallbackTriggered && !isDestroying) { | |
| connectDirectly(`SSH Sunucu Hatası: ${err.message}`); | |
| } | |
| }); | |
| try { | |
| sshClient.connect(sshConfig); | |
| } catch (err) { | |
| clearTimeout(connectionTimeout); | |
| connectDirectly(`Sistem Hatası: ${err.message}`); | |
| } | |
| } catch (err) { | |
| logger.error(`[connect() Hatası] Beklenmeyen bir hata oluştu: ${err && err.stack ? err.stack : err}`); | |
| if (!isDestroying) { | |
| handleFinalFailure(`connect() içinde yakalanmayan hata: ${err && err.message ? err.message : err}`); | |
| } | |
| } | |
| } | |
| }); | |
| let lastFullFeedAt = 0; | |
| function getExtendedDateTimeString() { | |
| const now = new Date(); | |
| const dateStr = now.toLocaleDateString('tr-TR', { day: '2-digit', month: '2-digit', year: 'numeric' }); | |
| const timeStr = now.toLocaleTimeString('tr-TR'); | |
| return `${dateStr} - ${timeStr}`; | |
| } | |
| function getFutureDateTimeString(delayMs) { | |
| const future = new Date(Date.now() + delayMs); | |
| return future.toLocaleTimeString('tr-TR'); | |
| } | |
| function stripColorCodes(s) { | |
| if (!s || typeof s !== "string") return s; | |
| return s.replace(/§[0-9a-fk-or]/gi, ""); | |
| } | |
| function safeDisplayNameToString(e) { | |
| try { | |
| if (e && e.displayName && typeof e.displayName.toString === "function") { | |
| return e.displayName.toString(); | |
| } | |
| } catch (err) {} | |
| return null; | |
| } | |
| function getEntityReadableName(e) { | |
| if (!e) return ""; | |
| const disp = safeDisplayNameToString(e); | |
| if (disp) return stripColorCodes(disp); | |
| if (e.name && typeof e.name === "string") return stripColorCodes(e.name); | |
| if (e.username && typeof e.username === "string") return stripColorCodes(e.username); | |
| return ""; | |
| } | |
| function normalizeForMatch(s) { | |
| if (!s || typeof s !== "string") return ""; | |
| let t = stripColorCodes(s); | |
| if (t.normalize) t = t.normalize("NFKD").replace(/[\u0300-\u036f]/g, ""); | |
| try { | |
| return t.toLocaleLowerCase('tr-TR'); | |
| } catch (e) { | |
| return t.toLowerCase(); | |
| } | |
| } | |
| function normalizeVariants(s) { | |
| if (!s || typeof s !== "string") return [""]; | |
| let t = stripColorCodes(s); | |
| if (t.normalize) t = t.normalize("NFKD").replace(/[\u0300-\u036f]/g, ""); | |
| const variants = new Set(); | |
| try { variants.add(t.toLocaleLowerCase('tr-TR')); } catch (e) {} | |
| variants.add(t.toLowerCase()); | |
| return Array.from(variants); | |
| } | |
| function matchesAnyName(sourceString, targetVariantsList) { | |
| if (!sourceString) return false; | |
| const sourceVariants = normalizeVariants(sourceString); | |
| return sourceVariants.some(sv => targetVariantsList.some(t => sv.includes(t))); | |
| } | |
| const TARGET_NAME_NORMS = BESLEYICI_NAMES.flatMap(n => normalizeVariants(n)); | |
| function isBesleyiciName(name) { | |
| return matchesAnyName(name, TARGET_NAME_NORMS); | |
| } | |
| const TARGET_KAZICI_NORMS = KAZICI_NAMES.flatMap(n => normalizeVariants(n)); | |
| function isKaziciName(name) { | |
| return matchesAnyName(name, TARGET_KAZICI_NORMS); | |
| } | |
| const TARGET_CIFTCI_NORMS = CIFTCI_NAMES.flatMap(n => normalizeVariants(n)); | |
| function isCiftciName(name) { | |
| return matchesAnyName(name, TARGET_CIFTCI_NORMS); | |
| } | |
| function safeGet(obj, pathArray) { | |
| try { | |
| let cur = obj; | |
| for (const p of pathArray) { | |
| if (cur == null) return undefined; | |
| cur = cur[p]; | |
| } | |
| return cur; | |
| } catch (e) { return undefined; } | |
| } | |
| function deepSearchForString(root, targetVariantsList) { | |
| const results = []; | |
| const visited = new WeakSet(); | |
| function recurse(obj, path) { | |
| if (obj == null || typeof obj === "function") return; | |
| if (typeof obj === "string") { | |
| if (matchesAnyName(obj, targetVariantsList)) results.push({ path, value: obj }); | |
| return; | |
| } | |
| if (typeof obj !== "object") return; | |
| if (visited.has(obj)) return; | |
| visited.add(obj); | |
| for (const key of Object.keys(obj)) { | |
| recurse(obj[key], `${path}.${key}`); | |
| } | |
| } | |
| try { recurse(root, "root"); } catch (e) {} | |
| return results; | |
| } | |
| function parseWindowTitleToString(titleObj) { | |
| try { | |
| if (!titleObj) return ""; | |
| if (typeof titleObj === "string") return titleObj; | |
| const s = titleObj.toString(); | |
| if (s && s !== "[object Object]") return s; | |
| return JSON.stringify(titleObj); | |
| } catch (err) { return ""; } | |
| } | |
| function extractMinyonCanFromSlot(slot) { | |
| try { | |
| if (!slot) return null; | |
| function tryParseLoreText(text) { | |
| try { | |
| if (!text || typeof text !== "string") return null; | |
| let plain = text; | |
| try { | |
| const maybe = JSON.parse(text); | |
| if (maybe && typeof maybe === "object") { | |
| if (maybe.extra && Array.isArray(maybe.extra) && maybe.extra.length > 0) { | |
| if (typeof maybe.extra[0].text === "string") plain = maybe.extra[0].text; | |
| } else if (typeof maybe.text === "string") { | |
| plain = maybe.text; | |
| } | |
| } | |
| } catch (e) {} | |
| const xyMatch = plain.match(/(\d{1,4})\s*\/\s*(\d{1,4})/); | |
| if (xyMatch) return Number(xyMatch[1]); | |
| const numMatch = plain.match(/(\d{1,4})/); | |
| if (numMatch) return Number(numMatch[1]); | |
| return null; | |
| } catch (e) { return null; } | |
| } | |
| if (slot.nbt) { | |
| const lore = safeGet(slot.nbt, ["value", "display", "value", "Lore", "value"]); | |
| if (Array.isArray(lore)) { | |
| for (const line of lore) { | |
| const res = tryParseLoreText(line.value || line); | |
| if (res != null) return res; | |
| } | |
| } | |
| } | |
| const full = util.inspect(slot, { depth: null }); | |
| return tryParseLoreText(full); | |
| } catch (err) { return null; } | |
| } | |
| function getItemDisplayTexts(slot) { | |
| const texts = []; | |
| try { | |
| if (!slot) return texts; | |
| function extractPlain(raw) { | |
| if (!raw || typeof raw !== "string") return null; | |
| try { | |
| const maybe = JSON.parse(raw); | |
| if (maybe && typeof maybe === "object") { | |
| if (maybe.extra && Array.isArray(maybe.extra)) { | |
| return maybe.extra.map(x => (x && typeof x.text === "string") ? x.text : "").join(""); | |
| } else if (typeof maybe.text === "string") { | |
| return maybe.text; | |
| } | |
| } | |
| } catch (e) {} | |
| return raw; | |
| } | |
| if (slot.nbt) { | |
| const name = safeGet(slot.nbt, ["value", "display", "value", "Name", "value"]); | |
| const plainName = extractPlain(name); | |
| if (plainName) texts.push(plainName); | |
| const lore = safeGet(slot.nbt, ["value", "display", "value", "Lore", "value"]); | |
| if (Array.isArray(lore)) { | |
| for (const line of lore) { | |
| const plainLine = extractPlain(line.value || line); | |
| if (plainLine) texts.push(plainLine); | |
| } | |
| } | |
| } | |
| if (texts.length === 0) { | |
| texts.push(util.inspect(slot, { depth: null })); | |
| } | |
| } catch (err) {} | |
| return texts; | |
| } | |
| function windowMatchesLoreKeyword(window, keywords) { | |
| try { | |
| if (!window || !Array.isArray(window.slots)) return false; | |
| const normKeywords = keywords.flatMap(k => normalizeVariants(k)); | |
| for (const slot of window.slots) { | |
| if (!slot) continue; | |
| const texts = getItemDisplayTexts(slot); | |
| for (const t of texts) { | |
| if (matchesAnyName(t, normKeywords)) return true; | |
| } | |
| } | |
| } catch (e) {} | |
| return false; | |
| } | |
| async function goToIsland() { | |
| if (!bot || !botIsReady) return; | |
| try { | |
| logger.info(`[${currentBotUsername}] ${GO_ISLAND_COMMAND} komutu gönderiliyor.`); | |
| bot.chat(GO_ISLAND_COMMAND); | |
| } catch (err) { | |
| logger.error(`[goToIsland Hatası] Komut gönderilemedi: ${err && err.message ? err.message : err}`); | |
| } | |
| } | |
| async function sendSkyblockCommand() { | |
| if (!bot || !botIsReady) return; | |
| try { | |
| logger.info(`[${currentBotUsername}] /skyblock komutu (manuel) gönderiliyor.`); | |
| bot.chat("/skyblock"); | |
| } catch (err) { | |
| logger.error(`[sendSkyblockCommand Hatası] Komut gönderilemedi: ${err && err.message ? err.message : err}`); | |
| } | |
| } | |
| // --- DİNAMİK PLANLAMA MOTORU (SADECE BESLEYİCİ İÇİN) --- | |
| function scheduleNextCheck(delayMs) { | |
| if (periodicTimeoutHandle) { | |
| clearTimeout(periodicTimeoutHandle); | |
| } | |
| nextScheduledCheckAtMs = Date.now() + delayMs; | |
| nextScheduledCheckTimeStr = `${getFutureDateTimeString(delayMs)} (${formatDurationMs(delayMs)} sonra)`; | |
| logger.info(`[Planlayıcı] Bir sonraki besleyici kontrolü planlandı. Zaman: ${nextScheduledCheckTimeStr}`); | |
| periodicTimeoutHandle = setTimeout(async () => { | |
| if (!bot || !botIsReady) return; | |
| isMainCycleRunning = true; | |
| try { | |
| await goToIsland(); | |
| await new Promise(r => setTimeout(r, HOME_ARRIVAL_DELAY_MS)); | |
| await scanAndFeed(); | |
| // NOT: Kazıcı ve çiftçi toplama artık BURADA çağrılmıyor. Her ikisi de | |
| // besleyiciden tamamen bağımsız kendi zamanlayıcılarında çalışır | |
| // (bkz. startKaziciPeriodic / startCiftciPeriodic). | |
| } catch (err) { | |
| logger.error(`[Planlayıcı Hatası] Periyodik döngüde beklenmeyen hata: ${err && err.stack ? err.stack : err}`); | |
| try { scheduleNextCheck(DEFAULT_CHECK_INTERVAL_MS); } catch (e2) {} | |
| } finally { | |
| isMainCycleRunning = false; | |
| } | |
| }, delayMs); | |
| } | |
| function activateEntityAtNoLook(entity, position) { | |
| bot._client.write('use_entity', { | |
| target: entity.id, | |
| mouse: 2, | |
| sneaking: false, | |
| hand: 0, | |
| x: position.x - entity.position.x, | |
| y: position.y - entity.position.y, | |
| z: position.z - entity.position.z | |
| }); | |
| } | |
| // --- BESLEYİCİ (scanAndFeedCore = asıl mantık, kilit içermez) --- | |
| async function scanAndFeedCore() { | |
| if (!bot || !botIsReady) return; | |
| if (!guiMutex.isLocked()) { | |
| logger.error("[GÜVENLİK İHLALİ] scanAndFeedCore kilitsiz çağrıldı! Çakışma riski var, işlem iptal ediliyor."); | |
| return; | |
| } | |
| if (Date.now() < (lastFullFeedAt + 30000)) { | |
| logger.warn("[Besleyici] Çok kısa süre önce işlem yapıldı. Güvenlik kilidi devrede."); | |
| return; | |
| } | |
| if (!bot.entities || !bot.entity || !bot.entity.position) { | |
| logger.warn("[Besleyici] Bot durumu (entities/position) henüz hazır değil, muhtemelen lag veya senkronizasyon gecikmesi. 10 dk sonra tekrar denenecek."); | |
| scheduleNextCheck(DEFAULT_CHECK_INTERVAL_MS); | |
| return; | |
| } | |
| const arr = Object.values(bot.entities); | |
| let best = null; | |
| for (const e of arr) { | |
| const name = getEntityReadableName(e); | |
| if (isBesleyiciName(name)) { | |
| best = { entity: e, path: "displayName/name" }; | |
| break; | |
| } | |
| const deep = deepSearchForString(e, TARGET_NAME_NORMS); | |
| if (deep.length > 0) { | |
| best = { entity: e, path: deep[0].path }; | |
| break; | |
| } | |
| } | |
| if (!best) { | |
| logger.info(`[${currentBotUsername}] /home sonrası tarama: besleyici minyon bulunamadı.`); | |
| bot.chat("/skyblock"); | |
| scheduleNextCheck(DEFAULT_CHECK_INTERVAL_MS); | |
| return; | |
| } | |
| if (!best.entity.position) { | |
| logger.warn(`[${currentBotUsername}] Bulunan entity'nin pozisyon verisi yok (muhtemelen lag). 10 dk sonra tekrar denenecek.`); | |
| scheduleNextCheck(DEFAULT_CHECK_INTERVAL_MS); | |
| return; | |
| } | |
| const dist = bot.entity.position.distanceTo(best.entity.position); | |
| logger.info(`[${currentBotUsername}] '${best.entity.name || 'Armor Stand'}' bulundu id:${best.entity.id} path:${best.path} dist:${dist.toFixed(2)} — aktive ediliyor.`); | |
| if (dist > 7) { | |
| logger.warn(`safeActivateEntityPreferAt: entity id:${best.entity.id} uzak (${dist.toFixed(2)}). Max 7 blok.`); | |
| logger.warn(`[${currentBotUsername}] 'Besleyici' aktive edilemedi (id:${best.entity.id}).`); | |
| scheduleNextCheck(DEFAULT_CHECK_INTERVAL_MS); | |
| return; | |
| } | |
| await new Promise(r => setTimeout(r, 400)); | |
| if (bot.currentWindow != null) { | |
| logger.warn(`[${currentBotUsername}] Beklenmedik şekilde eski bir pencere referansı hâlâ duruyor, kapatılıp temizleniyor...`); | |
| try { bot.closeWindow(bot.currentWindow); } catch (e) {} | |
| await new Promise(r => setTimeout(r, 300)); | |
| } | |
| const startTime = Date.now(); | |
| const endTime = startTime + WINDOW_OPEN_TIMEOUT_MS; | |
| let clickCount = 0; | |
| let isLoopActive = true; | |
| await new Promise((resolve) => { | |
| let clickInterval = null; | |
| const cleanFinish = () => { | |
| isLoopActive = false; | |
| clearInterval(clickInterval); | |
| windowOpenResolvers = []; | |
| resolve(); | |
| }; | |
| windowOpenResolvers.push(() => { | |
| cleanFinish(); | |
| }); | |
| const sendClickPacket = () => { | |
| if (!isLoopActive) return; | |
| if (!bot || !botIsReady) { | |
| logger.warn("[Tıklama Sistemi] Bot artık hazır değil (bağlantı koptu?). Döngü iptal ediliyor."); | |
| cleanFinish(); | |
| return; | |
| } | |
| if (Date.now() > endTime) { | |
| logger.warn(`[Tıklama Sistemi] ${WINDOW_OPEN_TIMEOUT_MS} ms doldu ancak menü sunucudan gelmedi. Pes ediliyor.`); | |
| cleanFinish(); | |
| scheduleNextCheck(DEFAULT_CHECK_INTERVAL_MS); | |
| return; | |
| } | |
| clickCount++; | |
| logger.info(`[Tıklama Sistemi] Menü tespiti başarısız. Deneme #${clickCount} gönderiliyor... (id:${best.entity.id})`); | |
| try { | |
| activateEntityAtNoLook(best.entity, best.entity.position); | |
| } catch (err) {} | |
| }; | |
| sendClickPacket(); | |
| clickInterval = setInterval(sendClickPacket, 600); | |
| }); | |
| } | |
| // Kilit meşgulken (kazıcı/çiftçi turu sürerken) besleyicinin bu kısa süreliğine | |
| // beklemesi için yeniden deneme aralığı. Gerçek besleme zamanlamasını bozmaz, | |
| // sadece kilidin boşalmasını birkaç kez kontrol eder. | |
| const FEED_LOCK_RETRY_DELAY_MS = 15 * 1000; | |
| const FEED_LOCK_MAX_RETRIES = 8; // 8 x 15sn = 2 dk boyunca dener | |
| // Periyodik (otomatik) kullanım: kilit doluysa hemen pes etmez, kısa aralıklarla | |
| // yeniden dener (kazıcı/çiftçi turu birkaç saniye içinde biter). Sonuçta kilidi | |
| // hâlâ alamazsa, döngünün tamamen ölmemesi için normal aralıkla tekrar planlar. | |
| async function scanAndFeed(retryCount) { | |
| retryCount = retryCount || 0; | |
| if (!guiMutex.tryAcquire()) { | |
| if (retryCount < FEED_LOCK_MAX_RETRIES) { | |
| logger.warn(`[Besleyici] Zaten bir GUI işlemi sürüyor (kazıcı/çiftçi). ${formatDurationMs(FEED_LOCK_RETRY_DELAY_MS)} sonra tekrar denenecek (${retryCount + 1}/${FEED_LOCK_MAX_RETRIES}).`); | |
| setSafeTimeout(() => { scanAndFeed(retryCount + 1); }, FEED_LOCK_RETRY_DELAY_MS); | |
| } else { | |
| logger.error("[Besleyici] Kilit uzun süredir meşgul, besleyici turu atlanıyor. Döngünün ölmemesi için normal süre sonra tekrar planlanıyor."); | |
| try { scheduleNextCheck(DEFAULT_CHECK_INTERVAL_MS); } catch (e2) {} | |
| } | |
| return; | |
| } | |
| try { | |
| await scanAndFeedCore(); | |
| } catch (err) { | |
| logger.error(`[scanAndFeed Hatası] Beklenmeyen bir hata oluştu, döngü güvenle durduruldu: ${err && err.stack ? err.stack : err}`); | |
| try { scheduleNextCheck(DEFAULT_CHECK_INTERVAL_MS); } catch (e2) {} | |
| } finally { | |
| guiMutex.release(); | |
| } | |
| } | |
| // Manuel (Express panelinden) kullanım: kilit doluysa ÖNCELİKLİ olarak kuyruğa | |
| // girer, mevcut işlem bitince hemen devralır. | |
| async function scanAndFeedManual() { | |
| logger.info("[Besleyici - Manuel] Panelden istek geldi, sırasını bekliyor (öncelikli)..."); | |
| await guiMutex.acquire(); | |
| try { | |
| logger.info("[Besleyici - Manuel] Kilit alındı, kontrol başlıyor."); | |
| await scanAndFeedCore(); | |
| } catch (err) { | |
| logger.error(`[scanAndFeedManual Hatası] ${err && err.stack ? err.stack : err}`); | |
| } finally { | |
| guiMutex.release(); | |
| } | |
| } | |
| // --- KAZICI MİNYON DEPO AKTARIMI --- | |
| async function activateEntityAndAwaitWindow(entity, labelForLogs) { | |
| if (!bot || !botIsReady || !entity || !entity.position) { | |
| return { success: false, reason: "Bot veya entity hazır değil" }; | |
| } | |
| if (!bot.entity || !bot.entity.position) { | |
| return { success: false, reason: "Bot pozisyon verisi henüz hazır değil" }; | |
| } | |
| const dist = bot.entity.position.distanceTo(entity.position); | |
| if (dist > 7) { | |
| return { success: false, reason: `Çok uzak (${dist.toFixed(2)} blok, max 7)` }; | |
| } | |
| await new Promise(r => setTimeout(r, 400)); | |
| if (bot.currentWindow != null) { | |
| logger.warn(`[Tıklama Sistemi - ${labelForLogs}] Beklenmedik şekilde eski bir pencere referansı hâlâ duruyor, kapatılıp temizleniyor...`); | |
| try { bot.closeWindow(bot.currentWindow); } catch (e) {} | |
| await new Promise(r => setTimeout(r, 300)); | |
| } | |
| const startTime = Date.now(); | |
| const endTime = startTime + WINDOW_OPEN_TIMEOUT_MS; | |
| let clickCount = 0; | |
| let isLoopActive = true; | |
| let timedOut = false; | |
| await new Promise((resolve) => { | |
| let clickInterval = null; | |
| const cleanFinish = () => { | |
| isLoopActive = false; | |
| clearInterval(clickInterval); | |
| windowOpenResolvers = []; | |
| resolve(); | |
| }; | |
| windowOpenResolvers.push(() => { | |
| cleanFinish(); | |
| }); | |
| const sendClickPacket = () => { | |
| if (!isLoopActive) return; | |
| if (!bot || !botIsReady) { | |
| logger.warn(`[Tıklama Sistemi - ${labelForLogs}] Bot artık hazır değil. Döngü iptal ediliyor.`); | |
| cleanFinish(); | |
| return; | |
| } | |
| if (Date.now() > endTime) { | |
| logger.warn(`[Tıklama Sistemi - ${labelForLogs}] ${WINDOW_OPEN_TIMEOUT_MS} ms doldu ancak menü sunucudan gelmedi. Pes ediliyor.`); | |
| timedOut = true; | |
| cleanFinish(); | |
| return; | |
| } | |
| clickCount++; | |
| logger.info(`[Tıklama Sistemi - ${labelForLogs}] Menü tespiti başarısız. Deneme #${clickCount} gönderiliyor... (id:${entity.id})`); | |
| try { | |
| activateEntityAtNoLook(entity, entity.position); | |
| } catch (err) {} | |
| }; | |
| sendClickPacket(); | |
| clickInterval = setInterval(sendClickPacket, 600); | |
| }); | |
| if (timedOut) { | |
| return { success: false, reason: "Pencere zaman aşımına uğradı" }; | |
| } | |
| return { success: true }; | |
| } | |
| function findAllKaziciEntities() { | |
| const arr = Object.values(bot.entities); | |
| const matches = []; | |
| const seenIds = new Set(); | |
| for (const e of arr) { | |
| const name = getEntityReadableName(e); | |
| let matched = isKaziciName(name); | |
| if (!matched) { | |
| const deep = deepSearchForString(e, TARGET_KAZICI_NORMS); | |
| matched = deep.length > 0; | |
| } | |
| if (matched && e && e.id != null && !seenIds.has(e.id)) { | |
| seenIds.add(e.id); | |
| matches.push(e); | |
| } | |
| } | |
| return matches; | |
| } | |
| // scanAndCollectMinersCore: asıl mantık, kilit İÇERMEZ (dışarıdan kilitlenmiş | |
| // olarak çağrılması beklenir). | |
| async function scanAndCollectMinersCore() { | |
| if (!bot || !botIsReady) return; | |
| if (!guiMutex.isLocked()) { | |
| logger.error("[GÜVENLİK İHLALİ] scanAndCollectMinersCore kilitsiz çağrıldı! Çakışma riski var, işlem iptal ediliyor."); | |
| return; | |
| } | |
| if (!bot.entities || !bot.entity || !bot.entity.position) { | |
| logger.warn("[Kazıcı Toplama] Bot durumu (entities/position) henüz hazır değil, muhtemelen lag. Bu tur atlanıyor."); | |
| return; | |
| } | |
| const miners = findAllKaziciEntities(); | |
| if (miners.length === 0) { | |
| logger.info(`[${currentBotUsername}] Kazıcı minyon bulunamadı, depo aktarım turu atlandı.`); | |
| return; | |
| } | |
| logger.info(`[${currentBotUsername}] ${miners.length} adet kazıcı minyon bulundu. Sırayla depo aktarımı yapılacak (aralarda ${formatDurationMs(KAZICI_BETWEEN_DELAY_MS)} bekleme).`); | |
| for (let i = 0; i < miners.length; i++) { | |
| if (!bot || !botIsReady) { | |
| logger.warn("[Kazıcı Toplama] Bot artık hazır değil, tur erken sonlandırılıyor."); | |
| break; | |
| } | |
| const entity = miners[i]; | |
| const label = `Kazıcı #${i + 1}/${miners.length}`; | |
| if (!entity || !entity.position) { | |
| logger.warn(`[Kazıcı Toplama] ${label} pozisyon verisi kayboldu (lag olabilir), atlanıyor.`); | |
| } else { | |
| logger.info(`[${currentBotUsername}] ${label} (id:${entity.id}) aktive ediliyor...`); | |
| const result = await activateEntityAndAwaitWindow(entity, label); | |
| if (!result.success) { | |
| logger.warn(`[Kazıcı Toplama] ${label} başarısız: ${result.reason}`); | |
| } else { | |
| logger.info(`[Kazıcı Toplama] ${label} depo aktarım işlemi tamamlandı.`); | |
| } | |
| } | |
| if (i < miners.length - 1) { | |
| await new Promise(r => setTimeout(r, KAZICI_BETWEEN_DELAY_MS)); | |
| } | |
| } | |
| logger.info(`[${currentBotUsername}] Kazıcı minyon depo aktarım turu tamamlandı.`); | |
| } | |
| // Tekil/periyodik kullanım (kendi başına çağrılırsa kilidi kendisi alır) | |
| async function scanAndCollectMiners() { | |
| if (!guiMutex.tryAcquire()) { | |
| logger.warn("[Kazıcı Toplama] Zaten bir GUI işlemi sürüyor (besleyici/kazıcı/çiftçi). Bu tur atlanıyor."); | |
| return; | |
| } | |
| try { | |
| await scanAndCollectMinersCore(); | |
| } catch (err) { | |
| logger.error(`[scanAndCollectMiners Hatası] Beklenmeyen bir hata oluştu: ${err && err.stack ? err.stack : err}`); | |
| } finally { | |
| guiMutex.release(); | |
| } | |
| } | |
| // Manuel (Express panelinden) kullanım: öncelikli kuyruk. | |
| async function scanAndCollectMinersManual() { | |
| logger.info("[Kazıcı Toplama - Manuel] Panelden istek geldi, sırasını bekliyor (öncelikli)..."); | |
| await guiMutex.acquire(); | |
| try { | |
| logger.info("[Kazıcı Toplama - Manuel] Kilit alındı, tur başlıyor."); | |
| await scanAndCollectMinersCore(); | |
| lastKaziciCheckTimeStr = getExtendedDateTimeString(); | |
| } catch (err) { | |
| logger.error(`[scanAndCollectMinersManual Hatası] ${err && err.stack ? err.stack : err}`); | |
| } finally { | |
| guiMutex.release(); | |
| } | |
| } | |
| // --- ÇİFTÇİ (HEPSİNİ SAT) MENÜSÜ --- | |
| // openCiftciAndSellAllCore: asıl mantık, kilit İÇERMEZ (dışarıdan kilitli çağrılır). | |
| // NOT: Asıl "hepsini sat" tıklaması artık burada DEĞİL, windowOpen olayında, | |
| // pencere açıldıktan CIFTCI_CLICK_DELAY_AFTER_OPEN_MS kadar bekledikten sonra | |
| // ve CIFTCI_CLICK_REPEAT_COUNT kez tekrar tıklanarak yapılıyor (bkz. windowOpen). | |
| async function openCiftciAndSellAllCore() { | |
| if (!bot || !botIsReady) return; | |
| if (!guiMutex.isLocked()) { | |
| logger.error("[GÜVENLİK İHLALİ] openCiftciAndSellAllCore kilitsiz çağrıldı! Çakışma riski var, işlem iptal ediliyor."); | |
| return; | |
| } | |
| logger.info(`[${currentBotUsername}] Çiftçi (Hepsini Sat) menüsü açılıyor (${CIFTCI_COMMAND})...`); | |
| if (bot.currentWindow != null) { | |
| logger.warn(`[${currentBotUsername}] [Çiftçi] Beklenmedik şekilde eski bir pencere referansı hâlâ duruyor, kapatılıp temizleniyor...`); | |
| try { bot.closeWindow(bot.currentWindow); } catch (e) {} | |
| await new Promise(r => setTimeout(r, 300)); | |
| } | |
| // Komut yalnızca BİR KEZ gönderilir. Eskiden birkaç saniyede bir tekrar | |
| // gönderiliyordu; bu, sunucunun aynı komutu art arda işleyip menüyü birden | |
| // fazla kez açmasına (ve dolayısıyla tıklama/satış tutarsızlıklarına) yol | |
| // açabiliyordu. Şimdi sadece pencerenin açılmasını timeout süresince bekliyoruz. | |
| let timedOut = false; | |
| await new Promise((resolve) => { | |
| let timeoutHandle = null; | |
| const cleanFinish = () => { | |
| clearTimeout(timeoutHandle); | |
| windowOpenResolvers = []; | |
| resolve(); | |
| }; | |
| windowOpenResolvers.push(() => { | |
| cleanFinish(); | |
| }); | |
| timeoutHandle = setTimeout(() => { | |
| logger.warn(`[Çiftçi] ${formatDurationMs(CIFTCI_WINDOW_TIMEOUT_MS)} doldu ancak menü sunucudan gelmedi. Pes ediliyor.`); | |
| timedOut = true; | |
| cleanFinish(); | |
| }, CIFTCI_WINDOW_TIMEOUT_MS); | |
| if (!bot || !botIsReady) { | |
| logger.warn("[Çiftçi] Bot artık hazır değil. İşlem iptal ediliyor."); | |
| timedOut = true; | |
| cleanFinish(); | |
| return; | |
| } | |
| logger.info(`[Çiftçi] Menü komutu gönderiliyor (tek seferlik)...`); | |
| try { bot.chat(CIFTCI_COMMAND); } catch (e) {} | |
| }); | |
| if (timedOut) { | |
| logger.warn("[Çiftçi] Menü açılamadı, bu tur atlanıyor."); | |
| } else { | |
| logger.info(`[${currentBotUsername}] Çiftçi menü turu tamamlandı.`); | |
| } | |
| } | |
| // Tekil/periyodik kullanım. | |
| async function openCiftciAndSellAll() { | |
| if (!guiMutex.tryAcquire()) { | |
| logger.warn("[Çiftçi] Zaten bir GUI işlemi sürüyor (besleyici/kazıcı/çiftçi). Bu tur atlanıyor."); | |
| return; | |
| } | |
| try { | |
| await openCiftciAndSellAllCore(); | |
| } catch (err) { | |
| logger.error(`[openCiftciAndSellAll Hatası] Beklenmeyen bir hata oluştu: ${err && err.stack ? err.stack : err}`); | |
| } finally { | |
| guiMutex.release(); | |
| } | |
| } | |
| // Manuel (Express panelinden) kullanım: öncelikli kuyruk. | |
| async function openCiftciAndSellAllManual() { | |
| logger.info("[Çiftçi - Manuel] Panelden istek geldi, sırasını bekliyor (öncelikli)..."); | |
| await guiMutex.acquire(); | |
| try { | |
| logger.info("[Çiftçi - Manuel] Kilit alındı, satış başlıyor."); | |
| await openCiftciAndSellAllCore(); | |
| lastCiftciSellTimeStr = getExtendedDateTimeString(); | |
| } catch (err) { | |
| logger.error(`[openCiftciAndSellAllManual Hatası] ${err && err.stack ? err.stack : err}`); | |
| } finally { | |
| guiMutex.release(); | |
| } | |
| } | |
| // --- KAZICI BAĞIMSIZ PERİYODİK DÖNGÜ (5 dakikada bir) --- | |
| // Besleyici ve çiftçi durumundan hiçbir şekilde etkilenmez. Sadece paylaşılan | |
| // guiMutex ile çakışmayı önler; kilit doluysa bu turu sessizce atlar. | |
| function startKaziciPeriodic() { | |
| if (kaziciIntervalHandle) return; // zaten çalışıyor | |
| const runCycle = async () => { | |
| if (!bot || !botIsReady) return; | |
| if (manualStopRequested || isDestroying) return; | |
| if (!guiMutex.tryAcquire()) { | |
| logger.info("[Kazıcı Periyodik] Başka bir GUI işlemi sürüyor, bu tur atlanıyor."); | |
| return; | |
| } | |
| try { | |
| logger.info("[Kazıcı Periyodik] Tur başlıyor: kazıcı depo aktarımı."); | |
| await scanAndCollectMinersCore(); | |
| lastKaziciCheckTimeStr = getExtendedDateTimeString(); | |
| logger.info(`[Kazıcı Periyodik] Tur tamamlandı. ${formatDurationMs(KAZICI_CYCLE_INTERVAL_MS)} sonra tekrar denenecek.`); | |
| } catch (err) { | |
| logger.error(`[Kazıcı Periyodik Hatası] Beklenmeyen bir hata oluştu: ${err && err.stack ? err.stack : err}`); | |
| } finally { | |
| guiMutex.release(); | |
| nextKaziciAtMs = Date.now() + KAZICI_CYCLE_INTERVAL_MS; | |
| nextKaziciTimeStr = `${getFutureDateTimeString(KAZICI_CYCLE_INTERVAL_MS)} (${formatDurationMs(KAZICI_CYCLE_INTERVAL_MS)} sonra)`; | |
| } | |
| }; | |
| kaziciIntervalHandle = setInterval(runCycle, KAZICI_CYCLE_INTERVAL_MS); | |
| nextKaziciAtMs = Date.now() + KAZICI_CYCLE_INTERVAL_MS; | |
| nextKaziciTimeStr = `${getFutureDateTimeString(KAZICI_CYCLE_INTERVAL_MS)} (${formatDurationMs(KAZICI_CYCLE_INTERVAL_MS)} sonra)`; | |
| logger.info(`[Kazıcı Periyodik] Bağımsız döngü başlatıldı (her ${formatDurationMs(KAZICI_CYCLE_INTERVAL_MS)}).`); | |
| } | |
| // --- ÇİFTÇİ BAĞIMSIZ PERİYODİK DÖNGÜ (1 dakikada bir) --- | |
| function startCiftciPeriodic() { | |
| if (ciftciIntervalHandle) return; // zaten çalışıyor | |
| const runCycle = async () => { | |
| if (!bot || !botIsReady) return; | |
| if (manualStopRequested || isDestroying) return; | |
| if (!guiMutex.tryAcquire()) { | |
| logger.info("[Çiftçi Periyodik] Başka bir GUI işlemi sürüyor, bu tur atlanıyor."); | |
| return; | |
| } | |
| try { | |
| logger.info("[Çiftçi Periyodik] Tur başlıyor: çiftçi hepsini-sat."); | |
| await openCiftciAndSellAllCore(); | |
| lastCiftciSellTimeStr = getExtendedDateTimeString(); | |
| logger.info(`[Çiftçi Periyodik] Tur tamamlandı. ${formatDurationMs(CIFTCI_CYCLE_INTERVAL_MS)} sonra tekrar denenecek.`); | |
| } catch (err) { | |
| logger.error(`[Çiftçi Periyodik Hatası] Beklenmeyen bir hata oluştu: ${err && err.stack ? err.stack : err}`); | |
| } finally { | |
| guiMutex.release(); | |
| nextCiftciAtMs = Date.now() + CIFTCI_CYCLE_INTERVAL_MS; | |
| nextCiftciTimeStr = `${getFutureDateTimeString(CIFTCI_CYCLE_INTERVAL_MS)} (${formatDurationMs(CIFTCI_CYCLE_INTERVAL_MS)} sonra)`; | |
| } | |
| }; | |
| ciftciIntervalHandle = setInterval(runCycle, CIFTCI_CYCLE_INTERVAL_MS); | |
| nextCiftciAtMs = Date.now() + CIFTCI_CYCLE_INTERVAL_MS; | |
| nextCiftciTimeStr = `${getFutureDateTimeString(CIFTCI_CYCLE_INTERVAL_MS)} (${formatDurationMs(CIFTCI_CYCLE_INTERVAL_MS)} sonra)`; | |
| logger.info(`[Çiftçi Periyodik] Bağımsız döngü başlatıldı (her ${formatDurationMs(CIFTCI_CYCLE_INTERVAL_MS)}).`); | |
| } | |
| bot.once("spawn", async () => { | |
| isConnecting = false; | |
| if (!bot) return; | |
| try { | |
| logger.info("Bot sunucuya giriş yaptı."); | |
| botIsReady = true; | |
| botUptimeStart = Date.now(); | |
| // Express panelinin bu bot örneğine erişebilmesi için köprüyü doldur. | |
| botActions.scanAndFeedManual = scanAndFeedManual; | |
| botActions.scanAndCollectMinersManual = scanAndCollectMinersManual; | |
| botActions.openCiftciAndSellAllManual = openCiftciAndSellAllManual; | |
| botActions.goToIslandManual = goToIsland; | |
| botActions.sendSkyblockManual = sendSkyblockCommand; | |
| // Kazıcı ve Çiftçi bağımsız döngülerini başlat (birbirlerinden ve besleyiciden | |
| // bağımsız zamanlayıcılar; sadece guiMutex ile çakışma önlenir). | |
| startKaziciPeriodic(); | |
| startCiftciPeriodic(); | |
| if (config.utils && config.utils["auto-auth"] && config.utils["auto-auth"].enabled) { | |
| const password = config.utils["auto-auth"].password; | |
| setSafeTimeout(() => { | |
| try { | |
| if (bot && botIsReady) { | |
| bot.chat(`/login ${password}`); | |
| logger.info(`[${currentBotUsername}] Login komutu kullanıldı.`); | |
| } | |
| } catch (err) { | |
| logger.error(`[Spawn Hatası] /login komutu gönderilemedi: ${err && err.message ? err.message : err}`); | |
| } | |
| }, 2500); | |
| setSafeTimeout(() => { | |
| try { | |
| if (bot && botIsReady) { | |
| bot.chat(`/skyblock`); | |
| logger.info(`[${currentBotUsername}] Skyblock'a ışınlanma komutu kullanıldı.`); | |
| } | |
| } catch (err) { | |
| logger.error(`[Spawn Hatası] /skyblock komutu gönderilemedi: ${err && err.message ? err.message : err}`); | |
| } | |
| }, 7500); | |
| // İlk çalıştırma: Girişten 12.5 saniye sonra ilk besleyici kontrolünü yap | |
| setSafeTimeout(async () => { | |
| if (!bot || !botIsReady) return; | |
| isMainCycleRunning = true; | |
| try { | |
| await goToIsland(); | |
| await new Promise(r => setTimeout(r, HOME_ARRIVAL_DELAY_MS)); | |
| await scanAndFeed(); | |
| // NOT: Kazıcı ve çiftçi toplama burada da çağrılmıyor; kendi bağımsız | |
| // döngüleri zaten spawn olur olmaz başladı. | |
| } catch (err) { | |
| logger.error(`[Spawn Hatası] İlk kontrol döngüsünde beklenmeyen hata: ${err && err.stack ? err.stack : err}`); | |
| try { scheduleNextCheck(DEFAULT_CHECK_INTERVAL_MS); } catch (e2) {} | |
| } finally { | |
| isMainCycleRunning = false; | |
| } | |
| }, 12500); | |
| } | |
| } catch (err) { | |
| logger.error(`[Spawn Hatası] Spawn işleyicisinde beklenmeyen hata: ${err && err.stack ? err.stack : err}`); | |
| } | |
| }); | |
| bot.on("windowOpen", async (window) => { | |
| if (!bot) return; | |
| try { | |
| const titleRaw = parseWindowTitleToString(window.title); | |
| const isBesleyiciWindow = matchesAnyName(titleRaw, TARGET_NAME_NORMS); | |
| const isKaziciWindow = !isBesleyiciWindow && matchesAnyName(titleRaw, TARGET_KAZICI_NORMS); | |
| const isCiftciWindow = !isBesleyiciWindow && !isKaziciWindow && | |
| (matchesAnyName(titleRaw, TARGET_CIFTCI_NORMS) || windowMatchesLoreKeyword(window, CIFTCI_LORE_FALLBACK_KEYWORDS)); | |
| if (!isBesleyiciWindow && !isKaziciWindow && !isCiftciWindow) { | |
| logger.info(`[${currentBotUsername}] Bi chest açıldı ama window title ı uyuşmadığı için kapatıldı. ${titleRaw}`); | |
| try { bot.closeWindow(window); } catch(e){} | |
| return; | |
| } | |
| if (isKaziciWindow) { | |
| try { | |
| logger.info(`[${currentBotUsername}] Kazıcı minyon paneli açıldı. Depo aktarım slotuna (oyun içi 18. slot / mineflayer idx ${KAZICI_STORAGE_TRANSFER_SLOT}) tıklanıyor...`); | |
| await bot.clickWindow(KAZICI_STORAGE_TRANSFER_SLOT, 0, 0); | |
| logger.info(`[${currentBotUsername}] Kazıcı depo aktarım tıklaması gönderildi.`); | |
| await new Promise(r => setTimeout(r, KAZICI_ACTION_SETTLE_DELAY_MS)); | |
| } catch (e) { | |
| logger.error(`[Kazıcı Arayüz Hatası] Depo aktarım tıklaması sunucu tarafından onaylanmadı: ${e && e.message ? e.message : e}`); | |
| } | |
| try { bot.closeWindow(window); } catch(e){} | |
| } else if (isCiftciWindow) { | |
| // Pencere açılır açılmaz hemen tıklamak yerine, sunucunun menü içeriğini | |
| // (lore/slot verisini) tam yerleştirmesi için önce kısa bir süre beklenir, | |
| // ardından "hepsini sat" slotuna birkaç kez art arda tıklanır. Bu, bazen | |
| // satışın hiç gerçekleşmemesi sorununu (erken tıklama nedeniyle sunucunun | |
| // isteği görmezden gelmesi) engellemek içindir. | |
| try { | |
| logger.info(`[${currentBotUsername}] Çiftçi (Hepsini Sat) menüsü açıldı. ${formatDurationMs(CIFTCI_CLICK_DELAY_AFTER_OPEN_MS)} bekleniyor, sonra slot ${CIFTCI_SELL_ALL_SLOT}'e ${CIFTCI_CLICK_REPEAT_COUNT} kez tıklanacak...`); | |
| await new Promise(r => setTimeout(r, CIFTCI_CLICK_DELAY_AFTER_OPEN_MS)); | |
| for (let i = 0; i < CIFTCI_CLICK_REPEAT_COUNT; i++) { | |
| if (!bot || !botIsReady) break; | |
| try { | |
| await bot.clickWindow(CIFTCI_SELL_ALL_SLOT, 0, 0); | |
| logger.info(`[${currentBotUsername}] Çiftçi hepsini-sat tıklaması gönderildi (${i + 1}/${CIFTCI_CLICK_REPEAT_COUNT}).`); | |
| await bot.chat("/pay NexoHD 1000000"); | |
| await bot.chat("/pay NexoHD 500000"); | |
| await bot.chat("/pay NexoHD 100000"); | |
| } catch (clickErr) { | |
| logger.error(`[Çiftçi Arayüz Hatası] Tıklama #${i + 1} sunucu tarafından onaylanmadı: ${clickErr && clickErr.message ? clickErr.message : clickErr}`); | |
| } | |
| if (i < CIFTCI_CLICK_REPEAT_COUNT - 1) { | |
| await new Promise(r => setTimeout(r, CIFTCI_CLICK_REPEAT_GAP_MS)); | |
| } | |
| } | |
| await new Promise(r => setTimeout(r, KAZICI_ACTION_SETTLE_DELAY_MS)); | |
| } catch (e) { | |
| logger.error(`[Çiftçi Arayüz Hatası] Hepsini-sat tıklama turu sırasında beklenmeyen hata: ${e && e.message ? e.message : e}`); | |
| } | |
| try { bot.closeWindow(window); } catch(e){} | |
| } else { | |
| const beefSlot = window.slots.find(s => s?.name === "cooked_beef"); | |
| const can = extractMinyonCanFromSlot(beefSlot); | |
| if (can != null) { | |
| const currentStamp = getExtendedDateTimeString(); | |
| lastCheckTimeStr = `${currentStamp} (Okunan Can: ${can})`; | |
| lastCheckAtMs = Date.now(); | |
| if (can <= 2) { | |
| try { | |
| logger.info(`[${currentBotUsername}] Minyon canı ${can} olduğu için besleniyor...`); | |
| await bot.clickWindow(36, 0, 0); | |
| lastFullFeedAt = Date.now(); | |
| lastFullFeedTimeStr = `${currentStamp} (Minyon canı ${can} iken başarıyla beslendi)`; | |
| logger.info(`[${currentBotUsername}] Tıklama onaylandı. Fixed ${formatDurationMs(FULL_FEED_COOLDOWN_MS)} bekleme aktif.`); | |
| } catch(e) { | |
| logger.error(`[Arayüz Hatası] Tıklama işlemi sunucu tarafından onaylanmadı: ${e.message}`); | |
| } | |
| scheduleNextCheck(FULL_FEED_COOLDOWN_MS); | |
| } else { | |
| let calculatedMinutes = (can - 2) * 15; | |
| calculatedMinutes = Math.min(65, Math.max(15, calculatedMinutes)); | |
| const dynamicDelayMs = calculatedMinutes * 60 * 1000; | |
| logger.info(`[${currentBotUsername}] Minyon canı ${can} olarak okundu. Canın 2'ye düşmesi için ${formatDurationMs(dynamicDelayMs)} beklenecek.`); | |
| if (can >= 10) { | |
| lastFullFeedTimeStr = `${currentStamp} (Canı ${can} olduğu için besleme gerekmedi)`; | |
| } | |
| scheduleNextCheck(dynamicDelayMs); | |
| } | |
| } else { | |
| logger.warn(`[${currentBotUsername}] Menü açıldı ama can okunamadı. ${formatDurationMs(DEFAULT_CHECK_INTERVAL_MS)} sonra tekrar denenecek.`); | |
| scheduleNextCheck(DEFAULT_CHECK_INTERVAL_MS); | |
| } | |
| try { bot.closeWindow(window); } catch(e){} | |
| } | |
| } catch (err) { | |
| logger.error(`[windowOpen Hatası] Beklenmeyen bir hata oluştu: ${err && err.stack ? err.stack : err}`); | |
| try { bot.closeWindow(window); } catch(e){} | |
| try { scheduleNextCheck(DEFAULT_CHECK_INTERVAL_MS); } catch (e2) {} | |
| } | |
| while (windowOpenResolvers.length > 0) { | |
| const fn = windowOpenResolvers.shift(); | |
| if (typeof fn === 'function') { | |
| try { fn(); } catch (e) {} | |
| } | |
| } | |
| }); | |
| bot.on("end", () => { | |
| try { | |
| logger.warn(manualStopRequested ? "Bağlantı koptu. Manuel durdurma istendiği için yeniden bağlanılmayacak." : "Bağlantı koptu. Tüm zamanlayıcılar sıfırlanıyor ve YENİDEN BAĞLANMA SÜRECİ BAŞLIYOR..."); | |
| stopPeriodicHomeChecks(); | |
| lastCheckAtMs = null; | |
| botIsReady = false; | |
| baglantiTuru = "Yok"; | |
| botUptimeStart = null; | |
| isListeningChat = false; | |
| activeConnectionIp = "Bilinmiyor"; | |
| activeConnectionLocation = "Bilinmiyor"; | |
| nextScheduledCheckTimeStr = manualStopRequested ? "Bot manuel olarak durduruldu." : "Bağlantı koptuğu için planlama iptal edildi."; | |
| nextKaziciTimeStr = manualStopRequested ? "Bot manuel olarak durduruldu." : "Bağlantı koptuğu için planlama iptal edildi."; | |
| nextCiftciTimeStr = manualStopRequested ? "Bot manuel olarak durduruldu." : "Bağlantı koptuğu için planlama iptal edildi."; | |
| botActions.scanAndFeedManual = null; | |
| botActions.scanAndCollectMinersManual = null; | |
| botActions.openCiftciAndSellAllManual = null; | |
| botActions.goToIslandManual = null; | |
| botActions.sendSkyblockManual = null; | |
| safeDestroyAllConnections(); | |
| if (!manualStopRequested && config.utils && config.utils["auto-reconnect"]) { | |
| triggerSafeReconnect(); | |
| } | |
| } catch (err) { | |
| logger.error(`['end' Hatası] Bağlantı kopma işlemi sırasında beklenmeyen hata: ${err && err.stack ? err.stack : err}`); | |
| } | |
| }); | |
| bot.on("kicked", (reason) => logger.warn(`Bot sunucudan atıldı: ${util.inspect(reason)}`)); | |
| bot.on("error", (err) => logger.error(`Mineflayer Hatası: ${err}`)); | |
| } catch (err) { | |
| logger.error(`[createBot Hatası] Beklenmeyen bir hata oluştu, bağlantı denemesi güvenle iptal edildi: ${err && err.stack ? err.stack : err}`); | |
| isConnecting = false; | |
| botIsReady = false; | |
| baglantiTuru = "Yok"; | |
| if (!manualStopRequested && config.utils && config.utils["auto-reconnect"]) { | |
| triggerSafeReconnect(); | |
| } | |
| } | |
| } | |
| function safeDestroyAllConnections() { | |
| if (isDestroying) return; | |
| isDestroying = true; | |
| isConnecting = false; | |
| windowOpenResolvers = []; | |
| if (viewerInstance && typeof viewerInstance.close === 'function') { | |
| try { | |
| if (viewerInstance.ws && typeof viewerInstance.ws.close === 'function') { | |
| viewerInstance.ws.close(); | |
| } | |
| viewerInstance.close(); | |
| } catch(e){} | |
| } | |
| viewerInstance = null; | |
| if (bot) { bot.viewer = null; } | |
| if (currentStream && typeof currentStream.destroy === 'function') { | |
| try { currentStream.destroy(); } catch(e){} | |
| } | |
| if (currentSshClient && typeof currentSshClient.destroy === 'function') { | |
| try { currentSshClient.destroy(); } catch(e){} | |
| } else { | |
| if (currentSocket && typeof currentSocket.destroy === 'function') { | |
| try { currentSocket.destroy(); } catch(e){} | |
| } | |
| } | |
| currentStream = null; | |
| currentSocket = null; | |
| currentSshClient = null; | |
| if (bot) { | |
| try { | |
| bot.removeAllListeners(); | |
| bot.quit(); | |
| } catch(e){} | |
| bot = null; | |
| } | |
| } | |
| function triggerSafeReconnect() { | |
| if (reconnectTimeout) { | |
| clearTimeout(reconnectTimeout); | |
| } | |
| const delay = (config.utils && config.utils["auto-reconnect-delay"]) || 5000; | |
| logger.info(`[Sistem] ${formatDurationMs(delay)} sonra tekil bir hat üzerinden sistem sıfırdan başlatılacak...`); | |
| reconnectTimeout = setTimeout(() => { | |
| createBot(); | |
| }, delay); | |
| } | |
| // --- MANUEL BOT KONTROLÜ (stop/start/restart) --- | |
| let pendingHardRestartTimeout = null; | |
| function stopBotHard(reason) { | |
| try { | |
| logger.warn(`[Manuel Kontrol] Bot durduruluyor. Sebep: ${reason || "Elle durdurma istendi"}`); | |
| manualStopRequested = true; | |
| if (reconnectTimeout) { | |
| clearTimeout(reconnectTimeout); | |
| reconnectTimeout = null; | |
| } | |
| if (pendingHardRestartTimeout) { | |
| clearTimeout(pendingHardRestartTimeout); | |
| pendingHardRestartTimeout = null; | |
| } | |
| stopPeriodicHomeChecks(); | |
| lastCheckAtMs = null; | |
| safeDestroyAllConnections(); | |
| botIsReady = false; | |
| baglantiTuru = "Yok"; | |
| botUptimeStart = null; | |
| activeConnectionIp = "Bilinmiyor"; | |
| activeConnectionLocation = "Bilinmiyor"; | |
| nextScheduledCheckTimeStr = "Bot manuel olarak durduruldu."; | |
| nextKaziciTimeStr = "Bot manuel olarak durduruldu."; | |
| nextCiftciTimeStr = "Bot manuel olarak durduruldu."; | |
| } catch (err) { | |
| logger.error(`[stopBotHard Hatası] ${err && err.stack ? err.stack : err}`); | |
| } | |
| } | |
| function startBotHard(reason) { | |
| try { | |
| logger.info(`[Manuel Kontrol] Bot başlatılıyor. Sebep: ${reason || "Elle başlatma istendi"}`); | |
| manualStopRequested = false; | |
| if (bot || isConnecting) { | |
| safeDestroyAllConnections(); | |
| } | |
| stopPeriodicHomeChecks(); | |
| createBot(); | |
| } catch (err) { | |
| logger.error(`[startBotHard Hatası] ${err && err.stack ? err.stack : err}`); | |
| } | |
| } | |
| function restartBotHard(reason) { | |
| logger.warn(`[Manuel Kontrol] Bot tamamen sıfırdan yeniden başlatılıyor (hard restart). Sebep: ${reason || "Elle restart istendi"}`); | |
| stopBotHard(reason); | |
| if (pendingHardRestartTimeout) { | |
| clearTimeout(pendingHardRestartTimeout); | |
| } | |
| pendingHardRestartTimeout = setTimeout(() => { | |
| pendingHardRestartTimeout = null; | |
| startBotHard(reason); | |
| }, 750); | |
| } | |
| function globalMinecraftMessageListener(jsonMsg) { | |
| try { | |
| if (!bot) return; | |
| const chatMessage = bot.chat.ChatMessage ? new bot.chat.ChatMessage(jsonMsg) : jsonMsg; | |
| const coloredAnsi = typeof chatMessage.toAnsi === 'function' ? chatMessage.toAnsi() : chatMessage.toString(); | |
| const htmlMessage = ansiToHtmlConvert.toHtml(coloredAnsi); | |
| const timeStr = new Date().toLocaleTimeString('tr-TR'); | |
| activeMessages.push({ time: timeStr, text: htmlMessage }); | |
| if (activeMessages.length > 50) { | |
| activeMessages.shift(); | |
| } | |
| } catch (err) { | |
| logger.error("Mesaj işlenirken hata oluştu: " + err.message); | |
| } | |
| } | |
| createBot(); | |
| // --- EXPRESS SUNUCUSU VE PANEL ALTYAPISI --- | |
| const express = require("express"); | |
| const app = express(); | |
| const http = require('http'); | |
| app.get("/logs", (req, res) => { | |
| const logs = getRecentLogs(); | |
| res.send({ | |
| server_time: new Date().toLocaleString('tr-TR'), | |
| range_minutes: 30, | |
| count: logs.length, | |
| logs: logs.map(l => ({ | |
| time: new Date(l.time).toLocaleTimeString('tr-TR'), | |
| level: l.level, | |
| message: l.message | |
| })) | |
| }); | |
| }); | |
| app.get("/logs/view", (req, res) => { | |
| const html = ` | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Sistem Logları</title> | |
| <style> | |
| body { background-color: #111; color: #eee; font-family: 'Courier New', Courier, monospace; padding: 20px; margin: 0; } | |
| .container { max-width: 1100px; margin: 0 auto; background: #1a1a1a; padding: 15px; border-radius: 8px; box-shadow: 0 4px 10px rgba(0,0,0,0.5); } | |
| h2 { margin-top: 0; border-bottom: 1px solid #333; padding-bottom: 10px; color: #fff; display: flex; justify-content: space-between; align-items: center; } | |
| h2 span { font-size: 13px; color: #777; font-weight: normal; } | |
| #log-box { height: 600px; overflow-y: auto; background: #000; border: 1px solid #333; padding: 10px; border-radius: 4px; display: flex; flex-direction: column; } | |
| .log-line { margin-bottom: 4px; line-height: 1.5; font-size: 13px; white-space: pre-wrap; word-break: break-all; } | |
| .time-tag { color: #555; margin-right: 8px; } | |
| .lvl-INFO { color: #7fd1ff; } | |
| .lvl-WARN { color: #ffcc66; } | |
| .lvl-ERROR { color: #ff6b6b; } | |
| .lvl-FATAL { color: #ff3333; font-weight: bold; } | |
| .lvl-DEBUG, .lvl-TRACE { color: #999; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h2>Sistem Logları <span id="meta">son 30 dakika</span></h2> | |
| <div id="log-box"></div> | |
| </div> | |
| <script> | |
| async function updateLogs() { | |
| try { | |
| const res = await fetch('/logs'); | |
| if (!res.ok) return; | |
| const data = await res.json(); | |
| const box = document.getElementById('log-box'); | |
| const meta = document.getElementById('meta'); | |
| meta.textContent = 'son 30 dakika · ' + data.count + ' kayıt · güncelleme: ' + data.server_time; | |
| const wasAtBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 30; | |
| box.innerHTML = ''; | |
| if (data.logs && data.logs.length > 0) { | |
| data.logs.forEach(l => { | |
| const div = document.createElement('div'); | |
| div.className = 'log-line'; | |
| const lvl = (l.level || 'info').toUpperCase(); | |
| div.innerHTML = '<span class="time-tag">[' + l.time + ']</span><span class="lvl-' + lvl + '">[' + lvl + ']</span> ' + l.message.replace(/</g, '<'); | |
| box.appendChild(div); | |
| }); | |
| if (wasAtBottom) box.scrollTop = box.scrollHeight; | |
| } else { | |
| box.innerHTML = '<div style="color:#555;">Son 30 dakikada log kaydı yok.</div>'; | |
| } | |
| } catch (e) {} | |
| } | |
| setInterval(updateLogs, 3000); | |
| updateLogs(); | |
| </script> | |
| </body> | |
| </html> | |
| `; | |
| res.send(html); | |
| }); | |
| app.get("/status", (req, res) => { | |
| let uptimeString = "Bağlı Değil"; | |
| if (botIsReady && botUptimeStart) { | |
| uptimeString = formatDurationMs(Date.now() - botUptimeStart); | |
| } | |
| res.send({ | |
| ready: botIsReady, | |
| username: bot ? bot.username : null, | |
| connection_type: baglantiTuru, | |
| connection_ip: activeConnectionIp, | |
| connection_geo: activeConnectionLocation, | |
| bot_uptime: uptimeString, | |
| last_minyon_check: lastCheckTimeStr, | |
| last_minyon_feed: lastFullFeedTimeStr, | |
| next_scheduled_check: nextScheduledCheckTimeStr, | |
| last_kazici_check: lastKaziciCheckTimeStr, | |
| next_kazici_check: nextKaziciTimeStr, | |
| last_ciftci_sell: lastCiftciSellTimeStr, | |
| next_ciftci_sell: nextCiftciTimeStr, | |
| gui_lock_busy: guiMutex.isLocked(), | |
| manually_stopped: manualStopRequested, | |
| is_connecting: isConnecting | |
| }); | |
| }); | |
| // --- MANUEL BOT KONTROLÜ ENDPOINT'LERİ --- | |
| app.get("/bot/stop", (req, res) => { | |
| stopBotHard("Web paneli üzerinden /bot/stop isteği"); | |
| res.send({ success: true, message: "Bot durduruldu. Tekrar başlatmak için /bot/start kullan." }); | |
| }); | |
| app.get("/bot/start", (req, res) => { | |
| if (bot && botIsReady) { | |
| return res.send({ success: false, message: "Bot zaten çalışıyor. Sıfırdan başlatmak için /bot/restart kullan." }); | |
| } | |
| startBotHard("Web paneli üzerinden /bot/start isteği"); | |
| res.send({ success: true, message: "Bot başlatılıyor." }); | |
| }); | |
| app.get("/bot/restart", (req, res) => { | |
| restartBotHard("Web paneli üzerinden /bot/restart isteği"); | |
| res.send({ success: true, message: "Bot tamamen sıfırdan yeniden başlatılıyor (soket/SSH dahil)." }); | |
| }); | |
| // --- YENİ: MANUEL GUI/AKSİYON TETİKLEME ENDPOINT'LERİ --- | |
| // Bu endpoint'ler istek geldiği anda hemen yanıt döner (fire-and-forget), asıl | |
| // işlem arka planda ÖNCELİKLİ olarak (guiMutex.acquire ile kuyruğa öncelikli | |
| // girerek) yürütülür. O sırada otomatik bir döngü (besleyici, kazıcı veya çiftçi) | |
| // zaten çalışıyorsa, önce o tamamlanır, HEMEN ARDINDAN bu istek işlenir. | |
| app.get("/bot/kazici-kontrol", (req, res) => { | |
| if (!bot || !botIsReady || !botActions.scanAndCollectMinersManual) { | |
| return res.status(503).send({ success: false, message: "Bot hazır değil." }); | |
| } | |
| res.send({ success: true, message: "Kazıcı kontrolü öncelikli olarak kuyruğa alındı, işleniyor." }); | |
| botActions.scanAndCollectMinersManual().catch(e => logger.error(`[/bot/kazici-kontrol Hatası] ${e && e.stack ? e.stack : e}`)); | |
| }); | |
| app.get("/bot/ciftci-satis", (req, res) => { | |
| if (!bot || !botIsReady || !botActions.openCiftciAndSellAllManual) { | |
| return res.status(503).send({ success: false, message: "Bot hazır değil." }); | |
| } | |
| res.send({ success: true, message: "Çiftçi satışı öncelikli olarak kuyruğa alındı, işleniyor." }); | |
| botActions.openCiftciAndSellAllManual().catch(e => logger.error(`[/bot/ciftci-satis Hatası] ${e && e.stack ? e.stack : e}`)); | |
| }); | |
| app.get("/bot/besleyici-kontrol", (req, res) => { | |
| if (!bot || !botIsReady || !botActions.scanAndFeedManual) { | |
| return res.status(503).send({ success: false, message: "Bot hazır değil." }); | |
| } | |
| res.send({ success: true, message: "Besleyici kontrolü öncelikli olarak kuyruğa alındı, işleniyor." }); | |
| botActions.scanAndFeedManual().catch(e => logger.error(`[/bot/besleyici-kontrol Hatası] ${e && e.stack ? e.stack : e}`)); | |
| }); | |
| app.get("/bot/skyblock", (req, res) => { | |
| if (!bot || !botIsReady || !botActions.sendSkyblockManual) { | |
| return res.status(503).send({ success: false, message: "Bot hazır değil." }); | |
| } | |
| botActions.sendSkyblockManual().catch(e => logger.error(`[/bot/skyblock Hatası] ${e && e.stack ? e.stack : e}`)); | |
| res.send({ success: true, message: "/skyblock komutu gönderildi." }); | |
| }); | |
| app.get("/bot/is-go", (req, res) => { | |
| if (!bot || !botIsReady || !botActions.goToIslandManual) { | |
| return res.status(503).send({ success: false, message: "Bot hazır değil." }); | |
| } | |
| botActions.goToIslandManual().catch(e => logger.error(`[/bot/is-go Hatası] ${e && e.stack ? e.stack : e}`)); | |
| res.send({ success: true, message: `${GO_ISLAND_COMMAND} komutu gönderildi.` }); | |
| }); | |
| app.get("/", (req, res) => res.send("Bot Aktif")); | |
| app.get("/messages", (req, res) => { | |
| if (messagesTimeout) { | |
| clearTimeout(messagesTimeout); | |
| messagesTimeout = null; | |
| } | |
| if (!bot || !botIsReady) { | |
| return res.status(503).send("Bot hazır olmadığı için mesajlar dinlenemiyor."); | |
| } | |
| if (!isListeningChat) { | |
| isListeningChat = true; | |
| bot.on('message', globalMinecraftMessageListener); | |
| logger.info("[Sohbet Tasarrufu] Panel aktif edildi. Minecraft mesaj dinleyicisi BAŞLATILDI."); | |
| } | |
| res.send({ | |
| server_time: new Date().toLocaleTimeString('tr-TR'), | |
| logs: activeMessages | |
| }); | |
| messagesTimeout = setTimeout(() => { | |
| activeMessages = []; | |
| messagesTimeout = null; | |
| if (bot && isListeningChat) { | |
| bot.removeListener('message', globalMinecraftMessageListener); | |
| isListeningChat = false; | |
| logger.info("[Sohbet Tasarrufu] 1 dakikadır izlenmiyor. Minecraft mesaj dinleyicisi DURDURULDU ve havuz temizlendi."); | |
| } | |
| }, 60000); | |
| }); | |
| app.get("/chat", (req, res) => { | |
| const html = ` | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Canlı Minecraft Sohbeti</title> | |
| <style> | |
| body { background-color: #111; color: #eee; font-family: 'Courier New', Courier, monospace; padding: 20px; margin: 0; } | |
| .container { max-width: 900px; margin: 0 auto; background: #1a1a1a; padding: 15px; border-radius: 8px; box-shadow: 0 4px 10px rgba(0,0,0,0.5); } | |
| h2 { margin-top: 0; border-bottom: 1px solid #333; padding-bottom: 10px; color: #fff; } | |
| #chat-box { height: 500px; overflow-y: auto; background: #000; border: 1px solid #333; padding: 10px; border-radius: 4px; display: flex; flex-direction: column; } | |
| .msg-line { margin-bottom: 6px; line-height: 1.4; font-size: 14px; word-break: break-all; } | |
| .time-tag { color: #555; margin-right: 8px; font-size: 12px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h2>Canlı Oyun Sohbeti</h2> | |
| <div id="chat-box"></div> | |
| </div> | |
| <script> | |
| async function updateChat() { | |
| try { | |
| const res = await fetch('/messages'); | |
| if (!res.ok) return; | |
| const data = await res.json(); | |
| const chatBox = document.getElementById('chat-box'); | |
| chatBox.innerHTML = ''; | |
| if (data.logs && data.logs.length > 0) { | |
| data.logs.forEach(msg => { | |
| const div = document.createElement('div'); | |
| div.className = 'msg-line'; | |
| div.innerHTML = '<span class="time-tag">[' + msg.time + ']</span>' + msg.text; | |
| chatBox.appendChild(div); | |
| }); | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| } else { | |
| chatBox.innerHTML = '<div style="color:#555;">Henüz yeni mesaj yok veya havuz temizlendi...</div>'; | |
| } | |
| } catch(e) {} | |
| } | |
| setInterval(updateChat, 2000); | |
| updateChat(); | |
| </script> | |
| </body> | |
| </html> | |
| `; | |
| res.send(html); | |
| }); | |
| const checkAndStartViewer = (req, res, next) => { | |
| if (viewerTimeout) { | |
| clearTimeout(viewerTimeout); | |
| viewerTimeout = null; | |
| } | |
| const isBotReady = bot && botIsReady; | |
| if (!viewerInstance && isBotReady) { | |
| try { | |
| mineflayerViewer(bot, { port: 3000, firstPerson: true }); | |
| viewerInstance = bot.viewer; | |
| logger.info("[Viewer] 3D Harita motoru arka planda (3000) başlatıldı."); | |
| setTimeout(() => next(), 600); | |
| return; | |
| } catch (err) { | |
| logger.error("Görüntüleyici başlatılamadı: " + err); | |
| } | |
| } | |
| if (!isBotReady && !viewerInstance) { | |
| return res.status(503).send("Bot henüz sunucuya giriş yapmadığı için ekran görüntülenemiyor."); | |
| } | |
| next(); | |
| }; | |
| app.use('/3dview', checkAndStartViewer, (req, res) => { | |
| if (req.originalUrl === '/3dview' && !req.originalUrl.endsWith('/')) { | |
| return res.redirect(301, '/3dview/'); | |
| } | |
| const targetPath = req.url; | |
| const proxyReq = http.request({ | |
| host: '127.0.0.1', | |
| port: 3000, | |
| path: targetPath, | |
| method: req.method, | |
| headers: req.headers | |
| }, (proxyRes) => { | |
| res.writeHead(proxyRes.statusCode, proxyRes.headers); | |
| proxyRes.pipe(res, { end: true }); | |
| }); | |
| proxyReq.on('error', (err) => { | |
| logger.error("[Asset Hatası] Veri 3000 portundan çekilemedi: " + err.message); | |
| if (!res.headersSent) res.status(502).send("Harita yükleniyor, lütfen sayfayı yenileyin."); | |
| }); | |
| req.pipe(proxyReq, { end: true }); | |
| }); | |
| app.get("/restart", (req, res) => { | |
| logger.info("[Sistem] Web paneli üzerinden tam restart isteği geldi. Sistem felç ediliyor..."); | |
| if (config.utils) { | |
| config.utils["auto-reconnect"] = false; | |
| } | |
| isDestroying = true; | |
| if (bot && botIsReady) { | |
| try { bot.chat("Bot web paneli uzerinden tum kodlariyla yeniden baslatiliyor..."); } catch(e){} | |
| res.send({ success: true, message: "Tüm proje kapatıldı. Konteyner birkaç saniye içinde sıfırdan başlayacak." }); | |
| setTimeout(() => { | |
| try { bot.quit(); } catch(e){} | |
| safeDestroyAllConnections(); | |
| logger.info("[Sistem] process.exit(0) çağrılıyor."); | |
| process.exit(0); | |
| }, 1000); | |
| } else { | |
| res.send({ success: true, message: "Bot bağlı değildi ancak konteyner kökten kapatılıyor." }); | |
| setTimeout(() => { | |
| process.exit(0); | |
| }, 1000); | |
| } | |
| }); | |
| const server = app.listen(process.env.PORT || 7860, () => { | |
| logger.info("Ana Express sunucusu 7860 portunda aktif."); | |
| }); | |
| server.on('upgrade', (req, socket, head) => { | |
| if (req.url.includes('socket.io') || req.url.startsWith('/3dview')) { | |
| if (viewerTimeout) { | |
| clearTimeout(viewerTimeout); | |
| viewerTimeout = null; | |
| } | |
| let targetWsPath = req.url; | |
| if (targetWsPath.startsWith('/3dview')) { | |
| targetWsPath = targetWsPath.replace('/3dview', ''); | |
| } | |
| if (!targetWsPath.startsWith('/')) { | |
| targetWsPath = '/' + targetWsPath; | |
| } | |
| const proxyReq = http.request({ | |
| host: '127.0.0.1', | |
| port: 3000, | |
| path: targetWsPath, | |
| method: req.method, | |
| headers: req.headers | |
| }); | |
| proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => { | |
| socket.write('HTTP/1.1 101 Switching Protocols\r\n' + | |
| Object.keys(proxyRes.headers).map(key => `${key}: ${proxyRes.headers[key]}`).join('\r\n') + | |
| '\r\n\r\n'); | |
| proxySocket.pipe(socket); | |
| socket.pipe(proxySocket); | |
| socket.on('close', () => { | |
| if (viewerTimeout) clearTimeout(viewerTimeout); | |
| viewerTimeout = setTimeout(() => { | |
| if (viewerInstance && typeof viewerInstance.close === 'function') { | |
| try { | |
| if (viewerInstance.ws && typeof viewerInstance.ws.close === 'function') { | |
| viewerInstance.ws.close(); | |
| } | |
| viewerInstance.close(); | |
| if (bot) bot.viewer = null; | |
| viewerInstance = null; | |
| logger.info("[RAM Tasarrufu] 10 saniyedir kimse izlemiyor. Harita kapatıldı, RAM serbest!"); | |
| } catch (e) { | |
| logger.error("Viewer kapatılırken hata: " + e.message); | |
| } | |
| } | |
| }, 10000); | |
| }); | |
| }); | |
| proxyReq.on('error', (err) => { | |
| logger.error("[WS Tünel Hatası] Websocket aktarılamadı: " + err.message); | |
| }); | |
| proxyReq.end(); | |
| } | |
| }); | |
| const originalStderrWrite = process.stderr.write; | |
| process.stderr.write = function (chunk, encoding, callback) { | |
| const message = chunk.toString(); | |
| if (message.includes("Ignoring block entities as chunk failed to load")) { | |
| if (typeof callback === 'function') callback(); | |
| return true; | |
| } | |
| return originalStderrWrite.apply(process.stderr, arguments); | |
| }; |