fix(fifa17): project active club items through squad.actives

FIFA 17 makes a club item resident ONLY through squad.actives. The squad
parser's arm for atom 11 computes the address of the i-th element of the
client's five-element club-item array and hands it to the item
deserializer as the out-handle:

    cmp  edi,0x5                    ; at most five entries are read
    mov  rax,QWORD PTR [r13+0x108]  ; the club-item array
    lea  rcx,[rax+rcx*8]            ; &array[edi]
    call 0x18013fe00                ; item deserializer, writing that slot

That deserializer inserts the record into the client's resident item map
- keyed by wire instance id, gated only on the id being non-zero - and
binds the slot handle to it. So each element must be a full item object
like squad.manager[].itemData; an id reference alone installs nothing,
because the manager installer looks its id up in that same map and does
nothing on a miss.

We emitted actives: [] as an 'observed constant', which was circular: it
came from our own captures and the Python oracle seeded it. The client
then read the array-end token immediately, parsed nothing, and left all
five slots null, so every lookup resolved to the static not-found
sentinel whose item pointer is NULL. That is why the pre-match kit
selector had no kits, and it is also why /club?type=kit could never fix
it: no /club response feeds that array.

Core already owns the designations via /club/active-items, so the host
reuses get_active_kits() and the adapter shapes each entry with the same
shape_club_item primitive /club?type=kit uses, keeping one wire dialect.
A Core transport error yields no actives and is reported rather than
silently empty. userInfo.actives already mirrors the squad's.

Verified against the client's own fcc_kitcards table: 6300006 is team
21's home card (category 2) and 6400003 the away card (category 3).
This commit is contained in:
funman300
2026-08-24 17:59:11 +00:00
parent 2fc335c37d
commit 0e200758f0
3 changed files with 267 additions and 22 deletions
+35 -11
View File
@@ -81,7 +81,7 @@ use openfut_adapter_fifa17::fut::squad_ext::{
build_squad_write, Fifa17SquadExtensionV1, SquadBuildError, EXT_NAMESPACE, EXT_SCHEMA_VERSION,
};
use openfut_adapter_fifa17::fut::squad_projection::{
project_squad, squad_list, user_mass_info_squad, ProjectionSlot, SquadExtInput,
project_squad, squad_actives, squad_list, user_mass_info_squad, ProjectionSlot, SquadExtInput,
SquadProjection, SquadProjectionInput,
};
use openfut_adapter_fifa17::fut::store_catalog::{
@@ -2663,8 +2663,10 @@ pub struct SquadLog {
/// The active squad projected from Core, with the freshness policy applied.
enum HostProjection {
/// Fresh: the projected FIFA squad object (before any endpoint envelope).
Squad(Value),
/// Fresh: the projected FIFA squad object (before any endpoint envelope),
/// plus the active club items for its `actives` array. They travel together
/// because both are derived from the SAME bounded Core fetch.
Squad { squad: Value, actives: Value },
/// Stored extension is stale vs the canonical squad — NEVER applied.
Stale,
/// No extension stored — nothing fabricated.
@@ -2747,8 +2749,28 @@ fn project_active_squad(deps: &SquadDeps<'_>) -> HostProjection {
owned: &owned_by_id,
manager,
};
// The active club designations Core already owns (`/club/active-items`), shaped
// into the `actives` array the client needs to make a club item resident. A
// transport error here is non-fatal and yields no actives: the squad still
// renders, exactly as it did before this array was populated. It is reported,
// because silently empty actives is precisely the failure that left the
// pre-match kit selector blank.
let actives = match deps.core.get_active_kits() {
Ok(assignments) => squad_actives(
&owned_by_id,
deps.resolver,
ActiveKitAssignments {
home: assignments.home_owned_card_id.as_deref(),
away: assignments.away_owned_card_id.as_deref(),
},
),
Err(error) => {
eprintln!("utas-host WARN active club items unavailable, squad.actives empty: {error}");
json!([])
}
};
match project_squad(&input, deps.resolver, deps.entities) {
Ok(SquadProjection::Projected(v)) => HostProjection::Squad(v),
Ok(SquadProjection::Projected(squad)) => HostProjection::Squad { squad, actives },
Ok(SquadProjection::Stale) => HostProjection::Stale,
Ok(SquadProjection::Missing) => HostProjection::Missing,
Err(e) => HostProjection::Error(e.to_string()),
@@ -2917,8 +2939,8 @@ pub fn handle_put_squad(body: &[u8], deps: &SquadDeps<'_>) -> (WireResponse, Squ
/// to an empty list, NEVER served from Python and NEVER projected from stale ext.
pub fn handle_squad_list(deps: &SquadDeps<'_>) -> (WireResponse, SquadLog) {
match project_active_squad(deps) {
HostProjection::Squad(v) => (
json_response(&squad_list(&v)),
HostProjection::Squad { squad, .. } => (
json_response(&squad_list(&squad)),
SquadLog {
outcome: "ok",
detail: String::new(),
@@ -2955,8 +2977,8 @@ pub fn handle_squad_list(deps: &SquadDeps<'_>) -> (WireResponse, SquadLog) {
/// (never 401/403, never a Python fallback that could mask split authority).
pub fn handle_squad_active(deps: &SquadDeps<'_>, persona_id: i64) -> (WireResponse, SquadLog) {
match project_active_squad(deps) {
HostProjection::Squad(v) => (
json_response(&user_mass_info_squad(v, persona_id)),
HostProjection::Squad { squad, actives } => (
json_response(&user_mass_info_squad(squad, persona_id, actives)),
SquadLog {
outcome: "ok",
detail: String::new(),
@@ -3081,8 +3103,8 @@ pub fn handle_user_mass_info(
})
.unwrap_or(0);
let (squad_val, log) = match project_active_squad(deps) {
HostProjection::Squad(v) => (
user_mass_info_squad(v, persona),
HostProjection::Squad { squad, actives } => (
user_mass_info_squad(squad, persona, actives),
SquadLog {
outcome: "ok",
detail: String::new(),
@@ -4556,7 +4578,9 @@ impl Server {
};
let deps = self.squad_deps();
let (squad, squad_outcome) = match project_active_squad(&deps) {
HostProjection::Squad(v) => (user_mass_info_squad(v, self.persona_id), "ok"),
HostProjection::Squad { squad, actives } => {
(user_mass_info_squad(squad, self.persona_id, actives), "ok")
}
HostProjection::Stale => (empty_squad_overlay(self.persona_id), "stale_integrity"),
HostProjection::Missing => (empty_squad_overlay(self.persona_id), "missing_integrity"),
HostProjection::Error(_) => (empty_squad_overlay(self.persona_id), "core_error"),