File size: 30,576 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
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
DefineClass.BehaviorFilter = {
	__parents = { "GedFilter" },
	properties = {
		{ id = "bins", name = "Bins", editor = "set", items = { "A", "B", "C", "D", "E", "F", "G", "H" }, max_items_in_set = 1 },
	},
	bins = set(),
}

function BehaviorFilter:FilterObject(o)
	if not self.bins or not IsSet(self.bins) then return false end
	
	for bin, value in pairs(self.bins) do
		if value and o:HasMember("bins") and not o.bins[bin] then
			return false -- filter item
		end
	end

	return true -- don't filter item
end

DefineClass.ParticleSystemSubItem = {
	__parents = { "InitDone" },
	EditorName = "Particle System Element",
	EditorSubmenu = "Other",
}

DefineClass.ParticleSystemPreset = {
	__parents = { "Preset" },
	properties = {
		{ id = "ui", name = "UI Particle System" , editor = "bool", default = false },
		{ id = "simul_fps", name = "Simul FPS", editor = "number", slider = true, min = 1, max = 60, help = "The simulation framerate" },
		{ id = "speed_up", name = "Speed-up", editor = "number", slider = true, min = 10, max = 10000, scale = 1000, help = "How many times the particle simulation is being sped up." }, 
		{ id = "presim_time", name = "Presim time", editor = "number", scale = "sec", help = "How many seconds to presimulate, before showing the system for the first time", min = 0, max = 120000, slider = true },
		{ id = "max_initial_catchup_time", name = "Initial Catchup Time", editor = "number", scale = 1000, help = "How much work should newly created renderobjs do before displaying them. 0 here is equivalent to ignore_game_object_age"},
		{ id = "rand_start_time", name = "Max starting phase time", editor = "number", scale = "sec", help = "Maximum additional presim time used to randomize the starting phase of the particle system.", min = 0, max = 10000, slider = true },
		
		{ id = "distance_bias", name = "Camera distance offset", editor = "number", min = -100000, max = 100000, scale = 1000, help = "How much to offset the particle system relative to the camera. It is used to make the particle system to appear always on top of some other transparent object.", slider = true },
		{ id = "particles_scale_with_object", name = "Particles scale with object", editor = "bool", help = "Particle size scales with the object scale" },
		{ id = "game_time_animated", name = "Game time animated", editor = "bool", help = "Will animate in game time, i.e. will pause when the game is paused or slowed down", read_only = function(self) return self.ui end },
		{ id = "ignore_game_object_age", name = "Ignore GameObject age", editor = "bool", default = false, help = "Long running particles remember when they were created, and try to catch up if they lose their state. Enables/disables this behaviour.", read_only = function(self) return self.ui end },
		{ id = "vanish", name = "Vanish when killed", editor = "bool", help = "Fx will disappear completely when destroyed, without waiting the existing particles to reach their age" },
		{ id = "post_lighting", name = "Post Lighting" , editor = "bool", default = false, help = "Render after lighting related post processing." },
		{ id = "stable_cam_distance", name = "Stable Camera Distance" , editor = "bool", default = false, help = "Render in consistent order while the camera & the particle system are not moving. This has a performance penalty for systems with particles far away from the center." },
		{ id = "testcode", category = "Custom Test Setup", name = "Test code", editor = "func", params = "self, ged, enabled", default = false, no_edit = function(self) return not self.ui end, },
	},
	
	simul_fps = 30,
	speed_up = 1000,
	presim_time = 0,
	max_initial_catchup_time = 2000, -- 0 is a better default, but 2000 is the "old" one, so we don't have to rework parsys
	rand_start_time = 0,
	distance_bias = 0,
	particles_scale_with_object = false,
	game_time_animated = false,
	vanish = false,
	
	
	-- Preset settings
	SingleFile = false,
	Actions = false,
	GlobalMap = "ParticleSystemPresets",
	ContainerClass = "ParticleSystemSubItem", -- can be ParticleBehavior or ParticleParam
	SubItemFilterClass = "BehaviorFilter",
	GedEditor = "GedParticleEditor",
	SingleGedEditorInstance = false,
	EditorMenubarName = false, 
	EditorMenubar = false,
}

if FirstLoad then
	ParticleSystemPreset_FXDetailThreshold = false
end

function ParticleSystemPreset:GetTextureFolders()
	return {
		{"svnAssets/Source/Textures/Particles/"}
	}
end

function ParticleSystemPreset:GetTextureBasePath()
	return "svnAssets/Source/"
end

function ParticleSystemPreset:GetTextureTargetPath()
	return "Textures/Particles/"
end

function ParticleSystemPreset:GetTextureTargetGamePath()
	return "Textures/Particles"
end

function ParticleSystemPreset:DynamicParams()
	return self:EditorData().dynamic_params or false
end

function ParticleSystemPreset:RefreshThread()
	return self:EditorData().refresh_thread or false
end

function ParticleSystemPreset:OverrideEmitterFuncs()
end

function GedOpOpenParticleEditor(ged, obj, locked)
	obj:OpenEditor(locked)
end

function ParticleSystemPreset:OpenEditor(lock_preset)
	if not IsRealTimeThread() then
		CreateRealTimeThread(ParticleSystemPreset.OpenEditor, self, lock_preset)
		return
	end
	lock_preset = not not lock_preset

	local context = ParticleSystemPreset:EditorContext()
	context.lock_preset = lock_preset
	local ged = OpenPresetEditor("ParticleSystemPreset", context)
	ged:SetSelection("root", PresetGetPath(self))
end

function GedListParticleSystemBehaviors(obj, filter, format, restrict_class)
	if not IsKindOf(obj, "ParticleSystemPreset") then
		return {}
	end
	
	local format = T{format}
	local objects, ids = {}, {}
	for i = 1, #obj do
		local item = obj[i]
		objects[#objects + 1] = type(item) == "string" and item or _InternalTranslate(format, item, false)
		ids[#ids + 1] = tostring(item)
	end
	
	-- Do filtering
	local filtered = {}
	if filter then
		for i = 1, #obj do
			local item = obj[i]
			-- filter by bins
			for bin, value in pairs(filter.bins) do
				if value and item:HasMember("bins") and not item.bins[bin] then
					filtered[i] = true
				end
			end
		end
	end
	objects.filtered = filtered
	objects.ids = ids
	return objects
end

if FirstLoad then
	l_streams_to_update = false
	g_ParticleLuaDefsLoaded = false
end

local load_lua_defs = Platform.developer and Platform.pc and IsFSUnpacked()

local particle_editors = {}
function OnMsg.GedOpened(ged_id)
	local gedApp = GedConnections[ged_id]
	if gedApp and gedApp.app_template == "GedParticleEditor" then
		hr.TrackParticleTimes = 1
		ParticleSystemPreset_FXDetailThreshold = hr.FXDetailThreshold
		
		if load_lua_defs and not g_ParticleLuaDefsLoaded then
			LoadLuaParticleSystemPresets()
			
			if Platform.developer then
				ParticleSystemPreset.TryUpdateKnownInvalidStreams(l_streams_to_update)
			end
		end
		
		particle_editors[ged_id] = true
	end
end

function OnMsg.GedClosing(ged_id)
	if particle_editors[ged_id] then
		hr.FXDetailThreshold = ParticleSystemPreset_FXDetailThreshold
		particle_editors[ged_id] = nil
	end
end

if FirstLoad then
	UIParticlesTestControl = false
	UIParticlesTestId = false
end

local function UpdateGedStatus()
	GedSetUiStatus("select_xcontrol", "Select UI control to attach this particle to.")
end

function GedTestUIParticle(ged, enabled)
	if UIParticlesTestControl and UIParticlesTestControl.window_state == "open" and UIParticlesTestControl:HasParticle(UIParticlesTestId) then
		UIParticlesTestControl:KillParSystem(UIParticlesTestId)
		UIParticlesTestControl = false
		UIParticlesTestId = false
		return
	end
	UIParticlesTestControl = false
	UIParticlesTestId = false

	local particle_sys = ged:ResolveObj("SelectedPreset")
	if not particle_sys then
		XRolloverMode(false)
		return
	end
	
	if particle_sys.testcode then
		particle_sys.testcode(particle_sys, ged, enabled)
		return
	end

	if not enabled then
		XRolloverMode(false)
		return
	end

	if not particle_sys.ui then
		ged:ShowMessage("Invalid selection", "Select a UI particle first!")
		XRolloverMode(false)
		return
	end

	GedSetUiStatus("select_xcontrol", "Select UI control to attach this particle to.")
	XRolloverMode(true, function(window, status)
		if window and window:IsKindOf("XControl") then
			XFlashWindow(window)
			if status == "done" then
				-- spawn particles
				UIParticlesTestControl = window
				UIParticlesTestId = window:AddParSystem(UIParticlesTestId, particle_sys.id, UIParticleInstance:new({
					lifetime = -1,
				}))
			end
		end
		if status == "done" or status == "cancel" then
			GedSetUiStatus("select_xcontrol")
		end
	end)
end

function GedSetParticleEmitDetail(ged, detail_name)
	local levels = OptionsData.Options.Effects
	local idx = table.find(levels, "value", detail_name)
	assert(idx)

	EngineOptions.Effects = levels[idx].value
	Options.ApplyEngineOptions(EngineOptions)

	local selected_preset = ged:ResolveObj("SelectedPreset")
	if selected_preset and selected_preset:IsKindOf("ParticleSystemPreset") then
		if selected_preset.ui then
			if UIParticlesTestControl and UIParticlesTestControl:HasParticle(UIParticlesTestId) then
				UIParticlesTestControl:KillParSystem(UIParticlesTestId)
			end
		else
			selected_preset:ResetParSystemInstances()
		end
	end
	print(string.format("Particle detail level '%s' preview set.", levels[idx].value))

	local detail_level_names = {"Low", "Medium", "High", "Ultra"}
	for i = 1, 4 do
		ged:Send("rfnApp", "SetActionToggled", "Preview" .. detail_level_names[i], detail_level_names[i] == levels[idx].value)
	end
end

if FirstLoad then
	ParticleSystemPresetCommitThread = false
end
function GedParticleSystemPresetCommit()
	ParticleSystemPresetCommitThread = IsValidThread(ParticleSystemPresetCommitThread) or CreateRealTimeThread(function()
		local assets_path = ConvertToOSPath("svnAssets/")
		local project_path = ConvertToOSPath("svnProject/")
		local err, exit_code = AsyncExec(string.format("cmd /c %s/Build TexturesParticles-win32", project_path))
		if err then 
			assert("Failed to build particle textures" and false)
			return
		end
		local err, exit_code = AsyncExec("cmd /c Build ParticlesSeparateFallbacks", project_path, true, true)
		if not err and exit_code == 0 then
			print("Fallbacks updated.")
		else
			print("Fallbacks failed to update", err, exit_code)
			assert("Failed to build particle fallbacks" and false)
			return
		end
		err, exit_code = AsyncExec(string.format("cmd /c TortoiseProc /command:commit /path:%s", assets_path))
		if err then assert("Failed to open commit dialog" and false) end
	end)
end

function ParticleSystemPreset:GetPresetStatusText()
	if IsValidThread(UpdateTexturesListThread) or IsValidThread(ParticleSystemPresetCommitThread) then
		return "Compress tasks in progress..."
	end
	return ""
end

function ParticleSystemPreset:SwitchParam(obj, prop, id)
	obj:ToggleProperty(prop, self:DynamicParams())
	ObjModified(obj)
end

function ParticleSystemPreset:BindParam(idx, userdata)
	local param = self[idx]
	local dp = { index = userdata, type = param.type, default_value = param.default_value }
		
	if param.type == "number" then
		dp.size = 1
	elseif param.type == "color" then
		dp.size = 1
	elseif param.type == "point" then
		dp.size = 3
	elseif param.type == "bool" then
		dp.size = 1
	end
	
	self:DynamicParams()[param.label] = dp
	
	return dp.size
end

function ParticleSystemPreset:BindParams()
	local idx = 1 -- start at 1 because custom data [0] is the particle system creation time
	self:EditorData().dynamic_params = {}
	for i = 1, #self do
		if IsKindOf(self[i], "ParticleParam") then
			idx = idx + self:BindParam(i, idx)
			if idx > const.CustomDataCount - 1 then
				print(string.format("warning: parameter %s exceeded the available userdata values!", self[i].label))
			end
		end
	end
end

function ParticleSystemPreset:EnableDynamicToggles()
	for i = 1, #self do
		if self[i]:IsKindOf("ParticleBehavior") then
			self[i]:EnableDynamicToggle(self:DynamicParams())
		end
	end
end

function ParticleSystemPreset:BindParamsAndUpdateProperties()
	self:BindParams()
	self:EnableDynamicToggles()
end

function OnMsg.DataLoading()
	for _, folder in ipairs(ParticleDirectories()) do
		LoadingBlacklist[folder] = true
	end
end

function LoadLuaParticleSystemPresets()
	if g_ParticleLuaDefsLoaded then
		return g_ParticleLuaDefsLoaded
	end
	
	local start_time = GetPreciseTicks()

	for _, folder in ipairs(ParticleDirectories()) do
		LoadingBlacklist[folder] = false
		LoadPresetFolder(folder)
	end

	local old_load_lua_defs = load_lua_defs
	load_lua_defs = false
	local count = 0
	ForEachPreset("ParticleSystemPreset", function(preset)
		preset:PostLoad()
		count = count + 1
	end)
	load_lua_defs = old_load_lua_defs

	if Platform.developer then
		--print("Loaded", count, "particle lua defs in", GetPreciseTicks() - start_time, "ms");
	end

	ObjModified(Presets.ParticleSystemPreset)


	-- Replace bins with lua defs in the engine. Log which happened to be different (outdated bins) and in case we want to resave them later
	local streams_to_update = ParticlesReload(false, false)
	l_streams_to_update = l_streams_to_update or {}
	for _, parsys in ipairs(streams_to_update) do
		local err = ParticleSystemPresets[parsys]:TestStream()
		if err then
			GameTestsError("ParSys", parsys, "error: ", err)
			table.insert(l_streams_to_update, parsys)
		end
	end

	g_ParticleLuaDefsLoaded = true
	return g_ParticleLuaDefsLoaded
end

function OnMsg.DataLoaded()
	local failed_to_load = {}
	for _, folder in ipairs(ParticleDirectories()) do
		LoadStreamParticlesFromDir(folder, failed_to_load)
	end
	l_streams_to_update = failed_to_load


	if load_lua_defs then
		LoadLuaParticleSystemPresets()
	end
end

function ParticleSystemPreset:OnEditorNew(parent, ged, is_paste)
	if load_lua_defs then
		ParticlesReload(self.id)
	end
	g_PresetLastSavePaths[self] = nil
	if Platform.editor then
		XEditorUpdateObjectPalette()
	end
end

function ParticleSystemPreset:OnEditorSelect(now_selected, ged)
	if now_selected then
		self:RefreshBehaviorUsageIndicators()
		self:BindParamsAndUpdateProperties()
		ged:Send("rfnApp", "SetIsUIParticle", self.ui)
	else
		self:ResetParSystemInstances()
	end
end

function BinAssetsUpdateInvalidParticleStreams()
	ParticleUpdateBinaryStreams()
end


function ParticleSystemPreset.TryUpdateKnownInvalidStreams(l_streams_to_update)
	if not l_streams_to_update or #l_streams_to_update == 0 then
		return
	end
	local streams_to_update = table.copy(l_streams_to_update)
	table.clear(l_streams_to_update)
	CreateRealTimeThread(function()
		local changed_outlines = ParticleUpdateOutlines()
		if #changed_outlines > 0 then
			print("Saving", #changed_outlines, "particles with modified outlines")
			for i=1,#changed_outlines do
				SaveParticleSystem(changed_outlines[i])
			end
			QueueCompressParticleTextures()
		end
		if #streams_to_update > 0 then
			ParticleNameListSaveToStream(streams_to_update)
		end
		ParticleUpdateBinaryStreams("create_missing_only")
	end)
end

function ParticleSystemPreset:RefreshBehaviorUsageIndicators(do_now)
	local editor_data = self:EditorData()
	local refresh_func = function(self)
		-- Search the behaviors to identify those with disabled emitters.
		-- Scheduled in a thread to avoid executing simultaneous refresh requests.
		for i = 1, #self do
			local behavior = self[i]
			if behavior:IsKindOf("ParticleBehavior") and not behavior:IsKindOf("ParticleEmitter") then
				local behavior_bins = behavior.bins
				local active_emitters = 0
				for j = 1, #self do
					local emitter = self[j]
					if emitter:IsKindOf("ParticleEmitter") and emitter.enabled then
						local emitter_bins = emitter.bins
						for bin, value in pairs(emitter_bins) do
							if value and behavior_bins[bin] then
								active_emitters = active_emitters + 1
							end
						end
					end
				end
				local new_active = active_emitters > 0
				if new_active ~= behavior.active then
					behavior.active = new_active
					ObjModified(self)
				end
				
				local flags = ParticlesGetBehaviorFlags(self.id, i - 1)
				if flags then
					local str = ""
					flags["emitter"] = nil
					for name, active in sorted_pairs(flags) do
						if GetDarkModeSetting() then active = not active end
						local color = active and RGB(74, 74, 74) or RGB(192, 192, 192)
						local r,g,b = GetRGB(color)
						str = string.format("%s<color %s %s %s>%s</color>", str, r,g,b, string.sub(name, 1, 1))
					end
					behavior.flags_label = str
				end
			end
		end
		editor_data.refresh_thread = nil
	end
	
	local refresh_thread = self:RefreshThread()
	if do_now and not refresh_thread then
		refresh_func(self)
	else
		editor_data.refresh_thread = refresh_thread or CreateRealTimeThread(refresh_func, self)
	end
end

function ParticleSystemPreset:PostLoad()
	Preset.PostLoad(self)
	
	if Platform.developer then self:CheckIntegrity() end
	self:BindParams()
	if load_lua_defs then ParticlesReload(self.id) end
end

function ParticleSystemPreset:CheckIntegrity()
	local count = table.maxn(self)
	for i = count, 1, -1 do
		if not rawget(self, i) then
			assert(false, "Particle system '" .. self.name .. "' initialization failed at: " .. tostring(i))
			self[i] = false
			table.remove(self, i)
		end
	end
end

local function KillParticlesWithName(name)
	if UIParticlesTestControl and UIParticlesTestControl:GetParticleName(UIParticlesTestId) == name then
		UIParticlesTestControl = false
		UIParticlesTestId = false
	end

	local xcontrols = GetChildrenOfKind(terminal.desktop, "XControl")
	for _, control in ipairs(xcontrols) do
		control:KillParticlesWithName(name)
	end
end

function OnMsg.GedPropertyEdited(ged_id, object, prop_id, old_value)
	if not GedConnections[ged_id] then return end

	local parent = GetParentTableOfKindNoCheck(object, "ParticleSystemPreset")
	if object:IsKindOf("ParticleParam") and parent then
		parent:BindParamsAndUpdateProperties()
		g_DynamicParamsDefs = {} -- invalidate the cached params
		ParticlesReload(parent:GetId())
	end
	if object:IsKindOf("ParticleSystemPreset") and prop_id == "Id" then
		KillParticlesWithName(old_value)
		ParticlesReload()
		ObjModified(GedConnections[ged_id]:ResolveObj("root"))
		if Platform.editor then
			XEditorUpdateObjectPalette()
		end
	elseif (object:IsKindOf("ParticleBehavior") or object:IsKindOf("ParticleSystemPreset")) and parent then
		parent:RefreshBehaviorUsageIndicators()
		if object:IsKindOf("ParticleEmitter") and object:IsOutlineProp(prop_id) then
			object:GenerateOutlines("forced")
		end
		ParticlesReload(parent:GetId())
		g_DynamicParamsDefs = {} -- invalidate the cached params
	end
end

function ParticleSystemPreset:OnEditorSetProperty(prop_id, old_value, ged)
	if prop_id == "ui" then
		for _, behaviour in ipairs(self) do
			if IsKindOf(behaviour, "ParticleEmitter") then
				behaviour:Setui(self.ui)
			end
		end
	end
	ParticlesReload(self:GetId())
	self:RefreshBehaviorUsageIndicators()
	
	if prop_id == "NewBehavior" then
		ParticleSystemPreset.ActionAddBehavior(ged:ResolveObj("root"), self, prop_id, ged)
	end

	Preset.OnEditorSetProperty(self, prop_id, old_value, ged)
end

function ParticleSystemPreset:ResetParSystemInstances()
	MapForEach("map", "ParSystem",
		function(x)
			if x:GetParticlesName() == self.id then
				x:SetParticlesName(self.id) -- resets time and flags in GO
				x:DestroyRenderObj()
			end
		end)
end

function GedResetAllParticleSystemInstances()
	MapForEach("map", "ParSystem", function(obj)
		obj:SetParticlesName(obj:GetParticlesName())
		obj:DestroyRenderObj()
	end)
end

function ParticleSystemPreset:SaveToStream(bin_name, skip_adding_to_svn)
	bin_name = bin_name or self:GetBinFileName()
	local id = self:GetId()
	if bin_name ~= "" then
		ParticlesReload(self:GetId(), false)
		local count, err = ParticlesSaveToStream(bin_name, id, true)
		self:EditorData().stream_error = err
		if err then
			printf("\"%s\" while trying to persist \"%s\" in \"%s\"", err, id, bin_name)
			return false, err
		end
		if not skip_adding_to_svn then
			SVNAddFile(bin_name)
		end
	end
	return bin_name
end

function ParticleSystemPreset:DeleteStream(bin_name)
	bin_name = bin_name or self:GetBinFileName()
	if bin_name ~= "" then
		SVNDeleteFile(bin_name)
	end
end

function ParticleSystemPreset:GetBinFileName(id)
	local path = self:GetSavePath()
	if not path or path == "" then return "" end
	return path:gsub(".lua$", ".bin")
end

function ParticleSystemPreset:TestStream()
	local binPath = self:GetBinFileName()
	if binPath then
		local id = self.id
		local err = ParticlesTestStream(binPath, id)
		self:EditorData().stream_error = err
		if err then
			return err
		end
	end
end

function ParticleSystemPreset:GetError()
	if #self > 55 then
		return "Too many particle behaviors."
	elseif #self < 1 then
		return "There are no particle behaviors. Please add some."
	end
	if self.ui then
		-- TODO: Figure out how this code can go in the ParticleEmitter
		for _, behavior in ipairs(self) do
			if IsKindOf(behavior, "ParticleEmitter") then
				if behavior.softness ~= 0 and behavior.enabled then
					return "A particle emitter with softness > 0 found. They are not supported in UI particles."
				end
			end
		end
	end
	local editor_data = self:EditorData()
	if editor_data.stream_error then
		return "Persist error:" .. editor_data.stream_error
	end
end
 
ParticleSystemPreset.ReloadWaitThread = false 


function ParticleSystemPreset:AddSourceTexturesToSVN()
	local textures = {}
	for i = 1, #self do
		local behavior = self[i]
		if IsKindOf(behavior, "ParticleEmitter") then
			if behavior.texture ~= "" then textures[#textures + 1] = "svnAssets/Source/" .. behavior.texture end
			if behavior.normalmap ~= "" then textures[#textures + 1] = "svnAssets/Source/" .. behavior.normalmap end
		end
	end
	if #textures > 0 then
		SVNAddFile(textures)
	end
end

function ParticleSystemPreset:OnPreSave(user_requested)
	local file_exists = io.exists(self:GetBinFileName())
	local last_save_path = g_PresetLastSavePaths[self]
	
	for i = 1, #self do
		local behavior = self[i]
		if IsKindOf(behavior, "ParticleEmitter") then
			behavior:GenerateOutlines()
		end
	end

	self:BindParamsAndUpdateProperties() -- generates the "fake" properties for dynamic params
	
	-- remove no longer used bin files.
	if last_save_path and last_save_path ~= self:GetSavePath() then
		local old_bin_path = (last_save_path or ""):gsub(".lua", ".bin")
		if old_bin_path ~= "" then
			self:DeleteStream(old_bin_path)
		end
	end

	QueueCompressParticleTextures()
	self:AddSourceTexturesToSVN()
	self:SaveToStream(false, not "dont_add_to_svn")
	
	print(#self, "particle behaviors in", self:GetId(), "saved")
	ObjModified(self)
end 

function ParticleSystemPreset:OnEditorDelete(...)
	self:DeleteStream()
end

function GetParticleBehaviorsCombo()
	local list = {}
	ClassDescendants("ParticleBehavior", function(name, class_def, list)
		if rawget(class_def, "EditorName") then
			list[#list + 1] = {value = name, text = class_def.EditorName}
		end
	end, list)
	table.sortby_field(list, "text")
	table.insert(list, 1, {value = "", text = ""})
	return list
end

function ParticleUpdateOutlines()
	local updated = {}
	ClearOutlinesCache()
	local list = GetParticleSystemList()
	for i = 1,#list do
		local parsys = list[i]
		for j = 1, #parsys do
			local behavior = parsys[j]
			if IsKindOf(behavior, "ParticleEmitter") then
				local success, generated = behavior:GenerateOutlines("update")
				if success then
					updated[parsys] = true
				end
				if generated and CanYield() then
					Sleep(10)
				end
			end
		end
	end
	updated = table.keys(updated)
	table.sort(updated, function(a, b) return CmpLower(a.id, b.id) end)
	return updated
end

function ParticleUpdateBinaryStreams(create_missing_only)
	if not g_ParticleLuaDefsLoaded then
		print("ParticleUpdateBinaryStreams: Particle defs not loaded.")
		return
	end

	local existing = {}
	
	for _, folder in ipairs(ParticleDirectories()) do
		local err, files = AsyncListFiles(folder, "*.bin")
		if err then
			print("Particle files listing failed:", err)
		else
			for i = 1, #files do
				existing[files[i]] = true
			end
		end
	end
	
	local particle_systems = GetParticleSystemList()
	
	local streams = {}
	local to_create = {}
	for i = 1, #particle_systems do
		local parsys = particle_systems[i]
		local stream = parsys:GetBinFileName()
		streams[stream] = true
		if not create_missing_only or not existing[stream] then
			to_create[#to_create + 1] = parsys
		end
	end
	
	if #to_create > 0 then
		local created = {}
		print("Creating", #to_create, "particle streams...")
		for i = 1, #to_create do
			local parsys = to_create[i]
			local stream = parsys:GetBinFileName()
			local success, err = parsys:SaveToStream(stream, "skip adding to svn")
			if success then
				created[#created + 1] = stream
			end
		end
		SVNAddFile(created)
		print("Created", #created, "/", #to_create, "particle streams.")
	end
	--[[
	if #updated > 0 then
		print(#updated, "particle stream(s) saved:")
		for i=1,#updated do
			print("\t", i, updated[i])
		end
	end
	--]]
	
	local to_delete = {}
	for stream in pairs(existing) do
		if not streams[stream] then
			to_delete[#to_delete + 1] = stream
		end
	end
	if #to_delete > 0 then
		print("Deleting", #to_delete, "particle streams...")
		local result, err = SVNDeleteFile(to_delete)
		if not result then
			err = err or ""
			printf("Failed to delete binary streams! %s", tostring(err))
		end 
	end
end

function ParticleSystemPreset:Getname()
	return self.id
end

function LoadStreamParticlesFromDir(dir, failed_to_load)
	local err, files = AsyncListFiles(dir, "*.bin")
	if err then
		print("Particle files listing failed:", err, " directory ", dir)
	else
		local start = GetPreciseTicks()
		local success = 0
		local failed_due_to_ver = 0
		for i = 1, #files do
			local err, count = ParticlesLoadFromStream(files[i])
			if not err and count ~= 0 then
				success = success + 1
			elseif err == "persist_version" then
				failed_due_to_ver = failed_due_to_ver + 1

				local _, parsys, __ = SplitPath(files[i])
				table.insert(failed_to_load, parsys)
			else
				print("Particles", files[i], "loading failed!", err)
				local _, parsys, __ = SplitPath(files[i])
				table.insert(failed_to_load, parsys)
			end
		end
		DebugPrint(print_format(success, "/", #files, "particle streams loaded in", GetPreciseTicks() - start, "ms.\n"))
		if failed_due_to_ver > 0 then
			print("Particle streams could not be loaded. Using", failed_due_to_ver, "lua descriptions instead. Reason: Persist version mismatch.")
		end
	end
end

function ParticleNameListSaveToStream(streams_to_update)
	local updated = {}
	print("Updating", #streams_to_update, "particle streams...")
	for i = 1, #streams_to_update do
		local parsys = GetParticleSystem(streams_to_update[i])
		if parsys then
			local success, err = parsys:SaveToStream()
			if success then
				updated[#updated + 1] = parsys
			end
		end
	end
	print("Updated", #updated, "/", #streams_to_update, "particle streams.")
end

function CheckParticleTextures()
	local source_path = "svnAssets/Source/Textures/Particles/"
	local packed_path = "Textures/Particles/"
	if not io.exists(source_path) then
		print("You need to checkout source textures for particles.")
		return {}
	end
	local err, rel_paths = AsyncListFiles(source_path, "*", "relative, recursive")
	local packed_paths = {}
	table.map(rel_paths, function(rel_path) packed_paths[#packed_paths+1] = packed_path .. rel_path end)
	
	local refs = {}
	local refs_lower = {}
	local instances = GetParticleSystemList()
	for i=1, #instances do
		local parsystem = instances[i]
		for b=1, #parsystem do
			local behavior = parsystem[b]
			if IsKindOf(behavior, "ParticleEmitter") then
				refs[behavior.texture] = parsystem:GetId()
				refs[behavior.normalmap] = parsystem:GetId()
				refs_lower[string.lower(behavior.texture)] = parsystem:GetId()
				refs_lower[string.lower(behavior.normalmap)] = parsystem:GetId()
			end
		end
	end	
	refs[""] = nil
	
	local unref = {}
	local present = {}
	local present_lower = {}
	local missing = {}
	local wrong_casing = {}
	
	for i=1, #packed_paths do
		local texture = packed_paths[i]
		local texture_lower = string.lower(texture)
		present[texture] = true
		present_lower[texture_lower] = true
		if not refs[texture] then
			if refs_lower[texture_lower] then
				wrong_casing[#wrong_casing+1] = texture
			else
				unref[#unref+1] = texture
			end
		end
	end
	for texture, parsys in pairs(refs) do
		if not present_lower[string.lower(texture)] then
			missing[texture] = parsys
		end
	end
	return refs, unref, wrong_casing, missing
end

if FirstLoad then
	UpdateTexturesListThread = false
end
function QueueCompressParticleTextures()
	if Platform.ged then return end
	if UpdateTexturesListThread then
		DeleteThread(UpdateTexturesListThread)
		UpdateTexturesListThread = false
	end
	UpdateTexturesListThread = CreateRealTimeThread(function()
		Sleep(300)
		local filepath = "svnProject/Data/ParticleSystemPreset/Textures.txt"
		local refs, _, wrong_casing, missing = CheckParticleTextures()
		local idx = {}
		local full_os_path = ConvertToOSPath("svnAssets/Source/"):gsub("\\", "/")
		local os_path = string.match(full_os_path, "/([^/]+/Source/)$")
		for texture, _ in sorted_pairs(refs) do
			if not missing[texture] and not table.find(wrong_casing, texture) then
				idx[#idx+1] = texture .. "=" .. os_path .. texture
			end
		end	
		AsyncStringToFile(filepath, table.concat(idx, "\r\n"))
		print("Textures.txt updated")

		local dir = ConvertToOSPath("svnProject/")
		local err, exit_code, other = AsyncExec("cmd /c Build TexturesParticles", dir, true, true)
		if err or exit_code ~= 0 then
			print("Particles failed to compress", err, exit_code, other)
		end

		local err, exit_code = AsyncExec("cmd /c Build ParticlesSeparateFallbacks", dir, true, true)
		if not err and exit_code == 0 then
			print("Fallbacks updated.")
		else
			print("Fallbacks failed to update", err, exit_code)
		end

		UpdateTexturesListThread = false
	end)
end



DefineClass.ParticleSystem = {
	__parents = {"PropertyObject"},
	StoreAsTable = false,
	properties = {{id = 'name', editor = 'text', default = '', },},
	
	simul_fps = 30,
	speed_up = 1000,
	presim_time = 0,
	max_initial_catchup_time = 2000,
	rand_start_time = 0,
	distance_bias = 0,
	particles_scale_with_object = false,
	game_time_animated = false,
	vanish = false,
}

function OnMsg.ClassesGenerate()
	table.iappend(ParticleSystem.properties, ParticleSystemPreset.properties)
end

function ParticleSystem:__fromluacode(...)
	local obj = PropertyObject.__fromluacode(self, ...)
	local converted = ParticleSystemPreset:new(obj)
	converted:SetId(obj.name)
	converted:SetGroup("Default")
	return converted
end