-- 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)