File size: 19,617 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
function GetTerrainImage(texture)
	local img = texture or "" --"UI/Editor/" .. texture
	if img ~= "" and not io.exists(img) then
		if string.ends_with(img, "tga", true) then
			img = string.sub(img, 1, string.len(img) - 3) .. "dds"
		end
	end
	return img
end

local save_order_cache = {}
local save_order_class = {}
local save_objects_order = config.SaveObjectsOrder or {}

local function FindSaveOrderByClass(obj)
	local obj_class = obj.class
	local save_order_idx = save_order_cache[obj_class]
	if save_order_idx then
		return save_order_idx, save_order_class[obj_class]
	end
	for i=1,#save_objects_order do
		local classes = save_objects_order[i]
		for j=1,#classes do
			if IsKindOf(obj, classes[j]) then
				save_order_cache[obj_class] = i
				save_order_class[obj_class] = classes[j]
				return i
			end
		end
	end
	save_order_cache[obj_class] = max_int
	save_order_class[obj_class] = ""
	return max_int
end

function CompareObjectsForSave(o1, o2)
	local class1 = FindSaveOrderByClass(o1)
	local class2 = FindSaveOrderByClass(o2)
	if class1 ~= class2 then
		return class1 < class2
	end
	
	local pos_cmp = MortonXYPosCompare(o1, o2)
	if pos_cmp ~= 0 then
		return pos_cmp < 0
	end
	
	return lessthan(rawget(o1, "handle"), rawget(o2, "handle"))
end

function ObjectsToLuaCode(objects, result, GetPropFunc)
	table.sort(objects, CompareObjectsForSave)
	if not IsPStr(result) then
		result = result or {}
		for _, obj in ipairs(objects) do
			result[#result + 1] = obj:__toluacode("", nil, GetPropFunc)
			result[#result + 1] = "\n"
		end
	else
		local class = ""
		for _, obj in ipairs(objects) do
			local _, new_class = FindSaveOrderByClass(obj)
			if new_class ~= class then
				if class ~= "" then
					result:appendf("-- end of objects of class %s\n", class)
				end
				class = new_class
			end
			obj:__toluacode("", result, GetPropFunc)
			result:append("\n")
		end
		if class and class ~= "" then
			result:appendf("-- end of objects of class %s\n", class)
		end
	end
	return result
end

function RemapCollections()
	local collection_map = {}
	local new_col_index = 1
	local max_index_value = const.GameObjectMaxCollectionIndex
	local current_collections = Collections
	for _ , col in pairs(Collections) do
		local col_index = col.Index
		if col_index > 0 and col_index < max_index_value then 
			collection_map[col_index] = col_index
		end
	end
	for _ , col in pairs(Collections) do
		local col_index = col.Index
		if not collection_map[col_index] then 
			while collection_map[new_col_index] do
				new_col_index = new_col_index + 1
			end
			collection_map[col_index] = new_col_index
			new_col_index = new_col_index + 1
		end
	end
	local all_collections = MapGet(true, "Collection")
	for _ , col in ipairs(all_collections) do
		local col_index = col.Index
		col:SetIndex(collection_map[col_index])
	end
end

local ReloadCollectionIndexes = false
function OnMsg.NewMapLoaded()
	if ReloadCollectionIndexes and MapCount(true, "Collection") > 0  then
		RemapCollections()
	end
end

function GetMapObjectsForSaving()
	return MapGet(true, "attached", false, nil, nil,	const.gofPermanent, nil, const.cfLuaObject,
		function(o)
			return not IsKindOf(o, "Collection") 
		end) or empty_table
end

function SaveObjects(filename)
	local code = pstr("", 64*1024)
	
	-- All valid object Collections.
	-- They get serialized first, so that objects being loaded will be able to set their "CollectionIndex" property... properly.
	local ol = Collection.GetValid()
	ObjectsToLuaCode(ol, code)
	
	ol = GetMapObjectsForSaving()
	
	local max_handle = const.HandlesSyncStart or 2000000000
	for _, obj in ipairs(ol) do
		if obj:IsSyncObject() then
			max_handle = Max(max_handle, obj.handle)
		end
	end

	code:appendf("SetNextSyncHandle(%d)\n", max_handle + 1)
	ObjectsToLuaCode(ol, code)
	mapdata.ObjectsHash = xxhash(code)
	code:append("\n\n-- objects without Lua object\n")
	__DumpObjPropsForSave(code)
	code:append("\n")

	local err = AsyncStringToFile(filename, code)
	if err then
		printf("Failed to save \"%s\": %s", filename, err)
	end
end

function MakeMapBackup()
	local max_backup_files = 100
	local fldMap  = GetMap()
	local fldBackup = "EditorBackup/"

	local tFolders = {}
	if not io.exists(fldBackup) then
		io.createpath(fldBackup)
	else
		tFolders = io.listfiles(fldBackup, "*", "folders") or {}
	end

	if #tFolders>=max_backup_files then --Find and remove the oldest
		local str = tFolders[1] -- find
		for i, v in ipairs(tFolders) do
			if v<str then str = v end
		end
		local tFiles = io.listfiles(str) --remove
		for i, v in ipairs(tFiles) do
			os.remove(v)
		end
		os.remove(str)
	end

	local fldBackupName = fldMap:sub(1,-2) -- Remove last'/'
	local i,j = fldBackupName:find("/(%w+)$")
	fldBackupName = fldBackupName:sub((i or 0)+1, -1) --the map name only
	local strData = os.date("%y%m%d%H%M%S")-- curent date and time <year><month><day>-<hour><min><sec>
	fldBackupName = fldBackup..fldBackupName.."-"..strData.."/"

	if not io.exists(fldBackupName) then
		io.createpath(fldBackupName)
	end

	--copy files
	local tMapFiles = io.listfiles(fldMap) or {}
	for _, v in ipairs(tMapFiles) do
		if not string.match(v, "%.hpk") and not string.match(v, "%.be") then
			local f, err = io.open(v,"rb")
			local strFile = ""
			if f then
				local i,j = v:find("/[- _%.%w]+$")
				--print ( v:sub((i or 0)+1,-1) )
				local backup_name = fldBackupName..v:sub((i or 0)+1,-1)
				local f1, err = io.open(backup_name, "wb")
				if f1 then
					while strFile do
						strFile = f:read(2048*1024)
						if strFile then
							f1:write(strFile)
						end
					end
					f1:close()
				else
					print("Cannot open backup file " .. backup_name .. " : " .. err)
				end
				f:close()
			else
				print("Cannot open map file " .. v .. " : " .. err)
			end
		end
	end
end

if FirstLoad then
	EditorSavingThread = false
end

function IsEditorSaving()
	return IsValidThread(EditorSavingThread)
end

function CreateCompatibilityMapCopy()
	local rev = mapdata and mapdata.AssetsRevision or 0
	if rev == 0 then
		return
	end
	local map = GetMapName()
	if IsOldMap(map) then
		return
	end
	-- if the map has been marked as 'published' and its revision dates before the last official build, the compatibility map should be included in future builds
	local force_pack = mapdata.PublishRevision > 0 and rev <= const.LastPublishedAssetsRevision or false
	local default_path = "svnAssets/Source/Maps/" ..  map .. "/"
	local new_map_name = map .. "_old" .. rev
	local new_path = "svnAssets/Source/Maps/" .. new_map_name .. "/"
	io.createpath(new_path)
	SVNAddFile(new_path)
	for _,file_path in ipairs(io.listfiles(default_path)) do
		local file_new_path = string.gsub(file_path, map, new_map_name)
		local err
		if file_path:ends_with("/mapdata.lua", true) then
			local err, str = AsyncFileToString(file_path)
			if not err then
				local idx = str:find("\tid = ", 1, true)
				if idx then
					local insert = "\tCreateRevisionOld = " .. tostring(AssetsRevision) .. ",\n\tForcePackOld = " .. tostring(force_pack) .. ",\n"
					str = str:sub(1, idx - 1) .. insert .. str:sub(idx)
					err = AsyncStringToFile(file_new_path, str)
				end
			end
		else
			err = CopyFile(file_path, file_new_path)
		end
		if err then
			print("Copying " .. file_new_path .. " failed due to: " .. err .. "<newline>Try to do it manually.")
		else
			SVNAddFile(file_new_path)
		end
	end
end

--[[
	options = {
		validate_properties = true/false, -- default false, run property validation for every Object - VERY SLOW
		validate_CObject = true/false, -- default true, validate CObjects
		validate_Object = true/false, -- default true, validate Objects -- just for consistency, ignored, Objects are always validated ;-)
	}
]]

function CheckEssentialWarning(obj)
	if IsKindOf(obj, "CObject") and obj:GetDetailClass() ~= "Essential" and not ObjEssentialCheck(obj) then
		StoreErrorSource(obj, "Non-Essential(with collision surfaces) should have BOTH efCollision AND efApplyToGrids turned off!")
	end
end

function ValidateMapObjects(options)
	DebugPrint("Validating map objects...\n")
	local st = GetPreciseTicks()
	
	SuspendThreadDebugHook("ValidateMapObjects")
	local silentVMEStack = config.SilentVMEStack
	config.SilentVMEStack = true
			
	Msg("ValidateMap")
	local procall = procall 
	
	local options = options or {}
	local validate_properties = options.validate_properties or false
	local validate_CObject = options.validate_CObject or true
	local validate_Object = options.validate_Object or true
	if validate_CObject and not validate_Object then
		assert(not "supported combination - Objects are always validated")
	end
	local gofFlagsAll = const.gofPermanent
	local cfFlagsAll = not validate_CObject and const.cfLuaObject or nil

	local count
	if validate_properties then
		count = MapForEach(true, nil, nil, gofFlagsAll, nil, cfFlagsAll, nil,
			function(obj)
				local msg = obj:GetDiagnosticMessage("verbose")
				if not msg then
					--
				elseif msg[#msg] == "warning" then
					StoreWarningSource(obj, msg[1])
				else
					StoreErrorSource(obj, msg[1])
				end
				CheckEssentialWarning(obj)
			end)
	else
		count = MapForEach(true, nil, nil, gofFlagsAll, nil, cfFlagsAll, nil,
			function(obj)
				local _, err_msg, err_param = procall(obj.GetError, obj)
				local _, warn_msg, warn_param = procall(obj.GetWarning, obj)	

				if err_msg then
					StoreErrorSource(err_param or obj, err_msg)
				end
				if warn_msg then
					StoreWarningSource(warn_param or obj, warn_msg)
				end
				CheckEssentialWarning(obj)
			end)
	end

	ResumeThreadDebugHook("ValidateMapObjects")
	config.SilentVMEStack = silentVMEStack
	
	DebugPrint("Validated", count, "objects in",  GetPreciseTicks() - st, "ms\n")
end

local function save_map(skipBackup, folder, silent)
	folder = folder or GetMap()
	AsyncCreatePath(folder)
	
	local backup_folder
	if Platform.developer and not skipBackup then
		-- do not back up new maps
		if io.exists(folder .. "objects.lua") then
			backup_folder = MakeMapBackup()
		end
	end
	
	Msg("PreSaveMap")
	
	if not silent then
		ValidateMapObjects()
	end
	
	Msg("SaveMap", folder, backup_folder)
	
	local new_terrain_hash = terrain.HashGrids(config.IgnorePassGridInTerrainHash)
	if config.StorePrevTerrainMapVersionOnSave
		and mapdata.GameLogic and mapdata.IsRandomMap
		and mapdata.TerrainHash ~= new_terrain_hash
		and (mapdata.AssetsRevision or 0) > 0
	then
		-- Save compatibility map only if this map is under subversion.
		local _, info = GetSvnInfo(folder)
		if next(info) then
			CreateCompatibilityMapCopy()
		end
	end
	
	local t = GetPreciseTicks()
	SaveObjects(folder .. "objects.lua")
	DebugPrint(string.format("Saved objects in %d ms\n", GetPreciseTicks() - t))
	
	mapdata.TerrainHash = new_terrain_hash
	terrain.Save(folder)
	
	WaitMinimapSaving()
	
	if Platform.developer and (config.SaveEntityList or mapdata.SaveEntityList) then
		SaveMapEntityList(folder .. "entlist.txt")
	end
	
	UpdateMapMaxObjRadius()
	UpdateTerrainStats()
	
	local old_net_hash = mapdata.NetHash
	mapdata.NetHash = xxhash(mapdata.TerrainHash, mapdata.ObjectsHash)
	if old_net_hash ~= mapdata.NetHash then
		mapdata.LuaRevision = LuaRevision
		mapdata.OrgLuaRevision = OrgLuaRevision
		mapdata.AssetsRevision = AssetsRevision
	end
	if folder == GetMap() then
		mapdata:Save()
	end
	
	Msg("PostSaveMap")
	
	EditorSavingThread = false
	Msg("SaveMapDone")
	
	SVNAddFile(io.listfiles(folder))
end

function SaveMap(skipBackup, force, folder, silent)
	if (IsEditorSaving() or not IsEditorActive() or IsChangingMap()) and not force then
		return
	end
	
	PauseInfiniteLoopDetection("SaveMap")
	
	EditorSavingThread = CurrentThread()
	if not silent then print("Saving...") end
	WaitNextFrame(4)
	local start_time = GetPreciseTicks()
	
	if EditedMapVariation then
		XEditorCreateMapPatch(EditedMapVariation:GetMapPatchPath(), "add_to_svn")
	else
		save_map(skipBackup, folder, silent)
	end
	
	if not silent then
		print(EditedMapVariation and "Map variation patch saved in" or "Map saved in", GetPreciseTicks() - start_time, "ms")
	end
	ResumeInfiniteLoopDetection("SaveMap")
end

local function check_radius(obj, radius, surf)
	local max_radius = obj.max_allowed_radius
	if Max(radius, surf) > max_radius then
		StoreErrorSource(obj, string.format("Object too large: %.3f / %.3f m", Max(radius, surf) * 1.0 / guim, max_radius * 1.0 / guim))
		radius = Min(max_radius, radius)
		surf = Min(max_radius, surf)
	end
	return radius, surf
end

function CalcMapMaxObjRadius(enum_flags_all, enum_flags_any, game_flags_all)
	local max_radius_obj, max_surf_obj
	local max_radius, max_surf = 0, 0
	local playbox = GetPlayBox()
	game_flags_all = game_flags_all or const.gofPermanent
	MapForEach("map", enum_flags_all, enum_flags_any, game_flags_all, function(obj, playbox)
		local radius = obj:GetRadius()
		local surf = radius
		if max_surf < radius then
			surf = obj:GetMaxSurfacesRadius2D()
		end
		radius, surf = check_radius(obj, radius, surf)
		if max_radius < radius then
			max_radius, max_radius_obj = radius, obj
		end
		if max_surf < surf and playbox:Dist2D2(obj) <= surf * surf then
			max_surf, max_surf_obj = surf, obj
		end
	end, playbox)
	return max_radius, max_surf, max_radius_obj, max_surf_obj
end

local function max_obj_radius(obj)
	local radius = obj:GetRadius()
	local surf = obj:GetMaxSurfacesRadius2D()
	radius, surf = check_radius(obj, radius, surf)
	for _, attach in ipairs(obj:GetAttaches() or empty_table) do
		local radius_i, surf_i = max_obj_radius(attach)
		radius = Max(radius, radius_i)
		surf = Max(surf, surf_i)
	end
	return radius, surf
end

function UpdateMapMaxObjRadius(obj)
	local radius, surf
	if obj then
		radius, surf = max_obj_radius(obj)
		radius = Max(mapdata.MaxObjRadius, radius)
		if GetPlayBox():Dist2D2(obj) > surf * surf then
			surf = 0
		end
		surf = Max(mapdata.MaxSurfRadius2D, surf)
	else
		radius, surf = CalcMapMaxObjRadius()
	end
	mapdata.MaxObjRadius = radius
	mapdata.MaxSurfRadius2D = surf
	SetMapMaxObjRadius(radius, surf)
end

function UpdateTerrainStats()
	local tavg, tmin, tmax = terrain.GetAreaHeight()
	mapdata.HeightMapAvg = tavg
	mapdata.HeightMapMin = tmin
	mapdata.HeightMapMax = tmax
end

function ShowMapMaxRadiusObj()
	local radius, surf, radius_obj, surf_obj = CalcMapMaxObjRadius()
	EditorViewMapObject(radius_obj, nil, true)
end

function ShowMapMaxSurfObj()
	local radius, surf, radius_obj, surf_obj = CalcMapMaxObjRadius()
	EditorViewMapObject(surf_obj, nil, true)
end

function OnMsg.EditorObjectOperation(op_finished, objs)
	if op_finished then
		for _, obj in ipairs(objs) do
			UpdateMapMaxObjRadius(obj)
		end
	end
end

if Platform.developer then
	ValidateAllMapsThread = false
	
	-- see ValidateMapObjects for documentation of options
	function WaitValidateAllMaps(options, filter)
		ValidateAllMapsThread = CurrentThread()
		local old = LocalStorage.DisableDLC
		SetAllDevDlcs(true)
		
		SuspendThreadDebugHook("ValidateAllMaps")
		
		filter = filter or GameMapFilter
	
		local mapdata = table.filter(MapData, filter)
		local maps = table.keys(mapdata, true)
		GameTestsPrintf("Validating %d maps %s...", #maps, ValueToLuaCode(options))
		for i, map in ipairs(maps) do
			GameTestsPrintf("\n[%d/%d] Validating map \"%s\"", i, #maps, map)
			ChangeMap(map)
			ValidateMapObjects(options)
			WaitGameTimeStart()
		end
		
		LocalStorage.DisableDLC = old
		SaveLocalStorage()
		
		ResumeThreadDebugHook("ValidateAllMaps")
		ValidateAllMapsThread = false
	end
	
	function OnMsg.PostNewMapLoaded(silent)
		if IsValidThread(ValidateAllMapsThread) or config.NoMapValidation then
			return
		end
		if not silent and Platform.desktop then
			ValidateMapObjects()
		end
		UpdateCollectionsEditor()
	end
	function GameTestsNightly.ValidateAllMaps()
		WaitValidateAllMaps{ validate_properties = true, validate_Object = true, validate_CObject = false }
	end
	
	-- example usage: *r WaitResaveAllMapdata(UpdateTerrainStats)
	function WaitResaveAllMapdata(callback, filter)
		if not callback then return end
		
		PauseGame(8)
		SuspendThreadDebugHook("WaitUpdateAllMapdata")
		SuspendFileSystemChanged("WaitUpdateAllMapdata")
		table.change(config, "WaitUpdateAllMapdata", {
			NoMapValidation = true
		})
		
		filter = filter or GameMapFilter
		local datas = table.filter(MapData, filter)
		local i, count = 0, table.count(datas)
		for map, mapdata in sorted_pairs(datas) do
			i = i + 1
			GameTestsPrintf("\n[%d/%d] Updating map \"%s\"", i, count, map)
			local game_logic = mapdata.GameLogic
			mapdata.GameLogic = false
			ChangeMap(map)
			mapdata.GameLogic = game_logic
			if not procall(callback) then
				break
			end
			mapdata:Save()
			DoneMap()
		end
		
		ChangeMap("")
		table.restore(config, "WaitUpdateAllMapdata")
		ResumeFileSystemChanged("WaitUpdateAllMapdata")
		ResumeThreadDebugHook("WaitUpdateAllMapdata")
		ResumeGame(8)
	end
end

function EnterEditorSaveMap()
	CreateRealTimeThread( function()
		while IsChangingMap() or IsEditorSaving() do
			WaitChangeMapDone()
			if IsEditorSaving() then
				WaitMsg("SaveMapDone")
			end
		end
		if not IsEditorActive() then
			EditorActivate()
		end
		SaveMap()
	end)
end

-- rotating objects feature
if FirstLoad then
	rotation_thread = false
	editor.RotatingObjects = {}
end

function OnMsg.GameEnterEditor()
	DeleteThread(rotation_thread)
	rotation_thread = CreateRealTimeThread(function()
		while true do
			for i=1, #editor.RotatingObjects do
				local item = editor.RotatingObjects[i]
				if IsValid(item.obj) then
					item.obj:SetAngle( item.obj:GetVisualAngle() + 60, 100 )
				end
			end
			Sleep(100)
		end
	end)
end

function OnMsg.GameExitEditor()
	DeleteThread(rotation_thread)
	for i = 1, #editor.RotatingObjects do
		local item = editor.RotatingObjects[i]
		if IsValid(item.obj) then
			item.obj:SetAngle(60 * item.angle)
		end
	end
	editor.RotatingObjects = {}
end

if Platform.developer then
	function DumpEntitiesSurfaces()
		local out = {}
		local visited = {}

		ClassDescendants("CObject", function(class_name, class, out, visited)
			local entity = class:GetEntity()

			if visited[entity] then	return end
			visited[entity] = 1

			local num_col, num_occ = GetEntityNumSurfaces(entity, EntitySurfaces.Collision), GetEntityNumSurfaces(entity, EntitySurfaces.Occluder)
			if num_col ~= 0 or num_occ ~= 0 then
				local s = entity .. '\t\thas ' .. num_col .. ' collision and ' .. num_occ .. ' occlusion surfs'
				out[#out + 1] = s
			end
		end, out, visited)

		table.sort(out)
		local f = io.open('surfs.txt', 'w')
		for _, l in ipairs(out) do
			f:write(l .. '\r\n')
		end
		f:close()
	end

	function RemoveAllOccluders()
		for _, obj in ipairs(MapGet("map", "CObject")) do
			obj:SetOccludes(false)
		end
	end
end

--------------------------------------------------------------------------------------------

function SelectSameFloorObjects(sel)
	local objs = MapGet("map", "attached", false, "collection", editor.GetLockedCollectionIdx(), true)
	local same_floor = {}
	local oztop, ozbottom = -1, 9999*guim
	if IsValid(sel) then
		sel = { sel }
	end
	
	for i=1,#sel do
		local o = sel[i]
		local ocenter, oradius = o:GetBSphere()
		local oz = o:GetVisualPos():z()
		local obbox = GetEntityBoundingBox(o:GetEntity())
		oztop = Max(oztop, oz + obbox:max():z() + 50 * guic)
		ozbottom = Min(ozbottom, oz  + obbox:min():z()- 50 * guic)
	end
	
	for i=1,#objs do
		local p = objs[i]
		local pz = p:GetVisualPos():z()
		local pbbox = GetEntityBoundingBox(p:GetEntity())

		local pztop = pz + pbbox:max():z()
		local pzbottom = pz  + pbbox:min():z()
		
		if pztop < oztop and pzbottom > ozbottom then
			same_floor[1+#same_floor] = objs[i]
		end
	end
	
	editor.ClearSel()
	editor.AddToSel(same_floor)
end