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
3 changes: 3 additions & 0 deletions .github/workflows/windows-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
- 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_memory_transaction_smoke.exe
- run: native/build-release/Release/cfb27_native_call_smoke.exe
- run: native/build-release/Release/cfb27_board_mutation_smoke.exe
- run: native/build-release/Release/cfb27_research_watch_smoke.exe
- run: native/build-release/Release/cfb27_frtk_profile_smoke.exe
- run: native/build-release/Release/cfb27_frtk_field_schema_smoke.exe
- run: native/build-release/Release/cfb27_frtk_discovery_smoke.exe
Expand Down
3 changes: 3 additions & 0 deletions docs/development/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ native/build-release/Release/cfb27_startup_smoke.exe native/build-release/Releas
native/build-release/Release/cfb27_memory_reader_smoke.exe
native/build-release/Release/cfb27_telemetry_smoke.exe
native/build-release/Release/cfb27_memory_transaction_smoke.exe
native/build-release/Release/cfb27_native_call_smoke.exe
native/build-release/Release/cfb27_board_mutation_smoke.exe
native/build-release/Release/cfb27_research_watch_smoke.exe
native/build-release/Release/cfb27_frtk_profile_smoke.exe
native/build-release/Release/cfb27_frtk_field_schema_smoke.exe
native/build-release/Release/cfb27_frtk_discovery_smoke.exe
Expand Down
33 changes: 33 additions & 0 deletions docs/lua-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ local matches = cfb.aob_scan("4D 5A ?? ??", 8)
-- byte, writable committed memory, and successful readback.
local changed = cfb.write_u8(address, expected, replacement)

-- Synchronously call executable code in the current process using the Win64
-- integer/pointer ABI. The target is followed by zero to eight 64-bit values.
local result = cfb.call(target, arg0, arg1)

-- Research capture: at most four process-local hardware breakpoint slots.
local slot, threads = cfb.watch(write_address, 4)
local exec_slot, exec_threads = cfb.watch_exec(function_address)
local hits = cfb.watch_hits(true)
local restored_threads = cfb.unwatch()

cfb.log("script loaded")

-- The trusted main-process client must register this type first with
Expand All @@ -78,6 +88,29 @@ The lowercase `cfb` functions above are the legacy host scripting surface and
are separate from `CFB27.db`; no raw-memory wrapper is added to the database
API.

`cfb.call` accepts any committed executable address in the current process,
zero to eight integer or pointer arguments, and returns the function's 64-bit
integer result. It is enabled only for the supported offline game build. Calls
are serialized and execute synchronously on the host worker that evaluates the
Lua buffer; the primitive does not move work onto a game-owned UI thread.
Floating-point/vector arguments, structure returns, and alternate ABI shapes
are not supported. A Windows structured exception becomes a Lua error, but
that guard cannot make an invalid native call safe or undo side effects that
occurred before the exception.

`cfb.watch(address, length)` arms a write breakpoint of length 1, 2, 4, or 8;
the address must be naturally aligned. `cfb.watch_exec(address)` arms an
execute breakpoint. At most four total slots may be active. Existing process
threads that are accessible and do not already own hardware breakpoints are
armed; the returned values are the zero-based slot and armed-thread count.
`cfb.watch_hits(clear)` returns at most 128 fixed snapshots plus a `dropped`
count. Each hit includes the integer registers, up to 256 stack qwords, and up
to eight safely readable qwords at each of `rbx`, `rsi`, `rdi`, `rcx`, `rdx`,
`r8`, and `r9` in fields such as `rcx_memory`. An unreadable pointer produces
an empty or partial array. `cfb.unwatch()` restores saved debug-register state.
These functions are current-process research tools; always collect and disarm
before continuing normal play.

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.
Expand Down
65 changes: 64 additions & 1 deletion docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ Error response:
readable private-memory ranges.
- `writeTransaction { transactionId, operations }` — apply a bounded guarded
batch with complete preflight comparison, readback, and rollback.
- `nativeCall { address, arguments }` — synchronously invoke an executable
in-process address with zero to eight Win64 integer/pointer arguments.
- `loadFrtkProfile { profile, layout }` — atomically validate and load a
matching version-1 bundle.
- `discoverFrtkCatalog {}` — resolve every required table into a new catalog.
Expand All @@ -61,7 +63,8 @@ Error response:
`hello.capabilities` advertises the memory commands as `memoryScan` and
`memoryRead`, allocation-aware scans as `memoryScanAllocationMetadata`, guarded
writes as `memoryWriteTransaction`, and structured event registration as
`telemetry`. `status.sessionWritesDisabled` reports whether an
`telemetry`. Direct native invocation is advertised as `nativeCall`.
`status.sessionWritesDisabled` reports whether an
unverifiable rollback has permanently disabled writes for the current host
session.

Expand Down Expand Up @@ -254,6 +257,64 @@ mutate memory while preflight, apply, verification, or rollback is running.
Callers must establish a stable window appropriate to the target data before
submitting a transaction.

### Native call

`nativeCall` is the low-level in-process invocation primitive. `address` and
every entry in `arguments` use canonical uppercase hexadecimal strings so all
64 bits survive JSON transport. The argument array may contain zero through
eight values. The host uses the Windows x64 integer/pointer ABI and returns the
64-bit integer result as another canonical hexadecimal string.

```json
{"protocol":1,"id":"call-1","command":"nativeCall","params":{"address":"0x1234AB80","arguments":["0x1","0x2"]}}
```

```json
{"address":"0x1234AB80","value":"0x24"}
```

The target must be a committed executable address in the current process, and
the host must recognize the supported offline game build. Calls are serialized
and run synchronously on the named-pipe request worker; this command does not
schedule onto a game-owned UI thread. The primitive supports integer and
pointer arguments only—no floating-point/vector arguments, structures, or
alternate calling conventions. `NATIVE_CALL_TARGET_INVALID` rejects a
non-executable target. `NATIVE_CALL_EXCEPTION` reports a structured-exception
code, but cannot roll back native side effects that occurred before the fault.

The SDK method is `client.nativeCall({ address, arguments })` and negotiates the
`nativeCall` capability before sending the request.

## Recruiting board mutations

`addBoard { recruitRow, teamRow }` and `removeBoard { recruitRow, teamRow }`
invoke the current supported build's verified full recruiting handlers. The
rows identify the requested Recruit record and the active Team record; no team
is hardcoded. Both commands require the `boardMutationV1` capability and must
be called while the recruiting runtime is loaded, but they do not depend on a
specific recruiting screen or selected UI row.

The host freshly resolves the recruiting controller and both record wrappers,
validates compact membership and freelist state before the call, and verifies
the complete table postcondition afterward. A successful result uses status
`applied_verified`; an already-satisfied add or remove uses `unchanged`.

```json
{"protocol":1,"id":"board-1","command":"addBoard","params":{"recruitRow":3182,"teamRow":92}}
```

An already-open Recruiting Board view refreshes on the next recruiting screen
change. The returned `uiRefresh` value is therefore
`next_recruiting_screen_change`. This is a rendering limitation, not delayed
table materialization; save durability follows the normal dynasty autosave
path.

Typed board failures include `RECRUITING_NOT_LOADED`,
`RUNTIME_DISCOVERY_AMBIGUOUS`, `BOARD_TABLE_DISCOVERY_FAILED`,
`BOARD_STATE_INVALID`, `BOARD_FULL`, `BOARD_NATIVE_CALL_FAILED`, and
`BOARD_POSTCONDITION_FAILED`. A postcondition failure disables further writes
for that host session.

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
second; Lua tick callbacks still run at their normal cadence.
Expand Down Expand Up @@ -293,6 +354,8 @@ transaction shapes, addresses, hex, and overlapping operations return
Typed FrTk commands additionally return `FRTK_PROFILE_INVALID`,
`FRTK_DISCOVERY_FAILED`, `FRTK_CATALOG_STALE`, `FRTK_FIELD_INVALID`, and
`FRTK_AUTHORITY_UNPROVEN`.
Native calls additionally return `NATIVE_CALL_TARGET_INVALID` and
`NATIVE_CALL_EXCEPTION`.

The unversioned legacy text pipe remains temporarily available for migration,
but it is not the integration contract for new tools.
47 changes: 47 additions & 0 deletions docs/research/runtime-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,50 @@ scholarships, scouting, board membership, pitch-intensity changes, and pitch or
visit creation/removal. Verification for this import is automated and offline;
no additional installed-host, CFB27, MMC, weekly-advance, or autosave gate was
performed.

## Independent board mutation verification on July 15, 2026

Board membership was retested on the supported current build using a disposable
dynasty with a verified backup. Brooks's dossier and earlier synthetic results
were treated only as leads.

Multiple vanilla UI adds and removes established the authoritative table
behavior. An add allocates one UserRecruitTarget row (4168), one
ActivePitchRecord row (5790), and appends one compact 4168 reference to the
user's 5847 membership row. A remove returns both rows to their freelists,
clears the Recruit and ActivePitches references, and compacts membership.
Tables 4176, 4190, and 4251 did not change in the no-pitch/no-visit cases.

The verified full current-build routines are:

- add: module RVA `0x8109060`;
- remove: module RVA `0x8166090`.

Both receive the active recruiting controller plus pointer cells containing an
active Team record wrapper and the requested Recruit record wrapper. The Team
row is dynamic; no school is hardcoded. The low-level remove routine at RVA
`0x80116B0` is prohibited because calling it alone left stale runtime state and
later produced a membership hole.

The full add routine first passed an already-targeted no-op with byte-identical
tables. It then added Storm Thompson Jr. (Recruit row 3182), produced the exact
vanilla allocation/membership diff, rendered after normal recruiting
navigation, and survived a dynasty reload.

The full remove chain was captured twice from real UI removals, including a
function-entry capture with zero dropped hits. An already-absent Julian Holmes
no-op left all six observed tables byte-identical. A synthetic removal of
Derrick Wilder from slot 0 compacted Keith Pearson from slot 1 into slot 0,
returned both allocated rows to their freelists, rendered after leaving and
re-entering the Recruiting Board, and survived a dynasty reload.

An already-open Recruiting Board does not redraw immediately after a direct
worker-thread call because the surrounding UI caller's refresh code is not
executed. The mutation is visible on the next recruiting screen change; this is
screen-change rendering, not reload materialization. Save durability follows
the normal dynasty autosave path.

The exact claim that board addition constructs six runtime objects was not
independently enumerated and remains unverified as a count. The public board API
does not synthesize those objects: it invokes the verified full game routines,
which own their required runtime construction and teardown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Board Membership Live Verification Plan

**Goal:** Get live board add/remove working through the game's own current-build
path with the minimum evidence needed to avoid shipping a table-only mutation.

**Scope:** Local offline `CollegeFB27.exe` only. Use a disposable dynasty or a
verified backup. The user performs vanilla UI actions when capture is armed; no
unannounced synthetic board mutation runs.

## Brooks inputs

These are research leads, not accepted conclusions:

| Commit | Useful input |
|---|---|
| `9f42cf13d1200da8212342c46d2a6b6fe6055d06` | Candidate table/header layouts. |
| `5d0a5748287cff0e8f9605fc51f6cf6c6f73a903` | Candidate ProspectInteraction footprint. |
| `b585faa5666245bbd8910a88646533b851b5a55a` | Data-breakpoint capture POC. |
| `cdcaafd01f6b47f52e2789070a4ed163e66cb820` | Execute capture and PE `.pdata` lookup POCs. |
| `9a37dba2bd1b53bc4ceb3f3872a192ad5efd79fd` | Candidate clean-chain RVAs. |
| `27dceed2fe9538fc977ae0b1e4ebc4c70c7bd932` | Six-runtime-object hypothesis. |
| `550274bd85dceec7f3c7d712d5d5fe687cfc5060` | Board add/remove dossier. |

## Gate 1: Capture the real UI path

Build only the tools needed to capture one clean vanilla sequence:

1. Snapshot the relevant board row, membership list, freelists, references,
hours, ProspectInteraction state, and recruit-keyed runtime objects.
2. Arm bounded data/execute breakpoints using Brooks's POCs as starting points.
3. With the user, capture UI add A, UI add B, and UI remove A.
4. Accept a handler only when its arguments identify the selected recruit and
its event produces the captured structural delta. Reject hot redraw and
generic-container hits.

Implementation files:

- `scripts/board-verification/board-state.cjs`
- `scripts/board-verification/capture-handler.cjs`
- `scripts/board-verification/pe-functions.cjs`
- `native/host/research_watch.h`
- `native/host/research_watch.cpp`
- `native/smoke/research_watch_smoke.cpp`
- `tests/board-verification.test.cjs`

Private captures stay under `.frtk/board-verification/`. If the two adds
materially disagree, repeat only the conflicting action on a second dynasty
load. The observed runtime-object count wins; six is not the expected answer.

The reusable call primitive required for replay is already implemented:

- `native/host/native_call.h/.cpp`
- protocol capability and command `nativeCall`
- SDK `client.nativeCall({ address, arguments })`
- Lua `cfb.call(target, ...)`

It accepts a committed executable address, zero to eight Win64 integer/pointer
arguments, and a 64-bit return value. Calls are serialized and guarded for
structured exceptions. It does not schedule itself onto a game-owned UI thread.

Gate 1 passes when add and remove each have an attributable current-build
handler, argument shape, and thread requirement. Otherwise record the exact
unverified boundary and stop.

## Gate 2: Reproduce add and remove

After Gate 1 passes, announce the mutation test and use the disposable/backup
dynasty:

1. Invoke the captured add handler for an off-board recruit using the native
call primitive and the verified thread/call path.
2. Compare the complete structural delta with the vanilla add delta.
3. Invoke the captured remove handler for that recruit and compare it with the
vanilla remove delta.
4. Confirm the recruiting screen reflects both operations immediately or after
the same ordinary screen transition vanilla needs.

Do not substitute direct table writes if a handler call fails. A table change
that appears only after a screen change or reload is recorded as such and does
not pass live reproduction.

Gate 2 passes only when both directions match vanilla structurally and render
live without destabilizing the game.

## Gate 3: Prove durability, then expose the API

For the hook-driven add and remove:

1. Save through the game UI, reload, and verify membership plus the captured
table/runtime invariants.
2. Record immediate rendering, reload materialization, and save durability as
separate results in `docs/research/runtime-verification.md`.
3. If both operations pass, implement guarded `addBoard` and `removeBoard`
wrappers that resolve only the verified build handlers and validate board
preconditions before dispatch.
4. If either direction fails, expose neither wrapper and document the boundary.

Before committing each implementation slice, run:

```powershell
npm run check
npm test
cmake --build native/build-release --config Release
```

No public board API exists until all three gates pass. Portable pitch, visit,
NIL, and contact-action work remains independent of this gate.
Loading
Loading