Commit Graph

242 Commits

Author SHA1 Message Date
OpenFUT Agent f3aebafcc7 chore(core): serialize WAL establishment (75b1830) 2026-08-13 20:03:40 +00:00
OpenFUT Agent 1df0bc4f00 test(fifa17): make economy integration harness deterministic
Serialize Core access with a single pooled connection (the harness drives Core
sequentially via the blocking client) to avoid a WAL-mode-establishment race
across connections warming up on a brand-new DB file, and raise the readiness
ceiling for heavy parallel test-binary load. 10/10 deterministic. (Fixed
alongside a real Core robustness fix: per-connection pragmas + busy_timeout,
core 0360135.)
2026-08-13 19:57:53 +00:00
OpenFUT Agent 7d5d0cff06 chore(core): sqlite busy_timeout + per-connection pragmas (bcc4f51 -> 0360135) 2026-08-13 19:55:13 +00:00
OpenFUT Agent 8d752cb0e4 test(fifa17): real host<->Core economy integration harness
Spawn Core (axum) on an ephemeral loopback port backed by a disposable temp-file
SQLite, seed a fifa17 profile via the real Core HTTP API, then drive the HOST's
REAL transport (HttpCoreClient: CoreEconomy) + handlers against it — no fakes:
credits reads Core balance; match-reward writer credits via Core grant_reward;
purchasegroup full-gen renders the owned pack from a Core entitlement (no
sentinel); userMassInfo overlay derives coins from the same Core state
(credits==massinfo==Core invariant). Restart phase reboots Core from the same
on-disk DB and proves coins + entitlements persist. Temp dir + 127.0.0.1:0 only;
no prod DB/ports/containers/.105. dev-deps: openfut-core, tokio, axum.
2026-08-13 19:50:42 +00:00
OpenFUT Agent 96e80ab293 feat(import-fifa17): seed unopenedPackIds as Core entitlements
Carry unopenedPackIds through the importer: Profile model -> Report ->
ApplyPlan. GenericImportRequest now emits entitlements[] (one definition_id
per unopened pack instance, order+duplicates preserved), which Core's import
seeds as unconsumed packs rows in the same transaction. Closes the economy
import gap so a migrated profile's unopened packs become Core entitlements
(feeding purchasegroup/credits/userMassInfo). Idempotency unchanged (import
fingerprint). +1 test; 26 pass, clippy -D warnings clean.
2026-08-13 19:38:04 +00:00
OpenFUT Agent 49c5185ae3 docs(utas-host): update economy cutover progress (readers + match writer landed)
Core API + purchase_items + entitlement import + host reader handlers
(credits/purchasegroup/userMassInfo) + match reward writer landed and tested;
classifier still unflipped (barrier pending BUY/pack-open/quick-sell item shaping,
market listing state, differential/concurrency/restart).
2026-08-13 19:32:45 +00:00
OpenFUT Agent 181bd94341 feat(fifa17): Rust purchasegroup, userMassInfo economy, match reward handlers
All Core-backed, fail-closed (503, never Python), NOT yet classifier-routed
(coherent barrier pending full cluster + Core seed):
- handle_purchasegroup: full Rust body from Core entitlements + StoreMode via
  the oracle-fixture-tested build_purchasegroup (no Python body dependency).
- overlay_massinfo_economy: set userInfo.currencies coins + unopenedPacks
  recoveredPacks from Core, preserving all other fields.
- handle_match_end + build_match_reward_body: derive outcome from endReason,
  credit via Core grant_reward, oracle-shaped destroy_match_body.
Invariant test: credits == userMassInfo == purchasegroup all read one Core state.
7 new host tests (+ FakeEconomy write methods honor fail flag).
2026-08-13 19:31:23 +00:00
OpenFUT Agent 09675a9f7f feat(fifa17): economy policy mappers (match reward, pack price)
Pure FIFA17 policy: match_result_coins/match_reward_total (oracle MATCH_COINS
won 400/draw 200/loss 100 + participation 0), result_from_end_reason (endReason
enum -> outcome, draw default), pack_price (catalogue buy-now, None for
unknown/owned-only). 3 unit tests.
2026-08-13 19:31:23 +00:00
OpenFUT Agent 56c364e4c9 chore(core): purchase_items + entitlement import (d32dc6e -> bcc4f51) 2026-08-13 19:27:06 +00:00
OpenFUT Agent 3021a4e761 docs(utas-host): record economy cutover progress in route-authority gate
Core economy HTTP API + host CoreEconomy client (fail-closed) + credits reader
landed; classifier not yet flipped (coherent barrier pending full cluster + Core
seed).
2026-08-13 19:14:39 +00:00
OpenFUT Agent d240a61157 feat(fifa17): host Core economy client + credits reader vertical
Add CoreEconomy transport (trait + HttpCoreClient impl over Core /economy/*):
balance, entitlements, purchase_entitlement, redeem_entitlement, sell_item,
grant_reward, purchase_item. Fail-closed by contract: any transport/status/parse
error surfaces a controlled error and NEVER falls back to Python (a fallback
would be a second writer).

Add the credits reader vertical: build_credits_body (byte-shape-identical to the
Python oracle: credits + currencies[].funds/finalFunds + optional
unopenedPacks.recoveredPacks) and handle_credits (coins = Core balance,
recoveredPacks = Core entitlement count; 503 fail-closed on Core error). Not yet
classifier-routed: the coins cluster flips as one coherent barrier once every
writer+reader moves together and Core is seeded. FakeEconomy double + 3 tests
(oracle shape, Core-backed read, fail-closed).
2026-08-13 19:14:13 +00:00
OpenFUT Agent d7c5307045 chore(core): expose economy HTTP API (c8269d0 -> d32dc6e)
Advance openfut-core gitlink to d32dc6e: generic /economy/* HTTP routes over
services::economy, server-side club resolution (game-scoped active profile, no
client-supplied club id), + list_unopened_entitlements reader. This is the
transport the FIFA17 economy cutover binds to. Core matrix 43 lib + 115
integration green; clippy clean; boundary audit clean.
2026-08-13 19:11:04 +00:00
OpenFUT Agent 838d76f95e docs(utas-host): add economy route-authority cutover gate
Machine-auditable ownership table for every FIFA17 UTAS route touching the
economy cluster (coins/inventory/entitlements), plus the writer->Core-primitive
map and the single-writer rule. Grounded in the Python economy writer audit:
4 coin-mutation routes (match reward, pack BUY, quick-sell, market buy-now),
synthetic-seller market (no sale-credit/expiry/fee), dead grant_coins/
grant_unopened_pack, no points writer. This is the deployment gate: no proxied
Python route may touch Core-owned state before R1.
2026-08-13 18:59:46 +00:00
OpenFUT Agent 9791eee67b chore(core): add generic purchase_item economy primitive (ee2caa0 -> c8269d0)
Advance openfut-core gitlink to c8269d0, which adds services::economy::purchase_item
(atomic debit + mint) — the generic Core primitive the FIFA17 synthetic-seller
transfer market needs. Evidence: the Python economy audit proved market buy-now
mints a new item with no real counterparty, so debit+mint (not two-party transfer)
is the correct generic model. Core matrix + clippy green.
2026-08-13 18:58:49 +00:00
OpenFUT Agent 5d119f5555 chore(core): reconcile Core to validated trunk + generic economy
Advance the openfut-core gitlink 3084a46 -> ee2caa0. This does two things:

1. Reconciliation: moves the canonical Core lineage onto the committed,
   validated migration trunk (66c88fb: game-scoped opaque extension,
   inventory service, squad-ext routes, content-pack loader, generic
   transactional profile-import service). The divergent local Core refactor
   that was dirtying the eab522a checkout is preserved verbatim on branch
   wip/core-local-development (f70cf44) for separate reconciliation; nothing
   is lost.

2. Economy foundation: ee2caa0 adds services::economy, a generic atomic
   profile-economy authority (currency/inventory/entitlements over the
   existing durable tables, single-transaction compound ops, fail-closed).
   The FIFA17 adapter economy engine sits on top of these primitives.

Core builds green; full matrix 41 lib + 111 integration + 16 = all pass;
clippy clean.
2026-08-13 18:45:21 +00:00
funman300 46e5f612c8 feat(fifa17): add authoritative economy engine + fut_profile importer
Adds openfut-adapter-fifa17 fut::economy — the single-writer FIFA 17 economy engine
the eventual cluster cutover needs: coins + unopened-pack entitlements + owned
inventory + stable item ids, with all-or-nothing transactional mutations faithfully
ported from the Python oracle's fut_store.Store primitives.

Atomic ops: debit (fail-closed), credit, grant_pack/consume_pack (consume-once),
allocate_item_id (unique/monotonic), add_item, and composed transactions buy_pack,
open_pack, quick_sell, market_buy_now, grant_reward. Fail-closed everywhere; the
65534 sentinel can never be bought/granted/opened (defense at the grant primitive,
mirroring grant_unopened_pack rejecting non-catalogue ids). from_fut_profile importer
round-trips coins/unopenedPackIds/items/nextItemId and floors nextItemId past the
highest existing id so re-import cannot mint a duplicate. 10 unit tests (atomicity,
sentinel safety, consume-once, quick-sell, item-id uniqueness, import round-trip).

NOT wired (R3): the live coin balance is one indivisible writer set spanning Store
BUY, pack-open, quick-sell, match rewards AND the transfer market, all in
fut_profile.json; and the generic home (OpenFUT Core) is a preserved-dirty/frozen
submodule. So a safe single-writer cutover cannot be wired yet — this engine +
importer is the coherent prerequisite. No dual-write introduced. No deployment.
2026-08-13 18:26:09 +00:00
funman300 41494bd18f feat(fifa17): port Store pack catalog + purchasegroup wire model (oracle parity)
Adds openfut-adapter-fifa17 fut::store_catalog — a pure, faithful Rust port of the
Python oracle's PACK_CATALOG + _pack_body + store_catalog assembly at production
flag defaults (FUT_STORE_DISPLAYGROUP=1, GROUPID=0, PRICE_PROBE=0):

- PackDef + PACK_CATALOG (ids 1/5/6/7/70; economy numbers are OpenFUT PLACEHOLDER,
  wire shape is oracle-verified; 65534 deliberately absent).
- pack_body() (_pack_body port), sentinel_body() (id-65534 compatibility shim),
  build_purchasegroup(unopened_ids, StoreMode) mirroring store_catalog(3627).
- Differential parity: fixtures generated from the Python oracle
  (tests/fixtures/purchasegroup_{zero_sentinel,zero_clean,pack70}.json); Rust output
  matches semantically (6 tests). Adapter 140 tests, host 24, fmt/clippy clean,
  Python A-R oracle green.

PURE wire shaping — NOT wired into the live host. Serving purchasegroup from Rust
requires an authoritative Rust owner of unopenedPackIds, which is blocked on the
economy-authority prerequisite (R3): coins are one shared balance written by many
Python-oracle routes (BUY spend, quick-sell credit, SBC/match/objective rewards)
persisted to fut_profile.json, so no single coin-touching route can move without a
whole-cluster migration. No dual-write introduced; no production deployment.
2026-08-13 18:16:47 +00:00
funman300 40ebf7c1e7 feat(fifa17): own UTAS auth/capability/purchasegroup session vertical in Rust
openfut-utas-host now classifies and owns three routes, wiring the landed
adapter store_session state machine while keeping the Store economy Python's:

- POST /ut/auth: proxy to Python (which mints X-UT-SID, adopts persona, refreshes
  save), OBSERVE the returned sid, and open a Rust session bound to peer IP +
  configured persona. Account/economy authority stays Python.
- POST /openfut/fifa17/capability: Rust-owned, no proxy — validate + register into
  SessionStore (bound/pending/ignored-late); fail-closed 400 on unsupported.
- GET .../store/purchasegroup: proxy to Python for the authoritative economy body,
  then overlay ONLY the empty-My-Packs topology from the frozen session mode —
  strip the 65534 sentinel for a verified clean-v1 SID, keep it otherwise. Rust
  never writes economy state.

Session state (Arc<Mutex<SessionStore>> + monotonic clock) lives on Server; new()
and from_config() initialise it (signatures unchanged). handle() gains a peer-IP
variant (handle_with_ip) threaded from handle_conn. Strict never-both routing is
preserved. Pure helpers (observe_sid, parse_capability_request, overlay_empty_mypacks)
+ classifier are unit-tested; adapter+host tests + Python A-R oracle all pass.

STOP-GATE: Store BUY / coins / unopenedPackIds NOT migrated — Rust has no
authoritative FIFA17 economy-mutation path (Python fut_profile.json is the source;
Core's economy is separate/unwired), so moving BUY would split store authority.
That cluster migration is the remaining R2 gap. No production deployment.
2026-08-13 17:38:59 +00:00
funman300 c7609252d2 feat(fifa17): add Rust FUT session/capability state machine (empty My Packs)
Ports the novel per-session empty-My-Packs capability negotiation — proven live
on staging and currently Python-only (fifa17-recon/tools/utas_server.py) — into
the production Rust FIFA17 adapter as a pure, dependency-free state machine
(openfut-adapter-fifa17 fut::store_session).

It owns: per-login X-UT-SID session table, single-use (ip,persona) launcher
capability hand-off (pending), capability binding (bound/pending/ignored-late),
the once-per-session clean-v1 vs sentinel freeze, TTL reaping, and fail-closed
rules (unknown/expired/ambiguous/late/cross-session/sid-ip-mismatch -> sentinel).
The clock and SID entropy are injected so it is fully unit-testable.

The full Python capability-negotiation matrix A-R is ported as Rust unit tests
(21 pass). Python remains the behavioural oracle. The delicate _pack_body UTAS
wire shaping, /ut/auth persona-adoption, /store/purchasegroup catalogue assembly
and the store BUY path are deliberately NOT ported here (documented gap); wiring
the three routes into openfut-utas-host without splitting store authority is the
remaining bounded slice toward full Rust authority. No production deployment.
2026-08-13 16:52:13 +00:00
funman300 4cf388dd3b chore: reconcile openfut-launcher submodule
Point the launcher gitlink at the reconciled merge ca7ce26
(integration/fifa17-launcher-capability-sbc), which retains BOTH launcher lineages:
  - 13339c1  FIFA 17 verified patched-client capability reporting
  - 958ff245 openfut-hook SBC request tracing / RE instrumentation

The previously-uncommitted openfut-hook WIP that blocked this move is preserved on the
submodule branch wip/openfut-hook-local (commit 4e44a37) + /tmp/openfut-hook-wip-preserved.patch
(sha256 8e65de2c…) + the untracked server.rs copy. Gitlink-only change; no other superproject
dirt staged. Backend/production unchanged.
2026-08-13 15:11:18 +00:00
funman300 00d85aa6e4 docs(fifa17): finalize capability deployment candidate
Record the overnight launcher-lineage reconciliation (merge ca7ce26 retaining both
feat/launcher-arming 13339c1 and feat/sbc-hook-tracing 958ff24; only src/process.rs
conflict, resolved keep-deleted), the deferred superproject gitlink bump (blocked by
uncommitted openfut-hook WIP overlapping the merged hook content), the validated
deployment-candidate commit tuple + local build artifacts, and the controlled A/B/C
deployment sequence. Production stays P2 active-sentinel until the A/B passes.
2026-08-13 05:18:14 +00:00
funman300 d9e80a774a test(fifa17): add explicit per-SID topology-freeze regression
Case R makes the F3 session-topology invariant explicit alongside the A-Q matrix:
for a single X-UT-SID the frozen empty-My-Packs mode never flips in either
direction (Sentinel stays Sentinel even if a capability later appears; Clean stays
Clean even if the capability is wiped), while a fresh SID from the same IP decides
independently. Complements F/G/K.
2026-08-13 05:18:13 +00:00
funman300 a82407c686 docs(fifa17): harden patched-client session binding
Record the per-IP -> per-session correction: why source-IP-only was unsafe (two
FIFA processes share an IP), the authoritative per-login X-UT-SID key with IP and
persona as auxiliary, the Capability/StoreMode state machine, the single-use
short-TTL launcher->session pending hand-off, activity-based session cleanup, and
the documented fail-closed residual for genuinely simultaneous same-(ip,persona)
logins. Design history is retained; the per-IP prototype is marked superseded.
2026-08-13 04:39:53 +00:00
funman300 805d754dc8 fix(fifa17): isolate patched-client capability per session
Harden the empty-My-Packs capability binding so a verified FIFA process can never
enable clean/no-sentinel Store topology for another unverified process that merely
shares its source IP. The prototype keyed the decision by source IP alone; two FIFA
processes (concurrent, or a relaunch) share an IP, so an unpatched process could
inherit a patched one's clean-v1 mode and crash. Source IP is now auxiliary only.

- Authoritative key = the per-login UTAS session id (X-UT-SID). /ut/auth now mints
  a fresh unique SID per login (was a shared constant) and opens a session record
  keyed by that SID; the client echoes it on every later call incl.
  /store/purchasegroup (live-confirmed). The legacy constant is still accepted by
  the retired security-question gate only, never to grant clean-v1.
- Session state: _FIFA17_SESSIONS[sid] = {ip, persona, resolver, mode, created,
  last_seen}. Store mode freezes at the first /store/purchasegroup of the session
  and is immutable thereafter. Fail-closed: unknown SID, or a SID presented from a
  different source IP than it was opened on, resolves to the sentinel.
- Launcher capability (out-of-band; cannot know the SID) is matched by (ip, persona)
  as a SINGLE-USE, short-TTL pending, bound to exactly one session at whichever comes
  first: its login (pending predates auth), the registration (session already live),
  or its first store request. Ambiguous same-(ip,persona) concurrent registration is
  ignored-late -> both sentinel (never a wrong clean).
- Session cleanup: activity-based TTL sweep (sessions 3600s idle, pendings 120s);
  reaping only removes expired entries and never affects another live session.
- account_sync now clears only stale pending for the machine (pre-launch hygiene);
  it no longer resets a per-IP mode (there is no per-IP mode any more).

Backend-only: the launcher registration payload (already carries personaId) is
unchanged. Additive; P2 sentinel remains the else-branch and the default.

Tests: matrix A-Q incl. same-IP concurrent (K), same-IP+persona relaunch (L),
same-IP failed-patch (M), late-registration-vs-frozen-sessions (N), TTL expiry (O),
duplicate/idempotent registration (P), and register-before-login pending (Q).
2026-08-13 04:39:53 +00:00
funman300 d4c3811665 docs(fifa17): document patched-client store negotiation
Design + cross-component contract for verified patched-client capability
negotiation: architecture inventory, transport choice (autopatch stdout ->
launcher, sibling /openfut/fifa17/capability endpoint), the versioned capability
and its VERIFIED semantics, autopatch verification states, launcher per-process
state, source-IP binding, the session-stable freeze point, the additive store
switch, the trust model (local preservation, not attestation), the fail-closed
matrix, and P2 retention.
2026-08-13 04:03:37 +00:00
funman300 b25761ea31 feat(fifa17): negotiate clean empty My Packs mode
Backend side of the handshake: suppress the synthetic 65534 My-Packs sentinel
ONLY for a session whose client has registered a verified resolver-guard
capability. Additive; the P2 active-sentinel path is retained as the else-branch
and the universal default. Fail-closed everywhere.

- Per-client state keyed by source IP (client_address[0]; the only per-connection
  discriminator in this single-account, stateless backend): _FIFA17_STORE[ip] =
  {resolver, mode}; mode in {None, "sentinel", "clean-v1"}, guarded by a lock.
- New POST /openfut/fifa17/capability endpoint: accepts only
  {"capability":"empty_mypacks_resolver","version":1,...}; unknown capability or
  version => 400 and records nothing (=> sentinel).
- account_sync (the launcher's required per-launch call) resets the per-ip record
  => a new FIFA process starts unfrozen with no inherited capability.
- Store topology is frozen at the FIRST /store/purchasegroup per session:
  clean-v1 iff a v1 capability is registered, else sentinel; immutable thereafter
  (late capability logged + ignored this session; a disappeared capability does
  not un-freeze a clean session). This enforces the SESSION-STABLE invariant.
- store_catalog zero-owned-packs branch: clean-v1 emits NO mypacks group (the
  client guard routes category -1 to Browse); every other case emits the existing
  active 65534 sentinel verbatim. PACK_CATALOG / pack 70 / normal packs / profile
  untouched. FIFA-17 only; not lifted into game-independent Core.
- Tests: full matrix A-J incl. concurrency isolation (two IPs, no global leak) and
  no cross-process capability leak.

Design: docs/plans/FIFA17_PATCHED_CLIENT_CAPABILITY.md.
2026-08-13 04:03:37 +00:00
funman300 1c396dd562 feat(fifa17): report verified client patch capability
autopatch side of the verified patched-client capability handshake: prove, at
runtime, that the empty-My-Packs resolver guard is active for a specific FIFA
process, and advertise it once on stdout for the launcher to relay.

- Add a per-pid guard verification state derived by a pure, testable
  guard_state_after(cur_before, orig, patch, wrote_ok, cur_after) returning one
  of VERIFIED / UNSUPPORTED_BUILD / WRITE_FAILED / VERIFY_FAILED (NOT_ATTEMPTED
  is the pre-evaluation constant). VERIFIED means the live bytes at RVA 0x14858
  are 7f 0f (JG) after enforcement (from an applied 75 0f->7f 0f, or already
  patched). The existing fail-closed byte guard (guarded_action / STORE_PATCHES_
  GUARDED) is unchanged — this only observes the outcome.
- Emit exactly once per FIFA pid: on VERIFIED,
    [store-guard] verified capability fifa17.empty_mypacks_resolver=1 fifa_pid=<pid>
  otherwise a non-advertising
    [store-guard] guard status=<STATE> fifa_pid=<pid> (no capability advertised)
- Capability constants: EMPTY_MYPACKS_RESOLVER_VERSION=1, fully-qualified name
  "fifa17.empty_mypacks_resolver".
- Tests: 5 guard-state cases + capability-constant assertions (standalone-runnable).

The capability = "the guard was verified in THIS FIFA process", never merely
"the code is present". Design: docs/plans/FIFA17_PATCHED_CLIENT_CAPABILITY.md.
2026-08-13 04:03:37 +00:00
funman300 fc29c2eb9b docs(fifa17): record no-sentinel client resolver proof
Land the client-side empty-My-Packs resolver evidence and the session-stability
invariant established by the F3/R1 experiments.

- PART III (F3, CONFOUNDED CRASH): a mid-process sentinel -> no-sentinel flip
  left a stale POSITIVE My-Packs ordinal that still took the resolve branch and
  crashed at 0x180014882. Preserved verbatim (not a guard failure).
- PART IV (R1, SUCCESS): backend set no-sentinel first, then a FRESH FIFA
  process; genuine purchasegroup response ids [1,5,6,7] is byte-identical to the
  F3 capture, so client process lifetime is the only changed variable. Store
  opens on Browse Packs, no crash, no dialog. Guard PROVEN on the tested build.
- New INVARIANT: empty-My-Packs capability MUST be session-stable -- the server
  must not switch a running client between sentinel-present and sentinel-absent
  for the My Packs group within one FIFA process, because the client caches the
  group ordinal and a stale positive ordinal still crashes the resolver.
- Both no-sentinel captures kept: client_guard (F3) and freshretest (R1).

Backend P2 active-sentinel (65534) remains production default; no capability
handshake is implemented yet.
2026-08-13 03:13:40 +00:00
funman300 b0d5e04bb9 fix(fifa17): guard missing store category resolution
Port the PROVEN empty-"My Packs" resolver crash-guard into the canonical
autopatch.py /proc-mem patcher. When no `mypacks` purchase group exists, a
fresh FIFA 17 client resolves category id -1; CardsDLL FUN_1800147f0 at RVA
0x14858 (`JNZ 0x14869`, bytes 75 0f) treats every non-zero category as
resolvable, calls FUN_180014420, gets NULL, and dereferences [NULL+0x48] at
0x180014882 (0xC0000005). Rewriting JNZ->JG (7f 0f) preserves positive-category
resolution (EDI>0) while routing zero/negative categories to the existing
Browse/list-all path -> no NULL lookup, no crash, Store opens on Browse Packs.

- STORE_PATCHES_GUARDED table pins RVA 0x180014858 orig 75 0f -> patch 7f 0f.
- Applied every tick, fail-closed via guarded_action(): apply only when the
  live bytes are the known original; no-op when already patched; SKIP+log an
  unrecognised CardsDLL build (never blindly overwritten).
- Runtime watch loop moved under `if __name__ == "__main__"` so the module
  imports cleanly for unit testing; script behavior is unchanged. Existing
  ProtoSSL cert-gate and STORE_PATCHES enforcement are byte-identical (indent
  only).
- test_autopatch_guard.py: pure test covering PATCH/NOOP/SKIP and pinning the
  exact guarded RVA/bytes.

Proven on the tested build (CardsDLL 4706a881...) by a clean fresh-process
no-sentinel A/B (R1). Dormant while the backend active-sentinel is present.
See docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md PART IV.
2026-08-13 03:13:39 +00:00
funman300 6746c75302 docs(fifa17): RE-backed native client-fix design for empty My Packs
Investigation + design only (no client/backend/binary changes, no live
Store experiment). Reconfirmed CardsDLL_Win64_retail.dll (4706a881..,
unpacked) against a freshly rebuilt Ghidra project on .105; FIFA17.exe
(29c31cef..) is Denuvo-packed so the Scaleform decision is unreadable.

PART II added to docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md:
- Native category path traced: FUN_18007dab0 (store render, RVA 0x7dab0)
  reads screen+0x290; My Packs funnels through FUN_1800147f0 (0x147f0) ->
  FUN_180014420 (0x14420, NULL on ordinal miss) -> crash MOV [RDX+0x8] at
  0x14882 ([NULL+0x48]), matching the Exp-B minidump. Tab->ordinal map
  FUN_180014580 (0=mypacks..5=special); category 0 = list-all (Browse).
- Unopened-pack count is a data-manager singleton (vtbl[0x4d8] get /
  [0x4e0] set), reachable from the store resolver.
- Vehicle: existing openfut-hook -> version.dll proxy (already deployed);
  reuse ssl_patch signature-scan + connect_hook inline detour. No new loader.
- Preferred strategy A: entry-hook FUN_18007dab0; when the requested
  category is My Packs and unopened count==0, force screen+0x290=0 (Browse).
  Removes crash + fake 65534 tile + dialog + nav gate; count>0 untouched.
- Ranked B (resolver NULL fallback, higher risk) and C (null-guard, crash-only).
- Build guard: module gate + SHA/PE + signature scan; unknown build -> no
  patch, backend sentinel remains fallback.
- First experiment design (needs a later, separately-authorized backend
  empty-no-sentinel test mode) + client rollback (config flag / dll swap).
- Keep backend 65534 sentinel deployed until strategy A is verified.

Describes the FIFA 17 client/data model only; not OpenFUT Core assumptions.
2026-08-13 01:48:58 +00:00
funman300 b2697b13dc docs(fifa17): establish verified card taxonomy
Single source of truth for FIFA 17 FUT card families, reconciled against
the authoritative shipped fcc_*.json + staff tables (verified byte-identical
between .105 and this repo, 36/36 sha256).

- docs/CARD_TAXONOMY.md: family -> table/rowcount/subtype/carddbid/cardassetid,
  with OBSERVED/INFERRED/HYPOTHESIS/UNKNOWN labels. Corrects four superseded
  claims (chem styles are 250-273 not 91-136; 6300/6400xxx are kits not badges;
  5004xxx misc and 8010xxx league logos exist). Manager-league precision kept
  distinct: shipped table 300-340 (41 rows) vs client enum 300-341 (341 defined,
  unshipped). Club-item wire subtype->family mapping preserved as UNKNOWN.
- docs/evidence/fifa17-recon/table-hashes.sha256: 36-file provenance manifest
  (31 fcc_*.json + 5 staff tables), combined hash 10f239ad...

Describes the FIFA 17 data/client model only; not OpenFUT Core assumptions.
2026-08-13 01:31:22 +00:00
funman300 e8ee6c34e7 docs(fifa17): record empty My Packs client contract
Full investigation record for bug 6c: baseline + Experiments A/B/C', Candidate F (contradicted), the explicit active-placeholder selection test, the minidump-confirmed CardsDLL crash, and the P2 decision. Marks ROOT CAUSE ESTABLISHED and documents the known UX limitations and the client-side follow-up.

Files: docs/evidence/STORE_TILE_6C.md, docs/evidence/FIFA17_EMPTY_MYPACKS_CLIENT_CONTRACT.md, the four genuine /store/purchasegroup captures (baseline, mypacks70, empty_no_sentinel, active_placeholder), and docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md (client-side design/research).
2026-08-13 01:08:47 +00:00
funman300 f42279f869 fix(fifa17): keep empty My Packs group client-safe
When the account owns zero unopened packs, store_catalog() emits a synthetic `mypacks` group placeholder (id 65534, absent from PACK_CATALOG). Change its state from "inactive" to "active".

Root cause (bug 6c): FIFA 17's Store/Scaleform path resolves the `mypacks` category even with zero unopened packs (category chosen client-side via the movie's CATEGORY_ID -> screen+0x290; no server field gates it). CardsDLL FUN_1800147f0 then dereferences the resolved group with no null guard, so an absent group crashes the client (CardsDLL+0x14882, [NULL+0x48], minidump-confirmed). An inactive placeholder avoids the crash but makes the Store report the pack unavailable on entry and bounce to the Hub; an active placeholder lets the Store open normally.

65534 stays economy-safe: pack_by_id() returns None, so store_buy()/purchased_items() cannot open it or grant items/coins, and grant_unopened_pack() rejects it. Explicit selection is rejected client-side ("This pack is no longer available") and sends no backend request. This is a FIFA-17 client-compatibility shim (P2), not an EA-authentic representation, confined to the FIFA-17 backend (not OpenFUT Core). A clean zero-pack UX needs a client-side fix (docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md).

Adds regression tests (test_empty_mypacks.py): empty -> one active 65534 placeholder (absent from PACK_CATALOG); non-empty [70] -> no placeholder, genuine pack shown; economy safety; normal packs 1/5/6/7 untouched.
2026-08-13 01:08:37 +00:00
funman300 54ad9e8f79 docs(state): Slice 8 — real-data staged retail A/B PASS
Records the operator-assisted live A/B on the REAL imported club (1949/1962, 13
Legends deferred): real club render, clean pagination, Special filter (1665, 0
base leaked), squad edit persistence + cold relaunch, and rollback->Python->Rust
re-enable. Documents the three real-data fidelity fixes (versioned resourceId
e187cd4, rareflag 626c972, rare=SP filter 6f16a23) and the nation/league/team
model correction (44fcf24). Marks PRODUCTION NOT READY pending .105 Legends name
data for the 13 deferred assets. Aggregate counts only; no private identity.
2026-08-12 21:37:42 +00:00
funman300 6f16a231fc fix(fifa17): implement rare=SP 'Special' club filter via rareflag
The club search 'Quality = Special' sends rare=SP, which was a deliberate no-op
('semantics UNKNOWN'), so it returned every card — base golds included. The
rareflag work now grounds it: a special is rareflag > 1 (base rare = 1),
evidence-backed by the FIFA17 taxonomy + the observed profile (base Ronaldo/Messi
rareflag 1; their informs 11/24).

rareflag lives in the FIFA catalog, not Core, so Core cannot filter it:
- map_to_core: rare=SP now sets CoreOwnedQuery.special (host-applied), not
  'unsupported'; any OTHER rare value stays unsupported. special is NEVER a Core
  /collection param. is_special_rareflag(rf)=rf>1 lives in the adapter.
- handle_club special path: fetch all items matching the OTHER filters (offset/
  limit stripped), shape (resolves rareflag), then special_filter_page() keeps
  rareflag>1 and paginates the FILTERED set locally (start/count over specials,
  not Core's unfiltered page) — no base leakage, no post-pagination drops.

Verified live on the real staged club: rare=SP -> 1665 items (=1949-284 base),
rareflag distribution all >1, zero base leaked; pagination page0==full[0:50],
page1==full[50:100], no overlap. Tests: adapter map rare=SP->special, unknown
rare stays unsupported, is_special_rareflag predicate; host special_filter_page
filter+paginate. adapter 113 + host 25 + importer 25 green; clippy -D clean.
2026-08-12 21:25:18 +00:00
funman300 626c972232 fix(fifa17): carry observed rareflag so special cards render as specials
shape_item hardcoded rareflag=1, so all 1949 cards shaped as basic rare gold
regardless of type; informs/specials lost their card art. The dev fixture is
base-only, so this was invisible until the real profile (10 distinct rareflag
values) exposed it on .105.

rareflag is definition-level FIFA identity metadata OBSERVED from the profile
(the raw wire integer, never a guessed marketing label), so it lives in the
FIFA catalog like asset_id/version, not in generic Core:
- ObservedDefinition.rareflag + emitted into the production catalog entry.
- Fifa17CardCatalog RawCard/Fifa17CardIdentity gain rareflag (default 1 when a
  base-only catalog omits it, preserving prior wire behaviour).
- Fifa17Identity.rareflag; host resolver populates it from the catalog.
- shape_item emits id.rareflag instead of a hardcoded 1.

Verified on the real staged /club: wire rareflag distribution == source exactly
(0 per-item mismatches across 1949; e.g. rareflag 3 x591, 24 x302, 21 x256).
adapter 111 + host 24 + importer 25 tests green; clippy -D clean. rareflag lives
in the host catalog, not Core, so no re-import was needed.
2026-08-12 21:13:51 +00:00
funman300 44fcf24d92 fix(import-fifa17): nation/league/team are instance metadata, not definition identity
Evidence (resourceId 169193): its 4 owned copies are IDENTICAL in asset/rating/
position/all attributes and differ ONLY in nation/team/league (and those resolve
inconsistently, e.g. team 240 'Atletico Madrid' under league 16 'Ligue 1'). A
player's club affiliation is an instance-time snapshot, not part of the card
DEFINITION identity.

Correct the model (not a special-case): the definition-consistency gate now
compares a DefIdentity projection (asset_id/version/rating/position/attrs/
rareflag) and EXCLUDES nation/league/team. A club-only difference between copies
of one resourceId is no longer a conflict; a real identity disagreement
(rating/position/attrs/asset) still trips it. The definition's display
nation/league/club use the first-observed copy (deterministic; display-only,
never identity). No --defer-conflict allowlist entry is needed for 169193 now.

On the real profile: conflicts 1->0, 169193 reclassified conflict->NoName
(still deferred, unnameable), supported still 1681, deferred instances still 13,
BLOCKERS none without any --defer-conflict flag. 2 new tests (club-only diff is
not a conflict; rating diff still is). crate suite 25 green; clippy -D clean.
2026-08-12 20:58:33 +00:00
funman300 e187cd49a2 fix(fifa17): preserve versioned resourceId on the wire (no special->base collapse)
shape_item emitted resourceId/definitionId = asset_id (base), collapsing every
versioned (special) card onto its base definition on the /club and squad wire.
The dev 32-card fixture is base-only (version 0), so Slice 7 never exposed it;
the real profile (1531 versioned cards) did.

Fifa17Identity now carries resource_id (= (version<<24)|asset_id, == asset_id
for a base card). shape_item emits resourceId/definitionId from resource_id and
assetId/cardassetid from asset_id — versioned and base stay distinct. The host
resolver populates resource_id from the catalog's reconstructed resource_id
(the catalog already parsed version; it was dropped before shaping).

Regression test: versioned 117617092 (v7 of asset 176580) shapes resourceId/
definitionId=117617092, assetId/cardassetid=176580.

Verified on the real staged /club: 1949 items, wire-id set exact, 0
wire->resourceId mismatches, 0 duplicate-multiplicity mismatches vs the source
manifest. adapter 111 + host 24 tests green; clippy -D warnings clean.
2026-08-12 20:46:48 +00:00
funman300 1631d3b1a2 feat(import-fifa17): --apply — recoverable two-store real-profile import
FIFA17-specific orchestration that installs the real profile across BOTH durable
stores (openfut-identity + Core SQLite) recoverably and idempotently, handing
Core only a GENERIC request (all FIFA17 semantics stay in this adapter layer).

apply module:
- owned_item_id(persona, wire) = deterministic UUIDv5 from a private namespace;
  identical in BOTH stores (identity core_id AND Core owned_cards.id), so the
  running host's wire->owned reverse lookup resolves exactly what Core stored.
- plan_apply(report, raw_profile, fp): pure translation to a GenericImportRequest
  (card_id = fifa17_<resourceId>) + the preserved (owned_item_id <-> source wire)
  mappings + watermark. Canonical squad + Fifa17SquadExtensionV1 are built by the
  SAME adapter code (parse_squad_put + build_squad_write) the retail-validated
  live squad path uses. Refuses if the report has blockers.
- Two-store protocol (apply): staging gate -> local Core-preflight mirror ->
  identity dry-preflight -> idempotent seed (insert_existing_mapping per instance
  + set_watermark) -> ONE generic Core import transaction (spawned binary) ->
  cross-store post-validation -> completion record. A crash after identity
  seeding re-converges on re-run (idempotent mappings + Core already_imported):
  no cleanup, no reminting.
- gate_staging: deferred players are ABSENT from an import; allowed only for a
  staged run behind --allow-deferred-players-for-staging (never a silent default;
  prints an INCOMPLETE banner). Production requires zero deferred instances.

CLI: --apply (with --emit-content, --core-bin, --core-db, --core-data,
--identity-store, --allow-deferred-players-for-staging). Depends on
openfut-adapter-fifa17 + openfut-identity + uuid(v5).

Proven end-to-end on the real 33068179/CAGE profile (staged): first apply
imports 1949 supported instances (293 base + versioned), 11/11 f433 squad +
opaque extension, coins 28,112,944, fingerprint 8dc5582d2414af28; re-run is an
idempotent no-op (already_imported, DB unchanged); staging-not-default refuses
before any write; every OwnedItemId is an opaque UUID; identity wire-id set ==
source supported set exactly (0 minted, 0 dropped); 13 deferred instances leak 0.

10 new apply tests (determinism, request/mapping/squad translation, blocker
refusal, staging gate both ways, local preflight, identity seed/dry/postvalidate/
idempotency, conflict detection, graceful spawn failure). clippy -D warnings
clean; crate suite 23 tests green.
2026-08-12 20:36:34 +00:00
funman300 c71c2a8d33 feat(import): --emit-content (production pack + host catalog + private manifest)
Fold entity resolution (nation/league/club id->name via committed tables,
mirroring seed_fifa17_cards.py) and quality-tier rarity into the analysis, so
the supported set is honest about unresolved entities too. Add an explicit
--defer-conflict <rid> allowlist: a reviewed conflict (169193) defers, any NEW
conflict still hard-fails (defer never becomes a silent conflict suppressor).

--emit-content writes three files, PUBLIC content separated from PRIVATE account
state: fifa17-production-cards.json (Core CardDefinition[] keyed fifa17_<resourceId>,
base+versioned, tier rarity, profile-derived, no promo labels), a versioned host
identity catalog {card_id:{asset_id,version}}, and a private import manifest
(supported instances' wire ids + deferred set with reasons + preserved watermark
+ target profile + snapshot fingerprint). Emit refuses while blockers exist.

Real profile (33068179/CAGE): 1681 supported defs (150 base + 1531 versioned),
9 NoName deferred, 1 approved-deferred conflict (169193, 4 copies), 1949
importable instances, watermark 100004617 -> next 100004617, active squad f433
11/11 supported. fmt + clippy -D warnings clean; 13 tests.
2026-08-12 19:41:17 +00:00
funman300 a51947562c feat(import): identity import API + FIFA17 real-profile dry-run importer
openfut-identity:
- insert_existing_mapping(game,kind,core_id,external_id): preserve an existing
  external wire id instead of minting; idempotent for an identical mapping,
  rejects conflicting forward/reverse with IdError::Conflict, persists atomically.
- persisted per-scope allocator watermark (set_watermark/watermark_for) so a
  future mint continues past the source high-water even across burned-id gaps;
  next id = max(base_floor, live_max+1, watermark). Backward-compatible on-disk
  format (legacy bare [Row] still loads). +4 tests (10 total).

openfut-import-fifa17 (new): read-only dry-run analysis of a real FIFA17 Python
profile for a faithful Core import. Enforces disjoint item-class balance;
proposes profile-derived CardDefinitions keyed fifa17_<resourceId> (base vs
versioned never collapse) with a resourceId-group consistency gate (hard-fail on
disagreement, never pick a winner) and honest buildability (roster name +
version formula + metadata, never fabricated); plans owned-instance identity
(preserve Python wire ids, preserve nextItemId watermark); checks active-squad
coverage. --apply/--emit-content refuse to write in this phase. 11 tests.

Real profile (33068179/CAGE) dry-run: 1982 items balance (1962 players + 17
consumables + 3 staff); 1681 supported defs (155 base + 1535 versioned), 9
NoName unsupported, 1 hard conflict (resourceId 169193: one of 4 copies has a
divergent nation/team/league); 1949 importable player instances, watermark
100004617 -> first new alloc 100004617; active squad f433 fully supported.
fmt + clippy -D warnings clean.
2026-08-12 19:23:19 +00:00
funman300 63f02c4fb1 docs(state): Slice 7 — FUT squad read+write retail-validated on FIFA 17
Record the staged retail A/B: FIFA consumed the Rust squad path end-to-end
(userMassInfo overlay, in-game swap -> squad-replace {"id":0}, formation
f442->f433 persisted to Core, cold relaunch returned the persisted squad),
with Python rollback / Rust re-enable proven by host-log presence. Note the
OPENFUT_DEV_CONTENT_GAMES=fifa17 startup requirement (silent empty /collection
if omitted) as a needed deployment/preflight assertion.
2026-08-12 18:41:24 +00:00
funman300 4fd5ee2608 redirector: a port probe must not forge the signature of a TLS fault
"TLS HANDSHAKE FAILED: ... unexpected EOF" is exactly how the certificate
mismatch presented -- the defect that cost three live gate attempts and was
invisible everywhere else. It is the one line this project has learned to
treat as serious.

A reachability probe forges it for free: TcpStream::connect followed by a
drop opens the connection and closes without sending a byte, and the
acceptor reports that as "unexpected EOF". The launcher's preflight makes
two such probes per run. On 2026-08-12 they produced ten of these lines and
sent a whole session diagnosing a client-side fault that did not exist --
autopatch, ptrace_scope and client_arm.sh were all investigated before the
pairing of the timestamps gave it away.

Classify before the acceptor sees the connection: peek one byte, and treat
EOF-before-any-byte as a probe with its own quiet line. A timeout is
deliberately NOT a probe -- a slow or broken client must still reach the
acceptor and produce a real diagnostic, since misclassifying a fault as
benign would defeat the point.

The counter exists because the test needs it. A probe produces no response
either way, so a test written against client-visible behaviour passes with
the classification deleted; asserting on a count is what makes the mutation
detectable. Verified: all three mutations (drop the classification, treat
undetermined as a probe, treat a speaking client as a probe) are killed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 17:25:21 +00:00
funman300 c7d4b9f753 style(adapter): rustfmt catalog test assertions
Trailing rustfmt reflow of two catalog unit-test assertions (no logic change); clears the adapter dirty state so verify-build-identity.sh passes for the UTAS A/B.
2026-08-12 17:15:14 +00:00
funman300 37c2e5d7ee feat(utas-host): serve GET /squad/active from Core
Migrate the active-squad READ off the Python oracle to the existing Core-backed projector, completing the squad authority (read + write + /squad/list + userMassInfo overlay) on one projector.

- classify: GET /ut/game/<t>/squad/active -> Route::SquadActive. Numeric GET /squad/<n> stays on Python (no Core multi-squad model yet).
- handle_squad_active returns the projector object via user_mass_info_squad(v, persona) — byte-identical to userMassInfo.squad; degrades to an empty overlay on stale/missing/Core-error, never falls back to Python.
- persona: new REQUIRED OPENFUT_PERSONA_ID (non-zero) on HostConfig, injected not baked, must match LSX/Blaze/POW/UTAS identity.
- tests: squad_active parity test; classify updated; README config table + A/B command.

fmt + clippy -D warnings + tests (24 host + adapter) green.
2026-08-12 17:10:04 +00:00
funman300 7dbd878398 test: update mutation-battery anchors after rustfmt reflow
Formatting reflowed two mutation target lines onto multiple lines; update
the battery anchors (adapter #14 kit lookup, host #19 Content-Length push)
to the new unique substrings. Both batteries kill all mutants again
(adapter 14/14, host 20/20).
2026-08-12 04:01:37 +00:00
funman300 c2e2e0d8f2 style: rustfmt squad host + adapter files
Formatting-only. Runs the project formatter over the files authored/edited
this session (host lib+tests, adapter item/squad_ext/squad_projection/mod +
projection test). The intentionally-preserved dirty catalog.rs and
pre-existing /club-era host drift beyond these files are out of scope.
2026-08-12 03:59:15 +00:00
funman300 afc909fd3b fix(fixtures): sanitize lab subnet from committed UTAS captures
The capture sanitizer redacted tokens/device ids but left the lab Host
address (10.10.0.x) in three committed UTAS fixtures, tripping
scripts/check-no-lab-addresses.sh. Replace with an RFC 5737 TEST-NET
address (192.0.2.120) per the guard's own doctrine. Host header is
plaintext metadata only (tests decode body_b64), so no test is affected.
Pre-existing leak (fixtures committed at 0b66662/eb8311a); no lab address
was introduced this session.
2026-08-12 03:59:15 +00:00
funman300 b607ff28cb chore: bump openfut-core gitlink to rustfmt'd squad-ext routes (3084a46) 2026-08-12 03:58:51 +00:00
funman300 cf86d4e425 test(utas-host): squad host mutation battery (20 mutants, all killed)
mutation-battery.sh injects each of the 20 required wrong behaviours into
the committed host (or the adapter it composes) source, runs the one host
test that must catch it, and requires a non-zero exit (killed), reverting
via git after each. Adds a duplicate-definition host round-trip test.

Kills: authz-skipped, authz-loop-empty, failure-masked-as-success,
PUT-as-partial-diff, extension-dropped, stale-accepted, missing-fabricated,
overlay-clobbers-{userInfo,settings,pile}, list-separate-shaping,
captain-as-resourceId, kit-by-slot, client-eval-dropped, host-trusts-fp,
per-slot-N+1, squad/active-to-Rust, duplicate-collapse, stale-content-length,
python-squad-after-failure. 20/20 killed.
2026-08-12 03:11:25 +00:00