This commit is contained in:
2026-04-20 23:43:01 +03:00
parent 9ff4398b14
commit aa3a5463c7
2 changed files with 119 additions and 45 deletions

View File

@@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Grepolis Remote Control // @name Grepolis Remote Control
// @namespace http://tampermonkey.net/ // @namespace http://tampermonkey.net/
// @version 2.1 // @version 2.3
// @description Polls grepo.haunter-pets.top for remote commands and executes them in-game // @description Polls grepo.haunter-pets.top for remote commands and executes them in-game
// @author Dimitrios // @author Dimitrios
// @match https://*.grepolis.com/game/* // @match https://*.grepolis.com/game/*
@@ -15,8 +15,22 @@
const uw = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; const uw = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const BASE_URL = 'https://grepo.haunter-pets.top'; 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 // Toolbar indicator button
@@ -303,7 +317,11 @@
log(`Resource check skipped: ${e}`); 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', { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: 'BuildingOrder', model_url: 'BuildingOrder',
action_name: 'buildUp', action_name: 'buildUp',
@@ -334,6 +352,11 @@
]; ];
const endpoint = navalUnits.includes(unit_id) ? 'building_docks' : 'building_barracks'; 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', { uw.gpAjax.ajaxPost(endpoint, 'build', {
unit_id, unit_id,
amount: parseInt(amount) || 1, 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() { async function pollAndExecute() {
if (paused) return; if (paused) return;
@@ -359,41 +382,86 @@
return; return;
} }
const cmd = cmdData.command; // Build queue and Recruit queue are now independent
if (!cmd) return; // nothing pending const buildCmd = cmdData.build;
const recruitCmd = cmdData.recruit;
log(`Executing command #${cmd.id} — type:${cmd.type} town:${cmd.town_id}`); const execute = async (cmd) => {
if (!cmd) return;
let result; log(`Executing command #${cmd.id} — type:${cmd.type} town:${cmd.town_id}`);
try { let result;
if (cmd.type === 'build') { try {
result = await executeBuild(cmd); if (cmd.type === 'build') result = await executeBuild(cmd);
} else if (cmd.type === 'recruit') { else if (cmd.type === 'recruit') result = await executeRecruit(cmd);
result = await executeRecruit(cmd); else result = { ok: false, msg: `Unknown type: ${cmd.type}` };
} else { } catch (e) {
result = { ok: false, msg: `Unknown command type: ${cmd.type}` }; result = { ok: false, msg: `Exception: ${e.message}` };
} }
} catch (e) { const finalStatus = result.requeue ? 'pending' : (result.ok ? 'done' : 'failed');
result = { ok: false, msg: `Exception: ${e.message}` }; 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'); // Run both queues concurrently — they do NOT block each other
log(`Command #${cmd.id} result: ${finalStatus === 'done' ? '✅' : (finalStatus === 'pending' ? '⏳' : '❌')} ${result.msg}`); await Promise.all([execute(buildCmd), execute(recruitCmd)]);
reportResult(cmd.id, finalStatus, result.msg); }
// ----------------------------------------------------------------
// 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 // Boot
// ---------------------------------------------------------------- // ----------------------------------------------------------------
window.addEventListener('load', () => { 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 4590 seconds (randomized)
setTimeout(pushState, 5000); setTimeout(pushState, 5000);
setInterval(pushState, STATE_INTERVAL_MS); jitterLoop(pushState, 45000, 90000);
// Poll for commands // Poll for commands every 818 seconds (randomized jitter)
setInterval(pollAndExecute, POLL_INTERVAL_MS); jitterLoop(pollAndExecute, 8000, 18000);
// Wire up game event observers after a short delay to ensure game is ready
setTimeout(setupObservers, 6000);
}); });
})(); })();

View File

@@ -62,38 +62,44 @@ def receive_state():
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# GET /api/commands/pending # GET /api/commands/pending
# Tampermonkey polls this to get the next command to execute. # 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 _fetch_pending_of_type(c, cmd_type):
def get_pending_command():
conn = get_db()
c = conn.cursor()
row = c.execute(''' row = c.execute('''
SELECT * FROM commands SELECT * FROM commands
WHERE status = 'pending' WHERE status = 'pending' AND type = ?
ORDER BY id ASC ORDER BY id ASC
LIMIT 1 LIMIT 1
''').fetchone() ''', (cmd_type,)).fetchone()
if not row: if not row:
conn.close() return None
return jsonify({'command': None})
c.execute(''' c.execute('''
UPDATE commands UPDATE commands
SET status = 'executing', updated_at = ? SET status = 'executing', updated_at = ?
WHERE id = ? WHERE id = ?
''', (datetime.utcnow().isoformat(), row['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.commit()
conn.close() conn.close()
return jsonify({ return jsonify({
'command': { 'build': build_cmd,
'id': row['id'], 'recruit': recruit_cmd
'town_id': row['town_id'],
'type': row['type'],
'payload': json.loads(row['payload'])
}
}) })