Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions .github/workflows/windows-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on: [push, pull_request]
jobs:
verify:
runs-on: windows-latest
env:
CFB27_NATIVE_ARTIFACTS: ${{ github.workspace }}\native\build-release\Release
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand All @@ -14,12 +16,14 @@ jobs:
- run: npm ci
- run: npm run check
- run: npm test
- run: cmake -S native -B native/build-active -A x64
- run: cmake --build native/build-active --config Release
- run: native/build-active/Release/cfb27_startup_smoke.exe native/build-active/Release/cfb27_lua_host.dll
- run: native/build-active/Release/cfb27_protocol_smoke.exe native/build-active/Release/cfb27_lua_host.dll
- run: cmake -S native -B native/build-release -A x64
- run: cmake --build native/build-release --config Release
- run: native/build-release/Release/cfb27_startup_smoke.exe native/build-release/Release/cfb27_lua_host.dll
- run: native/build-release/Release/cfb27_memory_reader_smoke.exe
- run: native/build-release/Release/cfb27_telemetry_smoke.exe
- run: native/build-release/Release/cfb27_protocol_smoke.exe native/build-release/Release/cfb27_lua_host.dll
- run: npm run pack:preview
- uses: actions/upload-artifact@v4
with:
name: cfb27-lua-hook-0.1.0-dev.1
name: cfb27-lua-hook-0.2.0-dev.1
path: dist/
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Offline Lua scripting runtime, Node SDK, and MMC startup tooling for EA SPORTS
College Football 27 on PC.

> Developer preview `0.1.0-dev.1`. The runtime supports one verified game
> Developer preview `0.2.0-dev.1`. The runtime supports one verified game
> build, is intended only for offline play, and does not include or provide an
> anticheat bypass.

Expand Down Expand Up @@ -35,7 +35,7 @@ cfb27lua logs [--follow]
cfb27lua doctor
```

These commands are implemented in the `0.1.0-dev.1` developer preview. One-shot
These commands are implemented in the `0.2.0-dev.1` developer preview. One-shot
`--json` output is a single object; followed logs use JSON Lines.

## Start here
Expand Down
39 changes: 39 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,45 @@ node packages/cli/bin/cfb27lua.cjs --help
- `doctor` — perform read-only game, install, host, build, and write checks.
- `logs [--follow]` — read bounded logs or follow new log events.
- `events [--after N]` — read a cursor page.
- `memory scan` — scan bounded private readable memory through the validated SDK.
- `memory read` — read one or more bounded canonical address ranges through the validated SDK.
- `telemetry register <type...>` — register structured telemetry type names for the host session.

The memory commands are read-only developer diagnostics. A scan requires
`--pattern`, `--mask`, `--max-matches`, and `--context`; context is applied on
each side of a match. The CLI automatically follows native continuation pages,
uses a ten-second timeout for each scan page, and accepts `--max-pages` from 1
through 4,096 (default 4,096). Callers cannot supply raw cursors or start/stop
ranges. A read accepts one or more
`--range 0xUPPERCASE_ADDRESS:length` options. Addresses must be canonical (for
example, `0x7FF612340000`, not a lowercase or zero-padded form). Write-like
options are not accepted.

Use `--allow-unsupported-build` only when intentionally running a memory
diagnostic against an unsupported build. Without that explicit flag, the SDK
rejects an unsupported-build result. The flag does not enable writes.

Examples:

```powershell
node packages/cli/bin/cfb27lua.cjs memory scan `
--pattern CFB27A1100A1B2C3D4E5F60718293A4B `
--mask FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF `
--max-matches 8 --context 32 --max-pages 4096 `
--allow-unsupported-build --json

node packages/cli/bin/cfb27lua.cjs memory read `
--range 0x7FF612340000:192 --allow-unsupported-build --json

node packages/cli/bin/cfb27lua.cjs telemetry register `
recruiting.snapshot recruiting.stability --json
```

Human memory output reports bounded counts and the canonical addresses returned
by the SDK. JSON output keeps the validated SDK result unchanged under the
standard `{ "ok": true, "command": "...", "result": ... }` CLI envelope.
Only `memory scan` selects the ten-second per-page client timeout; all other
commands retain the SDK default timeout.

Directory options are `--game-dir`, `--mmc-dir`, and `--artifacts-dir`. Their
environment fallbacks are `CFB27_GAME_DIR`, `CFB27_MMC_DIR`, and
Expand Down
23 changes: 13 additions & 10 deletions docs/development/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,27 @@ Visual Studio 2022 with the C++ desktop workload.
npm ci
npm run check
npm test
cmake -S native -B native/build-active -A x64
cmake --build native/build-active --config Release
cmake -S native -B native/build-release -A x64
cmake --build native/build-release --config Release
```

Verify both native harnesses:
Verify all four native harnesses:

```powershell
native/build-active/Release/cfb27_startup_smoke.exe native/build-active/Release/cfb27_lua_host.dll
native/build-active/Release/cfb27_protocol_smoke.exe native/build-active/Release/cfb27_lua_host.dll
native/build-release/Release/cfb27_startup_smoke.exe native/build-release/Release/cfb27_lua_host.dll
native/build-release/Release/cfb27_memory_reader_smoke.exe
native/build-release/Release/cfb27_telemetry_smoke.exe
native/build-release/Release/cfb27_protocol_smoke.exe native/build-release/Release/cfb27_lua_host.dll
```

Create the allowlisted preview bundle and checksum:
Create the allowlisted preview bundle and checksum from that exact build:

```powershell
$env:CFB27_NATIVE_ARTIFACTS = (Resolve-Path native/build-release/Release).Path
npm run pack:preview
```

Set `CFB27_NATIVE_ARTIFACTS` when the native DLLs are in a different Release
directory. Set `SOURCE_DATE_EPOCH` to normalize staged file timestamps. The
packager rejects archive content, game/save data, logs, dependencies, and build
intermediates.
`CFB27_NATIVE_ARTIFACTS` is required; the packager never guesses which build
directory to use. Set `SOURCE_DATE_EPOCH` to normalize staged file timestamps.
The packager rejects archive content, game/save data, logs, dependencies, and
build intermediates.
28 changes: 22 additions & 6 deletions docs/development/release-checklist.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
# Developer-preview release checklist

- [ ] Confirm version `0.1.0-dev.1` in the root, SDK, CLI, and native hello.
- [ ] Complete the read-only memory and telemetry automated gate before changing
the preview version.
- [ ] Run `npm ci`, `npm run check`, and `npm test`.
- [ ] Configure and build all native targets with Windows x64 MSVC.
- [ ] Run the one-MiB-stack startup smoke and framed protocol smoke.
- [ ] Run `npm run pack:preview`.
- [ ] Run startup, memory-reader, telemetry, and framed-protocol smoke
executables from the full Release build.
- [ ] Confirm CLI memory scans automatically follow continuation pages with a
bounded `--max-pages` value and retain the scan-only timeout.
- [ ] Set `CFB27_NATIVE_ARTIFACTS` to the absolute path of that exact Release
directory, then run `npm run pack:preview`.
- [ ] Run `git diff --check`.
- [ ] Confirm the staged package and both npm tarballs contain no archive,
game/save data, schema, logs, dependencies, or build intermediates.
- [ ] Verify `dist/SHA256SUMS.txt` against the preview zip.
- [ ] Verify the external `dist/SHA256SUMS.txt` against the preview zip. The ZIP
checksum cannot be embedded in documentation inside that same ZIP.
- [ ] Confirm Windows CI is green.
- [ ] Perform the documented offline runtime checklist without an additional
game-data write.
- [ ] With the game closed, install the exact automated-gate candidate host and
relaunch MMC offline so no previous DLL remains loaded.
- [ ] Perform the documented offline read-only runtime checklist: confirm hello
capabilities, bounded sentinel scan/read, advancing registered telemetry,
ten minutes of responsiveness, and a Dynasty hub transition. Do not use or
attempt a memory write.
- [ ] Record the date, executable hash, exact commands, and observed results in
`docs/research/runtime-verification.md` only after observing them.
- [ ] After the manual gate succeeds, set root, SDK, CLI, lockfile, release
packager, and native hello versions to `0.2.0-dev.1`, then repeat every
automated build, test, smoke, package-inspection, and diff-check step.
- [ ] Close the game and verify uninstall restores both known MMC hashes.
- [ ] Publish GitHub artifacts only; npm publication is not part of this preview.
46 changes: 45 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Getting started

CFB27 Lua Hook `0.1.0-dev.1` is a Windows x64 developer preview. It requires
CFB27 Lua Hook `0.2.0-dev.1` is a Windows x64 developer preview. It requires
Node.js 20 or later, CMake 3.24 or later, Visual Studio 2022 C++ build tools,
MMC, and a separately launched offline CFB27 session.

Expand Down Expand Up @@ -52,6 +52,50 @@ node packages/cli/bin/cfb27lua.cjs events --after 0 --json
eligibility separately. A failed write-eligibility check does not prevent safe
read-only scripts.

## Read live memory from a trusted Node process

The SDK exposes bounded, read-only memory discovery for trusted Node.js and
Electron main-process code. Do not expose these methods or their raw byte and
address results to an Electron renderer.

```js
const { createClient } = require('@cfb27/lua-hook');

const client = createClient({ pid: gamePid });
// scanMemory automatically follows validated continuation cursors.
const scan = await client.scanMemory({
patternHex: 'CFB27A1100A1B2C3D4E5F60718293A4B',
maskHex: 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
maxMatches: 2,
contextBefore: 4,
contextAfter: 4,
maxPages: 4096,
});

const read = await client.readMemory({
ranges: [{ address: scan.matches[0].address, length: 16 }],
});
```

Hex byte strings must be uppercase and addresses must use canonical uppercase
`0x[0-9A-F]+` strings; JavaScript numbers are never accepted as addresses. A
scan pattern is 8–4096 bytes, requests at most 64 matches, and allows at most
512 total context bytes before and after each match. Each native scan page is
bounded to 32 MiB of eligible memory using 4 MiB read chunks. Automatic scans
accept 1–4,096 pages and default to the 4,096-page, 128 GiB ceiling. Use
`client.scanMemoryPage(options)` when a trusted main-process caller needs manual
page control; its `nextCursor` may be passed only to the next page request.
A batch read contains at most 64 ranges of 64 KiB each and at most 256 KiB
total. Unsupported game builds
require `allowUnsupportedBuild: true` and report `supportedBuild: false`.

All memory methods validate requests before opening the pipe and every host
response field before returning it. A multi-page scan observes a live,
non-atomic memory map, so re-read and validate every selected candidate before
interpreting it. The methods can report `MEMORY_ACCESS_DENIED`,
`SCAN_LIMIT_EXCEEDED`, or `TOO_MANY_MATCHES`; malformed host results report
`INVALID_RESPONSE`. The SDK does not provide a memory-write API.

## Restore MMC

Close the game, then restore both verified original proxies:
Expand Down
22 changes: 22 additions & 0 deletions docs/lua-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ local changed = cfb.write_u8(address, expected, replacement)

cfb.log("script loaded")

-- The trusted main-process client must register this type first with
-- client.registerTelemetryTypes(["probe.snapshot"]).
cfb.emit("probe.snapshot", { sequence = 1, stable = true })

cfb.on("game_ready", function()
cfb.log("game ready")
end)
Expand All @@ -30,6 +34,24 @@ Supported callback names are `game_ready` and `tick`. The host runs `tick`
callbacks approximately every 100 ms. The event protocol coalesces observable
tick events to at most one per second.

## Structured telemetry

`cfb.emit(type, payload)` appends exactly one structured event to the existing
cursor-paged event ring and returns `true`. It does not write to the host log.
The type must first be registered for the host session through the
`registerTelemetry` protocol command or SDK `registerTelemetryTypes(types)`.
Custom types use `^[a-z][a-z0-9_.-]{0,63}$`; `game_ready`, `tick`, and `log` are
reserved, and no more than 16 distinct custom types may be registered per
session.

Payloads may contain Lua nil, booleans, finite numbers, strings, dense arrays,
and string-keyed objects. Tables are converted recursively and must not be
cyclic, sparse, mixed between array and object keys, or contain unsupported Lua
values. Limits are depth 4, 64 keys per object, 128 entries per array, 1,024
bytes per string, and 16 KiB after JSON serialization. Address and raw-byte
fields are rejected at every depth, including `address`, `addressHex`,
`regionBase`, `bytesHex`, `contextAddress`, and `contextHex`.

## Script execution

Protocol v1 accepts complete UTF-8 source buffers through `evaluate` and
Expand Down
112 changes: 112 additions & 0 deletions docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,113 @@ Error response:
- `evaluate { source }` — execute one complete multiline Lua buffer.
- `logs { limit }` — return up to 256 recent bounded log entries.
- `events { after, limit }` — return an ordered cursor page and `nextCursor`.
- `registerTelemetry { types }` — add trusted structured event names for the
current host session.
- `scanMemory { patternHex, maskHex, maxMatches, contextBefore,
contextAfter, allowUnsupportedBuild?, cursor? }` — scan one bounded page of
readable private memory and optionally resume from a continuation cursor.
- `readMemory { ranges, allowUnsupportedBuild? }` — read a bounded batch of
readable private-memory ranges.

`hello.capabilities` advertises the memory commands as `memoryScan` and
`memoryRead`, and structured event registration as `telemetry`. They are
read-only host operations and do not expose a write API.

### Structured telemetry

`registerTelemetry` accepts exactly one parameter, `types`, containing 1–16
unique strings. Names match `^[a-z][a-z0-9_.-]{0,63}$`; `game_ready`, `tick`,
and `log` are reserved. Registration is additive for the session and repeating
an already registered name in a later request is idempotent. At most 16
distinct custom names may be registered in total.

Request:

```json
{"protocol":1,"id":"telemetry-1","command":"registerTelemetry","params":{"types":["probe.snapshot"]}}
```

Result:

```json
{"types":["probe.snapshot"]}
```

The SDK method is `client.registerTelemetryTypes(types)`. Both the SDK and host
strictly validate the request and response contract. Registered Lua code may
call `cfb.emit(type, payload)` to append exactly one event to the cursor ring;
emission never writes to the file log.

Payloads are JSON-compatible and limited to depth 4, 64 keys per object, 128
entries per array, 1,024 bytes per string, and 16 KiB serialized. Numbers must
be finite. Address and raw-byte keys are rejected at every object depth,
including `address`, `addressHex`, `regionBase`, `bytesHex`, `contextAddress`,
and `contextHex`. Lua conversion additionally rejects cycles, functions,
userdata, threads, mixed or sparse tables, and non-string object keys.

### Memory scan

`patternHex` and `maskHex` are equal-length uppercase hexadecimal byte strings.
The pattern is 8–4,096 bytes. A mask byte selects significant bits using
`(live & mask) == (pattern & mask)`. `maxMatches` is an integer from 1 through
64. `contextBefore` and `contextAfter` are nonnegative integers no greater than
512, with at most 512 context bytes requested in total. One request scans at
most 32 MiB of eligible memory in chunks no larger than 4 MiB plus boundary
lookahead. An optional `cursor` is a canonical uppercase address identifying
the first virtual byte not covered by the preceding page.

Request:

```json
{"protocol":1,"id":"scan-1","command":"scanMemory","params":{"patternHex":"CFB27A1100A1B2C3D4E5F60718293A4B","maskHex":"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF","maxMatches":2,"contextBefore":4,"contextAfter":4,"allowUnsupportedBuild":true}}
```

Result:

```json
{"supportedBuild":false,"complete":false,"nextCursor":"0x7FF614340000","scannedBytes":33554432,"matches":[{"address":"0x7FF612340080","regionBase":"0x7FF612340000","regionSize":65536,"protection":4,"contextAddress":"0x7FF61234007C","contextHex":"00000000CFB27A1100A1B2C3D4E5F60718293A4B00000000"}]}
```

Every successful page contains exactly `complete`, `nextCursor`,
`scannedBytes`, `matches`, and `supportedBuild`. A partial page has
`complete:false` and a canonical string `nextCursor`; a terminal page has
`complete:true` and `nextCursor:null`. Completion means the address-space walk
reached the process maximum, not that the game was paused or that multiple
pages form an atomic snapshot. Eligible read failures return an error instead
of silently advancing the cursor.

The SDK exposes `client.scanMemoryPage(options)` for one protocol request and
`client.scanMemory(options)` for automatic aggregation. The aggregate method
owns continuation cursors, accepts `maxPages` from 1 through 4,096 (default
4,096), applies `maxMatches` globally, and rejects repeated or decreasing
cursors. The default bounds total eligible-byte work to 128 GiB. Before using a
candidate for interpretation, re-read it and validate its expected structure;
the live memory map can change between pages.

### Memory read

`ranges` contains 1–64 objects with exactly `address` and `length` keys.
Addresses use canonical uppercase `0x[0-9A-F]+` strings without redundant
leading zeroes. Each length is an integer from 1 through 65,536 bytes, and the
aggregate request is capped at 262,144 bytes. Every range is validated before
any bytes are copied, so a failure never returns partial results.

Request:

```json
{"protocol":1,"id":"read-1","command":"readMemory","params":{"allowUnsupportedBuild":true,"ranges":[{"address":"0x7FF612340080","length":16}]}}
```

Result:

```json
{"supportedBuild":false,"ranges":[{"address":"0x7FF612340080","length":16,"bytesHex":"CFB27A1100A1B2C3D4E5F60718293A4B"}]}
```

Both commands reject unknown parameter keys; range objects also reject unknown
keys. On an unsupported executable, `allowUnsupportedBuild` must be the JSON
boolean `true` or the command returns `UNSUPPORTED_BUILD`. Successful diagnostic
requests then return `supportedBuild:false`. This override never enables writes.

The host retains at most 512 log entries and 1,024 events. Event cursors are
monotonic for one host session. Tick events are coalesced to at most one per
Expand All @@ -49,5 +156,10 @@ timeout, invalid request/response, script failure, installation conflict, and
backup-verification failure. Consumers should branch on `error.code`, not error
message text.

Memory commands additionally return `MEMORY_ACCESS_DENIED` when a requested
range is not wholly readable private memory, `SCAN_LIMIT_EXCEEDED` when the
aggregate scan bound would be crossed, and `TOO_MANY_MATCHES` rather than
silently truncating a scan. These errors do not include memory or region dumps.

The unversioned legacy text pipe remains temporarily available for migration,
but it is not the integration contract for new tools.
Loading
Loading