From 1daaebaefad5273c619bb0c43d31ddd5d52a0217 Mon Sep 17 00:00:00 2001 From: funman300 Date: Fri, 26 Jun 2026 08:57:23 -0700 Subject: [PATCH] chore: add openfut-launcher submodule, CLAUDE.md, and setup.sh Co-Authored-By: Claude Sonnet 4.6 --- .gitmodules | 3 + CLAUDE.md | 180 ++++++++++++++++++++ openfut-launcher | 1 + setup.sh | 424 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 608 insertions(+) create mode 100644 CLAUDE.md create mode 160000 openfut-launcher create mode 100755 setup.sh diff --git a/.gitmodules b/.gitmodules index a4a54be..a88d297 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "openfut-bridge"] path = openfut-bridge url = https://git.aleshym.co/funman300/OpenFUT-Bridge.git +[submodule "openfut-launcher"] + path = openfut-launcher + url = https://git.aleshym.co/funman300/openfut-launcher.git diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f33a901 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,180 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Layout + +This is a monorepo containing two independent Rust crates as git submodules: + +| Submodule | Role | +|---|---| +| `openfut-core/` | Game-independent offline FUT backend (Axum + SQLite) | +| `openfut-bridge/` | FIFA 23 integration layer and reverse-engineering proxy | + +Each submodule has its own `Cargo.toml`, `src/`, `tests/`, and `target/`. Commands must be run from within the submodule directory or with `--manifest-path`. + +## Common Commands + +All commands run from within the relevant submodule directory. + +### openfut-core + +```bash +# Run (dev mode; creates openfut.db in current dir) +cargo run + +# Run (release) +cargo build --release && ./target/release/openfut-core + +# Test (full integration suite against in-memory SQLite) +cargo test + +# Run a single test +cargo test test_health_endpoint + +# Lint +cargo clippy -- -D warnings + +# Format +cargo fmt +``` + +### openfut-bridge + +```bash +# Run (Core must be running first on port 8080) +cargo run + +# Test +cargo test + +# Run a single test +cargo test test_bridge_health_endpoint + +# Lint / format same as core +cargo clippy -- -D warnings +cargo fmt +``` + +### Full-stack via setup.sh (Linux, from repo root) + +```bash +./setup.sh quickstart # build + start + hosts + NAT + TLS cert (first-time) +./setup.sh start # start both services in background +./setup.sh stop # stop both + clean up hosts/NAT +./setup.sh status # show running state +./setup.sh logs # tail both logs +./setup.sh watch-domains # find unknown EA domains hitting the bridge +``` + +### Environment variables + +**Core** (`openfut-core/`): + +| Variable | Default | +|---|---| +| `LISTEN_ADDR` | `127.0.0.1:8080` | +| `DATABASE_URL` | `sqlite://openfut.db` | +| `DATA_DIR` | `data` | + +**Bridge** (`openfut-bridge/`): + +| Variable | Default | +|---|---| +| `BRIDGE_LISTEN_ADDR` | `127.0.0.1:8443` | +| `CORE_URL` | `http://127.0.0.1:8080` | +| `CAPTURES_DIR` | `captures` | +| `PLACEHOLDER_MODE` | `true` | +| `TLS_ENABLED` | `true` | + +## Architecture + +``` +FIFA 23 client + │ + ▼ +openfut-bridge (port 8443) + │ + ├── Known route → openfut-core (port 8080) + └── Unknown route → placeholder JSON + saved to captures/ + │ + SQLite database + │ + data/ (JSON) +``` + +**Core** has zero knowledge of FIFA 23. It exposes a clean REST API and stores everything in SQLite. **Bridge** has zero game logic — it only translates FIFA 23's wire protocol into Core API calls. + +## openfut-core internals + +**Module map:** + +| Path | Purpose | +|---|---| +| `src/main.rs` | Entry point: tracing, config, pool, migrations, seed, serve | +| `src/lib.rs` | `build_app(pool, data_dir)` — used by integration tests | +| `src/app.rs` | Router construction, `AppState` definition | +| `src/config.rs` | `Config` struct from env vars | +| `src/db.rs` | Pool init + migration runner | +| `src/error.rs` | `AppError` enum + `IntoResponse` impl | +| `src/models/` | Pure data types (`Serde` + SQLx `FromRow`) | +| `src/services/` | Business logic; all DB access lives here | +| `src/routes/` | Axum handler functions; one file per domain | +| `src/seed/` | First-run starter pack grant | +| `src/modding/` | Generic JSON directory loader | +| `data/` | Moddable JSON content: cards, packs, objectives, SBCs, events | +| `migrations/` | SQLx SQL migrations (numbered sequentially) | + +**`AppState`** is cloned into every handler via `State`. It holds the SQLite pool and Arc-wrapped in-memory registries loaded at startup from `data/` (`CardDb`, `Vec`, etc.). + +**Layer discipline:** routes extract state and call services; services own all DB access and data-loading logic; models are pure data. + +**Single-profile design:** Only one profile per database. All services fetch "the active profile" by selecting the first row — this is intentional. + +## openfut-bridge internals + +**Module map:** + +| Path | Purpose | +|---|---| +| `src/main.rs` | Entry point: config, TLS setup, serve | +| `src/lib.rs` | Library root — re-exports modules | +| `src/config.rs` | `Config` struct | +| `src/proxy.rs` | `catch_all_handler` — the central routing brain | +| `src/mapper.rs` | `map_to_core(method, path)` — FIFA 23 → Core path translation | +| `src/capture.rs` | `CapturedRequest` struct + disk serialization | +| `src/shaper.rs` | Response body transformation (Core → FIFA 23 wire format) | +| `src/tls.rs` | Self-signed cert generation + rustls acceptor | +| `src/routes/` | Bridge admin endpoints (`/_bridge/*`) | +| `captures/` | JSON files written for every intercepted request | + +**Request flow:** all traffic hits `catch_all_handler` → `mapper::map_to_core()` → if known, proxy to Core; if unknown, return placeholder JSON and write to `captures/`. + +**Adding a new FIFA 23 endpoint:** +1. Capture it in `captures/` or `/_bridge/unknown` +2. Document it in `docs/endpoint-map.md` +3. Add a mapping in `src/mapper.rs` (`EXACT` array or `prefix_routes()`) +4. Implement the handler in Core if needed + +## Testing approach + +**Core** uses a single `tests/integration_test.rs` that spins up a full Axum app against an in-memory SQLite database (`build_app(pool, "data")`). Tests use raw HTTP via `tower::ServiceExt::oneshot`. The `data/` directory is always required at test time (tests run from `openfut-core/`). + +**Bridge** tests are in `tests/proxy_test.rs`. They build a bridge app in `placeholder_mode: true` pointing at a dummy Core URL — no live Core required. + +## Modding / data files + +All game content lives in `openfut-core/data/`. Files are JSON arrays loaded at startup. To add content, drop a JSON file into the right subdirectory and restart Core: + +``` +data/cards/ ← CardDefinition[] +data/packs/ ← PackDefinition[] +data/objectives/ ← ObjectiveDefinition[] +data/sbcs/ ← SbcDefinition[] +data/events/ ← EventDefinition[] +data/achievements/← AchievementDefinition[] +``` + +## Version control + +Both `openfut-core` and `openfut-bridge` are git submodules with their own independent history. Use `tea` (Gitea CLI) for remote operations, not `gh`. diff --git a/openfut-launcher b/openfut-launcher new file mode 160000 index 0000000..099241d --- /dev/null +++ b/openfut-launcher @@ -0,0 +1 @@ +Subproject commit 099241d9cad471bfc31fe2b9cd53dd8f65bd3d2b diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..cc459c0 --- /dev/null +++ b/setup.sh @@ -0,0 +1,424 @@ +#!/usr/bin/env bash +# OpenFUT Setup Script +# Builds, configures, and starts the OpenFUT offline FUT emulator for FIFA 23. +# Run as a regular user; the script will sudo only for hosts/cert/iptables steps. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CORE_DIR="$SCRIPT_DIR/openfut-core" +BRIDGE_DIR="$SCRIPT_DIR/openfut-bridge" + +CORE_PORT="${CORE_PORT:-8080}" +BRIDGE_PORT="${BRIDGE_PORT:-8443}" # internal port; iptables forwards 443 → this +TLS_ENABLED="${TLS_ENABLED:-true}" + +CORE_PID_FILE="/tmp/openfut-core.pid" +BRIDGE_PID_FILE="/tmp/openfut-bridge.pid" +CORE_LOG="/tmp/openfut-core.log" +BRIDGE_LOG="/tmp/openfut-bridge.log" + +# EA FUT domains to redirect. +# Run './setup.sh watch-domains' while starting FIFA 23 to discover additional domains. +EA_HOSTS=( + # Core FUT API + "fut.ea.com" + "utas.mob.v4.fut.ea.com" + # EA account / auth + "accounts.ea.com" + "signin.ea.com" + "gateway.ea.com" + # Origin / EA Desktop API + "api1.origin.com" + "api2.origin.com" + "api3.origin.com" + # Telemetry / assets (can safely be silenced) + "eas.ea.com" + "eaassets-a.akamaihd.net" + "eaassets-b.akamaihd.net" +) +HOSTS_MARKER="# OpenFUT — managed block (do not edit manually)" + +# ── Colours ──────────────────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' + +info() { echo -e "${CYAN}[INFO]${RESET} $*"; } +ok() { echo -e "${GREEN}[OK]${RESET} $*"; } +warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } +error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; } +heading() { echo -e "\n${BOLD}$*${RESET}"; } + +# ── Helpers ──────────────────────────────────────────────────────────────────── +require_cmd() { + command -v "$1" &>/dev/null || { error "Required command not found: $1"; exit 1; } +} + +is_running() { + local pid_file="$1" + [[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file")" 2>/dev/null +} + +stop_service() { + local pid_file="$1" name="$2" + if is_running "$pid_file"; then + kill "$(cat "$pid_file")" 2>/dev/null && ok "Stopped $name" + rm -f "$pid_file" + else + warn "$name is not running" + fi +} + +# ── Commands ─────────────────────────────────────────────────────────────────── + +cmd_build() { + heading "Building OpenFUT Core and Bridge…" + require_cmd cargo + + info "Building Core (release)…" + cargo build --release --manifest-path "$CORE_DIR/Cargo.toml" \ + 2>&1 | grep -E "^(error|warning\[)" || true + ok "Core built" + + info "Building Bridge (release)…" + cargo build --release --manifest-path "$BRIDGE_DIR/Cargo.toml" \ + 2>&1 | grep -E "^(error|warning\[)" || true + ok "Bridge built" +} + +cmd_start() { + heading "Starting OpenFUT services…" + + # ── Core ── + if is_running "$CORE_PID_FILE"; then + warn "Core is already running (PID $(cat "$CORE_PID_FILE"))" + else + # Run from CORE_DIR so the default sqlite://openfut.db relative path works + ( + cd "$CORE_DIR" + LISTEN_ADDR="127.0.0.1:$CORE_PORT" \ + DATA_DIR="$CORE_DIR/data" \ + "$CORE_DIR/target/release/openfut-core" \ + >"$CORE_LOG" 2>&1 + ) & + echo $! > "$CORE_PID_FILE" + ok "Core started (PID $!) → http://127.0.0.1:$CORE_PORT log: $CORE_LOG" + fi + + # Give Core a moment to bind before Bridge starts + sleep 1 + + # ── Bridge ── + if is_running "$BRIDGE_PID_FILE"; then + warn "Bridge is already running (PID $(cat "$BRIDGE_PID_FILE"))" + else + BRIDGE_LISTEN_ADDR="0.0.0.0:$BRIDGE_PORT" \ + CORE_URL="http://127.0.0.1:$CORE_PORT" \ + TLS_ENABLED="$TLS_ENABLED" \ + PLACEHOLDER_MODE="true" \ + CAPTURES_DIR="$BRIDGE_DIR/captures" \ + "$BRIDGE_DIR/target/release/openfut-bridge" \ + >"$BRIDGE_LOG" 2>&1 & + echo $! > "$BRIDGE_PID_FILE" + local scheme="http"; [[ "$TLS_ENABLED" == "true" ]] && scheme="https" + ok "Bridge started (PID $!) → ${scheme}://0.0.0.0:$BRIDGE_PORT log: $BRIDGE_LOG" + fi + + echo + info "Dashboard: http://127.0.0.1:$CORE_PORT/_bridge/dashboard (via bridge: https://127.0.0.1:$BRIDGE_PORT/_bridge/dashboard)" + info "Admin: https://127.0.0.1:$BRIDGE_PORT/_bridge/admin" + info "Setup guide: https://127.0.0.1:$BRIDGE_PORT/_bridge/guide" +} + +cmd_stop() { + heading "Stopping OpenFUT services…" + stop_service "$BRIDGE_PID_FILE" "Bridge" + stop_service "$CORE_PID_FILE" "Core" + cmd_unhosts + cmd_unnat +} + +cmd_status() { + heading "OpenFUT service status" + if is_running "$CORE_PID_FILE"; then + ok "Core is RUNNING (PID $(cat "$CORE_PID_FILE"))" + else + warn "Core is STOPPED" + fi + if is_running "$BRIDGE_PID_FILE"; then + ok "Bridge is RUNNING (PID $(cat "$BRIDGE_PID_FILE"))" + else + warn "Bridge is STOPPED" + fi + + echo + if grep -q "$HOSTS_MARKER" /etc/hosts 2>/dev/null; then + ok "Hosts entries are IN PLACE" + else + warn "Hosts entries are NOT configured (run: ./setup.sh hosts)" + fi + + if sudo iptables -t nat -L OUTPUT -n 2>/dev/null | grep -q "redir ports $BRIDGE_PORT"; then + ok "NAT redirect 443→$BRIDGE_PORT is ACTIVE" + else + warn "NAT redirect is NOT active (run: ./setup.sh nat)" + fi +} + +cmd_logs() { + heading "Tailing logs (Ctrl-C to stop)…" + tail -f "$CORE_LOG" "$BRIDGE_LOG" 2>/dev/null || warn "No log files yet — start the services first." +} + +cmd_hosts() { + heading "Adding EA FUT hosts entries…" + if grep -q "$HOSTS_MARKER" /etc/hosts 2>/dev/null; then + ok "Hosts entries already present — skipping" + return + fi + + local block + block="\n$HOSTS_MARKER\n" + for host in "${EA_HOSTS[@]}"; do + block+="127.0.0.1 $host\n" + done + block+="# end OpenFUT\n" + + echo -e "$block" | sudo tee -a /etc/hosts > /dev/null + ok "Added to /etc/hosts:" + for host in "${EA_HOSTS[@]}"; do + echo " 127.0.0.1 $host" + done + info "Flush DNS if needed: sudo resolvectl flush-caches (or: sudo systemd-resolve --flush-caches)" +} + +cmd_unhosts() { + if grep -q "$HOSTS_MARKER" /etc/hosts 2>/dev/null; then + sudo sed -i "/^$HOSTS_MARKER/,/^# end OpenFUT/d" /etc/hosts + ok "Removed OpenFUT hosts entries" + fi +} + +cmd_nat() { + heading "Setting up iptables redirect 443 → $BRIDGE_PORT…" + if sudo iptables -t nat -L OUTPUT -n 2>/dev/null | grep -q "redir ports $BRIDGE_PORT"; then + ok "NAT rule already active" + return + fi + sudo iptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to-port "$BRIDGE_PORT" + ok "iptables: OUTPUT 443 → $BRIDGE_PORT" + warn "This rule is NOT persistent across reboots. Re-run './setup.sh nat' after reboot or use iptables-save." +} + +cmd_unnat() { + if sudo iptables -t nat -L OUTPUT -n 2>/dev/null | grep -q "redir ports $BRIDGE_PORT"; then + sudo iptables -t nat -D OUTPUT -p tcp --dport 443 -j REDIRECT --to-port "$BRIDGE_PORT" + ok "Removed iptables NAT rule" + fi +} + +cmd_cert() { + heading "Installing TLS certificate…" + + local scheme="https" + local cert_url="${scheme}://127.0.0.1:$BRIDGE_PORT/_bridge/cert.pem" + local cert_dest="/tmp/openfut-ca.pem" + + if ! is_running "$BRIDGE_PID_FILE"; then + error "Bridge is not running. Start it first with './setup.sh start'" + exit 1 + fi + + info "Downloading certificate from $cert_url …" + curl -sk "$cert_url" -o "$cert_dest" || { error "Failed to download cert. Is the Bridge running with TLS_ENABLED=true?"; exit 1; } + + if [[ ! -s "$cert_dest" ]]; then + error "Downloaded cert is empty." + exit 1 + fi + + ok "Certificate saved to $cert_dest" + + # Detect distro cert store + if command -v update-ca-trust &>/dev/null; then + # Arch / Fedora / RHEL + sudo cp "$cert_dest" /etc/ca-certificates/trust-source/anchors/openfut-ca.pem + sudo update-ca-trust + ok "Certificate installed (update-ca-trust)" + elif command -v update-ca-certificates &>/dev/null; then + # Debian / Ubuntu + sudo cp "$cert_dest" /usr/local/share/ca-certificates/openfut-ca.crt + sudo update-ca-certificates + ok "Certificate installed (update-ca-certificates)" + else + warn "Cannot auto-install cert — manual steps:" + echo " Copy $cert_dest to your system's trusted CA store." + fi + + info "If using Steam/Proton, you may also need to add it to the game's certificate store." + info "Wine cert path (Proton): WINEDEBUG=-all wine certutil -addstore Root $cert_dest" +} + +cmd_proton_cert() { + heading "Installing certificate into Proton / Wine (for Steam FIFA 23)…" + + local cert_dest="/tmp/openfut-ca.pem" + if [[ ! -f "$cert_dest" ]]; then + error "No cert found at $cert_dest — run './setup.sh cert' first" + exit 1 + fi + + # Find Proton wine binary + local proton_wine + proton_wine=$(find "$HOME/.steam/steam/steamapps/common" -name "wine" -path "*/Proton*/wine" 2>/dev/null | head -1) + + if [[ -z "$proton_wine" ]]; then + warn "Could not auto-detect Proton wine binary." + echo " Manually run: wine certutil -addstore Root $cert_dest" + echo " with the correct WINEPREFIX for FIFA 23." + return + fi + + # Find FIFA 23 Proton prefix + local steam_id_fifa23="1811260" + local prefix="$HOME/.steam/steam/steamapps/compatdata/$steam_id_fifa23/pfx" + + if [[ ! -d "$prefix" ]]; then + warn "FIFA 23 Proton prefix not found at $prefix" + echo " Launch FIFA 23 at least once to create the prefix, then re-run this step." + return + fi + + WINEPREFIX="$prefix" WINEDEBUG=-all "$proton_wine" certutil -addstore Root "$cert_dest" \ + && ok "Certificate added to FIFA 23 Proton prefix" \ + || warn "certutil failed — you may need to add it manually." +} + +cmd_quickstart() { + heading "OpenFUT Quick Start" + echo "This will: build → start services → add hosts entries → set up NAT → install cert" + echo + read -rp "Continue? [y/N] " confirm + [[ "${confirm,,}" == "y" ]] || { info "Aborted."; exit 0; } + + cmd_build + cmd_start + + info "Waiting for services to initialise…" + sleep 3 + + # Verify Core is actually up before proceeding + if ! is_running "$CORE_PID_FILE"; then + error "Core failed to start. Check the log:" + tail -20 "$CORE_LOG" + exit 1 + fi + if ! is_running "$BRIDGE_PID_FILE"; then + error "Bridge failed to start. Check the log:" + tail -20 "$BRIDGE_LOG" + exit 1 + fi + + # sudo-gated steps — try them, print manual instructions if sudo not available + echo + heading "System configuration (requires sudo)…" + + if sudo -n true 2>/dev/null; then + cmd_hosts + cmd_nat + sleep 1 + cmd_cert + cmd_proton_cert + else + warn "Cannot acquire sudo non-interactively. Run these steps yourself:" + echo + echo " 1. sudo ./setup.sh hosts" + echo " 2. sudo ./setup.sh nat" + echo " 3. ./setup.sh cert (after the above two)" + echo " 4. ./setup.sh proton-cert" + fi + + echo + heading "Setup complete!" + ok "FIFA 23 FUT traffic will now route to OpenFUT." + echo + echo -e " Dashboard: ${CYAN}http://127.0.0.1:$CORE_PORT/_bridge/dashboard${RESET}" + echo -e " Admin panel: ${CYAN}https://127.0.0.1:$BRIDGE_PORT/_bridge/admin${RESET}" + echo -e " Logs: ${CYAN}./setup.sh logs${RESET}" + echo -e " Stop all: ${CYAN}./setup.sh stop${RESET}" +} + +cmd_watch_domains() { + heading "Watching for unknown EA domains (Ctrl-C to stop)…" + if ! is_running "$BRIDGE_PID_FILE"; then + warn "Bridge is not running. Start it first with './setup.sh start'" + exit 1 + fi + info "Launch FIFA 23 now. Any domain that reaches the real EA servers will appear here." + echo + # Watch the bridge log for lines that contain host/domain info not already in our list + tail -f "$BRIDGE_LOG" 2>/dev/null | grep --line-buffered -i "unknown\|placeholder\|host\|domain\|CONNECT" \ + | grep -v --line-buffered "$(IFS='|'; echo "${EA_HOSTS[*]}")" \ + | while IFS= read -r line; do + echo -e "${YELLOW}$(date +'%H:%M:%S')${RESET} $line" + done +} + +cmd_help() { + cat < + +${BOLD}Commands:${RESET} + quickstart Full setup in one step (build + start + hosts + nat + cert) + build Compile Core and Bridge in release mode + start Start Core and Bridge in the background + stop Stop both services and clean up hosts/NAT + restart Stop then start + status Show running state, hosts, and NAT status + logs Tail live logs from both services + watch-domains Show unknown EA domains hitting the bridge (add them to hosts) + + hosts Add EA FUT domains to /etc/hosts (requires sudo) + unhosts Remove OpenFUT entries from /etc/hosts + nat Add iptables redirect 443 → $BRIDGE_PORT (requires sudo) + unnat Remove iptables NAT rule + + cert Download TLS cert from Bridge and install system-wide + proton-cert Install TLS cert into the FIFA 23 Proton/Wine prefix + +${BOLD}Environment overrides:${RESET} + CORE_PORT Core listen port (default: 8080) + BRIDGE_PORT Bridge internal port (default: 8443) + TLS_ENABLED Enable HTTPS on Bridge (default: true) + +${BOLD}Example:${RESET} + ./setup.sh quickstart # first-time full setup + ./setup.sh start # start after a reboot + ./setup.sh stop # clean shutdown + +EOF +} + +# ── Dispatch ─────────────────────────────────────────────────────────────────── +case "${1:-help}" in + quickstart) cmd_quickstart ;; + build) cmd_build ;; + start) cmd_start ;; + stop) cmd_stop ;; + restart) cmd_stop; cmd_start ;; + status) cmd_status ;; + logs) cmd_logs ;; + hosts) cmd_hosts ;; + unhosts) cmd_unhosts ;; + nat) cmd_nat ;; + unnat) cmd_unnat ;; + cert) cmd_cert ;; + proton-cert) cmd_proton_cert ;; + watch-domains) cmd_watch_domains ;; + help|--help|-h) cmd_help ;; + *) error "Unknown command: $1"; cmd_help; exit 1 ;; +esac