Imported from Th1nhNg0/farm-merge-bot (
AGENTS.md). Install upstream withnpx skills add Th1nhNg0/farm-merge-bot. Copyright stays with the author.
AGENTS.md — FMV injection (auto-farm)
Guide for AI agents and maintainers working on this codebase.
Project version: 1.14.1 (see CHANGELOG.md).
What this is
CDP-based bot for Farm Merge Valley, injected into the game when it runs inside a
Discord Activity (https://<app-id>.discordsays.com/?instance_id=...). All bot
logic runs IN-FRAME by calling the game's own webpack modules — no pixel automation.
Layout
| File | Purpose |
|---|---|
src/cdp_lib.mjs |
CDP client (Node built-in WebSocket); port discovery via DevToolsActivePort + /json/version fallback; findGameTarget() matches the discordsays.com iframe |
src/poller.js |
In-frame poller; captures every webpack runtime require into window.__FMV_rt; prepends the pause-protection patch so fresh game loads are covered |
src/hunter.js |
In-frame module hunter; picks the main runtime and re-discovers root container / farm services / component map / MergeTrigger ctor structurally (no hardcoded ids) |
src/fmv_helper.js |
window.FMV v4: board / merge / move / swap / spawnCrate / services / req / I / root / rootServices |
src/menu.js |
In-game FMV Bot overlay (top-right): Farm/Cheat tabs — Farm groups: Board (Merge/Sort/Harvest/Fill), Work (Orders / Auto Orders with instant production finish / Clear / Auto Clear), Social (Visit/½ Gold); Cheat groups: Currency grants + Market (Flash Deals buy-all, Auto Flash Deals toggle with per-type selection, Tap Bubbles) + Speed (Finish Regen); toggle loops show ■ STOP; exposes window.FMV.menu; prepends plan.js + util.js sources |
src/util.js |
In-frame shared game-access helpers (window.FMVUtil): readBoard / tileModel / tileAt / getTapRouter / walkBehaviorRegistries / isProductCollectable / collectablesOnBoard / forEachCell |
src/install.mjs |
One-shot installer — poller → hunter → FMV → menu, evaluated live in the frame; exports VERSION |
src/eval.mjs |
One-off Runtime.evaluate in the game frame |
src/pause_protect.js |
Background-tab protection: fakes document.visibilityState/hasFocus, swallows visibilitychange, bridges requestAnimationFrame with a timer watchdog (the game's Pixi Ticker resolves bare rAF at call time, so the bridge is picked up on the next tick) |
src/plan.js |
Merge planner (5/10/15 chain grouping, move/swap ops) |
src/hunter.js / src/fmv_helper.js |
See above |
auto-farm-install.bat |
Launcher for src/install.mjs |
CHANGELOG.md |
Version history |
Key facts (Discord build, verified 2026-08-06)
Exploit surfaces (verified live 2026-08-15, client-authoritative backend)
-
Currency print:
rewardService._parseAndClaimRewards([{key,amount},...])(farm servicewindow.FMV.services().rewardService) grants inventory currency and callsautosave.forceSave(). Verified keys:coins,gems,energy,crates,wood,stone. Grants persist through game restarts (server stores the client's save as-is). Wrapped asFMV.grant(rewards). -
Object spawn:
rewardService._claimObjectRewards([{key,amount},...])creates a storage bubble (world entity,id='__UNIQUE__', NOT a grid cell) viastorageBubble.createBubble; blueprint keys verified:reward_crate_gold,reward_crate_gold_gazebo,cow_1..3,wood_1,stone_1,coin_1,gem_1,energy_1,ticket. Bubbles are SAVED (StorageBubbleModel) and restored on reload. Collect (verified):tapRouter._simulateClickdoes NOT work (bubbles have no valid GridPosition — off-map, the router rejects them). The correct path is thestorageBubbleTapfamily processor_onStorageBubbleTapped(bubble)(spawns each content item via_spawnObject→ world +moveContentToCellbehaviors; items land on the grid after ~6-20s in hidden tabs). Never tap a bubble twice — the content is consumed only when the pop completes, so double-taps duplicate the items. Do NOT call_initiateBubblePopdirectly (its async destroy crashed the game loop once). Wrapped asFMV.spawn/FMV.collectBubbles(settle rounds + 90s cross-call double-tap guard). -
CRATE SPAWN CAVEAT (verified, caused a freeze): crate blueprints must NEVER go through the bubble path —
_onStorageBubbleTappedadds them to the world butmoveContentToCellnever completes for crates, so they pile up as broken world objects with Cooldown behaviors (40+ of them froze the game loop). Crates are placed DIRECTLY instead: factory object + GridPosition ctor (new gpCtor({column,row}), ctor obtained from any board entity'sgetBehavior(I.GridPosition).constructor) + world.addGameObject- mapGrid.setContent + position.copyFrom. Produces real crates
(crateReward + cooldown + RewardCrateCooldown timer). The farm's gold
crates are the GAZEBO family (
reward_crate_gold_gazebo→ object idreward_crate_gold:gazebo— what the ½ Gold button targets), NOT the plainreward_crate_gold(reward_crate:gold).
- mapGrid.setContent + position.copyFrom. Produces real crates
(crateReward + cooldown + RewardCrateCooldown timer). The farm's gold
crates are the GAZEBO family (
-
Timer finishing:
timer._timerModel._timersentries with_state==='ACTIVE'→_remaining=0; _onFinish()fires the game's own completion path. Labels:RewardCrateCooldown(3-day crates),Order_*(productions),regenerate_*(energy/gems/crates),Cooldown:col,row(source chops, used by Clear). Wrapped asFMV.finishTimers(labelPrefix). -
Misc:
cheats.allowSavedefaults true;gameObjectFactoryhascreateById/createFromBlueprint/createFromSerializedData(no grid-placement helper — placement must reuse the move/swap machinery); storage slots are serialized{data, blueprint}(removeFromStorage does NOT spawn);detachedObjectsmodel_storeis empty in normal play;IAPservice exists but is off-limits (real money). -
Build is heavily obfuscated: runtime requires are
_0x552fd9/_0x34ae8b; string literals and some property names are mangled. Everything is discovered structurally at runtime — do NOT hardcode module ids or runtime names. -
Reference module ids (unreliable, may change): 69358 root container (export
H, services under_nonCriticalServices), 84511 component mapI, 10295HCMergeTrigger ctor, 28464 drop systemB, 60307 merge executor, 19376 crate spawn. -
Farm services:
req(69358).H._nonCriticalServices.timer._updatableGroup._members[i]._services(first member with.mapGrid). Contains mapGrid, world, gridFilter, axonometricProjection, crateQueue, crateContent, shovelService, interactionService… -
Board:
S.mapGrid._cells(Map, ~1006 cells). Entity API (unmangled):getObjectIdAndTier(),getBlueprintID(),hasBehavior(I.Mergeable),getBehavior(I.Mergeable). -
Crate spawn event:
rootServices().hudServiceRegistry._activeService._commonEvents.spawnCrates. -
Source tap machinery (Clear button uses this, no click simulation): every entity's
onBehaviorAddedis a shared event with ~177 behavior-family registries; each registry (_filter._behaviorTypes) exposesonGameObjectAdded._subscribers[0].context. The resource-gate payment service (context has_attemptPayment) does: worker check (gameWorkers.hasEnoughWorkers), energy deduction (inventory.deductItems), then marks the sourcelootable(tile save modellootable.loot). The lootable collector (registry filter containsinteractionTap+lootable, context has_onInteractionAdded) spawns the loot objects and -1 hp.tapRouter._simulateClickonly works for harvestables (animals) — sources need the popout→confirm flow, so call_attemptPaymentdirectly. -
Source chop = cooldown timer: paying a source starts a
MapSourceCooldown:col,rowtimer inFMV.rootServices().timer. _timerModel._timers(Map keyed by timerId; entries have_state,_remaining,_onFinish— the game's own completion path) plus a worker hold (tileworkerData+ entityWorkerDatabehavior). The source is onlylootablewhen the timer expires (worker released). VERIFIED 2026-08-15: finishing the timer alone (_remaining=0; _onFinish()) does NOT release the worker or clear the tile in this build — the cooldown processor (_getContentByTimerID) finds no entity with a matchingCooldownbehavior, so the tile stays stuck withworkerData+cooldownand the farm's ~6 workers block every payment ('no free workers' stall loops). The working skip isgameWorkers.releaseForObject(entity): it removes the entity'sWorkerDatabehavior and the game then clears the tile and marks the sourcelootableWITH its loot (e.g.tool_2 x3 + tool_1). Clear calls release right after each payment, so workers are instantly free and the loop pays continuously until energy out / board full. -
Harvest machinery (Harvest button): plain taps do NOT harvest crops — the game's harvest runs when a
LootReceivedbehavior lands on the entity (ctor = the trigger-module export whose instancetype === 'lootReceived', module 10295 alongside MergeTrigger). Harvest = addLootReceived(hp -1, cooldown, product becomes alootablebubble on the crop). Collect = tap the lootable (spawns the loot as groundCollectablebubbles on empty cells) then tap those Collectables to pick them up. The Harvest button does all three in iterative rounds (the ~1 fps background loop needs settle time). -
Game pauses while the tab is hidden (
document.visibilityState): taps queue in the entity_behaviorQueueand all fire on refocus — Clear refuses to tap while hidden. The pause-protection patch (pause_protect.js, installed first by install.mjs and embedded in the poller) fakes the visibility state and bridgesrequestAnimationFramewith a timer watchdog, so the game keeps ticking in background tabs. Without Chrome flags (--disable-background-timer-throttling --disable-renderer-backgrounding --disable-backgrounding-occluded-windows) background tabs throttle timers to ~1/s, so hidden mode runs at ~1 fps (bot ops still work; game time advances ~100 ms per tick). -
window.FMV.Iis a getter FUNCTION — always call it:FMV.I().Mergeable. BareFMV.Iyieldsundefinedand makes everything look non-mergeable.
Critical rules / gotchas
- Never navigate or reload the game frame. The Discord activity restarts on
reload (new
instance_id) and the injection is lost. Always evaluate in the live frame (Runtime.evaluate), neverPage.navigate. - The activity also restarts if the main thread stalls for seconds. Heavy
in-page work must be batched with event-loop breathing (
awaitbetween batches). - Injection does not survive Chrome restart / activity restart / game reload —
re-run
node src\install.mjs. - Never-move rule (family-based): an item moves only if its family has a mergeable member on the board. No-id buildings (trainstation/dairy/bbq/market/ bakery/loom…) and static families (tree, rock, area, premium, traintrack, delivery, decorative, decorative_timelimitedevent, blocker, toolbox) never move. Applied everywhere items move: sort AND plan groups (both menu and any CLI flow).
- Poller fake chunk ids are
0x7ff00000 + n; id0is permanently consumed after one use — never reuse it. - Only one game session should be open (
findGameTargetmatches by URL). - CLI evaluate helpers return CDP response objects — unwrap
.result.value.
How to run / verify
node src\install.mjs # full install (poller → hunter → FMV → menu)
node src\install.mjs poller # poller only (debug)
node src\install.mjs fmv # hunter + FMV only (debug)
node src\eval.mjs "window.FMV.board().filter(i => i.mergeable)"
node src\eval.mjs "window.FMV.merge(68, 68, 68, 69)"
node src\eval.mjs "window.FMV.spawnCrate(73, 70)"
Prerequisites: Chrome running with --remote-debugging-port=9222 and
IsolateSandboxedIframes — normally launched by auto-farm-install.bat, which
also passes the background flags (--disable-background-timer-throttling,
--disable-renderer-backgrounding, --disable-backgrounding-occluded-windows)
so the farm keeps running while the window is hidden; the Discord activity
open; Node.js ≥ 22.
Conventions
- JS modules (
.mjs) for Node-side tooling;.jsfor in-frame injected sources (exported asX_SOURCEstring constants). - No test framework — verification is live CDP runs + the session logs; keep code side-effect-free for the hunter's stubbed enumeration.
- No lint/typecheck config in this repo (nothing to run).
- Never commit secrets: session tokens / nakama backend tokens must not be committed.
- No auto commits or changelog updates: never commit and never touch
CHANGELOG.md/ theProject versionline unless the user explicitly asks. When the user does ask to commit and push code, first updateCHANGELOG.md(Keep a Changelog format; patch bump for fixes, minor for features — add anAdded/Fixedsection describing the change), bump theProject versionline in this file to match, and include both files in the commit. - Commit messages use conventional prefixes (
fix:,feat:,refactor:,release:).
Session history
Prior work is logged in the git history and summarized in CHANGELOG.md
(1.0.0 covers the full Discord-build feature set: menu, sort, harvest, fill,
plan+merge, orders, auto orders). Old STATUS.md session logs were removed in favor
of this file.