docs: add FLE bridge direction pivot and squad-injection test tooling

Adds the app-centric FLE bridge direction (direction.md) replacing the
Blaze-backend route as primary plan, plus a corrected foundational test
procedure and snapshot/diff/apply tooling for reverse-engineering FLE's
Freeze Lineup write mechanism. Also picks up prior untracked research docs
(status-review, fut-integration-options, fifa23-startup-flow, track-c) and
existing capture/export tooling that hadn't been committed yet.
This commit is contained in:
funman300
2026-06-30 13:19:27 -07:00
parent cbb9ea49ab
commit 1fb664710a
15 changed files with 2060 additions and 0 deletions
+4
View File
@@ -14,6 +14,10 @@ target/
# Captures (runtime data, not source)
openfut-bridge/captures/
# Python
__pycache__/
*.pyc
# Editor
.vscode/
.idea/
+250
View File
@@ -0,0 +1,250 @@
# OpenFUT — Direction Document
*The pivot: FUT lives in the app; FIFA 23 is the match renderer.*
*Supersedes the Blaze-backend approach as the primary plan. Last updated 2026-06-30.*
---
## 1. Goal (revised)
Deliver an **intuitive way to play a FUT-style experience with FIFA 23**, where:
- The entire **FUT experience** — cards, squads, packs, SBCs, coins, chemistry,
progression — lives in a **custom app** (web UI or desktop) built on the
already-complete OpenFUT Core economy backend.
- **FIFA 23 is demoted to a match renderer.** Its only job is to play a
single-player match using the squad the app built. No FUT mode, no online, no
Blaze, no EA servers.
This deliberately drops in-game FUT cards/UI (they live in the app) in exchange
for a project that **converges** instead of being gated behind months of
backend reverse-engineering.
### Why this replaces the backend plan
The status review confirmed the backend route (faking EA's online stack) is
blocked at an upstream in-process EbisuSDK gate, with Blaze/Fire2 unconfirmed
beyond it — realistically 36 months of expert RE that may not converge. The
app-centric route sidesteps **every** wall in that review by never making FIFA's
own FUT mode run.
---
## 2. Base mode: Career, not Kick-Off
**Career mode is the base.** Reasons:
- FLE's live-editing API (`EditDBTableField`, Freeze Lineup) is **confirmed to
work in career mode** and explicitly does NOT work in FUT/online modes.
- Career already provides the FUT-shaped scaffolding we'd otherwise fake:
persistent club, a fixture schedule, recorded results, progression across a
season.
- **Match results are written into the career DB**, making result capture a DB
read rather than a fragile live-memory grab.
**Kick-Off is the prototype sandbox.** Use it first to prove squad injection
works with nothing to corrupt (no save to break), then move the real loop onto
career. Run the foundational injection test in BOTH.
---
## 3. Core architecture: the bidirectional FLE bridge
The backbone is a **bidirectional channel between the app and a resident FLE Lua
script running inside the game.** Everything else is messages over this channel.
```
Custom App (FUT experience)
│ squad push ──────────────► ┌─────────────────────────────┐
│ │ Resident FLE Lua script │
│ ◄────────── game state │ (inside FIFA 23, career) │
│ ◄────────── match result │ - reads game state │
└────────────────────────────► │ - applies squad live │
(file-watch or local socket) │ - reads results from DB │
└─────────────────────────────┘
FIFA 23 plays the match
```
Three message types over the bridge:
1. **App → Game: squad push.** The app's chosen XI + stats applied LIVE via
`EditDBTableField`, replicating whatever DB write FLE's "Freeze Lineup"
feature performs (see `docs/foundational-xi-injection-test.md` — the exact
field(s) are found by diffing, not assumed). No restart, no
file-copy-reload. (File-load remains a fallback.)
2. **Game → App: game state.** The resident script polls the game's current
screen/menu state and reports "safe to apply" vs "not safe", driving a smart
Apply button in the app (see §5).
3. **Game → App: match result.** After full-time, the script reads the result
from the career DB and pushes score/scorers to the app, which awards
coins/progression. (Manual entry is the baseline fallback.)
The bridge transport can be a watched file the in-game Lua polls, or a local
socket — decided in build (see §7). Either way the *game keeps running*; a file,
if used, is just the message channel, not a reload.
---
## 4. Tiered mod scope
Build in tiers matched to risk. The core tier is all the SAME kind of DB write,
so it lands together once squad injection works.
### Tier 1 — Core writes (ride the same live DB-edit mechanism)
- **Squad / custom XI** — the load-bearing primitive (Freeze Lineup's
underlying write, replicated via script — see §6).
- **Player stats as "cards"** — card tiers, in-form versions, SBC upgrades all
expressed as written attribute values.
- **Chemistry as stat adjustment** — app computes FUT chemistry, applies it as
small stat bumps when writing players in (no in-game chem UI; that's in the app).
- **Appearance / identity** — kits, names, team assignment, so the club looks
like your club on the pitch.
- **Formation / tactics** — squad structure carries the app's build onto the pitch.
### Tier 2 — Confirm-then-add
- **Match difficulty per game** — to drive a Squad-Battles-style "this opponent is
World Class". Settable in-game trivially; programmatic drive needs confirming.
- **Match rules / modifiers** (half length, etc.) — for app-defined challenges.
### Tier 3 — Result capture (manual baseline + automated stretch)
- **Manual:** user enters the score in the app after the match. Zero RE, ships
first.
- **Automated:** resident script reads the career-DB result (or, for Kick-Off,
reads the in-match score from memory at full-time — precedent exists: the
CM cheat table's `export_season_stats.lua` already reads goals/cards from
memory via known offsets). Push to app → auto-award progression.
### Out of scope (stays in the app, by design)
- In-game FUT cards, FUT menus, pack-opening animation, chemistry board, FUT
presentation. The app is where it looks/feels like FUT.
---
## 5. The smart Apply button (state-aware)
Live DB edits only "stick" in safe menu states (the in-game "Edit Player" screen,
for example, overwrites edits). So the bridge reads game state and gates applying:
- Resident Lua script polls the game's current-screen value (a few Hz),
classifies **safe / not safe**, reports to the app.
- App's **Apply button is enabled only when the script confirms a safe state**
(squad hub, main menu); greyed otherwise.
- **Safe-by-default-OFF:** unknown state → button greyed → never a risky write.
Expand the known-safe list incrementally as states are confirmed.
- **v2 (more seamless):** instead of greying, the app always lets you click and
the script **queues** the apply, executing the moment a safe state is entered,
then confirms back. Greying is v1; queue-and-apply is v2.
`IsInCM()` is a confirmed state-read; the specific screen-state address + the
value→screen mapping is one-time reconnaissance (same technique as result reading).
---
## 6. What's confirmed vs what needs validating
**Confirmed (from FLE's own Lua API docs/wiki, checked 2026-06-30):**
- FLE live-edits the running career DB without restart, via `EditDBTableField`
(real signature: `EditDBTableField(cell)` where `cell = row["fieldname"]`
with `.value` mutated first — not the table/index/field/value form an
earlier draft of this doc assumed).
- FLE reads game state via `IsInCM()`.
- A `MEMORY` Lua class exists (`ReadInt`/`WriteInt`/`ReadMultilevelPointer`/
etc.) for arbitrary process memory — confirms the result-reading fallback
in §4 Tier 3 is a real, documented capability, not just cheat-table analogy.
- `GetPlayersStats()` is a documented function returning per-player
goals/assists/cards/etc. — a better confirmed path for match-result capture
than raw memory offsets.
- **Freeze Lineup** (Formation Editor → arrange XI → tick "Freeze Lineup" →
`Data → Save`) is FLE's actual documented mechanism for forcing a starting
XI in career mode. This **replaces** "selection bias" below.
- OpenFUT Core (economy) is complete and tested.
**Walked back — not actually confirmed:**
- "Selection bias forces specific players into the starting XI" — no such
field appears anywhere in FLE's documented Lua API or its own example
scripts. This was an unverified assumption carried over from general FIFA
modding precedent (other titles), not anything checked against FLE/FIFA 23.
See `docs/foundational-xi-injection-test.md` for the corrected plan, which
uses Freeze Lineup instead.
**Needs validating (the foundational tests — see §7):**
- Whether Freeze Lineup actually holds into a played match (FLE's wiki
documents the feature but not a live-match test of it).
- What DB table/field Freeze Lineup's `Data → Save` actually writes — it's
GUI-only and undocumented at that level; finding it is part of the
foundational test.
- Whether that write can be replicated by a script (`EditDBTableField`) well
enough to drive it from an EXTERNAL trigger, not just the Formation Editor
UI — required for the app↔game bridge.
- The app↔game bridge transport (file-watch vs socket) works cleanly under the
run setup.
- The screen-state address + safe/not-safe classification (FLE's `Events`
API page exists in the wiki index but its content is currently empty/
undocumented — this is more open than previously assumed).
- Result read-back from the career DB after a match.
**Standing caveat:** the whole stack rides on **EAAC staying neutralized**
(FLE's fake-launcher bypass). If a game update re-enables it, hooks fail. Keep
game updates off; confirm neutralized state each session.
---
## 7. Build order / next steps
Each is a bounded, verifiable step. Do them in order; later ones depend on
earlier answers.
1. **FOUNDATIONAL TEST — live custom XI in career.** Confirm Freeze Lineup
holds into a played match, reverse-engineer the DB write it makes, then
replicate that write from a script so it can be triggered externally
instead of through the Formation Editor UI. See
`docs/foundational-xi-injection-test.md` for the full procedure. *Done =
a script-driven write produces a match that fields the squad you
specified.* Everything rests on this.
2. **Pick the bridge transport.** Decide file-watch vs local socket for app↔game
messaging; implement the minimal app→game squad push. *Done = app sends a
squad, the resident script receives and applies it.*
3. **Game-state reader + smart Apply.** Find the screen-state address, classify
safe/not-safe, expose to the app, gate the Apply button. *Done = button greys
when you enter a match/edit screen, enables in the squad hub.*
4. **Result read-back.** Read the career-DB match result post-game, push to app,
award progression. Manual entry ships alongside as the fallback. *Done = app
updates coins from a played match.*
5. **Tier 1 breadth.** Extend the squad push to carry stats, appearance,
formation (same write mechanism). *Done = the club looks and plays like the
app's build.*
6. **Tier 2 + economy loop polish.** Difficulty drive, challenges, and the full
pack → SBC → squad → match → reward loop closed end-to-end.
### Decision still open
- **App form factor:** web UI vs desktop app. This affects the bridge transport
(a desktop app can hold a local socket more naturally; a web UI leans toward a
small local helper/file-watch). Decide before step 2.
---
## 8. Provenance
Clean-room throughout. This route relies on FLE's documented public API and the
game's own supported career mode — no EA backend, no Blaze, and nothing derived
from leaked EA source. The earlier backend RE remains clean-room and is preserved
as a spec artifact; it is simply no longer the primary path.
---
## 9. One-paragraph summary
OpenFUT becomes a **FUT companion app that uses FIFA 23 as a match engine.** The
app owns the entire FUT experience; a resident FLE Lua script in career mode
applies the app's squad live (no restart), reports game state to drive a safe
Apply button, and reads match results back to feed progression. This sidesteps
every backend wall, runs on confirmed FLE capabilities, builds on the finished
economy core, and delivers the intuitive, offline, FUT-flavored loop that is the
actual goal.
+108
View File
@@ -0,0 +1,108 @@
# FIFA 23 PC Startup Flow (Offline / Proton)
Observed via FLE log, hook log, and file inspection on 2026-06-26.
## Launch chain
```
umu-run / Steam → FIFA23.exe (via Proton/Wine)
├─ DLL load order (before entry point)
│ ntdll.dll, kernel32.dll, ws2_32.dll …
│ version.dll ← our hook DLL slot (loads here)
│ FIFALiveEditor.DLL ← injected by FLE launcher after ~100 ms
├─ anadius / LSX emulator (anadius64.dll)
│ Fakes EA App / Origin session
│ Reads HKLM\SOFTWARE\Wow6432Node\Origin\ClientPath
│ Writes AppData\Local\anadius\LSX emu\achievement-*.xml
│ Provides fake PersonaId=1144668899 / UserId=1000200030000
├─ EA Anti-Cheat (EAAntiCheat.GameServiceLauncher.exe)
│ Spawns as child; checks EAAntiCheat.cfg
│ Not active in offline/cracked builds (FakeEAACLauncher present)
└─ FIFA23.exe entry point
Frostbite engine init (BuildDate 2023-07-05, changelist 5417699)
Reads Data\initfs_Win32 ← Frostbite package manifest
Reads Data\layout.toc ← file-system layout
Reads Patch\initfs_Win32 ← patches on top of base
Reads Documents\FIFA 23\fifasetup.ini ← display settings
Reads Data\locale.ini ← language table
Reads Data\db_meta.xml (via FLE) ← DB schema for all tables
```
## Phase timing (observed, single machine)
| Phase | Time after launch | Trigger |
|------------------------------|-------------------|----------------------------------|
| DLL load + FLE injection | 0 0.3 s | OS loader |
| Engine + DirectX init | 0.3 5 s | FIFA23 entry point |
| "Press any key" splash | ~5 s | First rendered frame |
| Main menu | ~25 s | After key press |
| FUT mode entry (attempted) | user-driven | User selects FUT tile |
| Network calls to EA services | at FUT entry | DirtySDK / EAWebKit |
## Files read at startup (observed)
| File | Format | Purpose |
|------|--------|---------|
| `Data/initfs_Win32` | Frostbite pkg | Base asset manifest |
| `Data/layout.toc` | Frostbite TOC | File layout index |
| `Patch/initfs_Win32` | Frostbite pkg | Patch layer |
| `Data/locale.ini` | INI | String localisation |
| `Data/db_meta.xml` | XML | DB schema (loaded by FLE) |
| `Data/id_map.json` | JSON | Player/team ID→name map |
| `Data/char_conv.json` | JSON | Character conversion table |
| `Documents/FIFA 23/fifasetup.ini` | INI | Display/audio settings |
| `AppData/Local/Temp/FIFA 23/_replay0.bin` | binary | Replay buffer |
| `anadius.cfg` | VDF | Fake EA persona config |
| `AppData/Local/anadius/LSX emu/achievement-*.xml` | XML | Achievement state |
## Files written during a session (observed)
| File | When written | Content |
|------|-------------|---------|
| `Documents/FIFA 23/settings/Settings*` | Main menu reached | FBCHUNKS — controller/display prefs |
| `Documents/FIFA 23/settings/ProfileOptions` | Profile load | FBCHUNKS — 1.5 MB profile blob |
| `Documents/FIFA 23/filesystemcache/survey.state` | Startup | Empty state file |
| `Documents/FIFA 23/filesystemcache/atlPlayTimeJson/playtime_*.json` | Ongoing | Playtime tracking |
| `FIFA 23 Live Editor/config.json` | FLE ready | FLE settings (rewritten each session) |
| `Logs/log_DD-MM-YYYY.txt` | Throughout | FLE debug log |
## Save file formats
### FBCHUNKS (Frostbite chunk container)
- Magic: `46 42 43 48 55 4E 4B 53` (`FBCHUNKS`)
- Byte 8: version (01 seen)
- Offset 0x12: null-terminated label string (e.g. "Personal Settings 1", "Career - Player Progress 1")
- Remainder: compressed/binary chunk data — no public spec; requires Frostbite tooling to fully parse
- Tools: [Frosty Tool Suite](https://github.com/CadeEvs/FrostyToolSuite) can read/write these
### fifasetup.ini
- Plain `KEY = VALUE` ini, fully human-readable
- Safe to edit (display resolution, locale, vsync)
## Network calls at FUT entry (observed with iptables redirect)
Traffic pattern captured before changing strategy:
- Multiple TLS connections to port 443 (destination: EA servers, resolved as various EA IPs)
- TLS 1.3, AES-256-GCM (DirtySDK's copy of ProtoSSL, inline in FIFA23.exe)
- No SNI sent (DirtySDK does not set `server_name` extension)
- Connections originate from Wine/Proton network stack via Linux kernel TCP
Specific EA hostnames used (from openfut-bridge captures, not decoded from TLS):
- `fut.ea.com` (FUT API)
- `accounts.ea.com` (auth)
- `gateway.ea.com` (entitlements)
- `pin-river.data.ea.com` (telemetry)
## Key FLE Lua API hooks
FLE injects `FIFALiveEditor.DLL` and exposes a Lua engine that can:
- Read any in-memory DB table via `GetDBTableRows(tableName)`
- Write any cell via `EditDBTableField`
- Query career mode state via `IsInCM()`
- Get player/team names via `GetPlayerName`, `GetTeamName`
This is the primary safe integration path (see `fut-integration-options.md`).
+191
View File
@@ -0,0 +1,191 @@
# Foundational test — live custom XI via Freeze Lineup
**Status: PENDING — test has not yet been run.**
This is build-order step 1 from `docs/direction.md`: the test everything else
in the direction pivot depends on.
## What changed since the first draft of this doc
The first version of this test guessed at a "selection bias" DB field and a
candidate squad/lineup table name, based on general FIFA-modding precedent
that turned out not to hold for FLE's documented API — no such field appears
anywhere in FLE's actual Lua API docs or its own example scripts. While
researching an unrelated hotkey issue, a **confirmed, FLE-documented**
mechanism for forcing a starting XI turned up instead: the **Formation
Editor's "Freeze Lineup" feature** (FLE wiki, `Formation-Editor.md`):
> This feature can be used in player career mode if you want to manage the
> starting lineup of your team. Can be also used in manager career mode to
> manually manage your next opponent's starting lineup.
Steps (GUI, no scripting): open Formation Editor for a team → arrange players
on the pitch → tick **Freeze Lineup**`Data → Save`.
This is real and documented, but it's GUI-only — there is no Lua function for
it, and what DB write it actually performs under the hood is undocumented.
This test is now two phases: confirm the GUI feature works at all, then
reverse the DB write it makes so it can be replicated programmatically
(required for the app→game bridge in build-order step 2, which needs this
driven from outside the game, not from a person clicking checkboxes).
Also fixed in this pass: `EditDBTableField`'s real signature, confirmed from
FLE's own docs and `lua/scripts/99ovr_99pot.lua`, is
`EditDBTableField(cell)` where `cell` is `row["fieldname"]` with `.value`
mutated in place — **not** `EditDBTableField(table, row_index, field, value)`
as originally (incorrectly) written into the first draft of the injector
script.
## What this test settles
Whether a *specific, externally-chosen* 11 players can be forced into a
career (or Kick-Off) match's starting lineup, live, with no restart — and
whether the mechanism that does it (Freeze Lineup's underlying DB write) can
be driven by a script instead of a person clicking through the Formation
Editor UI.
If Freeze Lineup itself doesn't actually hold under match start (the wiki
doesn't show it being tested against a live match, only "you should be able
to see... when you play against them"), the whole bridge architecture in
`docs/direction.md` §3 needs rethinking — there is no other documented
mechanism for forcing a lineup.
## Prerequisites
- FIFA 23 launched normally (FLE injected, EAAC neutralized — same baseline
as `track-c-fut-table-test.md`)
- A career save loaded (Freeze Lineup is documented for career mode
specifically — confirm separately whether it does anything in Kick-Off,
don't assume it does)
- Note 11 player IDs from your club (`tools/squad-exporter/export_squad.lua`
output, `playerid` field) that are NOT currently your starting XI
## Phase 1 — confirm Freeze Lineup actually holds into a match
This has zero scripting and should be done first since everything else is
wasted effort if it fails.
1. Open the Live Editor overlay (F9, or `Windows → Settings` from the
overlay's own menu bar if the hotkey isn't registering — see the umu/Wine
hotkey note below).
2. `Features → Teams` → find your team → `Edit`.
3. `Team → Formation` to open the Formation Editor.
4. Swap players around on the pitch so the XI differs from your current
actual starting XI in some checkable way (e.g. swap two outfield players'
positions, or bench/start a specific player).
5. Tick **Freeze Lineup**.
6. `Data → Save`.
7. Hide Live Editor (F9), save your career **on a new slot** (don't overwrite
your main save in case this corrupts something), exit to main menu, reload
that save, and check the team's lineup screen / play a match and watch who
starts.
**Record in the Results table below whether the frozen lineup actually took
the pitch.** If not, stop here — Phase 2 is moot.
## Phase 2 — find the underlying DB write
Only proceed if Phase 1 confirmed Freeze Lineup works.
1. In FLE's Lua Engine, run `tools/squad-injector/snapshot_lineup_tables.lua`.
This dumps every DB table whose name contains `squad`, `lineup`,
`formation`, `tactic`, `teamsheet`, `selection`, `players`, or `teams` to
`C:\FIFA 23 Live Editor\openfut_snapshot_<timestamp>.json`. Note this
filename — this is your **before** snapshot.
2. Without restarting or reloading, repeat the Formation Editor steps from
Phase 1 (steps 26 only — open Formation Editor, change the lineup, tick
Freeze Lineup, `Data → Save`). Don't save/reload the career between
snapshot and this step — keep it to a single live session so the diff
isn't polluted by other state changes.
3. Run `snapshot_lineup_tables.lua` again. This is your **after** snapshot.
4. Copy both JSON files out of the Wine prefix (same path pattern as
`track-c-fut-table-test.md`: `~/Games/umu/.../drive_c/FIFA 23 Live
Editor/`) and run:
```bash
python3 tools/squad-injector/diff_snapshots.py before.json after.json
```
5. The output shows exactly which table(s) and field(s) changed. This is the
real, confirmed write Freeze Lineup performs — record it in the Results
table below.
## Phase 3 — replicate the write via script
1. Open `tools/squad-injector/apply_lineup_write.lua` and fill in
`TARGET_TABLE` and `TARGET_FIELDS` using Phase 2's diff output.
2. Edit `C:\FIFA 23 Live Editor\openfut_test_xi.json`:
```json
{
"team_id": 12345,
"xi": [
{ "player_id": 111111, "position": 0 },
{ "player_id": 222222, "position": 5 }
]
}
```
Use 11 entries. Position codes are **confirmed numeric 027**
(`GK=0, SW=1, RWB=2, RB=3, RCB=4, CB=5, LCB=6, LB=7, LWB=8, RDM=9, CDM=10,
LDM=11, RM=12, RCM=13, CM=14, LCM=15, LM=16, RAM=17, CAM=18, LAM=19,
RF=20, CF=21, LF=22, RW=23, RS=24, ST=25, LS=26, LW=27`) — from
`lua/scripts/export_season_stats.lua`'s `get_pos_name` table in FLE's own
repo, not a guess.
3. Run `apply_lineup_write.lua` from FLE's Lua Engine.
4. Repeat the save-to-new-slot / reload / check-lineup verification from
Phase 1, but this time without ever opening the Formation Editor — the
write was made entirely from the script.
## Classification criteria
### "Confirmed — full mechanism works"
Phase 1 holds, Phase 2 finds a clean diff, Phase 3's scripted write produces
the same in-match result as the manual GUI path.
**Verdict:** Build-order step 1 done. Proceed to step 2 (bridge transport) in
`docs/direction.md`.
### "GUI works, script doesn't"
Phase 1 holds but Phase 3's replicated write doesn't stick, even though the
diffed fields matched what changed in Phase 2.
**Verdict:** Freeze Lineup likely does more than a single DB field write
(e.g. an internal engine call beyond `EditDBTableField`'s reach, or a second
write the diff missed because it happened in a table outside the `KEYWORDS`
filter in `snapshot_lineup_tables.lua` — widen the filter and redo Phase 2).
### "Freeze Lineup doesn't hold at all"
Phase 1 fails — the lineup reverts to the game's own AI-picked XI regardless.
**Verdict:** No confirmed mechanism exists for forcing a lineup. This kills
the bridge architecture as designed in `direction.md` §3 and needs a return
to first principles — there is no fallback documented anywhere in FLE's wiki
for this specific case.
## A note on the umu/Wine F9/F11 hotkey issue
If FLE's F9 (hide/show) hotkey isn't registering under umu, this is plausibly
a Wine keyboard-hook limitation (FLE's global hotkey detection likely uses a
low-level hook that doesn't translate cleanly through Wine's input layer) —
not something documented anywhere in FLE's own troubleshooting docs, which
don't mention Linux/Wine at all. F11 specifically has **no documented FLE
function** — F9 is the only documented toggle. Workaround: click directly
into the FLE overlay window (it should still be visible/clickable even if the
hotkey doesn't fire) and use its own menu bar instead of relying on the
hotkey.
## Results
*(To be filled in after the test is run.)*
| Field | Value |
|---|---|
| Date run | — |
| Phase 1: Freeze Lineup holds into a match? | — |
| Phase 2: table(s)/field(s) changed | — |
| Phase 3: scripted write reproduces Phase 1 result? | — |
| **Classification** | **PENDING** |
+136
View File
@@ -0,0 +1,136 @@
# FUT Integration Options
How to connect FIFA 23 to the OpenFUT local simulator, ranked by safety and feasibility.
## Option A — FLE Lua scripting (RECOMMENDED)
**What it does:** Use FIFA Live Editor's in-memory Lua API to read and write the game's
database tables at runtime. FLE is already injected; no additional hooking needed.
**Why it's the right path:**
- Fully offline, no EA servers touched
- FLE is already trusted by the user (it's the launch mechanism)
- `GetDBTableRows` / `EditDBTableField` expose the full Frostbite DB in memory
- Scripts run inside the game process; no IPC complexity
- Same mechanism used by modders for career mode edits today
**Integration design:**
```
openfut-core (SQLite)
│ HTTP REST (localhost)
openfut-bridge (port 8080, plain HTTP, no TLS)
│ pulls club/squad/player data as JSON
FLE Lua bridge script
│ calls GetDBTableRows, EditDBTableField
FIFA 23 in-memory DB (Frostbite)
```
The Lua script polls openfut-core's REST API at intervals (or on FUT menu entry)
and writes simulator data (coins, items, squad) into the appropriate DB tables.
**Tables likely involved (to verify with export_squad.lua):**
| Table | Expected FUT content |
|-------|---------------------|
| `players` | Player attributes (OVR, potential, stats) |
| `teams` | Club identity, stadium, colors |
| `fut_clubs` | FUT club record (if in memory when FUT loads) |
| `fut_items` | Card inventory (if in memory) |
| `fut_squads` | Active squad (if in memory) |
**Steps to implement:**
1. Run `tools/squad-exporter/export_squad.lua` from FLE Lua Engine while in FUT to discover which tables are live
2. Map openfut-core's data model to the discovered table fields
3. Write a Lua polling script that fetches `/api/v1/club`, `/api/v1/squad`, etc. from openfut-core and calls `EditDBTableField` to populate them
4. Optionally add a small HTTP client to the Lua script using LuaSocket (FLE ships with Lua 5.4)
**Limitations:**
- Changes are in-memory only; they reset on game restart (acceptable for a simulator)
- Only works while FLE is running (always true in our setup)
- FUT tables may only be populated when the FUT hub is loaded; test with the exporter
---
## Option B — Local save file injection (career mode proxy)
**What it does:** Generate or modify offline career mode save files that contain FUT-like
squad/player data, using Frostbite's FBCHUNKS format.
**Feasibility:** Medium
- FBCHUNKS format is not publicly documented but has been partially reverse-engineered by the Frosty Tool Suite project
- Career saves are 16 MB — large and complex
- Changes take effect only after a game restart
**Best use:** Pre-populating a career club with the same players as the FUT simulator squad, so offline Squad Battles use "your" players.
**Steps:**
1. Use Frosty Tool Suite to open a career save and map the schema
2. Build a Python exporter that writes a valid FBCHUNKS save with simulator squad data
3. Test: replace the career save, launch FIFA, verify squad is correct
---
## Option C — Local companion web UI
**What it does:** The user manages their FUT simulator entirely in a web browser (openfut-core already has this). A button exports the current squad/club state to a format that a Lua script or file injector can consume.
**This is already implemented** — openfut-core serves the FUT simulator REST API. The missing piece is the Lua bridge script (Option A) that reads from it.
---
## Option D — Local proxy for non-secured local calls only
**What it does:** Intercept FIFA 23's calls to `localhost:*` or a known local endpoint (not EA servers) and respond with simulator data.
**Feasibility:** Low value in isolation
- FIFA 23 does not make calls to localhost in normal operation (except EA App on port 10853)
- All FUT API calls go to EA's servers over TLS
- Intercepting those would require the approach we explicitly ruled out
**Not recommended as a primary path.** Could be combined with Option A if the Lua script exposes a local socket that a coordinator process writes to.
---
## Option E — Memory bridge (Cheat Engine / FLE offsets)
**What it does:** Use known memory offsets (FLE's `offset_cache.json`) to read/write FUT state directly in FIFA23.exe's heap.
**Feasibility:** Medium — FLE already does this for career mode
- FLE's `offset_cache.json` contains addresses for many game structures
- FUT in-memory structs are separate from career structs and may not be mapped yet
- This is fragile (offsets change with game updates)
**Not recommended** unless Options A and B both fail — too brittle.
---
## Recommendation
**Start with Option A (FLE Lua scripting).**
1. Run `tools/squad-exporter/export_squad.lua` in-game to discover which DB tables exist in FUT mode
2. Use `tools/file-watch-diff/watch.sh` to snapshot file state entering FUT and identify any new local files
3. Use `tools/network-metadata-logger/netlog.sh` to log which EA hosts FIFA contacts at FUT entry (metadata only, no decryption)
4. Map findings back to openfut-core's data model
5. Implement the Lua bridge script that calls openfut-core's REST API and writes to discovered tables
If FUT tables are not exposed by FLE's DB API (they may not be — FUT data lives server-side in online mode), fall back to **Option B** (career save injection) to provide a squad that mirrors the simulator's club.
---
## Safety boundary
The following are out of scope and must not be implemented:
- Decrypting or inspecting EA's TLS traffic
- Spoofing EA domain names or impersonating EA servers
- Sending modified clients to EA's production services
- Bypassing EA App login or account verification
- Anything that could constitute online cheating or violate EA's ToS for online play
All integration must remain local/offline/single-player.
+208
View File
@@ -0,0 +1,208 @@
# OpenFUT Status Review
*Generated 2026-06-30 — read-only stocktake, no code changed.*
---
## Executive Summary
OpenFUT has a mature offline FUT economy backend (Core, 25 phases, fully functional in
isolation) and a sophisticated hook DLL that loads into FIFA 23, redirects EA hostnames
to loopback, and bypasses TLS certificate verification. The Blaze/ProtoSSL layer is
structurally ready: framing code exists, a TLS listener runs, cert-verify is patched.
However the project is currently blocked before any Blaze traffic is ever seen.
The fundamental problem is that FIFA 23 submits `GoOnline` to EbisuSDK and then
**waits for an asynchronous ONLINE_STATUS_EVENT push** from the EA-app LSX server —
a push that current code never sends. Every approach tried so far (flipping poll
return values, forcing the state flags, read-only probes) confirms the gate is
event-driven, not poll-driven. The Blaze captures directory contains six empty files.
No Fire2 frame from FIFA 23 has ever been decoded. Until the ONLINE_STATUS_EVENT push
is synthesized and delivered correctly, Milestones 27 are all waiting on the same
single wall.
---
## 1. Proven vs Assumed
| Claim | Status | Evidence |
|---|---|---|
| FIFA 23 uses DirtySDK / ProtoSSL | **Proven** | String scan hit `ProtoSSLSend`, `ProtoSSLRecv`, `gosredirector` in FIFA23.exe memory (Task 1) |
| `version.dll` loads and runs hook code | **Proven** | `hook.log` written at DLL_PROCESS_ATTACH |
| `getaddrinfo` IAT hook redirects EA domains to loopback | **Proven** | Hook log records every EA `getaddrinfo` call; connect_hook log confirms port redirects |
| ProtoSSL cert-verify prologue found and patched (FIFA23.exe) | **Proven** | ssl_patch.rs prologue confirmed at file offset 0xf0c850; hook log "ssl: main exe cert-verify patched" |
| ProtoSSL cert-verify patched in EAWebKit.dll | **Proven** (if loaded) | Lazy patch fires on first EA getaddrinfo call; hook log message confirms |
| Gate is upstream of DirtySDK — no DNS/connect fires on FUT entry | **Proven** | getaddrinfo, connect, WSASend/Recv hooks all show zero external traffic during "connecting to EA Servers" |
| `GoOnline` is called by the game | **Proven** | Read-only detour on `anadius64.dll+0x2BB90` confirmed hit |
| anadius returns GoOnline success | **Proven** | Handler observed returning successfully; game still retries every ~7 s |
| Gate is downstream of GoOnline | **Proven** | GoOnline called + returns success; no Blaze connect follows |
| Connection-state function: `GetInternetConnectedState @ anadius64.dll+0x27790` | **Proven** | Located via anadius LSX command-registration table; two-flag branch decoded (`+0xCAB1A`, `+0xCAB1B`) |
| Gate is event-driven (game waits for async push, not a poll return) | **Proven** | Forced both state flags AND GoOnline return to "1"; game kept retrying; worker-thread stack scan confirms handler runs on anadius IOCP thread, not FIFA's thread |
| GoOnline runs on anadius worker thread, not FIFA's call thread | **Proven** | Stack scan from inside detour found zero FIFA23.exe frames, sp ~2.4 KB from thread stack top |
| `protossl-scan` live toolkit is exhausted for finding GoOnline in FIFA23.exe | **Proven** | No `"GoOnline"` string in image; worker-thread call stack has no FIFA frames; jmpscan yields ~3875 hits (overwhelmingly data false positives) |
| FIFA 23 redirector config references `Authorization:` header (Nucleus token) | **Proven** | Found in FIFA23.exe .rdata pointer table @ `+0x83FC858` |
| openfut-core REST API complete and tested | **Proven** | 25 phases, 15 migrations, passing integration tests |
| Bridge LSX server starts and handles request-response | **Proven** (code) | `openfut-bridge/src/lsx.rs` + `main.rs` — server starts on 127.0.0.1:3216 |
| Bridge LSX server ACTUALLY receives FIFA's LSX connections | **UNCONFIRMED** | anadius may intercept the same calls in-process before the TCP connection reaches the bridge |
| Bridge LSX server `GetInternetConnectedState → connected="1"` unblocks the gate | **UNCONFIRMED (known to fail in-process)** | Flipping the value via anadius in-process failed; bridge path not yet confirmed working |
| ONLINE_STATUS_EVENT push XML format | **UNKNOWN** | No capture; format not derived |
| Fire2 framing is correct for FIFA 23 | **UNCONFIRMED** | Implemented based on post-2012 EA convention; all blaze captures are empty (0 bytes) |
| Blaze component / command IDs for FIFA 23 | **UNKNOWN** | Zero captures; dispatch table entirely empty placeholders |
| ProtoSSL recv-injection convention (non-blocking return values etc.) | **UNCONFIRMED** | Never reached M4; recv_hook module removed from active install path |
| FUT REST endpoint paths in mapper.rs | **SPECULATIVE** | Based on community knowledge of older FIFA titles; the one actual capture in `captures/` is an early GET from before the Blaze strategy |
| FLE Lua API exposes FUT DB tables in memory | **UNKNOWN** | `export_squad.lua` has never been run; FUT data may only exist server-side in online mode |
---
## 2. Milestone Status
| Milestone | Status | Blocker | Depends on unconfirmed assumption? |
|---|---|---|---|
| **M1** — Locate connection-state decision point | ✅ Done | — | No |
| **M2** — Flip gate, force "connected" | ⛔ Blocked | Game waits for async ONLINE_STATUS_EVENT push; no current code sends it | Yes — unknown event XML format |
| **M3** — First ProtoSSL plaintext on Blaze connection | 🔲 Not started | Depends on M2 | Yes — Fire2 framing unconfirmed |
| **M4** — Answer redirector + decode first Fire2 frame | 🔲 Not started | Hard wall: Fire2 framing, recv-injection convention, component/command IDs all unconfirmed | Yes — all three unknown |
| **M5** — Blaze preauth / login / postauth | 🔲 Not started | Depends on M4 | Yes — Blaze auth TDF body layout unknown |
| **M6** — FUT entry + hub load | 🔲 Not started | Depends on M5; also requires FUT REST response shapes confirmed | Yes — endpoint paths speculative |
| **M7** — Squad Battles (AI FUT) | 🔲 Not started | Depends on M6 | Yes |
**Note on roadmap.md wording:** Under M2M4, roadmap.md uses `**Done (observable):**` bullets. These describe the *success criterion* for each milestone, not an achieved state. The authoritative status is in `connection-gate-findings.md` (M2 attempts failed; M3/M4 never started). The roadmap has not been updated to reflect M2 failure.
### M4 is the first hard wall in detail
Even assuming M2 is solved, M4 requires three unconfirmed things simultaneously:
1. **Fire2 framing** — the 12-byte header layout is assumed; if FIFA 23 uses an older Fire variant or a custom delta, the codec will misparse every packet.
2. **ProtoSSL recv-injection** — delivering responses to the game via recv hook requires knowing what return values and buffer conventions ProtoSSL expects; recv_hook.rs exists but is not installed.
3. **Blaze component/command IDs** — the dispatch table is entirely empty; we cannot answer any request until IDs are known from captures.
All three are resolved by getting one real captured frame. M4 is primarily a capture problem, not a decoding problem — once bytes exist, the framing and IDs are immediately readable.
---
## 3. Blockers, Risks, Unknowns
### Blockers (stop progress now)
1. **ONLINE_STATUS_EVENT push not synthesized** *(M2 wall)*
The game calls GoOnline, gets success, then waits indefinitely for a push event on the LSX socket that never arrives. This is the single gate blocking all Blaze work. Options: (a) trace the event format via Ghidra on FIFA23.exe (xref `ONLINE_STATUS_EVENT` string + the game's EbisuSDK listener), (b) RE anadius's LSX event-send path (find what it would push in an "online" scenario), (c) brute-force push candidate event XMLs and observe whether the game advances.
2. **Bridge LSX server delivery unconfirmed** *(architectural risk converted to blocker)*
The hook passes port 3216 connections through, assuming the bridge LSX server on the Linux host receives them. If anadius's in-process hooks intercept the winsock calls before they reach the TCP stack, the bridge server is never reached. This must be confirmed by checking `openfut_hook.log` for a getaddrinfo on the LSX host, or by observing the bridge server's accept logs.
### Risks (could derail later)
3. **Fire2 framing wrong** *(M4 risk)*
If FIFA 23 uses Fire (pre-2012) or a modified frame layout, the codec misparses. Mitigation: the server has a `Raw` fallback mode for capturing raw bytes when framing fails.
4. **Secondary auth-token gate** *(M5 risk)*
`connection-gate-findings.md` noted the redirector request carries an `Authorization:` header. M1's final conclusion said `GetAuthCode` returns a fake token that appears accepted — but this was inferred, not confirmed by seeing the redirector request actually constructed with that token.
5. **EAAC not fully neutralized** *(persistent risk)*
`FakeEAACLauncher` bypasses the anticheat launcher. The hook DLL is unsigned. If EAAC is ever active (e.g., after a game update re-enables it), all hooks fail silently. Marked as "not active in offline/cracked builds" — assumed, not confirmed on every launch.
6. **FUT REST response shapes wrong** *(M6 risk)*
The 61 endpoint mappings in mapper.rs and the shaper stubs in shaper.rs are based on community guesses about older FIFA FUT APIs, not FIFA 23 captures. Response JSON shapes may differ enough to cause the client to fail silently or crash.
### Unknowns (open questions)
7. **ONLINE_STATUS_EVENT XML format** — exact tag names, field order, sender attribute, and any nonces/tokens required.
8. **GoOnline event sequence** — whether ONLINE_STATUS_EVENT alone is sufficient or a sequence of events (e.g., PROFILE_EVENT, LOGIN_EVENT, COMMERCE_EVENT) is expected.
9. **Whether FLE exposes FUT DB tables** — FUT card inventory and squad data likely live server-side in online mode; FLE may not surface them for in-process editing.
10. **Blaze component/command IDs for FIFA 23** — entirely unknown; no captures.
11. **openfut_hook.log current content** — we have the code but no log output in any document. Whether the current hook (with connect, ssl_patch, tls_bypass, WSAIoctl, origin_spy all installed) fires correctly and what it observes is unverified in this review.
---
## 4. Track Comparison
### Track A — Full EA-backend fake (M1M7, playable FUT vs AI)
**What it delivers:** The FIFA 23 FUT hub loads from OpenFUT Core; Squad Battles matches play and reward economy items.
**Effort:** Research-grade. Minimum path: synthesize ONLINE_STATUS_EVENT (unknown format, 12 weeks of RE), then capture Fire2 frames (days once M2 is solved), then implement Blaze auth handlers (weeks), then implement FUT entry (weeks), then Squad Battles (weeks). Realistic minimum: 36 months of expert RE work.
**Proven support:** Hook loads and redirects correctly. TLS bypass patched. Core economy backend complete. Blaze framing code and TLS listener exist.
**Assumed:** Fire2 framing correct; component/command IDs discoverable from captures; FUT REST shapes close enough to community guesses; no additional undiscovered gates.
**Evidence for:** Architecture is coherent. The M1 finding (gate precisely named and decoded) was achieved cleanly. The in-process hook approach is validated.
**Evidence against:** M2 was attempted and failed with the in-process approach. The event-driven architecture adds a full EbisuSDK emulation layer before even one Blaze byte is seen. The live toolkit is exhausted (Path A verdict); Ghidra-level work on a 505 MB binary is required. Six capture files with zero bytes.
---
### Track B — Clean-room spec deliverable (M1M5 documented)
**What it delivers:** A documented map of the connection gate, LSX event sequence, Blaze auth surface (transport, framing, gate conditions, component IDs, TDF schemas). Valuable as an archival/community artifact even if Track A stalls.
**Effort:** Medium. M1 is done. M2M5 documentation emerges as a by-product of engineering work. The spec itself (writing) is lightweight; the engineering to produce the captures is the cost.
**Proven support:** M1 complete and documented. connection-gate-findings.md is already a high-quality spec artifact.
**Assumed:** Same as Track A for the unconfirmed values, but the spec can mark them `TODO/CONFIRM` rather than needing to implement them.
**Evidence for:** The clean-room constraint means a spec is the only artifact that can be safely published. connection-gate-findings.md shows this approach produces real value. B finishes even if A is never fully playable.
**Evidence against:** Track B alone doesn't produce a playable FUT; it is a foundation, not an end-user product.
---
### Track C — FLE Lua bridge (local-match path, skip the backend gate)
**What it delivers:** FIFA 23 career mode or Kick-Off with an OpenFUT club's players and squad loaded via FLE's in-memory DB API. No online gate, no Blaze, no TLS. Fully offline from day one.
**Effort:** Low-to-medium. FLE is already loaded in the normal launch path. Tools exist (`tools/squad-exporter/`, `tools/profile-exporter/`). Primary unknown is whether FUT-relevant DB tables are accessible.
**Proven support:** FLE Lua API exposes `GetDBTableRows` / `EditDBTableField` for career mode. `fifa23-startup-flow.md` confirms FLE injects at load. `fut-integration-options.md` documents the integration path in detail and rates this as the recommended option.
**Assumed:** FUT card/club/squad data has in-memory DB table representations that FLE can write. If FUT data is purely server-side (loaded from EA servers, not from the Frostbite DB layer), Track C produces no FUT simulation at all — only career mode player stats.
**Evidence for:** Career mode already works with FLE edits (community precedent). Tools are present and designed for this path. No infrastructure work needed.
**Evidence against:** FUT in FIFA 23 uses server-side data. The cards in a player's FUT club, the coins, the squad — these are fetched from `fut.ea.com` REST APIs, not from the Frostbite embedded DB. FLE's `GetDBTableRows` likely exposes base player stats tables but not FUT item tables. The crucial test (run `export_squad.lua` while in FUT mode) has never been done.
---
### Recommendation
**Start Track C immediately as a parallel, low-cost validation.**
Run `export_squad.lua` in FLE while inside the FUT hub (or attempting to enter it). If FUT tables appear in the export, Track C is viable and is the fastest path to something a user can interact with. This test takes one session and costs nothing.
Simultaneously, **continue Track A/B with the next concrete RE step:** synthesize the ONLINE_STATUS_EVENT push. The most actionable option is to run `origin_spy` logs from the current hook to see what LSX events fire during a session, then attempt to push candidate event XMLs via the bridge LSX server and watch whether the game advances. This is bounded, testable work that either unblocks M2 or produces the spec value for Track B.
**Do not abandon Track A/B for Track C** — they are complementary. Core is already built; the bridge is mostly built. The gap is purely the RE wall at M2.
---
## 5. Architecture and Provenance Sanity-Check
### Hook + Brain coherence
The CLAUDE.md bridge architecture diagram (hook intercepts ProtoSSL → plain localhost TCP → blaze_brain → Core) remains coherent. The M1/M2 findings revealed one additional layer (EbisuSDK LSX event) that must precede the Blaze connection. The bridge has been updated to handle LSX directly. The overall design is sound; the M2 blocker is an implementation gap (event synthesis), not an architectural flaw.
**One inconsistency to flag:** The hook's `lsx.rs` contains a complete in-process LSX emulator (AES-128-ECB, CRandom, all response builders), but the recv/send hooks that activate it are explicitly removed (`lib.rs`: "recv/send hooks removed — LSX is now handled by the native openfut-bridge LSX server"). This is dead code. The bridge's LSX server is the current path. The in-process lsx.rs should either be deleted or documented as a fallback; its presence is confusing.
### Clean-room status
No evidence of EA leaked source anywhere in the tree. All RE work is derived from:
- Running the shipping binary and observing behavior (function return values, network traffic patterns)
- Memory scanning of the live process (string search, xref, disasm of observed addresses)
- Reading anadius's own compiled output (its exported symbols, its LSX XML format — which is anadius's own implementation, not EA's)
- Community FUT API knowledge (mapper.rs endpoint paths — plausible but speculative)
The Blaze framing in `fifa-blaze/crates/blaze-proto/src/frame.rs` cites "Fire2 used by ME3, BF3, and most post-2012 titles" — this is sourced from public community documentation of those older titles, not from any leaked EA source. **Clean-room intact.**
The `AES_KEY` in the hook's lsx.rs (`[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]`) is a placeholder key used for the LSX session encryption. The real session key is derived from the challenge seed via CRandom — this algorithm was RE'd from anadius's own binary. No EA source required.
---
## 6. If You Read Only This
- **The project is blocked at M2.** FIFA 23 submits `GoOnline`, gets success, then waits for an async `ONLINE_STATUS_EVENT` push on the LSX socket that no current code ever sends. All six Blaze capture files are empty (0 bytes). No Fire2 frame has ever been decoded.
- **M1 is the only completed milestone.** The gate function (`GetInternetConnectedState @ anadius64.dll+0x27790`) is precisely named and its two-flag branch decoded. Everything after M1 is either blocked or not started.
- **The next concrete action** is synthesizing the ONLINE_STATUS_EVENT push XML and testing whether the bridge's LSX server can deliver it to the game. This is the single thing that unblocks all Blaze work.
- **Track C (FLE Lua) is untested but cheap to validate.** Run `export_squad.lua` while in FUT to find out if FUT DB tables are accessible. If yes, it is the fastest path to user-visible results. If no, it is ruled out with one session.
- **openfut-core is complete and ready** — 25 phases, 15 migrations, full economy REST API, passing tests. It is not blocking anything; it is waiting for the bridge to connect to it.
+93
View File
@@ -0,0 +1,93 @@
# Track C — FUT DB table viability test
**Status: PENDING — test has not yet been run.**
## What this test settles
Track C ("FLE Lua bridge") would inject OpenFUT club data directly into FIFA 23's
in-memory Frostbite DB tables at runtime, bypassing the entire backend/Blaze stack.
It is only viable for FUT (not just career mode) if FUT-specific tables — card
inventory, squad composition with FUT fields, coins — are accessible in memory when
the game is in the FUT area.
FUT data in online mode is fetched server-side from `fut.ea.com`. It is not known
whether FIFA 23 mirrors any of this into the Frostbite in-memory DB that FLE
can read/write. This test settles that question directly.
## Test procedure
**Prerequisites:**
- FIFA 23 launched normally via umu-run/Steam
- FLE (FIFA Live Editor) injected and active (normal launch path)
- EAAC in offline/neutralized state
- Game navigated as deep into FUT as possible (FUT hub if reachable; otherwise the
furthest FUT screen before the gate blocks it)
**Run the exporter:**
1. In FLE's Lua Engine, open and run `tools/squad-exporter/export_squad.lua`
(full path on the Windows side: `C:\<game>\openfut_squad_export.json`)
2. Wait for the MessageBox "Done! N players, M teams." or "ERROR writing..."
3. Retrieve the output file from the Wine prefix:
`~/Games/umu/fifa23-tools/drive_c/FIFA 23 Live Editor/openfut_squad_export.json`
(or wherever `C:\FIFA 23 Live Editor\` maps in the active prefix)
**What to inspect in the output:**
- `all_db_tables` array — the complete list of table names visible to FLE right now
- `fut_tables` object — any table whose name contains `fut`, `club`, `pack`, `item`, or
`market` (the script auto-extracts these)
- `is_career_mode` — confirms whether FUT or career mode was active
## Classification criteria
### "FUT tables present"
`fut_tables` is non-empty AND contains FUT-specific fields beyond base player stats:
- e.g., `fut_items` with card-type / rating / chemistry fields
- e.g., a squad table with FUT formation / chemistry / loan-flag fields
- e.g., a coins or points balance field
**Verdict:** Track C is viable for FUT. Fastest path to user-visible results.
### "only base player tables"
`fut_tables` is empty (no `fut_*` / `club_*` / `item_*` / `market_*` table names found
in `all_db_tables`), OR those tables exist but contain only base player attributes
(OVR, potential, position, pace, …) — the same fields visible in career mode.
**Verdict:** Track C cannot produce FUT. It could at most provide a custom Kick-Off or
career-mode match with players sourced from OpenFUT Core. FUT items and coins exist
only on EA's servers (not in the in-memory DB in offline mode).
### "FUT area unreachable to test"
The connection gate blocked entering FUT deeply enough for FUT tables to be populated.
Record which tables were visible and at what screen the test was run.
**Verdict:** Retest after M2 is unblocked, OR test with `TLS_ENABLED=false` bridge
handling the entry check stub.
## Results
*(To be filled in after the test is run.)*
| Field | Value |
|---|---|
| Date run | — |
| FIFA screen at test time | — |
| `is_career_mode` | — |
| Total tables in `all_db_tables` | — |
| FUT-specific table names found | — |
| Key FUT fields present | — |
| **Classification** | **PENDING** |
## Honest prior
`fut-integration-options.md` rates this as the recommended path and lists `fut_clubs`,
`fut_items`, `fut_squads` as "expected" tables. However those expectations are based on
analogy with career mode (which does store club/squad in the DB). FUT's data model is
architecturally different — it is account-bound server-side. The expectation may be
wrong. This test is the oracle.
The `export_squad.lua` script checks `GetDBTablesNames()` exhaustively (not just
assumed names), so it will surface any FUT tables that actually exist, regardless of
what name they use.
@@ -0,0 +1,115 @@
# snapshot: baseline 2026-06-26T21:44:51Z
2023-06-02 22:13:13 280984156 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentaryfull_eng_us_ds/cas_02.cas
2023-06-02 22:13:13 52532092 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentaryfull_eng_us_ds/cas_03.cas
2023-06-02 22:20:08 768293618 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_07/cas_02.cas
2023-06-02 22:20:18 728251988 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_07/cas_03.cas
2023-06-02 22:20:37 1074835 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_spa_es/cas_01.cas
2023-06-02 22:20:37 1471597 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_ita_it/cas_01.cas
2023-06-02 22:20:37 1670951 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_spa_mx/cas_01.cas
2023-06-02 22:20:37 2184167 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_rus_ru/cas_01.cas
2023-06-02 22:20:37 2750298 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_ger_de/cas_01.cas
2023-06-02 22:20:37 275617825 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentaryfull_eng_us_ds/cas_01.cas
2023-06-02 22:20:37 34794805 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_jpn_jp/cas_01.cas
2023-06-02 22:20:37 5583406 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_eng_us_ds/cas_01.cas
2023-06-02 22:20:37 976736 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_chs_cn/cas_01.cas
2023-06-02 22:20:38 5481 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_04/cas_01.cas
2023-06-03 00:25:53 11768 /mnt/games/FIFA 23/Patch/Win32/commentarywc_fre_fr.toc
2023-06-03 00:25:53 1224 /mnt/games/FIFA 23/Patch/Win32/chants_install.toc
2023-06-03 00:25:53 125583 /mnt/games/FIFA 23/Patch/initfs_Win32
2023-06-03 00:25:53 12570 /mnt/games/FIFA 23/Patch/Win32/commentarywc_ita_it.toc
2023-06-03 00:25:53 12624 /mnt/games/FIFA 23/Patch/Win32/commentarywc_chs_cn.toc
2023-06-03 00:25:53 12624 /mnt/games/FIFA 23/Patch/Win32/commentarywc_dut_nl.toc
2023-06-03 00:25:53 12624 /mnt/games/FIFA 23/Patch/Win32/commentarywc_por_br.toc
2023-06-03 00:25:53 12624 /mnt/games/FIFA 23/Patch/Win32/commentarywc_rus_ru.toc
2023-06-03 00:25:53 12632 /mnt/games/FIFA 23/Patch/Win32/commentarywc_ger_de.toc
2023-06-03 00:25:53 12678 /mnt/games/FIFA 23/Patch/Win32/commentarywc_ara_sa.toc
2023-06-03 00:25:53 12678 /mnt/games/FIFA 23/Patch/Win32/commentarywc_pol_pl.toc
2023-06-03 00:25:53 12678 /mnt/games/FIFA 23/Patch/Win32/commentarywc_spa_es.toc
2023-06-03 00:25:53 12686 /mnt/games/FIFA 23/Patch/Win32/commentarywc_spa_mx.toc
2023-06-03 00:25:53 12796 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_chs_cn.toc
2023-06-03 00:25:53 14470 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_spa_es.toc
2023-06-03 00:25:53 16044 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_spa_mx.toc
2023-06-03 00:25:53 16440 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_ger_de.toc
2023-06-03 00:25:53 17494 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_ita_it.toc
2023-06-03 00:25:53 17890 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_rus_ru.toc
2023-06-03 00:25:53 22910 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_jpn_jp.toc
2023-06-03 00:25:53 24752 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_eng_us_ds.toc
2023-06-03 00:25:53 2966 /mnt/games/FIFA 23/Patch/Win32/commentarywc_eng_us_ds.toc
2023-06-03 00:25:53 45092 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_fre_fr.toc
2023-06-03 00:25:53 46980 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_eng_us_ds.toc
2023-06-03 00:25:53 49502 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_pol_pl.toc
2023-06-03 00:25:53 53840 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_ita_it.toc
2023-06-03 00:25:53 59752 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_jpn_jp.toc
2023-06-03 00:25:53 59878 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_ger_de.toc
2023-06-03 00:25:53 59878 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_spa_mx.toc
2023-06-03 00:25:53 61238 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_rus_ru.toc
2023-06-03 00:25:53 61940 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_por_br.toc
2023-06-03 00:25:53 63236 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_chs_cn.toc
2023-06-03 00:25:53 63236 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_spa_es.toc
2023-06-03 00:25:53 64802 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_ara_sa.toc
2023-06-03 00:25:53 65224 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_dut_nl.toc
2023-06-03 00:25:53 928 /mnt/games/FIFA 23/Patch/Win32/commentarywc_jpn_jp.toc
2023-08-22 15:21:19 110383 /mnt/games/FIFA 23/Patch/layout.toc
2023-08-22 15:21:19 1818 /mnt/games/FIFA 23/Patch/Win32/chants_full.toc
2023-08-22 15:21:19 2321 /mnt/games/FIFA 23/Patch/Win32/matchcinematicssba.toc
2023-08-22 15:21:19 263983 /mnt/games/FIFA 23/Patch/Win32/storycharsb.toc
2023-08-22 15:21:20 129098 /mnt/games/FIFA 23/Patch/Win32/careermodestorysba.toc
2023-08-22 15:21:20 1459 /mnt/games/FIFA 23/Patch/Win32/storyingamesba.toc
2023-08-22 15:21:20 1520 /mnt/games/FIFA 23/Patch/Win32/streaminginstallcinematicsbundle.toc
2023-08-22 15:21:20 1695 /mnt/games/FIFA 23/Patch/Win32/globalswc.toc
2023-08-22 15:21:20 1894949 /mnt/games/FIFA 23/Patch/Win32/contentlaunchsb.toc
2023-08-22 15:21:20 191908 /mnt/games/FIFA 23/Patch/Win32/worldcupsb.toc
2023-08-22 15:21:20 3052093 /mnt/games/FIFA 23/Patch/Win32/worldssb.toc
2023-08-22 15:21:20 3788 /mnt/games/FIFA 23/Patch/Win32/futsb.toc
2023-08-22 15:21:20 440453 /mnt/games/FIFA 23/Patch/Win32/presprematchsba.toc
2023-08-22 15:21:20 4886 /mnt/games/FIFA 23/Patch/Win32/presentationsb.toc
2023-08-22 15:21:20 531911 /mnt/games/FIFA 23/Patch/Win32/globalsfull.toc
2023-08-22 15:21:20 548511 /mnt/games/FIFA 23/Patch/Win32/careersba.toc
2023-08-22 15:21:20 7271 /mnt/games/FIFA 23/Patch/Win32/marketingsba.toc
2023-08-22 15:21:20 7360676 /mnt/games/FIFA 23/Patch/Win32/contentsb.toc
2023-08-22 15:21:21 24476 /mnt/games/FIFA 23/Patch/Win32/choreosb.toc
2023-08-22 15:21:21 268514476 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_07.cas
2023-08-22 15:21:21 58624 /mnt/games/FIFA 23/Patch/Win32/storysba.toc
2023-08-22 15:21:21 6144 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_01/cas_01.cas
2023-08-22 15:21:21 760492 /mnt/games/FIFA 23/Patch/Win32/globals.toc
2023-08-22 15:21:23 103085062 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_09.cas
2023-08-22 15:21:23 268483345 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_05.cas
2023-08-22 15:21:24 268448487 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_08.cas
2023-08-22 15:21:24 268563472 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_02.cas
2023-08-22 15:21:26 268663563 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_03.cas
2023-08-22 15:21:27 268440522 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_04.cas
2023-08-22 15:21:28 268597122 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_01.cas
2023-08-22 15:21:29 268587757 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_06.cas
2023-08-22 15:21:30 234374397 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_02/cas_02.cas
2023-08-22 15:21:31 410809408 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_02/cas_01.cas
2023-08-22 15:21:33 491728234 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_08/cas_01.cas
2023-08-22 15:21:36 287271203 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_05/cas_02.cas
2023-08-22 15:21:37 268438709 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_05/cas_01.cas
2023-08-22 15:21:38 271000144 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_05/cas_03.cas
2023-08-22 15:21:44 236583358 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_05/cas_04.cas
2023-08-22 15:21:46 492908031 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_07/cas_01.cas
2023-08-22 15:21:52 270993339 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_00/cas_04.cas
2023-08-22 15:21:58 273177012 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_00/cas_03.cas
2023-08-22 15:22:02 99254970 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_00/cas_06.cas
2023-08-22 15:22:05 270041465 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_00/cas_01.cas
2023-08-22 15:22:12 268490780 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_00/cas_02.cas
2023-08-22 15:22:18 295744650 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_00/cas_05.cas
2023-08-22 15:22:19 270176162 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_03/cas_01.cas
2023-08-22 15:22:20 118305 /mnt/games/FIFA 23/Patch/Win32/fifa/fifagame/fifagame.toc
2023-08-22 15:22:20 180049441 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_03/cas_02.cas
2026-06-23 15:41:54 1536026 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/ProfileOptions
2026-06-23 15:41:54 1 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/ProfileOptions_profile
2026-06-23 15:42:00 0 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/filesystemcache/survey.state
2026-06-23 15:48:11 16272228 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/Career20260623154811
2026-06-24 17:06:53 749 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/overrideAutodetect.lua
2026-06-24 17:07:05 304 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/fifasetup.ini
2026-06-24 17:16:24 16272228 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/Career20260624171624A
2026-06-24 17:16:39 16272228 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/Career20260624171639A
2026-06-24 17:33:42 515334 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/Settings20260624173342
2026-06-24 17:33:43 16272228 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/Career20260624173343A
2026-06-24 17:44:25 189 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/filesystemcache/atlPlayTimeJson/playtime_2143594531.json
2026-06-24 19:35:05 8499 /mnt/games/FIFA 23/Logs/log_launcher_24-06-2026.txt
2026-06-25 19:26:02 45957 /mnt/games/FIFA 23/Logs/log_launcher_25-06-2026.txt
2026-06-25 19:26:24 49997 /mnt/games/FIFA 23/Logs/log_25-06-2026.txt
2026-06-26 14:38:44 251783 /mnt/games/FIFA 23/Logs/log_launcher_26-06-2026.txt
2026-06-26 14:38:59 281281 /mnt/games/FIFA 23/Logs/log_26-06-2026.txt
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# file-watch-diff/watch.sh
# Snapshots FIFA 23 local file state and diffs it between labelled game phases.
# Usage: watch.sh snapshot <label>
# watch.sh diff <label-a> <label-b>
# watch.sh watch # live tail of changes (requires inotifywait)
#
# Labels: baseline | main-menu | entered-fut | squad-change | post-match
set -euo pipefail
SNAP_DIR="$(cd "$(dirname "$0")" && pwd)/snapshots"
mkdir -p "$SNAP_DIR"
# All directories that FIFA 23 writes to locally under Wine/Proton
WATCH_PATHS=(
"$HOME/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23"
"$HOME/Games/umu/fifa23-tools/pfx/drive_c/users/steamuser/Documents/FIFA 23"
"/mnt/games/FIFA 23/Logs"
"/mnt/games/FIFA 23/Patch"
)
# Build a listing (path + size + mtime) of every file under watched dirs
snapshot() {
local label="$1"
local out="$SNAP_DIR/${label}.txt"
echo "# snapshot: $label $(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$out"
for d in "${WATCH_PATHS[@]}"; do
[ -d "$d" ] || continue
find "$d" -type f -printf '%TY-%Tm-%Td %TH:%TM:%.2TS %7s %p\n' 2>/dev/null
done | sort >> "$out"
echo "Snapshot '$label' written to $out"
wc -l "$out"
}
# Diff two snapshots by path+size+mtime
diff_snapshots() {
local a="$SNAP_DIR/${1}.txt"
local b="$SNAP_DIR/${2}.txt"
[ -f "$a" ] || { echo "Missing snapshot: $1"; exit 1; }
[ -f "$b" ] || { echo "Missing snapshot: $2"; exit 1; }
echo "=== Files added between '$1' → '$2' ==="
comm -13 <(grep -v '^#' "$a" | awk '{print $NF}' | sort) \
<(grep -v '^#' "$b" | awk '{print $NF}' | sort)
echo ""
echo "=== Files removed between '$1' → '$2' ==="
comm -23 <(grep -v '^#' "$a" | awk '{print $NF}' | sort) \
<(grep -v '^#' "$b" | awk '{print $NF}' | sort)
echo ""
echo "=== Files changed (size or mtime) between '$1' → '$2' ==="
# Join on filename, report lines whose mtime/size differs
diff <(grep -v '^#' "$a" | sort -k4) \
<(grep -v '^#' "$b" | sort -k4) \
| grep '^[<>]' \
| awk '{print $NF}' \
| sort | uniq -d
}
# Live watch using /proc/inotify fallback (poll every 2s)
watch_live() {
if command -v inotifywait &>/dev/null; then
inotifywait -r -m -e close_write,create,delete,moved_to \
"${WATCH_PATHS[@]}" 2>/dev/null
else
echo "inotifywait not found. Install inotify-tools for live watching."
echo "Falling back to 2-second poll diff (Ctrl-C to stop)..."
local tmp_a tmp_b
tmp_a=$(mktemp)
tmp_b=$(mktemp)
for d in "${WATCH_PATHS[@]}"; do
[ -d "$d" ] || continue
find "$d" -type f -printf '%TY-%Tm-%Td %TH:%TM:%.2TS %7s %p\n' 2>/dev/null
done | sort > "$tmp_a"
while true; do
sleep 2
for d in "${WATCH_PATHS[@]}"; do
[ -d "$d" ] || continue
find "$d" -type f -printf '%TY-%Tm-%Td %TH:%TM:%.2TS %7s %p\n' 2>/dev/null
done | sort > "$tmp_b"
diff "$tmp_a" "$tmp_b" | grep '^[<>]' | while read -r line; do
echo "$(date +%H:%M:%S) $line"
done
cp "$tmp_b" "$tmp_a"
done
fi
}
case "${1:-}" in
snapshot) snapshot "${2:?Usage: watch.sh snapshot <label>}" ;;
diff) diff_snapshots "${2:?}" "${3:?Usage: watch.sh diff <label-a> <label-b>}" ;;
watch) watch_live ;;
*)
echo "Usage:"
echo " $0 snapshot <label> # take a file state snapshot"
echo " $0 diff <label-a> <label-b> # show what changed between two snapshots"
echo " $0 watch # live monitor (inotifywait or poll)"
echo ""
echo "Suggested workflow:"
echo " $0 snapshot baseline"
echo " # launch FIFA"
echo " $0 snapshot main-menu"
echo " # navigate to FUT"
echo " $0 snapshot entered-fut"
echo " # make squad change"
echo " $0 snapshot squad-change"
echo " $0 diff main-menu entered-fut"
;;
esac
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env bash
# network-metadata-logger/netlog.sh
# Logs FIFA 23 network connection metadata: host, port, protocol, timing.
# Does NOT capture payload, does NOT decrypt TLS.
#
# Usage: netlog.sh [--interval 2] [--duration 120]
# Polls /proc/net/tcp* and resolves FIFA23 PIDs every N seconds.
# Output: CSV to stdout and netlog-TIMESTAMP.csv in this directory.
set -euo pipefail
INTERVAL="${INTERVAL:-2}"
DURATION="${DURATION:-300}"
OUT_DIR="$(cd "$(dirname "$0")" && pwd)/logs"
mkdir -p "$OUT_DIR"
OUT_FILE="$OUT_DIR/netlog-$(date +%Y%m%d-%H%M%S).csv"
log() { echo "$*" | tee -a "$OUT_FILE"; }
find_fifa_pids() {
pgrep -x FIFA23.exe 2>/dev/null || \
pgrep -f FIFA23.exe 2>/dev/null || \
true
}
# Decode a hex IP:port pair from /proc/net/tcp
# e.g. "0F02000A:01BB" → "10.0.2.15:443"
decode_addr() {
local hex_ip="${1%%:*}"
local hex_port="${1##*:}"
local port=$((16#$hex_port))
# IPv4 in /proc/net/tcp is little-endian
local b1=$((16#${hex_ip:6:2}))
local b2=$((16#${hex_ip:4:2}))
local b3=$((16#${hex_ip:2:2}))
local b4=$((16#${hex_ip:0:2}))
echo "${b1}.${b2}.${b3}.${b4}:${port}"
}
# TCP state codes
state_name() {
case "$1" in
01) echo ESTABLISHED ;; 02) echo SYN_SENT ;;
03) echo SYN_RECV ;; 04) echo FIN_WAIT1 ;;
05) echo FIN_WAIT2 ;; 06) echo TIME_WAIT ;;
07) echo CLOSE ;; 08) echo CLOSE_WAIT ;;
09) echo LAST_ACK ;; 0A) echo LISTEN ;;
0B) echo CLOSING ;; *) echo "STATE_$1" ;;
esac
}
# Resolve remote IP → hostname (best-effort, non-blocking)
resolve_host() {
local ip="${1%%:*}"
# Skip local/loopback
case "$ip" in
127.*|10.*|192.168.*|172.1[6-9].*|172.2[0-9].*|172.3[0-1].*) echo "$ip"; return ;;
esac
getent hosts "$ip" 2>/dev/null | awk '{print $2}' | head -1 || echo "$ip"
}
# Categorise destination by port/host heuristic
categorise() {
local host="$1" port="$2"
case "$port" in
443|8443) echo "HTTPS/TLS" ;;
80) echo "HTTP" ;;
10853) echo "EA-LSX-auth" ;;
3216|3478|3479|3480) echo "EA-UDP-STUN-or-relay" ;;
*)
case "$host" in
*ea.com*|*easports.com*|*footapi.com*|*ugc.*) echo "EA-service" ;;
*akamai*|*amazonaws*|*cloudfront*) echo "CDN" ;;
*) echo "other" ;;
esac
;;
esac
}
# Build inode→pid map from /proc
build_inode_pid_map() {
declare -gA INODE_PID
INODE_PID=()
for pid_dir in /proc/[0-9]*/fd; do
local pid="${pid_dir%%/fd}"
pid="${pid##/proc/}"
for link in "$pid_dir"/[0-9]* 2>/dev/null; do
local target
target=$(readlink "$link" 2>/dev/null) || continue
if [[ "$target" == "socket:["* ]]; then
local inode="${target//[^0-9]/}"
INODE_PID["$inode"]="$pid"
fi
done
done 2>/dev/null || true
}
log "timestamp,pid,proc,local_addr,remote_host,remote_addr,port,state,category"
declare -A SEEN_CONNS
END_TIME=$(( $(date +%s) + DURATION ))
echo "Logging FIFA 23 network metadata to $OUT_FILE (${DURATION}s, Ctrl-C to stop)..."
while [ "$(date +%s)" -lt "$END_TIME" ]; do
TS=$(date +%H:%M:%S)
build_inode_pid_map
# Read /proc/net/tcp (IPv4 ESTABLISHED and SYN_SENT)
while read -r _ local_hex remote_hex state _ _ _ _ _ inode _rest; do
[ "$state" = "01" ] || [ "$state" = "02" ] || continue
local_addr=$(decode_addr "$local_hex")
remote_addr=$(decode_addr "$remote_hex")
remote_ip="${remote_addr%%:*}"
remote_port="${remote_addr##*:}"
pid="${INODE_PID[$inode]:-}"
[ -z "$pid" ] && continue
# Filter to FIFA-related PIDs (or log all if no FIFA found)
proc=$(cat "/proc/$pid/comm" 2>/dev/null) || continue
state_str=$(state_name "$state")
key="${pid}:${local_addr}:${remote_addr}"
if [ -z "${SEEN_CONNS[$key]:-}" ] || [ "$state_str" != "${SEEN_CONNS[$key]}" ]; then
SEEN_CONNS["$key"]="$state_str"
host=$(resolve_host "$remote_ip")
cat=$(categorise "$host" "$remote_port")
log "$TS,$pid,$proc,$local_addr,$host,$remote_addr,$remote_port,$state_str,$cat"
fi
done < /proc/net/tcp 2>/dev/null || true
sleep "$INTERVAL"
done
echo "Done. Output: $OUT_FILE"
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""
profile-exporter/export_profile.py
Reads FIFA 23 local profile/settings files and exports what can be read.
FIFA 23 uses the Frostbite FBCHUNKS container format for saves.
This exporter handles:
- fifasetup.ini → plain text, fully readable
- ProfileOptions → FBCHUNKS binary (partial parse of header + string scan)
- Settings* → FBCHUNKS binary (contains "Personal Settings 1" label)
- filesystemcache/ → JSON files, fully readable
- anadius.cfg → Valve KeyValues format, contains persona info
Usage:
python3 export_profile.py [--output out.json]
"""
import struct
import json
import re
import sys
from pathlib import Path
from datetime import datetime
STEAM_PFX = Path.home() / ".local/share/Steam/steamapps/compatdata/3887260930/pfx"
UMU_PFX = Path.home() / "Games/umu/fifa23-tools/pfx"
GAME_DIR = Path("/mnt/games/FIFA 23")
DOCS_PATHS = [
STEAM_PFX / "drive_c/users/steamuser/Documents/FIFA 23",
UMU_PFX / "drive_c/users/steamuser/Documents/FIFA 23",
]
def find_docs() -> Path | None:
for p in DOCS_PATHS:
if p.exists():
return p
return None
def read_ini(path: Path) -> dict:
out = {}
for line in path.read_text(errors="replace").splitlines():
line = line.strip()
if "=" in line and not line.startswith(";") and not line.startswith("#"):
k, _, v = line.partition("=")
out[k.strip()] = v.strip().strip('"')
return out
def parse_fbchunks_header(data: bytes) -> dict:
"""Extract what we can from the FBCHUNKS container without a full format spec."""
if not data[:8] == b"FBCHUNKS":
return {"error": "not FBCHUNKS"}
info: dict = {"magic": "FBCHUNKS", "size_bytes": len(data)}
# The label string appears at offset 0x12 in Settings files
# Try to pull the first null-terminated ASCII string after offset 8
try:
label_start = 0x12
end = data.index(b"\x00", label_start)
label = data[label_start:end].decode("ascii", errors="ignore")
if label:
info["label"] = label
except (ValueError, IndexError):
pass
# String-scan for recognisable names/values
strings = re.findall(rb"[ -~]{6,}", data)
info["readable_strings_sample"] = [s.decode("ascii") for s in strings[:40]]
return info
def read_filesystem_cache(docs: Path) -> dict:
cache_dir = docs / "filesystemcache"
out = {}
if not cache_dir.exists():
return out
for f in cache_dir.rglob("*"):
if f.is_file():
try:
out[str(f.relative_to(cache_dir))] = json.loads(f.read_text())
except Exception:
out[str(f.relative_to(cache_dir))] = f.read_text(errors="replace")
return out
def parse_anadius_cfg(path: Path) -> dict:
"""Parse Valve KeyValues (VDF) format anadius.cfg for persona/user info."""
out: dict = {}
current_section: str | None = None
for line in path.read_text(errors="replace").splitlines():
line = line.strip()
if line.startswith('"') and line.endswith('"') and "{" not in line:
# lone quoted string — section name coming next
current_section = line.strip('"')
elif line.startswith('"') and "\t" in line:
parts = [p.strip().strip('"') for p in line.split("\t") if p.strip()]
if len(parts) == 2:
key, val = parts[0], parts[1]
section_key = f"{current_section}.{key}" if current_section else key
out[section_key] = val
return out
def export_settings_files(docs: Path) -> list:
settings_dir = docs / "settings"
entries = []
if not settings_dir.exists():
return entries
for f in sorted(settings_dir.iterdir()):
if f.is_file():
data = f.read_bytes()
entry: dict = {
"file": f.name,
"size_bytes": len(data),
"mtime": datetime.fromtimestamp(f.stat().st_mtime).isoformat(),
}
if data[:8] == b"FBCHUNKS":
entry["format"] = "FBCHUNKS"
entry["header"] = parse_fbchunks_header(data)
elif data and all(0x20 <= b < 0x7F or b in (0x09, 0x0A, 0x0D) for b in data[:256]):
entry["format"] = "text"
entry["content"] = f.read_text(errors="replace")[:2000]
else:
entry["format"] = "binary"
entries.append(entry)
return entries
def main():
out_path = None
if "--output" in sys.argv:
idx = sys.argv.index("--output")
out_path = Path(sys.argv[idx + 1])
docs = find_docs()
result: dict = {
"exported_at": datetime.utcnow().isoformat() + "Z",
"docs_path": str(docs) if docs else None,
}
# fifasetup.ini
if docs:
ini_path = docs / "fifasetup.ini"
if ini_path.exists():
result["fifasetup_ini"] = read_ini(ini_path)
# anadius.cfg (persona / user info)
anadius_cfg = GAME_DIR / "anadius.cfg"
if anadius_cfg.exists():
result["anadius_cfg"] = parse_anadius_cfg(anadius_cfg)
# Settings/ save files
if docs:
result["settings_files"] = export_settings_files(docs)
# filesystemcache/
if docs:
result["filesystem_cache"] = read_filesystem_cache(docs)
output = json.dumps(result, indent=2, ensure_ascii=False)
if out_path:
out_path.write_text(output)
print(f"Written to {out_path}")
else:
print(output)
if __name__ == "__main__":
main()
+133
View File
@@ -0,0 +1,133 @@
-- squad-exporter/export_squad.lua
-- FIFA Live Editor Lua script (v2 API).
-- Run from FLE's Lua Engine while in-game to export squad/FUT-relevant
-- database table data to JSON in the FIFA 23 Live Editor directory.
--
-- Exports:
-- players table (id, name, overall, potential, position, team)
-- teams table (id, name, league, budget)
-- fut_items table (if present in memory — only when FUT is loaded)
--
-- Output: C:\FIFA 23 Live Editor\openfut_squad_export.json
local OUT_PATH = "C:\\FIFA 23 Live Editor\\openfut_squad_export.json"
-- Minimal JSON serialiser (no external deps)
local function json_val(v)
local t = type(v)
if t == "nil" then return "null"
elseif t == "boolean" then return tostring(v)
elseif t == "number" then
if v ~= v then return "null" end -- NaN
return string.format("%.6g", v)
elseif t == "string" then
return '"' .. v:gsub('\\', '\\\\'):gsub('"', '\\"'):gsub('\n','\\n') .. '"'
elseif t == "table" then
-- array check: sequential integer keys starting at 1
local n = #v
local is_arr = n > 0
if is_arr then
local parts = {}
for i=1,n do parts[i] = json_val(v[i]) end
return "[" .. table.concat(parts, ",") .. "]"
else
local parts = {}
for k,vv in pairs(v) do
parts[#parts+1] = json_val(tostring(k)) .. ":" .. json_val(vv)
end
return "{" .. table.concat(parts, ",") .. "}"
end
end
return "null"
end
local function export_table(tbl_name, fields)
local rows = GetDBTableRows(tbl_name)
if not rows or #rows == 0 then
Log("[squad-exporter] table '" .. tbl_name .. "': empty or not found")
return {}
end
Log("[squad-exporter] table '" .. tbl_name .. "': " .. #rows .. " rows")
local out = {}
for _, row in ipairs(rows) do
local rec = {}
for _, f in ipairs(fields) do
local cell = row[f]
if cell then
rec[f] = cell.value
end
end
out[#out+1] = rec
end
return out
end
-- ── Export players ────────────────────────────────────────────────────────────
local players = export_table("players", {
"playerid", "overallrating", "potential", "preferredposition1",
"height", "weight", "birthdate", "nationality", "skillmoves",
"weakfootabilitytypecode", "attackingworkrate", "defensiveworkrate",
})
-- Enrich with names (GetPlayerName is an FLE function)
for _, p in ipairs(players) do
local pid = p.playerid
if pid then
local ok, name = pcall(GetPlayerName, math.floor(pid))
if ok and name then p.name = name end
local ok2, tid = pcall(GetTeamIdFromPlayerId, math.floor(pid))
if ok2 and tid then
p.teamid = tid
local ok3, tname = pcall(GetTeamName, tid)
if ok3 then p.teamname = tname end
end
end
end
-- ── Export teams ─────────────────────────────────────────────────────────────
local teams = export_table("teams", {
"teamid", "leagueid", "teamcolor1r", "teamcolor1g", "teamcolor1b",
"stadiumid", "domesticprestige", "internationalprestige",
"transferbudget", "wagebudget",
})
for _, t in ipairs(teams) do
local ok, name = pcall(GetTeamName, math.floor(t.teamid or 0))
if ok then t.name = name end
end
-- ── Export FUT tables if available ───────────────────────────────────────────
-- These tables are only in memory when FUT mode is active.
local fut_tables = {}
local all_table_names = GetDBTablesNames()
for _, tname in ipairs(all_table_names) do
local lower = tname:lower()
if lower:find("fut") or lower:find("club") or lower:find("pack")
or lower:find("item") or lower:find("market") then
Log("[squad-exporter] found FUT-related table: " .. tname)
fut_tables[tname] = export_table(tname, GetDBTableFields(tname) or {})
end
end
-- ── Write output ─────────────────────────────────────────────────────────────
local payload = {
exported_at = os.date("!%Y-%m-%dT%H:%M:%SZ"),
is_career_mode = IsInCM(),
player_count = #players,
team_count = #teams,
players = players,
teams = teams,
fut_tables = fut_tables,
all_db_tables = all_table_names,
}
local f = io.open(OUT_PATH, "w")
if f then
f:write(json_val(payload))
f:close()
Log("[squad-exporter] Written to " .. OUT_PATH)
MessageBox("OpenFUT Squad Export", "Done! " .. #players .. " players, "
.. #teams .. " teams.\n" .. OUT_PATH)
else
Log("[squad-exporter] ERROR: could not open " .. OUT_PATH)
MessageBox("OpenFUT Squad Export", "ERROR writing to " .. OUT_PATH)
end
+217
View File
@@ -0,0 +1,217 @@
-- squad-injector/apply_lineup_write.lua
-- FIFA Live Editor Lua script (v2 API).
--
-- TEMPLATE — fill in TARGET_TABLE / TARGET_FIELDS below using whatever
-- diff_snapshots.py reports after the snapshot/Freeze-Lineup/snapshot cycle
-- described in docs/foundational-xi-injection-test.md. This script cannot
-- run correctly until those are filled in; it's the second half of the
-- foundational test, not a standalone tool.
--
-- Once filled in, this replicates the discovered write programmatically
-- (no GUI/Formation Editor needed), which is what build-order step 2 (the
-- app->game bridge) needs to drive a squad push from outside the game.
--
-- Correct EditDBTableField usage (confirmed from FLE's own Lua API docs and
-- lua/scripts/99ovr_99pot.lua): mutate the cell's .value in place, then pass
-- THE CELL ITSELF (not table/index/field-name/value as separate args) to
-- EditDBTableField:
--
-- local rows = GetDBTableRows("players")
-- local row = rows[1]
-- row["overallrating"].value = "99"
-- local ok = EditDBTableField(row["overallrating"])
local IN_PATH = "C:\\FIFA 23 Live Editor\\openfut_test_xi.json"
local OUT_PATH = "C:\\FIFA 23 Live Editor\\openfut_apply_result.json"
-- ── FILL IN AFTER RUNNING diff_snapshots.py ──────────────────────────────────
local TARGET_TABLE = nil -- e.g. "teamsheet" — set this once the diff identifies it
local TARGET_FIELDS = {
-- e.g. { name = "selected", value = function(entry) return "1" end },
-- e.g. { name = "position", value = function(entry) return entry.position end },
}
local function json_val(v)
local t = type(v)
if t == "nil" then return "null"
elseif t == "boolean" then return tostring(v)
elseif t == "number" then return string.format("%.6g", v)
elseif t == "string" then
return '"' .. v:gsub('\\', '\\\\'):gsub('"', '\\"'):gsub('\n', '\\n') .. '"'
elseif t == "table" then
local n = #v
if n > 0 then
local parts = {}
for i = 1, n do parts[i] = json_val(v[i]) end
return "[" .. table.concat(parts, ",") .. "]"
else
local parts = {}
for k, vv in pairs(v) do
parts[#parts + 1] = json_val(tostring(k)) .. ":" .. json_val(vv)
end
return "{" .. table.concat(parts, ",") .. "}"
end
end
return "null"
end
local function parse_json(s)
local i = 1
local function skip_ws() while s:sub(i, i):match("%s") do i = i + 1 end end
local parse_value
local function parse_string()
i = i + 1
local start = i
local out = {}
while s:sub(i, i) ~= '"' do
if s:sub(i, i) == "\\" then
out[#out + 1] = s:sub(start, i - 1)
local esc = s:sub(i + 1, i + 1)
out[#out + 1] = ({ n = "\n", t = "\t", ['"'] = '"', ["\\"] = "\\" })[esc] or esc
i = i + 2
start = i
else
i = i + 1
end
end
out[#out + 1] = s:sub(start, i - 1)
i = i + 1
return table.concat(out)
end
local function parse_number()
local start = i
while s:sub(i, i):match("[%-%+%.eE0-9]") do i = i + 1 end
return tonumber(s:sub(start, i - 1))
end
local function parse_array()
i = i + 1
local out = {}
skip_ws()
if s:sub(i, i) == "]" then i = i + 1; return out end
while true do
skip_ws()
out[#out + 1] = parse_value()
skip_ws()
if s:sub(i, i) == "," then i = i + 1 else break end
end
skip_ws()
i = i + 1
return out
end
local function parse_object()
i = i + 1
local out = {}
skip_ws()
if s:sub(i, i) == "}" then i = i + 1; return out end
while true do
skip_ws()
local key = parse_string()
skip_ws()
i = i + 1
skip_ws()
out[key] = parse_value()
skip_ws()
if s:sub(i, i) == "," then i = i + 1 else break end
end
skip_ws()
i = i + 1
return out
end
parse_value = function()
skip_ws()
local c = s:sub(i, i)
if c == '"' then return parse_string()
elseif c == "{" then return parse_object()
elseif c == "[" then return parse_array()
elseif c == "t" then i = i + 4; return true
elseif c == "f" then i = i + 5; return false
elseif c == "n" then i = i + 4; return nil
else return parse_number()
end
end
skip_ws()
return parse_value()
end
local function read_file(path)
local f = io.open(path, "r")
if not f then return nil end
local content = f:read("*a")
f:close()
return content
end
local function write_file(path, content)
local f = io.open(path, "w")
if not f then return false end
f:write(content)
f:close()
return true
end
if not TARGET_TABLE or #TARGET_FIELDS == 0 then
MessageBox("OpenFUT Apply", "TARGET_TABLE / TARGET_FIELDS are not filled in yet. " ..
"Run the snapshot/Freeze-Lineup/snapshot/diff cycle first (see docs/foundational-xi-injection-test.md).")
return
end
local raw = read_file(IN_PATH)
if not raw then
MessageBox("OpenFUT Apply", "ERROR: could not read " .. IN_PATH)
return
end
local ok_parse, request = pcall(parse_json, raw)
if not ok_parse or not request or not request.xi then
MessageBox("OpenFUT Apply", "ERROR: could not parse " .. IN_PATH)
return
end
local rows = GetDBTableRows(TARGET_TABLE) or {}
local result = {
attempted_at = os.date("!%Y-%m-%dT%H:%M:%SZ"),
is_career_mode = IsInCM(),
target_table = TARGET_TABLE,
rows_written = {},
errors = {},
}
for _, entry in ipairs(request.xi) do
local pid = entry.player_id
local matched_row = nil
for _, row in ipairs(rows) do
local cell = row["playerid"] or row["player_id"]
if cell and math.floor(cell.value) == math.floor(pid) then
matched_row = row
break
end
end
if not matched_row then
result.errors[#result.errors + 1] = "player_id " .. tostring(pid) .. " not found in " .. TARGET_TABLE
else
local row_result = { player_id = pid, writes = {} }
for _, fdef in ipairs(TARGET_FIELDS) do
local cell = matched_row[fdef.name]
if cell then
cell.value = fdef.value(entry)
local ok = EditDBTableField(cell)
row_result.writes[#row_result.writes + 1] = { field = fdef.name, value = cell.value, ok = ok }
else
row_result.writes[#row_result.writes + 1] = { field = fdef.name, ok = false, err = "field not found on row" }
end
end
result.rows_written[#result.rows_written + 1] = row_result
end
end
write_file(OUT_PATH, json_val(result))
Log("[apply] done, see " .. OUT_PATH)
MessageBox("OpenFUT Apply", "Rows written: " .. #result.rows_written .. " / " .. #request.xi ..
"\nErrors: " .. #result.errors .. "\n\n" .. OUT_PATH)
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Diff two openfut_snapshot_*.json files from snapshot_lineup_tables.lua.
Usage: diff_snapshots.py before.json after.json
Reports, per table, which rows changed and which fields differed —
this is how the real DB write behind FLE's "Freeze Lineup" GUI feature
gets discovered (see docs/foundational-xi-injection-test.md).
"""
import json
import sys
def row_key(row):
for candidate in ("playerid", "player_id", "id", "rowid"):
if candidate in row:
return row[candidate]
return json.dumps(row, sort_keys=True)
def diff_table(name, before, after):
before_rows = {row_key(r): r for r in before.get("rows", [])}
after_rows = {row_key(r): r for r in after.get("rows", [])}
changes = []
for key, after_row in after_rows.items():
before_row = before_rows.get(key)
if before_row is None:
changes.append({"row": key, "type": "added", "row_data": after_row})
continue
field_diffs = {}
for field in set(before_row) | set(after_row):
bval = before_row.get(field)
aval = after_row.get(field)
if bval != aval:
field_diffs[field] = {"before": bval, "after": aval}
if field_diffs:
changes.append({"row": key, "type": "modified", "fields": field_diffs})
for key in before_rows:
if key not in after_rows:
changes.append({"row": key, "type": "removed"})
if changes:
print(f"\n=== {name}: {len(changes)} changed row(s) ===")
for c in changes:
print(json.dumps(c, indent=2))
def main():
if len(sys.argv) != 3:
print(__doc__)
sys.exit(1)
with open(sys.argv[1]) as f:
before = json.load(f)
with open(sys.argv[2]) as f:
after = json.load(f)
table_names = set(before.get("tables", {})) | set(after.get("tables", {}))
any_changes = False
for name in sorted(table_names):
b = before.get("tables", {}).get(name, {"rows": []})
a = after.get("tables", {}).get(name, {"rows": []})
before_len = len(b.get("rows", []))
after_len = len(a.get("rows", []))
if before_len != after_len:
print(f"{name}: row count changed {before_len} -> {after_len}")
any_changes = True
diff_table(name, b, a)
if not any_changes:
print("(row-count-only check found no differences; per-field diffs above are authoritative)")
if __name__ == "__main__":
main()
@@ -0,0 +1,111 @@
-- squad-injector/snapshot_lineup_tables.lua
-- FIFA Live Editor Lua script (v2 API).
--
-- Dumps every DB table whose name looks lineup/formation/squad-related,
-- full rows + fields, to a timestamped JSON file. Run it once BEFORE using
-- FLE's GUI "Freeze Lineup" feature (Formation Editor -> arrange players on
-- pitch -> tick "Freeze Lineup" -> Data -> Save) and once AFTER, then diff
-- the two snapshots with diff_snapshots.py to find which table/field(s) the
-- GUI feature actually writes.
--
-- This exists because "Freeze Lineup" is a confirmed, documented mechanism
-- for forcing a starting XI (FLE wiki: Formation-Editor.md) but it's GUI-only
-- — its underlying DB write is not documented anywhere. Diffing before/after
-- snapshots is how we find it, the same way export_squad.lua's exhaustive
-- table dump was the oracle for Track C.
--
-- Output: C:\FIFA 23 Live Editor\openfut_snapshot_<unix-ish timestamp>.json
local OUT_DIR = "C:\\FIFA 23 Live Editor\\"
-- Keyword filter, broader than squad-exporter's FUT-table filter — Freeze
-- Lineup is a formation/tactics feature so the write could land in any of
-- these families. "players" is included because position fields on the
-- player row itself may also change.
local KEYWORDS = { "squad", "lineup", "formation", "tactic", "teamsheet", "selection", "players", "teams" }
local function json_val(v)
local t = type(v)
if t == "nil" then return "null"
elseif t == "boolean" then return tostring(v)
elseif t == "number" then
if v ~= v then return "null" end
return string.format("%.6g", v)
elseif t == "string" then
return '"' .. v:gsub('\\', '\\\\'):gsub('"', '\\"'):gsub('\n', '\\n') .. '"'
elseif t == "table" then
local n = #v
if n > 0 then
local parts = {}
for i = 1, n do parts[i] = json_val(v[i]) end
return "[" .. table.concat(parts, ",") .. "]"
else
local parts = {}
for k, vv in pairs(v) do
parts[#parts + 1] = json_val(tostring(k)) .. ":" .. json_val(vv)
end
return "{" .. table.concat(parts, ",") .. "}"
end
end
return "null"
end
local function export_table(tbl_name)
local fields = GetDBTableFields(tbl_name) or {}
local field_names = {}
for _, f in ipairs(fields) do field_names[#field_names + 1] = f.name end
local rows = GetDBTableRows(tbl_name)
if not rows or #rows == 0 then
Log("[snapshot] table '" .. tbl_name .. "': empty or not found")
return { fields = field_names, rows = {} }
end
local out_rows = {}
for _, row in ipairs(rows) do
local rec = {}
for _, fname in ipairs(field_names) do
local cell = row[fname]
if cell then rec[fname] = cell.value end
end
out_rows[#out_rows + 1] = rec
end
Log("[snapshot] table '" .. tbl_name .. "': " .. #out_rows .. " rows, " .. #field_names .. " fields")
return { fields = field_names, rows = out_rows }
end
local all_table_names = GetDBTablesNames()
local matched = {}
for _, tname in ipairs(all_table_names) do
local lower = tname:lower()
for _, kw in ipairs(KEYWORDS) do
if lower:find(kw) then
matched[#matched + 1] = tname
break
end
end
end
local snapshot = {
captured_at = os.date("!%Y-%m-%dT%H:%M:%SZ"),
is_career_mode = IsInCM(),
matched_table_names = matched,
tables = {},
}
for _, tname in ipairs(matched) do
snapshot.tables[tname] = export_table(tname)
end
local out_path = OUT_DIR .. "openfut_snapshot_" .. os.date("!%Y%m%dT%H%M%SZ") .. ".json"
local f = io.open(out_path, "w")
if f then
f:write(json_val(snapshot))
f:close()
Log("[snapshot] written to " .. out_path)
MessageBox("OpenFUT Snapshot", "Snapshotted " .. #matched .. " tables.\n" .. out_path ..
"\n\nTake one snapshot BEFORE Freeze Lineup, one AFTER, then run diff_snapshots.py on the two files.")
else
Log("[snapshot] ERROR: could not open " .. out_path)
MessageBox("OpenFUT Snapshot", "ERROR writing to " .. out_path)
end