File size: 47,781 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
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
function GetSpotOffset(obj, name, idx, state, phase)
	assert(obj)
	if not IsValid(obj) then
		return 0, 0, 0, "obj"
	end
	idx = idx or obj:GetSpotBeginIndex(name) or -1
	if idx == -1 then
		return 0, 0, 0, "spot"
	end
	state = state or "idle"
	phase = phase or 0
	local x, y, z = GetEntitySpotPos(obj, state, phase, idx, idx, true):xyz()
	local s = obj:GetWorldScale()
	if s ~= 100 then
		x, y, z = x * s / 100, y * s / 100, z * s / 100
	end
	return x, y, z
end

function GetLocalAngleDiff(attach, local_angle)
	return abs(AngleDiff(attach:GetVisualAngleLocal(), local_angle))
end

function GetLocalRotationTime(attach, local_angle, speed)
	return MulDivRound(1000, GetLocalAngleDiff(attach, local_angle), speed)
end

function GetLocalAngle(obj, angle)
	return AngleDiff(angle, obj:GetAngle())
end

----

DefineClass.CompositeBodyPart = {
	__parents = { "ComponentAnim", "ComponentAttach", "ColorizableObject" },
	flags = { gofSyncState = true, efWalkable = false, efApplyToGrids = false, efCollision = false, efSelectable = true },
}

function CompositeBodyPart:GetName()
	local parent = self:GetParent()
	while IsValid(parent) do
		if IsKindOf(parent, "CompositeBody") then
			for name, part in pairs(parent.attached_parts) do
				if part == self then
					return name
				end
			end
			return
		else
			parent = parent:GetParent()
		end
	end
end

local function RecomposeBody(obj)
	for name, part in pairs(obj.attached_parts) do
		if part ~= obj then
			obj:RemoveBodyPart(part, name)
		end
	end
	obj.attached_parts = nil
	obj:ComposeBodyParts()
end
		
local function EditorRecomposeBodiesOnMap(obj, root, prop_id, ged)
	if IsValid(obj) then
		RecomposeBody(obj)
	elseif obj.object_class then
		MapForEach("map", obj.object_class, RecomposeBody)
	end
end

local function get_body_parts_count(self)
	local class_name = self.id
	local class = g_Classes[class_name] or empty_table
	local target = self.composite_part_target or class.composite_part_target or class_name
	local composite_part_groups = self.composite_part_groups or class.composite_part_groups or { class_name }
	local part_presets = Presets.CompositeBodyPreset
	local count = 0
	for _, part_name in ipairs(self.composite_part_names or class.composite_part_names) do
		for _, part_group in ipairs(composite_part_groups) do
			for _, part_preset in ipairs(part_presets[part_group] or empty_table) do
				if (not target or part_preset.Target == target) and (part_preset.Parts or empty_table)[part_name] then
					count = count + 1
				end
			end
		end
	end
	return count
end

-- Composite bodies change the entity, scale and colors of the unit."
DefineClass.CompositeBody = {
	__parents = { "Object", "CompositeBodyPart" },

	properties = {
		{ category = "Composite Body", id = "recompose",                name = "Recompose", editor = "buttons", default = false, template = true, buttons = { { name = "Recompose", func = function(...) return EditorRecomposeBodiesOnMap(...) end, } } },
		{ category = "Composite Body", id = "composite_part_names",     name = "Parts", editor = "string_list", template = true, help = "Composite body parts. Each body preset may cover one or more parts. Each part may have another part as a parent and a custom attach spot.", body_part_match = true },
		{ category = "Composite Body", id = "composite_part_main",      name = "Main Part", editor = "choice", items = PropGetter("composite_part_names"), template = true, help = "Main body part to be applied directly to the composite object." },
		{ category = "Composite Body", id = "composite_part_target",    name = "Target", editor = "text", template = true, help = "Will match composite body presets having the same target. If not specified, the class name is used.", body_part_match = true },
		{ category = "Composite Body", id = "composite_part_groups",    name = "Groups", editor = "string_list", items = PresetGroupsCombo("CompositeBodyPreset"), template = true, help = "Will match composite body presets from those groups. If not specified, the class name is used as a group name.", body_part_match = true },
		{ category = "Composite Body", id = "CompositePartCount",       name = "Parts Found", editor = "number", template = true, default = 0, dont_save = true, read_only = 0, getter = get_body_parts_count },
		{ category = "Composite Body", id = "composite_part_parent",    name = "Parent", editor = "prop_table", read_only = true, template = true, help = "Defines custom parent for each body part." },
		{ category = "Composite Body", id = "composite_part_spots",     name = "Spots", editor = "prop_table", read_only = true, template = true, help = "Defines custom attach spots for each body part." },
		{ category = "Composite Body", id = "cycle_colors",             name = "Cycle Colors", editor = "bool", default = false, template = true, help = "If you can cycle through the composite body colors during construction.", },
	},

	flags = { gofSyncState = false, gofPropagateState = true },
	
	composite_seed = false,
	colorization_offset = 0,
	composite_part_target = false,
	composite_part_names = { "Body" },
	composite_part_spots = false,
	composite_part_parent = false,
	composite_part_main = "Body",
	composite_part_groups = false,
	
	attached_parts = false,
	override_parts = false,
	override_parts_spot = false,
	
	InitBodyParts = empty_func,
	SetAutoAttachMode = empty_func,
	ChangeEntityDisabled = empty_func,
}

function CompositeBody:CheatCompose()
	self:ComposeBodyParts()
end

local props = CompositeBody.properties
for i=1,10 do
	local category = "Composite Body Hierarchy"
	local function no_edit(self)
		local names = self:GetProperty("composite_part_names") or empty_table
		local name = names[i]
		return not name or name == self:GetProperty("composite_part_main")
	end
	local function GetPartName(self)
		local names = self:GetProperty("composite_part_names")
		return names[i] or ""
	end
	local function GetSpotName(self)
		local name = GetPartName(self)
		return name .. " Spot"
	end
	local function GetParentName(self)
		local name = GetPartName(self)
		return name .. " Parent"
	end
	local spot_id = "composite_part_spot_" .. i
	local parent_id = "composite_part_parent_" .. i
	local function getter(self, prop_id)
		local target_id
		if prop_id == spot_id then
			target_id = "composite_part_spots"
		elseif prop_id == parent_id then
			target_id = "composite_part_parent"
		else
			return ""
		end
		local name = GetPartName(self)
		local map = self:GetProperty(target_id)
		return map and map[name] or ""
	end
	local function setter(self, value, prop_id)
		local target_id
		if prop_id == spot_id then
			target_id = "composite_part_spots"
		elseif prop_id == parent_id then
			target_id = "composite_part_parent"
		else
			return
		end
		local name = GetPartName(self)
		local map = self:GetProperty(target_id) or empty_table
		map = table.raw_copy(map)
		map[name] = (value or "") ~= "" and value or nil
		rawset(self, target_id, map)
	end
	local function GetParentItems(self)
		local names = self:GetProperty("composite_part_names") or empty_table
		if names[i] then
			names = table.icopy(names)
			table.remove_value(names, names[i])
		end
		return names, return_true
	end
	table.iappend(props, {
		{ category = category, id = spot_id, name = GetSpotName, editor = "text", default = "", dont_save = true, getter = getter, setter = setter, no_edit = no_edit, template = true },
		{ category = category, id = parent_id, name = GetParentName, editor = "choice", default = "", items = GetParentItems, dont_save = true, getter = getter, setter = setter, no_edit = no_edit, template = true },
	})
	CompositeBody["Get" .. spot_id] = function(self)
		return getter(self, spot_id)
	end
	CompositeBody["Get" .. parent_id] = function(self)
		return getter(self, parent_id)
	end
	CompositeBody["Set" .. spot_id] = function(self, value)
		return setter(self, spot_id, value)
	end
	CompositeBody["Set" .. parent_id] = function(self, value)
		return setter(self, parent_id, value)
	end
end

function CompositeBody:Done()
	-- allow garbage collection of CompositeBody objects which otherwise have a non-weak reference to themselves
	self.attached_parts = nil
	self.override_parts = nil
end

function CompositeBody:GetPart(name)
	local parts = self.attached_parts
	return parts and parts[name]
end

function CompositeBody:GetPartName(part_to_find)
	for name, part in pairs(self.attached_parts) do
		if part == part_to_find then
			return name
		end
	end
end

function CompositeBody:ForEachBodyPart(func, ...)
	local attached_parts = self.attached_parts or empty_table
	for _, name in ipairs(self.composite_part_names) do
		local part = attached_parts[name]
		if part then
			func(part, self, ...)
		end
	end
end

function CompositeBody:UpdateEntity()
	return self:ComposeBodyParts()
end

local function ResolveCompositeMainEntity(classdef)
	if not classdef then return end
	local composite_part_groups = classdef.composite_part_groups
	local composite_part_group = composite_part_groups and composite_part_groups[1] or classdef.class
	local part_presets = table.get(Presets, "CompositeBodyPreset", composite_part_group)
	if next(part_presets) then
		local composite_part_target = classdef.composite_part_target
		local composite_part_main = classdef.composite_part_main or "Body"
		for _, part_preset in ipairs(part_presets) do
			if not composite_part_target or composite_part_target == part_preset.Target then
				if (part_preset.Parts or empty_table)[composite_part_main] then
					return part_preset.Entity
				end
			end
		end
	end
	return classdef.entity or classdef.class
end

function ResolveTemplateEntity(self)
	local entity = IsValid(self) and self:GetEntity()
	if IsValidEntity(entity) then
		return entity
	end
	local class = self.id or self.class
	local classdef = g_Classes[class]
	if not classdef then return end
	entity = ResolveCompositeMainEntity(classdef)
	return IsValidEntity(entity) and entity
end

function TemplateSpotItems(self)
	local entity = ResolveTemplateEntity(self)
	if not entity then return {} end
	local spots = {{ value = false, text = "" }}
	local seen = {}
	local spbeg, spend = GetAllSpots(entity)
	for spot = spbeg, spend do
		local name = GetSpotName(entity, spot)
		if not seen[name] then
			seen[name] = true
			spots[#spots + 1] = { value = name, text = name }
		end
	end
	table.sortby_field(spots, "text")
	return spots
end

function CompositeBody:CollectBodyParts(part_to_preset, seed)
	local target = self.composite_part_target or self.class
	local composite_part_groups = self.composite_part_groups or { self.class }
	local part_presets = Presets.CompositeBodyPreset
	for _, part_name in ipairs(self.composite_part_names) do
		if not part_to_preset[part_name] then
			local matched_preset, matched_presets
			for _, part_group in ipairs(composite_part_groups) do
				for _, part_preset in ipairs(part_presets[part_group]) do
					if (not target or part_preset.Target == target) and (part_preset.Parts or empty_table)[part_name] then
						local matched = true
						for _, filter in ipairs(part_preset.Filters) do
							if not filter:Match(self) then
								matched = false
								break
							end
						end
						if matched then
							if not matched_preset or matched_preset.ZOrder < part_preset.ZOrder then
								matched_preset = part_preset
								matched_presets = nil
							elseif matched_preset.ZOrder == part_preset.ZOrder then
								if matched_presets then
									matched_presets[#matched_presets + 1] = part_preset
								else
									matched_presets = { matched_preset, part_preset }
								end
							end
						end
					end
				end
			end
			if matched_presets then
				seed = self:ComposeBodyRand(seed)
				matched_preset = table.weighted_rand(matched_presets, "Weight", seed)
			end
			if matched_preset then
				part_to_preset[part_name] = matched_preset
			end
		end
	end
	return seed
end

function CompositeBody:GetConstructionCopyObjectData(copy_data)
	table.rawset_values(copy_data, self,         "composite_seed", "colorization_offset")
end

function CompositeBody:GetConstructionCursorDynamicData(controller, cursor_data)
	table.rawset_values(cursor_data, controller, "composite_seed", "colorization_offset")
end

function CompositeBody:GetConstructionControllerDynamicData(controller_data)
	table.rawset_values(controller_data, self,   "composite_seed", "colorization_offset")
end

function OnMsg.GatherConstructionInitData(construction_init_data)
	rawset(construction_init_data, "composite_seed", true)
	rawset(construction_init_data, "colorization_offset", true)
end

function CompositeBody:ComposeBodyRand(seed, ...)
	seed = seed or self.composite_seed or self:RandSeed("Body")
	self.composite_seed = self.composite_seed or seed
	return BraidRandom(seed, ...)
end

function CompositeBody:GetPartFXTarget(part)
	return self
end

function CompositeBody:ComposeBodyParts(seed)
	if self:ChangeEntityDisabled() then
		return
	end
	local part_to_preset = { }
	-- collect the best matched body presets for the remaining parts without equipment
	seed = self:CollectBodyParts(part_to_preset, seed) or seed
	
	-- apply the main body entity (all others are attached to this one)
	local main_name = self.composite_part_main	
	local main_preset = main_name and part_to_preset[main_name]
	if not main_preset and not IsValidEntity(self:GetEntity()) then
		return
	end
	local applied_presets = {}
	local changed
	if main_preset then
		local changed_i, seed_i = self:ApplyBodyPart(self, main_preset, main_name, seed)
		assert(IsValidEntity(self:GetEntity()))
		changed = changed_i or changed
		seed = seed_i or seed
		applied_presets = { [main_preset] = true }
	end

	local last_part_class, part_def
	
	local override_parts = self.override_parts or empty_table
	-- apply all the remaining as attaches (removing the unused ones from the previous procedure)
	local attached_parts = self.attached_parts or {}
	attached_parts[main_name] = self
	self.attached_parts = attached_parts
	for _, part_name in ipairs(self.composite_part_names) do
		if part_name == main_name then
			goto continue
		end
		local part_obj = attached_parts[part_name]
		--body part overriding
		local override = override_parts[part_name]
		if override then
			if override ~= part_obj then
				if part_obj then
					self:RemoveBodyPart(part_obj, part_name)
				end
				attached_parts[part_name] = override
				local parent = self
				if override:GetParent() ~= parent then
					local spot = self.override_parts_spot and self.override_parts_spot[part_name]
					spot = spot or self.composite_part_spots[part_name]
					local spot_idx = spot and parent:GetSpotBeginIndex(spot)
					parent:Attach(override, spot_idx)
				end
			end
			goto continue
		end
		--preset search
		local preset = part_to_preset[part_name]
		if preset and not applied_presets[preset] then
			applied_presets[preset] = true
			if preset.Entity ~= "" then
				local part_class = preset.PartClass or "CompositeBodyPart"
				if not IsValid(part_obj) or part_obj.class ~= part_class then
					if last_part_class ~= part_class then
						last_part_class = part_class
						part_def = g_Classes[part_class]
						assert(part_def)
						part_def = part_def or CompositeBodyPart
					end
					DoneObject(part_obj)
					part_obj = part_def:new()
					attached_parts[part_name] = part_obj
					changed = true
				end
				local changed_i, seed_i = self:ApplyBodyPart(part_obj, preset, part_name, seed)
				changed = changed_i or changed
				seed = seed_i or seed 
				goto continue
			end
		end
		-- 1) body part preset not found
		-- 2) part already covered, should be removed
		-- 3) part used to specify a missing part
		if part_obj then
			attached_parts[part_name] = nil
			self:RemoveBodyPart(part_obj, part_name)
		end
		::continue::
	end
	if changed then
		self:NetUpdateHash("BodyChanged", seed)
	end
	self:InitBodyParts()
	return changed
end

local def_scale = range(100, 100)

function CompositeBody:ChangeBodyPartEntity(part, preset, name)
	local entity = preset.Entity
	if (preset.AffectedBy or "") ~= "" and (preset.EntityWhenAffected or "") ~= "" and self.attached_parts[preset.AffectedBy] then
		entity = preset.EntityWhenAffected
	end
	
	local current_entity = part:GetEntity()
	if current_entity == entity or not IsValidEntity(entity) then
		return
	end
	if current_entity ~= "" then
		PlayFX("ApplyBodyPart", "end", part, self:GetPartFXTarget(part))
	end
	local state = part:GetGameFlags(const.gofSyncState) == 0 and EntityStates.idle or nil
	part:ChangeEntity(entity, state)
	return true
end

function CompositeBody:ChangeBodyPartScale(part, name, scale)
	if part:GetScale() ~= scale then
		part:SetScale(scale)
		return true
	end
end

function CompositeBody:ApplyBodyPart(part, preset, name, seed)
	-- entity
	local changed_entity = self:ChangeBodyPartEntity(part, preset, name)
	local changed = changed_entity
	-- mirrored
	if part:GetMirrored() ~= preset.Mirrored then
		part:SetMirrored(preset.Mirrored)
		changed = true
	end
	-- scale
	local scale = 100
	local scale_range = preset.Scale
	if scale_range ~= def_scale then
		local scale_min, scale_max = scale_range.from, scale_range.to
		if scale_min == scale_max then
			scale = scale_min
		else
			scale, seed = self:ComposeBodyRand(seed, scale_min, scale_max)
		end
	end
	if self:ChangeBodyPartScale(part, name, scale) then
		changed = true
	end
	-- color
	seed = self:ColorizeBodyPart(part, preset, name, seed) or seed
	-- attach
	if part ~= self then
		local axis = preset.Axis
		if axis and part:GetAxisLocal() ~= axis then
			part:SetAxis(axis)
			changed = true
		end
		local angle = preset.Angle
		if angle and part:GetAngleLocal() ~= angle then
			part:SetAngle(angle)
			changed = true
		end
		local spot_name = preset.AttachSpot or ""
		if spot_name == "" then
			local spots = self.composite_part_spots
			spot_name = spots and spots[name] or ""
			if spot_name == "" then
				spot_name = "Origin"
			end
		end
		local sync_state = preset.SyncState
		if sync_state == "auto" then
			sync_state = spot_name == "Origin"
		end
		if not sync_state then
			part:ClearGameFlags(const.gofSyncState)
		else
			part:SetGameFlags(const.gofSyncState)
		end
		local prev_parent, prev_spot_idx = part:GetParent(), part:GetAttachSpot()
		local parents = self.composite_part_parent
		local parent_part = preset.Parent or parents and parents[name] or ""
		local parent = parent_part ~= "" and self.attached_parts[parent_part] or self
		local spot_idx = parent:GetSpotBeginIndex(spot_name)
		assert(spot_idx ~= -1, string.format("Failed to attach body part %s to spot %s of %s with state %s", name, spot_name, parent:GetEntity(), parent:GetStateText()))
		if prev_parent ~= parent or prev_spot_idx ~= spot_idx then
			parent:Attach(part, spot_idx)
			changed = true
		end
		local attach_offset = preset.AttachOffset or point30
		local attach_axis = preset.AttachAxis or axis_z
		local attach_angle = preset.AttachAngle or 0
		if attach_offset ~= part:GetAttachOffset() or attach_axis ~= part:GetAttachAxis() or attach_angle ~= part:GetAttachAngle() then
			part:SetAttachOffset(attach_offset)
			part:SetAttachAxis(attach_axis)
			part:SetAttachAngle(attach_angle)
			changed = true
		end
	end
	
	local changed_fx
	local fx_actor_class = (preset.FxActor or "") ~= "" and preset.FxActor or nil
	local current_fx_actor = rawget(part, "fx_actor_class") -- avoid clearing class fx actor with the default FxActor value
	if current_fx_actor ~= fx_actor_class then
		if current_fx_actor then
			PlayFX("ApplyBodyPart", "end", part, self:GetPartFXTarget(part))
		end
		part.fx_actor_class = fx_actor_class
		changed_fx = true
	end

	if changed_fx or changed_entity then
		PlayFX("ApplyBodyPart", "start", part, self:GetPartFXTarget(part))
	end
	
	return changed, seed
end

function CompositeBody:ColorizeBodyPart(part, preset, name, seed)
	local inherit_from = preset.ColorInherit
	local colorization = inherit_from ~= "" and table.get(self.attached_parts, inherit_from)
	if not colorization then
		seed = self:ComposeBodyRand(seed)
		local colors = preset.Colors or empty_table
		local idx
		colorization, idx = table.weighted_rand(colors, "Weight", seed)
		local offset = self.colorization_offset
		if idx and offset then
			idx = ((idx + offset - 1) % #colors) + 1
			colorization = colors[idx]
		end
	end
	part:SetColorization(colorization)
	return seed
end

function CompositeBody:SetColorizationOffset(offset)
	local part_to_preset = {}
	local seed = self.composite_seed
	self:CollectBodyParts(part_to_preset, seed)
	local attached_parts = self.attached_parts
	self.colorization_offset = offset
	for _, part_name in ipairs(self.composite_part_names) do
		local preset = part_to_preset[part_name]
		if preset then
			local part = attached_parts[part_name]
			self:ColorizeBodyPart(part, preset, part_name, seed, offset)
		end
	end
end

function CompositeBody:RemoveBodyPart(part, name)
	DoneObject(part)
end

function CompositeBody:OverridePart(name, obj, spot)
	if not IsValid(self) or IsBeingDestructed(self) then
		return
	end
	assert(table.find(self.composite_part_names, name), "Invalid part name")
	if type(obj) == "string" and IsValidEntity(obj) then
		local entity = obj
		obj = CompositeBodyPart:new()
		obj:ChangeEntity(entity)
		AutoAttachObjects(obj)
	end
	if IsValid(obj) then
		self.override_parts = self.override_parts or {}
		assert(not self.override_parts[name], "Part already overridden")
		self.override_parts[name] = obj
		self.override_parts_spot = self.override_parts_spot or {}
		self.override_parts_spot[name] = spot
	elseif self.override_parts then
		obj = self.override_parts[name]
		if self.attached_parts[name] == obj then
			self.attached_parts[name] = nil
		end
		self.override_parts[name] = nil
		self.override_parts_spot[name] = nil
	end
	self:ComposeBodyParts()
	return obj
end

function CompositeBody:RemoveOverridePart(name)
	local part = self:OverridePart(name, false)
	if IsValid(part) then
		self:RemoveBodyPart(part)
	end
end

local composite_body_targets, composite_body_filters, composite_body_parts, composite_body_defs

function CompositeBody:OnEditorSetProperty(prop_id, old_value, ged)
	local prop_meta = self:GetPropertyMetadata(prop_id) or empty_table
	if prop_meta.body_part_match then
		composite_body_targets = nil
	end
	if prop_meta.body_part_filter then
		self:ComposeBodyParts()
	end
	return Object.OnEditorSetProperty(self, prop_id, old_value, ged)
end

----
-- Editor only code:

local function UpdateItems()
	if composite_body_targets then
		return
	end
	composite_body_filters, composite_body_parts, composite_body_defs = {}, {}, {}
	ClassDescendantsList("CompositeBody", function(class, def)
		local target = def.composite_part_target or class
		
		local filters = composite_body_filters[target] or {}
		for _, prop in ipairs(def:GetProperties()) do
			if prop.body_part_filter then
				filters[prop.id] = filters[prop.id] or prop
			end
		end
		composite_body_filters[target] = filters
		
		local defs = composite_body_defs[target] or {}
		if not defs[class] then
			defs[class] = true
			table.insert(defs, def)
		end
		composite_body_defs[target] = defs
		
		local parts = composite_body_parts[target] or {}
		for _, part in ipairs(def.composite_part_names) do
			table.insert_unique(parts, part)
		end
		composite_body_parts[target] = parts
	end, "")
	composite_body_targets = table.keys2(composite_body_parts, true, "")
end

function GetBodyPartEntityItems()
	local items = {}
	for entity in pairs(GetAllEntities()) do
		local data = EntityData[entity]
		if data then
			items[#items + 1] = entity
		end
	end
	table.sort(items)
	table.insert(items, 1, "")
	return items
end

function GetBodyPartNameItems(preset)
	UpdateItems()
	return composite_body_parts[preset.Target]
end

function GetBodyPartNameCombo(preset)
	local items = table.copy(GetBodyPartNameItems(preset) or empty_table)
	table.insert(items, 1, "")
	return items
end

function GetBodyPartTargetItems(preset)
	UpdateItems()
	return composite_body_targets
end

function EntityStatesCombo(entity, ...)
	entity = entity or ""
	if entity == "" then
		return { ... }
	end
	local anims = GetStates(entity)
	table.sort(anims)
	table.insert(anims, 1, "")
	return anims
end

function EntityStateMomentsCombo(entity, anim, ...)
	entity = entity or ""
	anim = anim or ""
	if entity == "" or anim == "" then
		return { ... }
	end
	local moments = GetStateMomentsNames(entity, anim)
	table.insert(moments, 1, "")
	return moments
end

----

DefineClass.CompositeBodyPreset = {
	__parents = { "Preset" },
	properties = {
		{ id = "Target",       name = "Target",        editor = "choice",      default = "",    items = GetBodyPartTargetItems },
		{ id = "Parts",        name = "Covered Parts", editor = "set",         default = false, items = GetBodyPartNameItems },
		{ id = "CustomMatch",  name = "Custom Match",  editor = "bool",        default = false, },
		{ id = "BodiesFound",  name = "Bodies Found",  editor = "text",        default = "", dont_save = true, read_only = 0, lines = 1, max_lines = 3, no_edit = PropChecker("CustomMatch", true) },
		{ id = "Parent",       name = "Parent Part",   editor = "choice",      default = false, items = GetBodyPartNameItems },
		{ id = "Entity",       name = "Entity",        editor = "choice",      default = "",    items = GetBodyPartEntityItems },
		{ id = "PartClass",    name = "Custom Class",  editor = "text",        default = false, translate = false, validate = function(self) return self.PartClass and not g_Classes[self.PartClass] and "Invalid class" end },
		{ id = "AttachSpot",   name = "Attach Spot",   editor = "text",        default = "",    translate = false, help = "Force attach spot" },
		{ id = "Scale",        name = "Scale",         editor = "range",       default = def_scale },
		{ id = "Axis",         name = "Axis",          editor = "point",       default = false, help = "Force a specific axis" },
		{ id = "Angle",        name = "Angle",         editor = "number",      default = false, scale = "deg", min = -180*60, max = 180*60, slider = true, help = "Force a specific angle" },
		{ id = "Mirrored",     name = "Mirrored",      editor = "bool",        default = false },
		{ id = "SyncState",    name = "Sync State",    editor = "choice",      default = "auto", items = {true, false, "auto"}, help = "Force sync state" },
		{ id = "ZOrder",       name = "ZOrder",        editor = "number",      default = 0,     },
		{ id = "Weight",       name = "Weight",        editor = "number",      default = 1000,  min = 0, scale = 10 },
		{ id = "FxActor",      name = "Fx Actor",      editor = "combo",       default = "",    items = ActorFXClassCombo },
		{ id = "Filters",      name = "Filters",       editor = "nested_list", default = false, base_class = "CompositeBodyPresetFilter", inclusive = true },
		{ id = "ColorInherit", name = "Color Inherit", editor = "choice",      default = "",    items = GetBodyPartNameCombo },
		{ id = "Colors",       name = "Colors",        editor = "nested_list", default = false, base_class = "CompositeBodyPresetColor", inclusive = true, no_edit = function(self) return self.ColorInherit ~= "" end },
		{ id = "Lights",       name = "Lights",        editor = "nested_list", default = false, base_class = "CompositeBodyPresetLight", inclusive = true },
		{ id = "AffectedBy",   name = "Affected by",   editor = "choice",      default = "",    items = GetBodyPartNameCombo },
		{ id = "EntityWhenAffected", name = "Entity when affected", editor = "choice", default = "", items = GetBodyPartEntityItems, no_edit = function(o) return not o.AffectedBy end },
		{ id = "AttachOffset", name = "Attach Offset", editor = "point",       default = point30, },
		{ id = "AttachAxis",   name = "Attach Axis",   editor = "point",       default = axis_z, },
		{ id = "AttachAngle",  name = "Attach Angle",  editor = "number",      default = 0, scale = "deg", min = -180*60, max = 180*60, slider = true },
		
		{ id = "ApplyAnim",       name = "Apply Anim",        editor = "choice", default = "", items = function(self) return EntityStatesCombo(self.AnimTestEntity, "") end },
		{ id = "UnapplyAnim",     name = "Unapply Anim",      editor = "choice", default = "", items = function(self) return EntityStatesCombo(self.AnimTestEntity, "") end },
		{ id = "ApplyAnimMoment", name = "Apply Anim Moment", editor = "choice", default = "hit", items = function(self) return EntityStateMomentsCombo(self.AnimTestEntity, self.ApplyAnim, "", "hit") end, },
		{ id = "AnimTestEntity",  name = "Anim Test Entity",  editor = "text",   default = false },
	},
	GlobalMap = "CompositeBodyPresets",
	EditorMenubar = "Editors.Art",
	EditorMenubarName = "Composite Body Parts",
	EditorIcon = "CommonAssets/UI/Icons/atom molecule science.png",
	
	StoreAsTable = false,
}

CompositeBodyPreset.Documentation = [[The composite body system is a matching system for attaching parts to a body.

A body collects its potential parts not from all part presets, but from a specified preset <style GedHighlight>Group</style>. The matched parts are those having the same <style GedHighlight>Target</style> property as the body target property.

If no matching information is specified in the body, then its class name is used instead for all matching.

Each part can contain filters for additional conditions during the matching process.

Each part covers a specific named location on the body specified by <style GedHighlight>Covered Parts</style> property. If several parts are matched for the same location, a single one is chosen based on the <style GedHighlight>ZOrder</style> property. If there are still multiple parts with equal ZOrder, then a part is randomly selected based on the <style GedHighlight>Weight</style> property.]]

function CompositeBodyPreset:GetError()
	if self.CustomMatch then
		return
	end
	local parts = self.Parts
	if not next(parts) then
		return "No covered parts specified!"
	end
	UpdateItems()
	local defs = composite_body_defs[self.Target]
	if not defs then
		return string.format("No composite bodies found with target '%s'", self.Target)
	end
	local group = self.group
	local count_group = 0
	local count_part = 0
	for _, def in ipairs(defs) do
		local composite_part_groups = def.composite_part_groups or { def.class }
		if table.find(composite_part_groups, group) then
			count_group = count_group + 1
			for _, part_name in ipairs(def.composite_part_names) do
				if parts[part_name] then
					count_part = count_part + 1
					break
				end
			end
		end
	end
	if count_group == 0 then
		return string.format("No composite bodies found with group '%s'", tostring(group))
	end
	if count_part == 0 then
		return string.format("No composite bodies found with parts %s", table.concat(table.keys(parts, true)))
	end
end

function CompositeBodyPreset:GetBodiesFound()
	UpdateItems()
	local parts = self.Parts
	if not next(parts) then
		return 0
	end
	local found = {}
	for _, def in ipairs(composite_body_defs[self.Target]) do
		local composite_part_groups = def.composite_part_groups or { def.class }
		if table.find(composite_part_groups, self.group) then
			for _, part_name in ipairs(def.composite_part_names) do
				if parts[part_name] then
					found[def.class] = true
					break
				end
			end
		end
	end
	return table.concat(table.keys(found, true), ", ")
end

function CompositeBodyPreset:OnEditorSetProperty(prop_id, old_value, ged)
	if prop_id == "Entity" then
		for _, obj in ipairs(self.Colors) do
			ObjModified(obj) -- properties for modifiable colors have changed
		end
	end
end

local function FindParentPreset(obj, member)
	return GetParentTableOfKind(obj, "CompositeBodyPreset")
end

function OnMsg.ClassesGenerate()
	DefineModItemPreset("CompositeBodyPreset", {
		EditorSubmenu = "Other",
		EditorName = "Composite body",
		EditorShortcut = false,
	})
end

----

local function GetBodyFilters(filter)
	UpdateItems()
	local parent = FindParentPreset(filter)
	local props = parent and composite_body_filters[parent.Target]
	if not props then
		return {}
	end
	local filters = {}
	for _, def in ipairs(composite_body_defs[parent.Target]) do
		for name, prop in pairs(props) do
			local items
			if prop.items then
				items = prop_eval(prop.items, def, prop)
			elseif prop.preset_class then
				local filter = prop.preset_filter
				items = {}
				ForEachPreset(prop.preset_class, function(preset, group, items)
					if not filter or filter(preset) then
						items[#items + 1] = preset.id
					end
				end, items)
				table.sort(items)
			end
			if items and #items > 0 then
				local prev_filters = filters[name]
				if not prev_filters then
					filters[name] = items
				else
					for _, value in ipairs(items) do
						table.insert_unique(prev_filters, value)
					end
				end
			end
		end
	end
	return filters
end

local function GetFilterNameItems(filter)
	local filters = GetBodyFilters(filter)
	local items = filters and table.keys(filters, true)
	if items[1] ~= "" then
		table.insert(items, 1, "")
	end
	return items
end

local function GetFilterValueItems(filter)
	local filters = GetBodyFilters(filter)
	return filters and filters[filter.Name] or {""}
end

DefineClass.CompositeBodyPresetFilter = {
	__parents = { "PropertyObject" },
	properties = {
		{ id = "Name",  name = "Name",  editor = "choice", default = "", items = GetFilterNameItems, },
		{ id = "Value", name = "Value", editor = "choice", default = "", items = GetFilterValueItems, },
		{ id = "Test",  name = "Test",  editor = "choice", default = "=", items = {"=", ">", "<"}, },
	},
	EditorView = Untranslated("<Name> <Test> <Value>"),
}

function CompositeBodyPresetFilter:Match(obj)
	local obj_value, value, test = obj[self.Name], self.Value, self.Test
	if test == '=' then
		return obj_value == value
	elseif test == '>' then
		return obj_value > value
	elseif test == '<' then
		return obj_value < value
	end
end

----

DefineClass.CompositeBodyPresetColor = {
	__parents = { "ColorizationPropSet" },
	properties = {
		{ id = "Weight",  name = "Weight",  editor = "number", default = 1000, min = 0, scale = 10 },
	},
}

function CompositeBodyPresetColor:GetMaxColorizationMaterials()
	PopulateParentTableCache(self)
	if not ParentTableCache[self] then
		return ColorizationPropSet.GetMaxColorizationMaterials(self)
	end
	local parent = FindParentPreset(self)
	return parent and ColorizationMaterialsCount(parent.Entity) or 0
end

function CompositeBodyPresetColor:GetError()
	if self:GetMaxColorizationMaterials() == 0 then
		local parent = FindParentPreset(self)
		if not parent or parent.Entity == "" then
			return "The composite body entity is not set."
		else
			return "There are no modifiable colors in the composite body entity."
		end
	end
end

----

local light_props = {}
function OnMsg.ClassesBuilt()
	local function RegisterProps(class, classdef)
		local props = {}
		for _, prop in ipairs(classdef:GetProperties()) do
			if prop.category == "Visuals"
			and not prop_eval(prop.no_edit, classdef, prop)
			and not prop_eval(prop.read_only, classdef, prop) then
				props[#props + 1] = prop
				props[prop.id] = classdef:GetDefaultPropertyValue(prop.id, prop)
			end
		end
		light_props[class] = props
	end
	RegisterProps("Light", Light)
	ClassDescendants("Light", RegisterProps)
end

function OnMsg.GatherFXActors(list)
	for _, preset in pairs(CompositeBodyPresets) do
		if (preset.FxActor or "") ~= "" then
			list[#list + 1] = preset.FxActor
		end
	end
end

function OnMsg.DataLoaded()
	PopulateParentTableCache(Presets.CompositeBodyPreset)
end

local function GetEntitySpotsItems(light)
	local parent = FindParentPreset(light)
	local entity = parent and parent.Entity or ""
	local states = IsValidEntity(entity) and GetStates(entity) or ""
	if #states == 0 then return empty_table end
	local idx = table.find(states, "idle")
	local spots = {}
	local spbeg, spend = GetAllSpots(entity, states[idx] or states[1])
	for spot = spbeg, spend do
		spots[GetSpotName(entity, spot)] = true
	end
	return table.keys(spots, true)
end

DefineClass.CompositeBodyPresetLight = {
	__parents = { "PropertyObject" },
	properties = {
		{ id = "LightType",  name = "Light Type", editor = "choice", default = "Light",  items = ToCombo(light_props) },
		{ id = "LightSpot",  name = "Light Spot", editor = "combo",  default = "Origin", items = GetEntitySpotsItems },
		{ id = "LightSIEnable",     name = "SI Apply", editor = "bool", default = true },
		{ id = "LightSIModulation", name = "SI Modulation", editor = "number", default = 255, min = 0, max = 255, slider = true, no_edit = function(self) return not self.LightSIEnable end  },
		{ id = "night_mode", name = "Night mode", editor = "dropdownlist", items = { "Off", "On" }, default = "On" },
		{ id = "day_mode",   name = "Day mode",   editor = "dropdownlist", items = { "Off", "On" }, default = "Off" },
	},
	EditorView = Untranslated("<LightType>: <LightSpot>"),
}

function CompositeBodyPresetLight:GetError()
	if not light_props[self.LightType] then
		return "Invalid light type selected!"
	end
end

function CompositeBodyPresetLight:ApplyToLight(light)
	local props = light_props[self.LightType] or empty_table
	for _, prop in ipairs(props) do
		local prop_id = prop.id
		local prop_value = rawget(self, prop_id)
		if prop_value ~= nil then
			light:SetProperty(prop_id, prop_value)
		end
	end
end

function CompositeBodyPresetLight:GetProperties()
	local props = table.icopy(self.properties)
	table.iappend(props, light_props[self.LightType] or empty_table)
	return props
end

function CompositeBodyPresetLight:GetDefaultPropertyValue(prop_id, prop_meta)
	local def = table.get(light_props, self.LightType, prop_id)
	if def ~= nil then
		return def
	end
	return PropertyObject.GetDefaultPropertyValue(self, prop_id, prop_meta)
end

DefineClass.BaseLightObject = {
	__parents = { "Object" },
}

function BaseLightObject:UpdateLight(lm, delayed)
end

function BaseLightObject:GameInit()
	Game:AddToLabel("Lights", self)
end

function BaseLightObject:Done()
	Game:RemoveFromLabel("Lights", self)
end

if FirstLoad then
	UpdateLightsThread = false
end

function OnMsg.DoneMap()
	UpdateLightsThread = false
end

function UpdateLights(lm, delayed)
	local lights = table.get(Game, "labels", "Lights")
	for _, obj in ipairs(lights) do
		obj:UpdateLight(lm, delayed)
	end
end

function UpdateLightsDelayed(lm, delayed_time)
	DeleteThread(UpdateLightsThread)
	UpdateLightsThread = false
	if delayed_time > 0 then
		UpdateLightsThread = CreateGameTimeThread(function(lm, delayed_time)
			Sleep(delayed_time)
			UpdateLights(lm, true)
			UpdateLightsThread = false
		end, lm, delayed_time)
	else
		UpdateLights(lm)
	end
end

function OnMsg.LightmodelChange(view, lm, time)
	UpdateLightsDelayed(lm, time/2)
end

function OnMsg.GatherAllLabels(labels)
	labels.Lights = true
end

DefineClass.CompositeLightObject = {
	__parents = { "CompositeBody", "BaseLightObject" },

	light_parts = false,
	light_objs = false,
}

function CompositeLightObject:ComposeBodyParts(seed)
	self.light_parts = nil
	
	local changed = CompositeBody.ComposeBodyParts(self, seed)
	
	local light_parts = self.light_parts
	local light_objs = self.light_objs
	for i = #(light_objs or ""),1,-1 do
		local config = light_objs[i]
		local part = light_parts and light_parts[config]
		if not part then
			DoneObject(light_objs[config])
			light_objs[config] = nil
			table.remove_value(light_objs, config)
		end
	end
	for _, config in ipairs(light_parts) do
		light_objs = light_objs or {}
		if light_objs[config] == nil then
			light_objs[config] = false
			light_objs[#light_objs + 1] = config
		end
	end
	self.light_objs = light_objs
	
	return changed
end

function CompositeLightObject:ApplyBodyPart(part, preset, ...)
	local light_parts = self.light_parts
	for _, config in ipairs(preset.Lights) do
		light_parts = light_parts or {}
		light_parts[config] = part
		light_parts[#light_parts + 1] = config
	end
	self.light_parts = light_parts
	
	return CompositeBody.ApplyBodyPart(self, part, preset, ...)
end

function CompositeLightObject:IsBodyPartLightOn(config)
	local mode = GameState.Night and config.night_mode or config.day_mode
	return mode == "On"
end

function CompositeLightObject:UpdateLight(delayed)
	local light_objs = self.light_objs or empty_table
	local IsBodyPartLightOn = self.IsBodyPartLightOn
	for _, config in ipairs(light_objs) do
		local light = light_objs[config]
		local part = self.light_parts[config]
		local turned_on = IsBodyPartLightOn(self, config)
		if turned_on and not light then
			light = PlaceObject(config.LightType)
			config:ApplyToLight(light)
			part:Attach(light, GetSpotBeginIndex(part, config.LightSpot))
			light_objs[config] = light
		elseif not turned_on and light then
			DoneObject(light)
			light_objs[config] = false
		end
		if config.LightSIEnable then
			part:SetSIModulation(turned_on and config.LightSIModulation or 0)
		end
	end
end

----

DefineClass.BlendedCompositeBody = {
	__parents = { "CompositeBody", "Object" },
	composite_part_blend = false,
	
	blended_body_parts_params = false,
	blended_body_parts = false,
}

function BlendedCompositeBody:Init()
	self.blended_body_parts_params = { }
	self.blended_body_parts = { }
end

function BlendedCompositeBody:ForceComposeBlendedBodyParts()
	self.blended_body_parts_params = { }
	self.blended_body_parts = { }
	self:ComposeBodyParts()
end

function BlendedCompositeBody:ForceRevertBlendedBodyParts()
	if next(self.attached_parts) then
		local part_to_preset = {}
		self:CollectBodyParts(part_to_preset)
		for name,preset in sorted_pairs(part_to_preset) do
			local part = self.attached_parts[name]
			local entity = preset.Entity
			if IsValid(part) and IsValidEntity(entity) then
				Msg("RevertBlendedBodyPart", part)
				part:ChangeEntity(entity)
			end
		end
	end
end

function BlendedCompositeBody:UpdateBlendPartParams(params, part, preset, name, seed)
	return part:GetEntity()
end

function BlendedCompositeBody:ShouldBlendPart(params, part, preset, name, seed)
	return false
end

if FirstLoad then
	g_EntityBlendLocks = { }
	--g_EntityBlendLog = { }
end

local function BlendedEntityLocksGet(entity_name)
	return g_EntityBlendLocks[entity_name] or 0
end

function BlendedEntityIsLocked(entity_name)
	--table.insert(g_EntityBlendLog, GameTime() .. " lock " .. entity_name)
	return BlendedEntityLocksGet(entity_name) > 0
end

function BlendedEntityLock(entity_name)
	--table.insert(g_EntityBlendLog, GameTime() .. " unlock " .. entity_name)
	g_EntityBlendLocks[entity_name] = BlendedEntityLocksGet(entity_name) + 1
end

function BlendedEntityUnlock(entity_name)
	local locks_count = BlendedEntityLocksGet(entity_name)
	assert(locks_count >= 1, "Unlocking a blended entity that isn't locked")
	if locks_count > 1 then
		g_EntityBlendLocks[entity_name] = locks_count - 1
	else
		g_EntityBlendLocks[entity_name] = nil
	end
end

function WaitBlendEntityLocks(obj, entity_name)
	while BlendedEntityIsLocked(entity_name) do
		if obj and not IsValid(obj) then
			return false
		end
		WaitNextFrame(1)
	end
	
	return true
end

function BlendedCompositeBody:BlendEntity(t, e1, e2, e3, w1, w2, w3, m2, m3)
	--table.insert(g_EntityBlendLog, GameTime() .. " " .. self.class .. " blend " .. t)
	assert(BlendedEntityIsLocked(t), "To blend an entity you must lock it using BlendedEntityLock")
	assert(t ~= e1 and t ~= e2 and t ~= e3)

	SetMaterialBlendMaterials(
		GetEntityIdleMaterial(t), --target
		GetEntityIdleMaterial(e1), --base
		m2, GetEntityIdleMaterial(e2), --weight 1, material
		m3, GetEntityIdleMaterial(e3)) --weight 2, material
	WaitNextFrame(1)
	
	local err = AsyncOpWait(nil, nil, "AsyncMeshBlend", 
		t, 0, --target, LOD
		e1, w1, --entity 1, weight
		e2, w2, --entity 2, weight
		e3, w3) --entity 3, weight
	if err then print("Failed to blend meshes: ", err) end
end

function BlendedCompositeBody:AsyncBlendEntity(obj, t, e1, e2, e3, w1, w2, w3, m2, m3, callback)
	return CreateRealTimeThread(function(self, obj, t, e1, e2, e3, w1, w2, w3, m2, m3, callback)
		WaitBlendEntityLocks(obj, t)
		BlendedEntityLock(t)
		
		self:BlendEntity(t, e1, e2, e3, w1, w2, w3, m2, m3)
		
		if callback then
			callback(self, obj, t, e1, e2, e3, w1, w2, w3, m2, m3)
		end
		
		BlendedEntityUnlock(t)
	end, self, obj, t, e1, e2, e3, w1, w2, w3, m2, m3, callback)
end

function BlendedCompositeBody:ApplyBlendBodyPart(blended_entity, part, preset, name, seed)
	return CompositeBody.ApplyBodyPart(self, preset, name, seed)
end

function BlendedCompositeBody:BlendBodyPartFailed(blended_entity, part, preset, name, seed)
	return CompositeBody.ApplyBodyPart(self, part, preset, name, seed)
end

-- if the body part is declared as "to be blended"
function BlendedCompositeBody:IsBlendBodyPart(name)
	return self.composite_part_blend and self.composite_part_blend[name]
end

-- if the body part is using a blended entity or is being blended at the moment
function BlendedCompositeBody:IsCurrentlyBlendedBodyPart(name)
	return self.blended_body_parts and self.blended_body_parts[name]
end

function BlendedCompositeBody:ColorizeBodyPart(part, preset, name, seed)
	if self:IsCurrentlyBlendedBodyPart(name) then
		return
	end
	return CompositeBody.ColorizeBodyPart(self, part, preset, name, seed)
end

function BlendedCompositeBody:ChangeBodyPartEntity(part, preset, name)
	if self:IsCurrentlyBlendedBodyPart(name) then
		return
	end
	return CompositeBody.ChangeBodyPartEntity(self, part, preset, name)
end

function BlendedCompositeBody:ApplyBodyPart(part, preset, name, seed)
	if self:IsBlendBodyPart(name) then
		self.blended_body_parts_params = self.blended_body_parts_params or { }
		local params = self.blended_body_parts_params[name]
		if not params or self:ShouldBlendPart(params, part, preset, name, seed) then
			params = params or { }
			local blended_entity = self:UpdateBlendPartParams(params, part, preset, name, seed)
			if IsValidEntity(blended_entity) then
				self.blended_body_parts_params[name] = params
				self.blended_body_parts[name] = (self.blended_body_parts[name] or 0) + 1
				return self:ApplyBlendBodyPart(blended_entity, part, preset, name, seed)
			else
				self.blended_body_parts[name] = nil
				return self:BlendBodyPartFailed(blended_entity, part, preset, name, seed)
			end
		end
	end
	
	return CompositeBody.ApplyBodyPart(self, part, preset, name, seed)
end

function BlendedCompositeBody:RemoveBodyPart(part, name)
	if self:IsBlendBodyPart(name) and self.blended_body_parts_params then
		self.blended_body_parts_params[name] = nil
	end
	return CompositeBody.RemoveBodyPart(self, part, name)
end

function ForceRecomposeAllBlendedBodies()
	local objs = MapGet("map", "BlendedCompositeBody")
	for i,obj in ipairs(objs) do
		obj:ForceRevertBlendedBodyParts()
	end
	for i,obj in ipairs(objs) do
		obj:ForceComposeBlendedBodyParts()
	end
end

function OnMsg.PostLoadGame()
	ForceRecomposeAllBlendedBodies()
end

function OnMsg.AdditionalEntitiesLoaded()
	if type(__cobjectToCObject) ~= "table" then return end
	ForceRecomposeAllBlendedBodies()
end

local body_to_states
function CompositeBodyAnims(classdef)
	local id = classdef.id or classdef.class
	body_to_states = body_to_states or {}
	local states = body_to_states[id]
	if not states then
		local entity = ResolveTemplateEntity(classdef)
		states = IsValidEntity(entity) and GetStates(entity) or empty_table
		table.sort(states)
		body_to_states[id] = states
	end
	return states
end

function SavegameFixups.BlendedBodyPartsList()
	MapForEach(true, "BlendedCompositeBody", function(obj)
		obj.blended_body_parts = {}
	end)
end

function SavegameFixups.BlendedBodyBlendIDs()
	MapForEach(true, "BlendedCompositeBody", function(obj)
		for name in pairs(obj.blended_body_parts) do
			obj.blended_body_parts[name] = 1
		end
	end)
end

function SavegameFixups.FixSyncStateFlag2()
	MapForEach(true, "CompositeBody", "Building", function(obj)
		obj:ClearGameFlags(const.gofSyncState)
		obj:SetGameFlags(const.gofPropagateState)
	end)
end