File size: 20,068 Bytes
b6a38d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
DefineClass("TestValidator")

IsGameReplayRunning = empty_func

if Platform.developer then
	RecursiveCallMethods.GetTestData = "call"
end

function IsGameRecordingSupported()
	return config.SupportGameRecording and Libs.Network == "sync"
end

GameRecordVersion = 2

function ApplyRecordVersion(record)
	local version = not record and GameRecordVersion or record.version or 1
	if version == GameRecordCurrentVersion then
		return
	end
	GameRecordCurrentVersion = version
	if version == 1 then
		RECORD_GTIME = 1
		RECORD_EVENT = 2
		RECORD_PARAM = 3
		--RECORD_RAND = 4
		--RECORD_HANDLE = 5
		RECORD_RTIME = 6
		RECORD_ETYPE = 7
		--RECORD_HASH = 8
		RECORD_SPEED = 9
	else
		RECORD_GTIME = 1
		RECORD_EVENT = 2
		RECORD_PARAM = 3
		RECORD_RTIME = 4
		RECORD_ETYPE = 5
		RECORD_SPEED = 6
	end
end

if FirstLoad then
	GameRecordCurrentVersion = false
	ApplyRecordVersion()
end

config.GameRecordsPath = "AppData/GameRecords"
local records_path = config.GameRecordsPath

MapVar("GameRecord", false) -- to be available in the saves
MapVar("GameReplay", false)
MapVar("GameReplayThread", false)

if FirstLoad then
	GameRecordScheduled = false
	
	GameReplayScheduled = false
	GameReplayPath = false
	GameReplaySaveLoading = false
	GameReplayUnresolved = false
	GameReplayWaitMap = false
	GameReplayToInject = false
	GameReplayFastForward = false

	GameRecordSaveRequests = false
end

function IsGameReplayRunning()
	return IsValidThread(GameReplayThread) and GameReplay
end

if not IsGameRecordingSupported() then
	return
end

local IsGameReplayRunning = IsGameReplayRunning

function OnMsg.ChangeMap()
	GameReplayPath = false
	GameRecordScheduled = false
	StopGameReplay()
end
 
function OnMsg.NewMapLoaded()
	GameReplay = false
	GameRecord = GameRecordScheduled
	GameRecordScheduled = false
	if not not GameRecord then
		Msg("GameRecordingStarted")
	end
end

function OnMsg.NetGameJoined()
	GameRecord = false
end

function OnMsg.GameTestsBegin(auto_test)
	table.change(config, "GameTests_GameRecording", {
		EnableGameRecording = false,
	})
end

function OnMsg.GameTestsEnd(auto_test)
	table.restore(config, "GameTests_GameRecording", true)
end

function SerializeRecordParams(...)
	return SerializeEx(const.SerializeCustom, CustomSerialize, ...)
end

function UnserializeRecordParams(params_str)
	return UnserializeEx(const.SerializeCustom, CustomUnserialize, params_str)
end

function PrepareRecordForSaving(record)
	record = record or GameRecord
	if record ~= GameRecord then return end
	record.game_time = GameTime()
end

function PlayGameRecord(record, start_idx)
	record = record or GameReplayScheduled
	if not record then return end
	assert(IsGameTimeThread())
	assert(record.start_rand == MapLoadRandom)
	start_idx = start_idx or 1
	GameReplay = record
	GameReplayScheduled = false
	GameReplaySaveLoading = false
	GameReplayThread = CurrentThread()
	if GameReplayWaitMap then
		WaitWakeup() -- ensure the map loading is complete
	end
	ApplyRecordVersion(record)
	local desync_any
	local total_time = Max((record[#record] or empty_table)[RECORD_GTIME] or 0, record.game_time or 0)
	local start_time = GameTime()
	if start_idx > #record or start_time > record[start_idx][RECORD_GTIME] then
		GameTestsPrint("Replay injection start mismatch!")
		start_idx = 1
		while start_idx <= #record and start_time > record[start_idx][RECORD_GTIME] do
			start_idx = start_idx + 1
		end
	end
	local version = record.version or 1
	GameTestsPrint("Replay start at", Min(start_idx, #record), "/", #record, "events", "|", start_time, "/", total_time, "ms", "|", "Lua rev", record.lua_rev or 0, "/", LuaRevision, "|", "assets rev", record.assets_rev or 0, "/", AssetsRevision)
	for i = start_idx,#record do
		local event_time = record[i][RECORD_GTIME]
		local delay = event_time - now()
		local yield
		if delay > 0 then
			yield = record[i][RECORD_SPEED] == 0
			Sleep(delay)
		else
			local last_record = record[i - 1]
			local prev_real_time = last_record and last_record[RECORD_RTIME] or record.real_time
			yield = prev_real_time ~= record[i][RECORD_RTIME]
		end
		if yield then
			-- make sure all game time threads created by the previous event have been started 
			WaitAllOtherThreads()
		end
		if GameReplayThread ~= CurrentThread() or GameReplay ~= record then
			return
		end
		print("Replay", i, '/', #record)
		CreateGameTimeThread(function(record, i)
			local entry = record[i]
			local event, params_str = entry[RECORD_EVENT], entry[RECORD_PARAM]
			GameReplayUnresolved = false
			local success, err = ExecuteSyncEvent(event, UnserializeRecordParams(params_str))
			if not success then
				GameTestsError("Replay", i, '/', #record, event, err)
			end
			if GameReplayUnresolved then
				GameTestsPrint("Replay", i, '/', #record, event, "unresolved objects:")
				for _, data in ipairs(GameReplayUnresolved) do
					local handle, class, pos = table.unpack(data)
					GameTestsPrint("\t", class, handle, "at", pos)
				end
			end
			Msg("GameRecordPlayed", i, record)
		end, record, i)
	end
	Sleep((record.game_time or 0) - now())
	Sleep(0) Sleep(0) Sleep(0) Sleep(0)
	
	GameTestsPrint("Replay finished")
	Msg("GameReplayEnd", record)
end

local function IsSameClass(obj, class)
	if not obj then
		return -- object handle changed or object not yet spawned
	end
	if not class or obj.class == class then
		return true
	end
	local classdef = g_Classes[class]
	return not classdef or IsKindOf(classdef, obj.class) or IsKindOf(obj, class) -- recorded class renamed
end

function CustomSerialize(obj)
	local handle = obj.handle
	if handle and IsValid(obj) and obj:IsValidPos() then
		if obj:GetGameFlags(const.gofSyncObject | const.gofPermanent) == 0 then
			StoreErrorSource(obj, "Async object in sync event")
		end
		local record = GameRecord
		local handles = record and record.handles
		if handles then
			local class = obj.class
			handles[handle] = class
			return { handle, class, obj:GetPos() }
		end
	end
end

function CustomUnserialize(tbl)
	local handle, class, pos = table.unpack(tbl)
	local obj = HandleToObject[handle]
	if IsSameClass(obj, class) then
		return obj
	end
	local map_obj = MapGetFirst(pos, 0, class)
	if map_obj then
		return map_obj
	end
	GameReplayUnresolved = table.create_add(GameReplayUnresolved, tbl)
end

function CreateRecordedEvent(event_type)
	local origGameEvent = _G[event_type]
	_G[event_type] = function(event, ...)
		if IsGameReplayRunning() then
			if not config.GameReplay_EventsDuringPlaybackExpected then
				print("Ignoring", event_type, event, "during replay!")
			end

			return
		end
		local record = GameRecord
		if record then
			local params, err = SerializeRecordParams(...)
			assert(params, err)
			CreateGameTimeThread(function(event, event_type, record, params)
				-- in a thread to have the correct sync values (as the event will be started in a thread)
				local time = GameTime()
				local n = #record
				while n > 0 and record[n][RECORD_GTIME] > time do
					record[n] = nil
					n = n - 1
				end
				n = n + 1
				record[n] = {
					[RECORD_GTIME] = time,
					[RECORD_EVENT] = event,
					[RECORD_PARAM] = params,
					[RECORD_RTIME] = RealTime(),
					[RECORD_ETYPE] = event_type,
					[RECORD_SPEED] = GetTimeFactor(),
				}
				if config.GameRecordingAutoSave then 
					SaveGameRecord()
				end
			end, event, event_type, record, params)
		end
		return origGameEvent(event, ...)
	end
end

function CreateRecordedMapLoadRandom()
	local origInitMapLoadRandom = InitMapLoadRandom
	InitMapLoadRandom = function()
		local rand
		if GameReplayScheduled then
			CreateGameTimeThread(PlayGameRecord)
			rand = GameReplayScheduled.start_rand
		else
			rand = origInitMapLoadRandom()
			if mapdata and mapdata.GameLogic and config.EnableGameRecording then
				assert(Game)
				assert(not IsGameReplayRunning())
				GameReplay = false
				GameRecordScheduled = {
					start_rand = rand,
					map_name = GetMapName(),
					map_hash = mapdata.NetHash,
					os_time = os.time(),
					real_time = RealTime(),
					game = CloneObject(Game),
					lua_rev = LuaRevision,
					assets_rev = AssetsRevision,
					handles = {},
					version = GameRecordVersion,
					net_update_hash = config.DebugReplayDesync,
				}
				Msg("GameRecordScheduled")
			end
		end
		return rand
	end
end

function CreateRecordedGenerateHandle()
	local origGenerateSyncHandle = GenerateSyncHandle
	GenerateSyncHandle = function(self)
		local h0, h = NextSyncHandle, origGenerateSyncHandle()
		if IsGameReplayRunning() and GameReplay.NextSyncHandle and GameReplay.handles and not IsSameClass(self, GameReplay.handles[h]) then
			-- the handle should be associated with another object, not yet spawn because of a previois desync
			NextSyncHandle = GameReplay.NextSyncHandle
			h = origGenerateSyncHandle()
			NextSyncHandle = h0
		end
		return h
	end
end

function RegisterGameRecordOverrides()
	CreateRecordedEvent("NetSyncEvent")
	CreateRecordedMapLoadRandom()
	CreateRecordedGenerateHandle()
end

function OnMsg.ClassesGenerate()
	RegisterGameRecordOverrides()
	GameTests.PlayGameRecords = TestGameRecords
end

function TestGameRecords()
	local list = {}
	for _, test in ipairs(GetHGProjectTests()) do
		for _, info in ipairs(test.test_replays) do
			local name = string.format("%s.%s", test.id, info:GetRecordName())
			if info.Disabled then
				GameTestsPrint("Skipping disabled test replay", name)
			else
				list[#list + 1] = {name = name, record = info.Record}
			end
		end
	end
	GameTestsPrintf("Found %d game records to test.", #(list or ""))
	for i, entry in ipairs(list) do
		GameTestsPrintf("Testing record %d/%d %s", i, #list, entry.name)
		local st = RealTime()
		local err, record = LoadGameRecord(entry.record)
		if err then
			GameTestsPrintf("Failed to load record %s", entry.record, err)
		else
			err = ReplayGameRecord(record)
			if err then
				GameTestsError("Replay error:", err)
			elseif not WaitReplayEnd() then
				GameTestsError("Replay timeout.")
			else
				GameTestsPrint("Replay finished in:", RealTime() - st, "ms")
			end
		end
	end
end

local function GenerateRecordPath(record)
	local err = AsyncCreatePath(records_path)
	if err then 
		assert(err)
		return false
	end
	record = record or GameRecord
	local name = string.format("Record_%s_%s", os.date("%Y_%m_%d_%H_%M_%S", record.os_time), GetMapName())
	if record.continue then
		name = name .. "_continue"
	end
	return string.format("%s/%s.lua", records_path, name)
end

function SaveGameRecord()
	if not GameRecord then return end
	local path = GameReplayPath or GenerateRecordPath(GameRecord)
	GameRecord.NextSyncHandle = NextSyncHandle
	if not GameRecordSaveRequests then
		GameRecordSaveRequests = {}
		CreateRealTimeThread(function()
			while true do
				for path, record in pairs(GameRecordSaveRequests) do
					GameRecordSaveRequests[path] = nil
					WaitSaveGameRecord(path, record)
				end
				if not next(GameRecordSaveRequests) then
					GameRecordSaveRequests = false
					break
				end
			end
		end)
	end
	GameRecordSaveRequests[path] = GameRecord
end

function WaitSaveGameRecord(path, record)
	record = record or GameRecord
	if not record then return end
	PrepareRecordForSaving(record)
	local code = pstr("return ", 64*1024)
	TableToLuaCode(record, nil, code)
	path = path or GenerateRecordPath(record)
	local err = AsyncStringToFile(path, code)
	if err then return err end
	Msg("GameReplaySaved", path)
	return nil, path
end

function ReplayGameRecord(record)
	record = record or not IsGameReplayRunning() and GameRecord or GameReplay
	local err
	if type(record) == "string" then
		local _, _, ext = SplitPath(record)
		local path = ext ~= "" and record or string.format("%s/%s.lua", records_path, record)
		err, record = LoadGameRecord(path)
	end
	if not record then
		return err or "No record found!"
	end
	PrepareRecordForSaving(record)
	Msg("GameRecordEnd", record)
	StopGameReplay()
	Msg("GameReplayStart", record)
	GameReplayScheduled = record
	if record.start_save then
		GameReplaySaveLoading = true
		GameReplayThread = CreateRealTimeThread(ReplayLoadGameSpecificSave, record)
	else
		CloseMenuDialogs()
		CreateRealTimeThread(function()
			LoadingScreenOpen("idLoadingScreen", "ReplayGameRecord")
			GameReplayWaitMap = true
			local game = CloneObject(record.game)
			ChangeMap("")
			NewGame(game)
			local map_name = record.map_name
			local map_hash = record.map_hash
			if map_hash and map_hash ~= table.get(MapData, map_name, "NetHash") then
				local matched
				for map, data in sorted_pairs(MapData) do
					if map_hash == data.NetHash then
						matched = map
						break
					end
				end
				if not matched then
					GameTestsPrint("Replay map has been modified!")
				elseif matched ~= map_name then
					GameTestsPrint("Replay map changed to", matched)
				end
				map_name = matched or map_name
			end
			ChangeMap(map_name)
			GameReplayWaitMap = false
			Wakeup(GameReplayThread)
			LoadingScreenClose("idLoadingScreen", "ReplayGameRecord")
		end)
	end
end

function ResaveGameRecord(path)
	local _, _, ext = SplitPath(path)
	local path = ext ~= "" and path or string.format("%s/%s.lua", records_path, path)
	local err, record = LoadGameRecord(path)
	if not record then
		return err or "No record found!"
	end
	StopGameReplay()
	if record.start_save then
		assert(false, "Not implemented!")
	else
		CloseMenuDialogs()
		CreateRealTimeThread(function()
			table.change(config, "ResaveGameRecord", {
				FixedMapLoadRandom = record.start_rand,
				StartGameOnPause = true,
			})
			ChangeMap(record.map_name)
			table.restore(config, "ResaveGameRecord")
			GameReplayPath = path
		end)
	end
end

function WaitReplayEnd()
	while not WaitMsg("GameReplayEnd", 100) do
		if not IsChangingMap() and not IsValidThread(GameReplayThread) then
			return
		end
	end
	return true
end

function StopGameReplay()
	local thread = not GameReplaySaveLoading and GameReplayThread
	if not IsValidThread(thread) then return end
	GameReplayScheduled = false
	GameReplayThread = false
	Msg("GameReplayEnd")
	DeleteThread(thread, true)
	return true
end

function LoadGameRecord(path)
	local func, err = loadfile(path, nil, _ENV)
	if not func then
		return err
	end
	local success, record = procall(func)
	if not success then return record end
	return nil, record
end

function OnMsg.LoadGame()
	if not GameReplayToInject then
		return
	end
	StopGameReplay()
	if not GameRecord then
		print("Replay injection failed: No saved record found (maybe a saved game during a replay?)")
		return
	end
	for _, key in ipairs{"start_rand", "map_name", "os_time", "lua_rev", "assets_rev"} do
		if GameRecord[key] ~= GameReplayToInject[key] then
			print("Replay injection failed: Wrong game!")
			return
		end
	end
	print("Replay Injection Success.")
	CreateGameTimeThread(PlayGameRecord, GameReplayToInject, #GameRecord + 1)
	GameReplayToInject = false
end

function ToggleGameReplayInjection(record)
	record = record or GameRecord or GameReplay
	if record and record == GameReplayToInject then
		record = false
		print("Replay Injection Cancelled")
	elseif record then
		print("Replay Injection Ready")
	else
		print("No record found to inject")
	end
	GameReplayToInject = record
end

----

function ReplayLoadGameSpecificSave(save, callbackOnload)
	print("You must implement your game loading function in ReplayLoadGameSpecificSave to use game replays with saves.")
	return true
end

function ReplayToggleFastForward(set)
	if not IsGameReplayRunning() then
		set = false
	elseif set == nil then
		set = not GameReplayFastForward
	end
	if GameReplayFastForward == set then
		return
	end
	GameReplayFastForward = set
	TurboSpeed(set, true)
end

function OnMsg.GameReplayEnd(record)
	if GameReplayFastForward then
		ReplayToggleFastForward()
	end
	-- allow to continue the recording
	if record then
		record.continue = true
		GameRecord = record
	end
end

function OnMsg.GameRecordPlayed(i, record)
	if record and GameReplayFastForward then
		local events_before_end = config.ReplayFastForwardBeforeEnd or 10
		if i >= #record - events_before_end then
			print(events_before_end, "events before the end reached, stopping fast forward...")
			ReplayToggleFastForward()
			SetTimeFactor(0)
		end
	end
end

----

if config.DebugReplayDesync then

if FirstLoad then
	GameRecordSyncLog = false
	GameRecordSyncTest = false
	GameRecordSyncIdx = false
	HashLogSize = 32
end

function OnMsg.AutorunEnd()
	pairs = totally_async_pairs
end

function OnMsg.ReloadLua()
	pairs = g_old_pairs
end

function OnMsg.NetUpdateHashReasons(enable_reasons)
	local record = GameRecord or GameRecordScheduled or GameReplay or GameReplayScheduled
	enable_reasons.GameRecord = record and record.net_update_hash and true or nil
end

function OnMsg.LoadGame()
	GameRecordSyncLog = false
end

local function StartSyncLogSaving(replay)
	local err = AsyncCreatePath("AppData/ReplaySyncLogs")
	if err then
		print("Failed to create NetHashLogs folder:", err)
		return
	end
	GameRecordSyncLog = true
	GameRecordSyncTest = replay and ((GameRecordSyncTest or 0) + 1) or false
	GameRecordSyncIdx = 1
end

function OnMsg.GameRecordScheduled()
	StartSyncLogSaving()
end

function OnMsg.GameReplayStart()
	StartSyncLogSaving(true)
end

function OnMsg.GameReplayEnd(record)
	NetSaveHashLog("E", "Replay", GameRecordSyncTest)
	GameRecordSyncLog = false
end

function OnMsg.GameRecordEnd(record)
	NetSaveHashLog("E", "Record")
	GameRecordSyncLog = false
end

function OnMsg.SyncEvent()
	if not GameRecordSyncIdx then
		return
	end
	if GameRecordSyncTest then
		NetSaveHashLog(GameRecordSyncIdx, "Replay", GameRecordSyncTest)
	else
		NetSaveHashLog(GameRecordSyncIdx, "Record")
	end
	GameRecordSyncIdx = GameRecordSyncIdx + 1
end

function NetSaveHashLog(prefix, logtype, suffix)
	if not GameRecordSyncLog then
		return
	end
	local str = pstr("")
	NetGetHashLog(str)
	if #str == 0 then
		return
	end
	local path = string.format("AppData/ReplaySyncLogs/%s%s%s.log", (prefix and tostring(prefix) .. "_" or ""), logtype, suffix and ("_" .. tostring(suffix)) or "")
	CreateRealTimeThread(function(path, str)
		local err = AsyncStringToFile(path, str)
		if err then
			printf("Failed to save %s: %s", path, err)
		end
	end, path, str)
end

function OnMsg.BugReportStart(print_func)
	local replay = IsGameReplayRunning()
	if not replay then return end
	print_func("\nGame replay running:", replay.lua_rev, replay.assets_rev)
end

end -- config.DebugReplayDesync

if Platform.developer then

function TestValidator:CollectTestData()
	local data = {}
	if Platform.developer then
		self:GetTestData(data)
	end
	return data
end

function TestValidator:rfnTestData(orig_data)
	local new_data = self:CollectTestData()
	local ignore_missing = true -- avoid breaking existing tests when adding or removing test validation entries
	if table.equal_values(orig_data, new_data, -1, ignore_missing) then
		return
	end
	GameTestsError("Test data validation failed for", self.class,
		"\n--- Orig data: ", ValueToStr(orig_data),
		"\n--- New data: ", ValueToStr(new_data))
end

function TestValidator:CreateValidation()
	local data = self:CollectTestData()
	if next(data) == nil then
		print("No test data to validate!")
		return
	end
	NetSyncEvent("ObjFunc", self, "rfnTestData", data)
	print("Test data collected:", ValueToStr(data))
end

function TestValidator:AsyncCheatValidate()
	self:CreateValidation()
end

function ReplayCreateUpdateScript()
	local record = GameReplay or GameRecord
	if not record then
		print("No replay running!")
		return
	end

	local lua_rev = record.lua_rev or LuaRevision
	local assets_rev = record.assets_rev or AssetsRevision
	
	local src_path = ConvertToOSPath("svnSrc/")
	local assets_path = ConvertToOSPath("svnAssets/")
	
	local scrip = {
		"@echo off",
		"cd " .. src_path,
		"svn cleanup",
		"svn up -r " .. lua_rev,
		"cd " .. assets_path,
		"svn cleanup",
		"svn up -r " .. assets_rev,
	}
	local path = string.format("%sUpdateToRev_%d_%d.bat", src_path, lua_rev, assets_rev)
	local err = AsyncStringToFile(path, table.concat(scrip, "\n"))
	if err then
		print("Failed to create script:", err)
	else
		print("Script created at:", path)
	end
end

end -- Platform.developer