commit c1bfbaf36a9f4e527e5bd35418134f9587ee82b5 Author: srift Date: Mon Jun 29 21:03:19 2026 +0200 scriptcore: reconstructed from preserved seed files Reconstructed Lua scripting infrastructure consumed by every per-game flash script. Two source files preserved at D:\B\Srift\games\Flash\: - functional.lua (109 lines): table.from/max/min/find/filter/map - internal.lua (172 lines): Markers, Rule.All/Any sugar, Hex/Addr, print override, gadget enumerators Surface inventory at D:\B\Srift\games\Flash\SURFACE.md confirms these two files cover scriptcore's lua-level responsibilities for CODMW, CODMW2, IW9, DayZ, Template. RB6 may need a Rule.Refs stub. Server contract (Builder.ts:286-380): pushing this repo to Gitea as srift/core triggers the standard webhook pipeline and produces S.Build doc { kind:'script', product:'scriptcore' }. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79c28a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Editor / OS +.vscode/ +.idea/ +*.swp +.DS_Store +Thumbs.db + +# Node noise (Builder.BuildVirtual ignores these anyway — kept for hygiene) +node_modules/ +package-lock.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..d752386 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# srift/core (scriptcore) + +Universal Lua scripting infrastructure consumed by every per-game flash script. + +**Server contract:** this repo, when pushed to Gitea under owner `srift` and name `core`, triggers the standard Gitea webhook pipeline: + +``` +git push → Gitea → POST /ggit → overwatch app.ts:31 + → RepoUpdate.Schedule('srift/core') + → Gitea.UpdateRepos('srift', 'core', branch, commit) + → Builder.Build('srift', 'core', localdir) + → Builder.Upload(result, details, branch, commit) + → S.Build doc { kind:'script', product:'scriptcore', pid:@ } +``` + +After upload, `GameSession.loadProfile` at `D:\B\Srift\site\www\api\native\client\GameSession.ts:272` finds the Build (`Store.PickBuild('script', user, 'scriptcore')`) and ships its files into the per-session VFS at prefix `core/` (per `Builder.BuildVirtual` line 110, prefix = repo name). + +## Contents + +| File | Role | Lines | +|---|---|---| +| `functional.lua` | Lua stdlib extensions: `table.from/max/min/find/filter/map` | 109 | +| `internal.lua` | `_G.Markers`, `Rule.All`/`Rule.Any` sugar, `Hex`/`Addr`/`print` override, three `Register(...)` calls for `_fast_return_gadgets` / `_fix_arg_gadgets` / `_stack_spoof_gadgets` | 172 | + +## Surface inventory + +See `D:\B\Srift\games\Flash\SURFACE.md` for the full enumeration of what scriptcore must export vs. what chost provides natively. + +## Reconstruction provenance + +Clean-room reconstruction 2026-06-29 from preserved seed files at `D:\B\Srift\games\Flash\`. Original `srift/core` Gitea repo not accessible; surface was reverse-engineered from per-game flash script call sites (`games\Games\*\flash\*.lua`). diff --git a/functional.lua b/functional.lua new file mode 100644 index 0000000..bfa042c --- /dev/null +++ b/functional.lua @@ -0,0 +1,110 @@ +table.from = function(f) + local tbl = {} + local i = 1 + for v in f do + tbl[i] = v + i = i + 1 + end + return tbl +end +table.max = function(t) + local maxk, maxv + if type(t) == "table" then + for k,v in pairs(t) do + if not maxk or maxv < v then + maxk = k + maxv = v + end + end + else + for k,v in t do + if not maxk or maxv < v then + maxk = k + maxv = v + end + end + end + return maxk, maxv +end + +table.min = function(t) + local mink, minv + if type(t) == "table" then + for k,v in pairs(t) do + if not mink or minv > v then + mink = k + minv = v + end + end + else + for k,v in t do + if not mink or minv > v then + mink = k + minv = v + end + end + end + return mink, minv +end + +table.find = function(t, f) + if type(t) == "table" then + for k,v in pairs(t) do + if f(k,v) then + return k,v + end + end + else + for k,v in t do + if f(k,v) then + return k,v + end + end + end +end + +table.filter = function(t, f) + if type(t) == "table" then + local it = nil + return function() + while true do + it = next(t, it) + if not it then + break + elseif f(it,t[it]) then + return it,t[it] + end + end + end + else + return function() + while true do + local k,v = t() + if not k then + break + elseif f(k,v) then + return k,v + end + end + end + end +end + +table.map = function(t, f) + if type(t) == "table" then + local it = nil + return function() + it = next(t, it) + if it then + return f(it, t[it]) + end + end + else + return function() + local k,v = t() + if k then + return f(k,v) + end + end + end +end \ No newline at end of file diff --git a/internal.lua b/internal.lua new file mode 100644 index 0000000..cfb2992 --- /dev/null +++ b/internal.lua @@ -0,0 +1,173 @@ + +-- Implement the marker table. +-- +local markerObjects = {} +_G.Markers = { Clear = function() markerObjects = {} end } +setmetatable(_G.Markers, { + __index = function(_, name) + local marker = markerObjects[name] + return marker and Rule.ReadMarker(marker) + end +}) + +-- Parses a table of rules and returns a table of valid Rule objects. +-- +local function parseRuleList(arg) + local result = {} + local it = 1 + while true do + local v = arg[it] + if v == nil then break end + local t = type(v) + + if t == "string" then + if v:sub(1,1) == "#" then + v = v:sub(2) + local m = markerObjects[v] + if not m then + m = Rule.Marker() + markerObjects[v] = m + end + result[#result+1] = m + elseif type(arg[it+1]) == "number" then + result[#result+1] = Rule.Instruction(v, arg[it+1]) + it = it + 1 + else + result[#result+1] = Rule.Pattern(v) + end + elseif t == "table" then + result[#result+1] = Rule.Pad(v[1], v[2]) + elseif t == "function" then + result[#result+1] = Rule.Filter(v) + elseif t == "userdata" then + result[#result+1] = v + else + error("Did not expect: " .. t) + end + it = it + 1 + end + return result +end + +-- Implement Rule.All and Rule.Any. +-- +function Rule.All(arg) + return Rule.AllImpl(parseRuleList(arg)) +end +function Rule.Any(arg) + return Rule.AnyImpl(parseRuleList(arg)) +end + +-- Hex helper, override print. +-- +function Hex(value) return value and string.format("%x", value) or "nil" end +function Addr(value) return value and string.format("%x", value + Image.base) or "nil" end + +function print(...) + local args = table.pack(...) + local msg = "" + for i=1, args.n do + if #msg ~= 0 then + msg = msg .. " " .. tostring(args[i]) + else + msg = tostring(args[i]) + end + end + Log(msg) +end + +local function findAny(patternList) + local output = {0} + local n = 0 + for k,v in ipairs(patternList) do + for rva in Image:Find(Rule.Pattern(v[1])) do + local res = rva + ((#v[1]+1)/3) + v[2] + output[n+2] = res + n = n + 1 + end + end + output[1] = n + return output +end + +-- Implement searchers for user call gadgets. +-- +Register("_fast_return_gadgets", R_ARR, function() + return findAny{ + { "E8 ?? 0F 05 00", -3 }, -- call rel32 + { "0F 8? 0F 05 00", -3 }, -- jcc rel32 + { "FF 15 ?? 0F 05", -2 }, -- call [rip+rel32] + { "48 8D ?? ?? 0F 05", -2 }, -- lea [rip+rel32] + { "48 89 ?? ?? 0F 05", -2 }, -- mov [rip+rel32] + { "4C 8D ?? ?? 0F 05", -2 }, -- lea [rip+rel32] + { "4C 89 ?? ?? 0F 05", -2 }, -- mov [rip+rel32] + } +end) + +Register("_fix_arg_gadgets", R_ARR, function() + return findAny{ + { "F3 41 0F 59 C3", -2 }, -- mulss, mulps, vmulss, vmulps + { "F3 43 0F 59 C3", -2 }, + { "41 0F 59 C3", -2 }, + { "F3 0F 59 C3", -2 }, + { "C4 C1 ?? 59 C3", -2 } + } +end) + +Register("_stack_spoof_gadgets", R_ARR, function() + -- Matches a call m64 instruction + -- reg = /2 + local callPattern = { "FF 10", "FF 38" } + + -- Matches an epilogue + -- + local epiPatterns = { + { "48 83 C4 08 C3", "FF FF FF 8F FF" }, -- [add rsp, imm8], no negative/misaligned immediates + { "48 81 C4 08 00 00 00 C3", "FF FF FF 0F FF FF FF FF" } -- [add rsp, imm32], no negative/misaligned/huge immediates + } + + -- For each epilogue pattern: + -- + local results = {} + for _,v in ipairs(epiPatterns) do + -- For each call instruction suffix length: + -- + for suf=5,0,-1 do + -- Find the gadget: + -- + local value = callPattern[1] .. string.rep(" 00", suf) .. " " .. v[1] + local mask = callPattern[2] .. string.rep(" 00", suf) .. " " .. v[2] + for match in Image:Find(Rule.Pattern(value, mask)) do + -- If valid: + -- + if Image:DecodeLen(match) == (2+suf) then + -- Decode the sub instruction, if it is not at arg area, write the result: + -- + local subinsn = Image:Decode(match+2+suf) + if subinsn.imm0 >= (8*9) then + results[#results+1] = { + callRva = match, + subRva = match + 2 + suf, + delta = subinsn.imm0 + 8 + } + end + end + end + end + end + + -- Sort the table based on call complexity. + -- + table.sort(results, function(a, b) return (a.subRva-a.callRva)>(b.subRva-b.callRva) end) + + -- Get table length, trimming to a maximum of 32 and write the immediates. + -- + local len = math.min(32, #results) + local output = {} + output[1] = len + for i=1,len do + output[i*2 + 0] = results[i].subRva + output[i*2 + 1] = results[i].delta + end + return output +end) \ No newline at end of file