Files
OpenFUT/fifa17-recon/tools/test_empty_mypacks.py
T
funman300 f42279f869 fix(fifa17): keep empty My Packs group client-safe
When the account owns zero unopened packs, store_catalog() emits a synthetic `mypacks` group placeholder (id 65534, absent from PACK_CATALOG). Change its state from "inactive" to "active".

Root cause (bug 6c): FIFA 17's Store/Scaleform path resolves the `mypacks` category even with zero unopened packs (category chosen client-side via the movie's CATEGORY_ID -> screen+0x290; no server field gates it). CardsDLL FUN_1800147f0 then dereferences the resolved group with no null guard, so an absent group crashes the client (CardsDLL+0x14882, [NULL+0x48], minidump-confirmed). An inactive placeholder avoids the crash but makes the Store report the pack unavailable on entry and bounce to the Hub; an active placeholder lets the Store open normally.

65534 stays economy-safe: pack_by_id() returns None, so store_buy()/purchased_items() cannot open it or grant items/coins, and grant_unopened_pack() rejects it. Explicit selection is rejected client-side ("This pack is no longer available") and sends no backend request. This is a FIFA-17 client-compatibility shim (P2), not an EA-authentic representation, confined to the FIFA-17 backend (not OpenFUT Core). A clean zero-pack UX needs a client-side fix (docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md).

Adds regression tests (test_empty_mypacks.py): empty -> one active 65534 placeholder (absent from PACK_CATALOG); non-empty [70] -> no placeholder, genuine pack shown; economy safety; normal packs 1/5/6/7 untouched.
2026-08-13 01:08:37 +00:00

137 lines
5.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Regression tests for the empty-My-Packs FIFA 17 compatibility workaround (bug 6c).
Pins the behavior store_catalog() now depends on:
- unopenedPackIds == [] -> exactly one synthetic active `mypacks` placeholder id 65534
- unopenedPackIds == [70] -> no synthetic placeholder; the genuine owned pack is shown
- synthetic id 65534 stays economy-safe (non-resolvable, non-openable, non-granting)
- normal store packs (1/5/6/7) are untouched by the empty-state behavior
See docs/evidence/STORE_TILE_6C.md and FIFA17_EMPTY_MYPACKS_CLIENT_CONTRACT.md.
Standalone unit test in the project style: `python3 test_empty_mypacks.py`.
"""
import importlib
import os
import sys
import tempfile
TOOLS = os.path.dirname(os.path.abspath(__file__))
if TOOLS not in sys.path:
sys.path.insert(0, TOOLS)
SENTINEL_ID = 65534
def _set_unopened(fut_store, ids):
"""Deterministically set the active profile's owned unopened packs."""
p = fut_store.STORE.load()
p["unopenedPackIds"] = list(ids)
fut_store.STORE._save()
def _mypacks(catalog):
return [p for p in catalog["purchase"]
if (p.get("displayGroup") or {}).get("value") == "mypacks"]
def _fake_request(command, body):
class _H:
pass
h = _H()
h.command = command
h._body = body
return h
def main():
with tempfile.TemporaryDirectory() as state:
os.environ["FUT_ACCOUNT_PATH"] = os.path.join(state, "active_account.json")
os.environ["FUT_PROFILE_ROOT"] = os.path.join(state, "accounts")
os.environ.pop("FUT_PROFILE", None)
import fut_account
import fut_store
import fut_accounts
import utas_server
importlib.reload(fut_account)
importlib.reload(fut_store)
importlib.reload(fut_accounts)
importlib.reload(utas_server)
fut_accounts.activate({"personaId": 111001, "personaName": "TEST_A"})
catalog_ids = [p["id"] for p in fut_store.PACK_CATALOG]
# ---- A. Empty unopened packs -> one active synthetic 65534 placeholder ----
_set_unopened(fut_store, [])
utas_server._OPENED_PACK_GRACE.clear()
status, cat = utas_server.store_catalog(None)
assert status == 200
myp = _mypacks(cat)
assert len(myp) == 1, "expected exactly one mypacks entry, got %r" % myp
s = myp[0]
assert s["id"] == SENTINEL_ID, s
assert s["state"] == "active", s # the P2 fix: active, not inactive
assert (s.get("displayGroup") or {}).get("value") == "mypacks", s
assert SENTINEL_ID not in catalog_ids, "65534 must not be in PACK_CATALOG"
assert fut_store.pack_by_id(SENTINEL_ID) is None
print("A empty-state active placeholder: PASS")
# ---- D (empty half). Normal packs untouched in empty state ----
norm = {p["id"]: p for p in cat["purchase"] if p["id"] in (1, 5, 6, 7)}
assert set(norm) == {1, 5, 6, 7}, sorted(norm)
assert all(norm[i]["state"] == "active" for i in norm), norm
assert norm[1]["packType"] == "BRONZE" and norm[1]["description"] == "Bronze Pack"
# ---- B. Non-empty unopened packs -> NO synthetic; genuine owned pack shown ----
_set_unopened(fut_store, [70])
utas_server._OPENED_PACK_GRACE.clear()
status, cat = utas_server.store_catalog(None)
assert status == 200
ids = [p["id"] for p in cat["purchase"]]
assert SENTINEL_ID not in ids, "synthetic placeholder must be suppressed when a pack exists"
myp = _mypacks(cat)
assert len(myp) == 1 and myp[0]["id"] == 70, myp
assert myp[0]["state"] == "active" and myp[0]["unopened"] is True, myp[0]
# normal packs still intact alongside the owned pack
assert {1, 5, 6, 7}.issubset(set(ids)), sorted(ids)
print("B non-empty-state genuine pack: PASS")
# ---- C. Economy safety of the synthetic placeholder ----
_set_unopened(fut_store, [])
utas_server._OPENED_PACK_GRACE.clear()
coins0 = fut_store.STORE.coins()
items0 = len(fut_store.STORE.items())
next0 = fut_store.STORE.load()["nextItemId"]
assert fut_store.pack_by_id(SENTINEL_ID) is None
# store_buy: a confirmed-buy transaction for 65534 must be a no-op {}
status, body = utas_server.store_buy(
_fake_request("PUT", b'{"packId":65534,"state":"TRANSACTIONCREATED"}'))
assert status == 200 and body == {}, (status, body)
# purchased_items: POST buy for 65534 must not open/grant anything
status, body = utas_server.purchased_items(
_fake_request("POST", b'{"packId":65534,"useCredits":1,"usePreOrder":0,"currency":"COINS"}'))
assert status == 200, (status, body)
assert "createPackResponse" not in body, body
# 65534 cannot enter the owned-pack pile (not a catalog pack)
assert fut_store.STORE.grant_unopened_pack(SENTINEL_ID) is False
assert SENTINEL_ID not in fut_store.STORE.unopened_packs()
# nothing mutated
assert fut_store.STORE.coins() == coins0, (fut_store.STORE.coins(), coins0)
assert len(fut_store.STORE.items()) == items0
assert fut_store.STORE.load()["nextItemId"] == next0
assert not any(i.get("id") == SENTINEL_ID or i.get("resourceId") == SENTINEL_ID
for i in fut_store.STORE.items())
print("C economy safety (65534 non-openable / non-granting): PASS")
print("empty My Packs compatibility: PASS")
return 0
if __name__ == "__main__":
raise SystemExit(main())