Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions luarules/gadgets/cmd_dev_helpers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -651,11 +651,13 @@ if gadgetHandler:IsSyncedCode() then
))
ExecuteRemoveUnitDefName(team1unitDefName)
ExecuteRemoveUnitDefName(team2unitDefName)
SendToUnsynced("fightertest_synchash_end")
return
end
fighterteststartgameframe = Spring.GetGameFrame()
fightertesttotalunitsspawned = 0
initrandom(7654321)
SendToUnsynced("fightertest_synchash_begin")
if words[2] and UnitDefNames[words[2]] then team1unitDefName = words[2]
else Spring.Echo(words[2], "is not a valid unitDefName, using", team1unitDefName, "instead") end

Expand Down Expand Up @@ -740,6 +742,10 @@ if gadgetHandler:IsSyncedCode() then
adjustFeatureHeight()
end
if fightertestenabled then
if Engine.FeatureSupport.hasChecksums then
SendToUnsynced("fightertest_synchash_frame", n, Spring.GetPrevFrameChecksum())
end

if (n % 3 == 0) then
SpawnUnitDefsForTeamSynced(0, team1unitDefName)
SpawnUnitDefsForTeamSynced(1, team2unitDefName)
Expand Down Expand Up @@ -947,6 +953,62 @@ else -- UNSYNCED
local vsx,vsy = Spring.GetViewGeometry()
local uiScale = vsy / 1080

-- ----- Fightertest sync-hash collection -----
-- Receives per-frame sync checksums from the synced half during a
-- fightertest run, folds them into a single MD5 on end, and writes the
-- result to "fightertest_synchash.txt" in the Spring write directory.
-- Used by the engine CI sync test (RecoilEngine#2910).
local synchashBuffer = {}
local synchashFirstFrame, synchashLastFrame

local function onSynchashBegin()
synchashBuffer = {}
synchashFirstFrame, synchashLastFrame = nil, nil
end

local function onSynchashFrame(_, frame, checksum)
frame = tonumber(frame) or 0
checksum = tostring(checksum or "")
synchashBuffer[#synchashBuffer + 1] = string.format("%d:%s", frame, checksum)
if not synchashFirstFrame then synchashFirstFrame = frame end
synchashLastFrame = frame
end

local function onSynchashEnd()
local count = #synchashBuffer
if count == 0 then
Spring.Echo("[fightertest] sync-hash: no frames collected (engine may lack Spring.GetPrevFrameChecksum)")
return
end
local blob = table.concat(synchashBuffer, "\n")
local digest = VFS.CalculateHash(blob, 0)

local path = "fightertest_synchash.txt"
local content = digest .. "\n"
.. string.format("frames=%d first=%d last=%d\n",
count, synchashFirstFrame or -1, synchashLastFrame or -1)
.. blob .. "\n"

-- Unsynced gadgets have no io (LuaHandleSynced.cpp comments it out),
-- so we delegate the file write to a widget via Script.LuaUI, which
-- runs in the LuaUI context where io is available.
if Script.LuaUI('FightertestSyncHashWrite') then
local ok, result = Script.LuaUI.FightertestSyncHashWrite(path, content)
if ok then
Spring.Echo(string.format(
"[fightertest] sync-hash: wrote %s (md5=%s, %d frames %d..%d)",
result, digest, count, synchashFirstFrame or -1, synchashLastFrame or -1))
end
else
Spring.Echo("[fightertest] sync-hash: widget 'Fightertest Sync-Hash Writer' not loaded, cannot write file")
Spring.Echo(string.format(
"[fightertest_synchash] md5=%s frames=%d first=%d last=%d",
digest, count, synchashFirstFrame or -1, synchashLastFrame or -1))
end

synchashBuffer = {}
end

function gadget:Initialize()
-- doing it via GotChatMsg ensures it will only listen to the caller
gadgetHandler:AddChatAction('givecat', GiveCat, "") -- Give a category of units, options /luarules givecat [cor|arm|scav|raptor] or /luarules givecat unitname [teamid]
Expand All @@ -973,6 +1035,10 @@ else -- UNSYNCED
gadgetHandler:AddChatAction('playertoteam', playertoteam, "") -- /luarules playertoteam [playerID] [teamID] -- playerID+teamID are optional, no playerID given = your own playerID, no teamID = selected unit team or hovered unit team
gadgetHandler:AddChatAction('killteam', killteam, "") -- /luarules killteam [teamID] -- kills the team
gadgetHandler:AddChatAction('desync', desync) -- /luarules desync

gadgetHandler:AddSyncAction('fightertest_synchash_begin', onSynchashBegin)
gadgetHandler:AddSyncAction('fightertest_synchash_frame', onSynchashFrame)
gadgetHandler:AddSyncAction('fightertest_synchash_end', onSynchashEnd)
end

function gadget:Shutdown()
Expand All @@ -996,6 +1062,10 @@ else -- UNSYNCED
gadgetHandler:RemoveChatAction('playertoteam')
gadgetHandler:RemoveChatAction('killteam')
gadgetHandler:RemoveChatAction('desync')

gadgetHandler:RemoveSyncAction('fightertest_synchash_begin')
gadgetHandler:RemoveSyncAction('fightertest_synchash_frame')
gadgetHandler:RemoveSyncAction('fightertest_synchash_end')
end

function xpUnits(_, line, words, playerID)
Expand Down
32 changes: 32 additions & 0 deletions luaui/Widgets/dbg_fightertest_synchash_writer.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
local widget = widget ---@type Widget

function widget:GetInfo()
return {
name = "Fightertest Sync-Hash Writer",
desc = "Writes the fightertest sync-hash artifact to disk on behalf of cmd_dev_helpers.lua (unsynced gadgets have no io, widgets do). See RecoilEngine#2910.",
author = "Bruno-DaSilva",
date = "2026-04-12",
license = "GNU GPL, v2 or later",
layer = 0,
enabled = true,
}
end

local function FightertestSyncHashWrite(path, content)
local f, err = io.open(path, "w")
if not f then
Spring.Echo("[fightertest] sync-hash: widget failed to open " .. tostring(path) .. ": " .. tostring(err))
return false, tostring(err)
end
f:write(content)
f:close()
return true, path
end

function widget:Initialize()
widgetHandler:RegisterGlobal('FightertestSyncHashWrite', FightertestSyncHashWrite)
end

function widget:Shutdown()
widgetHandler:DeregisterGlobal('FightertestSyncHashWrite')
end