myspace / Lua /GameRecording.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
10.1 kB
function ReplayLoadGameSpecificSave(gameRecord)
Pause("load-replay-save")
LoadGameSessionData(gameRecord.start_save)
Resume("load-replay-save")
end
config.GameReplay_EventsDuringPlaybackExpected = true
if FirstLoad then
origSetTimeFactor = SetTimeFactor
end
local test_time = 50000
if FirstLoad then
GameTesting = false
end
function OnMsg.GameTestsBegin(auto_test)
GameTesting = true
end
function OnMsg.GameTestsEnd(auto_test)
GameTesting = false
end
function OnMsg.Resume()
if GameTesting then __SetTimeFactor(test_time) end
end
function OnMsg.GameReplayStart()
GameRecord = false
SetGameRecording(false)
end
function OnMsg.GameReplaySaved(path)
print("Saved replay " .. path)
GameRecord = false
SetGameRecording(false)
end
local _netFuncsToOverride = { "NetSyncEvent", "NetEchoEvent" }
local _netFuncToNetFuncArray = { ["NetSyncEvent"] = "NetSyncEvents", ["NetEchoEvent"] = "NetEvents" }
local _defaultNetFunc = "NetSyncEvent"
_replayDesynced = false
function IsGameReplayRecording()
return not not GameRecord
end
function StopGameRecord()
if IsValidThread(GameReplayThread) then
DeleteThread(GameReplayThread)
GameReplayThread = false
Resume("UI")
Msg("GameReplayEnd")
GameRecord = false
end
end
function NetSyncEvents.ReplayEnded()
GameTestsPrint("Replay done")
if IsValidThread(GameReplayThread) then DeleteThread(GameReplayThread) end
Msg("GameReplayEnd")
end
function ZuluStartScheduledReplay()
if not GameReplayScheduled then return end
local record = GameReplayScheduled
GameReplayScheduled = false
GameReplay = record
GameReplay.next_hash = 1
GameReplay.next_rand = 1
_replayDesynced = false
GameReplayThread = CreateGameTimeThread(function()
if GameReplayThread ~= CurrentThread() and IsValidThread(GameReplayThread) then
DeleteThread(GameReplayThread)
end
assert(GameTime() == 0)
assert(IsGameTimeThread())
assert(record.map_name == GetMapName())
assert(record.start_rand == MapLoadRandom)
local total_time = Max((record[#record] or empty_table)[RECORD_GTIME] or 0, record.game_time or 0)
Msg("GameReplayStart")
GameTestsPrint("Replay start:", #record, "events", "|", string.format(total_time * 0.001), "sec", "|", "Lua rev", record.lua_rev or 0, "/", LuaRevision, "|", "assets rev", record.assets_rev or 0, "/", AssetsRevision)
for i = 1, #record do
local entry = record[i]
local event, params = entry[RECORD_EVENT], entry[RECORD_PARAM]
local gtime, rtime, etype = entry[RECORD_GTIME], entry[RECORD_RTIME], entry[RECORD_ETYPE]
ScheduleSyncEvent(event, Serialize(UnserializeRecordParams(params)), gtime)
if event == "FenceReceived" then
WaitMsg("ReplayFenceCleared")
end
if i == #record then
ScheduleSyncEvent("ReplayEnded", false, gtime)
end
end
GameReplayThread = CreateGameTimeThread(function()
WaitMsg("GameReplayEnd")
end)
end)
end
function OnMsg.CanSaveGameQuery(query)
query.replay_running = IsGameReplayRunning() or nil
query.replay_recording = IsGameReplayRecording() or nil
end
if FirstLoad then
ContinueOnReplayDesync = not not Platform.trailer
end
local function lReplayDesynced()
_replayDesynced = true
if ContinueOnReplayDesync then
return
end
DeleteThread(GameReplayThread)
Msg("GameReplayEnd", GameReplay)
end
function RegisterGameRecordOverrides()
for i, event_type in ipairs(_netFuncsToOverride) do
CreateRecordedEvent(event_type)
end
CreateRecordedMapLoadRandom()
CreateRecordedGenerateHandle()
if FirstLoad then -- overwrite only on first load as it is a C function
local origNetUpdateNesh = NetUpdateHash
local function hashRecordingUpdate(...)
local hash = origNetUpdateNesh(...)
NetHashRecordingTracker(...)
return hash
end
NetUpdateHash = hashRecordingUpdate
end
local origInteractionRand = InteractionRand
local function RecordedInteractionRand(...)
local rand = origInteractionRand(...)
InteractionRandRecordingTracker(rand, ...)
return rand
end
InteractionRand = RecordedInteractionRand
end
function InteractionRandRecordingTracker(rolledRand, ...)
local playingReplay = IsGameReplayRunning()
local recordingReplay = IsGameReplayRecording()
if not playingReplay and not recordingReplay then return end
local paramsSerialized = Serialize({...})
local hash = xxhash(GameTime(), rolledRand, paramsSerialized)
if playingReplay then
local expectedHashIdx = GameReplay.next_rand
local expectedHashData = GameReplay.rand_list[expectedHashIdx]
local expectedHash = type(expectedHashData) == "table" and expectedHashData[1] or expectedHashData
if not expectedHash and expectedHashIdx > #GameReplay.rand_list then
return -- its over!
end
if hash ~= expectedHash and not _replayDesynced then
--randoms can start before record starts an event, but after load
if expectedHashIdx == 1 and playingReplay and not recordingReplay then
if GameState.loading_savegame or GameState.loading then
return
end
end
local params = {...}
GameTestsError("Replay desynced @", GameTime(), "Rand expected", expectedHash, "but got", hash, " expectedHashIdx", expectedHashIdx)
print("incoming :", GameTime(), rolledRand, GetStack())
print("expected :", expectedHashData[4], expectedHashData[3], expectedHashData[5])
lReplayDesynced()
end
GameReplay.next_rand = expectedHashIdx + 1
elseif GameRecord and GameRecord.rand_list then
--when rand_list gets serialized for saving, valid objs will get serialized as PlaceObject(..., which will then place them when the file is booted;
--get rid of those now;
local params = {...}
for i = #params, 1, -1 do
local o = params[i]
if IsValid(o) then
params[i] = string.format("Obj with class %s and handle %d", o.class, o:HasMember("handle") and o.handle or "N/A")
end
end
GameRecord.rand_list[#GameRecord.rand_list + 1] = { hash, params, rolledRand, GameTime(), GetStack() }
end
end
function Dbg_StringToBytesAsString(str) -- For use with paramsSerialized
return table.concat(({str}), ", ")
end
function NetHashRecordingTracker(...)
local params = ({...})
if GameReplayScheduled then
if params[1] == "NewMapLoaded" then
ZuluStartScheduledReplay()
end
end
local playingReplay = IsGameReplayRunning()
local recordingReplay = IsGameReplayRecording()
if not playingReplay and not recordingReplay then return end
-- Old replays dont have these captured.
if GameReplay and GameReplay.lua_rev == 327744 then
if params[1] == "ResetInteractionRand" or params[1] == "InteractionRand" then
return
end
end
-- Replay over event
if playingReplay and params and params[1] == "SyncEvent" and params[2] == "ReplayEnded" then
return
end
local paramsSerialized = Serialize(params) -- for debugging
local netHashVal = NetGetHashValue()
local hash = xxhash(GameTime(), netHashVal)
assert(netHashVal ~= 1)
-- Check if the incoming hash is the same as the next expected hash if replaying,
-- or record it if recording.
if playingReplay then
local expectedHashIdx = GameReplay.next_hash
local expectedHashData = GameReplay.hash_list[expectedHashIdx]
local expectedHash = type(expectedHashData) == "table" and expectedHashData[1] or expectedHashData
if not expectedHash and expectedHashIdx > #GameReplay.hash_list then
return -- its over, no need to check anymore!
end
if hash ~= expectedHash and not _replayDesynced then
GameTestsError("Replay desynced @", GameTime(), "Hash expected", expectedHash, "but got", hash)
lReplayDesynced()
end
GameReplay.next_hash = expectedHashIdx + 1
elseif GameRecord and GameRecord.hash_list then
GameRecord.hash_list[#GameRecord.hash_list + 1] = { hash, paramsSerialized, GameTime(), GetStack() }
end
end
function SetGameRecording(val)
config.EnableGameRecording = val
end
function CreateRecordedMapLoadRandom()
local origInitMapLoadRandom = InitMapLoadRandom
InitMapLoadRandom = function()
if GameTime() ~= 0 then -- Coming from InitGameVar and not ChangeMap
return origInitMapLoadRandom()
end
local rand
if GameReplayScheduled then
rand = GameReplayScheduled.start_rand
else
rand = origInitMapLoadRandom()
if mapdata and mapdata.GameLogic and config.EnableGameRecording then
assert(not IsGameReplayRunning())
GameReplay = false
print("Game is being recorded.")
GameRecordScheduled = {
start_rand = rand,
map_name = GetMapName(),
os_time = os.time(),
real_time = RealTime(),
game = Game,
lua_rev = LuaRevision,
assets_rev = AssetsRevision,
handles = {},
version = GameRecordVersion,
hash_list = {},
rand_list = {},
net_update_hash = true
}
end
end
return rand
end
end
function ZuluStartRecordingReplay()
if GameReplayScheduled then return end
CreateRealTimeThread(function()
local save = GatherSessionData():str()
SetGameRecording(true)
assert(not GameRecord)
LoadGameSessionData(save)
GameRecord.start_save = save
assert(GameRecord)
end)
end
local function SuspendAutosave()
config.AutosaveSuspended = true
end
local function ResumeAutosave()
config.AutosaveSuspended = false
end
OnMsg.GameReplayStart = SuspendAutosave
OnMsg.GameRecordingStarted = SuspendAutosave
OnMsg.GameReplayEnd = ResumeAutosave
OnMsg.GameReplaySaved = ResumeAutosave
if FirstLoad then
ShowReplayUI = not not Platform.trailer
ReplayUISpeed = false
end
function OnMsg.GameReplayStart()
ObjModified("replay_ui")
if ShowReplayUI then
ReplayUISpeed = ReplayUISpeed or const.DefaultTimeFactor
SetTimeFactor(ReplayUISpeed)
Pause("UI")
end
end
function OnMsg.GameReplayEnd()
GameRecord = false
ObjModified("replay_ui")
Resume("UI")
SetTimeFactor(const.DefaultTimeFactor)
end
function OnMsg.GameRecordingStarted()
ObjModified("replay_ui")
end
function OnMsg.GameReplaySaved()
ObjModified("replay_ui")
end
-- During replay playback all incoming NetSyncEvents are bypassed.
function PlaybackNetSyncEvent(eventId, ...)
if IsGameReplayRunning() then
NetSyncEvents[eventId](...)
else
NetSyncEvent(eventId, ...)
end
end