diff --git a/GrepolisRemoteControl.user.js b/GrepolisRemoteControl.user.js index 6401da7..a18f4e0 100644 --- a/GrepolisRemoteControl.user.js +++ b/GrepolisRemoteControl.user.js @@ -786,6 +786,80 @@ await sleep(500); return { ok: true, msg: `Market offer posted: ${offer} ${offer_type} => ${demand} ${demand_type}` }; } + // ---------------------------------------------------------------- + // Execute: Scan Market (On-Demand) + // ---------------------------------------------------------------- + async function executeScanMarket(cmd) { + const { town_id } = cmd; + + const reactionMs = randInt(1500, 3000); + log(`Waiting ${reactionMs}ms before scanning market...`); + await sleep(reactionMs); + + if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' }; + + return new Promise((resolve) => { + uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { + model_url: 'BuildingMarket', + action_name: 'getData', + arguments: { + limit: 20, + offset: 0, + demand_type: 'all_but_gold', + offer_type: 'all_but_gold', + max_ratio: 3, + max_delivery_time: 172800, + visibility: 2, + order_by: 'ratio', + order_direction: 'desc' + }, + town_id: town_id, + nl_init: true + }, false, { + success: async function(resp) { + try { + // Send the data back to our backend + await fetch(`${BASE_URL}/api/market_data?player_id=${uw.Game.player_id}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(resp) + }); + resolve({ ok: true, msg: 'Market scanned and data uploaded' }); + } catch (e) { + resolve({ ok: false, msg: 'Failed to upload market data: ' + e }); + } + }, + error: function() { + resolve({ ok: false, msg: 'Failed to fetch market data from game' }); + } + }); + }); + } + + // ---------------------------------------------------------------- + // Execute: Accept Market Offer + // ---------------------------------------------------------------- + async function executeAcceptMarketOffer(cmd) { + const { town_id, payload } = cmd; + const { offer_id, amount } = payload; + + const reactionMs = randInt(800, 2000); + log(`Waiting ${reactionMs}ms before accepting offer...`); + await sleep(reactionMs); + + if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' }; + + uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { + model_url: 'BuildingMarket', + action_name: 'acceptOffer', + arguments: { offer_id, amount }, + town_id: town_id, + nl_init: true + }); + + await sleep(500); + return { ok: true, msg: `Accepted market offer ${offer_id} for amount ${amount}` }; + } // ---------------------------------------------------------------- // Execute: Research (Academy) @@ -836,6 +910,8 @@ // Build queue, Recruit queue and Market queue are independent const buildCmd = cmdData.build; const recruitCmd = cmdData.recruit; + const scanMarketCmd = cmdData.scan_market; + const acceptMarketCmd = cmdData.accept_market_offer; const marketCmd = cmdData.market; const researchCmd = cmdData.research; const farmCmd = cmdData.farm; @@ -851,17 +927,24 @@ 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 === 'scan_market') result = await executeScanMarket(cmd); + else if (cmd.type === 'accept_market_offer') result = await executeAcceptMarketOffer(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 result = { ok: false, msg: `Unknown type: ${cmd.type}` }; } catch (e) { - result = { ok: false, msg: `Exception: ${e.message}` }; + 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}`); @@ -872,6 +955,8 @@ await execute(buildCmd); await execute(recruitCmd); await execute(marketCmd); + await execute(scanMarketCmd); + await execute(acceptMarketCmd); await execute(researchCmd); await execute(farmCmd); await execute(farmUpgradeCmd); diff --git a/routes/api.py b/routes/api.py index e67ac0b..4ae1cd4 100644 --- a/routes/api.py +++ b/routes/api.py @@ -101,6 +101,8 @@ def get_pending_command(): farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id) farm_upgrade_cmd = _fetch_pending_of_type(c, 'farm_upgrade', player_id) research_cmd = _fetch_pending_of_type(c, 'research', player_id) + scan_market_cmd = _fetch_pending_of_type(c, 'scan_market', player_id) + accept_market_offer_cmd = _fetch_pending_of_type(c, 'accept_market_offer', player_id) sync_req = _check_and_reset_sync(c, player_id) # Also return current farm settings so TM knows loot_option @@ -119,6 +121,8 @@ def get_pending_command(): 'build': build_cmd, 'recruit': recruit_cmd, 'market': market_cmd, + 'scan_market': scan_market_cmd, + 'accept_market_offer': accept_market_offer_cmd, 'research': research_cmd, 'farm': farm_cmd, 'farm_upgrade': farm_upgrade_cmd, @@ -202,3 +206,28 @@ def captcha_alert(): conn.commit() conn.close() return jsonify({'ok': True}) + +# ------------------------------------------------------------------ +# POST /api/market_data +# Tampermonkey uploads the market scan data. +# ------------------------------------------------------------------ +@api.route('/api/market_data', methods=['POST']) +def upload_market_data(): + player_id = request.args.get('player_id') + if not player_id: + return jsonify({'error': 'no player_id provided'}), 400 + + data = request.get_json(silent=True) or {} + kv_key = f'market_data_{player_id}' + + conn = get_db() + 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())) + conn.commit() + conn.close() + return jsonify({'ok': True}) diff --git a/routes/dashboard.py b/routes/dashboard.py index bc80c04..3fdb865 100644 --- a/routes/dashboard.py +++ b/routes/dashboard.py @@ -129,6 +129,24 @@ def get_farm_data(): return jsonify(farms_summary) +# ------------------------------------------------------------------ +# GET /dashboard/market-data +# Returns the latest market scan data for a player. +# ------------------------------------------------------------------ +@dashboard.route('/dashboard/market-data', methods=['GET']) +def get_market_data(): + player_id = request.args.get('player_id') + conn = get_db() + row = conn.execute( + "SELECT value, updated_at FROM kv_store WHERE key = ?", (f'market_data_{player_id}', ) + ).fetchone() + conn.close() + + if row: + return jsonify({'data': json.loads(row['value']), 'updated_at': row['updated_at']}) + return jsonify({'data': None, 'updated_at': None}) + + # ------------------------------------------------------------------ # GET /dashboard/towns # Returns all known towns with their latest state snapshot. @@ -266,8 +284,8 @@ def create_command(): return jsonify({'error': f'missing field: {field}'}), 400 cmd_type = data['type'] - if cmd_type not in ('build', 'recruit', 'market_offer', 'farm_loot', 'farm_upgrade', 'research'): - return jsonify({'error': 'type must be build, recruit, market_offer, farm_loot, farm_upgrade, or research'}), 400 + if cmd_type not in ('build', 'recruit', 'market_offer', 'farm_loot', 'farm_upgrade', 'research', 'scan_market', 'accept_market_offer'): + return jsonify({'error': 'type must be build, recruit, market_offer, farm_loot, farm_upgrade, research, scan_market, or accept_market_offer'}), 400 # Reject if the Tampermonkey client is offline (no state push in last 150 s) conn = get_db() diff --git a/static/js/api.js b/static/js/api.js index 796fb1f..b620640 100644 --- a/static/js/api.js +++ b/static/js/api.js @@ -247,9 +247,6 @@ window.requestLiveSync = async function() { const data = await res.json(); if (data.ok) { btn.textContent = '✅ Requested!'; - setTimeout(() => { - btn.textContent = originalText; - btn.disabled = false; }, 5000); } } catch (e) { @@ -260,3 +257,133 @@ window.requestLiveSync = async function() { }, 3000); } }; + +window.scanMarket = async function() { + if (!window.clientOnline) return alert('Το script είναι offline.'); + const town = window.getSelectedTown(); + if (!town) return alert('Select a town first.'); + + try { + const res = await fetch('/dashboard/commands', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + town_id: town.town_id, + town_name: town.town_name, + type: 'scan_market', + payload: {}, + player_id: window.PLAYER_ID + }) + }); + const data = await res.json(); + if (data.ok) { + document.getElementById('market-offers-content').innerHTML = '⏳ Αναζήτηση σε εξέλιξη... Περιμένετε!'; + window.fetchLog(); + + // Auto fetch market data every 2 seconds for the next 15 seconds + let attempts = 0; + const interval = setInterval(() => { + window.fetchMarketData(); + attempts++; + if (attempts > 7) clearInterval(interval); + }, 2000); + } else { + alert('Error: ' + JSON.stringify(data)); + } + } catch (e) { + alert('Failed to send scan_market command: ' + e); + } +}; + +window.acceptMarketOffer = async function(offer_id, amount) { + if (!window.clientOnline) return alert('Το script είναι offline.'); + const town = window.getSelectedTown(); + if (!town) return alert('Select a town first.'); + + try { + const res = await fetch('/dashboard/commands', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + town_id: town.town_id, + town_name: town.town_name, + type: 'accept_market_offer', + payload: { offer_id: parseInt(offer_id), amount: parseInt(amount) }, + player_id: window.PLAYER_ID + }) + }); + const data = await res.json(); + if (data.ok) { + window.fetchLog(); + alert(`Εστάλη εντολή αποδοχής για την προσφορά #${offer_id}!`); + // Update local UI immediately so user doesn't double click + document.getElementById(`offer-btn-${offer_id}`).disabled = true; + document.getElementById(`offer-btn-${offer_id}`).innerText = '⏳'; + } else { + alert('Error: ' + JSON.stringify(data)); + } + } catch (e) { + alert('Failed to send accept_market_offer command: ' + e); + } +}; + +window.lastMarketUpdate = null; +window.fetchMarketData = async function() { + try { + const res = await fetch('/dashboard/market-data?player_id=' + window.PLAYER_ID); + const data = await res.json(); + if (!data || !data.data || !data.data.offers) return; + + // Only re-render if the timestamp has changed + if (window.lastMarketUpdate === data.updated_at) return; + window.lastMarketUpdate = data.updated_at; + + const offers = data.data.offers; + if (offers.length === 0) { + document.getElementById('market-offers-content').innerHTML = 'Καμία διαθέσιμη προσφορά.'; + return; + } + + let html = `
| Παίκτης | +Προσφέρει | +Ζητάει | +Λόγος | +Διάρκεια | ++ |
|---|---|---|---|---|---|
| ${off.player_name || 'NPC'} | +${off.offer} ${offerEmoji} | +${off.demand} ${demandEmoji} | +1 : ${ratio} | +${timeStr} | ++ + | +