cortico-world-pvz 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +268 -0
- package/THIRD_PARTY_NOTICES.md +30 -0
- package/cursor-companion/README.md +66 -0
- package/cursor-companion/generated/companion.png +0 -0
- package/cursor-companion/generated/companion.svg +213 -0
- package/cursor-companion/generated/companion@8x.png +0 -0
- package/cursor-companion/generated/cursor_companion.h +155 -0
- package/cursor-companion/generated/pointer.svg +217 -0
- package/cursor-companion/pointer/pointer.json +47 -0
- package/cursor-companion/scripts/generate.mjs +292 -0
- package/cursor-companion/sprite/companion.json +77 -0
- package/dist/console.js +2 -0
- package/dist/console.js.map +7 -0
- package/package.json +57 -0
- package/scripts/build-console.mjs +35 -0
- package/src/ENV_PROMPT.md +23 -0
- package/src/bridge.ts +802 -0
- package/src/card-mechanics.ts +258 -0
- package/src/collectibles.ts +32 -0
- package/src/conditions.ts +434 -0
- package/src/config.ts +102 -0
- package/src/console/client.ts +109 -0
- package/src/definition.ts +32 -0
- package/src/engine-child.ts +254 -0
- package/src/engine-ipc.ts +98 -0
- package/src/events.ts +729 -0
- package/src/executor.ts +935 -0
- package/src/fingerprint.ts +53 -0
- package/src/index.ts +13 -0
- package/src/names.ts +197 -0
- package/src/native/ADAPTING.md +198 -0
- package/src/native/README.md +294 -0
- package/src/native/build.cmd +55 -0
- package/src/native/implant.cpp +10400 -0
- package/src/native/implant.def +8 -0
- package/src/native/injector.cpp +1317 -0
- package/src/native/json.h +185 -0
- package/src/native/profile.h +383 -0
- package/src/native/relative_plant.h +664 -0
- package/src/native-build.ts +165 -0
- package/src/native-reasons.ts +357 -0
- package/src/progress.ts +81 -0
- package/src/protocol.ts +1250 -0
- package/src/proxy.ts +1375 -0
- package/src/render.ts +754 -0
- package/src/runtime.ts +1191 -0
- package/src/semantic.ts +669 -0
- package/src/skills.ts +489 -0
- package/src/timing.ts +78 -0
- package/src/tools.ts +301 -0
- package/src/triggers.ts +185 -0
- package/src/unlocks.ts +28 -0
- package/src/world.ts +2450 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Phantivia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# `src/definition.ts`: cortico-world-pvz
|
|
2
|
+
|
|
3
|
+
`cortico-world-pvz` exposes the original PopCap Plants vs. Zombies as a semantic, event-driven World. The model can operate menus, choose seeds, play ordinary and special levels, and confirm progression without deriving state from pixels.
|
|
4
|
+
|
|
5
|
+
## Relationship with Cortico
|
|
6
|
+
|
|
7
|
+
This is an extension package for [Cortico](https://github.com/Pal-AI-Lab/Cortico), declared through the extension contract:
|
|
8
|
+
|
|
9
|
+
```jsonc
|
|
10
|
+
"cortico": { "kind": "world", "api": 4, "consoleClient": "dist/console.js" }
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
At runtime it imports the framework as `cortico/<path under src>` (`cortico/world.ts`, `cortico/core/types.ts`, `cortico/paths.ts`); the module hook registered by the framework's `src/extensions/runtime.ts` resolves those specifiers to the framework's own sources, so the extension and the framework share one instance. The package must therefore be `"type": "module"`. The browser side (`src/console/**`) only `import type`s from `cortico/*`.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Build the console panel first; `dist/` is not tracked and the console shows an empty PvZ page without it:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
corepack pnpm install
|
|
21
|
+
corepack pnpm build
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then either install it from the console's extensions page with this directory's absolute path, or run `corepack pnpm add --ignore-workspace <absolute path>` inside `<Cortico>/extensions/`. Restart the whole Cortico process afterwards; World definitions are read at startup.
|
|
25
|
+
|
|
26
|
+
Windows only. The definition's `preflight` rejects activation on any other platform.
|
|
27
|
+
|
|
28
|
+
## The game
|
|
29
|
+
|
|
30
|
+
The game is not part of this package. The operator installs their own copy and points `worlds.pvz.executable` at `PlantsVsZombies.exe`; `main.pak` must sit next to it. Both files are hashed before every launch (`src/fingerprint.ts`) and only the build listed under "Supported build" passes. Adapting the bridge to another build is documented in [`src/native/ADAPTING.md`](src/native/ADAPTING.md).
|
|
31
|
+
|
|
32
|
+
## Native bridge
|
|
33
|
+
|
|
34
|
+
The injector and the implant are C++ sources under `src/native/`; nothing prebuilt is downloaded. The World compiles them on the operator's machine with Visual Studio Build Tools (x86 MSVC toolchain) the first time it starts, into `<deployment root>/runtimes/pvz/<source hash>/`, or into `worlds.pvz.nativeBuildDir` when that is set. A rebuild happens when any file under `src/native/` changes. The build directory holds `pvz-injector.exe`, `pvz-implant.dll` and an `artifacts.json` with both hashes; recovery attach refuses artifacts whose hashes no longer match. Details in [`src/native/README.md`](src/native/README.md).
|
|
35
|
+
|
|
36
|
+
Windows Smart App Control in enforcing mode may block an unsigned locally built injector; the framework's `docs/runtimes.md` describes how to check.
|
|
37
|
+
|
|
38
|
+
The managed pointer and its pixel companion live in [`cursor-companion/`](cursor-companion/README.md): JSON sources, a generator, and the generated header the implant includes.
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
`worlds.pvz` in the deployment's `config.json`; every key is also on the console's configuration page.
|
|
43
|
+
|
|
44
|
+
| Key | Default | Meaning |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `enabled` | `false` | Mount the World |
|
|
47
|
+
| `executable` | `""` | Path to `PlantsVsZombies.exe`; surrounding double quotes are stripped |
|
|
48
|
+
| `nativeBuildDir` | `""` | Build output root; empty means `<deployment root>/runtimes/pvz/` |
|
|
49
|
+
| `closeOnStop` | `true` | Close a game the World launched when the World stops |
|
|
50
|
+
| `pollHz` | `15` | Snapshot rate, 10–20 |
|
|
51
|
+
| `cursorDurationMs` | `[80, 280]` | Bounds of one internal cursor leg |
|
|
52
|
+
| `actionTimeoutMs` | `5000` | Causal verification deadline after a terminal native result |
|
|
53
|
+
| `emitBoardDeltas` | `true` | Deliver board change events |
|
|
54
|
+
| `launch`, `attachPid` | `true`, `null` | Recovery attach only; not operator settings |
|
|
55
|
+
|
|
56
|
+
Ownership records (process id, creation time, owner token, artifact directory) are written under the deployment's data directory, `pvz/ownership/`, and are never logged.
|
|
57
|
+
|
|
58
|
+
## Console
|
|
59
|
+
|
|
60
|
+
One panel, `game`: start and stop, phase, process id and the last error detail. Starting compiles the bridge if needed, hashes the game, launches it suspended, injects, restores the 800 × 600 client and reports `running` once the implant's hello arrives.
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
`tsconfig.json` (`paths`) and `vitest.config.ts` (`resolve.alias`) point `cortico/*` at `../BOT/src/`, a framework checkout next to this directory; change both when it lives elsewhere.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
corepack pnpm typecheck
|
|
68
|
+
corepack pnpm test # 37 files; the ten native fixtures need the x86 MSVC toolchain
|
|
69
|
+
corepack pnpm build # esbuild → dist/console.js
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Tests never start the game. The native fixtures compile `implant.cpp` against fake memory laid out at the profile's offsets; see [`src/native/ADAPTING.md`](src/native/ADAPTING.md) §4.
|
|
73
|
+
|
|
74
|
+
## Process boundary
|
|
75
|
+
|
|
76
|
+
The Core mounts `PvzWorldProxy` in the main process. The proxy stays idle until the operator starts the game from its console panel, then forks `engine-child.ts`. The 引擎子进程 owns the native named pipe, game launch, injection, 15 Hz observation, state differencing, semantic task queue, and receipt verification. The x86 implant reads coherent state and sends mouse messages only to the PvZ window.
|
|
77
|
+
|
|
78
|
+
Commands and captures require both the implant hello and the injector's resumed process identity. Requests made while either is pending fail without sending input or disconnecting the pipe.
|
|
79
|
+
|
|
80
|
+
The console panel owns both directions: `start` launches, `stop` shuts the 引擎子进程 down, closes the owned game, and releases the persistent lease. Automatic restarts cover exactly one failure — the 引擎子进程 died while the game is still alive — and re-attach through the lease without touching the game. If the owned process itself is gone, the module concludes the operator closed the game: it stops at `stopped`, recycles the lease, and never relaunches. Recovery attempts are capped (4 within a rolling 10 minutes) and the module reports when it gives up.
|
|
81
|
+
|
|
82
|
+
The main process receives events, deferred board summaries, console status, and requested PNG frames. It does not receive raw memory, hidden entities, or high-frequency coordinates.
|
|
83
|
+
|
|
84
|
+
The managed window is pinned to a Per-Monitor V2 client of exactly 800x600, wholly inside one monitor's visible bounds. Both facts ride on every snapshot as `presentation`. Repair runs inline on the window messages that can change geometry and, as the backstop, on every poll tick, so a repair that once failed does not leave the window clipped; it is suppressed while the window is minimized and while a person is dragging it. Drag suppression is held by the mouse button rather than by `WM_EXITSIZEMOVE` alone, so a move that never delivers that message cannot latch repair off for the rest of the session. Off that invariant the implant refuses screenshots and mouse messages — a window DC only reads pixels that are actually on screen — and the module refuses to queue work with the measured size in the receipt instead of letting each step fail without a reason.
|
|
85
|
+
|
|
86
|
+
The simulated cursor is game-local. The implant draws a Fitts-scaled asymmetric trajectory with smooth bounded motor noise, occasional endpoint correction, a pixel-perfect animated companion, and state-dependent click pacing into the PvZ frame. Remaining-route compression accelerates a backlog within a hard logical speed limit. It never moves, locks, or captures the Windows system cursor, so a user can continue using the physical mouse while queued work runs.
|
|
87
|
+
|
|
88
|
+
## Supported build
|
|
89
|
+
|
|
90
|
+
The first profile is deliberately specific; what establishes each value and how to produce a profile for another build is in [`src/native/ADAPTING.md`](src/native/ADAPTING.md).
|
|
91
|
+
|
|
92
|
+
- profile: `goty-apac-ja-chs-south_sniper`
|
|
93
|
+
- executable SHA-256: `9ba1c9b23ed2b240ad29a54c7b9fd55bcbfac8b7f83ddfac69f7907d7b7198ed`
|
|
94
|
+
- PE32 x86, image base `0x400000`, image size `0x450000`, no relocation table
|
|
95
|
+
- FileVersion `1.2.0.1073`; bundled installer metadata calls the localized product `1.1.0.1056`
|
|
96
|
+
- CodeView source path identifies the PopCap GOTY APAC Japanese localized branch
|
|
97
|
+
- `gLawnApp` absolute address `0x7578F8`, established from the unique accessor and constructor stores in this executable
|
|
98
|
+
- `main.pak` SHA-256: `89971bafb5bee1d5de9012007b469c65e6147a1b12cf5058be3292ff8c6ba9b8`
|
|
99
|
+
|
|
100
|
+
The implant verifies the PE fingerprint and critical instruction bytes before enabling actions. A matching version-resource string is insufficient: common English 1.2.0.1073 builds use a different layout, and the widely published `module+0x329670` pointer does not address `gLawnApp` in this localized executable.
|
|
101
|
+
|
|
102
|
+
## Perception boundary
|
|
103
|
+
|
|
104
|
+
Dialog buttons use the same callable action names as the menu list, including a repeated
|
|
105
|
+
`restart` or `main_menu` when a confirmation dialog offers that action.
|
|
106
|
+
|
|
107
|
+
The disclosure boundary is enforced inside the implant, before JSON crosses the pipe.
|
|
108
|
+
|
|
109
|
+
- Dynamic objects beyond the rendered play area or behind the active fog mask are omitted.
|
|
110
|
+
- Fog levels use the rendered fog alpha grid and fog offset. Missing or inconsistent fog samples hide the entity.
|
|
111
|
+
- Invisighoul zombies are never disclosed from internal entity state.
|
|
112
|
+
- Vase contents appear only after the game's transparency state makes them visible.
|
|
113
|
+
- Entity health is reduced to sprite-readable condition bands.
|
|
114
|
+
- Every disclosed plant includes its cell, condition, and semantic phase. Arming plants such as Potato Mine expose the phase visible to a player.
|
|
115
|
+
- Every disclosed zombie includes its cell, condition, semantic phase, and speed band.
|
|
116
|
+
- Card cooldown seconds and percentages report the remaining recharge interval, computed from the total duration minus elapsed recharge ticks.
|
|
117
|
+
- The seed-picker may expose the preview roster because the game shows it to the player; the same pre-spawned entities are omitted after play begins.
|
|
118
|
+
|
|
119
|
+
`pvz_glance` is mounted only when the active model provider accepts images. Its frame is for compatibility diagnosis and does not loosen the semantic filter.
|
|
120
|
+
|
|
121
|
+
## Task execution
|
|
122
|
+
|
|
123
|
+
`pvz_do` accepts ordered semantic steps and returns as soon as the task is queued. One native input runs at a time. A later `pvz.task` event reports the terminal state: `done`, `partial`, `blocked`, `unverified`, or `cancelled`.
|
|
124
|
+
|
|
125
|
+
The queue modes are:
|
|
126
|
+
|
|
127
|
+
- `replace`: the default; drops waiting and conditionally parked work while allowing the entire running task to reach its terminal result
|
|
128
|
+
- `append`: adds the task at the tail
|
|
129
|
+
- `now`: interrupts the running task, verifies release of held cards, tools, and the internal cursor, then inserts the task at the head while preserving previously queued work
|
|
130
|
+
|
|
131
|
+
An accepted `pvz_do` receipt contains the acceptance line and queue status. The triggering wake already delivered a world snapshot (`pvz.board.snapshot` is rendered at dispatch time), and a second queue cannot be submitted in the same turn, so the receipt omits the snapshot. Task completion arrives separately as a `pvz.task` event.
|
|
132
|
+
|
|
133
|
+
`renderPvzQueue` renders the queue for acceptance receipts, `pvz.task` events, and delivered observations. It omits the label of a task already described step by step in the surrounding text. Card reservations are listed after the queue; armed triggers follow as `待触发`.
|
|
134
|
+
|
|
135
|
+
A plant step with `when:"ready"` or `when:"ready_and_affordable"` parks the task at that step until its reserved card is ready; steps after it wait. Sun keeps arriving while a task is parked because the module collects it on its own lane.
|
|
136
|
+
|
|
137
|
+
PvZ queues primarily hold short actions and pending intent. Acceptance receipts and delivery-time observations expose the current step, parked steps, remaining steps, reserved cards, and armed triggers. A receipt also carries 落点现状: for each absolute planting step, what already stands on that cell, or that the cell still wants a carrier the queue never places. It states the cell and withdraws nothing. The same reading is appended to a rejection when the implant only says the cell will not take the plant. `pvz_do({cancel:[taskId],queue:"append",steps:[...]})` withdraws selected old intent and submits its replacement in one call. Parsing and admission complete before cancellation; an invalid replacement leaves the old tasks intact. Cancellation of a finished ID is a no-op. Running input must cross the existing release and execution-exit barrier before new input starts. `pvz_stop({taskId})` withdraws one task without submitting a replacement, `pvz_stop({triggerId})` disarms one trigger; omitting both stops everything, triggers included.
|
|
138
|
+
|
|
139
|
+
Waiting for a fact is not a queue step. `pvz_arm({when, steps, queue?, expiresInMs?})` arms a trigger: `when` is re-evaluated on every fresh snapshot and, the moment it is true, `steps` are submitted as an ordinary `pvz_do` queue (`queue` defaults to `now`); the trigger fires once. It reserves no card and blocks nothing; if the card is not ready when it fires, the submitted queue's own `when` decides. `expiresInMs` counts from arming and withdraws an unfired trigger. A trigger is bound to the board run it was armed on; a terminal screen or another run withdraws it. Every outcome — fired, expired, cancelled, invalidated — is a `pvz.trigger` event, and a fired trigger's queue reports through `pvz.task` like any other. Trigger and task ids share one sequence.
|
|
140
|
+
|
|
141
|
+
Conditions contain four observed predicates: `sun:{min?,max?}`, `card:{plant,ready?,affordable?}`, `cell:{row,column,layer,empty}`, and `zombie:{row,minColumn?,maxColumn?,minCount?}`. Compose them with `all`, `any`, and `not`; empty compositions are invalid. `layer` distinguishes `main`, `base`, and `pumpkin`, so an empty main layer can still have a lily pad. Hidden or unavailable facts remain unknown under negation; only true fires.
|
|
142
|
+
|
|
143
|
+
The zombie predicate counts visible living hostiles inside its inclusive interval and is true once the count reaches `minCount`, defaulting to one. `row` also accepts an array of rows, whose matches are summed rather than tested separately, which is what an area one-shot is waiting for: a Cherry Bomb covers three rows, a Doom-shroom more, and `any` of several single-row predicates can only ask whether each row is non-empty. Fog, darkness, and Invisighoul keep an unreached count unknown, so a threshold never becomes false on the strength of a hidden interval.
|
|
144
|
+
|
|
145
|
+
Staleness is the reason these primitives exist: the interval between the module delivering a board snapshot and the resulting `pvz_do` reaching the tool layer is measured per run (`ENV_PROMPT.md` quotes the current figure), and an ordinary zombie keeps walking through it. A trigger moves the decision to the moment the fact holds, and a relative column moves the coordinate to the moment of input. Triggers used to be a per-step `startWhen`; that parked the whole queue behind the waiting step, so the model pushed every conditional step to the tail and cancelled it before it ever fired.
|
|
146
|
+
|
|
147
|
+
| Game situation | Intent |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| A Cherry Bomb is ready, but the approaching group is still too far right. | Arm on `all` of the chosen row's zombie interval, card readiness and affordability; fire with `column:{aheadOf:"nearest_hostile",minGap:0}`. |
|
|
150
|
+
| A Cherry Bomb should wait until the wave clumps rather than burn on the first arrival. | Arm on `zombie:{row:[2,3,4],minColumn,maxColumn,minCount:4}` over the rows its blast covers. |
|
|
151
|
+
| A blocker on a lily pad is about to disappear. | Arm on that cell's `main` layer becoming empty, then replace the blocker; the `base` layer remains distinct. |
|
|
152
|
+
| A committed build can proceed after either lane clears. | Arm on `any` of two negated zombie predicates. Fog that hides either lane remains unknown. |
|
|
153
|
+
| An emergency placement has waited beyond its useful window. | Give the trigger `expiresInMs`; expiry withdraws it. |
|
|
154
|
+
| A different lane now needs the reserved card. | Submit the old task ID in `cancel` alongside the new steps with `queue:"append"`; admission can reuse the released card without disturbing other intent. |
|
|
155
|
+
|
|
156
|
+
These conditions specify one finite action; plant choice, lane choice, and repetition remain model decisions.
|
|
157
|
+
|
|
158
|
+
On a conveyor board the seed bank refills from the belt: consuming one packet shifts the rest one place left. How many steps may ask for a given card is how many of that identity the belt holds right now, counted across everything the model has in flight: the remaining steps of every queued or parked task plus the steps carried by every armed trigger. Identity is type plus imitated plant; packets sharing it are interchangeable, so the step names the card without its `#N` ordinal and supplies its own target cell. Conditional planting reserves this quantity and resolves the current slot before each step. A fired trigger reaches the executor without passing through `pvz_do`, so the budget is charged at arm time. Collection and shovelling do not touch cards and are never counted. The board kind is inferred from every card reporting a null cost, which is how the implant serializes a conveyor bank. A bowling throw changes neither screen nor special phase, so it is exempt from the rule that a phase-changing step ends its queue.
|
|
159
|
+
|
|
160
|
+
Planting also accepts `column:{aheadOf:"nearest_hostile",minGap:0..8}` instead of an integer column, and it is the default for anything whose usefulness depends on where the zombies are — single-use burst and trap plants, and blockers meant to stand in front of the lane. The native input worker selects the nearest disclosed living hostile in the chosen row when input starts, then retains that run and target identity. The cell is `floor((x-40)/80)+1-minGap` using the target's unrounded position, with smaller columns toward the house: the first term is the cell the game itself assigns the zombie — the one whose centre is nearest — so `minGap:0` is the cell the target stands in and `minGap:1` the cell in front of it. A result before the first column is clamped to column 1, the only cell still ahead of a target that has reached the house; a result past column 9 is refused instead, because the target has not walked onto the board yet. `minGap` is a lower bound: the landing cell is the first cell from there toward the house that takes the plant, because a target chewing on a plant stands on that plant's cell and "ahead of this target" still has exactly one nearest answer; when no cell ahead takes it, the attempt ends with that reason. It rechecks the landing at the actual input boundary and permits bounded cursor correction before pressing. Target loss, a changed row, unsupported motion, no usable cell, or a changed run ends the attempt with that reason, and never by retargeting, changing row, or adding a missing lily pad. A rejected relative step is local to that step, like a rejected absolute cell: the queue continues with the next step. Relative placement receipts name the cell actually committed and require the expected plant on it; card consumption alone is insufficient evidence.
|
|
161
|
+
|
|
162
|
+
Taking the shovel clicks a point measured off a real 800x600 board frame: the centroid of the grey pixels of the shovel icon. Whether that sign shifts with the seed bank width is untested, so a selection that never reaches the shovel cursor reports the point it clicked, the cursor it kept seeing, and the seed bank rect read from memory, which is enough to derive the relation from one attempt.
|
|
163
|
+
|
|
164
|
+
Each native step is validated against the latest coherent snapshot and accepted or rejected by the implant. An implant `executed` result carries the in-game consequence it observed — a consumed packet, a collected drop, a struck target — and that is the receipt. A later snapshot diff is preferred evidence when it arrives inside the budget; when it does not, the observed consequence stands rather than being downgraded. An abort that carries a reason is a definite failure — the implant knows why it stopped — so `unverified` is reserved for the deadline passing with no independent evidence, for an abort that names no reason at all, and for the few implant reasons that state the effect itself could not be established. Every implant reason string maps to one exact Chinese sentence in `native-reasons.ts`, which also carries that failed/unknown distinction; a reason with no mapping is reported verbatim and logged rather than dressed up as an explanation, and `tests/native-reasons.test.ts` extracts the literals from the native sources so a reworded reason fails the suite instead of silently falling back. A Whack batch carries exact requested, attempted, released, confirmed, stale, and scope-transition counts; target loss or a released click without a visible hit remains local to that repeatable skill. A transient `loading` sample waits for the bound run to become coherent again; a stable scope change or terminal screen ends the old queue.
|
|
165
|
+
|
|
166
|
+
`partial` and `yield` are local to an independent step, so later steps continue and the final task is reported as partial. State that moved between admission and the actual click — a card back on cooldown, sun spent by an earlier step, a menu action that has already advanced — is reported as `yield`, so the rest of the queue still runs. The reason given is the implant's own; the card and cell are re-read only when it supplies none, because by then the state that caused the rejection has usually recovered. A durable prerequisite failure is `blocked`, and an ambiguity whose input release or semantic extent is unknown is `unverified`; both stop the remaining task.
|
|
167
|
+
|
|
168
|
+
Conditional planting expresses future intent. `when:"ready"` waits for cooldown; `when:"ready_and_affordable"` also waits for sufficient sun. The semantic card is reserved while parked and appears in `pvz_observe`, `pvz_queue`, and receipts. Parked conditions do not occupy the native input channel, so other tasks and the module's own sun sweeps keep running.
|
|
169
|
+
|
|
170
|
+
Board tasks retain their admitted run identity. Leaving that board expires every queued step: a different run, a changed mode, or a menu screen. A terminal result on a board that is still standing expires only the steps that change the plant layout, because the drops it left behind are still there for collection. Parked steps stay parked during pause and retain their admitted card binding across preceding steps. A hostile crossing the chosen cell does not cancel the model's planting intent.
|
|
171
|
+
|
|
172
|
+
Collection is a multi-action skill. `until:"visible_clear"` re-scans after every verified batch and continues until the requested visible semantic category is clear, including objects that appeared during execution. `what` covers `coins`, `resources`, `award`, and `usable_seed`; sun belongs to none of them.
|
|
173
|
+
|
|
174
|
+
## Sun
|
|
175
|
+
|
|
176
|
+
The module collects sun automatically. `collect` rejects `what:"sun"`, `resources` excludes sun, sun appearances are not published as events, and sweeps produce no receipt.
|
|
177
|
+
|
|
178
|
+
A sweep is armed from any snapshot that shows sun on a sweepable board, not from the snapshot diff: a sweep that was replaced, preempted, or refused leaves its sun on the board, and arming on arrival alone left that sun for the next drop to pick up — a wait its lifetime does not survive. One sweep is outstanding at a time and issues one native batch for the sun visible when it starts. A verified sweep re-arms immediately if sun remains; a sweep that collected nothing holds off for 400 ms doubled per consecutive failure up to 6 s, so one uncollectable drop cannot take the cursor over and over.
|
|
179
|
+
|
|
180
|
+
The sweep is an ordinary executor task in the module's own lane: ahead of waiting model tasks because drops expire and queued planting intent does not, behind whatever is already executing, and preemptable by the model's `queue:"now"`. It is invisible in `pvz_queue` and its terminal state never reaches the model.
|
|
181
|
+
|
|
182
|
+
Between the steps of a model task the module collects sun inline, before every step but the first. A queue can run for half a minute, and an internal task never preempts one that is executing, so sun that lands mid-queue would otherwise expire before the queue ends. The first step is left alone because it carries the decision the model just made.
|
|
183
|
+
|
|
184
|
+
The internal cursor is exclusive, so a sweep is armed only on a live, unpaused board whose run has not settled and whose cursor is normal or Whack-a-Zombie's hammer. Special actions and sun collection share the executor; offering a special action does not disable collection. A held seed packet and a paused board keep collection pending. The same check runs when a queued sweep starts and on later snapshots, so collection resumes after the cursor is released. Three consecutive collection failures other than vanished drops publish one `pvz.sun.stuck` event.
|
|
185
|
+
|
|
186
|
+
## Tools
|
|
187
|
+
|
|
188
|
+
- `pvz_observe`: semantic screen, board map, cards, resources, progression, queued tasks, and reserved cards
|
|
189
|
+
- `pvz_do`: queue ordered menu, profile, seed-selection, planting, shoveling, collection, special-level, interaction, or compatibility-click steps
|
|
190
|
+
- `pvz_queue`: inspect running, waiting, and parked tasks, armed triggers, and the most recent terminal result
|
|
191
|
+
- `pvz_arm`: arm a trigger — a condition and the queue to submit the moment it holds
|
|
192
|
+
- `pvz_stop`: withdraw one task or trigger, or clear everything and release all game-local held input
|
|
193
|
+
|
|
194
|
+
- `pvz_glance`: capture a PNG frame for compatibility diagnosis; available only to image-capable models
|
|
195
|
+
|
|
196
|
+
Whack prefetch cues leave support, menu transitions, and stopping available. Clearing all tasks also closes the prefetch window; continuing requires another finite queue from the model.
|
|
197
|
+
|
|
198
|
+
The user-selection dialog exposes each visible profile as `profile:<exact name>`. Use a `menu` step with that action to select its row, followed by `menu` action `confirm` to activate it. Selection is shown as pending confirmation; completion requires the dialog to close and the chosen profile to become active. Native input revalidates the current list, row identity, selection, and button before clicking. Compatibility clicks remain restricted to unknown screens without semantic menus or dialogs.
|
|
199
|
+
|
|
200
|
+
Restart and main-menu actions can open a confirmation dialog. That transition is verified separately; the next action confirms or cancels it. Shovel input follows the visible button layout for the current seed-bank size, including Slot Machine's separate position.
|
|
201
|
+
|
|
202
|
+
Receipts, events, and snapshots name rows and columns the way a person would — 第4排第8.6列 — and use plant and zombie display names rather than engine identifiers. The public surface accepts plant names, cells, and semantic selectors. Special steps use `at`, `to`, `card`, or `target:{kind,name?,at?}`. Whack-a-Zombie uses `targets:[{kind:"zombie",scope:"all_visible"}]`; each skill binds at most 32 then-visible zombies when it starts and freezes those identities for one finite batch. Native object identifiers, seed-bank slots, and target identifiers never cross the agent-facing mapping layer.
|
|
203
|
+
|
|
204
|
+
Each model response may submit one `pvz_do` skill queue. A special action, confirmed seed selection, award collection, interaction, or visual fallback is a decision barrier and must end that queue; its terminal receipt carries the fresh state used to plan the next queue. A usable seed packet may be paired only with one immediately following, queue-final `launch`.
|
|
205
|
+
|
|
206
|
+
## Events and progression
|
|
207
|
+
|
|
208
|
+
The 引擎子进程 projects lifecycle, screen, seed-picker, level start, visible progress, card readiness, close threats, mower use, victory, defeat, award, and committed profile progress. Victory and defeat come from a persistent, monotonic run result rather than inference from the current screen; award and defeat screens are supporting evidence for the same result. The game-enforced unique visible player name scopes profile commits and mode-record baselines, so changing users cannot be reported as progress.
|
|
209
|
+
|
|
210
|
+
Event urgency and snapshot freshness are independent. Lifecycle transitions, task outcomes, near threats, mower use, critical plant damage, and new special-action opportunities request `flush`. Ordinary battlefield changes use `debounce` and retain the global batch floor and ceiling. Ordinary currency appearances and regular card cooldown/affordability notifications use `piggyback`; they neither start nor extend a batch timer. Special-level resources and newly arrived conveyor cards retain their urgency. Routine duplicates remain archive-only. A sample containing an urgent event flushes after all its facts have been queued in order. A flush makes input ready for the next delivery boundary; operator pause and delivery gates still apply, and an active model request is not interrupted.
|
|
211
|
+
|
|
212
|
+
Explicit observation, task admission, each execution step, and deferred board rendering request a new native sample. The read command bypasses the input queue and returns only after its correlated result and a newer snapshot. Read failure never substitutes the cached board. Input receipts also require a post-result sample before an observed native effect can count as verified. Task admission and stopping are serialized; an interrupted step must pass the existing native release barrier before replacement input executes.
|
|
213
|
+
|
|
214
|
+
Whack target observations also request a fresh native sample at delivery; vanished targets evaporate, and a superseded renderer cannot clear the next ticket.
|
|
215
|
+
|
|
216
|
+
Deferred board observations carry the `snapshot` tag across IPC. Renderer tickets are consumed once and superseded tickets cannot read a later callback. At handoff the proxy synchronously arms a flush observation for the rebuilt session's first batch; rendering refreshes both board and queue. Task result text labels its queue as the state at receipt time. Module diagnostics record native acknowledgement, result, fresh sample, receipt, task admission, step start, and terminal timing without storing model text.
|
|
217
|
+
|
|
218
|
+
Board snapshots enumerate disclosed plants by cell and phase and disclosed zombies by cell, speed, and phase. Profile, unlock, and mode-record changes are retained during play and emitted only on the main menu, mode selector, or seed picker, keeping level context focused on actionable state.
|
|
219
|
+
|
|
220
|
+
Progress combines the current scene, mode, board-level meter, flag state, Challenge stage, award screen, adventure level/completion, and unlock bits. The level script's wave counter is internal and never crosses the disclosure boundary: what a player reads off the progress meter is how many flags have gone by, and that is what the snapshot carries. Survival repicks and same-run stage advances in Survival, Last Stand, Vasebreaker, and I Zombie are milestones rather than terminal victories. Adventure-profile commits are tracked separately.
|
|
221
|
+
|
|
222
|
+
For Adventure 1-1, 1-2, and 1-3, submit conditional planting as queued intent and append immediate responses as separate runnable tasks. This preserves the intended placement across cooldown and affordability changes without polling or competing clicks.
|
|
223
|
+
|
|
224
|
+
## Special-level contracts
|
|
225
|
+
|
|
226
|
+
Zomboss phases and visible ice/fire balls have semantic state and change events. A
|
|
227
|
+
`bossProjectile: { kind: "fireball" | "iceball", row?: number }` condition can arm a single
|
|
228
|
+
response to an already visible ball. Omit `row` to match any lane. The model chooses the plant
|
|
229
|
+
and its empty flower pot; the condition neither selects a tactic nor reveals a future attack.
|
|
230
|
+
|
|
231
|
+
- Seed selection and removal settle only after the packet's travel animation ends, so replacement can immediately reselect a removed packet.
|
|
232
|
+
- Resuming a saved minigame is verified when its menu advances to the same mode's active board, including menus that have no board snapshot yet.
|
|
233
|
+
- Board identity remains valid while its frame counter advances during observation. A changed scene, mode, board pointer, level, or counter rollback still invalidates the read.
|
|
234
|
+
- Invisighoul reports invisible zombie counts and positions as unknown. Its night pool background is separate from a rendered fog mask.
|
|
235
|
+
- Zombiquarium reports the green hunger tint and recovery as events and text state. Swimming across the aquarium does not emit approaching-house threats.
|
|
236
|
+
- Wall-nut Bowling uses conveyor packets and lane launch coordinates; it does not pretend a nut was planted on a grid cell.
|
|
237
|
+
- Slot Machine waits for the roll state to leave its settled value and return before verification. Reel symbols are omitted from plantable cards; usable seeds are collected from the resulting drops.
|
|
238
|
+
- Raining Seeds interacts with the visible usable-packet coin at its real position.
|
|
239
|
+
- Collectibles drawn above fog remain in observations and can be collected; fog still hides entities drawn beneath it.
|
|
240
|
+
- Slot Machine, Raining Seeds, and Vasebreaker pick up one usable packet at a time, place the held packet, then collect another.
|
|
241
|
+
- Text and compact snapshots name the current minigame and held object. Usable-seed pickup and release emit cursor changes; legal placement cells remain in the special targets, so placement does not require interpreting an image.
|
|
242
|
+
- `pvz_arm` can use `collectible: { kind: "usable_seed" }` to queue one pickup when a visible packet appears. The trigger fires once; the held packet remains available for a later `launch` decision. Collectible conditions also support `minCount` and preserve unknown visibility under darkness or fog.
|
|
243
|
+
- Seed pickup and collectible conditions accept `plant` to select a particular plant's packet. A missing match leaves other packet types untouched; omitting `plant` keeps the any-packet behavior.
|
|
244
|
+
- Vasebreaker never transmits opaque vase contents.
|
|
245
|
+
- Vase, roll, gem move, zombie placement, trophy purchase, onslaught transition, garden action, and cannon shot end their skill queue. Bowling and Zombiquarium purchases/feedings may share a finite queue; each step checks current targets and resources and verifies its result before the next step.
|
|
246
|
+
- Beghouled waits for the board to settle after a swap or twist before comparing the matrix and score.
|
|
247
|
+
- Twist targets contain four occupied cells within the eight-column puzzle. The target is the top-left cell of the clockwise 2×2 rotation.
|
|
248
|
+
- Whack-a-Zombie retains the ordinary collect, plant, and shovel surface. A queue that chooses Whack is a dedicated finite six-skill queue: each `all_visible` skill waits for one surfaced batch, freezes up to 32 identities when that skill starts, and issues one native batch; later zombies can only be selected by a later skill. A confirmed hit means the implant observed a body or armor effect, not that the target was defeated. Tactical cues and receipts include resources, cards, playable cells, plants, and the remaining visible targets with their body, armor, and shield condition bands. A support queue may interrupt for expiring sun, or one Whack append queue may be prefetched while the current queue runs; neither the 引擎子进程 nor implant creates additional skills.
|
|
249
|
+
- I Zombie places pseudo-seed zombie cards within the per-level boundary and tracks visible brains eaten.
|
|
250
|
+
- Last Stand exposes setup and onslaught on the same board run; Survival alone uses the inter-stage seed picker.
|
|
251
|
+
- Cob Cannon verifies source readiness, target selection, and the subsequent cooldown state.
|
|
252
|
+
- Seeing Stars publishes only its remaining painted cells as semantic objective observations. These cells are completed with a normal `plant` step using `starfruit`; they are never special-action targets.
|
|
253
|
+
- Portal Combat reports both pairs, including portals at the visible right boundary outside the planting grid. Portal relocation and visible zombies changing rows emit urgent events.
|
|
254
|
+
- Visible ice trails mark blocked cells and prevent planting there. Trail growth and removal emit row-specific terrain events; hidden cells retain their existing visibility rules.
|
|
255
|
+
|
|
256
|
+
## Research basis
|
|
257
|
+
|
|
258
|
+
The implementation uses public source as behavioral documentation, not as a binary dependency:
|
|
259
|
+
|
|
260
|
+
- [PvZ-A11y](https://github.com/game-a11y/PvZ-A11y), MIT: supported-version notes, widget structures, and Windows input behavior
|
|
261
|
+
- [re-plants-vs-zombies](https://github.com/Patoke/re-plants-vs-zombies), CC0: class layouts, fog rendering semantics, level modes, and GOTY address annotations
|
|
262
|
+
- [Plants-vs.-Zombies-Online-Battle](https://github.com/Zhuagenborn/Plants-vs.-Zombies-Online-Battle), MIT: an independent example of x86 DLL injection and in-process hooks for a different game build
|
|
263
|
+
|
|
264
|
+
Addresses are accepted only after comparison with the locally installed executable. Sources targeting 1.0.0.1051 or the English GOTY build are never used as address fallbacks.
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT, see [LICENSE](LICENSE). Third-party notices, including the game itself, are in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
This package ships no game files, no binaries and no third-party code.
|
|
4
|
+
|
|
5
|
+
## The game
|
|
6
|
+
|
|
7
|
+
PopCap's Plants vs. Zombies is the operator's own installation. `worlds.pvz.executable` points at
|
|
8
|
+
it; the package hashes `PlantsVsZombies.exe` and `main.pak` before launch and refuses any other
|
|
9
|
+
build. Neither file, nor any asset extracted from them, is part of this repository or of the npm
|
|
10
|
+
package.
|
|
11
|
+
|
|
12
|
+
## Toolchain
|
|
13
|
+
|
|
14
|
+
The injector and implant are compiled on the operator's machine with Microsoft Visual Studio
|
|
15
|
+
Build Tools (x86 MSVC, Windows SDK). Nothing is downloaded by the package; the toolchain is
|
|
16
|
+
installed and licensed by the operator.
|
|
17
|
+
|
|
18
|
+
## Research sources
|
|
19
|
+
|
|
20
|
+
Public source used as behavioral documentation for this executable family. No code from them is
|
|
21
|
+
included; addresses were re-established against the supported build.
|
|
22
|
+
|
|
23
|
+
- [PvZ-A11y](https://github.com/game-a11y/PvZ-A11y), MIT
|
|
24
|
+
- [re-plants-vs-zombies](https://github.com/Patoke/re-plants-vs-zombies), CC0
|
|
25
|
+
- [Plants-vs.-Zombies-Online-Battle](https://github.com/Zhuagenborn/Plants-vs.-Zombies-Online-Battle), MIT
|
|
26
|
+
|
|
27
|
+
## This package
|
|
28
|
+
|
|
29
|
+
MIT, see `LICENSE`. The framework [Cortico](https://github.com/Pal-AI-Lab/Cortico) is MIT as well;
|
|
30
|
+
the two are connected through the extension contract and licensed separately.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# `sprite/companion.json`, `pointer/pointer.json`: cursor companion
|
|
2
|
+
|
|
3
|
+
The managed pointer and its pixel companion, kept apart from the native code so the art can be
|
|
4
|
+
edited without touching `implant.cpp`. `implant.cpp` includes the generated header from here;
|
|
5
|
+
[cortico-world-canvas](https://github.com/Phantivia/cortico-world-canvas) carries a copy of the
|
|
6
|
+
generated SVG for its web cursor.
|
|
7
|
+
|
|
8
|
+
<p>
|
|
9
|
+
<img src="generated/companion@8x.png" width="192" height="192" alt="companion sprite">
|
|
10
|
+
<img src="generated/pointer.svg" width="208" height="208" alt="pointer with companion">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
## Files
|
|
14
|
+
|
|
15
|
+
| Path | Content |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `sprite/companion.json` | The sprite source: palette, 24 rows of pixel indices, four animations, draw offset |
|
|
18
|
+
| `pointer/pointer.json` | Pointer geometry: arrow polygon, cyan pulse line, press ring, companion placement, edge flip |
|
|
19
|
+
| `scripts/generate.mjs` | Writes `generated/` from the two JSON files; `--check` compares without writing |
|
|
20
|
+
| `generated/cursor_companion.h` | C++17 header, namespace `pvz::cursorCompanion`; included by `src/native/implant.cpp` |
|
|
21
|
+
| `generated/companion.svg` | One `<rect>` per opaque pixel; the canvas World's cursor image |
|
|
22
|
+
| `generated/companion.png`, `generated/companion@8x.png` | Bitmaps at 1× and 8× nearest-neighbour |
|
|
23
|
+
| `generated/pointer.svg` | 1:1 composite preview of pointer and companion |
|
|
24
|
+
|
|
25
|
+
`generated/` is committed; regenerate after editing a source. The header ends with
|
|
26
|
+
`static_assert`s pinning the FNV-1a hash of the pixel data, the opaque pixel count and each
|
|
27
|
+
animation's total duration, so a header that no longer matches its source fails to compile.
|
|
28
|
+
The native build identity (`src/native-build.ts`) covers the generated header, so an edit here
|
|
29
|
+
triggers a rebuild of the injector and implant.
|
|
30
|
+
|
|
31
|
+
## Sprite
|
|
32
|
+
|
|
33
|
+
- 24 × 24, seven palette entries; index 0 is transparent and written `.` in `pixels`, indices 1–6
|
|
34
|
+
are the six colours.
|
|
35
|
+
- Drawn at `(20, 20)` from the pointer hotspot, lower right. When the hotspot is within
|
|
36
|
+
`20 + 24 + 1` pixels of the right or bottom client edge, that axis flips to the other side and the
|
|
37
|
+
arrow mirrors with it.
|
|
38
|
+
- Four animations, integer offsets only, no scaling or interpolation:
|
|
39
|
+
|
|
40
|
+
| State | Frames `[dx, dy, ms]` | Loops |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| idle | `[0,0,280] [0,-1,180] [1,-1,320] [0,0,180]` | yes |
|
|
43
|
+
| moving | `[0,0,60] [1,-1,60] [0,-1,60] [-1,0,60]` | yes |
|
|
44
|
+
| pressed | `[0,0,45] [0,1,75] [0,1,120]` | no, holds the last frame |
|
|
45
|
+
| released | `[0,0,55] [0,-1,75]` | no, holds the last frame |
|
|
46
|
+
|
|
47
|
+
The implant draws a black outline around the sprite (every transparent pixel adjacent to an opaque
|
|
48
|
+
one) before filling the pixels.
|
|
49
|
+
|
|
50
|
+
## Pointer
|
|
51
|
+
|
|
52
|
+
Coordinates are relative to the hotspot (arrow tip) in client pixels. The arrow is a seven-point
|
|
53
|
+
polygon, white with a 2-pixel black outline; a 2-pixel cyan (`#00eeff`) line runs from `(2, 5)` to
|
|
54
|
+
`(3, 15)` and extends downward by the state's pulse value; while a button is down a hollow cyan
|
|
55
|
+
ring of radius `9 + pulse` is drawn around the hotspot. Pulse: idle 0, moving 1, pressed and
|
|
56
|
+
dragging 2, released 2 for the first 55 ms then 1.
|
|
57
|
+
|
|
58
|
+
## Regenerating
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
node cursor-companion/scripts/generate.mjs # write generated/
|
|
62
|
+
node cursor-companion/scripts/generate.mjs --check # compare; exit 1 when stale
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Copy `generated/companion.svg` to `cortico-world-canvas/src/public/corti-cursor.svg` after a
|
|
66
|
+
sprite change; the header needs no copying.
|
|
Binary file
|