74768693ec
A club item's `assetId` (record +0x20) is family specific and is NOT the carddbid: per the client's own tables a kit carries the art class from fcc_kitcards.assetid - 14 for the 63xxxxx home/third band, 15 for the 64xxxxx away band - a badge carries its team id, and a ball and stadium their own asset number. The catalog shipped `asset_id`, the carddbid, in that slot. Measured on the live client with both kits resident: record +0x20 held 6300006 (home) and 6400003 (away) where the table says 14 and 15, while every other field - resourceId, cardassetid 35, category 2/3, teamid 21, year 0, itemState 101/102 - already matched. Operator reports both pre-match kit tiles rendering identically. assetId is the only field that diverges from the client's own data, and an assetId that is not a valid kit art class cannot resolve to distinct art. `resource_id` is derived from `asset_id`, and every home kit shares art class 14, so the two cannot be the same field: catalogs now carry an optional `club_asset_id`, defaulting to `asset_id` so a catalog predating the field and every non-club kind are unchanged. resolve_kit emits it as the wire `assetId`. Fixed at the source too - scripts/sold-staging-up.py emitted asset_id as the wire assetId for all four club families, so a re-emit would have regressed it. Field-offset note: club_items.json's _record_map is authoritative and my earlier working note had these transposed - assetId is +0x20 and cardassetid is +0x1c, not the reverse. Adds tools/live/diff_kit_records.py, which byte-diffs the two resident kit records and names the fields the decoded clone query consumes. Staging wire now reads assetId 14/15 with cardassetid 35 on both squad.actives and /club?type=equippables. Workspace 1250 passed, 0 failed. Client re-parse still to be confirmed visually.
136 lines
4.0 KiB
Python
Executable File
136 lines
4.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Byte-level diff of the two resident kit records in a live FIFA17 client.
|
|
|
|
The pre-match selector draws each kit from a clone query keyed on the record's
|
|
own fields, so if both tiles render identically the question is precisely: which
|
|
bytes of the home record differ from the away record? This prints every differing
|
|
offset with the known field names attached, and dumps the fields the decoded
|
|
clone query consumes.
|
|
|
|
Read-only. Never writes to the process.
|
|
|
|
Decoded query (FUN_1801c3480 -> FUN_1801c44b0):
|
|
teamtechid == record+0x94
|
|
teamkittypetechid == derived from itemState (101 -> 0 home, 102 -> 1 away)
|
|
year == record+0xba
|
|
"""
|
|
import re
|
|
import struct
|
|
import sys
|
|
|
|
PID = int(sys.argv[1])
|
|
WANT = [int(a) for a in sys.argv[2:]] or [100004874, 100004873]
|
|
|
|
mem = open(f"/proc/{PID}/mem", "rb", buffering=0)
|
|
|
|
|
|
def rd(a, n):
|
|
mem.seek(a)
|
|
return mem.read(n)
|
|
|
|
|
|
def q(a):
|
|
return struct.unpack("<Q", rd(a, 8))[0]
|
|
|
|
|
|
named = []
|
|
for ln in open(f"/proc/{PID}/maps"):
|
|
m = re.match(r"([0-9a-f]+)-([0-9a-f]+) \S{4} \S+ \S+ \S+\s+(.+)", ln)
|
|
if m:
|
|
named.append((int(m.group(1), 16), m.group(3).strip()))
|
|
named.sort()
|
|
base = next((s for s, p in named if p.endswith("CardsDLL_Win64_retail.dll")), None)
|
|
if base is None:
|
|
sys.exit("CardsDLL mapping not found")
|
|
|
|
|
|
def live(static):
|
|
return base + (static - 0x180000000)
|
|
|
|
|
|
if rd(live(0x180026FEA), 5) != bytes.fromhex("ba75750000"):
|
|
sys.exit("SANITY FAILED - wrong base")
|
|
print(f" CardsDLL base = {base:#x} (sanity ok)")
|
|
|
|
owner = q(live(0x1802E6398))
|
|
sentinel = owner + 0x160C8
|
|
root = q(owner + 0x160D8)
|
|
|
|
# Known record fields, offset -> (name, width)
|
|
FIELDS = {
|
|
0x08: ("id", 8),
|
|
0x18: ("resourceId/definitionId", 4),
|
|
0x1C: ("assetId", 4),
|
|
0x20: ("cardassetid", 4),
|
|
0x38: ("discardValue", 4),
|
|
0x4C: ("cardtype", 4),
|
|
0x50: ("cardsubtypeid", 4),
|
|
0x5C: ("itemState", 4),
|
|
0x60: ("category(club slot)", 4),
|
|
0x8C: ("contract", 4),
|
|
0x94: ("teamid", 4),
|
|
0xB4: ("rating", 4),
|
|
0xB8: ("wire category", 1),
|
|
0xBA: ("year", 2),
|
|
0x148: ("nation", 4),
|
|
0x154: ("leagueId", 4),
|
|
}
|
|
|
|
|
|
def walk(node, out):
|
|
if not node or node == sentinel:
|
|
return
|
|
walk(q(node + 0x00), out)
|
|
# The record is EMBEDDED at node+0x28 — NOT a pointer stored there.
|
|
out.append((struct.unpack("<q", rd(node + 0x20, 8))[0], node + 0x28))
|
|
walk(q(node + 0x08), out)
|
|
|
|
|
|
nodes = []
|
|
walk(root, nodes)
|
|
recs = {k: v for k, v in nodes}
|
|
|
|
found = [(w, recs[w]) for w in WANT if w in recs]
|
|
if len(found) < 2:
|
|
sys.exit(f" need two resident kit records, found {[w for w, _ in found]}")
|
|
|
|
(id_a, ptr_a), (id_b, ptr_b) = found[0], found[1]
|
|
a = rd(ptr_a, 0x180)
|
|
b = rd(ptr_b, 0x180)
|
|
print(f" A = {id_a} @ {ptr_a:#x}")
|
|
print(f" B = {id_b} @ {ptr_b:#x}")
|
|
|
|
print("\n --- fields the clone query consumes ---")
|
|
for off in (0x94, 0x5C, 0xBA):
|
|
name = FIELDS[off][0]
|
|
w = FIELDS[off][1]
|
|
va = int.from_bytes(a[off : off + w], "little")
|
|
vb = int.from_bytes(b[off : off + w], "little")
|
|
flag = "" if va != vb else " <== IDENTICAL"
|
|
print(f" +{off:#05x} {name:24} A={va:<12} B={vb:<12}{flag}")
|
|
|
|
print("\n --- every differing byte range ---")
|
|
diffs = [i for i in range(0x180) if a[i] != b[i]]
|
|
runs = []
|
|
for i in diffs:
|
|
if runs and i == runs[-1][1] + 1:
|
|
runs[-1][1] = i
|
|
else:
|
|
runs.append([i, i])
|
|
for s, e in runs:
|
|
named_field = next(
|
|
(n for o, (n, w) in FIELDS.items() if o <= s < o + w), "(unmapped)"
|
|
)
|
|
va = int.from_bytes(a[s : e + 1], "little")
|
|
vb = int.from_bytes(b[s : e + 1], "little")
|
|
print(f" +{s:#05x}..{e:#05x} {named_field:24} A={va:<12} B={vb}")
|
|
print(f"\n {len(diffs)} differing bytes in {len(runs)} runs")
|
|
|
|
print("\n --- known fields, side by side ---")
|
|
for off in sorted(FIELDS):
|
|
name, w = FIELDS[off]
|
|
va = int.from_bytes(a[off : off + w], "little")
|
|
vb = int.from_bytes(b[off : off + w], "little")
|
|
mark = " DIFFERS" if va != vb else ""
|
|
print(f" +{off:#05x} {name:24} A={va:<12} B={vb:<12}{mark}")
|