diff --git a/GrepoRemoteLoader.user.js b/GrepoRemoteLoader.user.js
new file mode 100644
index 0000000..d982662
--- /dev/null
+++ b/GrepoRemoteLoader.user.js
@@ -0,0 +1,51 @@
+// ==UserScript==
+// @name Grepolis Remote Loader
+// @namespace http://tampermonkey.net/
+// @version 4.0.0
+// @description Dynamically loads the Grepolis Remote Control bot from the server
+// @author Dimitrios
+// @match https://*.grepolis.com/game/*
+// @grant unsafeWindow
+// @grant GM_xmlhttpRequest
+// @connect grepo.haunter-pets.top
+// @updateURL https://git.haunter-pets.top/haunter/grepo-remote/raw/branch/main/GrepoRemoteLoader.user.js
+// @downloadURL https://git.haunter-pets.top/haunter/grepo-remote/raw/branch/main/GrepoRemoteLoader.user.js
+// ==/UserScript==
+
+(function() {
+ 'use strict';
+
+ // Set to your VPS domain
+ const BASE_URL = 'https://grepo.haunter-pets.top';
+
+ function loadBot() {
+ console.log('[Loader] Fetching bot code from server...');
+
+ GM_xmlhttpRequest({
+ method: 'GET',
+ url: `${BASE_URL}/api/bot?t=${Date.now()}`,
+ onload: function(response) {
+ if (response.status === 200) {
+ console.log('[Loader] Bot code downloaded successfully! Executing...');
+ try {
+ eval(response.responseText);
+ } catch (e) {
+ console.error('[Loader] Error executing bot code:', e);
+ }
+ } else {
+ console.error('[Loader] Failed to download bot. Server returned:', response.status);
+ }
+ },
+ onerror: function(err) {
+ console.error('[Loader] Connection error while trying to reach the server.', err);
+ }
+ });
+ }
+
+ // Wait for page to be ready before loading the heavy bot logic
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', loadBot);
+ } else {
+ loadBot();
+ }
+})();
diff --git a/bot_modules/00_config.js b/bot_modules/00_config.js
new file mode 100644
index 0000000..2c4bfce
--- /dev/null
+++ b/bot_modules/00_config.js
@@ -0,0 +1,21 @@
+
+ const uw = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
+ const BASE_URL = 'https://grepo.haunter-pets.top';
+
+ // ---- 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();
+ }
+
+ // ----------------------------------------------------------------
diff --git a/bot_modules/01_ui.js b/bot_modules/01_ui.js
new file mode 100644
index 0000000..f34f8a0
--- /dev/null
+++ b/bot_modules/01_ui.js
@@ -0,0 +1,45 @@
+ // Toolbar indicator button
+ // ----------------------------------------------------------------
+ const btnHtml = `
+
+ `;
+
+ let paused = false;
+
+ function togglePause() {
+ paused = !paused;
+ const label = document.getElementById('grc_label');
+ const btn = document.getElementById('grc_btn');
+ if (paused) {
+ label.textContent = 'Paused';
+ btn.style.filter = 'brightness(70%) sepia(100%) hue-rotate(-50deg) saturate(1000%) contrast(0.8)';
+ } else {
+ label.textContent = 'Remote';
+ btn.style.filter = 'brightness(294%) sepia(100%) hue-rotate(200deg) saturate(1000%) contrast(0.8)';
+ }
+ log(`Remote is now ${paused ? 'PAUSED' : 'ACTIVE'}`);
+ }
+
+ setTimeout(() => {
+ if (!document.getElementById('grc_btn')) {
+ uw.$('.tb_activities, .toolbar_activities').find('.middle').append(btnHtml);
+ }
+ }, 4000);
+
+ uw.$(document).on('click', '#grc_btn', togglePause);
+
+ // ----------------------------------------------------------------
+ // Helpers
+ // ----------------------------------------------------------------
+ function log(msg) {
+ console.log(`[GRC] ${msg}`);
+ }
+
+ function sleep(ms) {
+ return new Promise(r => setTimeout(r, ms));
+ }
+
+ // ----------------------------------------------------------------
diff --git a/bot_modules/02_state.js b/bot_modules/02_state.js
new file mode 100644
index 0000000..9052e7c
--- /dev/null
+++ b/bot_modules/02_state.js
@@ -0,0 +1,285 @@
+ // Push town state to relay
+ // ----------------------------------------------------------------
+ function gatherState() {
+ const towns = uw.ITowns?.towns || {};
+ const player = uw.Game?.player_name || '';
+ const player_id = uw.Game?.player_id ?? null;
+ const alliance_id = uw.Game?.alliance_id ?? null;
+ let alliance_name = null;
+ try {
+ const pm = uw.MM.getModels().Player[player_id];
+ if (pm && pm.attributes) alliance_name = pm.attributes.alliance_name;
+ console.log("GrepoRemote: Extracted alliance_name =", alliance_name);
+ } catch (e) {
+ console.log("GrepoRemote: Failed to extract alliance_name", e);
+ }
+
+ const total_points = uw.Game?.player_points ?? 0;
+ const world = uw.Game?.world_id || '';
+
+ const townList = Object.values(towns).map(town => {
+ const res = town.resources();
+ const buildings = town.buildings()?.attributes ?? {};
+
+ const unitsObj = {};
+ try {
+ const units = town.units();
+ if (units) {
+ Object.keys(units).forEach(k => {
+ unitsObj[k] = typeof units[k] === 'number'
+ ? units[k]
+ : (units[k]?.getAmount?.() ?? 0);
+ });
+ }
+ } catch (e) { }
+
+ let buildQueue = [];
+ try {
+ const bo = town.buildingOrders?.();
+ if (bo?.models) buildQueue = bo.models.map(m => m.attributes);
+ } catch (e) { }
+
+ let buildDataMap = {};
+ try {
+ const buildDataRaw = uw.MM?.getModels?.()?.BuildingBuildData?.[town.id]?.attributes?.building_data || {};
+ for (const k in buildDataRaw) {
+ buildDataMap[k] = {
+ buildable: buildDataRaw[k].buildable,
+ dependencies: buildDataRaw[k].dependencies_fulfilled !== false,
+ wood: buildDataRaw[k].resources_for?.wood || 0,
+ stone: buildDataRaw[k].resources_for?.stone || 0,
+ iron: buildDataRaw[k].resources_for?.iron || 0,
+ pop: buildDataRaw[k].population_for || 0,
+ build_time: buildDataRaw[k].building_time || '',
+ can_upgrade: !!buildDataRaw[k].can_upgrade,
+ enough_resources: !!buildDataRaw[k].enough_resources,
+ missing_dependencies: buildDataRaw[k].missing_dependencies || [],
+ has_max_level: !!buildDataRaw[k].has_max_level
+ };
+ }
+ } catch (e) {
+ log(`Failed to gather build data: ${e}`);
+ }
+
+ // ---- Storage capacity -----------------------------------------------
+ // res.storage / res.storage_capacity are always 0 in Grepolis.
+ // Real capacity lives in GameData keyed by the storage building level.
+ let storageCapacity = 0;
+ try {
+ // Strategy 1: dedicated getter (exists on some server versions)
+ storageCapacity = town.getStorageCapacity?.() || 0;
+
+ // Strategy 2: GameData.buildingData.storage.max_storage[level]
+ if (!storageCapacity) {
+ const storageLevel = buildings.storage ?? 0;
+ const gd = uw.GameData?.buildingData?.storage;
+ storageCapacity = gd?.max_storage?.[storageLevel]
+ || gd?.storage?.[storageLevel]
+ || 0;
+ }
+
+ // Strategy 3: resource object fallback keys
+ if (!storageCapacity) {
+ storageCapacity = res.capacity || res.storage_capacity || res.storage || 0;
+ }
+ } catch (e) {
+ log(`storage capacity lookup failed: ${e}`);
+ }
+
+ // ---- Market / Trade capacity -----------------------------------------
+ let marketCapacity = 0;
+ try {
+ const marketLevel = buildings.market ?? 0;
+ const gd = uw.GameData?.buildingData?.market;
+ marketCapacity = gd?.capacity_per_level?.[marketLevel] || 0;
+
+ // Add Trade Office bonus if present
+ if (buildings.trade_office && buildings.trade_office > 0) {
+ marketCapacity += (uw.GameData?.buildingData?.trade_office?.capacity_extra_per_level || 500) * marketLevel;
+ }
+ } catch (e) {
+ log(`market capacity lookup failed: ${e}`);
+ }
+
+ // ---- Coordinates & sea zone -----------------------------------------
+ let x = null, y = null, sea = null;
+ try {
+ x = town.getIslandCoordinateX?.() ?? null;
+ y = town.getIslandCoordinateY?.() ?? null;
+ if (typeof x === 'number' && typeof y === 'number') {
+ sea = Math.floor(x / 100) * 10 + Math.floor(y / 100);
+ }
+ } catch (e) { }
+
+ // ---- Researches -----------------------------------------------------
+ let researches = {};
+ try {
+ const r = town.researches?.();
+ if (r) researches = r.attributes ?? (typeof r === 'object' ? r : {});
+ } catch (e) { log(`[Debug] town.researches() failed: ${e}`); }
+
+ // ---- Unit Data (Costs & Dependencies) -------------------------------
+ let unitDataMap = {};
+ try {
+ const gdUnits = uw.GameData?.units || {};
+
+ for (const u in gdUnits) {
+ if (u === 'militia') continue;
+
+ const reqBuildings = gdUnits[u].building_dependencies || {};
+ const reqResearch = gdUnits[u].research_dependencies || [];
+
+ let missing_deps = {};
+ for (const reqB in reqBuildings) {
+ if ((buildings[reqB] || 0) < reqBuildings[reqB]) {
+ missing_deps[reqB] = { name: reqB, needed_level: reqBuildings[reqB] };
+ }
+ }
+ for (const reqR of reqResearch) {
+ if (!researches[reqR]) {
+ missing_deps[reqR] = { name: reqR, needed_level: 'Έρευνα' };
+ }
+ }
+
+ const cost = gdUnits[u].resources || {};
+ const w = cost.wood || 0;
+ const s = cost.stone || 0;
+ const i = cost.iron || 0;
+
+ let enough = true;
+ if (res.wood < w || res.stone < s || res.iron < i) enough = false;
+
+ unitDataMap[u] = {
+ wood: w,
+ stone: s,
+ iron: i,
+ pop: gdUnits[u].population || 0,
+ build_time: gdUnits[u].build_time || 0,
+ enough_resources: enough,
+ missing_dependencies: missing_deps
+ };
+ }
+ } catch (e) { log(`Failed to gather unit data: ${e}`); }
+
+ // ---- Farm town data -----------------------------------------------
+ let farms = [];
+ try {
+ const farmCollection = uw.MM.getOnlyCollectionByName('FarmTown');
+ const relCollection = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation');
+ if (farmCollection && relCollection) {
+ const ix = town.getIslandCoordinateX?.();
+ const iy = town.getIslandCoordinateY?.();
+ farmCollection.models.forEach(farm => {
+ if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) return;
+ relCollection.models.forEach(rel => {
+ if (rel.attributes.farm_town_id === farm.attributes.id &&
+ rel.attributes.relation_status >= 1) {
+ farms.push({
+ farm_town_id: farm.attributes.id,
+ farm_name: farm.attributes.name || '',
+ relation_id: rel.id,
+ relation_status: rel.attributes.relation_status,
+ expansion_stage: rel.attributes.expansion_stage || 0,
+ expansion_at: rel.attributes.expansion_at || 0,
+ lootable_at: rel.attributes.lootable_at || 0
+ });
+ }
+ });
+ });
+ }
+ } catch (e) { }
+
+ // ---- Extra town flags -----------------------------------------------
+ let has_premium = false;
+ let bonuses = {};
+ let wonder_points = 0;
+ try {
+ has_premium = uw.GameDataPremium?.isAdvisorActivated?.('curator') || false;
+ } catch (e) { }
+
+ return {
+ town_id: town.id,
+ town_name: town.name,
+ x, y, sea,
+ wood: res.wood,
+ stone: res.stone,
+ iron: res.iron,
+ storage: storageCapacity,
+ market_capacity: marketCapacity,
+ population: res.population,
+ points: town.getPoints?.() ?? 0,
+ god: town.god?.() ?? null,
+ buildings,
+ units: unitsObj,
+ buildingOrder: buildQueue,
+ buildData: buildDataMap,
+ unitData: unitDataMap,
+ researches,
+ has_premium,
+ bonuses,
+ wonder_points,
+ alliance_name,
+ farms,
+ };
+ });
+
+ return { player, player_id, alliance_id, total_points, world_id: world, towns: townList };
+ }
+
+ function pushState() {
+ if (paused) return;
+ try {
+ const payload = gatherState();
+ fetch(`${BASE_URL}/api/state`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ })
+ .then(() => log(`State pushed — ${payload.towns.length} towns`))
+ .catch(e => log(`State push failed: ${e}`));
+ } catch (e) {
+ log(`gatherState error: ${e}`);
+ }
+ }
+
+ // ----------------------------------------------------------------
+ // AJAX Interceptor for Zero-Delay Push
+ // ----------------------------------------------------------------
+ let pushTimeout = null;
+ function debouncedPushState() {
+ if (paused) return;
+ if (pushTimeout) clearTimeout(pushTimeout);
+ pushTimeout = setTimeout(() => {
+ log('⚡ State change detected (AJAX). Syncing to Remote...');
+ pushState();
+ }, 1200); // Wait 1.2s for memory models to update after AJAX finishes
+ }
+
+ if (uw.$) {
+ uw.$(document).ajaxComplete(function (e, xhr, opt) {
+ if (!opt || !opt.url) return;
+
+ // Ignore requests to our own bot server or map data
+ if (opt.url.includes(BASE_URL)) return;
+ if (opt.url.includes('map_tiles')) return;
+
+ // Most game actions use "action=" parameter
+ // Switching towns uses "switch_town"
+ if (opt.url.includes('action=') || opt.url.includes('switch_town')) {
+ debouncedPushState();
+ }
+ });
+ }
+
+ // ----------------------------------------------------------------
+ // Report command result back to relay
+ // ----------------------------------------------------------------
+ function reportResult(cmdId, status, message) {
+ fetch(`${BASE_URL}/api/commands/${cmdId}/result`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ status, message })
+ }).catch(e => log(`reportResult failed: ${e}`));
+ }
+
+ // ----------------------------------------------------------------
diff --git a/bot_modules/03_captcha.js b/bot_modules/03_captcha.js
new file mode 100644
index 0000000..4e649e0
--- /dev/null
+++ b/bot_modules/03_captcha.js
@@ -0,0 +1,48 @@
+ // Captcha detection via MutationObserver
+ // Watches document.body for #hcaptcha_window being added/removed.
+ // Confirmed selector from live DOM: DIV#hcaptcha_window > DIV.h-captcha
+ // ----------------------------------------------------------------
+ let captchaActive = false;
+
+ function reportCaptcha(detected) {
+ 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 })
+ }).catch(e => log(`captcha report failed: ${e}`));
+ }
+
+ function detectCaptcha() {
+ setInterval(() => {
+ const win = document.getElementById('hcaptcha_window');
+ let isVisible = false;
+
+ if (win) {
+ // Check if it's actually visible on screen (not display: none by the game)
+ const style = window.getComputedStyle(win);
+ if (style.display !== 'none' && style.visibility !== 'hidden') {
+ isVisible = true;
+ }
+ }
+
+ if (isVisible && !captchaActive) {
+ captchaActive = true;
+ paused = true;
+ const label = document.getElementById('grc_label');
+ const btn = document.getElementById('grc_btn');
+ if (label) label.textContent = '⚠ CAPTCHA';
+ if (btn) btn.style.filter = 'brightness(70%) sepia(100%) hue-rotate(300deg) saturate(1000%) contrast(0.8)';
+ log('⚠ CAPTCHA detected — bot paused, alerting server');
+ reportCaptcha(true);
+ } else if (!isVisible && captchaActive) {
+ captchaActive = false;
+ // Don't auto-resume — let the user click the button manually
+ log('✅ Captcha resolved — alert cleared (bot remains paused)');
+ reportCaptcha(false);
+ }
+ }, 1000);
+ }
+
+ // ----------------------------------------------------------------
diff --git a/bot_modules/04_execute.js b/bot_modules/04_execute.js
new file mode 100644
index 0000000..c6dfaf3
--- /dev/null
+++ b/bot_modules/04_execute.js
@@ -0,0 +1,406 @@
+ // Execute: Farm Upgrade / Unlock
+ // Iterates all farm relations. Locked villages (status 0) get
+ // unlocked; unlocked villages below max level get upgraded.
+ // Uses random 800ms–2000ms delay between each action.
+ // payload.threshold = minimum kill points to keep (default 0)
+ // ----------------------------------------------------------------
+ async function executeFarmUpgrade(cmd) {
+ const threshold = parseInt(cmd.payload?.threshold ?? 0);
+ const now = Math.floor(Date.now() / 1000);
+
+ let farmModels, relModels;
+ try {
+ farmModels = uw.MM.getOnlyCollectionByName('FarmTown')?.models;
+ relModels = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation')?.models;
+ } catch (e) {
+ return { ok: false, msg: `Cannot access farm collections: ${e.message}` };
+ }
+ if (!farmModels || !relModels) {
+ return { ok: false, msg: 'Farm collections not loaded yet' };
+ }
+
+ // Build polis list (one town per island)
+ const islandsSeen = new Set();
+ const polisList = [];
+ try {
+ for (const town of uw.MM.getCollections().Town[0].models) {
+ const { on_small_island, island_id, id } = town.attributes;
+ if (on_small_island) continue;
+ if (!islandsSeen.has(island_id)) {
+ islandsSeen.add(island_id);
+ polisList.push(id);
+ }
+ }
+ } catch (e) {
+ return { ok: false, msg: `Cannot build town list: ${e.message}` };
+ }
+
+ let upgraded = 0;
+ let unlocked = 0;
+ let skipped = 0;
+ let errors = 0;
+
+ for (const town_id of polisList) {
+ const town = uw.ITowns?.towns?.[town_id];
+ if (!town) continue;
+ const ix = town.getIslandCoordinateX();
+ const iy = town.getIslandCoordinateY();
+ if (ix == null || iy == null) continue;
+
+ for (const farm of farmModels) {
+ if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) continue;
+
+ for (const rel of relModels) {
+ if (rel.attributes.farm_town_id !== farm.attributes.id) continue;
+
+ const status = rel.attributes.relation_status;
+ const level = rel.attributes.expansion_stage || 0;
+ const expAt = rel.attributes.expansion_at || 0;
+
+ // Skip if upgrade already in progress
+ if (expAt > now) { skipped++; continue; }
+ // Skip if already max level
+ if (status === 1 && level >= 5) { skipped++; continue; }
+ // Skip if locked and we can't unlock (status -1 means enemy)
+ if (status < 0) { skipped++; continue; }
+
+ const isLocked = status === 0;
+ const action = isLocked ? 'unlock' : 'upgrade';
+
+ const requestedAction = cmd.payload?.action_type;
+ if (requestedAction && requestedAction !== action) {
+ skipped++; continue;
+ }
+
+ if (paused) {
+ return { ok: false, msg: 'Aborted due to pause/captcha' };
+ }
+
+ log(`Farm ${action}: farm_id=${farm.attributes.id} level=${level} town=${town_id}`);
+ try {
+ uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
+ model_url: `FarmTownPlayerRelation/${rel.id}`,
+ action_name: action,
+ arguments: { farm_town_id: farm.attributes.id },
+ town_id
+ });
+ isLocked ? unlocked++ : upgraded++;
+ } catch (e) { errors++; }
+
+ // Random delay between actions: 1200ms – 2500ms
+ await sleep(randInt(1200, 2500));
+ }
+ }
+ }
+
+ pushState(); // refresh farm data after upgrades
+ return { ok: true, msg: `Farm upgrade done: ${unlocked} unlocked, ${upgraded} upgraded, ${skipped} skipped, ${errors} errors` };
+ }
+
+ // ----------------------------------------------------------------
+ // Execute: Farm Loot
+ // Claims all ready farm towns across all towns that match
+ // the cmd payload (town_ids list + loot_option).
+ // Between-claim delay: random 500ms–1500ms (never below 500ms)
+ // Between-town-group delay: random 30s–90s
+ // ----------------------------------------------------------------
+ async function executeFarmLoot(cmd) {
+ const { loot_option } = cmd.payload || {};
+ const option = parseInt(loot_option) || 1;
+ const now = Math.floor(Date.now() / 1000);
+
+ let farmModels, relModels;
+ try {
+ farmModels = uw.MM.getOnlyCollectionByName('FarmTown')?.models;
+ relModels = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation')?.models;
+ } catch (e) {
+ return { ok: false, msg: `Cannot access farm collections: ${e.message}` };
+ }
+ if (!farmModels || !relModels) {
+ return { ok: false, msg: 'Farm collections not loaded yet — open island view first' };
+ }
+
+ // Build island groups using MM (all towns, not just visible)
+ const islandTownsMap = {}; // island_id -> [town_id1, town_id2]
+ try {
+ const allTowns = uw.MM.getCollections().Town[0].models;
+ for (const town of allTowns) {
+ const { on_small_island, island_id, id } = town.attributes;
+ if (on_small_island) continue;
+ if (!islandTownsMap[island_id]) islandTownsMap[island_id] = [];
+ islandTownsMap[island_id].push(id);
+ }
+ } catch (e) {
+ return { ok: false, msg: `Cannot build town list: ${e.message}` };
+ }
+ const islandList = Object.keys(islandTownsMap);
+
+ log(`Farm: processing ${islandList.length} islands with option=${option}`);
+
+ let claimed = 0;
+ let skipped = 0;
+ let errors = 0;
+
+ for (let i = 0; i < islandList.length; i++) {
+ const island_id = islandList[i];
+ const townIds = islandTownsMap[island_id];
+
+ let selected_town_id = null;
+ let lowest_total_res = Infinity;
+
+ for (const t_id of townIds) {
+ const t = uw.ITowns?.towns?.[t_id];
+ if (!t) continue;
+
+ let storageCapacity = t.getStorageCapacity?.() || 0;
+ if (!storageCapacity) {
+ const buildings = t.buildings?.()?.attributes || {};
+ const storageLevel = buildings.storage ?? 0;
+ const gd = uw.GameData?.buildingData?.storage;
+ storageCapacity = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0;
+ }
+
+ const res = t.resources?.() || {};
+ const w = res.wood || 0;
+ const s = res.stone || 0;
+ const ir = res.iron || 0;
+
+ // If completely full (all 3 resources >= max storage), skip this town
+ if (storageCapacity > 0 && w >= storageCapacity && s >= storageCapacity && ir >= storageCapacity) {
+ continue;
+ }
+
+ // Pick town with most space (lowest total resources)
+ const total_res = w + s + ir;
+ if (total_res < lowest_total_res) {
+ lowest_total_res = total_res;
+ selected_town_id = t_id;
+ }
+ }
+
+ if (!selected_town_id) {
+ log(`Farm: Skipping island ${island_id} (All towns are 100% full)`);
+ skipped++;
+ continue;
+ }
+
+ const town_id = selected_town_id;
+ const town = uw.ITowns?.towns?.[town_id];
+ if (!town) { skipped++; continue; }
+
+ // Use the same method as the original script
+ const ix = town.getIslandCoordinateX();
+ const iy = town.getIslandCoordinateY();
+
+ if (ix == null || iy == null) { skipped++; continue; }
+
+ // Find ready farms on this island (mirrors original getLootableFarms exactly)
+ const readyFarms = [];
+ for (const farm of farmModels) {
+ if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) continue;
+ for (const rel of relModels) {
+ if (
+ rel.attributes.farm_town_id === farm.attributes.id &&
+ rel.attributes.relation_status === 1 &&
+ (!rel.attributes.lootable_at || now >= rel.attributes.lootable_at)
+ ) {
+ readyFarms.push({
+ town_id,
+ farm_town_id: rel.attributes.farm_town_id,
+ relation_id: rel.id
+ });
+ }
+ }
+ }
+
+ if (readyFarms.length === 0) { skipped++; continue; }
+ log(`Farm: ${readyFarms.length} ready on island of town ${town_id}`);
+
+ for (const farm of readyFarms) {
+ if (paused) {
+ return { ok: false, msg: 'Aborted due to pause/captcha' };
+ }
+
+ try {
+ uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
+ model_url: `FarmTownPlayerRelation/${farm.relation_id}`,
+ action_name: 'claim',
+ arguments: { farm_town_id: farm.farm_town_id, type: 'resources', option },
+ town_id: farm.town_id
+ });
+ claimed++;
+ } catch (e) { errors++; }
+
+ // Random per-claim delay: 1000ms – 2200ms
+ await sleep(randInt(1000, 2200));
+ }
+
+ // Refresh map icons after claiming (same as original)
+ try { uw.WMap.removeFarmTownLootCooldownIconAndRefreshLootTimers(); } catch (e) { }
+
+ // Random between-island delay: 30s – 90s (only if more islands remain)
+ if (i < islandList.length - 1) {
+ if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
+ const gap = randInt(30000, 90000);
+ log(`Farm: island done. Waiting ${(gap / 1000).toFixed(0)}s before next island...`);
+ await sleep(gap);
+ }
+ }
+
+ return { ok: true, msg: `Farm done: ${claimed} claimed, ${skipped} islands skipped, ${errors} errors` };
+ }
+
+
+ // ----------------------------------------------------------------
+ // Execute: Build
+ // ----------------------------------------------------------------
+ async function executeBuild(cmd) {
+ const { town_id, payload } = cmd;
+ const { building_id } = payload;
+
+ const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
+ if (!town) {
+ return { ok: false, msg: `Town ${town_id} not found in ITowns` };
+ }
+
+ // Check build queue
+ const queueLen = town.buildingOrders?.()?.length ?? 0;
+ const hasCurator = uw.GameDataPremium?.isAdvisorActivated?.('curator');
+ const maxQueue = hasCurator ? 7 : 2;
+ if (queueLen >= maxQueue) {
+ return { ok: false, requeue: true, msg: `Build queue full (${queueLen}/${maxQueue})` };
+ }
+
+ // Check resources
+ try {
+ const buildData = uw.MM.getModels?.()?.BuildingBuildData?.[town_id]
+ ?.attributes?.building_data?.[building_id];
+ if (buildData) {
+ const res = town.resources();
+ const { resources_for, population_for } = buildData;
+ if (town.getAvailablePopulation?.() < population_for) {
+ return { ok: false, requeue: true, msg: `Not enough population for ${building_id}` };
+ }
+ if (res.wood < resources_for.wood || res.stone < resources_for.stone || res.iron < resources_for.iron) {
+ return { ok: false, requeue: true, msg: `Not enough resources for ${building_id}` };
+ }
+ }
+ } catch (e) {
+ log(`Resource check skipped: ${e}`);
+ }
+
+ // 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);
+
+ if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
+
+ uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
+ model_url: 'BuildingOrder',
+ action_name: 'buildUp',
+ arguments: { building_id },
+ town_id
+ });
+
+ await sleep(500);
+ return { ok: true, msg: `buildUp ${building_id} queued` };
+ }
+
+ // ----------------------------------------------------------------
+ // Execute: Recruit
+ // ----------------------------------------------------------------
+ async function executeRecruit(cmd) {
+ const { town_id, payload } = cmd;
+ const { unit_id, amount } = payload;
+
+ const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
+ if (!town) {
+ return { ok: false, msg: `Town ${town_id} not found` };
+ }
+
+ // Determine endpoint based on unit type
+ const navalUnits = [
+ 'big_transporter', 'small_transporter', 'bireme',
+ 'attack_ship', 'trireme', 'colonize_ship', 'sea_monster'
+ ];
+ 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);
+
+ if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
+
+ uw.gpAjax.ajaxPost(endpoint, 'build', {
+ unit_id,
+ amount: parseInt(amount) || 1,
+ town_id
+ });
+
+ await sleep(500);
+ return { ok: true, msg: `Recruit ${amount}x ${unit_id} submitted` };
+ }
+
+ // ----------------------------------------------------------------
+ // Execute: Market
+ // ----------------------------------------------------------------
+ async function executeMarketOffer(cmd) {
+ const { town_id, payload } = cmd;
+ const { offer, offer_type, demand, demand_type, max_delivery_time, visibility } = payload;
+
+ const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
+ if (!town) {
+ return { ok: false, msg: `Town ${town_id} not found` };
+ }
+
+ const reactionMs = randInt(800, 2500);
+ log(`Waiting ${reactionMs}ms before firing market offer (reaction time)...`);
+ await sleep(reactionMs);
+
+ if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
+
+ uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
+ model_url: 'CreateOffers/' + town_id,
+ action_name: 'createOffer',
+ captcha: null,
+ arguments: {
+ offer, offer_type, demand, demand_type, max_delivery_time, visibility
+ }
+ });
+
+ await sleep(500);
+ return { ok: true, msg: `Market offer posted: ${offer} ${offer_type} => ${demand} ${demand_type}` };
+ }
+
+ // ----------------------------------------------------------------
+ // Execute: Research (Academy)
+ // ----------------------------------------------------------------
+ async function executeResearch(cmd) {
+ const { town_id, payload } = cmd;
+ const { research_id } = payload;
+
+ const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
+ if (!town) {
+ return { ok: false, msg: `Town ${town_id} not found` };
+ }
+
+ const reactionMs = randInt(800, 2500);
+ log(`Waiting ${reactionMs}ms before firing research (reaction time)...`);
+ await sleep(reactionMs);
+
+ if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
+
+ uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
+ model_url: 'ResearchOrder',
+ action_name: 'research',
+ arguments: { id: research_id },
+ town_id: town_id
+ });
+
+ await sleep(500);
+ return { ok: true, msg: `Research ${research_id} queued` };
+ }
+
+ // ----------------------------------------------------------------
diff --git a/bot_modules/05_main.js b/bot_modules/05_main.js
new file mode 100644
index 0000000..668f526
--- /dev/null
+++ b/bot_modules/05_main.js
@@ -0,0 +1,149 @@
+ // Poll for and execute pending commands (build + recruit + market)
+ // ----------------------------------------------------------------
+ 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?player_id=${player_id}`);
+ cmdData = await res.json();
+ } catch (e) {
+ log(`Poll failed: ${e}`);
+ return;
+ }
+
+ // Build queue, Recruit queue and Market queue are independent
+ const buildCmd = cmdData.build;
+ const recruitCmd = cmdData.recruit;
+ const marketCmd = cmdData.market;
+ const researchCmd = cmdData.research;
+ const farmCmd = cmdData.farm;
+ const farmUpgradeCmd = cmdData.farm_upgrade;
+
+
+
+ if (cmdData.sync_requested) {
+ log('Sync requested by server — pushing state immediately');
+ pushState();
+ }
+
+ const execute = async (cmd) => {
+ if (!cmd) return;
+ log(`Executing command #${cmd.id} — type:${cmd.type} town:${cmd.town_id}`);
+ if (paused) {
+ log(`[Paused] Ignoring command #${cmd.id}`);
+ return;
+ }
+
+ let result;
+ try {
+ if (cmd.type === 'build') result = await executeBuild(cmd);
+ else if (cmd.type === 'recruit') result = await executeRecruit(cmd);
+ else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd);
+ else if (cmd.type === 'research') result = await executeResearch(cmd);
+ else if (cmd.type === 'farm_loot') result = await executeFarmLoot(cmd);
+ else if (cmd.type === 'farm_upgrade') result = await executeFarmUpgrade(cmd);
+ else result = { ok: false, msg: `Unknown type: ${cmd.type}` };
+ } catch (e) {
+ result = { ok: false, msg: `Exception: ${e}` };
+ }
+ 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);
+ };
+
+ // Run sequentially — humans cannot perform 3 actions simultaneously!
+ await execute(buildCmd);
+ await execute(recruitCmd);
+ await execute(marketCmd);
+ await execute(researchCmd);
+ await execute(farmCmd);
+ await execute(farmUpgradeCmd);
+
+ // Auto-farm: if enabled, claim all ready farms (no explicit command needed)
+ const farmSettings = cmdData.farm_settings || {};
+ if (farmSettings.enabled && !farmCmd) {
+ const nowTs = Math.floor(Date.now() / 1000);
+ // Check if ANY farm relation is ready
+ let readyFarms = [];
+ try {
+ const coll = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation');
+ readyFarms = coll?.models?.filter(r =>
+ r.attributes.relation_status === 1 &&
+ (r.attributes.lootable_at || 0) <= nowTs
+ ) || [];
+ } catch (e) { /* silent */ }
+
+ if (readyFarms.length > 0) {
+ // Check if the CURRENT town's warehouse is full (>95%)
+ let allFull = true;
+ let claimedAny = false;
+
+ // Iterate over all towns that have ready farms
+ const towns = Object.values(uw.ITowns?.towns || {});
+ for (const town of towns) {
+ const storage = town.resources?.()?.storage || town.get?.('storage') || 0;
+ const wood = town.resources?.()?.wood || town.get?.('wood') || 0;
+ const stone = town.resources?.()?.stone || town.get?.('stone') || 0;
+ const iron = town.resources?.()?.iron || town.get?.('iron') || 0;
+ if (!storage) continue;
+
+ const maxRes = Math.max(wood, stone, iron);
+ const pct = maxRes / storage;
+
+ if (pct < 0.95) {
+ // This town has room — loot using its town_id context
+ allFull = false;
+ log(`⚡ Auto-farm: looting into town ${town.get?.('name')} (${Math.round(pct*100)}% full)`);
+ await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } });
+ claimedAny = true;
+ pushState();
+ break; // one loot pass is enough per poll cycle
+ }
+ }
+
+ if (allFull) {
+ log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle');
+ // Report full status to backend so farm.html can show notice
+ try {
+ await fetch(`${BASE_URL}/api/farm_status?player_id=${uw.Game.player_id}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ warehouse_full: true })
+ });
+ } catch(e) {}
+ } else if (claimedAny) {
+ // Clear the full flag
+ try {
+ await fetch(`${BASE_URL}/api/farm_status?player_id=${uw.Game.player_id}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ warehouse_full: false })
+ });
+ } catch(e) {}
+ }
+ }
+ }
+ }
+
+ // ----------------------------------------------------------------
+ // Boot
+ // ----------------------------------------------------------------
+ window.addEventListener('load', () => {
+ log('Grepolis Remote Control v3.5.9 loaded');
+
+ // Start captcha watcher immediately
+ detectCaptcha();
+
+ // Push state once after load, then heartbeat every 1-2 minutes
+ // The AJAX interceptor handles the real-time syncing!
+ setTimeout(pushState, 5000);
+ jitterLoop(pushState, 60000, 120000);
+
+ // Poll for commands every 8–18 seconds (randomized jitter)
+ jitterLoop(pollAndExecute, 8000, 18000);
+ });
+
+})();
diff --git a/routes/api.py b/routes/api.py
index 9da1e85..989a838 100644
--- a/routes/api.py
+++ b/routes/api.py
@@ -2,6 +2,8 @@ from flask import Blueprint, request, jsonify
from db import get_db
import json
from datetime import datetime
+import os
+from flask import make_response
api = Blueprint('api', __name__)
@@ -255,3 +257,34 @@ def farm_status():
row = conn.execute('SELECT value FROM kv_store WHERE key=?', (kv_key,)).fetchone()
conn.close()
return jsonify(json.loads(row['value']) if row else {'warehouse_full': False})
+
+# ------------------------------------------------------------------
+# GET /api/bot
+# Serves the modular bot code concatenated into a single response
+# ------------------------------------------------------------------
+@api.route('/api/bot', methods=['GET'])
+def serve_bot():
+ bot_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'bot_modules')
+ if not os.path.exists(bot_dir):
+ return make_response("Bot modules directory not found", 404)
+
+ modules = sorted([f for f in os.listdir(bot_dir) if f.endswith('.js')])
+
+ combined_code = []
+ combined_code.append("(function() {")
+ combined_code.append(" 'use strict';\n")
+
+ for module in modules:
+ with open(os.path.join(bot_dir, module), 'r', encoding='utf-8') as f:
+ combined_code.append(f" // --- BEGIN {module} ---")
+ combined_code.append(f.read())
+ combined_code.append(f" // --- END {module} ---\n")
+
+ combined_code.append("})();")
+
+ response = make_response("\\n".join(combined_code))
+ response.headers['Content-Type'] = 'application/javascript'
+ # Prevent caching so updates are instant
+ response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
+ return response
+
diff --git a/split_script.py b/split_script.py
new file mode 100644
index 0000000..4808289
--- /dev/null
+++ b/split_script.py
@@ -0,0 +1,68 @@
+import os
+import re
+
+input_file = '/media/haunter/e11cc3d4-c894-42cd-8c43-fe2cb25293fd/Vcode_raidbot/grepo-remote/GrepolisRemoteControl.user.js'
+out_dir = '/media/haunter/e11cc3d4-c894-42cd-8c43-fe2cb25293fd/Vcode_raidbot/grepo-remote/bot_modules'
+
+if not os.path.exists(out_dir):
+ os.makedirs(out_dir)
+
+with open(input_file, 'r', encoding='utf-8') as f:
+ lines = f.readlines()
+
+# Find the start of the IIFE
+start_idx = 0
+for i, line in enumerate(lines):
+ if line.strip().startswith('(function'):
+ start_idx = i + 1
+ break
+
+# Extract the body of the IIFE (excluding the last '})();')
+end_idx = len(lines)
+for i in range(len(lines) - 1, -1, -1):
+ if line.strip().startswith('})();'):
+ end_idx = i
+ break
+
+# Remove use strict if present
+body_lines = [line for line in lines[start_idx:end_idx] if 'use strict' not in line]
+
+# We will group them manually into files based on logical sections
+sections = []
+current_section = []
+for line in body_lines:
+ current_section.append(line)
+
+content = "".join(body_lines)
+
+# Split by known markers
+blocks = {
+ '00_config.js': [],
+ '01_ui.js': [],
+ '02_state.js': [],
+ '03_captcha.js': [],
+ '04_execute.js': [],
+ '05_main.js': []
+}
+
+current_file = '00_config.js'
+
+for line in body_lines:
+ if '// Toolbar indicator button' in line:
+ current_file = '01_ui.js'
+ elif '// Push town state to relay' in line:
+ current_file = '02_state.js'
+ elif '// Captcha detection' in line:
+ current_file = '03_captcha.js'
+ elif '// Execute: Farm Upgrade' in line:
+ current_file = '04_execute.js'
+ elif '// Poll for and execute pending commands' in line:
+ current_file = '05_main.js'
+
+ blocks[current_file].append(line)
+
+for filename, lines in blocks.items():
+ with open(os.path.join(out_dir, filename), 'w', encoding='utf-8') as f:
+ f.writelines(lines)
+
+print("Split completed.")