diff --git a/GrepolisRemoteControl.user.js b/GrepolisRemoteControl.user.js index 1c3e241..91b2bdf 100644 --- a/GrepolisRemoteControl.user.js +++ b/GrepolisRemoteControl.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Grepolis Remote Control // @namespace http://tampermonkey.net/ -// @version 2.1 +// @version 2.3 // @description Polls grepo.haunter-pets.top for remote commands and executes them in-game // @author Dimitrios // @match https://*.grepolis.com/game/* @@ -15,8 +15,22 @@ const uw = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; const BASE_URL = 'https://grepo.haunter-pets.top'; - const POLL_INTERVAL_MS = 5000; // poll for commands - const STATE_INTERVAL_MS = 30000; // push town state + + // ---- Jitter helpers ----------------------------------------------- + // Returns a random integer between min and max (inclusive) + function randInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + // Schedules fn to run after a random ms delay, then reschedules itself + function jitterLoop(fn, minMs, maxMs) { + function schedule() { + setTimeout(async () => { + await fn(); + schedule(); // reschedule with a NEW random delay every time + }, randInt(minMs, maxMs)); + } + schedule(); + } // ---------------------------------------------------------------- // Toolbar indicator button @@ -303,7 +317,11 @@ log(`Resource check skipped: ${e}`); } - // Fire the build request + // Fire the build request — with a human-like reaction delay + const reactionMs = randInt(800, 2500); + log(`Waiting ${reactionMs}ms before firing buildUp (reaction time)...`); + await sleep(reactionMs); + uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { model_url: 'BuildingOrder', action_name: 'buildUp', @@ -334,6 +352,11 @@ ]; const endpoint = navalUnits.includes(unit_id) ? 'building_docks' : 'building_barracks'; + // Fire the recruit request — with a human-like reaction delay + const reactionMs = randInt(800, 2500); + log(`Waiting ${reactionMs}ms before firing recruit (reaction time)...`); + await sleep(reactionMs); + uw.gpAjax.ajaxPost(endpoint, 'build', { unit_id, amount: parseInt(amount) || 1, @@ -345,7 +368,7 @@ } // ---------------------------------------------------------------- - // Poll for and execute a pending command + // Poll for and execute pending commands (build + recruit in parallel) // ---------------------------------------------------------------- async function pollAndExecute() { if (paused) return; @@ -359,41 +382,86 @@ return; } - const cmd = cmdData.command; - if (!cmd) return; // nothing pending + // Build queue and Recruit queue are now independent + const buildCmd = cmdData.build; + const recruitCmd = cmdData.recruit; - log(`Executing command #${cmd.id} — type:${cmd.type} town:${cmd.town_id}`); - - let result; - try { - if (cmd.type === 'build') { - result = await executeBuild(cmd); - } else if (cmd.type === 'recruit') { - result = await executeRecruit(cmd); - } else { - result = { ok: false, msg: `Unknown command type: ${cmd.type}` }; + const execute = async (cmd) => { + if (!cmd) return; + log(`Executing command #${cmd.id} — type:${cmd.type} town:${cmd.town_id}`); + let result; + try { + if (cmd.type === 'build') result = await executeBuild(cmd); + else if (cmd.type === 'recruit') result = await executeRecruit(cmd); + else result = { ok: false, msg: `Unknown type: ${cmd.type}` }; + } catch (e) { + result = { ok: false, msg: `Exception: ${e.message}` }; } - } catch (e) { - result = { ok: false, msg: `Exception: ${e.message}` }; - } + 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); + }; - const finalStatus = result.requeue ? 'pending' : (result.ok ? 'done' : 'failed'); - log(`Command #${cmd.id} result: ${finalStatus === 'done' ? '✅' : (finalStatus === 'pending' ? '⏳' : '❌')} ${result.msg}`); - reportResult(cmd.id, finalStatus, result.msg); + // Run both queues concurrently — they do NOT block each other + await Promise.all([execute(buildCmd), execute(recruitCmd)]); + } + + // ---------------------------------------------------------------- + // Observers — instant triggers on meaningful game events + // ---------------------------------------------------------------- + function setupObservers() { + try { + // 1. Town switch — instant dashboard sync when you click a different town + uw.$.Observer(uw.GameEvents.town.town_switch).subscribe(() => { + log('Observer: town switched — pushing state'); + pushState(); + }); + } catch(e) { log(`Observer town_switch failed: ${e}`); } + + try { + // 2. Building finished — push state AND immediately try the next build command + uw.$.Observer(uw.GameEvents.town.building.order_completed).subscribe(() => { + log('Observer: building completed — pushing state + polling'); + pushState(); + pollAndExecute(); + }); + } catch(e) { log(`Observer building.order_completed failed: ${e}`); } + + try { + // 3. Troops finished training — same logic for the recruit queue + uw.$.Observer(uw.GameEvents.town.unit.order_completed).subscribe(() => { + log('Observer: unit order completed — pushing state + polling'); + pushState(); + pollAndExecute(); + }); + } catch(e) { log(`Observer unit.order_completed failed: ${e}`); } + + try { + // 4. Points changed — fires when a building finishes (extra safety net) + uw.$.Observer(uw.GameEvents.player.points.changed).subscribe(() => { + log('Observer: player points changed — pushing state'); + pushState(); + }); + } catch(e) { log(`Observer player.points.changed failed: ${e}`); } + + log('Observers registered successfully'); } // ---------------------------------------------------------------- // Boot // ---------------------------------------------------------------- window.addEventListener('load', () => { - log('Grepolis Remote Control loaded'); + log('Grepolis Remote Control v2.3 loaded'); - // Push state immediately, then on interval + // Push state once after load, then every 45–90 seconds (randomized) setTimeout(pushState, 5000); - setInterval(pushState, STATE_INTERVAL_MS); + jitterLoop(pushState, 45000, 90000); - // Poll for commands - setInterval(pollAndExecute, POLL_INTERVAL_MS); + // Poll for commands every 8–18 seconds (randomized jitter) + jitterLoop(pollAndExecute, 8000, 18000); + + // Wire up game event observers after a short delay to ensure game is ready + setTimeout(setupObservers, 6000); }); })(); diff --git a/routes/api.py b/routes/api.py index b11e65d..8bda1b0 100644 --- a/routes/api.py +++ b/routes/api.py @@ -62,38 +62,44 @@ def receive_state(): # ------------------------------------------------------------------ # GET /api/commands/pending # Tampermonkey polls this to get the next command to execute. -# Returns ONE command at a time, marks it as 'executing'. +# Returns one 'build' AND one 'recruit' command independently, +# so both queues are served in parallel without blocking each other. # ------------------------------------------------------------------ -@api.route('/api/commands/pending', methods=['GET']) -def get_pending_command(): - conn = get_db() - c = conn.cursor() +def _fetch_pending_of_type(c, cmd_type): row = c.execute(''' SELECT * FROM commands - WHERE status = 'pending' + WHERE status = 'pending' AND type = ? ORDER BY id ASC LIMIT 1 - ''').fetchone() - + ''', (cmd_type,)).fetchone() if not row: - conn.close() - return jsonify({'command': None}) - + return None c.execute(''' UPDATE commands SET status = 'executing', updated_at = ? WHERE id = ? ''', (datetime.utcnow().isoformat(), row['id'])) + return { + 'id': row['id'], + 'town_id': row['town_id'], + 'type': row['type'], + 'payload': json.loads(row['payload']) + } + +@api.route('/api/commands/pending', methods=['GET']) +def get_pending_command(): + conn = get_db() + c = conn.cursor() + + build_cmd = _fetch_pending_of_type(c, 'build') + recruit_cmd = _fetch_pending_of_type(c, 'recruit') + conn.commit() conn.close() return jsonify({ - 'command': { - 'id': row['id'], - 'town_id': row['town_id'], - 'type': row['type'], - 'payload': json.loads(row['payload']) - } + 'build': build_cmd, + 'recruit': recruit_cmd })