Files
OpenFUT/fifa17-recon/tools/openfut-fut.sh
T
funman300 5d5198f5d1 fifa17-recon: match rewards, POW online layer, account backend, quick sell
Second session. FUT core loop, the EASFC/POW online layer, a central account
backend, and a lot of corrections. Everything risky is behind an env flag with
the default set to whatever was live-proven.

WORKING END TO END (live-verified this session):
  * match loop -- POST/PUT/POST/DELETE ut/%s/match, rewards via FutDestroyMatch
    (0x180121b60). Play a match, get coins, W/D/L updates.
  * packs -- buy, cards land in the club, session survives (FUT_PACK_AUTOCLUB=1)
  * quick sell -- POST ut/delete/%s/item was UNMAPPED and paid NOTHING; six cards
    were destroyed for 0 coins. Now credits discardValue.
  * POW/EASFC online -- the "EA FC servers unreachable" banner is powdll's layer,
    a THIRD http api on :8094 nobody had served. Redirect needs no root: powdll
    FUN_18005a460 reads FIFA_POW_URL from the same client-config store as
    ROSTERUPDATE_URL. FUT_POW=1.
  * account backend -- fut_account.py replaces 7 hardcoded copies of the persona
    across 5 files; club/persona/online-profile editable via CLI.

CORRECTIONS TO ENDPOINT_MAP (all re-extracted from the deserializers):
  * FutStoreGetPackTypes: id/packType/isPremium/quantity/saleType/purchaseLimit/
    purchaseCount are NOT skipped no-ops -- all are parsed. extPrice inner objects
    take externalPriceId(0x11a), not amount/currency.
  * FutMoveCard 0x180128600 has NO skip handler (FUN_180135ff0 appears zero times,
    unique among FUT deserializers) and parses only itemData -> dreamSquads.
  * class -> deserializer resolution: the name literal is preceded by a 4-BYTE
    HEADER and the factory LEA points at the header, so look up name_addr - 4.
    Six attempts failed on this; now ghidra_env.class_deser(). Unlocked 11 SBC/
    Draft schemas.
  * live-only endpoints the request table never lists: ut/%s/squad/list,
    ut/%s/user/club, ut/%s/club/stats/*, ut/%s/clientdata/<key>. The template
    table is a floor, not a ceiling -- the log is the only ground truth.
  * 163 RS4 call names exist; we served 17. All now served.

FIXED: club/stats/* was answering with the entire 28-item club inventory on every
poll (it fell through to the generic /club route).

UNSOLVED: the pack reveal's "Send to Club" (PUT ut/%s/item) kills the FUT session
whatever we answer -- {} included -- while its sibling quick-sell endpoint accepts
a bare {}. Seven hypotheses eliminated by live test, documented in
REBUILD_RESEARCH.md S14c so none get re-walked. FUT_PACK_AUTOCLUB routes around it.
Also unfixed: store tiles render "unknown" (displayGroup is parsed RECURSIVELY by
the same element parser; sending it FROZE the store, so FUT_STORE_GROUPS=1 is
default off).

Tests: test_fut_contract.py 380 (live, read-only) + test_match_rewards.py 51 (pure).

Note: fut_store.py carries some pre-existing uncommitted changes from before this
session (pack catalogue ids, pending-pile behaviour) that could not be separated
from this session's additions in the same file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
2026-08-04 09:42:59 -07:00

109 lines
4.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# ============================================================================
# OpenFUT — FIFA 17 offline FUT backend orchestrator
#
# ./openfut-fut.sh start bring up the whole harness (arm host + all servers)
# ./openfut-fut.sh stop shut the servers down
# ./openfut-fut.sh restart stop + start
# ./openfut-fut.sh status show what's up
#
# After `start`, launch FIFA 17 and select Ultimate Team. See RUNBOOK below (status).
# Volatile host state (sysctls/iptables/cert) does NOT survive a reboot; `start`
# re-arms everything, so just re-run it after booting. /etc/hosts persists.
# ============================================================================
set -uo pipefail
cd "$(dirname "$(readlink -f "$0")")" # tools/
CERT=redir_cert.pem; KEY=redir_key.pem
# name script "port[,port...]" extra-env
SERVERS=(
"lsx lsx_responder_v2.py 4216 OPENFUT_LSX_EVENT_COUNT=100000"
"blaze blaze_responder_v3b.py 42127,42130,42131 -"
"roster roster_server.py 8081 -"
"utas utas_server.py 8099 -"
"autopatch autopatch.py - -"
# POW/EASFC — the "EA FC servers unreachable" layer. Harmless when idle: it just
# binds 8094/8080 and nothing points at it unless FUT_POW=1 makes blaze serve the
# FIFA_POW_URL redirect keys. See pow_server.py for the powdll evidence.
"pow pow_server.py 8094,8080 -"
)
c() { printf ' %s\n' "$*"; }
up() { ss -tlnp 2>/dev/null | grep -q ":$1 "; }
ensure_cert() {
[ -s "$CERT" ] && [ -s "$KEY" ] && return 0
echo "[*] generating self-signed TLS cert (redirector MITM; ProtoSSL verify is patched)"
openssl req -x509 -newkey rsa:2048 -nodes -keyout "$KEY" -out "$CERT" -days 3650 \
-subj "/CN=winter15.gosredirector.ea.com" \
-addext "subjectAltName=DNS:winter15.gosredirector.ea.com,DNS:*.gosredirector.ea.com,DNS:*.ea.com,IP:127.0.0.1" \
>/dev/null 2>&1
}
armed() {
[ "$(cat /proc/sys/kernel/yama/ptrace_scope 2>/dev/null)" = 0 ] \
&& [ "$(cat /proc/sys/net/ipv4/conf/lo/route_localnet 2>/dev/null)" = 1 ] \
&& grep -q '[[:space:]]easw\.easports\.com\b' /etc/hosts 2>/dev/null
}
ensure_armed() {
if armed; then c "host already armed (ptrace_scope=0, route_localnet=1, /etc/hosts ok)"; return 0; fi
echo "[*] arming host state (needs root — a password dialog will appear)"
pkexec sh "$PWD/root_arm.sh" || { echo "!! root_arm failed. Run manually: pkexec sh $PWD/root_arm.sh"; return 1; }
}
start() {
ensure_cert
ensure_armed || exit 1
echo "[*] starting servers (detached)…"
for s in "${SERVERS[@]}"; do
read -r name script ports env <<<"$s"
pkill -9 -f "$script" 2>/dev/null; :
done
sleep 1
for s in "${SERVERS[@]}"; do
read -r name script ports env <<<"$s"
local envprefix=""; [ "$env" != "-" ] && envprefix="env $env"
: > "/tmp/${name}.log" 2>/dev/null || true
setsid bash -c "exec $envprefix python3 -u $script" </dev/null >"/tmp/${name}.log" 2>&1 &
disown
done
sleep 2
status
}
stop() {
echo "[*] stopping servers…"
for s in "${SERVERS[@]}"; do read -r name script _ <<<"$s"; pkill -9 -f "$script" 2>/dev/null; :; done
sleep 1; c "servers stopped (host arm + /etc/hosts left intact)"
}
status() {
echo "== OpenFUT FIFA17 FUT backend =="
armed && c "host: ARMED ✓" || c "host: NOT armed (run: pkexec sh $PWD/root_arm.sh)"
for s in "${SERVERS[@]}"; do
read -r name script ports env <<<"$s"
if pgrep -f "$script" >/dev/null; then
if [ "$ports" = "-" ]; then c "$name ✓ (running)"
else
local ok=1; IFS=',' read -ra ps <<<"$ports"
for p in "${ps[@]}"; do up "$p" || ok=0; done
[ $ok = 1 ] && c "$name ✓ ($ports)" || c "$name ⚠ running but a port is down ($ports)"
fi
else c "$name ✗ DOWN"; fi
done
echo "-- RUNBOOK --"
c "1. (this must be done BEFORE launching FIFA — servers bind the ports the game needs)"
c "2. Launch FIFA 17 fresh: ~/Desktop/launch-fifa17.sh (a FRESH launch avoids the live-DB error)"
c "3. In-game: select Ultimate Team. At the 'security question', type ANY answer -> Continue -> OK."
c "4. -> the FUT hub. Logs: /tmp/{lsx,blaze,roster,utas,autopatch}.log"
}
case "${1:-status}" in
start) start ;;
stop) stop ;;
restart) stop; start ;;
status) status ;;
*) echo "usage: $0 {start|stop|restart|status}"; exit 1 ;;
esac