enchance farming/fix
This commit is contained in:
@@ -3,6 +3,17 @@
|
|||||||
// Depends on: everything above
|
// 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() {
|
async function pollAndExecute() {
|
||||||
if (paused) return;
|
if (paused) return;
|
||||||
const player_id = uw.Game?.player_id;
|
const player_id = uw.Game?.player_id;
|
||||||
@@ -17,13 +28,16 @@ async function pollAndExecute() {
|
|||||||
return;
|
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)
|
// Feature flags — default to all on if server doesn't send them (backward compatible)
|
||||||
const features = cmdData.enabled_features || ['farm', 'admin'];
|
const features = cmdData.enabled_features || ['farm', 'admin'];
|
||||||
const farmOn = features.includes('farm');
|
const farmOn = features.includes('farm');
|
||||||
const adminOn = features.includes('admin');
|
const adminOn = features.includes('admin');
|
||||||
|
|
||||||
// Build: one command per town (server returns an array)
|
// 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 recruitCmd = adminOn ? cmdData.recruit : null;
|
||||||
const marketCmd = adminOn ? cmdData.market : null;
|
const marketCmd = adminOn ? cmdData.market : null;
|
||||||
const researchCmd = adminOn ? cmdData.research : 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 === 'recruit') result = await executeRecruit(cmd);
|
||||||
else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd);
|
else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd);
|
||||||
else if (cmd.type === 'research') result = await executeResearch(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_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}` };
|
else result = { ok: false, msg: `Unknown type: ${cmd.type}` };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result = { ok: false, msg: `Exception: ${e}` };
|
result = { ok: false, msg: `Exception: ${e}` };
|
||||||
@@ -76,88 +99,103 @@ async function pollAndExecute() {
|
|||||||
await execute(researchCmd);
|
await execute(researchCmd);
|
||||||
await execute(farmCmd);
|
await execute(farmCmd);
|
||||||
await execute(farmUpgradeCmd);
|
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;
|
// autoFarmLoop — runs every 60–120 s (independent of main poll)
|
||||||
let claimedAny = false;
|
// 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 || {});
|
const farmSettings = lastKnownFarmSettings;
|
||||||
for (const town of towns) {
|
if (!farmSettings.enabled) return;
|
||||||
// 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 wood = res.wood || 0;
|
// Don't overlap with an explicit farm_loot command running in main loop
|
||||||
const stone = res.stone || 0;
|
if (farmLootRunning) {
|
||||||
const iron = res.iron || 0;
|
log('Auto-farm: explicit farm_loot in progress — skipping this cycle');
|
||||||
if (!storage) continue;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const maxRes = Math.max(wood, stone, iron);
|
// Check if any farms are actually ready before doing anything heavy
|
||||||
const pct = maxRes / storage;
|
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) {
|
if (readyFarms.length === 0) return;
|
||||||
allFull = false;
|
log(`⚡ Auto-farm: ${readyFarms.length} ready farms found`);
|
||||||
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 (allFull) {
|
// Check if ALL warehouses are already full (>95%) — no point looting
|
||||||
log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle');
|
const towns = Object.values(uw.ITowns?.towns || {});
|
||||||
try {
|
let allFull = true;
|
||||||
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${uw.Game.player_id}`, {
|
for (const town of towns) {
|
||||||
method: 'POST',
|
const res = town.resources?.() || {};
|
||||||
headers: { 'Content-Type': 'application/json' },
|
let storage = town.getStorageCapacity?.() || 0;
|
||||||
body: JSON.stringify({ warehouse_full: true })
|
if (!storage) {
|
||||||
});
|
const buildings = town.buildings?.()?.attributes || {};
|
||||||
} catch (e) {}
|
const storageLevel = buildings.storage ?? 0;
|
||||||
} else if (claimedAny) {
|
const gd = uw.GameData?.buildingData?.storage;
|
||||||
try {
|
storage = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0;
|
||||||
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) {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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.
|
// Boot — works whether page is already loaded or not.
|
||||||
// When eval()'d dynamically the 'load' event has already fired,
|
// When eval()'d dynamically the 'load' event has already fired,
|
||||||
// so we check readyState and boot immediately in that case.
|
// so we check readyState and boot immediately in that case.
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
function boot() {
|
function boot() {
|
||||||
log('Grepolis Remote Control v4.0.0 (remote) loaded');
|
log('Grepolis Remote Control v4.1.0 (remote) loaded');
|
||||||
detectCaptcha();
|
detectCaptcha();
|
||||||
setTimeout(pushState, 5000);
|
setTimeout(pushState, 5000);
|
||||||
jitterLoop(pushState, 60000, 120000);
|
jitterLoop(pushState, 60000, 120000); // state sync every 1-2 min
|
||||||
jitterLoop(pollAndExecute, 8000, 18000);
|
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') {
|
if (document.readyState === 'complete') {
|
||||||
|
|||||||
@@ -256,15 +256,29 @@ def sync_request():
|
|||||||
@api.route('/api/commands/<int:cmd_id>/result', methods=['POST'])
|
@api.route('/api/commands/<int:cmd_id>/result', methods=['POST'])
|
||||||
def command_result(cmd_id):
|
def command_result(cmd_id):
|
||||||
data = request.get_json(silent=True) or {}
|
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', '')
|
msg = data.get('message', '')
|
||||||
|
now = datetime.utcnow().isoformat()
|
||||||
|
|
||||||
conn = get_db()
|
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('''
|
conn.execute('''
|
||||||
UPDATE commands
|
UPDATE commands
|
||||||
SET status = ?, result_msg = ?, updated_at = ?
|
SET status = ?, result_msg = ?, updated_at = ?
|
||||||
WHERE id = ?
|
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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'ok': True})
|
return jsonify({'ok': True})
|
||||||
@@ -334,10 +348,17 @@ def farm_status():
|
|||||||
conn = get_db()
|
conn = get_db()
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
|
now = datetime.utcnow().isoformat()
|
||||||
conn.execute('''
|
conn.execute('''
|
||||||
INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?)
|
INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?)
|
||||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at
|
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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'ok': True})
|
return jsonify({'ok': True})
|
||||||
|
|||||||
@@ -126,6 +126,12 @@ def get_farm_data():
|
|||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
'SELECT town_id, town_name, data FROM town_state WHERE player_id = ?', (player_id,)
|
'SELECT town_id, town_name, data FROM town_state WHERE player_id = ?', (player_id,)
|
||||||
).fetchall()
|
).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()
|
conn.close()
|
||||||
|
|
||||||
now_ts = int(datetime.utcnow().timestamp())
|
now_ts = int(datetime.utcnow().timestamp())
|
||||||
@@ -142,7 +148,7 @@ def get_farm_data():
|
|||||||
'ready_farms': len(ready),
|
'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)
|
'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})
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -324,10 +324,11 @@
|
|||||||
<th>Έτοιμα</th>
|
<th>Έτοιμα</th>
|
||||||
<th>Σύνολο</th>
|
<th>Σύνολο</th>
|
||||||
<th>Επόμενο</th>
|
<th>Επόμενο</th>
|
||||||
|
<th>Τελευταία Λεηλασία</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="farm-table-body">
|
<tbody id="farm-table-body">
|
||||||
<tr><td colspan="4"><div class="empty-state">⏳ <p>Φόρτωση δεδομένων...</p></div></td></tr>
|
<tr><td colspan="5"><div class="empty-state">⏳ <p>Φόρτωση δεδομένων...</p></div></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -395,17 +396,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -- Load farm data table --
|
// -- 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() {
|
function loadFarmData() {
|
||||||
fetch(`/dashboard/farm-data?player_id=${PLAYER_ID}`)
|
fetch(`/dashboard/farm-data?player_id=${PLAYER_ID}`)
|
||||||
.then(r => r.json())
|
.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');
|
const tbody = document.getElementById('farm-table-body');
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="4"><div class="empty-state">🌱 <p>Δεν υπάρχουν δεδομένα χωριών ακόμη.<br>Βεβαιώσου ότι το script v3.3+ τρέχει στο παιχνίδι.</p></div></td></tr>';
|
tbody.innerHTML = '<tr><td colspan="5"><div class="empty-state">🌱 <p>Δεν υπάρχουν δεδομένα χωριών ακόμη.<br>Βεβαιώσου ότι το script v3.3+ τρέχει στο παιχνίδι.</p></div></td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
tbody.innerHTML = data.map(t => {
|
tbody.innerHTML = data.map((t, idx) => {
|
||||||
const readyBadge = t.ready_farms > 0
|
const readyBadge = t.ready_farms > 0
|
||||||
? `<span class="badge ready">✓ ${t.ready_farms} Έτοιμα</span>`
|
? `<span class="badge ready">✓ ${t.ready_farms} Έτοιμα</span>`
|
||||||
: `<span class="badge waiting">Αναμονή</span>`;
|
: `<span class="badge waiting">Αναμονή</span>`;
|
||||||
@@ -420,11 +431,16 @@
|
|||||||
nextStr = '<span class="countdown">Τώρα</span>';
|
nextStr = '<span class="countdown">Τώρα</span>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Show last farmed only in first row — same value for all rows
|
||||||
|
const lastFarmedCell = idx === 0
|
||||||
|
? `<td rowspan="${data.length}" style="color:#4acc64;font-size:0.82rem;vertical-align:middle;">${lastFarmed}</td>`
|
||||||
|
: '';
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td><strong>${t.town_name}</strong></td>
|
<td><strong>${t.town_name}</strong></td>
|
||||||
<td>${readyBadge}</td>
|
<td>${readyBadge}</td>
|
||||||
<td><span style="color:#888">${t.total_farms}</span></td>
|
<td><span style="color:#888">${t.total_farms}</span></td>
|
||||||
<td>${nextStr}</td>
|
<td>${nextStr}</td>
|
||||||
|
${lastFarmedCell}
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user