Files
OpenFUT/fifa17-recon/tools
funman300 24bbc32da5 fifa17-recon: fix the match tail -- real URLs, endReason, and coins in the right place
The whole match family is one RPC descriptor block (rows 49-54, every row using URL
template index 16 = `ut/%s/match`) with a fixed suffix appended per call:

  CREATEMATCH  ut/game/fifa17/match          PLAYGAME    ut/game/fifa17/match
  MATCHREADY   ut/game/fifa17/match/ready    DESTROYMATCH ut/game/fifa17/match/end
  RESETMATCH   ut/game/fifa17/match/reset    KEEPALIVE   ut/game/fifa17/match/keepalive

THERE IS NO /match/{id} URL. The id travels in the body. Our reward path was gated on
`h.command == "DELETE" or "/ut/delete/" in h.path` and extracted the id with
re.search(r"/match/(\d+)"), so it was waiting for a request the client does not make.
The gate is now widened to include a /match/end path with ANY verb, because the verb
genuinely cannot be determined statically: the strings "PUT" and "DELETE" do not exist
anywhere in cardsdll.dll (0 hits each), so verb selection happens outside this DLL. A
reviewer flagged "the reward path can never fire" as overreach on exactly that point;
widening rather than replacing the gate is the response.

THE RESULT SIGNAL IS `endReason` (atom 260), a STRING enum with nine values: WIN DRAW
LOSS DNF QUIT NO_CONTEST DNF_WIN DNF_DRAW DNF_LOSS. Not a score comparison. The score
lives in `myMatchStats.goals` / `opponentMatchStats.goals`, two literal-keyed objects
of 15 int fields each, and the client OMITS both when endReason is DNF or QUIT, so
nothing may require them. _match_result() now reads endReason first and keeps the old
spelling probe only as a fallback, because request-side static findings are a floor:
PUT /item's swap/tradeId appeared in no static listing either.

THREE CORRECTIONS TO THE RESPONSE, all of which were shipping wrong:

1. `coins` (atom 149) is NOT a top-level key. It is read only inside `gameModeAward`.
   The one field most obviously named "the reward" was being silently skipped.
2. `qualifiedChampionEventId` (0x269) has a SIDE EFFECT: its branch calls through a
   manager vtable after storing. Sending a habitual zero poked champion-event
   machinery for no benefit. Removed.
3. `bidTokens` (atom 89) inside gameModeAward is MATCHED and then handled by nothing,
   so its value token is left unconsumed. That is the precondition for the desync
   spin. A freeze trap dressed as an ordinary field; now guarded by a unit check.

test_match_rewards.py rewrote its expectations. The old version asserted a top-level
`coins` and passed happily while the server shipped a body whose reward field the
client never read. A test that encodes the wrong schema converts a bug into a
guarantee. New test_end_reason_is_authoritative covers all nine enum values, the
stats-less DNF case, and that endReason beats a contradictory score probe.

DEFAULT ON (FUT_MATCH_END=0 reverts), a reasoned exception to the flag convention:
nothing here is live-proven because no match has ever been played, and the old
behaviour is not a working screen but a path that provably could not fire.

61 unit checks (58 with the flag off), 392 contract checks green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
2026-08-04 13:47:05 -07:00
..