Files
OpenFUT/openfut-utas-host/src
funman300 ce5d4204ac feat(host): staging-only consumable-apply probe; reverse the success contract
Claims POST ut/<sku>/item/resource/<resourceId> -- the consumable apply captured
live 2026-08-21 -- behind OPENFUT_FIFA17_APPLY_PROBE=1, default OFF. With the
gate off the route takes the extracted `passthrough` method, i.e. byte-for-byte
the behaviour that existed before this commit, so production cannot serve a
diagnostic even if the route is reached.

The handler is NON-AUTHORITATIVE BY CONSTRUCTION: it consumes no source card,
mutates no target, touches no contract/fitness/chemistry/training/injury state,
mints no coins and changes no ownership. It exists only to observe the client's
success path, because the EFFECT of a consumable is still unreversed and
implementing one on an inferred value is not acceptable.

RESPONSE SHAPE, from static RE rather than convenience (the brief was explicit
that `{}` must not be chosen because it is easy):

  * The apply completion handler is CardsDLL 0x180035520. It does
    `mov ecx,[rdx+0x1c]; test ecx,ecx; jne FAILURE`, raising
    EVENT_CARDS_APPLY_CARD_SUCCESS (0x1801f37f0) on zero and
    EVENT_CARDS_APPLY_CARD_FAILURE (0x1801f3810) otherwise. It tests exactly one
    field -- the transport code -- and never inspects the body.
  * That is materially different from the MOVE ack (0x180128600), which builds
    per-item verdict records and reports FAILURE when the vector is EMPTY. The
    `{}`-is-broken precedent does not transfer.
  * The response object's constructor (0x1800a4ce0) initialises its record vector
    (+0x50/+0x58/+0x60, 0x20-byte elements) EMPTY, so an empty parse result is a
    legal state here, and the destructor (0x1800682b0) frees it accordingly.
  * The legacy oracle routes `item/resource` method-agnostically to defs_route,
    so historically this path answered with an `itemData` OBJECT.

`{"itemData":[]}` is the smallest candidate consistent with all four, and it is
labelled a PROBE, not a proven contract.

`apply` is an array, but only len==1 has ever been observed, so a multi-target
request is logged and refused (400 apply_batch_unsupported) rather than given
invented batch semantics.

Operands are identified READ-ONLY for the capture: the source by Core card id
(`<sku>_<resourceId>`, no new resolver method for a probe) with a copy count, the
target by reversing the wire id through the identity store -- never a guess,
`UNRESOLVED_WIRE_ID` when unknown.

Also records the reversed protocol and the `development` finding in
CLIENT_ROUTE_SURFACE.md.

122 host tests (+2: the verb/resource-id classification boundary, and target
parsing incl. the exact captured bytes). clippy and fmt clean.
2026-08-22 00:49:23 +00:00
..