fifa17-recon: reconcile authoritative tools with running backend (B)
- Add 8 files present in docker/fifa17-python/tools but missing from the top-level tree: fut_accounts.py + 7 test_*.py contracts (all committed in the server's docker tree; byte-identical to the running image). - Preserve newer responder work already matching the running container: utas_server.py (offlineSeason), lsx_responder_v2.py (OPENFUT_BIND), blaze_responder_v3b.py, autopatch.py, pow_server.py, fut_store.py, test_fut_contract.py, fifa17-hook-m1.sh. - Add 30 newer ghidra_queries (draft purchase/state, SBC 9-26, runtime registries). Local tree is now a strict superset of B with all shared files byte-identical.
This commit is contained in:
+113
-10
@@ -226,6 +226,56 @@ def _item(item_id, asset, rating, pos, nation, league, team, attrs, version=0x00
|
||||
# the club showing different numbers for the same card.
|
||||
|
||||
|
||||
SPECIAL_CARD_TYPES = {
|
||||
# name: (rareflag, revision byte, rating/attribute boost, selection weight)
|
||||
# rareflag names come from FIFA 17's ItemRareType enum. Revisions are local,
|
||||
# stable identities; the client resolves the footballer from the low 24 bits.
|
||||
"TOTW": (3, 1, 2, 34),
|
||||
"PURPLE": (4, 2, 3, 7),
|
||||
"TOTY": (5, 3, 6, 3),
|
||||
"RECORD_BREAKER": (6, 4, 5, 2),
|
||||
"TOTS": (11, 5, 5, 7),
|
||||
"OTW": (21, 6, 2, 14),
|
||||
"HALLOWEEN": (22, 7, 3, 8),
|
||||
"MOVEMBER": (23, 8, 3, 8),
|
||||
"SBC": (24, 9, 4, 17),
|
||||
}
|
||||
|
||||
|
||||
def choose_special_type(player, rng=None):
|
||||
"""Choose a rating-appropriate FIFA 17 promo family for one pool row."""
|
||||
import random
|
||||
rng = rng or random
|
||||
rating = player[1]
|
||||
eligible = []
|
||||
for name, spec in SPECIAL_CARD_TYPES.items():
|
||||
if name in ("TOTY", "RECORD_BREAKER") and rating < 85:
|
||||
continue
|
||||
if name == "TOTS" and rating < 75:
|
||||
continue
|
||||
eligible.append((name, spec[3]))
|
||||
names, weights = zip(*eligible)
|
||||
return rng.choices(names, weights=weights, k=1)[0]
|
||||
|
||||
|
||||
def player_item(item_id, player, special=False):
|
||||
"""Build a base or named FIFA 17 special revision from a pool row.
|
||||
|
||||
`special=True` remains supported and chooses a weighted eligible family;
|
||||
callers and tests may also pass an explicit name such as ``"TOTY"``.
|
||||
"""
|
||||
asset, rating, pos, nation, league, team, attrs = player
|
||||
if special:
|
||||
special_name = choose_special_type(player) if special is True else special
|
||||
rareflag, version, boost, _weight = SPECIAL_CARD_TYPES[special_name]
|
||||
rating = min(99, rating + boost)
|
||||
attrs = [min(99, value + boost) for value in attrs]
|
||||
else:
|
||||
rareflag, version = 1, 0
|
||||
return _item(item_id, asset, rating, pos, nation, league, team, attrs,
|
||||
version=version, rareflag=rareflag)
|
||||
|
||||
|
||||
# FUT_DISCARD_SEND: put discardValue (atom 0xd7) on the wire so the CLIENT DISPLAYS
|
||||
# the same number the server pays.
|
||||
#
|
||||
@@ -293,6 +343,10 @@ def _new_profile():
|
||||
"purchased": [], # unassigned/pending items from opened packs
|
||||
"squads": [], # saved squads (raw squad objects from PUT /squad)
|
||||
"packsOpened": 0,
|
||||
# Owned reward packs are separate from purchased items. Pack 70 is a
|
||||
# one-time migration grant used to bring the retail My Packs flow online.
|
||||
"unopenedPackIds": [70],
|
||||
"unopenedSeeded": True,
|
||||
}
|
||||
|
||||
|
||||
@@ -311,6 +365,10 @@ class Store:
|
||||
self._p = _new_profile()
|
||||
self._sync_identity()
|
||||
self._save()
|
||||
if not self._p.get("unopenedSeeded"):
|
||||
self._p.setdefault("unopenedPackIds", []).append(70)
|
||||
self._p["unopenedSeeded"] = True
|
||||
self._save()
|
||||
self._sync_identity()
|
||||
return self._p
|
||||
|
||||
@@ -513,6 +571,32 @@ class Store:
|
||||
sq = self.load()["squads"]
|
||||
return sq[0] if sq else None
|
||||
|
||||
def unopened_packs(self):
|
||||
"""Owned reward-pack template IDs, including repeated grants."""
|
||||
return list(self.load().get("unopenedPackIds", []))
|
||||
|
||||
def consume_unopened_pack(self, pack_id):
|
||||
"""Atomically consume one owned instance of a reward pack."""
|
||||
with _LOCK:
|
||||
p = self.load()
|
||||
owned = p.setdefault("unopenedPackIds", [])
|
||||
try:
|
||||
owned.remove(pack_id)
|
||||
except ValueError:
|
||||
return False
|
||||
self._save()
|
||||
return True
|
||||
|
||||
def grant_unopened_pack(self, pack_id):
|
||||
"""Persist one additional owned reward-pack instance."""
|
||||
if pack_by_id(pack_id) is None:
|
||||
return False
|
||||
with _LOCK:
|
||||
p = self.load()
|
||||
p.setdefault("unopenedPackIds", []).append(pack_id)
|
||||
self._save()
|
||||
return True
|
||||
|
||||
def reconstruct_squad(self, squad):
|
||||
"""FIFA's updateActiveSquad PUT stores each slot as itemData={id:<clubItemId>}
|
||||
(a reference). Re-embed the FULL club item by id so the squad reloads with
|
||||
@@ -557,7 +641,8 @@ class Store:
|
||||
return i
|
||||
|
||||
|
||||
def open_pack(self, price, count, gold=True, tiers=None):
|
||||
def open_pack(self, price, count, gold=True, tiers=None, special_chance=0.0,
|
||||
players_only=False):
|
||||
"""Deduct `price` coins, generate `count` player items from the pool, and
|
||||
place them in the PENDING purchased pile (unassigned). They are NOT owned
|
||||
club items until moved there via FutMoveCard (PUT /item). Returns None if
|
||||
@@ -579,19 +664,31 @@ class Store:
|
||||
# fixed number so it scales from a 5-card bronze to an 11-card premium.
|
||||
n_extra = 0
|
||||
extras = []
|
||||
if PACK_MIX and count >= 5:
|
||||
if PACK_MIX and not players_only and count >= 5:
|
||||
n_extra = max(1, count // 4)
|
||||
extras = _pack_extras(n_extra, self)
|
||||
n_extra = len(extras)
|
||||
n_players = max(1, count - n_extra)
|
||||
if tiers:
|
||||
picks = [random.choice(fut_cards.pool_for(random.choice(tiers)))
|
||||
for _ in range(n_players)]
|
||||
# Draw each tier independently but reject duplicate asset IDs inside
|
||||
# one pack. The real pool is large enough that this normally succeeds
|
||||
# on the first attempt; the cap makes malformed tiny test pools safe.
|
||||
picks = []
|
||||
used_assets = set()
|
||||
for _ in range(n_players):
|
||||
tier_pool = fut_cards.pool_for(random.choice(tiers))
|
||||
available = [p for p in tier_pool if p[0] not in used_assets]
|
||||
pick = random.choice(available or tier_pool)
|
||||
picks.append(pick)
|
||||
used_assets.add(pick[0])
|
||||
else:
|
||||
pool = [p for p in PACK_POOL if (p[1] >= 75) == gold] or PACK_POOL
|
||||
picks = [random.choice(pool) for _ in range(n_players)]
|
||||
items = [_item(self.new_item_id(), a, r, p, n, lg, tm, at)
|
||||
for (a, r, p, n, lg, tm, at) in picks]
|
||||
picks = random.sample(pool, min(n_players, len(pool)))
|
||||
while len(picks) < n_players:
|
||||
picks.append(random.choice(pool))
|
||||
items = [player_item(self.new_item_id(), pick,
|
||||
special=random.random() < special_chance)
|
||||
for pick in picks]
|
||||
items += extras
|
||||
random.shuffle(items)
|
||||
with _LOCK:
|
||||
@@ -677,11 +774,17 @@ _LEGACY_POOL = STARTER_PLAYERS + [
|
||||
# no silver or bronze players at all, so all three packs were identical in practice.
|
||||
PACK_CATALOG = [
|
||||
{"id": 1, "name": "Bronze Pack", "price": 400, "count": 5, "gold": False,
|
||||
"tiers": ["bronze"] * 8 + ["silver"] * 2},
|
||||
"tiers": ["bronze"] * 8 + ["silver"] * 2, "specialChance": 0.005},
|
||||
{"id": 5, "name": "Gold Pack", "price": 5000, "count": 7, "gold": True,
|
||||
"tiers": ["gold"] * 6 + ["silver"] * 4},
|
||||
"tiers": ["gold"] * 6 + ["silver"] * 4, "specialChance": 0.03},
|
||||
{"id": 6, "name": "Premium Gold", "price": 15000, "count": 11, "gold": True,
|
||||
"tiers": ["gold"] * 9 + ["silver"] * 1},
|
||||
"tiers": ["gold"] * 9 + ["silver"] * 1, "specialChance": 0.08},
|
||||
{"id": 7, "name": "Special Players Pack", "price": 25000, "count": 11,
|
||||
"gold": True, "tiers": ["gold"], "specialChance": 1.0,
|
||||
"playersOnly": True},
|
||||
{"id": 70, "name": "Reward Special Players Pack", "price": 0, "count": 11,
|
||||
"gold": True, "tiers": ["gold"], "specialChance": 1.0,
|
||||
"playersOnly": True, "ownedOnly": True},
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user