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==
// @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;
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 command type: ${cmd.type}` };
}
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}` };
}
const finalStatus = result.requeue ? 'pending' : (result.ok ? 'done' : 'failed');
log(`Command #${cmd.id} result: ${finalStatus === 'done' ? '✅' : (finalStatus === 'pending' ? '⏳' : '❌')} ${result.msg}`);
log(`Command #${cmd.id}: ${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 4590 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 818 seconds (randomized jitter)
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
# 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']))
conn.commit()
conn.close()
return jsonify({
'command': {
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({
'build': build_cmd,
'recruit': recruit_cmd
})