6c97bc4e2b
POST /ut/game/fifa17/item/resource/<rid> {"apply":[{"id":N}]} now performs a
durable atomic contract application instead of falling through to Python.
THE RULE. grant = fcc_contractcards[card][tier(TARGET.rating)], then
min(99, contract + grant). The column is keyed on the TARGET's tier, NOT the
card's own -- all 36 cells of EA's shipped table match the published FIFA 17
matrix, and staging discriminates the two readings outright: a bronze-RARE
card on a rating-89 player granted 3 (the gold column), where the card-level
reading predicts 15.
No client binary reads fcc_contractcards -- a string scan of every .exe/.dll
in the install finds it referenced nowhere, and CardsDLL reads only 14 fcc_
tables (fcc_discardcoins among them, which is why quick-sell prices locally).
Consumable effects are server-authoritative, so EA's shipped table is the only
non-invented source and the client renders whatever we persist and re-serve.
The host computes the grant, Core owns the mutation -- the same split
quick-sell already uses (host prices via discard_value, Core performs
sell_item), and what migration 0027 means by "Core defines NO per-category
formula".
FAILS CLOSED, never 200-and-do-nothing: manager contracts 409 because staff
ratings are unimported so the target tier is unknowable; every other family
409 as unproven; batch 400; unresolvable operand 404. Core's deterministic
refusals pass through with their own status instead of collapsing to 503,
which would tell the client to retry a request that can never succeed.
`contract: 7` stops being a hardcode in shape_item/shape_staff_item and
becomes the fallback for an instance Core tracks no contract for. `fitness: 99`
is the same class of hardcode and is deliberately untouched.
CLEAN CUTOVER: Route::ConsumableApplyProbe, its handler, apply_probe_enabled,
the OPENFUT_FIFA17_APPLY_PROBE gate and both probe scripts are deleted. A
handler no classifier can reach is this repo's recurring defect class, and the
new economy arm preempts the probe. fifa17-migration-rehearse.py also drove
the probe (spelled "apply probe", so an apply-probe grep missed it) and would
have eaten a card off the rehearsal profile; retargeted to a non-mutating
assertion.
Not implemented, on purpose: the stored-manager bonus (real mechanic, rule
appears in no shipped table -- guessing it would corrupt the proven part) and
contract decrement per match (nothing spends contracts yet).
111 lines
4.4 KiB
Python
Executable File
111 lines
4.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Serve the MIGRATED + RECLASSIFIED rehearsal copy with the candidate Rust
|
|
stack and validate the wire surface. Isolated ports, copy-only state, no
|
|
production or staging resource touched."""
|
|
import json
|
|
import os
|
|
import signal
|
|
import subprocess
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
R = "/home/alex/openfut-migration/rehearsal-20260822"
|
|
REPO = "/home/alex/OpenFUT"
|
|
CORE_PORT, HOST_PORT = 18099, 18098
|
|
H = {"X-OpenFUT-Game": "fifa17"}
|
|
procs = []
|
|
|
|
|
|
def start(name, binary, env):
|
|
e = dict(os.environ)
|
|
e.update(env)
|
|
log = open("%s/evidence/%s.log" % (R, name), "w")
|
|
p = subprocess.Popen([binary], cwd=REPO, env=e, stdout=log, stderr=log,
|
|
start_new_session=False)
|
|
procs.append(p)
|
|
return p
|
|
|
|
|
|
def get(port, path):
|
|
req = urllib.request.Request("http://127.0.0.1:%d%s" % (port, path), headers=H)
|
|
with urllib.request.urlopen(req, timeout=25) as r:
|
|
return json.loads(r.read())
|
|
|
|
|
|
try:
|
|
start("rehearsal-core", R + "/artifacts/openfut-core", {
|
|
"LISTEN_ADDR": "127.0.0.1:%d" % CORE_PORT,
|
|
"DATABASE_URL": "sqlite://%s/work/core.db" % R,
|
|
"DATA_DIR": "%s/openfut-core/data" % REPO,
|
|
"OPENFUT_CONTENT_PACKS": "%s/emit/content/fifa17-production-cards.json" % R,
|
|
"RUST_LOG": "warn",
|
|
})
|
|
time.sleep(6)
|
|
start("rehearsal-host", R + "/artifacts/openfut-utas-host", {
|
|
"OPENFUT_UTAS_HOST_ADDR": "127.0.0.1:%d" % HOST_PORT,
|
|
"OPENFUT_UTAS_PYTHON_URL": "http://127.0.0.1:19999", # deliberately dead
|
|
"OPENFUT_CORE_URL": "http://127.0.0.1:%d" % CORE_PORT,
|
|
"OPENFUT_FIFA17_TABLES_DIR": "%s/fifa17-recon/data/tables" % REPO,
|
|
"OPENFUT_FIFA17_CATALOG": "%s/emit/content/fifa17-production-catalog.json" % R,
|
|
"OPENFUT_IDENTITY_STORE": "%s/work/identity.json" % R,
|
|
"OPENFUT_PERSONA_ID": "33068179",
|
|
"OPENFUT_MARKET_DB": "%s/work/market.db" % R,
|
|
"OPENFUT_PILE_DB": "%s/work/pile.db" % R,
|
|
"OPENFUT_FIFA17_DISCARD_TABLE": "1",
|
|
"RUST_LOG": "warn",
|
|
})
|
|
time.sleep(6)
|
|
|
|
checks = {}
|
|
club = get(HOST_PORT, "/ut/game/fifa17/club?type=player&start=0&count=5")
|
|
checks["club_total"] = club.get("totalResults")
|
|
checks["club_first_discard"] = (club.get("itemData") or [{}])[0].get("discardValue")
|
|
checks["club_first_rating"] = (club.get("itemData") or [{}])[0].get("rating")
|
|
|
|
umi = get(HOST_PORT, "/ut/game/fifa17/userMassInfo")
|
|
checks["squad_slots"] = len(((umi.get("userInfo") or {}).get("squad") or
|
|
umi.get("squad") or {}).get("players", []) or [])
|
|
|
|
for cat in ("contracts", "development", "fitness"):
|
|
d = get(HOST_PORT, "/ut/game/fifa17/club/consumables/" + cat)
|
|
st = d.get("itemData") or []
|
|
checks["consumables_" + cat] = (len(st), sum(s.get("count", 0) for s in st))
|
|
|
|
sq = get(HOST_PORT, "/ut/game/fifa17/squad/0")
|
|
checks["squad_players"] = len((sq.get("squad") or sq).get("players") or [])
|
|
|
|
print(json.dumps(checks, indent=2, sort_keys=True))
|
|
|
|
# The consumable apply is Rust-owned and MUTATES, so the rehearsal must not
|
|
# send one that would succeed: a two-target body is refused (400
|
|
# apply_batch_unsupported) before anything is resolved or written. That
|
|
# refusal is only reachable if the candidate host CLAIMS the route -- a 502
|
|
# means it fell through to the (dead) upstream, i.e. the cutover is missing.
|
|
print("\n=== the candidate must OWN the consumable apply ===")
|
|
req = urllib.request.Request(
|
|
"http://127.0.0.1:%d/ut/game/fifa17/item/resource/5001004" % HOST_PORT,
|
|
data=b'{"apply":[{"id":100000003},{"id":100000004}]}',
|
|
headers={"X-OpenFUT-Game": "fifa17", "Content-Type": "application/json"},
|
|
method="POST")
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=20) as r:
|
|
st, body = r.status, r.read().decode()
|
|
except urllib.error.HTTPError as e:
|
|
st, body = e.code, e.read().decode()
|
|
print(" status=%s body=%s" % (st, body))
|
|
print(" apply route Rust-owned, nothing mutated (must be 400 "
|
|
"apply_batch_unsupported): %s"
|
|
% (st == 400 and "apply_batch_unsupported" in body))
|
|
finally:
|
|
for p in procs:
|
|
try:
|
|
os.kill(p.pid, signal.SIGTERM)
|
|
except ProcessLookupError:
|
|
pass
|
|
time.sleep(2)
|
|
for p in procs:
|
|
if p.poll() is None:
|
|
os.kill(p.pid, signal.SIGKILL)
|
|
print("\nrehearsal processes stopped")
|