diff --git a/GrepolisRemoteControl.user.js b/GrepolisRemoteControl.user.js index 94cf07c..401c3a6 100644 --- a/GrepolisRemoteControl.user.js +++ b/GrepolisRemoteControl.user.js @@ -1,8 +1,8 @@ // ==UserScript== // @name Grepolis Remote Control // @namespace http://tampermonkey.net/ -// @version 2.7 -// @description Polls grepo.haunter-pets.top for remote commands and executes them in-game +// @version 2.8 +// @description Polls grepo.haunter-pets.top for remote commands and executes them in-game (Multi-Player) // @author Dimitrios // @match https://*.grepolis.com/game/* // @grant unsafeWindow @@ -287,7 +287,9 @@ let captchaActive = false; function reportCaptcha(detected) { - fetch(`${BASE_URL}/api/captcha/alert`, { + const player_id = uw.Game?.player_id; + if (!player_id) return; + fetch(`${BASE_URL}/api/captcha/alert?player_id=${player_id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ detected }) @@ -447,10 +449,12 @@ // ---------------------------------------------------------------- async function pollAndExecute() { if (paused) return; + const player_id = uw.Game?.player_id; + if (!player_id) return; let cmdData; try { - const res = await fetch(`${BASE_URL}/api/commands/pending`); + const res = await fetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}`); cmdData = await res.json(); } catch (e) { log(`Poll failed: ${e}`); @@ -487,7 +491,7 @@ // Boot // ---------------------------------------------------------------- window.addEventListener('load', () => { - log('Grepolis Remote Control v2.5 loaded'); + log('Grepolis Remote Control v2.8 loaded'); // Start captcha watcher immediately detectCaptcha(); diff --git a/db.py b/db.py index 295d543..7f655a2 100644 --- a/db.py +++ b/db.py @@ -62,6 +62,7 @@ def init_db(): 'ALTER TABLE town_state ADD COLUMN x REAL', 'ALTER TABLE town_state ADD COLUMN y REAL', 'ALTER TABLE town_state ADD COLUMN sea INTEGER', + 'ALTER TABLE commands ADD COLUMN player_id TEXT', ]: try: c.execute(_col) diff --git a/routes/api.py b/routes/api.py index f2b6ea9..a0df951 100644 --- a/routes/api.py +++ b/routes/api.py @@ -65,13 +65,13 @@ def receive_state(): # Returns one 'build' AND one 'recruit' command independently, # so both queues are served in parallel without blocking each other. # ------------------------------------------------------------------ -def _fetch_pending_of_type(c, cmd_type): +def _fetch_pending_of_type(c, cmd_type, player_id): row = c.execute(''' SELECT * FROM commands - WHERE status = 'pending' AND type = ? + WHERE status = 'pending' AND type = ? AND player_id = ? ORDER BY id ASC LIMIT 1 - ''', (cmd_type,)).fetchone() + ''', (cmd_type, player_id)).fetchone() if not row: return None c.execute(''' @@ -88,12 +88,16 @@ def _fetch_pending_of_type(c, cmd_type): @api.route('/api/commands/pending', methods=['GET']) def get_pending_command(): + player_id = request.args.get('player_id') + if not player_id: + return jsonify({'error': 'no player_id provided'}), 400 + conn = get_db() c = conn.cursor() - build_cmd = _fetch_pending_of_type(c, 'build') - recruit_cmd = _fetch_pending_of_type(c, 'recruit') - market_cmd = _fetch_pending_of_type(c, 'market_offer') + build_cmd = _fetch_pending_of_type(c, 'build', player_id) + recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id) + market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id) conn.commit() conn.close() @@ -133,16 +137,22 @@ def command_result(cmd_id): # ------------------------------------------------------------------ @api.route('/api/captcha/alert', methods=['POST']) def captcha_alert(): + 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 {} detected = bool(data.get('detected', False)) + kv_key = f'captcha_active_{player_id}' + conn = get_db() conn.execute(''' INSERT INTO kv_store (key, value, updated_at) - VALUES ('captcha_active', ?, ?) + VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at - ''', ('1' if detected else '0', datetime.utcnow().isoformat())) + ''', (kv_key, '1' if detected else '0', datetime.utcnow().isoformat())) conn.commit() conn.close() return jsonify({'ok': True}) diff --git a/routes/dashboard.py b/routes/dashboard.py index 997decb..e9e7aa1 100644 --- a/routes/dashboard.py +++ b/routes/dashboard.py @@ -12,7 +12,14 @@ dashboard = Blueprint('dashboard', __name__) # ------------------------------------------------------------------ @dashboard.route('/') def index(): - return render_template('dashboard.html') + conn = get_db() + players = conn.execute('SELECT DISTINCT player, player_id FROM town_state WHERE player IS NOT NULL ORDER BY player ASC').fetchall() + conn.close() + return render_template('index.html', players=players) + +@dashboard.route('/player/') +def player_dashboard(player_id): + return render_template('dashboard.html', player_id=player_id) # ------------------------------------------------------------------ @@ -21,13 +28,15 @@ def index(): # ------------------------------------------------------------------ @dashboard.route('/dashboard/towns', methods=['GET']) def get_towns(): + player_id = request.args.get('player_id') conn = get_db() rows = conn.execute(''' SELECT town_id, town_name, player, player_id, alliance_id, world_id, x, y, sea, data, updated_at FROM town_state + WHERE player_id = ? ORDER BY town_name ASC - ''').fetchall() + ''', (player_id, )).fetchall() conn.close() towns = [] @@ -74,10 +83,11 @@ def get_towns(): # ------------------------------------------------------------------ @dashboard.route('/dashboard/client-status', methods=['GET']) def client_status(): + player_id = request.args.get('player_id') conn = get_db() row = conn.execute(''' - SELECT MAX(updated_at) AS last_seen FROM town_state - ''').fetchone() + SELECT MAX(updated_at) AS last_seen FROM town_state WHERE player_id = ? + ''', (player_id, )).fetchone() conn.close() last_seen = row['last_seen'] if row else None @@ -100,9 +110,10 @@ def client_status(): # ------------------------------------------------------------------ @dashboard.route('/dashboard/captcha-status', methods=['GET']) def captcha_status(): + player_id = request.args.get('player_id') conn = get_db() row = conn.execute( - "SELECT value FROM kv_store WHERE key = 'captcha_active'" + "SELECT value FROM kv_store WHERE key = ?", (f'captcha_active_{player_id}', ) ).fetchone() conn.close() active = bool(row and row['value'] == '1') @@ -115,13 +126,15 @@ def captcha_status(): # ------------------------------------------------------------------ @dashboard.route('/dashboard/commands', methods=['GET']) def get_commands(): + player_id = request.args.get('player_id') conn = get_db() rows = conn.execute(''' SELECT id, town_id, town_name, type, payload, status, result_msg, created_at, updated_at FROM commands + WHERE player_id = ? ORDER BY id DESC LIMIT 50 - ''').fetchall() + ''', (player_id, )).fetchall() conn.close() return jsonify([dict(r) for r in rows]) @@ -138,7 +151,7 @@ def create_command(): if not data: return jsonify({'error': 'no data'}), 400 - required = ['town_id', 'type', 'payload'] + required = ['town_id', 'type', 'payload', 'player_id'] for field in required: if field not in data: return jsonify({'error': f'missing field: {field}'}), 400 @@ -149,7 +162,7 @@ def create_command(): # Reject if the Tampermonkey client is offline (no state push in last 150 s) conn = get_db() - row = conn.execute('SELECT MAX(updated_at) AS last_seen FROM town_state').fetchone() + row = conn.execute('SELECT MAX(updated_at) AS last_seen FROM town_state WHERE player_id = ?', (data['player_id'], )).fetchone() last_seen = row['last_seen'] if row else None client_online = False if last_seen: @@ -165,15 +178,16 @@ def create_command(): c = conn.cursor() c.execute(''' - INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at) - VALUES (?, ?, ?, ?, 'pending', ?, ?) + INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id) + VALUES (?, ?, ?, ?, 'pending', ?, ?, ?) ''', ( str(data['town_id']), data.get('town_name', ''), cmd_type, json.dumps(data['payload']), datetime.utcnow().isoformat(), - datetime.utcnow().isoformat() + datetime.utcnow().isoformat(), + str(data['player_id']) )) cmd_id = c.lastrowid conn.commit() @@ -202,6 +216,7 @@ def cancel_command(cmd_id): # ------------------------------------------------------------------ @dashboard.route('/dashboard/commands/fail-stale', methods=['POST']) def fail_stale_commands(): + player_id = request.args.get('player_id') conn = get_db() c = conn.cursor() c.execute(''' @@ -209,8 +224,8 @@ def fail_stale_commands(): SET status = 'failed', result_msg = 'Client went offline', updated_at = ? - WHERE status IN ('pending', 'executing') - ''', (datetime.utcnow().isoformat(),)) + WHERE status IN ('pending', 'executing') AND player_id = ? + ''', (datetime.utcnow().isoformat(), player_id)) affected = c.rowcount conn.commit() conn.close() diff --git a/static/js/api.js b/static/js/api.js index f2ccb64..659c813 100644 --- a/static/js/api.js +++ b/static/js/api.js @@ -4,7 +4,7 @@ window.fetchTowns = async function() { try { - const res = await fetch('/dashboard/towns'); + const res = await fetch('/dashboard/towns?player_id=' + window.PLAYER_ID); window.towns = await res.json(); window.renderTowns(); window.updateServerStatus(true); @@ -22,14 +22,14 @@ window.fetchTowns = async function() { window.fetchClientStatus = async function() { try { - const res = await fetch('/dashboard/client-status'); + const res = await fetch('/dashboard/client-status?player_id=' + window.PLAYER_ID); const data = await res.json(); const isOnline = data.online === true; // Detect transition: online → offline if (window.wasClientOnline === true && !isOnline) { // Fail all queued commands immediately - await fetch('/dashboard/commands/fail-stale', { method: 'POST' }); + await fetch('/dashboard/commands/fail-stale?player_id=' + window.PLAYER_ID, { method: 'POST' }); window.fetchLog(); } @@ -43,7 +43,7 @@ window.fetchClientStatus = async function() { window.fetchLog = async function() { try { - const res = await fetch('/dashboard/commands'); + const res = await fetch('/dashboard/commands?player_id=' + window.PLAYER_ID); const cmds = await res.json(); window.renderLog(cmds); } catch (e) {} @@ -161,7 +161,8 @@ window.sendCommand = async function() { town_id: town.town_id, town_name: town.town_name, type, - payload + payload, + player_id: window.PLAYER_ID }) }); const data = await res.json(); @@ -184,7 +185,7 @@ window.cancelCommand = async function(id) { window.fetchCaptchaStatus = async function() { try { - const res = await fetch('/dashboard/captcha-status'); + const res = await fetch('/dashboard/captcha-status?player_id=' + window.PLAYER_ID); const data = await res.json(); const banner = document.getElementById('captcha-banner'); if (!banner) return; diff --git a/templates/dashboard.html b/templates/dashboard.html index 1a0c3d6..397c16b 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -194,6 +194,9 @@ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..c5eb7dc --- /dev/null +++ b/templates/index.html @@ -0,0 +1,75 @@ + + + + + +Grepolis Remote Dashboard - Select Player + + + + +
+

⚔️ Grepolis Remote

+

Select an active account to manage

+ + {% if not players %} +

No players found! Install the Tampermonkey script and log into the game first.

+ {% endif %} + + {% for p in players %} + + {{ p.player }} (ID: {{ p.player_id }}) + + {% endfor %} +
+ +