Commit Graph

15 Commits

Author SHA1 Message Date
funman300 9026220533 feat(fifa17): manager contracts, from a Core-owned staff tier
ROOT CAUSE, one line. openfut-import-fifa17 emitted `"overall": 0` for every
non-player Core definition while `d.rating` already held EA's authoritative
`value` -- and the very next block wrote that same number correctly to the
adapter catalog. So the tier existed host-side but never reached Core:
Core overall 0 -> /collection effective_overall 0 -> CoreOwnedItem.rating 0 ->
tier_for_rating(0) = Bronze for a Gold (88) manager. That silent mis-grant is
exactly what the 409 was protecting against, so the refusal was correct.

The emitter now also writes `source_rating`, keeping `overall` at 0. Regenerating
the production pack changes exactly 18 entries and exactly one field each
(source_rating None -> value); same 1710 ids, same fingerprint 28c333f1e833338a.

WHY value IS the tier source, and why the thresholds are the player ladder:
LIVE_PROVEN, not inferred. The client re-rates staff from its own
managercards/*coachcards/physiocards by carddbid and applies discard_level's
65/75 ladder; coach_probe/discard_probe agree 4/4 (manager value 88 -> level 3,
coaches 66 -> level 2). The shipped coach tables corroborate: each family has
exactly 3 tiers x 2 rarities, and only 65/75 splits them 2/2/2.

Manager contracts stop refusing and now resolve the TARGET's tier from
Core-owned state. Still fail-closed everywhere it matters: a coach or physio is
`contract_target_not_a_manager` (only cardsubtypeid 4 is a manager), and a
manager Core carries no source_rating for is `manager_tier_unknown` rather than
a guessed tier. Core's own content_kind token is sent as target_kind, because
Core calls the squad manager `manager` while the catalog classifies it
`staff`+subtype 4.

NOT implemented, unchanged: STORED_MANAGER_BONUS and MATCH_CONTRACT_DECREMENT.
2026-08-22 20:08:26 +00:00
funman300 755f237f17 fifa17: carry the staff rating the client re-rates to, verified live
Staff quick-sell could not be priced correctly: for cardtypes 2/3/4/5/10 the
client overwrites the rating and rare flag we send with values from its own card
database, and a staff wire record carries no rating, no rareflag and no
discardValue at all. The server had no way to know the displayed price from what
it sent, so pricing declined for staff and fell back to the placeholder ladder.

The missing input was read straight out of the running client (pid 6580), no UI
interaction required:

  * tools/coach_probe.py grades all four resident staff records HIT, which by
    construction requires record +0xb4 == the table's `value` and +0x58 == its
    `rare`. That settles `value`-is-the-rating, which was previously an inference
    and was deliberately not shipped on that basis.
  * tools/discard_probe.py (new) reads both discard slots -- +0x38, the value we
    sent, and +0x3c, the value the client computed for itself:

      1000509  sub 4  ct 2  rat 88  rare 1   sent 0   calc 282   predicted 282
      9000081  sub 6  ct 10 rat 66  rare 0   sent 0   calc  36   predicted  36
      3000083  sub 8  ct 4  rat 66  rare 0   sent 0   calc  36   predicted  36

    4 of 4 agree, 0 disagree. 36 on the value-66 GK coach was the exact falsifier
    written for this last commit.

Entities::enrich_staff now fills rating from `value` and rareflag from `rare` for
the five staff families, and the catalog emits the real rareflag instead of a
hardcoded 0 (it is not cosmetic -- it selects the discard price column, which is
why the rare-1 manager prices at 282 and a rare-0 coach at 36). Players and
consumables are untouched; their wire values are authoritative.

Verified on staging: a GK coach quick-sells for 36, not the 150 floor. The
catalog diff is exactly the two coach entries gaining rating 66; 1710 entries in
and out, nothing else changed.

The same probe shows what production does to PLAYERS today: every resident player
carries sent+38 = 1500, which suppresses the client's own computation, against a
real 688..752 for a gold rare and 72,800 / 74,400 for the two legends.

Still open, and not a discard problem: manager fifa17_1000509 is owned in Core
but has no catalog entry or definition (it reaches the client through the opaque
squad extension), so it declines to the ladder. That is definition coverage.

Importer 41 tests, fmt and clippy clean.
2026-08-21 22:51:16 +00:00
funman300 43aa114bcd style(import-fifa17): rustfmt the club-family classifier and its tests 2026-08-21 20:47:05 +00:00
funman300 7a4dab04d2 feat(import-fifa17): classify every club family, not just kits
Kits were recognised by the id range 6_300_000..=6_400_654, so stadiums,
badges, balls and league logos all fell through to `Other` and were silently
dropped from the import — a club could own them and Core would never hear.

Club items are now settled by `cardsubtypeid` (kit 9, stadium 10, badge 11,
ball 30, league logo 31), which is the discriminator the client's own club-item
resolver uses and cannot collide with the other classes: consumables occupy
51..=341 and staff 4/6/8.

The render gate generalises with it. Each family ships a CONSTANT cardassetid
(kit 35, stadium 36, ball 37, badge 39, logo 40 — verified across all 2302
shipped rows), so a copy carrying anything else is deferred rather than drawn
as the wrong art. Only kits additionally require a teamid, because their
identity resolver keys on it and real owned kits carry it; demanding one of the
other families would defer every legitimate badge, ball and stadium.

League logos map to `misc`: they have no equipped slot, so Core holds them as
generic owned content rather than inventing a designation.

The real profile is unaffected (it owns no club items beyond its kits) and its
emitted catalog is byte-identical.
2026-08-21 20:19:04 +00:00
funman300 09db5413cd feat(import-fifa17): emit the Core reclassify request from the catalog
The importer already knows every definition's kind, so it writes the mapping
Core needs to correct a club imported before the taxonomy existed. Applied to
the real 1989-item club: 20 rows corrected (17 consumables + 3 staff), 1966
players already right, 0 unmatched definitions.
2026-08-21 19:55:46 +00:00
funman300 770029f207 fix(import-fifa17): carry the fields a consumable needs to exist
Two defects that together made the club's 17 owned consumables invisible while
club/stats still counted them — the count gate promised 17, the item route
served 0.

1. The catalog omitted `card_asset_id`, `amount`, `contract` and `rating` for
   non-player definitions. Without an art id the adapter refuses to emit the
   card (it would draw the notfound box), and the families that read
   `amount`/`contract` would render "-1" or grant nothing. All four values are
   present in the source wire and were simply dropped on the way out.

2. Owned rows were imported without a `content_kind`, and Core defaults an
   unstated row to `player` — durably recording a fitness coach and a contract
   card as players in the ownership authority, even though the catalog-driven
   wire looked right.

These are definition-level fields, so every owned copy must agree; a group that
disagrees is deferred rather than resolved by taking the first copy's value.
Measured on the real profile: observed `amount` equals the `fcc_*` table row for
every consumable that carries one (1, 2, 4, 5, 10, 15), and a wire omission
corresponds to a table amount of 0. It is therefore NOT a stack count — two
copies of 5003068 are two instances — so the import states no `quantity` at all.
2026-08-21 19:50:13 +00:00
funman300 db743ffd1f feat(fifa17): project owned kits with active home/away designation
Closes the server side of the FUT kit selector. Ownership stays generic in
Core (submodule bump: club_kit_assignments + GET/PUT /club/kits); this
commit adds the FIFA17 representation, the host projection and importer
support.

adapter:
* ContentKind::Kit ("kit") so kits are classified alongside player/staff/
  consumable instead of being mistaken for 0-rated players.
* Fifa17CardIdentity carries card_asset_id and team_id; RawCard keeps both
  optional because the emitted catalog writes null for non-kit definitions.
* shape_kit_item emits only the fields the client's kit path reads
  (id/resourceId/assetId/cardassetid/cardsubtypeid/itemState/owners/
  untradeable/teamid) — no attributeList, no itemType.
* itemState on the wire is the STRING token activeHomeKit/activeAwayKit;
  the 101/102 integers are the client's post-deserialisation runtime enum
  (item+0x5c) and are never emitted.
* club_stats S_KITS (0x28) now counts owned kits instead of a hard zero.

host:
* CoreKitAssignments + CoreAccess::get_active_kits (GET /club/kits),
  defaulting to no active kits so a Core without the endpoint degrades
  instead of fabricating a designation.
* handle_club classifies type=player|kit, rejects any other type with an
  empty page and outcome=unsupported_type, and now always fetches
  unpaginated from Core: kind and transfer-pile membership are host-side
  concepts Core cannot express, so filtering and pagination must both
  happen after shaping or pages come back short.

importer:
* ItemClass::Kit (cardsubtypeid == 9 and resourceId in 6_300_000..=6_400_654),
  kit counts/balances, and card_asset_id/team_id carried into the emitted
  catalog and manifest.
* a kit group missing cardassetid == 35 or teamid is DEFERRED
  (missing_kit_render_metadata) rather than defaulted; conflicting render
  metadata across instances defers as render_metadata_conflict.

staging: sold-staging-up.py seeds two owned kits (6300006 home / 6400003
away, team 21) plus both active designations so the projection can be
verified over HTTP before involving the client.
2026-08-21 03:17:57 +00:00
funman300 d37a9d5b5e feat(fifa17): ownership-backed squad manager, not opaque round-trip
Move the squad manager from an opaque, unvalidated wire ref in the squad
extension to a resolved, ownership-backed assignment (Core migration 0023
squad_managers).

- squad::to_proposed reverse-resolves the manager wire ref to a Core
  owned_card_id on ProposedSquad; an unresolvable manager is reported as
  an unresolved wire id (a live save is refused rather than assigning a
  manager the club does not own).
- squad_ext: drop the opaque manager field from Fifa17SquadExtensionV1
  (clean cutover) and the now-unused SquadEntityRef->WireItemRef From.
- squad_projection: project the manager as the STATIC_REVERSED [{id,dream}]
  wire ref resolved from the owned assignment; absent -> [] (never faked).
  Richer manager itemData (contract/league/nation) is INFERRED-only and
  left out pending wire reversal.
- import(apply): drop a historical dangling manager ref rather than
  failing the whole import (live PUTs still refuse an unresolved manager).
2026-08-20 16:43:39 +00:00
funman300 abe9e663c1 feat(fifa17): import consumable + staff content as first-class Core content
Close 20 of the 33-record content gap (17 consumables + 3 staff; 13 Legends are
unrecoverable from PC data). Verdict A (no Core change): consumables/staff become
ordinary Core CardDefinitions (neutral player fields + honest family/role names)
and owned instances via the SAME generic import path; a catalog kind lets the
adapter exclude them from the player-only /club projection.

- adapter fut::content_taxonomy: evidence-based cardsubtypeid->family/label
  (Ghidra-derived ranges) + staff role map; unknown subtype => defer, never fabricate.
- adapter catalog: Fifa17CardIdentity/RawCard gain optional kind+subtype
  (backward-compat: legacy catalogs load as player); kind_of/subtype_of lookups.
- adapter item/club_response: shape_club_response excludes non-player kinds
  (ShapeStats.excluded_non_player); ItemIdentityResolver::kind_of default=Player.
- host Fifa17IdentityResolver overrides kind_of to delegate to the catalog so
  /club excludes consumables/staff in production.
- import: Item gains cardsubtypeid/cardassetid/amount/contract; plan_non_player_definitions
  (resourceId-grouped, subtype-consistency gated); emit_content writes non-player
  defs + catalog kind + manifest; apply mints owned instances via owned_item_id.

Real profile 33068179: 1962 players + 20 non-player = 1982 owned; 18 non-player
defs (16 consumable + 2 staff, dup resourceIds shared); 0 deferred non-player; 0 blockers.
2026-08-14 05:26:02 +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
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 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