From 0643422a30af463c3cc0a9342b8e9b9914261144 Mon Sep 17 00:00:00 2001 From: haunter Date: Wed, 29 Apr 2026 23:22:25 +0300 Subject: [PATCH] enchance farming/fix --- bot_modules/05_main.js | 168 +++++++++++++++++++++++++---------------- routes/api.py | 27 ++++++- routes/dashboard.py | 8 +- templates/farm.html | 24 +++++- 4 files changed, 154 insertions(+), 73 deletions(-) diff --git a/bot_modules/05_main.js b/bot_modules/05_main.js index 8d9fb03..2c656d3 100644 --- a/bot_modules/05_main.js +++ b/bot_modules/05_main.js @@ -3,6 +3,17 @@ // 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; @@ -17,13 +28,16 @@ async function pollAndExecute() { return; } + // Cache farm settings so autoFarmLoop can read them without an extra call + lastKnownFarmSettings = cmdData.farm_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 buildCmds = adminOn ? (cmdData.builds || []) : []; const recruitCmd = adminOn ? cmdData.recruit : null; const marketCmd = adminOn ? cmdData.market : null; const researchCmd = adminOn ? cmdData.research : null; @@ -49,8 +63,17 @@ async function pollAndExecute() { 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_loot') result = await executeFarmLoot(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}` }; @@ -76,88 +99,103 @@ async function pollAndExecute() { await execute(researchCmd); await execute(farmCmd); await execute(farmUpgradeCmd); +} - // Auto-farm: only if farm feature is enabled - const farmSettings = cmdData.farm_settings || {}; - if (farmOn && farmSettings.enabled && !farmCmd) { - 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) { /* silent */ } - if (readyFarms.length > 0) { - let allFull = true; - let claimedAny = false; +// ---------------------------------------------------------------- +// 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 towns = Object.values(uw.ITowns?.towns || {}); - for (const town of towns) { - // Use same multi-strategy lookup as gatherState() — res.storage is often 0 in Grepolis - 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; + const farmSettings = lastKnownFarmSettings; + if (!farmSettings.enabled) return; - const wood = res.wood || 0; - const stone = res.stone || 0; - const iron = res.iron || 0; - if (!storage) continue; + // 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; + } - const maxRes = Math.max(wood, stone, iron); - const pct = maxRes / storage; + // 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 (pct < 0.95) { - allFull = false; - log(`⚡ Auto-farm: looting into town ${town.get?.('name')} (${Math.round(pct * 100)}% full)`); - await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } }); - claimedAny = true; - pushState(); - break; - } - } + if (readyFarms.length === 0) return; + log(`⚡ Auto-farm: ${readyFarms.length} ready farms found`); - if (allFull) { - log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle'); - try { - await apiFetch(`${BASE_URL}/api/farm_status?player_id=${uw.Game.player_id}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ warehouse_full: true }) - }); - } catch (e) {} - } else if (claimedAny) { - try { - await apiFetch(`${BASE_URL}/api/farm_status?player_id=${uw.Game.player_id}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ warehouse_full: false }) - }); - } catch (e) {} - } + // 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.0.0 (remote) loaded'); + log('Grepolis Remote Control v4.1.0 (remote) loaded'); detectCaptcha(); setTimeout(pushState, 5000); - jitterLoop(pushState, 60000, 120000); - jitterLoop(pollAndExecute, 8000, 18000); + 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 (independent) } if (document.readyState === 'complete') { diff --git a/routes/api.py b/routes/api.py index 7763d09..5774002 100644 --- a/routes/api.py +++ b/routes/api.py @@ -256,15 +256,29 @@ def sync_request(): @api.route('/api/commands//result', methods=['POST']) def command_result(cmd_id): data = request.get_json(silent=True) or {} - status = data.get('status', 'done') # 'done' | 'failed' + status = data.get('status', 'done') # 'done' | 'failed' | 'pending' (requeue) msg = data.get('message', '') + now = datetime.utcnow().isoformat() conn = get_db() + # Look up type + player_id for post-update hooks + cmd = conn.execute( + 'SELECT type, player_id FROM commands WHERE id = ?', (cmd_id,) + ).fetchone() + conn.execute(''' UPDATE commands SET status = ?, result_msg = ?, updated_at = ? WHERE id = ? - ''', (status, msg, datetime.utcnow().isoformat(), cmd_id)) + ''', (status, msg, now, cmd_id)) + + # When an explicit farm_loot command succeeds, record the timestamp + if cmd and cmd['type'] == 'farm_loot' and status == 'done' and cmd['player_id']: + conn.execute(''' + INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at + ''', (f'last_farmed_{cmd["player_id"]}', now, now)) + conn.commit() conn.close() return jsonify({'ok': True}) @@ -334,10 +348,17 @@ def farm_status(): conn = get_db() if request.method == 'POST': data = request.get_json(silent=True) or {} + now = datetime.utcnow().isoformat() conn.execute(''' INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at - ''', (kv_key, json.dumps(data), datetime.utcnow().isoformat())) + ''', (kv_key, json.dumps(data), now)) + # Auto-farm reports warehouse_full=false when it successfully looted something + if not data.get('warehouse_full', True): + conn.execute(''' + INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at + ''', (f'last_farmed_{player_id}', now, now)) conn.commit() conn.close() return jsonify({'ok': True}) diff --git a/routes/dashboard.py b/routes/dashboard.py index 57e2f2e..3eaa565 100644 --- a/routes/dashboard.py +++ b/routes/dashboard.py @@ -126,6 +126,12 @@ def get_farm_data(): rows = conn.execute( 'SELECT town_id, town_name, data FROM town_state WHERE player_id = ?', (player_id,) ).fetchall() + + # Also fetch when the bot last farmed + lf_row = conn.execute( + "SELECT value FROM kv_store WHERE key = ?", (f'last_farmed_{player_id}',) + ).fetchone() + last_farmed_at = lf_row['value'] if lf_row else None conn.close() now_ts = int(datetime.utcnow().timestamp()) @@ -142,7 +148,7 @@ def get_farm_data(): 'ready_farms': len(ready), 'next_ready_at': min((f['lootable_at'] for f in farm_data if f.get('lootable_at', 0) > now_ts and f.get('relation_status', 0) == 1), default=None) }) - return jsonify(farms_summary) + return jsonify({'towns': farms_summary, 'last_farmed_at': last_farmed_at}) # ------------------------------------------------------------------ diff --git a/templates/farm.html b/templates/farm.html index 22c2734..f0ac4e0 100644 --- a/templates/farm.html +++ b/templates/farm.html @@ -324,10 +324,11 @@ Έτοιμα Σύνολο Επόμενο + Τελευταία Λεηλασία -

Φόρτωση δεδομένων...

+

Φόρτωση δεδομένων...

@@ -395,17 +396,27 @@ } // -- Load farm data table -- + function timeAgo(isoStr) { + if (!isoStr) return '—'; + const diff = Math.floor((Date.now() - new Date(isoStr + (isoStr.endsWith('Z') ? '' : 'Z'))) / 1000); + if (diff < 60) return `${diff}δ πριν`; + if (diff < 3600) return `${Math.floor(diff / 60)}λ πριν`; + return `${Math.floor(diff / 3600)}ω πριν`; + } + function loadFarmData() { fetch(`/dashboard/farm-data?player_id=${PLAYER_ID}`) .then(r => r.json()) - .then(data => { + .then(resp => { + const data = resp.towns || []; + const lastFarmed = resp.last_farmed_at ? timeAgo(resp.last_farmed_at) : '—'; const tbody = document.getElementById('farm-table-body'); if (!data || data.length === 0) { - tbody.innerHTML = '
🌱

Δεν υπάρχουν δεδομένα χωριών ακόμη.
Βεβαιώσου ότι το script v3.3+ τρέχει στο παιχνίδι.

'; + tbody.innerHTML = '
🌱

Δεν υπάρχουν δεδομένα χωριών ακόμη.
Βεβαιώσου ότι το script v3.3+ τρέχει στο παιχνίδι.

'; return; } const now = Math.floor(Date.now() / 1000); - tbody.innerHTML = data.map(t => { + tbody.innerHTML = data.map((t, idx) => { const readyBadge = t.ready_farms > 0 ? `✓ ${t.ready_farms} Έτοιμα` : `Αναμονή`; @@ -420,11 +431,16 @@ nextStr = 'Τώρα'; } } + // Show last farmed only in first row — same value for all rows + const lastFarmedCell = idx === 0 + ? `${lastFarmed}` + : ''; return ` ${t.town_name} ${readyBadge} ${t.total_farms} ${nextStr} + ${lastFarmedCell} `; }).join(''); });