diff --git a/bot_modules/04c_execute_bootcamp_trade.js b/bot_modules/04c_execute_bootcamp_trade.js index 57d6f73..e16fc5a 100644 --- a/bot_modules/04c_execute_bootcamp_trade.js +++ b/bot_modules/04c_execute_bootcamp_trade.js @@ -17,13 +17,13 @@ let lastKnownBotSettings = {}; // ---------------------------------------------------------------- // botLog — sends a log entry to the server // ---------------------------------------------------------------- -async function botLog(player_id, feature, message) { +async function botLog(player_id, world_id, feature, message) { log(`[${feature}] ${message}`); try { await apiFetch(`${BASE_URL}/api/bot-logs`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ player_id, feature, message }) + body: JSON.stringify({ player_id, world_id, feature, message }) }); } catch (e) { /* non-critical */ } } @@ -38,7 +38,8 @@ async function autoBootcampLoop() { if (!settings.bootcamp_enabled) return; const player_id = uw.Game?.player_id; - if (!player_id) return; + const world_id = uw.Game?.world_id; + if (!player_id || !world_id) return; let model; try { @@ -67,7 +68,7 @@ async function autoBootcampLoop() { action_name: 'useReward', arguments: {} }); - await botLog(player_id, 'bootcamp', `Reward used: ${reward.power_id}`); + await botLog(player_id, world_id, 'bootcamp', 'Διεκδίκηση αμοιβής ληστών...'); } else if (stashable) { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { model_url: `PlayerAttackSpot/${player_id}`, @@ -75,7 +76,7 @@ async function autoBootcampLoop() { arguments: {} }, 0, { success: () => { - botLog(player_id, 'bootcamp', `Reward stashed: ${reward.power_id}`); + botLog(player_id, world_id, 'bootcamp', `Reward stashed: ${reward.power_id}`); }, error: () => { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { @@ -83,7 +84,7 @@ async function autoBootcampLoop() { action_name: 'useReward', arguments: {} }); - botLog(player_id, 'bootcamp', `Reward used (inventory full fallback): ${reward.power_id}`); + botLog(player_id, world_id, 'bootcamp', 'Αποτυχία διεκδίκησης αμοιβής.'); } }); } else { @@ -92,7 +93,7 @@ async function autoBootcampLoop() { action_name: 'useReward', arguments: {} }); - await botLog(player_id, 'bootcamp', `Reward used (fallback): ${reward.power_id}`); + await botLog(player_id, world_id, 'bootcamp', `Reward used (fallback): ${reward.power_id}`); } await sleep(randInt(3000, 7000)); return; // Wait for next cycle to attack @@ -110,7 +111,7 @@ async function autoBootcampLoop() { const cooldown = model.getCooldownDuration(); if (cooldown > 0) { const minRemaining = Math.round(cooldown / 60); - await botLog(player_id, 'bootcamp', `Camp on cooldown — ${minRemaining} min remaining`); + await botLog(player_id, world_id, 'bootcamp', `Camp on cooldown — ${minRemaining} min remaining`); return; } @@ -119,7 +120,7 @@ async function autoBootcampLoop() { if (movements) { for (const mv of Object.values(movements)) { if (mv.attributes.destination_is_attack_spot || mv.attributes.origin_is_attack_spot) { - await botLog(player_id, 'bootcamp', 'Attack already in flight — skipping'); + await botLog(player_id, world_id, 'bootcamp', 'Attack already in flight — skipping'); return; } } @@ -152,7 +153,7 @@ async function autoBootcampLoop() { } if (Object.keys(units).length === 0) { - await botLog(player_id, 'bootcamp', 'No available units — skipping attack'); + await botLog(player_id, world_id, 'bootcamp', 'No available units — skipping attack. (Χωρίς αμυντικά)'); return; } @@ -163,10 +164,10 @@ async function autoBootcampLoop() { }); const unitSummary = Object.entries(units).map(([u, n]) => `${n}x${u}`).join(', '); - await botLog(player_id, 'bootcamp', `Attack sent — ${unitSummary}`); + await botLog(player_id, world_id, 'bootcamp', `Στέλνω ${JSON.stringify(units)} στο camp...`); } catch (e) { - await botLog(player_id, 'bootcamp', `Error during attack: ${e}`); + await botLog(player_id, world_id, 'bootcamp', `Error during attack: ${e}`); } } @@ -187,7 +188,8 @@ async function autoRuralTradeLoop() { if (!settings.rural_trade_enabled) return; const player_id = uw.Game?.player_id; - if (!player_id) return; + const world_id = uw.Game?.world_id; + if (!player_id || !world_id) return; const minRatio = RATIO_MAP[settings.rural_trade_ratio] ?? 0.75; @@ -285,12 +287,12 @@ async function autoRuralTradeLoop() { arguments: { farm_town_id: farm.attributes.id, amount }, town_id: parseInt(town_id_str) }); - await botLog(player_id, 'rural_trade', + await botLog(player_id, world_id, 'rural_trade', `Traded ${amount} ${missingResource} ← ${farm.attributes.name} via ${town_obj.getName?.() ?? town_id_str}`); tradesTotal++; tradeMade = true; } catch (e) { - await botLog(player_id, 'rural_trade', `Trade error: ${e}`); + await botLog(player_id, world_id, 'rural_trade', `Trade error: ${e}`); } await sleep(randInt(800, 1800)); @@ -300,7 +302,7 @@ async function autoRuralTradeLoop() { } if (!tradeMade && missingResource) { - await botLog(player_id, 'rural_trade', + await botLog(player_id, world_id, 'rural_trade', `${town_obj.getName?.() ?? town_id_str} needs ${missingResource} but no suitable village found`); } diff --git a/routes/api.py b/routes/api.py index 397111e..6418102 100644 --- a/routes/api.py +++ b/routes/api.py @@ -117,13 +117,28 @@ def receive_state(): # ------------------------------------------------------------------ def _fetch_pending_of_type(c, cmd_type, player_id, world_id): """Fetch a single oldest pending command of a given type (recruit, market, etc.).""" - row = c.execute(''' - SELECT c.* FROM commands c - JOIN town_state ts ON c.town_id = ts.town_id - WHERE c.status = 'pending' AND c.type = ? AND c.player_id = ? AND ts.world_id = ? - ORDER BY c.updated_at ASC, c.id ASC - LIMIT 1 - ''', (cmd_type, player_id, world_id)).fetchone() + + # We use LEFT JOIN because global commands (like farm_loot) use a pseudo town_id like "0_gr121" + # which does not exist in town_state. + global_town_id = f"0_{world_id}" if world_id else "0" + + if world_id: + row = c.execute(''' + SELECT c.* FROM commands c + LEFT JOIN town_state ts ON c.town_id = ts.town_id + WHERE c.status = 'pending' AND c.type = ? AND c.player_id = ? + AND (ts.world_id = ? OR c.town_id = ?) + ORDER BY c.updated_at ASC, c.id ASC + LIMIT 1 + ''', (cmd_type, player_id, world_id, global_town_id)).fetchone() + else: + row = c.execute(''' + SELECT * FROM commands + WHERE status = 'pending' AND type = ? AND player_id = ? + ORDER BY updated_at ASC, id ASC + LIMIT 1 + ''', (cmd_type, player_id)).fetchone() + if not row: return None c.execute(''' @@ -242,9 +257,12 @@ def get_pending_command(): research_cmd = _fetch_pending_of_type(c, 'research', player_id, world_id) sync_req = _check_and_reset_sync(c, player_id) + # Determine player_key for world-specific settings if world_id is provided + player_key = f"{player_id}_{world_id}" if world_id else player_id + # Farm settings farm_row = c.execute( - 'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,) + 'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_key,) ).fetchone() farm_settings = { 'enabled': bool(farm_row['enabled']) if farm_row else False, @@ -253,7 +271,7 @@ def get_pending_command(): # Bot settings (bootcamp + rural trade) bot_row = c.execute( - 'SELECT * FROM bot_settings WHERE player_id = ?', (str(player_id),) + 'SELECT * FROM bot_settings WHERE player_id = ?', (player_key,) ).fetchone() bot_settings = { 'bootcamp_enabled': bool(bot_row['bootcamp_enabled']) if bot_row else False, @@ -263,7 +281,7 @@ def get_pending_command(): } # One-shot manual attack flag - attack_now_key = f'bootcamp_attack_now_{player_id}' + attack_now_key = f'bootcamp_attack_now_{player_key}' flag_row = c.execute('SELECT value FROM kv_store WHERE key = ?', (attack_now_key,)).fetchone() if flag_row and flag_row['value'] == '1': bot_settings['attack_now'] = True @@ -457,16 +475,19 @@ def api_bot_logs(): data = request.get_json(silent=True) or {} player_id = str(data.get('player_id', '')) + world_id = str(data.get('world_id', '')) feature = data.get('feature', '') message = data.get('message', '') if not player_id or not feature or not message: return jsonify({'error': 'missing fields'}), 400 + player_key = f"{player_id}_{world_id}" if world_id else player_id + conn = get_db() conn.execute( 'INSERT INTO bot_logs (player_id, feature, message) VALUES (?, ?, ?)', - (player_id, feature, message) + (player_key, feature, message) ) # Keep only latest 50 per player/feature conn.execute(''' @@ -477,7 +498,7 @@ def api_bot_logs(): WHERE player_id = ? AND feature = ? ORDER BY id DESC LIMIT 50 ) - ''', (player_id, feature, player_id, feature)) + ''', (player_key, feature, player_key, feature)) conn.commit() conn.close() return jsonify({'ok': True}) diff --git a/routes/dashboard.py b/routes/dashboard.py index 974a174..0540020 100644 --- a/routes/dashboard.py +++ b/routes/dashboard.py @@ -84,9 +84,12 @@ def player_farm(player_id, world_id): @dashboard.route('/dashboard/farm-settings', methods=['GET']) def get_farm_settings(): player_id = request.args.get('player_id') + world_id = request.args.get('world_id', '') + player_key = f"{player_id}_{world_id}" if world_id else player_id + conn = get_db() row = conn.execute( - 'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,) + 'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_key,) ).fetchone() conn.close() if row: @@ -99,6 +102,9 @@ def set_farm_settings(): if not data or 'player_id' not in data: return jsonify({'error': 'missing player_id'}), 400 player_id = data['player_id'] + world_id = data.get('world_id', '') + player_key = f"{player_id}_{world_id}" if world_id else player_id + enabled = 1 if data.get('enabled') else 0 loot_option = int(data.get('loot_option', 1)) conn = get_db() @@ -109,7 +115,7 @@ def set_farm_settings(): enabled = excluded.enabled, loot_option = excluded.loot_option, updated_at = excluded.updated_at - ''', (player_id, enabled, loot_option, datetime.utcnow().isoformat())) + ''', (player_key, enabled, loot_option, datetime.utcnow().isoformat())) conn.commit() conn.close() return jsonify({'ok': True}) @@ -521,15 +527,18 @@ def fail_stale_commands(): @dashboard.route('/dashboard/bot-settings', methods=['GET', 'POST']) def bot_settings(): player_id = request.args.get('player_id') or (request.json or {}).get('player_id') + world_id = request.args.get('world_id') or (request.json or {}).get('world_id', '') if not player_id: return jsonify({'error': 'missing player_id'}), 400 + + player_key = f"{player_id}_{world_id}" if world_id else player_id conn = get_db() c = conn.cursor() if request.method == 'GET': row = c.execute( - 'SELECT * FROM bot_settings WHERE player_id = ?', (player_id,) + 'SELECT * FROM bot_settings WHERE player_id = ?', (player_key,) ).fetchone() conn.close() if row: @@ -555,7 +564,7 @@ def bot_settings(): rural_trade_ratio = excluded.rural_trade_ratio, updated_at = excluded.updated_at ''', ( - player_id, + player_key, int(bool(data.get('bootcamp_enabled', 0))), int(bool(data.get('bootcamp_use_def', 0))), int(bool(data.get('rural_trade_enabled', 0))), @@ -574,8 +583,11 @@ def bot_settings(): @dashboard.route('/dashboard/bot-logs', methods=['GET', 'POST']) def bot_logs(): player_id = request.args.get('player_id') or (request.json or {}).get('player_id') + world_id = request.args.get('world_id') or (request.json or {}).get('world_id', '') if not player_id: return jsonify({'error': 'missing player_id'}), 400 + + player_key = f"{player_id}_{world_id}" if world_id else player_id conn = get_db() c = conn.cursor() @@ -583,7 +595,7 @@ def bot_logs(): if request.method == 'GET': feature = request.args.get('feature', '') query = 'SELECT * FROM bot_logs WHERE player_id = ?' - params = [player_id] + params = [player_key] if feature: query += ' AND feature = ?' params.append(feature) @@ -598,7 +610,7 @@ def bot_logs(): message = data.get('message', '') c.execute( 'INSERT INTO bot_logs (player_id, feature, message) VALUES (?, ?, ?)', - (player_id, feature, message) + (player_key, feature, message) ) # Prune: keep only the latest 50 per player/feature c.execute(''' @@ -609,7 +621,7 @@ def bot_logs(): WHERE player_id = ? AND feature = ? ORDER BY id DESC LIMIT 50 ) - ''', (player_id, feature, player_id, feature)) + ''', (player_key, feature, player_key, feature)) conn.commit() conn.close() return jsonify({'ok': True}) @@ -623,9 +635,12 @@ def bot_logs(): @dashboard.route('/dashboard/bootcamp-attack-now', methods=['POST']) def bootcamp_attack_now(): player_id = (request.json or {}).get('player_id') + world_id = (request.json or {}).get('world_id', '') if not player_id: return jsonify({'error': 'missing player_id'}), 400 - key = f'bootcamp_attack_now_{player_id}' + + player_key = f"{player_id}_{world_id}" if world_id else player_id + key = f'bootcamp_attack_now_{player_key}' conn = get_db() conn.execute(''' INSERT INTO kv_store (key, value, updated_at) VALUES (?, '1', ?) diff --git a/templates/farm.html b/templates/farm.html index cbe1e26..9aa7a36 100644 --- a/templates/farm.html +++ b/templates/farm.html @@ -446,7 +446,7 @@ fetch('/dashboard/farm-settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ player_id: PLAYER_ID, enabled, loot_option: selectedOption }) + body: JSON.stringify({ player_id: PLAYER_ID, world_id: WORLD_ID, enabled, loot_option: selectedOption }) }) .then(r => r.json()) .then(() => { @@ -458,7 +458,7 @@ // -- Load current settings -- function loadSettings() { - fetch(`/dashboard/farm-settings?player_id=${PLAYER_ID}`) + fetch(`/dashboard/farm-settings?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`) .then(r => r.json()) .then(cfg => { document.getElementById('farm-enabled').checked = cfg.enabled; @@ -564,7 +564,7 @@ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ player_id: PLAYER_ID, - town_id: 0, + town_id: WORLD_ID ? `0_${WORLD_ID}` : "0", type: 'farm_upgrade', payload: { threshold: threshold, action_type: actionType } }) @@ -591,7 +591,7 @@ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ player_id: PLAYER_ID, - town_id: 0, + town_id: WORLD_ID ? `0_${WORLD_ID}` : "0", type: 'farm_loot', payload: { loot_option: selectedOption } }) @@ -617,7 +617,7 @@ fetch('/dashboard/bootcamp-attack-now', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ player_id: PLAYER_ID }) + body: JSON.stringify({ player_id: PLAYER_ID, world_id: WORLD_ID }) }) .then(r => r.json()) .then(() => { @@ -633,7 +633,7 @@ async function checkWarehouseStatus() { try { - const res = await fetch(`/api/farm_status?player_id=${PLAYER_ID}`); + const res = await fetch(`/api/farm_status?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`); const data = await res.json(); const banner = document.getElementById('warehouse-full-banner'); if (banner) banner.style.display = data.warehouse_full ? 'flex' : 'none'; @@ -651,7 +651,7 @@ } function loadBotSettings() { - fetch(`/dashboard/bot-settings?player_id=${PLAYER_ID}`) + fetch(`/dashboard/bot-settings?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`) .then(r => r.json()) .then(cfg => { document.getElementById('bootcamp-enabled').checked = !!cfg.bootcamp_enabled; @@ -669,6 +669,7 @@ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ player_id: PLAYER_ID, + world_id: WORLD_ID, bootcamp_enabled: document.getElementById('bootcamp-enabled').checked, bootcamp_use_def: document.getElementById('bootcamp-use-def').checked, rural_trade_enabled: document.getElementById('rural-trade-enabled').checked, @@ -708,9 +709,9 @@ } function loadBotLogs() { - fetch(`/dashboard/bot-logs?player_id=${PLAYER_ID}&feature=bootcamp`) + fetch(`/dashboard/bot-logs?player_id=${PLAYER_ID}&world_id=${WORLD_ID}&feature=bootcamp`) .then(r => r.json()).then(data => renderBotLog('bootcamp-log', data)); - fetch(`/dashboard/bot-logs?player_id=${PLAYER_ID}&feature=rural_trade`) + fetch(`/dashboard/bot-logs?player_id=${PLAYER_ID}&world_id=${WORLD_ID}&feature=rural_trade`) .then(r => r.json()).then(data => renderBotLog('rural-trade-log', data)); }