diff --git a/GrepolisRemoteControl.user.js b/GrepolisRemoteControl.user.js index c2df43f..84da835 100644 --- a/GrepolisRemoteControl.user.js +++ b/GrepolisRemoteControl.user.js @@ -833,8 +833,8 @@ return; } - // Build queue, Recruit queue and Market queue are independent - const buildCmd = cmdData.build; + // Build queue: one command per town (all towns build in the same poll cycle) + const buildCmds = cmdData.builds || []; const recruitCmd = cmdData.recruit; const marketCmd = cmdData.market; const researchCmd = cmdData.research; @@ -873,8 +873,16 @@ reportResult(cmd.id, finalStatus, result.msg); }; - // Run sequentially — humans cannot perform 3 actions simultaneously! - await execute(buildCmd); + // Execute ALL town build commands (one per town, sequential with inter-town delay) + for (let i = 0; i < buildCmds.length; i++) { + await execute(buildCmds[i]); + if (i < buildCmds.length - 1) { + // Random gap between towns so it doesn't look like a macro + const gap = randInt(1500, 3000); + log(`Build: town done. Waiting ${gap}ms before next town...`); + await sleep(gap); + } + } await execute(recruitCmd); await execute(marketCmd); await execute(researchCmd); diff --git a/bot_modules/05_main.js b/bot_modules/05_main.js index 7b9c313..8d9fb03 100644 --- a/bot_modules/05_main.js +++ b/bot_modules/05_main.js @@ -22,7 +22,8 @@ async function pollAndExecute() { const farmOn = features.includes('farm'); const adminOn = features.includes('admin'); - const buildCmd = adminOn ? cmdData.build : null; + // Build: one command per town (server returns an array) + const buildCmds = adminOn ? (cmdData.builds || []) : []; const recruitCmd = adminOn ? cmdData.recruit : null; const marketCmd = adminOn ? cmdData.market : null; const researchCmd = adminOn ? cmdData.research : null; @@ -60,8 +61,16 @@ async function pollAndExecute() { reportResult(cmd.id, finalStatus, result.msg); }; - // Run sequentially — humans cannot perform 3 actions simultaneously! - await execute(buildCmd); + // Execute one build command per town (simultaneous queue draining across all villages) + for (let i = 0; i < buildCmds.length; i++) { + await execute(buildCmds[i]); + if (i < buildCmds.length - 1) { + // Random inter-town gap — avoids looking like a macro + const gap = randInt(1500, 3000); + log(`Build: town done. Waiting ${gap}ms before next town...`); + await sleep(gap); + } + } await execute(recruitCmd); await execute(marketCmd); await execute(researchCmd); diff --git a/grepo.db b/grepo.db index 38c9f30..a665011 100644 Binary files a/grepo.db and b/grepo.db differ diff --git a/routes/api.py b/routes/api.py index ac72e0d..7763d09 100644 --- a/routes/api.py +++ b/routes/api.py @@ -105,10 +105,11 @@ def receive_state(): # so both queues are served in parallel without blocking each other. # ------------------------------------------------------------------ def _fetch_pending_of_type(c, cmd_type, player_id): + """Fetch a single oldest pending command of a given type (recruit, market, etc.).""" row = c.execute(''' SELECT * FROM commands WHERE status = 'pending' AND type = ? AND player_id = ? - ORDER BY id ASC + ORDER BY updated_at ASC, id ASC LIMIT 1 ''', (cmd_type, player_id)).fetchone() if not row: @@ -125,6 +126,49 @@ def _fetch_pending_of_type(c, cmd_type, player_id): 'payload': json.loads(row['payload']) } + +def _fetch_pending_builds_all_towns(c, player_id): + """ + Fetch ONE pending 'build' command per distinct town_id. + This allows all towns to build in parallel within a single poll cycle. + Within each town the oldest-updated command is picked first, so requeued + commands (updated_at = now) naturally sort behind fresh ones. + """ + # Get every town that has at least one pending build, ordered by + # which town has been waiting longest (MIN updated_at across its commands). + town_rows = c.execute(''' + SELECT town_id + FROM commands + WHERE status = 'pending' AND type = 'build' AND player_id = ? + GROUP BY town_id + ORDER BY MIN(updated_at) ASC + ''', (player_id,)).fetchall() + + results = [] + now = datetime.utcnow().isoformat() + for town_row in town_rows: + town_id = town_row['town_id'] + row = c.execute(''' + SELECT * FROM commands + WHERE status = 'pending' AND type = 'build' + AND player_id = ? AND town_id = ? + ORDER BY updated_at ASC, id ASC + LIMIT 1 + ''', (player_id, town_id)).fetchone() + if not row: + continue + c.execute(''' + UPDATE commands SET status = 'executing', updated_at = ? + WHERE id = ? + ''', (now, row['id'])) + results.append({ + 'id': row['id'], + 'town_id': row['town_id'], + 'type': row['type'], + 'payload': json.loads(row['payload']) + }) + return results + @api.route('/api/commands/pending', methods=['GET']) def get_pending_command(): player_id = request.args.get('player_id') @@ -134,7 +178,7 @@ def get_pending_command(): conn = get_db() c = conn.cursor() - build_cmd = _fetch_pending_of_type(c, 'build', player_id) + build_cmds = _fetch_pending_builds_all_towns(c, player_id) # one per town recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id) market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id) farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id) @@ -164,7 +208,7 @@ def get_pending_command(): conn.close() return jsonify({ - 'build': build_cmd, + 'builds': build_cmds, # list: one build command per town 'recruit': recruit_cmd, 'market': market_cmd, 'research': research_cmd,