docs: add FLE bridge direction pivot and squad-injection test tooling
Adds the app-centric FLE bridge direction (direction.md) replacing the Blaze-backend route as primary plan, plus a corrected foundational test procedure and snapshot/diff/apply tooling for reverse-engineering FLE's Freeze Lineup write mechanism. Also picks up prior untracked research docs (status-review, fut-integration-options, fifa23-startup-flow, track-c) and existing capture/export tooling that hadn't been committed yet.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# snapshot: baseline 2026-06-26T21:44:51Z
|
||||
2023-06-02 22:13:13 280984156 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentaryfull_eng_us_ds/cas_02.cas
|
||||
2023-06-02 22:13:13 52532092 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentaryfull_eng_us_ds/cas_03.cas
|
||||
2023-06-02 22:20:08 768293618 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_07/cas_02.cas
|
||||
2023-06-02 22:20:18 728251988 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_07/cas_03.cas
|
||||
2023-06-02 22:20:37 1074835 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_spa_es/cas_01.cas
|
||||
2023-06-02 22:20:37 1471597 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_ita_it/cas_01.cas
|
||||
2023-06-02 22:20:37 1670951 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_spa_mx/cas_01.cas
|
||||
2023-06-02 22:20:37 2184167 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_rus_ru/cas_01.cas
|
||||
2023-06-02 22:20:37 2750298 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_ger_de/cas_01.cas
|
||||
2023-06-02 22:20:37 275617825 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentaryfull_eng_us_ds/cas_01.cas
|
||||
2023-06-02 22:20:37 34794805 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_jpn_jp/cas_01.cas
|
||||
2023-06-02 22:20:37 5583406 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_eng_us_ds/cas_01.cas
|
||||
2023-06-02 22:20:37 976736 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_commentarylaunch_chs_cn/cas_01.cas
|
||||
2023-06-02 22:20:38 5481 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_04/cas_01.cas
|
||||
2023-06-03 00:25:53 11768 /mnt/games/FIFA 23/Patch/Win32/commentarywc_fre_fr.toc
|
||||
2023-06-03 00:25:53 1224 /mnt/games/FIFA 23/Patch/Win32/chants_install.toc
|
||||
2023-06-03 00:25:53 125583 /mnt/games/FIFA 23/Patch/initfs_Win32
|
||||
2023-06-03 00:25:53 12570 /mnt/games/FIFA 23/Patch/Win32/commentarywc_ita_it.toc
|
||||
2023-06-03 00:25:53 12624 /mnt/games/FIFA 23/Patch/Win32/commentarywc_chs_cn.toc
|
||||
2023-06-03 00:25:53 12624 /mnt/games/FIFA 23/Patch/Win32/commentarywc_dut_nl.toc
|
||||
2023-06-03 00:25:53 12624 /mnt/games/FIFA 23/Patch/Win32/commentarywc_por_br.toc
|
||||
2023-06-03 00:25:53 12624 /mnt/games/FIFA 23/Patch/Win32/commentarywc_rus_ru.toc
|
||||
2023-06-03 00:25:53 12632 /mnt/games/FIFA 23/Patch/Win32/commentarywc_ger_de.toc
|
||||
2023-06-03 00:25:53 12678 /mnt/games/FIFA 23/Patch/Win32/commentarywc_ara_sa.toc
|
||||
2023-06-03 00:25:53 12678 /mnt/games/FIFA 23/Patch/Win32/commentarywc_pol_pl.toc
|
||||
2023-06-03 00:25:53 12678 /mnt/games/FIFA 23/Patch/Win32/commentarywc_spa_es.toc
|
||||
2023-06-03 00:25:53 12686 /mnt/games/FIFA 23/Patch/Win32/commentarywc_spa_mx.toc
|
||||
2023-06-03 00:25:53 12796 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_chs_cn.toc
|
||||
2023-06-03 00:25:53 14470 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_spa_es.toc
|
||||
2023-06-03 00:25:53 16044 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_spa_mx.toc
|
||||
2023-06-03 00:25:53 16440 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_ger_de.toc
|
||||
2023-06-03 00:25:53 17494 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_ita_it.toc
|
||||
2023-06-03 00:25:53 17890 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_rus_ru.toc
|
||||
2023-06-03 00:25:53 22910 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_jpn_jp.toc
|
||||
2023-06-03 00:25:53 24752 /mnt/games/FIFA 23/Patch/Win32/commentarylaunch_eng_us_ds.toc
|
||||
2023-06-03 00:25:53 2966 /mnt/games/FIFA 23/Patch/Win32/commentarywc_eng_us_ds.toc
|
||||
2023-06-03 00:25:53 45092 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_fre_fr.toc
|
||||
2023-06-03 00:25:53 46980 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_eng_us_ds.toc
|
||||
2023-06-03 00:25:53 49502 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_pol_pl.toc
|
||||
2023-06-03 00:25:53 53840 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_ita_it.toc
|
||||
2023-06-03 00:25:53 59752 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_jpn_jp.toc
|
||||
2023-06-03 00:25:53 59878 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_ger_de.toc
|
||||
2023-06-03 00:25:53 59878 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_spa_mx.toc
|
||||
2023-06-03 00:25:53 61238 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_rus_ru.toc
|
||||
2023-06-03 00:25:53 61940 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_por_br.toc
|
||||
2023-06-03 00:25:53 63236 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_chs_cn.toc
|
||||
2023-06-03 00:25:53 63236 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_spa_es.toc
|
||||
2023-06-03 00:25:53 64802 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_ara_sa.toc
|
||||
2023-06-03 00:25:53 65224 /mnt/games/FIFA 23/Patch/Win32/commentaryfull_dut_nl.toc
|
||||
2023-06-03 00:25:53 928 /mnt/games/FIFA 23/Patch/Win32/commentarywc_jpn_jp.toc
|
||||
2023-08-22 15:21:19 110383 /mnt/games/FIFA 23/Patch/layout.toc
|
||||
2023-08-22 15:21:19 1818 /mnt/games/FIFA 23/Patch/Win32/chants_full.toc
|
||||
2023-08-22 15:21:19 2321 /mnt/games/FIFA 23/Patch/Win32/matchcinematicssba.toc
|
||||
2023-08-22 15:21:19 263983 /mnt/games/FIFA 23/Patch/Win32/storycharsb.toc
|
||||
2023-08-22 15:21:20 129098 /mnt/games/FIFA 23/Patch/Win32/careermodestorysba.toc
|
||||
2023-08-22 15:21:20 1459 /mnt/games/FIFA 23/Patch/Win32/storyingamesba.toc
|
||||
2023-08-22 15:21:20 1520 /mnt/games/FIFA 23/Patch/Win32/streaminginstallcinematicsbundle.toc
|
||||
2023-08-22 15:21:20 1695 /mnt/games/FIFA 23/Patch/Win32/globalswc.toc
|
||||
2023-08-22 15:21:20 1894949 /mnt/games/FIFA 23/Patch/Win32/contentlaunchsb.toc
|
||||
2023-08-22 15:21:20 191908 /mnt/games/FIFA 23/Patch/Win32/worldcupsb.toc
|
||||
2023-08-22 15:21:20 3052093 /mnt/games/FIFA 23/Patch/Win32/worldssb.toc
|
||||
2023-08-22 15:21:20 3788 /mnt/games/FIFA 23/Patch/Win32/futsb.toc
|
||||
2023-08-22 15:21:20 440453 /mnt/games/FIFA 23/Patch/Win32/presprematchsba.toc
|
||||
2023-08-22 15:21:20 4886 /mnt/games/FIFA 23/Patch/Win32/presentationsb.toc
|
||||
2023-08-22 15:21:20 531911 /mnt/games/FIFA 23/Patch/Win32/globalsfull.toc
|
||||
2023-08-22 15:21:20 548511 /mnt/games/FIFA 23/Patch/Win32/careersba.toc
|
||||
2023-08-22 15:21:20 7271 /mnt/games/FIFA 23/Patch/Win32/marketingsba.toc
|
||||
2023-08-22 15:21:20 7360676 /mnt/games/FIFA 23/Patch/Win32/contentsb.toc
|
||||
2023-08-22 15:21:21 24476 /mnt/games/FIFA 23/Patch/Win32/choreosb.toc
|
||||
2023-08-22 15:21:21 268514476 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_07.cas
|
||||
2023-08-22 15:21:21 58624 /mnt/games/FIFA 23/Patch/Win32/storysba.toc
|
||||
2023-08-22 15:21:21 6144 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_01/cas_01.cas
|
||||
2023-08-22 15:21:21 760492 /mnt/games/FIFA 23/Patch/Win32/globals.toc
|
||||
2023-08-22 15:21:23 103085062 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_09.cas
|
||||
2023-08-22 15:21:23 268483345 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_05.cas
|
||||
2023-08-22 15:21:24 268448487 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_08.cas
|
||||
2023-08-22 15:21:24 268563472 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_02.cas
|
||||
2023-08-22 15:21:26 268663563 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_03.cas
|
||||
2023-08-22 15:21:27 268440522 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_04.cas
|
||||
2023-08-22 15:21:28 268597122 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_01.cas
|
||||
2023-08-22 15:21:29 268587757 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_06/cas_06.cas
|
||||
2023-08-22 15:21:30 234374397 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_02/cas_02.cas
|
||||
2023-08-22 15:21:31 410809408 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_02/cas_01.cas
|
||||
2023-08-22 15:21:33 491728234 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_08/cas_01.cas
|
||||
2023-08-22 15:21:36 287271203 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_05/cas_02.cas
|
||||
2023-08-22 15:21:37 268438709 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_05/cas_01.cas
|
||||
2023-08-22 15:21:38 271000144 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_05/cas_03.cas
|
||||
2023-08-22 15:21:44 236583358 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_05/cas_04.cas
|
||||
2023-08-22 15:21:46 492908031 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_07/cas_01.cas
|
||||
2023-08-22 15:21:52 270993339 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_00/cas_04.cas
|
||||
2023-08-22 15:21:58 273177012 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_00/cas_03.cas
|
||||
2023-08-22 15:22:02 99254970 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_00/cas_06.cas
|
||||
2023-08-22 15:22:05 270041465 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_00/cas_01.cas
|
||||
2023-08-22 15:22:12 268490780 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_00/cas_02.cas
|
||||
2023-08-22 15:22:18 295744650 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_00/cas_05.cas
|
||||
2023-08-22 15:22:19 270176162 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_03/cas_01.cas
|
||||
2023-08-22 15:22:20 118305 /mnt/games/FIFA 23/Patch/Win32/fifa/fifagame/fifagame.toc
|
||||
2023-08-22 15:22:20 180049441 /mnt/games/FIFA 23/Patch/Win32/superbundlelayout/fifa_installpackage_03/cas_02.cas
|
||||
2026-06-23 15:41:54 1536026 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/ProfileOptions
|
||||
2026-06-23 15:41:54 1 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/ProfileOptions_profile
|
||||
2026-06-23 15:42:00 0 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/filesystemcache/survey.state
|
||||
2026-06-23 15:48:11 16272228 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/Career20260623154811
|
||||
2026-06-24 17:06:53 749 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/overrideAutodetect.lua
|
||||
2026-06-24 17:07:05 304 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/fifasetup.ini
|
||||
2026-06-24 17:16:24 16272228 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/Career20260624171624A
|
||||
2026-06-24 17:16:39 16272228 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/Career20260624171639A
|
||||
2026-06-24 17:33:42 515334 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/Settings20260624173342
|
||||
2026-06-24 17:33:43 16272228 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/settings/Career20260624173343A
|
||||
2026-06-24 17:44:25 189 /home/alex/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23/filesystemcache/atlPlayTimeJson/playtime_2143594531.json
|
||||
2026-06-24 19:35:05 8499 /mnt/games/FIFA 23/Logs/log_launcher_24-06-2026.txt
|
||||
2026-06-25 19:26:02 45957 /mnt/games/FIFA 23/Logs/log_launcher_25-06-2026.txt
|
||||
2026-06-25 19:26:24 49997 /mnt/games/FIFA 23/Logs/log_25-06-2026.txt
|
||||
2026-06-26 14:38:44 251783 /mnt/games/FIFA 23/Logs/log_launcher_26-06-2026.txt
|
||||
2026-06-26 14:38:59 281281 /mnt/games/FIFA 23/Logs/log_26-06-2026.txt
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bash
|
||||
# file-watch-diff/watch.sh
|
||||
# Snapshots FIFA 23 local file state and diffs it between labelled game phases.
|
||||
# Usage: watch.sh snapshot <label>
|
||||
# watch.sh diff <label-a> <label-b>
|
||||
# watch.sh watch # live tail of changes (requires inotifywait)
|
||||
#
|
||||
# Labels: baseline | main-menu | entered-fut | squad-change | post-match
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SNAP_DIR="$(cd "$(dirname "$0")" && pwd)/snapshots"
|
||||
mkdir -p "$SNAP_DIR"
|
||||
|
||||
# All directories that FIFA 23 writes to locally under Wine/Proton
|
||||
WATCH_PATHS=(
|
||||
"$HOME/.local/share/Steam/steamapps/compatdata/3887260930/pfx/drive_c/users/steamuser/Documents/FIFA 23"
|
||||
"$HOME/Games/umu/fifa23-tools/pfx/drive_c/users/steamuser/Documents/FIFA 23"
|
||||
"/mnt/games/FIFA 23/Logs"
|
||||
"/mnt/games/FIFA 23/Patch"
|
||||
)
|
||||
|
||||
# Build a listing (path + size + mtime) of every file under watched dirs
|
||||
snapshot() {
|
||||
local label="$1"
|
||||
local out="$SNAP_DIR/${label}.txt"
|
||||
echo "# snapshot: $label $(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$out"
|
||||
for d in "${WATCH_PATHS[@]}"; do
|
||||
[ -d "$d" ] || continue
|
||||
find "$d" -type f -printf '%TY-%Tm-%Td %TH:%TM:%.2TS %7s %p\n' 2>/dev/null
|
||||
done | sort >> "$out"
|
||||
echo "Snapshot '$label' written to $out"
|
||||
wc -l "$out"
|
||||
}
|
||||
|
||||
# Diff two snapshots by path+size+mtime
|
||||
diff_snapshots() {
|
||||
local a="$SNAP_DIR/${1}.txt"
|
||||
local b="$SNAP_DIR/${2}.txt"
|
||||
[ -f "$a" ] || { echo "Missing snapshot: $1"; exit 1; }
|
||||
[ -f "$b" ] || { echo "Missing snapshot: $2"; exit 1; }
|
||||
|
||||
echo "=== Files added between '$1' → '$2' ==="
|
||||
comm -13 <(grep -v '^#' "$a" | awk '{print $NF}' | sort) \
|
||||
<(grep -v '^#' "$b" | awk '{print $NF}' | sort)
|
||||
|
||||
echo ""
|
||||
echo "=== Files removed between '$1' → '$2' ==="
|
||||
comm -23 <(grep -v '^#' "$a" | awk '{print $NF}' | sort) \
|
||||
<(grep -v '^#' "$b" | awk '{print $NF}' | sort)
|
||||
|
||||
echo ""
|
||||
echo "=== Files changed (size or mtime) between '$1' → '$2' ==="
|
||||
# Join on filename, report lines whose mtime/size differs
|
||||
diff <(grep -v '^#' "$a" | sort -k4) \
|
||||
<(grep -v '^#' "$b" | sort -k4) \
|
||||
| grep '^[<>]' \
|
||||
| awk '{print $NF}' \
|
||||
| sort | uniq -d
|
||||
}
|
||||
|
||||
# Live watch using /proc/inotify fallback (poll every 2s)
|
||||
watch_live() {
|
||||
if command -v inotifywait &>/dev/null; then
|
||||
inotifywait -r -m -e close_write,create,delete,moved_to \
|
||||
"${WATCH_PATHS[@]}" 2>/dev/null
|
||||
else
|
||||
echo "inotifywait not found. Install inotify-tools for live watching."
|
||||
echo "Falling back to 2-second poll diff (Ctrl-C to stop)..."
|
||||
local tmp_a tmp_b
|
||||
tmp_a=$(mktemp)
|
||||
tmp_b=$(mktemp)
|
||||
for d in "${WATCH_PATHS[@]}"; do
|
||||
[ -d "$d" ] || continue
|
||||
find "$d" -type f -printf '%TY-%Tm-%Td %TH:%TM:%.2TS %7s %p\n' 2>/dev/null
|
||||
done | sort > "$tmp_a"
|
||||
|
||||
while true; do
|
||||
sleep 2
|
||||
for d in "${WATCH_PATHS[@]}"; do
|
||||
[ -d "$d" ] || continue
|
||||
find "$d" -type f -printf '%TY-%Tm-%Td %TH:%TM:%.2TS %7s %p\n' 2>/dev/null
|
||||
done | sort > "$tmp_b"
|
||||
diff "$tmp_a" "$tmp_b" | grep '^[<>]' | while read -r line; do
|
||||
echo "$(date +%H:%M:%S) $line"
|
||||
done
|
||||
cp "$tmp_b" "$tmp_a"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
snapshot) snapshot "${2:?Usage: watch.sh snapshot <label>}" ;;
|
||||
diff) diff_snapshots "${2:?}" "${3:?Usage: watch.sh diff <label-a> <label-b>}" ;;
|
||||
watch) watch_live ;;
|
||||
*)
|
||||
echo "Usage:"
|
||||
echo " $0 snapshot <label> # take a file state snapshot"
|
||||
echo " $0 diff <label-a> <label-b> # show what changed between two snapshots"
|
||||
echo " $0 watch # live monitor (inotifywait or poll)"
|
||||
echo ""
|
||||
echo "Suggested workflow:"
|
||||
echo " $0 snapshot baseline"
|
||||
echo " # launch FIFA"
|
||||
echo " $0 snapshot main-menu"
|
||||
echo " # navigate to FUT"
|
||||
echo " $0 snapshot entered-fut"
|
||||
echo " # make squad change"
|
||||
echo " $0 snapshot squad-change"
|
||||
echo " $0 diff main-menu entered-fut"
|
||||
;;
|
||||
esac
|
||||
Executable
+135
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bash
|
||||
# network-metadata-logger/netlog.sh
|
||||
# Logs FIFA 23 network connection metadata: host, port, protocol, timing.
|
||||
# Does NOT capture payload, does NOT decrypt TLS.
|
||||
#
|
||||
# Usage: netlog.sh [--interval 2] [--duration 120]
|
||||
# Polls /proc/net/tcp* and resolves FIFA23 PIDs every N seconds.
|
||||
# Output: CSV to stdout and netlog-TIMESTAMP.csv in this directory.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INTERVAL="${INTERVAL:-2}"
|
||||
DURATION="${DURATION:-300}"
|
||||
OUT_DIR="$(cd "$(dirname "$0")" && pwd)/logs"
|
||||
mkdir -p "$OUT_DIR"
|
||||
OUT_FILE="$OUT_DIR/netlog-$(date +%Y%m%d-%H%M%S).csv"
|
||||
|
||||
log() { echo "$*" | tee -a "$OUT_FILE"; }
|
||||
|
||||
find_fifa_pids() {
|
||||
pgrep -x FIFA23.exe 2>/dev/null || \
|
||||
pgrep -f FIFA23.exe 2>/dev/null || \
|
||||
true
|
||||
}
|
||||
|
||||
# Decode a hex IP:port pair from /proc/net/tcp
|
||||
# e.g. "0F02000A:01BB" → "10.0.2.15:443"
|
||||
decode_addr() {
|
||||
local hex_ip="${1%%:*}"
|
||||
local hex_port="${1##*:}"
|
||||
local port=$((16#$hex_port))
|
||||
# IPv4 in /proc/net/tcp is little-endian
|
||||
local b1=$((16#${hex_ip:6:2}))
|
||||
local b2=$((16#${hex_ip:4:2}))
|
||||
local b3=$((16#${hex_ip:2:2}))
|
||||
local b4=$((16#${hex_ip:0:2}))
|
||||
echo "${b1}.${b2}.${b3}.${b4}:${port}"
|
||||
}
|
||||
|
||||
# TCP state codes
|
||||
state_name() {
|
||||
case "$1" in
|
||||
01) echo ESTABLISHED ;; 02) echo SYN_SENT ;;
|
||||
03) echo SYN_RECV ;; 04) echo FIN_WAIT1 ;;
|
||||
05) echo FIN_WAIT2 ;; 06) echo TIME_WAIT ;;
|
||||
07) echo CLOSE ;; 08) echo CLOSE_WAIT ;;
|
||||
09) echo LAST_ACK ;; 0A) echo LISTEN ;;
|
||||
0B) echo CLOSING ;; *) echo "STATE_$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Resolve remote IP → hostname (best-effort, non-blocking)
|
||||
resolve_host() {
|
||||
local ip="${1%%:*}"
|
||||
# Skip local/loopback
|
||||
case "$ip" in
|
||||
127.*|10.*|192.168.*|172.1[6-9].*|172.2[0-9].*|172.3[0-1].*) echo "$ip"; return ;;
|
||||
esac
|
||||
getent hosts "$ip" 2>/dev/null | awk '{print $2}' | head -1 || echo "$ip"
|
||||
}
|
||||
|
||||
# Categorise destination by port/host heuristic
|
||||
categorise() {
|
||||
local host="$1" port="$2"
|
||||
case "$port" in
|
||||
443|8443) echo "HTTPS/TLS" ;;
|
||||
80) echo "HTTP" ;;
|
||||
10853) echo "EA-LSX-auth" ;;
|
||||
3216|3478|3479|3480) echo "EA-UDP-STUN-or-relay" ;;
|
||||
*)
|
||||
case "$host" in
|
||||
*ea.com*|*easports.com*|*footapi.com*|*ugc.*) echo "EA-service" ;;
|
||||
*akamai*|*amazonaws*|*cloudfront*) echo "CDN" ;;
|
||||
*) echo "other" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Build inode→pid map from /proc
|
||||
build_inode_pid_map() {
|
||||
declare -gA INODE_PID
|
||||
INODE_PID=()
|
||||
for pid_dir in /proc/[0-9]*/fd; do
|
||||
local pid="${pid_dir%%/fd}"
|
||||
pid="${pid##/proc/}"
|
||||
for link in "$pid_dir"/[0-9]* 2>/dev/null; do
|
||||
local target
|
||||
target=$(readlink "$link" 2>/dev/null) || continue
|
||||
if [[ "$target" == "socket:["* ]]; then
|
||||
local inode="${target//[^0-9]/}"
|
||||
INODE_PID["$inode"]="$pid"
|
||||
fi
|
||||
done
|
||||
done 2>/dev/null || true
|
||||
}
|
||||
|
||||
log "timestamp,pid,proc,local_addr,remote_host,remote_addr,port,state,category"
|
||||
|
||||
declare -A SEEN_CONNS
|
||||
END_TIME=$(( $(date +%s) + DURATION ))
|
||||
|
||||
echo "Logging FIFA 23 network metadata to $OUT_FILE (${DURATION}s, Ctrl-C to stop)..."
|
||||
|
||||
while [ "$(date +%s)" -lt "$END_TIME" ]; do
|
||||
TS=$(date +%H:%M:%S)
|
||||
build_inode_pid_map
|
||||
|
||||
# Read /proc/net/tcp (IPv4 ESTABLISHED and SYN_SENT)
|
||||
while read -r _ local_hex remote_hex state _ _ _ _ _ inode _rest; do
|
||||
[ "$state" = "01" ] || [ "$state" = "02" ] || continue
|
||||
local_addr=$(decode_addr "$local_hex")
|
||||
remote_addr=$(decode_addr "$remote_hex")
|
||||
remote_ip="${remote_addr%%:*}"
|
||||
remote_port="${remote_addr##*:}"
|
||||
pid="${INODE_PID[$inode]:-}"
|
||||
[ -z "$pid" ] && continue
|
||||
|
||||
# Filter to FIFA-related PIDs (or log all if no FIFA found)
|
||||
proc=$(cat "/proc/$pid/comm" 2>/dev/null) || continue
|
||||
|
||||
state_str=$(state_name "$state")
|
||||
key="${pid}:${local_addr}:${remote_addr}"
|
||||
if [ -z "${SEEN_CONNS[$key]:-}" ] || [ "$state_str" != "${SEEN_CONNS[$key]}" ]; then
|
||||
SEEN_CONNS["$key"]="$state_str"
|
||||
host=$(resolve_host "$remote_ip")
|
||||
cat=$(categorise "$host" "$remote_port")
|
||||
log "$TS,$pid,$proc,$local_addr,$host,$remote_addr,$remote_port,$state_str,$cat"
|
||||
fi
|
||||
done < /proc/net/tcp 2>/dev/null || true
|
||||
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
|
||||
echo "Done. Output: $OUT_FILE"
|
||||
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
profile-exporter/export_profile.py
|
||||
Reads FIFA 23 local profile/settings files and exports what can be read.
|
||||
|
||||
FIFA 23 uses the Frostbite FBCHUNKS container format for saves.
|
||||
This exporter handles:
|
||||
- fifasetup.ini → plain text, fully readable
|
||||
- ProfileOptions → FBCHUNKS binary (partial parse of header + string scan)
|
||||
- Settings* → FBCHUNKS binary (contains "Personal Settings 1" label)
|
||||
- filesystemcache/ → JSON files, fully readable
|
||||
- anadius.cfg → Valve KeyValues format, contains persona info
|
||||
|
||||
Usage:
|
||||
python3 export_profile.py [--output out.json]
|
||||
"""
|
||||
|
||||
import struct
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
STEAM_PFX = Path.home() / ".local/share/Steam/steamapps/compatdata/3887260930/pfx"
|
||||
UMU_PFX = Path.home() / "Games/umu/fifa23-tools/pfx"
|
||||
GAME_DIR = Path("/mnt/games/FIFA 23")
|
||||
|
||||
DOCS_PATHS = [
|
||||
STEAM_PFX / "drive_c/users/steamuser/Documents/FIFA 23",
|
||||
UMU_PFX / "drive_c/users/steamuser/Documents/FIFA 23",
|
||||
]
|
||||
|
||||
def find_docs() -> Path | None:
|
||||
for p in DOCS_PATHS:
|
||||
if p.exists():
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def read_ini(path: Path) -> dict:
|
||||
out = {}
|
||||
for line in path.read_text(errors="replace").splitlines():
|
||||
line = line.strip()
|
||||
if "=" in line and not line.startswith(";") and not line.startswith("#"):
|
||||
k, _, v = line.partition("=")
|
||||
out[k.strip()] = v.strip().strip('"')
|
||||
return out
|
||||
|
||||
|
||||
def parse_fbchunks_header(data: bytes) -> dict:
|
||||
"""Extract what we can from the FBCHUNKS container without a full format spec."""
|
||||
if not data[:8] == b"FBCHUNKS":
|
||||
return {"error": "not FBCHUNKS"}
|
||||
info: dict = {"magic": "FBCHUNKS", "size_bytes": len(data)}
|
||||
# The label string appears at offset 0x12 in Settings files
|
||||
# Try to pull the first null-terminated ASCII string after offset 8
|
||||
try:
|
||||
label_start = 0x12
|
||||
end = data.index(b"\x00", label_start)
|
||||
label = data[label_start:end].decode("ascii", errors="ignore")
|
||||
if label:
|
||||
info["label"] = label
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
# String-scan for recognisable names/values
|
||||
strings = re.findall(rb"[ -~]{6,}", data)
|
||||
info["readable_strings_sample"] = [s.decode("ascii") for s in strings[:40]]
|
||||
return info
|
||||
|
||||
|
||||
def read_filesystem_cache(docs: Path) -> dict:
|
||||
cache_dir = docs / "filesystemcache"
|
||||
out = {}
|
||||
if not cache_dir.exists():
|
||||
return out
|
||||
for f in cache_dir.rglob("*"):
|
||||
if f.is_file():
|
||||
try:
|
||||
out[str(f.relative_to(cache_dir))] = json.loads(f.read_text())
|
||||
except Exception:
|
||||
out[str(f.relative_to(cache_dir))] = f.read_text(errors="replace")
|
||||
return out
|
||||
|
||||
|
||||
def parse_anadius_cfg(path: Path) -> dict:
|
||||
"""Parse Valve KeyValues (VDF) format anadius.cfg for persona/user info."""
|
||||
out: dict = {}
|
||||
current_section: str | None = None
|
||||
for line in path.read_text(errors="replace").splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith('"') and line.endswith('"') and "{" not in line:
|
||||
# lone quoted string — section name coming next
|
||||
current_section = line.strip('"')
|
||||
elif line.startswith('"') and "\t" in line:
|
||||
parts = [p.strip().strip('"') for p in line.split("\t") if p.strip()]
|
||||
if len(parts) == 2:
|
||||
key, val = parts[0], parts[1]
|
||||
section_key = f"{current_section}.{key}" if current_section else key
|
||||
out[section_key] = val
|
||||
return out
|
||||
|
||||
|
||||
def export_settings_files(docs: Path) -> list:
|
||||
settings_dir = docs / "settings"
|
||||
entries = []
|
||||
if not settings_dir.exists():
|
||||
return entries
|
||||
for f in sorted(settings_dir.iterdir()):
|
||||
if f.is_file():
|
||||
data = f.read_bytes()
|
||||
entry: dict = {
|
||||
"file": f.name,
|
||||
"size_bytes": len(data),
|
||||
"mtime": datetime.fromtimestamp(f.stat().st_mtime).isoformat(),
|
||||
}
|
||||
if data[:8] == b"FBCHUNKS":
|
||||
entry["format"] = "FBCHUNKS"
|
||||
entry["header"] = parse_fbchunks_header(data)
|
||||
elif data and all(0x20 <= b < 0x7F or b in (0x09, 0x0A, 0x0D) for b in data[:256]):
|
||||
entry["format"] = "text"
|
||||
entry["content"] = f.read_text(errors="replace")[:2000]
|
||||
else:
|
||||
entry["format"] = "binary"
|
||||
entries.append(entry)
|
||||
return entries
|
||||
|
||||
|
||||
def main():
|
||||
out_path = None
|
||||
if "--output" in sys.argv:
|
||||
idx = sys.argv.index("--output")
|
||||
out_path = Path(sys.argv[idx + 1])
|
||||
|
||||
docs = find_docs()
|
||||
result: dict = {
|
||||
"exported_at": datetime.utcnow().isoformat() + "Z",
|
||||
"docs_path": str(docs) if docs else None,
|
||||
}
|
||||
|
||||
# fifasetup.ini
|
||||
if docs:
|
||||
ini_path = docs / "fifasetup.ini"
|
||||
if ini_path.exists():
|
||||
result["fifasetup_ini"] = read_ini(ini_path)
|
||||
|
||||
# anadius.cfg (persona / user info)
|
||||
anadius_cfg = GAME_DIR / "anadius.cfg"
|
||||
if anadius_cfg.exists():
|
||||
result["anadius_cfg"] = parse_anadius_cfg(anadius_cfg)
|
||||
|
||||
# Settings/ save files
|
||||
if docs:
|
||||
result["settings_files"] = export_settings_files(docs)
|
||||
|
||||
# filesystemcache/
|
||||
if docs:
|
||||
result["filesystem_cache"] = read_filesystem_cache(docs)
|
||||
|
||||
output = json.dumps(result, indent=2, ensure_ascii=False)
|
||||
|
||||
if out_path:
|
||||
out_path.write_text(output)
|
||||
print(f"Written to {out_path}")
|
||||
else:
|
||||
print(output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,133 @@
|
||||
-- squad-exporter/export_squad.lua
|
||||
-- FIFA Live Editor Lua script (v2 API).
|
||||
-- Run from FLE's Lua Engine while in-game to export squad/FUT-relevant
|
||||
-- database table data to JSON in the FIFA 23 Live Editor directory.
|
||||
--
|
||||
-- Exports:
|
||||
-- players table (id, name, overall, potential, position, team)
|
||||
-- teams table (id, name, league, budget)
|
||||
-- fut_items table (if present in memory — only when FUT is loaded)
|
||||
--
|
||||
-- Output: C:\FIFA 23 Live Editor\openfut_squad_export.json
|
||||
|
||||
local OUT_PATH = "C:\\FIFA 23 Live Editor\\openfut_squad_export.json"
|
||||
|
||||
-- Minimal JSON serialiser (no external deps)
|
||||
local function json_val(v)
|
||||
local t = type(v)
|
||||
if t == "nil" then return "null"
|
||||
elseif t == "boolean" then return tostring(v)
|
||||
elseif t == "number" then
|
||||
if v ~= v then return "null" end -- NaN
|
||||
return string.format("%.6g", v)
|
||||
elseif t == "string" then
|
||||
return '"' .. v:gsub('\\', '\\\\'):gsub('"', '\\"'):gsub('\n','\\n') .. '"'
|
||||
elseif t == "table" then
|
||||
-- array check: sequential integer keys starting at 1
|
||||
local n = #v
|
||||
local is_arr = n > 0
|
||||
if is_arr then
|
||||
local parts = {}
|
||||
for i=1,n do parts[i] = json_val(v[i]) end
|
||||
return "[" .. table.concat(parts, ",") .. "]"
|
||||
else
|
||||
local parts = {}
|
||||
for k,vv in pairs(v) do
|
||||
parts[#parts+1] = json_val(tostring(k)) .. ":" .. json_val(vv)
|
||||
end
|
||||
return "{" .. table.concat(parts, ",") .. "}"
|
||||
end
|
||||
end
|
||||
return "null"
|
||||
end
|
||||
|
||||
local function export_table(tbl_name, fields)
|
||||
local rows = GetDBTableRows(tbl_name)
|
||||
if not rows or #rows == 0 then
|
||||
Log("[squad-exporter] table '" .. tbl_name .. "': empty or not found")
|
||||
return {}
|
||||
end
|
||||
Log("[squad-exporter] table '" .. tbl_name .. "': " .. #rows .. " rows")
|
||||
local out = {}
|
||||
for _, row in ipairs(rows) do
|
||||
local rec = {}
|
||||
for _, f in ipairs(fields) do
|
||||
local cell = row[f]
|
||||
if cell then
|
||||
rec[f] = cell.value
|
||||
end
|
||||
end
|
||||
out[#out+1] = rec
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- ── Export players ────────────────────────────────────────────────────────────
|
||||
local players = export_table("players", {
|
||||
"playerid", "overallrating", "potential", "preferredposition1",
|
||||
"height", "weight", "birthdate", "nationality", "skillmoves",
|
||||
"weakfootabilitytypecode", "attackingworkrate", "defensiveworkrate",
|
||||
})
|
||||
|
||||
-- Enrich with names (GetPlayerName is an FLE function)
|
||||
for _, p in ipairs(players) do
|
||||
local pid = p.playerid
|
||||
if pid then
|
||||
local ok, name = pcall(GetPlayerName, math.floor(pid))
|
||||
if ok and name then p.name = name end
|
||||
local ok2, tid = pcall(GetTeamIdFromPlayerId, math.floor(pid))
|
||||
if ok2 and tid then
|
||||
p.teamid = tid
|
||||
local ok3, tname = pcall(GetTeamName, tid)
|
||||
if ok3 then p.teamname = tname end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ── Export teams ─────────────────────────────────────────────────────────────
|
||||
local teams = export_table("teams", {
|
||||
"teamid", "leagueid", "teamcolor1r", "teamcolor1g", "teamcolor1b",
|
||||
"stadiumid", "domesticprestige", "internationalprestige",
|
||||
"transferbudget", "wagebudget",
|
||||
})
|
||||
for _, t in ipairs(teams) do
|
||||
local ok, name = pcall(GetTeamName, math.floor(t.teamid or 0))
|
||||
if ok then t.name = name end
|
||||
end
|
||||
|
||||
-- ── Export FUT tables if available ───────────────────────────────────────────
|
||||
-- These tables are only in memory when FUT mode is active.
|
||||
local fut_tables = {}
|
||||
local all_table_names = GetDBTablesNames()
|
||||
for _, tname in ipairs(all_table_names) do
|
||||
local lower = tname:lower()
|
||||
if lower:find("fut") or lower:find("club") or lower:find("pack")
|
||||
or lower:find("item") or lower:find("market") then
|
||||
Log("[squad-exporter] found FUT-related table: " .. tname)
|
||||
fut_tables[tname] = export_table(tname, GetDBTableFields(tname) or {})
|
||||
end
|
||||
end
|
||||
|
||||
-- ── Write output ─────────────────────────────────────────────────────────────
|
||||
local payload = {
|
||||
exported_at = os.date("!%Y-%m-%dT%H:%M:%SZ"),
|
||||
is_career_mode = IsInCM(),
|
||||
player_count = #players,
|
||||
team_count = #teams,
|
||||
players = players,
|
||||
teams = teams,
|
||||
fut_tables = fut_tables,
|
||||
all_db_tables = all_table_names,
|
||||
}
|
||||
|
||||
local f = io.open(OUT_PATH, "w")
|
||||
if f then
|
||||
f:write(json_val(payload))
|
||||
f:close()
|
||||
Log("[squad-exporter] Written to " .. OUT_PATH)
|
||||
MessageBox("OpenFUT Squad Export", "Done! " .. #players .. " players, "
|
||||
.. #teams .. " teams.\n" .. OUT_PATH)
|
||||
else
|
||||
Log("[squad-exporter] ERROR: could not open " .. OUT_PATH)
|
||||
MessageBox("OpenFUT Squad Export", "ERROR writing to " .. OUT_PATH)
|
||||
end
|
||||
@@ -0,0 +1,217 @@
|
||||
-- squad-injector/apply_lineup_write.lua
|
||||
-- FIFA Live Editor Lua script (v2 API).
|
||||
--
|
||||
-- TEMPLATE — fill in TARGET_TABLE / TARGET_FIELDS below using whatever
|
||||
-- diff_snapshots.py reports after the snapshot/Freeze-Lineup/snapshot cycle
|
||||
-- described in docs/foundational-xi-injection-test.md. This script cannot
|
||||
-- run correctly until those are filled in; it's the second half of the
|
||||
-- foundational test, not a standalone tool.
|
||||
--
|
||||
-- Once filled in, this replicates the discovered write programmatically
|
||||
-- (no GUI/Formation Editor needed), which is what build-order step 2 (the
|
||||
-- app->game bridge) needs to drive a squad push from outside the game.
|
||||
--
|
||||
-- Correct EditDBTableField usage (confirmed from FLE's own Lua API docs and
|
||||
-- lua/scripts/99ovr_99pot.lua): mutate the cell's .value in place, then pass
|
||||
-- THE CELL ITSELF (not table/index/field-name/value as separate args) to
|
||||
-- EditDBTableField:
|
||||
--
|
||||
-- local rows = GetDBTableRows("players")
|
||||
-- local row = rows[1]
|
||||
-- row["overallrating"].value = "99"
|
||||
-- local ok = EditDBTableField(row["overallrating"])
|
||||
|
||||
local IN_PATH = "C:\\FIFA 23 Live Editor\\openfut_test_xi.json"
|
||||
local OUT_PATH = "C:\\FIFA 23 Live Editor\\openfut_apply_result.json"
|
||||
|
||||
-- ── FILL IN AFTER RUNNING diff_snapshots.py ──────────────────────────────────
|
||||
local TARGET_TABLE = nil -- e.g. "teamsheet" — set this once the diff identifies it
|
||||
local TARGET_FIELDS = {
|
||||
-- e.g. { name = "selected", value = function(entry) return "1" end },
|
||||
-- e.g. { name = "position", value = function(entry) return entry.position end },
|
||||
}
|
||||
|
||||
local function json_val(v)
|
||||
local t = type(v)
|
||||
if t == "nil" then return "null"
|
||||
elseif t == "boolean" then return tostring(v)
|
||||
elseif t == "number" then return string.format("%.6g", v)
|
||||
elseif t == "string" then
|
||||
return '"' .. v:gsub('\\', '\\\\'):gsub('"', '\\"'):gsub('\n', '\\n') .. '"'
|
||||
elseif t == "table" then
|
||||
local n = #v
|
||||
if n > 0 then
|
||||
local parts = {}
|
||||
for i = 1, n do parts[i] = json_val(v[i]) end
|
||||
return "[" .. table.concat(parts, ",") .. "]"
|
||||
else
|
||||
local parts = {}
|
||||
for k, vv in pairs(v) do
|
||||
parts[#parts + 1] = json_val(tostring(k)) .. ":" .. json_val(vv)
|
||||
end
|
||||
return "{" .. table.concat(parts, ",") .. "}"
|
||||
end
|
||||
end
|
||||
return "null"
|
||||
end
|
||||
|
||||
local function parse_json(s)
|
||||
local i = 1
|
||||
local function skip_ws() while s:sub(i, i):match("%s") do i = i + 1 end end
|
||||
local parse_value
|
||||
|
||||
local function parse_string()
|
||||
i = i + 1
|
||||
local start = i
|
||||
local out = {}
|
||||
while s:sub(i, i) ~= '"' do
|
||||
if s:sub(i, i) == "\\" then
|
||||
out[#out + 1] = s:sub(start, i - 1)
|
||||
local esc = s:sub(i + 1, i + 1)
|
||||
out[#out + 1] = ({ n = "\n", t = "\t", ['"'] = '"', ["\\"] = "\\" })[esc] or esc
|
||||
i = i + 2
|
||||
start = i
|
||||
else
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
out[#out + 1] = s:sub(start, i - 1)
|
||||
i = i + 1
|
||||
return table.concat(out)
|
||||
end
|
||||
|
||||
local function parse_number()
|
||||
local start = i
|
||||
while s:sub(i, i):match("[%-%+%.eE0-9]") do i = i + 1 end
|
||||
return tonumber(s:sub(start, i - 1))
|
||||
end
|
||||
|
||||
local function parse_array()
|
||||
i = i + 1
|
||||
local out = {}
|
||||
skip_ws()
|
||||
if s:sub(i, i) == "]" then i = i + 1; return out end
|
||||
while true do
|
||||
skip_ws()
|
||||
out[#out + 1] = parse_value()
|
||||
skip_ws()
|
||||
if s:sub(i, i) == "," then i = i + 1 else break end
|
||||
end
|
||||
skip_ws()
|
||||
i = i + 1
|
||||
return out
|
||||
end
|
||||
|
||||
local function parse_object()
|
||||
i = i + 1
|
||||
local out = {}
|
||||
skip_ws()
|
||||
if s:sub(i, i) == "}" then i = i + 1; return out end
|
||||
while true do
|
||||
skip_ws()
|
||||
local key = parse_string()
|
||||
skip_ws()
|
||||
i = i + 1
|
||||
skip_ws()
|
||||
out[key] = parse_value()
|
||||
skip_ws()
|
||||
if s:sub(i, i) == "," then i = i + 1 else break end
|
||||
end
|
||||
skip_ws()
|
||||
i = i + 1
|
||||
return out
|
||||
end
|
||||
|
||||
parse_value = function()
|
||||
skip_ws()
|
||||
local c = s:sub(i, i)
|
||||
if c == '"' then return parse_string()
|
||||
elseif c == "{" then return parse_object()
|
||||
elseif c == "[" then return parse_array()
|
||||
elseif c == "t" then i = i + 4; return true
|
||||
elseif c == "f" then i = i + 5; return false
|
||||
elseif c == "n" then i = i + 4; return nil
|
||||
else return parse_number()
|
||||
end
|
||||
end
|
||||
|
||||
skip_ws()
|
||||
return parse_value()
|
||||
end
|
||||
|
||||
local function read_file(path)
|
||||
local f = io.open(path, "r")
|
||||
if not f then return nil end
|
||||
local content = f:read("*a")
|
||||
f:close()
|
||||
return content
|
||||
end
|
||||
|
||||
local function write_file(path, content)
|
||||
local f = io.open(path, "w")
|
||||
if not f then return false end
|
||||
f:write(content)
|
||||
f:close()
|
||||
return true
|
||||
end
|
||||
|
||||
if not TARGET_TABLE or #TARGET_FIELDS == 0 then
|
||||
MessageBox("OpenFUT Apply", "TARGET_TABLE / TARGET_FIELDS are not filled in yet. " ..
|
||||
"Run the snapshot/Freeze-Lineup/snapshot/diff cycle first (see docs/foundational-xi-injection-test.md).")
|
||||
return
|
||||
end
|
||||
|
||||
local raw = read_file(IN_PATH)
|
||||
if not raw then
|
||||
MessageBox("OpenFUT Apply", "ERROR: could not read " .. IN_PATH)
|
||||
return
|
||||
end
|
||||
|
||||
local ok_parse, request = pcall(parse_json, raw)
|
||||
if not ok_parse or not request or not request.xi then
|
||||
MessageBox("OpenFUT Apply", "ERROR: could not parse " .. IN_PATH)
|
||||
return
|
||||
end
|
||||
|
||||
local rows = GetDBTableRows(TARGET_TABLE) or {}
|
||||
local result = {
|
||||
attempted_at = os.date("!%Y-%m-%dT%H:%M:%SZ"),
|
||||
is_career_mode = IsInCM(),
|
||||
target_table = TARGET_TABLE,
|
||||
rows_written = {},
|
||||
errors = {},
|
||||
}
|
||||
|
||||
for _, entry in ipairs(request.xi) do
|
||||
local pid = entry.player_id
|
||||
local matched_row = nil
|
||||
for _, row in ipairs(rows) do
|
||||
local cell = row["playerid"] or row["player_id"]
|
||||
if cell and math.floor(cell.value) == math.floor(pid) then
|
||||
matched_row = row
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not matched_row then
|
||||
result.errors[#result.errors + 1] = "player_id " .. tostring(pid) .. " not found in " .. TARGET_TABLE
|
||||
else
|
||||
local row_result = { player_id = pid, writes = {} }
|
||||
for _, fdef in ipairs(TARGET_FIELDS) do
|
||||
local cell = matched_row[fdef.name]
|
||||
if cell then
|
||||
cell.value = fdef.value(entry)
|
||||
local ok = EditDBTableField(cell)
|
||||
row_result.writes[#row_result.writes + 1] = { field = fdef.name, value = cell.value, ok = ok }
|
||||
else
|
||||
row_result.writes[#row_result.writes + 1] = { field = fdef.name, ok = false, err = "field not found on row" }
|
||||
end
|
||||
end
|
||||
result.rows_written[#result.rows_written + 1] = row_result
|
||||
end
|
||||
end
|
||||
|
||||
write_file(OUT_PATH, json_val(result))
|
||||
Log("[apply] done, see " .. OUT_PATH)
|
||||
MessageBox("OpenFUT Apply", "Rows written: " .. #result.rows_written .. " / " .. #request.xi ..
|
||||
"\nErrors: " .. #result.errors .. "\n\n" .. OUT_PATH)
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Diff two openfut_snapshot_*.json files from snapshot_lineup_tables.lua.
|
||||
|
||||
Usage: diff_snapshots.py before.json after.json
|
||||
|
||||
Reports, per table, which rows changed and which fields differed —
|
||||
this is how the real DB write behind FLE's "Freeze Lineup" GUI feature
|
||||
gets discovered (see docs/foundational-xi-injection-test.md).
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def row_key(row):
|
||||
for candidate in ("playerid", "player_id", "id", "rowid"):
|
||||
if candidate in row:
|
||||
return row[candidate]
|
||||
return json.dumps(row, sort_keys=True)
|
||||
|
||||
|
||||
def diff_table(name, before, after):
|
||||
before_rows = {row_key(r): r for r in before.get("rows", [])}
|
||||
after_rows = {row_key(r): r for r in after.get("rows", [])}
|
||||
|
||||
changes = []
|
||||
for key, after_row in after_rows.items():
|
||||
before_row = before_rows.get(key)
|
||||
if before_row is None:
|
||||
changes.append({"row": key, "type": "added", "row_data": after_row})
|
||||
continue
|
||||
field_diffs = {}
|
||||
for field in set(before_row) | set(after_row):
|
||||
bval = before_row.get(field)
|
||||
aval = after_row.get(field)
|
||||
if bval != aval:
|
||||
field_diffs[field] = {"before": bval, "after": aval}
|
||||
if field_diffs:
|
||||
changes.append({"row": key, "type": "modified", "fields": field_diffs})
|
||||
|
||||
for key in before_rows:
|
||||
if key not in after_rows:
|
||||
changes.append({"row": key, "type": "removed"})
|
||||
|
||||
if changes:
|
||||
print(f"\n=== {name}: {len(changes)} changed row(s) ===")
|
||||
for c in changes:
|
||||
print(json.dumps(c, indent=2))
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
with open(sys.argv[1]) as f:
|
||||
before = json.load(f)
|
||||
with open(sys.argv[2]) as f:
|
||||
after = json.load(f)
|
||||
|
||||
table_names = set(before.get("tables", {})) | set(after.get("tables", {}))
|
||||
any_changes = False
|
||||
for name in sorted(table_names):
|
||||
b = before.get("tables", {}).get(name, {"rows": []})
|
||||
a = after.get("tables", {}).get(name, {"rows": []})
|
||||
before_len = len(b.get("rows", []))
|
||||
after_len = len(a.get("rows", []))
|
||||
if before_len != after_len:
|
||||
print(f"{name}: row count changed {before_len} -> {after_len}")
|
||||
any_changes = True
|
||||
diff_table(name, b, a)
|
||||
|
||||
if not any_changes:
|
||||
print("(row-count-only check found no differences; per-field diffs above are authoritative)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,111 @@
|
||||
-- squad-injector/snapshot_lineup_tables.lua
|
||||
-- FIFA Live Editor Lua script (v2 API).
|
||||
--
|
||||
-- Dumps every DB table whose name looks lineup/formation/squad-related,
|
||||
-- full rows + fields, to a timestamped JSON file. Run it once BEFORE using
|
||||
-- FLE's GUI "Freeze Lineup" feature (Formation Editor -> arrange players on
|
||||
-- pitch -> tick "Freeze Lineup" -> Data -> Save) and once AFTER, then diff
|
||||
-- the two snapshots with diff_snapshots.py to find which table/field(s) the
|
||||
-- GUI feature actually writes.
|
||||
--
|
||||
-- This exists because "Freeze Lineup" is a confirmed, documented mechanism
|
||||
-- for forcing a starting XI (FLE wiki: Formation-Editor.md) but it's GUI-only
|
||||
-- — its underlying DB write is not documented anywhere. Diffing before/after
|
||||
-- snapshots is how we find it, the same way export_squad.lua's exhaustive
|
||||
-- table dump was the oracle for Track C.
|
||||
--
|
||||
-- Output: C:\FIFA 23 Live Editor\openfut_snapshot_<unix-ish timestamp>.json
|
||||
|
||||
local OUT_DIR = "C:\\FIFA 23 Live Editor\\"
|
||||
|
||||
-- Keyword filter, broader than squad-exporter's FUT-table filter — Freeze
|
||||
-- Lineup is a formation/tactics feature so the write could land in any of
|
||||
-- these families. "players" is included because position fields on the
|
||||
-- player row itself may also change.
|
||||
local KEYWORDS = { "squad", "lineup", "formation", "tactic", "teamsheet", "selection", "players", "teams" }
|
||||
|
||||
local function json_val(v)
|
||||
local t = type(v)
|
||||
if t == "nil" then return "null"
|
||||
elseif t == "boolean" then return tostring(v)
|
||||
elseif t == "number" then
|
||||
if v ~= v then return "null" end
|
||||
return string.format("%.6g", v)
|
||||
elseif t == "string" then
|
||||
return '"' .. v:gsub('\\', '\\\\'):gsub('"', '\\"'):gsub('\n', '\\n') .. '"'
|
||||
elseif t == "table" then
|
||||
local n = #v
|
||||
if n > 0 then
|
||||
local parts = {}
|
||||
for i = 1, n do parts[i] = json_val(v[i]) end
|
||||
return "[" .. table.concat(parts, ",") .. "]"
|
||||
else
|
||||
local parts = {}
|
||||
for k, vv in pairs(v) do
|
||||
parts[#parts + 1] = json_val(tostring(k)) .. ":" .. json_val(vv)
|
||||
end
|
||||
return "{" .. table.concat(parts, ",") .. "}"
|
||||
end
|
||||
end
|
||||
return "null"
|
||||
end
|
||||
|
||||
local function export_table(tbl_name)
|
||||
local fields = GetDBTableFields(tbl_name) or {}
|
||||
local field_names = {}
|
||||
for _, f in ipairs(fields) do field_names[#field_names + 1] = f.name end
|
||||
|
||||
local rows = GetDBTableRows(tbl_name)
|
||||
if not rows or #rows == 0 then
|
||||
Log("[snapshot] table '" .. tbl_name .. "': empty or not found")
|
||||
return { fields = field_names, rows = {} }
|
||||
end
|
||||
|
||||
local out_rows = {}
|
||||
for _, row in ipairs(rows) do
|
||||
local rec = {}
|
||||
for _, fname in ipairs(field_names) do
|
||||
local cell = row[fname]
|
||||
if cell then rec[fname] = cell.value end
|
||||
end
|
||||
out_rows[#out_rows + 1] = rec
|
||||
end
|
||||
Log("[snapshot] table '" .. tbl_name .. "': " .. #out_rows .. " rows, " .. #field_names .. " fields")
|
||||
return { fields = field_names, rows = out_rows }
|
||||
end
|
||||
|
||||
local all_table_names = GetDBTablesNames()
|
||||
local matched = {}
|
||||
for _, tname in ipairs(all_table_names) do
|
||||
local lower = tname:lower()
|
||||
for _, kw in ipairs(KEYWORDS) do
|
||||
if lower:find(kw) then
|
||||
matched[#matched + 1] = tname
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local snapshot = {
|
||||
captured_at = os.date("!%Y-%m-%dT%H:%M:%SZ"),
|
||||
is_career_mode = IsInCM(),
|
||||
matched_table_names = matched,
|
||||
tables = {},
|
||||
}
|
||||
|
||||
for _, tname in ipairs(matched) do
|
||||
snapshot.tables[tname] = export_table(tname)
|
||||
end
|
||||
|
||||
local out_path = OUT_DIR .. "openfut_snapshot_" .. os.date("!%Y%m%dT%H%M%SZ") .. ".json"
|
||||
local f = io.open(out_path, "w")
|
||||
if f then
|
||||
f:write(json_val(snapshot))
|
||||
f:close()
|
||||
Log("[snapshot] written to " .. out_path)
|
||||
MessageBox("OpenFUT Snapshot", "Snapshotted " .. #matched .. " tables.\n" .. out_path ..
|
||||
"\n\nTake one snapshot BEFORE Freeze Lineup, one AFTER, then run diff_snapshots.py on the two files.")
|
||||
else
|
||||
Log("[snapshot] ERROR: could not open " .. out_path)
|
||||
MessageBox("OpenFUT Snapshot", "ERROR writing to " .. out_path)
|
||||
end
|
||||
Reference in New Issue
Block a user