211 lines
8.7 KiB
JavaScript
211 lines
8.7 KiB
JavaScript
// ================================================================
|
||
// 05_main.js — Poll loop, command dispatch, boot
|
||
// Depends on: everything above
|
||
// ================================================================
|
||
|
||
// Shared farm state — prevents auto-farm and explicit farm_loot commands
|
||
// from running concurrently. Also caches last-known farm settings so the
|
||
// auto-farm loop doesn't need its own API call.
|
||
let farmLootRunning = false;
|
||
let lastKnownFarmSettings = {};
|
||
|
||
// ----------------------------------------------------------------
|
||
// pollAndExecute — runs every 8–18 s (main command loop)
|
||
// Handles builds, recruits, market, research, explicit farm commands.
|
||
// Auto-farm has its own separate loop below.
|
||
// ----------------------------------------------------------------
|
||
async function pollAndExecute() {
|
||
if (paused) return;
|
||
const player_id = uw.Game?.player_id;
|
||
if (!player_id) return;
|
||
|
||
let cmdData;
|
||
try {
|
||
const res = await apiFetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}`);
|
||
cmdData = await res.json();
|
||
} catch (e) {
|
||
log(`Poll failed: ${e}`);
|
||
return;
|
||
}
|
||
|
||
// Cache farm + bot settings so autonomous loops can read them without extra calls
|
||
lastKnownFarmSettings = cmdData.farm_settings || {};
|
||
lastKnownBotSettings = cmdData.bot_settings || {};
|
||
|
||
// Feature flags — default to all on if server doesn't send them (backward compatible)
|
||
const features = cmdData.enabled_features || ['farm', 'admin'];
|
||
const farmOn = features.includes('farm');
|
||
const adminOn = features.includes('admin');
|
||
|
||
// Build: one command per town (server returns an array)
|
||
const buildCmds = adminOn ? (cmdData.builds || []) : [];
|
||
const recruitCmd = adminOn ? cmdData.recruit : null;
|
||
const marketCmd = adminOn ? cmdData.market : null;
|
||
const researchCmd = adminOn ? cmdData.research : null;
|
||
const farmCmd = farmOn ? cmdData.farm : null;
|
||
const farmUpgradeCmd = farmOn ? cmdData.farm_upgrade : null;
|
||
|
||
if (cmdData.sync_requested) {
|
||
log('Sync requested by server — pushing state immediately');
|
||
pushState();
|
||
}
|
||
|
||
const execute = async (cmd) => {
|
||
if (!cmd) return;
|
||
log(`Executing command #${cmd.id} — type:${cmd.type} town:${cmd.town_id}`);
|
||
if (paused) {
|
||
log(`[Paused] Ignoring command #${cmd.id}`);
|
||
return;
|
||
}
|
||
|
||
let result;
|
||
try {
|
||
if (cmd.type === 'build') result = await executeBuild(cmd);
|
||
else if (cmd.type === 'recruit') result = await executeRecruit(cmd);
|
||
else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd);
|
||
else if (cmd.type === 'research') result = await executeResearch(cmd);
|
||
else if (cmd.type === 'farm_upgrade') result = await executeFarmUpgrade(cmd);
|
||
else if (cmd.type === 'farm_loot') {
|
||
// Guard: if auto-farm is mid-run, requeue rather than overlap
|
||
if (farmLootRunning) {
|
||
result = { ok: false, requeue: true, msg: 'Auto-farm in progress — requeueing' };
|
||
} else {
|
||
farmLootRunning = true;
|
||
try { result = await executeFarmLoot(cmd); }
|
||
finally { farmLootRunning = false; }
|
||
}
|
||
}
|
||
else result = { ok: false, msg: `Unknown type: ${cmd.type}` };
|
||
} catch (e) {
|
||
result = { ok: false, msg: `Exception: ${e}` };
|
||
}
|
||
|
||
const finalStatus = result.requeue ? 'pending' : (result.ok ? 'done' : 'failed');
|
||
log(`Command #${cmd.id}: ${finalStatus === 'done' ? '✅' : finalStatus === 'pending' ? '⏳' : '❌'} ${result.msg}`);
|
||
reportResult(cmd.id, finalStatus, result.msg);
|
||
};
|
||
|
||
// Execute one build command per town (simultaneous queue draining across all villages)
|
||
for (let i = 0; i < buildCmds.length; i++) {
|
||
await execute(buildCmds[i]);
|
||
if (i < buildCmds.length - 1) {
|
||
// Random inter-town gap — avoids looking like a macro
|
||
const gap = randInt(1500, 3000);
|
||
log(`Build: town done. Waiting ${gap}ms before next town...`);
|
||
await sleep(gap);
|
||
}
|
||
}
|
||
await execute(recruitCmd);
|
||
await execute(marketCmd);
|
||
await execute(researchCmd);
|
||
await execute(farmCmd);
|
||
await execute(farmUpgradeCmd);
|
||
}
|
||
|
||
|
||
// ----------------------------------------------------------------
|
||
// autoFarmLoop — runs every 60–120 s (independent of main poll)
|
||
// Checks warehouse capacity and loots ready farms automatically.
|
||
// Completely decoupled from pollAndExecute so builds/recruits
|
||
// are never blocked by the long inter-island farm delays.
|
||
// ----------------------------------------------------------------
|
||
async function autoFarmLoop() {
|
||
if (paused) return;
|
||
|
||
const farmSettings = lastKnownFarmSettings;
|
||
if (!farmSettings.enabled) return;
|
||
|
||
// Don't overlap with an explicit farm_loot command running in main loop
|
||
if (farmLootRunning) {
|
||
log('Auto-farm: explicit farm_loot in progress — skipping this cycle');
|
||
return;
|
||
}
|
||
|
||
// Check if any farms are actually ready before doing anything heavy
|
||
const nowTs = Math.floor(Date.now() / 1000);
|
||
let readyFarms = [];
|
||
try {
|
||
const coll = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation');
|
||
readyFarms = coll?.models?.filter(r =>
|
||
r.attributes.relation_status === 1 &&
|
||
(r.attributes.lootable_at || 0) <= nowTs
|
||
) || [];
|
||
} catch (e) { return; }
|
||
|
||
if (readyFarms.length === 0) return;
|
||
log(`⚡ Auto-farm: ${readyFarms.length} ready farms found`);
|
||
|
||
// Check if ALL warehouses are already full (>95%) — no point looting
|
||
const towns = Object.values(uw.ITowns?.towns || {});
|
||
let allFull = true;
|
||
for (const town of towns) {
|
||
const res = town.resources?.() || {};
|
||
let storage = town.getStorageCapacity?.() || 0;
|
||
if (!storage) {
|
||
const buildings = town.buildings?.()?.attributes || {};
|
||
const storageLevel = buildings.storage ?? 0;
|
||
const gd = uw.GameData?.buildingData?.storage;
|
||
storage = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0;
|
||
}
|
||
if (!storage) storage = res.capacity || res.storage_capacity || res.storage || 0;
|
||
if (!storage) continue;
|
||
const maxRes = Math.max(res.wood || 0, res.stone || 0, res.iron || 0);
|
||
if (maxRes / storage < 0.95) { allFull = false; break; }
|
||
}
|
||
|
||
const player_id = uw.Game?.player_id;
|
||
if (allFull) {
|
||
log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle');
|
||
try {
|
||
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${player_id}`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ warehouse_full: true })
|
||
});
|
||
} catch (e) {}
|
||
return;
|
||
}
|
||
|
||
// All clear — run the loot
|
||
farmLootRunning = true;
|
||
try {
|
||
await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } });
|
||
// Report success so dashboard shows last_farmed_at
|
||
try {
|
||
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${player_id}`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ warehouse_full: false })
|
||
});
|
||
} catch (e) {}
|
||
pushState();
|
||
} finally {
|
||
farmLootRunning = false;
|
||
}
|
||
}
|
||
|
||
|
||
// ----------------------------------------------------------------
|
||
// Boot — works whether page is already loaded or not.
|
||
// When eval()'d dynamically the 'load' event has already fired,
|
||
// so we check readyState and boot immediately in that case.
|
||
// ----------------------------------------------------------------
|
||
function boot() {
|
||
log('Grepolis Remote Control v4.2.0 (remote) loaded');
|
||
detectCaptcha();
|
||
setTimeout(pushState, 5000);
|
||
jitterLoop(pushState, 60000, 120000); // state sync every 1–2 min
|
||
jitterLoop(pollAndExecute, 8000, 18000); // command poll every 8–18 s
|
||
jitterLoop(autoFarmLoop, 60000, 120000); // auto-farm every 1–2 min
|
||
jitterLoop(autoBootcampLoop, 720000, 1320000); // bootcamp every 12–22 min
|
||
jitterLoop(autoRuralTradeLoop, 1500000, 2700000); // rural trade every 25–45 min
|
||
}
|
||
|
||
if (document.readyState === 'complete') {
|
||
// Page already loaded (normal case when eval()'d dynamically)
|
||
boot();
|
||
} else {
|
||
// Fallback: wait for load event (shouldn't happen but safe to keep)
|
||
window.addEventListener('load', boot);
|
||
}
|