AI Empire  Check-in [28693ba5f2]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:initial commit of v0.4.3 of AI Empire
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | trunk
Files: files | file ages | folders
SHA1:28693ba5f2fef65a48df55bf51306291c0bd98ad
User & Date: pollen 2019-06-19 04:29:05
Context
2019-06-19
04:29
initial commit of v0.4.3 of AI Empire Leaf check-in: 28693ba5f2 user: pollen tags: trunk
04:07
initial empty check-in check-in: bf6341cca7 user: pollen tags: trunk
Changes

Added data/skin definitions/default.txt.










































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
Font: Small = DroidSans_8
Font: Normal = DroidSans_11
Font: Bold = DroidSans_11_Bold
Font: Italic = OpenSans_11_Italic
Font: Medium = DroidSans_16
Font: Big = DroidSans_20

Font: Subsys = DroidSans_11
Font: Name = GoodTimes_9
Font: Detail = DroidSans_8

Font: Title = GoodTimes_18
Font: Subtitle = DroidSans_14
Font: Button = DroidSans_11

Color: Text = #fff
Color: Selected = #a0000080
Color: Disabled = #888888ff

Color: ButtonText = #fff

// {{{ Generic Styles
Style: RoundedBox
	Element: Normal
		Rect: [3,3][182,39]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

	Element: Hovered
		Rect: [3,43][182,79]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

	Element: Active
		Rect: [3,83][182,119]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

	Element: Hovered, Active
		Inherit: RoundedBox, Active

	Element: Disabled
		Rect: [195,343][374,379]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

Style: StraightBox
	Element: Normal
		Rect: [194,4][373,39]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

	Element: Active
		Rect: [194,84][373,119]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

	Element: Hovered
		Rect: [194,44][373,79]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

	Element: Active, Hovered
		Rect: [194,124][373,159]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

Style: TinyBox
	Element: Normal
		Rect: [194,4][373,39]
		Margin: 2
		Vertical: Scaled
		Horizontal: Scaled

	Element: Hovered
		Rect: [194,84][373,119]
		Margin: 2
		Vertical: Scaled
		Horizontal: Scaled

Style: ItemBox
	Element: Normal
		Rect: [3,124][182,159]
		Margin: 4
		Vertical: Scaled
		Horizontal: Scaled

	Element: Hovered
		Rect: [3,164][182,199]
		Margin: 4
		Vertical: Scaled
		Horizontal: Scaled

	Element: Active
		Rect: [3,204][182,239]
		Margin: 4
		Vertical: Scaled
		Horizontal: Scaled

	Element: Hovered, Active
		Rect: [3,699][182,734]
		Margin: 4
		Vertical: Scaled
		Horizontal: Scaled

	Element: Disabled
		Rect: [3,124][182,159]
		Margin: 4
		Vertical: Scaled
		Horizontal: Scaled

		Add Gradient:
			TopLeft : #00000020
			TopRight: #00000020
			BotLeft : #00000020
			BotRight: #00000020

			GX1: 0
			GY1: 0
			GX2: 100%
			GY2: 100%

Style: InputBox
	Element: Normal
		Rect: [2,493][181,528]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

	Element: Active
		Rect: [2,533][181,568]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

	Element: Hovered
		Rect: [2,573][181,608]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

	Element: Active, Hovered
		Rect: [2,613][181,648]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

	Element: Disabled
		Rect: [2,653][181,692]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

Style: PlainBox
	Element: Normal
		Rect: [400,40][426,61]
		Margin: 2
		
		Horizontal: Scaled
		Vertical: Scaled

Style: PlainNameBox
	Element: Normal
		Rect: [429,42][459,56]
		Margin: 16,14
		Horizontal: Scaled

Style: HexPattern
	Element: Normal
		Rect: [390,11][412,36]
		Margin: 1
		Vertical: Tiled
		Horizontal: Tiled

Style: SmallHexPattern
	Element: Normal
		Rect: [390,40][395,56]
		Margin: 0
		Vertical: Tiled
		Horizontal: Tiled

Style: PatternBox
	Element: Normal
		Layer: PlainBox
		Layer: SmallHexPattern
			Color Override: #ffffff40
			OX1: +3
			OY1: +3
			OX2: -3
			OY2: -3

		GradientMode: Overlay
		Add Gradient:
			TopLeft : #4443
			TopRight: #4443
			BotLeft : #2221
			BotRight: #2221

			GX1: +3
			GY1: +3
			GX2: -3
			GY2: -3

Style: Panel
	Element: Normal
		Rect: [195,179][375,215]
		Margin: 6

		Vertical: Scaled
		Horizontal: Scaled

		Add Gradient:
			TopLeft : #1b1b1bff
			TopRight: #1b1b1bff
			BotLeft : #101010ff
			BotRight: #101010ff

			GX1: +4
			GY1: 50%
			GX2: -5
			GY2: -5

Style: HorizBar
	Element: Normal
		Rect: [205,221][365,257]
		Margin: 6

		Vertical: Scaled
		Horizontal: Scaled

Style: PlainOverlay
	Element: Normal
		Rect: [400,40][426,61]
		Margin: 2
		
		Horizontal: Scaled
		Vertical: Scaled

		Add Gradient:
			TopLeft : #1c1c1cff
			TopRight: #1c1c1cff
			BotLeft : #151515ff
			BotRight: #151515ff

			GX1: 1
			GY1: 1
			GX2: -1
			GY2: -1

	Element: Hovered
		Rect: [400,40][426,61]
		Margin: 2
		
		Horizontal: Scaled
		Vertical: Scaled

		Add Gradient:
			TopLeft : #2c2c2cff
			TopRight: #2c2c2cff
			BotLeft : #202020ff
			BotRight: #202020ff

			GX1: 1
			GY1: 1
			GX2: -1
			GY2: -1

	Element: Disabled
		Inherit: PlainOverlay, Normal

Style: OverlayBox
	Element: Normal
		Inherit: Panel
			
Style: LightPanel
	Element: Normal
		Rect: [195,221][375,257]
		Margin: 6

		Vertical: Scaled
		Horizontal: Scaled

Style: Highlight
	Element: Normal
		Rect: [5,740][182,773]
		Margin: 6

		Vertical: Scaled
		Horizontal: Scaled

Style: Glow
	Element: Normal
		Rect: [4,780][184,849]
		Margin: 10

		Vertical: Scaled
		Horizontal: Scaled

Style: SubtleGlow
	Element: Normal
		Rect: [4,854][184,922]
		Margin: 10

		Vertical: Scaled
		Horizontal: Scaled

Style: HighlightPanel
	Element: Normal
		Inherit: Panel, Normal

	Element: Hovered
		Inherit: Panel, Normal

		Add Gradient:
			TopLeft : #2c2c2c40
			TopRight: #2c2c2c40
			BotLeft : #20202040
			BotRight: #20202040

			GX1: 3
			GY1: 3
			GX2: -3
			GY2: -3

Style: RoundedTitle
	Element: Normal
		Rect: [195,264][393,294]
		Margin: 6, 6, 120, 6

		Horizontal: Scaled
		Vertical: Scaled

Style: CenterTitle
	Element: Normal
		Rect: [195,308][410,334]
		Margin: 90, 6

		Horizontal: Scaled
		Vertical: Scaled

Style: FullTitle
	Element: Normal
		Rect: [198,450][332,480]
		Margin: 6

		Horizontal: Scaled
		Vertical: Scaled

Style: PanelTitle
	Element: Normal
		Layer: FullTitle
			Color Override: #fff
			OX1: 0
			OY1: 0
			OX2: 100%
			OY2: 100%
		Layer: RoundedTitle
			OX1: 0
			OY1: 0
			OX2: 70%
			OY2: 100%

Style: WindowTitle
	Inherit: FullTitle

Style: SubTitle
	Element: Normal
		Rect: [201,487][327,517]
		Margin: 6

		Horizontal: Scaled
		Vertical: Scaled

Style: HorizAccent
	Element: Normal
		Rect: [197,385][302,439]
		Margin: 8

		Horizontal: Scaled
		Vertical: Scaled
// }}}
// {{{ Generic Parts
Style: DownArrow
	Element: Normal
		Rect: [443,6][455,19]

Style: UpArrow
	Element: Normal
		Rect: [457,6][470,19]
		
Style: RightArrow
	Element: Normal
		Rect: [429,6][441,19]

Style: LeftArrow
	Element: Normal
		Rect: [416,6][428,19]

Style: Field
	Inherit: PlainBox

Style: FieldName
	Inherit: PlainNameBox

Style: BG3D
	Element: Normal
		Rect: [202,526][446,636]

Style: ProgressBarBG
	Element: Normal
		Rect: [382,70][488,80]
		Margin: 6, 4
		Horizontal: Scaled
		Vertical: Scaled

Style: ProgressBar
	Element: Normal
		Rect: [383,83][487,91]
		Margin: 3, 4
		Horizontal: Scaled
		Vertical: Scaled

Style: DragHandle
	Element: Normal
		Inherit: ItemBox, Active

Style: ResizeHandle
	Element: Normal
		Inherit: ItemBox, Normal
	Element: Active
		Inherit: ItemBox, Active

Style: Checkmark
	Element: Normal
		Rect: [419,23][430,34]
// }}}
// {{{ Basic GUI Elements
Style: Button
	Inherit: RoundedBox

Style: BaselineButton
	Element: Normal
		Rect: [3,3][182,35]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

	Element: Hovered
		Rect: [3,43][182,75]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

	Element: Active
		Rect: [3,83][182,115]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

	Element: Hovered, Active
		Inherit: BaselineButton, Active

	Element: Disabled
		Rect: [195,343][374,375]
		Margin: 6
		Vertical: Scaled
		Horizontal: Scaled

Style: IconButton
	Element: Normal
	Element: Hovered
		Inherit: Highlight
	Element: Active
		Inherit: Highlight
	Element: Hovered,Active
		Inherit: Highlight

Style: IconToggle
	Element: Normal
	Element: Hovered
		Inherit: Highlight
	Element: Active
		Inherit: Highlight
		Layer: Button
	Element: Hovered,Active
		Inherit: Highlight
		Layer: Button

Style: Tab
	Inherit: Button

Style: PageTab
	Element: Normal
		Layer: Button
	Element: Hovered
		Layer: Button
		Layer: SubtleGlow
			Color Override: #ffffff
	Element: Active
		Layer: Button
		Layer: SubtleGlow

Style: Listbox

Style: ListboxItem
	Inherit: ItemBox

Style: StaticListboxItem
	Element: Normal
		Inherit: ItemBox, Normal

Style: DropdownList

Style: DropdownListItem
	Element: Normal
		Inherit: ItemBox

	Element: Hovered
		Inherit: ItemBox, Active

	Element: Active
		Inherit: ItemBox, Active

	Element: Active, Hovered
		Inherit: ItemBox, Active, Hovered

Style: Dropdown
	Inherit: RoundedBox

Style: DropdownArrow
	Element: Normal
		Layer: DownArrow
			OX1: +7
			OY1: +7
			OX2: -7
			OY2: -7

Style: Dialog
	Element: Normal
		Inherit: Panel

Style: Tooltip
	Element: Normal
		Rect: [2,454][181,489]
		Margin: 6

		Horizontal: Scaled
		Vertical: Scaled

Style: Textbox
	Inherit: InputBox

	Element: Focused
		Inherit: InputBox, Active

Style: HoverTextbox
	Element: Normal
	Element: Disabled
	Element: Disabled, Hovered
	Element: Hovered
		Inherit: InputBox
	Element: Focused
		Inherit: InputBox
	Element: Hovered, Focused
		Inherit: InputBox, Active

Style: HoverButton
	Element: Normal
	Element: Disabled
	Element: Disabled, Hovered
	Element: Active
		Inherit: Button, Active
	Element: Hovered
		Inherit: Button
	Element: Hovered, Active
		Inherit: Button, Active

Style: GlowButton
	Element: Normal
		Inherit: PlainBox
	Element: Hovered
		Layer: PlainBox
		Layer: Highlight
			Color Override: #00c0ff
	Element: Hovered, Disabled
		Inherit: PlainBox
	Element: Active
		GradientMode: Overlay
		Layer: PlainBox

		Add Gradient:
			TopLeft : #2228
			TopRight: #2228
			BotLeft : #00c0ff48
			BotRight: #00c0ff48

			GX1: 0
			GY1: 0
			GX2: 100%
			GY2: 100%
	Element: Active, Hovered
		Layer: GlowButton, Active
		Layer: Highlight
			Color Override: #00c0ff

Style: TabButton
	Element: Normal
		GradientMode: Overlay
		Layer: PlainBox

		Add Gradient:
			TopLeft : #4448
			TopRight: #4448
			BotLeft : #2228
			BotRight: #2228

			GX1: 1
			GY1: 1
			GX2: -1
			GY2: -1
	Element: Hovered
		GradientMode: Overlay
		Layer: TabButton, Normal
		Add Gradient:
			TopLeft : #fff0
			TopRight: #fff0
			BotLeft : #fff3
			BotRight: #fff3

			GX1: 1
			GY1: 1
			GX2: -1
			GY2: -1
	Element: Active
		GradientMode: Overlay
		Layer: PlainBox
			Color Override: #00c0ff

		Add Gradient:
			TopLeft : #2228
			TopRight: #2228
			BotLeft : #00c0ff48
			BotRight: #00c0ff48

			GX1: 1
			GY1: 1
			GX2: -1
			GY2: -1
	Element: Active, Hovered
		GradientMode: Overlay
		Layer: TabButton, Active
		Add Gradient:
			TopLeft : #fff0
			TopRight: #fff0
			BotLeft : #fff2
			BotRight: #fff2

			GX1: 1
			GY1: 1
			GX2: -1
			GY2: -1

Style: AccordionHeader
	Element: Normal
		Inherit: PatternBox

	Element: Hovered
		Inherit: PatternBox
		Layer: Highlight

	Element: Active
		Inherit: PatternBox

	Element: Hovered, Active
		Inherit: PatternBox
		Layer: Highlight

	Element: Disabled
		GradientMode: Overlay

		Add Gradient:
			TopLeft : #2221
			TopRight: #2221
			BotLeft : #4446
			BotRight: #4446

			GX1: 0
			GY1: 0
			GX2: 100%
			GY2: -2

		Add Gradient:
			TopLeft : #444f
			TopRight: #444f
			BotLeft : #444f
			BotRight: #444f

			GX1: 0
			GY1: -2
			GX2: 100%
			GY2: 100%

Style: BuildElement
	Element: Normal
		GradientMode: Overlay

		Add Gradient:
			TopLeft : #0000
			TopRight: #0000
			BotLeft : #2224
			BotRight: #2224

			GX1: 0
			GY1: 0
			GX2: 100%
			GY2: -1

		Add Gradient:
			TopLeft : #222f
			TopRight: #222f
			BotLeft : #222f
			BotRight: #222f

			GX1: 0
			GY1: -1
			GX2: 100%
			GY2: 100%

	Element: Hovered
		GradientMode: Overlay

		Add Gradient:
			TopLeft : #222a
			TopRight: #222a
			BotLeft : #444a
			BotRight: #444a

			GX1: 0
			GY1: 0
			GX2: 100%
			GY2: -1

		Add Gradient:
			TopLeft : #222f
			TopRight: #222f
			BotLeft : #222f
			BotRight: #222f

			GX1: 0
			GY1: -1
			GX2: 100%
			GY2: 100%

Style: SpinButton
	Inherit: TinyBox

Style: ContextMenu

Style: ContextMenuItem
	Element: Normal
		Inherit: ItemBox

	Element: Hovered
		Inherit: ItemBox, Active

	Element: Active
		Inherit: ItemBox, Active

	Element: Active, Hovered
		Inherit: ItemBox, Active

Style: Checkbox
	Element: Normal
		Inherit: ItemBox

	Element: Hovered
		Inherit: ItemBox, Hovered

	Element: Active
		Layer: ItemBox, Active

		Layer: Checkmark
			OX1: 4
			OY1: 0
			OX2: 100%
			OY2: -4

	Element: Active, Hovered
		Layer: ItemBox, Active, Hovered

		Layer: Checkmark
			OX1: 4
			OY1: 0
			OX2: 100%
			OY2: -4

Style: Radiobox
	Element: Normal
		Inherit: ItemBox

	Element: Hovered
		Inherit: ItemBox, Hovered

	Element: Active
		Layer: ItemBox

		Layer: ItemBox, Active
			OX1: 20%
			OY1: 20%
			OX2: -20%
			OY2: -20%

	Element: Active, Hovered
		Layer: ItemBox, Hovered

		Layer: ItemBox, Active
			OX1: 20%
			OY1: 20%
			OX2: -20%
			OY2: -20%

Style: ScrollVert
	Inherit: Panel

Style: ScrollVertHandle
	Element: Normal
		Inherit: StraightBox

	Element: Hovered
		Inherit: StraightBox, Hovered

	Element: Active
		Inherit: StraightBox, Active

Style: ScrollHoriz
	Inherit: Panel

Style: ScrollHorizHandle
	Element: Normal
		Inherit: StraightBox

	Element: Hovered
		Inherit: StraightBox, Hovered

	Element: Active
		Inherit: StraightBox, Active

Style: ScrollButton
	Inherit: Button

Style: ScrollUp
	Element: Normal
		Layer: ScrollButton
		Layer: UpArrow
			OX1: +4
			OY1: +4
			OX2: -4
			OY2: -4
	
	Element: Hovered
		Layer: ScrollButton, Hovered
		Layer: UpArrow
			OX1: +4
			OY1: +4
			OX2: -4
			OY2: -4	

	Element: Active
		Layer: ScrollButton, Active
		Layer: UpArrow
			OX1: +4
			OY1: +4
			OX2: -4
			OY2: -4

Style: ScrollDown
	Element: Normal
		Layer: ScrollButton
		Layer: DownArrow
			OX1: +4
			OY1: +4
			OX2: -4
			OY2: -4

	Element: Hovered
		Layer: ScrollButton, Hovered
		Layer: DownArrow
			OX1: +4
			OY1: +4
			OX2: -4
			OY2: -4

	Element: Active
		Layer: ScrollButton, Active
		Layer: DownArrow
			OX1: +4
			OY1: +4
			OX2: -4
			OY2: -4

Style: ScrollLeft
	Element: Normal
		Layer: ScrollButton
		Layer: LeftArrow
			OX1: +4
			OY1: +4
			OX2: -4
			OY2: -4

	Element: Hovered
		Layer: ScrollButton, Hovered
		Layer: LeftArrow
			OX1: +4
			OY1: +4
			OX2: -4
			OY2: -4

	Element: Active
		Layer: ScrollButton, Active
		Layer: LeftArrow
			OX1: +4
			OY1: +4
			OX2: -4
			OY2: -4

Style: ScrollRight
	Element: Normal
		Layer: ScrollButton
		Layer: RightArrow
			OX1: +4
			OY1: +4
			OX2: -4
			OY2: -4

	Element: Hovered
		Layer: ScrollButton, Hovered
		Layer: RightArrow
			OX1: +4
			OY1: +4
			OX2: -4
			OY2: -4

	Element: Active
		Layer: ScrollButton, Active
		Layer: RightArrow
			OX1: +4
			OY1: +4
			OX2: -4
			OY2: -4

Style: DistributionBar
	Inherit: StraightBox

Style: DistributionElement
	Element: Normal
		GradientMode: Overlay
		Add Gradient:
			TopLeft : #bbb9
			TopRight: #bbb9
			BotLeft : #4449
			BotRight: #4449

			GX1: 0
			GY1: 0
			GX2: 100%
			GY2: 100%

Style: ChoiceBox
	Inherit: ItemBox

// }}}
// {{{ Tab Bar
Style: GameTabBar
	Element: Normal
		Rect: [1,400] [100,424]
		Margin: 0

		Vertical: Scaled
		Horizontal: Scaled

Style: GameTab
	Element: Normal
		Rect: [2,345] [86,371]
		Margin: 17, 0
		Horizontal: Scaled

	Element: Hovered
		Rect: [2,317] [86,343]
		Margin: 17, 0
		Horizontal: Scaled

	Element: Pressed
		Inherit: GameTab, Normal

	Element: Active
		Rect: [2,372] [86,398]
		AspectMargin: Horizontal
		Margin: 17,0
		Horizontal: Scaled

Style: GameTabClose
	Element: Normal
		Rect: [3,429][17,443]
		
	Element: Hovered
		Rect: [17,429][31,443]

	Element: Active
		Rect: [32,429][46,443]
		
		
Style: GameTabNew
	Element: Normal
		Rect: [138,350][169,369]
		Margin: 9
		Horizontal: Scaled

	Element: Hovered
		Rect: [106,350][136,369]
		Margin: 9
		Horizontal: Scaled
		
	Element: Active
		Rect: [138,373][169,392]
		Margin: 9
		Horizontal: Scaled
		
Style: HomeIcon
	Element: Normal
		Rect: [105, 300][136, 319]
		
	Element: Hovered
		Rect: [138, 300][169, 319]
		
	Element: Active
		Rect: [105, 324][136,343]
		
Style: GoIcon
	Element: Normal
		Rect: [105,253][136,272]

	Element: Hovered
		Rect: [138,253][169,272]

	Element: Active
		Rect: [105,277][136,296]		

Style: GalaxyIcon
	Element: Normal
		Rect: [132,428] [157,451]

		Vertical: Uniform
		Horizontal: Uniform

Style: PlanetIcon
	Inherit: GalaxyIcon

Style: SupportIcon
	Inherit: GalaxyIcon

Style: SystemIcon
	Inherit: GalaxyIcon

Style: DesignsIcon
	Element: Normal
		Rect: [94,429] [119,451]

		Vertical: Uniform
		Horizontal: Uniform

Style: ResearchIcon
	Element: Normal
		Rect: [62,428] [87,451]

		Vertical: Uniform
		Horizontal: Uniform

Style: GlobalBar
	Element: Normal
		Rect: [107,374] [136,425]
		Margin: 4

		Vertical: Scaled
		Horizontal: Scaled

		Add Gradient:
			TopLeft : #1b1b1b44
			TopRight: #1b1b1b44
			BotLeft : #00000044
			BotRight: #00000044

			GX1: 1
			GY1: 1
			GX2: -1
			GY2: -1

Style: GoDialog
	Element: Normal
		Inherit: Panel

Style: GoItem
	Element: Normal
		Inherit: ItemBox

	Element: Active
		Inherit: ItemBox, Active

	Element: Hovered
		Inherit: ItemBox, Hovered

	Element: Active, Hovered
		Inherit: ItemBox, Active, Hovered
// }}}
// {{{ Global Bar
Style: BudgetProgress
	Inherit: ProgressBarBG

Style: BudgetProgressBar
	Element: Normal
		Inherit: ProgressBar

Style: ResearchProgress
	Inherit: ProgressBarBG

Style: ResearchProgressBar
	Element: Normal
		Inherit: ProgressBar

Style: Notification
	Element: Normal
		Inherit: PlainBox

Style: TimeDisplay
	Element: Normal
		Inherit: PlainBox

		Layer: SmallHexPattern
			OX1: +3
			OY1: +3
			OX2: -3
			OY2: -3
// }}}
// {{{ AI Empire Tab
Style: AIEmpireBG
	Element: Normal
		Inherit: HexPattern

		Add Gradient:
			TopLeft : #333f
			TopRight: #333f
			BotLeft : #000e
			BotRight: #000e

			GX1: 0%
			GY1: 0%
			GX2: 100%
			GY2: 100%

Style: ResearchField
	Inherit: HighlightPanel
// }}}
// {{{ Research Tab
Style: ResearchBG
	Element: Normal
		Inherit: HexPattern

		Add Gradient:
			TopLeft : #212e
			TopRight: #212e
			BotLeft : #000e
			BotRight: #000e

			GX1: 0%
			GY1: 0%
			GX2: 100%
			GY2: 100%

Style: ResearchField
	Inherit: HighlightPanel
// }}}
// {{{ Design Tabs
Style: DesignOverviewBG
	Element: Normal
		Inherit: HexPattern

		Add Gradient:
			TopLeft : #112e
			TopRight: #112e
			BotLeft : #000e
			BotRight: #000e

			GX1: 0%
			GY1: 0%
			GX2: 100%
			GY2: 100%

Style: DesignClassHeader
	Element: Normal
		Inherit: PanelTitle

Style: DesignClass
	Element: Normal
		Inherit: Panel

Style: DesignBorder
	Element: Normal
		Inherit: Panel
	Element: Hovered
		Inherit: Panel

		Add Gradient:
			TopLeft : #1112
			TopRight: #1112
			BotLeft : #aaa2
			BotRight: #aaa2

			GX1: +4
			GY1: +4
			GX2: -4
			GY2: -4

	Element: Active
		Inherit: Panel

		Add Gradient:
			TopLeft : #fff3
			TopRight: #fff3
			BotLeft : #fff3
			BotRight: #fff3

			GX1: 0
			GY1: 0
			GX2: 100%
			GY2: +4

		Add Gradient:
			TopLeft : #fff3
			TopRight: #fff3
			BotLeft : #fff3
			BotRight: #fff3

			GX1: 0
			GY1: -4
			GX2: 100%
			GY2: 100%

		Add Gradient:
			TopLeft : #fff3
			TopRight: #fff3
			BotLeft : #fff3
			BotRight: #fff3

			GX1: 0
			GY1: +4
			GX2: +4
			GY2: -4

		Add Gradient:
			TopLeft : #fff3
			TopRight: #fff3
			BotLeft : #fff3
			BotRight: #fff3

			GX1: -4
			GY1: +4
			GX2: 100%
			GY2: -4

	Element: Hovered, Active
		Inherit: DesignBorder, Active

		Add Gradient:
			TopLeft : #1112
			TopRight: #1112
			BotLeft : #aaa2
			BotRight: #aaa2

			GX1: +4
			GY1: +4
			GX2: -4
			GY2: -4

Style: DesignGradient
	Element: Normal
		GradientMode: Overlay
		Add Gradient:
			TopLeft : #aaa3
			TopRight: #8883
			BotLeft : #8883
			BotRight: #2223

			GX1: 0
			GY1: 0
			GX2: 100%
			GY2: 100%

Style: DesignSummary
	Element: Normal
		Layer: DesignBorder, Normal
			Color Override: #fff
		Layer: DesignGradient
			OX1: +4
			OY1: +4
			OX2: -4
			OY2: -4
		Layer: PlainBox
			OX1: 2
			OY1: -34
			OX2: -2
			OY2: -2

Style: DesignEditorBG
	Inherit: DesignOverviewBG

Style: DesignNavigationClass
	Inherit: LightPanel

Style: DesignNavigationIcon
	Inherit: RoundedBox

Style: ModuleButton
	Inherit: Button
// }}}
// {{{ Diplomacy Tabs
Style: DiplomacyBG
	Element: Normal
		Inherit: HexPattern

		Add Gradient:
			TopLeft : #121e
			TopRight: #121e
			BotLeft : #000e
			BotRight: #000e

			GX1: 0%
			GY1: 0%
			GX2: 100%
			GY2: 100%

Style: EmpireBox
	Element: Normal
		Layer: RoundedBox

		Layer: Panel
			Color Override: #fff
			OX1: +4
			OY1: +4
			OX2: -4
			OY2: -4

Style: PlayerEmpireBox
	Inherit: EmpireBox

Style: DelegationBox
	Element: Normal
		Layer: RoundedBox
			Color Override: #fff
			OX1: 0
			OY1: 4
			OX2: 100%
			OY2: -4

		Layer: RoundedBox
			OX1: 0
			OY1: 0
			OX2: -46
			OY2: 100%

Style: VotingBox
	Element: Normal
		Inherit: PlainBox

Style: VoteTotal
	Element: Normal
		Layer: PlainBox

Style: InfluenceVoteBox
	Inherit: PatternBox

Style: TreatyBox
	Inherit: PatternBox

Style: InfluenceEffectBox
	Inherit: PatternBox
// }}}
// {{{ Planet Tab
Style: QueueBackground
	Inherit: SmallHexPattern

Style: ConstructionBox
	Inherit: Panel

Style: PlanetBox
	Element: Normal
		Layer: RoundedBox
			Color Override: #ffffff80

Style: PlanetElement
	Element: Normal
		Layer: Panel
			OX1: 0
			OY1: 0
			OX2: 100%
			OY2: 100%

		Layer: PlainBox
			OX1: +4
			OY1: +4
			OX2: -4
			OY2: -4
// }}}
// {{{ System Tab
Style: SystemListBG
	Element: Normal
		Inherit: HexPattern

		Add Gradient:
			TopLeft : #210e
			TopRight: #210e
			BotLeft : #000e
			BotRight: #000e

			GX1: 0%
			GY1: 0%
			GX2: 100%
			GY2: 100%

Style: SystemPanel
	Element: Normal
		Layer: Panel
			Color Override: #fff

		Layer: PanelTitle
			OX1: +1
			OY1: +1
			OX2: -2
			OY2: +31

Style: PlanetBar
	Element: Normal
		Inherit: PlainBox
			
// }}}
// {{{ Support Tab
Style: GroupPanel
	Inherit: Panel

Style: GroupSupportClass
	Inherit: PatternBox
// }}}
// {{{ Wiki Tab
Style: WikiBG
	Element: Normal
		Inherit: HexPattern

		Add Gradient:
			TopLeft : #211e
			TopRight: #211e
			BotLeft : #000e
			BotRight: #000e

			GX1: 0%
			GY1: 0%
			GX2: 100%
			GY2: 100%
// }}}
// {{{ Popups
Style: PopupBG
	Element: Normal
		Layer: Panel
			Color Override: #fff

		Layer: SubTitle
			OX1: +2
			OY1: +1
			OX2: -3
			OY2: 25

		Layer: BG3D
			OX1: +3
			OY1: +24
			OX2: -4
			OY2: -35

Style: ShipPopupBG
	Element: Normal
		Layer: Panel
			Color Override: #fff

		Layer: SubTitle
			OX1: +2
			OY1: +1
			OX2: -3
			OY2: 25

		Layer: BG3D
			OX1: +3
			OY1: +24
			OX2: -4
			OY2: -80

Style: GenericPopupBG
	Element: Normal
		Layer: Panel
			Color Override: #fff

		Layer: SubTitle
			OX1: +2
			OY1: +1
			OX2: -3
			OY2: 25

		Layer: BG3D
			OX1: +3
			OY1: +24
			OX2: -4
			OY2: -4

Style: SelectablePopup
	Inherit: PopupBG

Style: ManageButton
	Element: Normal
		Rect: [203,644][280,664]
// }}}
// {{{ Info Bar
Style: InfoBar
	Element: Normal
		Inherit: PlainBox

Style: InfoBarPlain
	Element: Normal
		Layer: Panel
			Color Override: #fff
			OX1: 0
			OY1: 0
			OX2: 100%
			OY2: 130%
// }}}
// {{{ Main Menu
Style: MapSelectorItem
	Element: Normal
		Inherit: Panel

	Element: Hovered
		Inherit: Panel, Active

Style: EmpireSetupItem
	Element: Normal
		Layer: Panel
			OX1: 0
			OY1: 0
			OX2: 100%
			OY2: 100%

Style: GalaxySetupItem
	Element: Normal
		Layer: PatternBox
			OX1: 0
			OY1: 0
			OX2: 100%
			OY2: 100%
		Layer: Panel
			OX1: 0
			OY1: 0
			OX2: 100%
			OY2: 40

Style: MainMenuPanel
	Element: Normal
		Layer: Panel
			Color Override: #ffffffff

Style: MainMenuDescPanel
	Inherit: MainMenuPanel

Style: MainMenuItem
	Element: Normal
	Element: Hovered
		Inherit: ItemBox, Active
	Element: Active
		Inherit: PlainBox, Normal

		Add Gradient:
			TopLeft : #aa444440
			TopRight: #aa444440
			BotLeft : #aa444440
			BotRight: #aa444440

			GX1: 0
			GY1: 0
			GX2: 100%
			GY2: 100%

	Element: Hovered, Active
		Inherit: ItemBox, Active
// }}}

Added logo.png.

cannot compute difference between binary files

Added modinfo.txt.




































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
	Name: AI Empire
	Compatibility: 200
	Description: <<
		Forces the AI on for player Empires, allows configuration of player AI (difficulty, cheats) at the new game screen, and allows runtime configuration of what roles the AI manages.

		[h1]Current version: 0.4.3[/h1]

		[h1][b]Defects[/b][/h1]
		- I'm having a lot of trouble preventing all the construction that the AI wants to do. In particular it buys a factory right away and it'll fill up fleets for you.
		- The tab UI hasn't gotten a lot of love yet.
		- This probably doesn't save/load well.
		- This probably doesn't work with multiplayer well.

		[h1][b]Changelog[/b][/h1]
		- 0.4.3 - clean up the UI a bit. controllable: scuttling.
		- 0.4.2 - controllable: artifact use
		- 0.4.1 - controls are now also in AI's race and FTL components
		- 0.4.0 - controllable: construction. 'diplomacy' now includes treaties and war/peace declarations.
			- controllable: scouting and also (Anomaly Resolution) whether scouts will decide for themselves which option to pick after scanning an anomaly.
		- 0.3.3 - minor fixes, depend on Missing Expansion rather than directly on the community patch
		- 0.3.0 - added 'prevent achievements' button. Doesn't turn cheats on, but flags game as having ever had cheats on.
		- 0.2.0 - first version with AI controls
			  - 'AI Empire' tab added
			  - controllable: diplomacy, colonization, remnant hunting, research
		    also, defense of systems and attacks on enemy systems when at war
		  - all disabled initially
		- 0.1.0 - initial version, AI is on for player but you have no control over it
	>>
	Derives From: Missing Expansion
	Override: scripts/menu/new_game.as
	Override: scripts/server/empire_ai/EmpireAI.as
	Override: scripts/server/empire_ai/weasel
	Override: scripts/server/cheats.as
	Override: data/skin definitions/default.txt

Added scripts/gui/tabs/AIEmpireTab.as.




















































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import tabs.Tab;
import elements.GuiButton;
import elements.GuiPanel;
import elements.GuiMarkupText;
import icons;
from tabs.tabbar import newTab, switchToTab;

const Color colorForbidden = Color(0xaaaaaaff);
const Color colorAllowed = colors::Energy;

Tab@ createAIEmpireTab() {
        return AIEmpireTab();
}

void init() {
	Tab@ tab = createAIEmpireTab();
	newTab(tab);
	cheatCommandAI(playerEmpire, "forbid all");
}

class AIEmpireTab : Tab {
	GuiPanel@ panel;
	GuiButton@ forbidDiplomacyButton;
	GuiMarkupText@ forbidDiplomacyText;
	bool forbidDiplomacy;
	GuiButton@ forbidColonizationButton;
	GuiMarkupText@ forbidColonizationText;
	bool forbidColonization;
	GuiButton@ forbidCreepingButton;
	GuiMarkupText@ forbidCreepingText;
	bool forbidCreeping;
	GuiButton@ forbidResearchButton;
	GuiMarkupText@ forbidResearchText;
	bool forbidResearch;
	GuiButton@ forbidDefenseButton;
	GuiMarkupText@ forbidDefenseText;
	bool forbidDefense;
	GuiButton@ forbidAttackButton;
	GuiMarkupText@ forbidAttackText;
	bool forbidAttack;
	GuiButton@ forbidConstructionButton;
	GuiMarkupText@ forbidConstructionText;
	bool forbidConstruction;
	GuiButton@ forbidScoutingButton;
	GuiMarkupText@ forbidScoutingText;
	bool forbidScouting;
	GuiButton@ forbidAnomalyChoiceButton;
	GuiMarkupText@ forbidAnomalyChoiceText;
	bool forbidAnomalyChoice;
	GuiButton@ forbidArtifactButton;
	GuiMarkupText@ forbidArtifactText;
	bool forbidArtifact;
	GuiButton@ forbidScuttleButton;
	GuiMarkupText@ forbidScuttleText;
	bool forbidScuttle;
	GuiButton@ preventAchievementsButton;
	GuiMarkupText@ preventAchievementsText;
	bool preventAchievements;

	GuiButton@ enableAIButton;
	GuiMarkupText@ enableAIText;
	GuiButton@ disableAIButton;
	GuiMarkupText@ disableAIText;

	AIEmpireTab() {
		super();
		title = "AI Empire";

		forbidDiplomacy = true;
		forbidColonization = true;
		forbidCreeping = true;
		forbidResearch = true;
		forbidDefense = true;
		forbidAttack = true;
		forbidConstruction = true;
		forbidScouting = true;
		forbidAnomalyChoice = true;
		forbidArtifact = true;
		forbidScuttle = true;
		preventAchievements = false;

		@panel = GuiPanel(this, Alignment());

		@forbidDiplomacyButton = GuiButton(panel, recti_area(15,50, 120,25));
		@forbidDiplomacyText = GuiMarkupText(forbidDiplomacyButton, Alignment(Left, Top, Right, Bottom));
		forbidDiplomacyText.text = "[center]Diplomacy[/center]";
		forbidDiplomacyButton.color = colorForbidden;

		@preventAchievementsButton = GuiButton(panel, recti_area(0,370, 200,25));
		@preventAchievementsText = GuiMarkupText(preventAchievementsButton, Alignment(Left, Top, Right, Bottom));
		preventAchievementsText.text = "[center]Prevent Achievements[/center]";
		preventAchievementsButton.color = colorForbidden;

		@forbidColonizationButton = GuiButton(panel, recti_area(15,80, 120,25));
		@forbidColonizationText = GuiMarkupText(forbidColonizationButton, Alignment(Left, Top, Right, Bottom));
		forbidColonizationText.text = "[center]Colonization[/center]";
		forbidColonizationButton.color = colorForbidden;

		@forbidCreepingButton = GuiButton(panel, recti_area(15,110, 120,25));
		@forbidCreepingText = GuiMarkupText(forbidCreepingButton, Alignment(Left, Top, Right, Bottom));
		forbidCreepingText.text = "[center]Remnant Hunting[/center]";
		forbidCreepingButton.color = colorForbidden;

		@forbidResearchButton = GuiButton(panel, recti_area(15,140, 120,25));
		@forbidResearchText = GuiMarkupText(forbidResearchButton, Alignment(Left, Top, Right, Bottom));
		forbidResearchText.text = "[center]Research[/center]";
		forbidResearchButton.color = colorForbidden;

		@forbidDefenseButton = GuiButton(panel, recti_area(15,230, 120,25));
		@forbidDefenseText = GuiMarkupText(forbidDefenseButton, Alignment(Left, Top, Right, Bottom));
		forbidDefenseText.text = "[center]Wartime Defense[/center]";
		forbidDefenseButton.color = colorForbidden;

		@forbidAttackButton = GuiButton(panel, recti_area(15,200, 120,25));
		@forbidAttackText = GuiMarkupText(forbidAttackButton, Alignment(Left, Top, Right, Bottom));
		forbidAttackText.text = "[center]Wartime Offense[/center]";
		forbidAttackButton.color = colorForbidden;

		@forbidConstructionButton = GuiButton(panel, recti_area(15+200,50, 120,25));
		@forbidConstructionText = GuiMarkupText(forbidConstructionButton, Alignment(Left, Top, Right, Bottom));
		forbidConstructionText.text = "[center]Construction[/center]";
		forbidConstructionButton.color = colorForbidden;

		@forbidScoutingButton = GuiButton(panel, recti_area(15,170, 120,25));
		@forbidScoutingText = GuiMarkupText(forbidScoutingButton, Alignment(Left, Top, Right, Bottom));
		forbidScoutingText.text = "[center]Scouting[/center]";
		forbidScoutingButton.color = colorForbidden;

		@forbidAnomalyChoiceButton = GuiButton(panel, recti_area(15,260, 200,25));
		@forbidAnomalyChoiceText = GuiMarkupText(forbidAnomalyChoiceButton, Alignment(Left, Top, Right, Bottom));
		forbidAnomalyChoiceText.text = "[center]Anomaly Resolution[/center]";
		forbidAnomalyChoiceButton.color = colorForbidden;

		@forbidArtifactButton = GuiButton(panel, recti_area(15,290, 120,25));
		@forbidArtifactText = GuiMarkupText(forbidArtifactButton, Alignment(Left, Top, Right, Bottom));
		forbidArtifactText.text = "[center]Artifact Use[/center]";
		forbidArtifactButton.color = colorForbidden;

		@forbidScuttleButton = GuiButton(panel, recti_area(15,320, 120,25));
		@forbidScuttleText = GuiMarkupText(forbidScuttleButton, Alignment(Left, Top, Right, Bottom));
		forbidScuttleText.text = "[center]Scuttling[/center]";
		forbidScuttleButton.color = colorForbidden;

		@enableAIButton = GuiButton(panel, recti_area(0,0, 60,25));
		@enableAIText = GuiMarkupText(enableAIButton, Alignment(Left, Top, Right, Bottom));
		enableAIText.text = "[center]All on[/center]";
		enableAIButton.color = colorAllowed;

		@disableAIButton = GuiButton(panel, recti_area(70,0, 60,25));
		@disableAIText = GuiMarkupText(disableAIButton, Alignment(Left, Top, Right, Bottom));
		disableAIText.text = "[center]All off[/center]";
		disableAIButton.color = colorForbidden;
	}

	void tick(double time) override {
	}

	bool onGuiEvent(const GuiEvent& event) {
		if (event.type == GUI_Clicked) {
			if (event.caller is enableAIButton) {
				cheatCommandAI(playerEmpire, "allow all");
				setAll(colorAllowed, false);
				return true;
			}
			else if (event.caller is disableAIButton) {
				cheatCommandAI(playerEmpire, "forbid all");
				setAll(colorForbidden, true);
				return true;
			}
			else if (event.caller is forbidDiplomacyButton) {
				toggle(forbidDiplomacyButton, forbidDiplomacy, "Diplomacy");
				return true;
			}
			else if (event.caller is forbidColonizationButton) {
				toggle(forbidColonizationButton, forbidColonization, "Colonization");
				return true;
			}
			else if (event.caller is forbidCreepingButton) {
				toggle(forbidCreepingButton, forbidCreeping, "Creeping");
				return true;
			}
			else if (event.caller is forbidResearchButton) {
				toggle(forbidResearchButton, forbidResearch, "Research");
				return true;
			}
			else if (event.caller is forbidDefenseButton) {
				toggle(forbidDefenseButton, forbidDefense, "Defense");
				return true;
			}
			else if (event.caller is forbidAttackButton) {
				toggle(forbidAttackButton, forbidAttack, "Attack");
				return true;
			}
			else if (event.caller is forbidConstructionButton) {
				toggle(forbidConstructionButton, forbidConstruction, "Construction");
				return true;
			}
			else if (event.caller is forbidScoutingButton) {
				toggle(forbidScoutingButton, forbidScouting, "Scouting");
				return true;
			}
			else if (event.caller is forbidAnomalyChoiceButton) {
				toggle(forbidAnomalyChoiceButton, forbidAnomalyChoice, "AnomalyChoice");
				return true;
			}
			else if (event.caller is forbidArtifactButton) {
				toggle(forbidArtifactButton, forbidArtifact, "Artifact");
				return true;
			}
			else if (event.caller is forbidScuttleButton) {
				toggle(forbidScuttleButton, forbidScuttle, "Scuttle");
				return true;
			}
			else if (event.caller is preventAchievementsButton) {
				cheatCommandAI(playerEmpire, "no achievements");
				preventAchievementsButton.color = colorAllowed;
				preventAchievements = true;
				return true;
			}
		}
		return BaseGuiElement::onGuiEvent(event);
	}

	void toggle (GuiButton& btn, bool& flag, string cmd) {
		if (flag) {
			btn.color = colorAllowed;
			cheatCommandAI(playerEmpire, "allow " + cmd);
			flag = false;
		} else {
			btn.color = colorForbidden;
			cheatCommandAI(playerEmpire, "forbid " + cmd);
			flag = true;
		}
	}

	void setAll(Color color, bool b) {
		forbidDiplomacy = b;
		forbidColonization = b;
		forbidCreeping = b;
		forbidResearch = b;
		forbidDefense = b;
		forbidAttack = b;
		forbidConstruction = b;
		forbidScouting = b;
		forbidAnomalyChoice = b;
		forbidArtifact = b;
		forbidScuttle = b;
		forbidDiplomacyButton.color = color;
		forbidColonizationButton.color = color;
		forbidCreepingButton.color = color;
		forbidResearchButton.color = color;
		forbidDefenseButton.color = color;
		forbidAttackButton.color = color;
		forbidConstructionButton.color = color;
		forbidScoutingButton.color = color;
		forbidAnomalyChoiceButton.color = color;
		forbidArtifactButton.color = color;
		forbidScuttleButton.color = color;
	}

        void draw() {
                skin.draw(SS_AIEmpireBG, SF_Normal, AbsolutePosition);
                Tab::draw();
        }
}

Added scripts/menu/new_game.as.






























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
import menus;
import elements.BaseGuiElement;
import elements.GuiButton;
import elements.GuiPanel;
import elements.GuiOverlay;
import elements.GuiSprite;
import elements.GuiText;
import elements.GuiTextbox;
import elements.GuiSpinbox;
import elements.GuiCheckbox;
import elements.GuiDropdown;
import elements.GuiContextMenu;
import elements.GuiIconGrid;
import elements.GuiEmpire;
import elements.GuiMarkupText;
import elements.MarkupTooltip;
import elements.GuiBackgroundPanel;
import dialogs.SaveDialog;
import dialogs.LoadDialog;
import dialogs.MessageDialog;
import dialogs.QuestionDialog;
import util.settings_page;
import empire_data;
import traits;
import icons;
from util.draw_model import drawLitModel;

import void showMultiplayer() from "multiplayer_menu";

from maps import Map, maps, mapCount, getMap;

import settings.game_settings;
import util.game_options;

const int EMPIRE_SETUP_HEIGHT = 96;
const int GALAXY_SETUP_HEIGHT = 200;

const int REC_MAX_PEREMP = 25;
const int REC_MAX_OPTIMAL = 150;
const int REC_MAX_BAD = 400;
const int REC_MAX_OHGOD = 1000;

const array<Color> QDIFF_COLORS = {Color(0x00ff00ff), Color(0x1197e0ff), Color(0xff0000ff)};
const array<string> QDIFF_NAMES = {locale::AI_DIFF_EASY, locale::AI_DIFF_NORMAL, locale::AI_DIFF_HARD};
const array<string> QDIFF_DESC = {locale::AI_DIFF_EASY_DESC, locale::AI_DIFF_NORMAL_DESC, locale::AI_DIFF_HARD_DESC};
const array<Sprite> QDIFF_ICONS = {Sprite(spritesheet::AIDifficulty, 0), Sprite(spritesheet::AIDifficulty, 1), Sprite(spritesheet::AIDifficulty, 2)};

NameGenerator empireNames;
bool empireNamesInitialized = false;

class ConfirmStart : QuestionDialogCallback {
	void questionCallback(QuestionDialog@ dialog, int answer) {
		if(answer == QA_Yes) {
			new_game.start();
			hideNewGame(true);
		}
	}
};

class NewGame : BaseGuiElement {
	GameSettings settings;

	GuiBackgroundPanel@ empireBG;
	GuiBackgroundPanel@ gameBG;
	GuiBackgroundPanel@ chatBG;

	GuiButton@ backButton;
	GuiButton@ inviteButton;
	GuiButton@ playButton;

	EmpirePortraitCreation portraits;

	int nextEmpNum = 1;
	GuiPanel@ empirePanel;
	EmpireSetup@[] empires;
	GuiButton@ addAIButton;

	GuiSkinElement@ gameHeader;
	GuiButton@ mapsButton;
	array<GuiButton@> settingsButtons;
	array<GuiPanel@> settingsPanels;
	GuiButton@ resetButton;

	GuiPanel@ galaxyPanel;
	GalaxySetup@[] galaxies;
	GuiButton@ addGalaxyButton;

	GuiPanel@ mapPanel;
	GuiText@ mapHeader;
	GuiListbox@ mapList;

	GuiPanel@ chatPanel;
	GuiMarkupText@ chatLog;
	GuiTextbox@ chatBox;

	bool animating = false;
	bool hide = false;
	bool fromMP = false;
	bool choosingMap = false;

	string chatMessages;

	NewGame() {
		super(null, recti());

		@empireBG = GuiBackgroundPanel(this, Alignment(
			Left+0.05f, Top+0.1f, Left+0.5f-6, Bottom-0.1f));
		empireBG.title = locale::MENU_EMPIRES;
		empireBG.titleColor = Color(0x00ffe9ff);

		@gameBG = GuiBackgroundPanel(this, Alignment(
			Left+0.5f+6, Top+0.1f, Left+0.95f, Bottom-0.1f));

		@gameHeader = GuiSkinElement(gameBG, Alignment(Left+1, Top+1, Right-2, Top+41), SS_FullTitle);

		@mapsButton = GuiButton(gameHeader, Alignment(Left, Top+1, Width=200, Height=38));
		mapsButton.text = locale::MENU_GALAXIES;
		mapsButton.buttonIcon = Sprite(material::SystemUnderAttack);
		mapsButton.toggleButton = true;
		mapsButton.font = FT_Medium;
		mapsButton.pressed = true;
		mapsButton.style = SS_TabButton;

		@chatBG = GuiBackgroundPanel(this, Alignment(
			Left+0.05f, Bottom-0.1f-250, Left+0.5f-6, Bottom-0.1f));
		chatBG.title = locale::CHAT;
		chatBG.titleColor = Color(0xff8000ff);
		chatBG.visible = false;

		//Empire list
		@empirePanel = GuiPanel(empireBG,
			Alignment(Left, Top+34, Right, Bottom-4));

		//Game settings
		for(uint i = 0, cnt = GAME_SETTINGS_PAGES.length; i < cnt; ++i) {
			auto@ panel = GuiPanel(gameBG,
				Alignment(Left, Top+46, Right, Bottom-40));
			panel.visible = false;
			settingsPanels.insertLast(panel);

			auto@ page = GAME_SETTINGS_PAGES[i];
			page.create(panel);

			auto@ button = GuiButton(gameHeader, Alignment(Left+200+(i*200), Top+1, Width=200, Height=38));
			button.text = page.header;
			button.buttonIcon = page.icon;
			button.toggleButton = true;
			button.pressed = false;
			button.font = FT_Medium;
			button.style = SS_TabButton;
			settingsButtons.insertLast(button);
		}

		@resetButton = GuiButton(gameBG, Alignment(Left+0.5f-120, Bottom-40, Width=240, Height=35), locale::NG_RESET);
		resetButton.color = Color(0xff8080ff);
		resetButton.buttonIcon = icons::Reset;
		resetButton.visible = false;

		//Galaxy list
		@galaxyPanel = GuiPanel(gameBG,
			Alignment(Left, Top+46, Right, Bottom-4));
		galaxyPanel.visible = false;

		@addGalaxyButton = GuiButton(galaxyPanel,
			recti_area(vec2i(), vec2i(260, 36)),
			locale::ADD_GALAXY);
		addGalaxyButton.buttonIcon = Sprite(spritesheet::CardCategoryIcons, 3);

		//Maps choice list
		@mapPanel = GuiPanel(gameBG,
			Alignment(Left, Top+46, Right, Bottom-4));
		mapPanel.visible = true;
		choosingMap = true;

		@mapHeader = GuiText(mapPanel, Alignment(Left, Top, Right, Top+30));
		mapHeader.font = FT_Medium;
		mapHeader.horizAlign = 0.5;
		mapHeader.stroke = colors::Black;
		mapHeader.text = locale::CHOOSE_MAP;

		@mapList = GuiListbox(mapPanel,
			Alignment(Left+4, Top+34, Right-4, Bottom-4));
		mapList.itemStyle = SS_DropdownListItem;
		mapList.itemHeight = 100;

		updateMapList();

		//Chat
		@chatPanel = GuiPanel(chatBG, Alignment(Left+8, Top+34, Right-8, Bottom-38));
		@chatLog = GuiMarkupText(chatPanel, recti_area(0, 0, 100, 100));
		@chatBox = GuiTextbox(chatBG, Alignment(Left+6, Bottom-36, Right-6, Bottom-6));

		//Actions
		@playButton = GuiButton(this, Alignment(
			Right-0.05f-200, Bottom-0.1f+6, Width=200, Height=46),
			locale::START_GAME);
		playButton.buttonIcon = Sprite(spritesheet::MenuIcons, 9);

		@addAIButton = GuiButton(this, Alignment(
			Left+300, Bottom-0.1f+6, Width=200, Height=46),
			locale::ADD_AI);
		addAIButton.buttonIcon = icons::Add;

		@backButton = GuiButton(this, Alignment(
			Left+0.05f, Bottom-0.1f+6, Width=200, Height=46),
			locale::BACK);
		backButton.buttonIcon = Sprite(spritesheet::MenuIcons, 11);

		@inviteButton = GuiButton(this, Alignment(
			Left+0.05f+408, Bottom-0.1f+6, Width=200, Height=46),
			locale::INVITE_FRIEND);
		inviteButton.buttonIcon = Sprite(spritesheet::MenuIcons, 13);
		inviteButton.visible = cloud::inLobby;

		updateAbsolutePosition();
	}

	void updateMapList() {
		mapList.clearItems();
		for(uint i = 0, cnt = mapCount; i < cnt; ++i) {
			auto@ mp = getMap(i);
			if(mp.isUnique) {
				bool found = false;
				for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) {
					if(galaxies[i].mp.id == mp.id) {
						found = true;
						break;
					}
				}
				if(found)
					continue;
			}
			if(mp.isListed && !mp.isScenario && (mp.dlc.length == 0 || hasDLC(mp.dlc)))
				mapList.addItem(MapElement(mp));
		}
	}

	void init() {
		if(!empireNamesInitialized) {
			empireNames.read("data/empire_names.txt");
			empireNames.useGeneration = false;
			empireNamesInitialized = true;
		}

		portraits.reset();
		clearEmpires();
		addEmpire(true, getRacePreset(0));
		if(!mpServer && !fromMP) {
			addEmpire(false);
			addEmpire(false);
			RaceChooser(empires[0], true);
		}
		updateAbsolutePosition();

		switchPage(0);

		if(fromMP) {
			mapPanel.visible = false;
			galaxyPanel.visible = true;
			choosingMap = false;
		}
		else {
			mapPanel.visible = true;
			galaxyPanel.visible = false;
			choosingMap = true;
			updateMapList();
		}

		addGalaxyButton.visible = !fromMP;
		addAIButton.visible = !fromMP;
		chatMessages = "";

		if(fromMP) {
			playButton.text = locale::MP_NOT_READY;
			playButton.color = colors::Orange;
		}
		else {
			playButton.text = locale::START_GAME;
			playButton.color = colors::White;
		}
	}

	void addChat(const string& str) {
		chatMessages += str+"\n";
		bool wasBottom = chatPanel.vert.pos >= (chatPanel.vert.end - chatPanel.vert.page);
		chatLog.text = chatMessages;
		chatPanel.updateAbsolutePosition();
		if(wasBottom) {
			chatPanel.vert.pos = max(0.0, chatPanel.vert.end - chatPanel.vert.page);
			chatPanel.updateAbsolutePosition();
		}
	}

	void resetAIColors() {
		for(uint i = 0, cnt = empires.length; i < cnt; ++i) {
			auto@ setup = empires[i];
			if(setup.player)
				continue;
			setup.settings.color = colors::Invisible;
		}
		for(uint i = 0, cnt = empires.length; i < cnt; ++i) {
			auto@ setup = empires[i];
			if(setup.player)
				continue;
			setUniqueColor(setup);
		}
	}

	void resetAIRaces() {
		for(uint i = 0, cnt = empires.length; i < cnt; ++i) {
			auto@ setup = empires[i];
			if(setup.player)
				continue;
			setup.settings.raceName = "";
		}
		for(uint i = 0, cnt = empires.length; i < cnt; ++i) {
			auto@ setup = empires[i];
			if(setup.player)
				continue;
			setup.applyRace(getUniquePreset());
		}
	}

	RacePreset@ getUniquePreset() {
		uint index = randomi(0, getRacePresetCount() - 1);
		for(uint i = 0, cnt = getRacePresetCount(); i < cnt; ++i) {
			auto@ preset = getRacePreset((index+i) % cnt);
			if(preset.dlc.length != 0 && !hasDLC(preset.dlc))
				continue;
			bool has = false;
			for(uint n = 0, ncnt = empires.length; n < ncnt; ++n) {
				if(empires[n].settings.raceName == preset.name) {
					has = true;
					break;
				}
			}
			if(!has) {
				return preset;
			}
		}
		for(uint i = 0, cnt = getRacePresetCount(); i < cnt; ++i) {
			auto@ preset = getRacePreset((index+i) % cnt);
			if(preset.dlc.length != 0 && !hasDLC(preset.dlc))
				continue;
			return preset;
		}
		return getRacePreset(index);
	}

	void setUniqueColor(EmpireSetup@ setup) {
		bool found = false;
		Color setColor;
		for(uint i = 0, cnt = getEmpireColorCount(); i < cnt; ++i) {
			Color col = getEmpireColor(i).color;
			bool has = false;
			for(uint n = 0, ncnt = empires.length; n < ncnt; ++n) {
				if(empires[n] !is setup && empires[n].settings.color.color == col.color) {
					has = true;
					break;
				}
			}
			if(!has) {
				found = true;
				setColor = col;
				break;
			}
		}
		if(!found) {
			Colorf rnd;
			rnd.fromHSV(randomd(0, 360.0), randomd(0.5, 1.0), 1.0);
			setColor = Color(rnd);
		}
		setup.settings.color = setColor;
		setup.update();
	}

	void tick(double time) {
		if(mapIcons.length == 0) {
			mapIcons.length = mapCount;
			for(uint i = 0, cnt = mapCount; i < cnt; ++i) {
				auto@ mp = getMap(i);
				if(mp.isListed && !mp.isScenario && mp.icon.length != 0)
					mapIcons[i].load(mp.icon);
			}
		}
		inviteButton.visible = cloud::inLobby;
		addAIButton.disabled = empires.length >= 28;
		if(mpServer) {
			bool allReady = true;
			for(uint n = 0, ncnt = empires.length; n < ncnt; ++n) {
				auto@ emp = empires[n];
				if(emp.playerId != -1 && emp.playerId != CURRENT_PLAYER.id) {
					emp.found = false;
					if(!emp.settings.ready)
						allReady = false;
				}
			}

			array<Player@>@ players = getPlayers();
			for(uint i = 0, cnt = players.length; i < cnt; ++i) {
				Player@ pl = players[i];
				if(pl == CURRENT_PLAYER)
					continue;

				//Find if we already have an empire
				bool found = false;
				for(uint n = 0, ncnt = empires.length; n < ncnt; ++n) {
					auto@ emp = empires[n];
					if(emp.playerId == pl.id) {
						emp.found = true;
						found = true;
						if(pl.name.length != 0 && emp.name.text.length == 0)
							emp.name.text = pl.name;
					}
				}

				if(!found) {
					auto@ emp = addEmpire(false, getRacePreset(0));
					emp.name.text = pl.name;
					emp.address = pl.address;
					emp.setPlayer(pl.id);
				}
			}

			//Prune disconnected players
			for(uint n = 0, ncnt = empires.length; n < ncnt; ++n) {
				auto@ emp = empires[n];
				if(emp.playerId != -1 && !emp.found) {
					removeEmpire(emp);
					--n; --ncnt;
				}
			}

			//Update play button
			if(allReady)
				playButton.color = colors::Green;
			else
				playButton.color = colors::Orange;
		}
		else if(fromMP) {
			if(game_running) {
				hideNewGame(true);
				switchToMenu(main_menu, snap=true);
				return;
			}
			if(awaitingGalaxy) {
				hideNewGame(true);
				switchToMenu(main_menu, snap=true);
				showMultiplayer();
				return;
			}

			auto@ pl = findPlayer(CURRENT_PLAYER.id);
			if(pl !is null && pl.settings.ready) {
				playButton.text = locale::MP_READY;
				playButton.color = colors::Green;
			}
			else {
				playButton.text = locale::MP_NOT_READY;
				playButton.color = colors::Orange;
			}

			if(!mpIsConnected()) {
				message("Lost connection to server:\n    "
						+localize("DISCONNECT_"+uint(mpDisconnectReason)));

				hideNewGame(true);
				switchToMenu(main_menu, snap=true);
				showMultiplayer();
			}
		}
	}

	EmpireSetup@ addEmpire(bool player = false, const RacePreset@ preset = null) {
		if(empires.length >= 28)
			return null;
		empireBG.title = locale::MENU_EMPIRES + " (" + (empires.length + 1) + ")";
		uint y = empires.length * (EMPIRE_SETUP_HEIGHT + 8) + 8;
		EmpireSetup@ emp = EmpireSetup(this,
			Alignment(Left+4, Top+y, Right-4, Top+y + EMPIRE_SETUP_HEIGHT),
			player);
		portraits.randomize(emp.settings);
		if(player && settings::sNickname.length != 0)
			emp.name.text = settings::sNickname;
		else
			emp.name.text = "Empire "+(nextEmpNum++);
		if(preset is null) {
			if(player)
				@preset = getRacePreset(0);
			else
				@preset = getUniquePreset();
		}
		emp.defaultName = emp.name.text;
		emp.update();
		empires.insertLast(emp);
		empirePanel.updateAbsolutePosition();
		if(preset !is null)
			emp.applyRace(preset);
		else if(!player)
			emp.resetName();
		if(!player)
			setUniqueColor(emp);
		return emp;
	}

	EmpireSetup@ findPlayer(int id) {
		for(uint i = 0, cnt = empires.length; i < cnt; ++i) {
			if(empires[i].playerId == id)
				return empires[i];
		}
		return null;
	}

	void clearEmpires() {
		for(uint i = 0, cnt = empires.length; i < cnt; ++i)
			empires[i].remove();
		empires.length = 0;
		nextEmpNum = 2;
		updateEmpirePositions();
	}

	void removeEmpire(EmpireSetup@ emp) {
		emp.remove();
		empires.remove(emp);
		updateEmpirePositions();
	}

	void updateEmpirePositions() {
		uint cnt = empires.length;
		for(uint i = 0; i < cnt; ++i) {
			EmpireSetup@ emp = empires[i];
			emp.alignment.top.pixels = i * (EMPIRE_SETUP_HEIGHT + 8) + 8;
			emp.alignment.bottom.pixels = emp.alignment.top.pixels + EMPIRE_SETUP_HEIGHT;
			emp.updateAbsolutePosition();
		}
	}

	GalaxySetup@ addGalaxy(Map@ mp) {
		uint y = galaxies.length * (GALAXY_SETUP_HEIGHT + 8) + 8;
		GalaxySetup@ glx = GalaxySetup(this,
			Alignment(Left+8, Top+y, Right-8, Top+y + GALAXY_SETUP_HEIGHT),
			mp);

		if(mp.eatsPlayers) {
			for(uint i = 0, cnt = galaxies.length; i < cnt; ++i)
				galaxies[i].setHomeworlds(false);
		}
		else {
			bool haveEating = false;
			for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) {
				if(galaxies[i].mp.eatsPlayers) {
					haveEating = true;
				}
			}

			if(haveEating)
				glx.setHomeworlds(false);
		}

		addGalaxyButton.position = vec2i((galaxyPanel.size.width - addGalaxyButton.size.width)/2, y + GALAXY_SETUP_HEIGHT);
		galaxies.insertLast(glx);
		galaxyPanel.updateAbsolutePosition();
		updateGalaxyPositions();
		return glx;
	}

	void removeGalaxy(GalaxySetup@ glx) {
		glx.remove();
		galaxies.remove(glx);
		updateGalaxyPositions();

		if(glx.mp.eatsPlayers) {
			bool haveEating = false;
			for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) {
				if(galaxies[i].mp.eatsPlayers) {
					haveEating = true;
				}
			}

			if(!haveEating) {
				for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) {
					galaxies[i].setHomeworlds(true);
				}
			}
		}

		if(galaxies.length == 0) {
			mapHeader.text = locale::CHOOSE_MAP;
			mapPanel.visible = true;
			galaxyPanel.visible = false;
			choosingMap = true;
			updateMapList();
		}
	}

	void updateGalaxyPositions() {
		uint cnt = galaxies.length;
		for(uint i = 0; i < cnt; ++i) {
			GalaxySetup@ glx = galaxies[i];
			glx.alignment.top.pixels = i * (GALAXY_SETUP_HEIGHT + 8) + 8;
			glx.alignment.bottom.pixels = glx.alignment.top.pixels + GALAXY_SETUP_HEIGHT;
			glx.updateAbsolutePosition();
		}
		addGalaxyButton.position = vec2i((galaxyPanel.size.width - addGalaxyButton.size.width)/2, cnt * (GALAXY_SETUP_HEIGHT + 8) + 6);
		galaxyPanel.updateAbsolutePosition();
	}

	void apply() {
		apply(settings);
	}

	void reset() {
		uint newCnt = settings.empires.length;
		uint oldCnt = empires.length;
		for(uint i = newCnt; i < oldCnt; ++i) {
			removeEmpire(empires[i]);
			--i; --oldCnt;
		}
		for(uint i = 0; i < newCnt; ++i) {
			EmpireSetup@ setup;
			if(i >= oldCnt)
				@setup = addEmpire();
			else
				@setup = empires[i];
			auto@ sett = settings.empires[i];
			if(setup.playerId == sett.playerId
					&& setup.playerId == CURRENT_PLAYER.id) {
				if(setup.settings.delta > sett.delta)
					setup.apply(settings.empires[i]);
				else
					setup.load(settings.empires[i]);
			}
			else {
				setup.load(settings.empires[i]);
			}
		}
		updateEmpirePositions();

		newCnt = settings.galaxies.length;
		oldCnt = galaxies.length;
		for(uint i = newCnt; i < oldCnt; ++i) {
			removeGalaxy(galaxies[i]);
			--i; --oldCnt;
		}
		for(uint i = 0; i < newCnt; ++i) {
			GalaxySetup@ setup;
			if(i >= oldCnt)
				@setup = addGalaxy(getMap(settings.galaxies[i].map_id));
			else
				@setup = galaxies[i];
			setup.load(settings.galaxies[i]);
		}
		updateGalaxyPositions();

		for(uint i = 0, cnt = GAME_SETTINGS_PAGES.length; i < cnt; ++i)
			GAME_SETTINGS_PAGES[i].load(settings);

		addGalaxyButton.visible = !mpClient;
		addAIButton.visible = !mpClient;
	}

	void reset(GameSettings& settings) {
		this.settings = settings;
		reset();
	}

	void apply(GameSettings& settings) {
		uint empCnt = empires.length;
		settings.empires.length = empCnt;
		for(uint i = 0; i < empCnt; ++i) {
			settings.empires[i].index = i;
			empires[i].apply(settings.empires[i]);
		}

		uint glxCnt = galaxies.length;
		settings.galaxies.length = glxCnt;
		for(uint i = 0; i < glxCnt; ++i)
			galaxies[i].apply(settings.galaxies[i]);

		for(uint i = 0, cnt = GAME_SETTINGS_PAGES.length; i < cnt; ++i)
			GAME_SETTINGS_PAGES[i].apply(settings);
	}

	void start(){
		apply();

		Message msg;
		settings.write(msg);

		startNewGame(msg);
	}

	void switchPage(uint page) {
		mapsButton.pressed = page == 0;
		galaxyPanel.visible = page == 0 && !choosingMap;
		mapPanel.visible = page == 0 && choosingMap;
		if(mapPanel.visible)
			updateMapList();
		//if(page == 0)
		//	gameHeader.color = Color(0xff003fff);
		resetButton.visible = page != 0 && !mpClient;

		for(uint i = 0, cnt = settingsButtons.length; i < cnt; ++i) {
			settingsButtons[i].pressed = page == i+1;
			settingsPanels[i].visible = page == i+1;
			//if(page == i+1)
			//	gameHeader.color = GAME_SETTINGS_PAGES[i].color;
		}
	}

	bool onGuiEvent(const GuiEvent& event) {
		switch(event.type) {
			case GUI_Clicked:
				if(event.caller is playButton) {
					if(fromMP) {
						auto@ pl = findPlayer(CURRENT_PLAYER.id);
						if(pl !is null) {
							pl.settings.ready = !pl.settings.ready;
							pl.submit();
						}
					}
					else {
						if(mpServer) {
							bool allReady = true;
							for(uint n = 0, ncnt = empires.length; n < ncnt; ++n) {
								auto@ emp = empires[n];
								if(emp.playerId != -1 && emp.playerId != CURRENT_PLAYER.id) {
									if(!emp.settings.ready)
										allReady = false;
								}
							}

							if(!allReady) {
								question(locale::MP_CONFIRM_NOT_READY, ConfirmStart());
								return true;
							}
						}
						else {
							uint sysCount = 0;
							apply();
							for(uint i = 0, cnt = settings.galaxies.length; i < cnt; ++i)
								sysCount += settings.galaxies[i].systemCount * settings.galaxies[i].galaxyCount;
							uint empCount = empires.length;
							if(sysCount > REC_MAX_OHGOD) {
								question(locale::NG_WARN_OHGOD, ConfirmStart());
								return true;
							}
							else if(sysCount > REC_MAX_BAD) {
								question(locale::NG_WARN_BAD, ConfirmStart());
								return true;
							}
							else if(sysCount > REC_MAX_OPTIMAL) {
								question(locale::NG_WARN_OPTIMAL, ConfirmStart());
								return true;
							}
							else if(sysCount > REC_MAX_PEREMP * empCount) {
								question(locale::NG_WARN_PEREMP, ConfirmStart());
								return true;
							}
						}
						start();
						hideNewGame(true);
					}
					return true;
				}
				else if(event.caller is backButton) {
					if(!game_running)
						mpDisconnect();
					hideNewGame();
					return true;
				}
				else if(event.caller is inviteButton) {
					cloud::inviteFriend();
					return true;
				}
				else if(event.caller is addAIButton) {
					addEmpire();
					return true;
				}
				else if(event.caller is addGalaxyButton) {
					mapHeader.text = locale::ADD_GALAXY;
					mapPanel.visible = true;
					galaxyPanel.visible = false;
					updateMapList();
					choosingMap = true;
					return true;
				}
				else if(event.caller is resetButton) {
					for(uint i = 0, cnt = GAME_SETTINGS_PAGES.length; i < cnt; ++i) {
						if(settingsPanels[i].visible)
							GAME_SETTINGS_PAGES[i].reset();
					}
					return true;
				}
				else if(event.caller is mapsButton) {
					switchPage(0);
					return true;
				}
				else {
					for(uint i = 0, cnt = settingsButtons.length; i < cnt; ++i) {
						if(event.caller is settingsButtons[i]) {
							switchPage(i+1);
							return true;
						}
					}
				}
			break;
			case GUI_Confirmed:
				if(event.caller is chatBox) {
					string message = chatBox.text;
					if(message.length != 0)
						menuChat(message);
					chatBox.text = "";
				}
			break;
			case GUI_Changed:
				if(event.caller is mapList) {
					if(mapList.selected != -1)
						addGalaxy(cast<MapElement>(mapList.selectedItem).mp);
					if(galaxies.length != 0) {
						mapList.clearSelection();
						mapPanel.visible = false;
						galaxyPanel.visible = true;
						choosingMap = false;
					}
					return true;
				}
			break;
			case GUI_Animation_Complete:
				animating = false;
				return true;
		}

		return BaseGuiElement::onGuiEvent(event);
	}

	void updateAbsolutePosition() {
		if(!animating) {
			if(!hide) {
				size = parent.size;
				position = vec2i(0, 0);
			}
			else {
				size = parent.size;
				position = vec2i(size.x, 0);
			}
		}
		if(fromMP || mpServer) {
			chatBG.visible = true;
			chatLog.size = vec2i(chatPanel.size.width-20, chatLog.size.height);
			empireBG.alignment.bottom.pixels = 262;
		}
		else {
			chatBG.visible = false;
			empireBG.alignment.bottom.pixels = 0;
		}
		addGalaxyButton.position = vec2i((galaxyPanel.size.width - addGalaxyButton.size.width)/2, addGalaxyButton.position.y);
		BaseGuiElement::updateAbsolutePosition();
	}

	void animateIn() {
		animating = true;
		hide = false;

		rect = recti_area(vec2i(parent.size.x, 0), parent.size);
		animate_time(this, recti_area(vec2i(), parent.size), MSLIDE_TIME);
	}

	void animateOut() {
		animating = true;
		hide = true;

		rect = recti_area(vec2i(), parent.size);
		animate_time(this, recti_area(vec2i(parent.size.x, 0), parent.size), MSLIDE_TIME);
	}
};

void drawRace(const Skin@ skin, const recti& absPos, const string& name,
		const string& portrait, const array<const Trait@>@ traits = null, bool showTraits = true) {
	const Font@ normal = skin.getFont(FT_Normal);
	const Font@ bold = skin.getFont(FT_Bold);
	recti namePos = recti_area(absPos.topLeft + vec2i(8, 0), vec2i(absPos.width * 0.35, absPos.height));

	//Portrait
	auto@ prt = getEmpirePortrait(portrait);
	if(prt !is null) {
		prt.portrait.draw(recti_area(absPos.topLeft + vec2i(8, 0), vec2i(absPos.height, absPos.height)));
		namePos.topLeft.x += absPos.height+8;
	}

	//Race name
	bold.draw(pos=namePos, text=name);

	//FTL Method
	recti ftlPos = recti_area(absPos.topLeft + vec2i(absPos.width*0.35 + 16, 0),
			vec2i(absPos.width * 0.35, absPos.height));

	//Traits
	if(traits !is null) {
		recti pos = recti_area(vec2i(absPos.botRight.x - 32, absPos.topLeft.y + 3), vec2i(24, 24));
		for(uint i = 0, cnt = traits.length; i < cnt; ++i) {
			auto@ trait = traits[i];
			if(trait.unique == "FTL") {
				trait.icon.draw(recti_area(ftlPos.topLeft, vec2i(absPos.height, absPos.height)).aspectAligned(trait.icon.aspect));
				ftlPos.topLeft.x += absPos.height+8;
				normal.draw(text=trait.name, pos=ftlPos);
			}
			else if(showTraits) {
				traits[i].icon.draw(pos.aspectAligned(traits[i].icon.aspect));
				pos -= vec2i(24, 0);
			}
		}
	}
}

Color colorFromNumber(int num) {
	float hue = (num*26534371)%360;
	Colorf col;
	col.fromHSV(hue, 1.f, 1.f);
	return Color(col);
}

class RaceElement : GuiListElement {
	const RacePreset@ preset;

	RaceElement(const RacePreset@ preset) {
		@this.preset = preset;
	}

	void draw(GuiListbox@ ele, uint flags, const recti& absPos) {
		drawRace(ele.skin, absPos, preset.name, preset.portrait, preset.traits);
	}
};

class CustomRaceElement : GuiListElement {
	const EmpireSettings@ settings;

	CustomRaceElement(const EmpireSettings@ settings) {
		@this.settings = settings;
	}

	void draw(GuiListbox@ ele, uint flags, const recti& absPos) {
		drawRace(ele.skin, absPos, settings.raceName, settings.portrait, settings.traits);
	}
};

class CurrentRaceElement : GuiListElement {
	EmpireSettings@ settings;
	bool valid = true;

	CurrentRaceElement(EmpireSettings@ settings) {
		@this.settings = settings;
	}

	void update() {
		valid = settings.getTraitPoints() >= 0 && !settings.hasTraitConflicts();
	}

	void draw(GuiListbox@ ele, uint flags, const recti& absPos) {
		if(!valid) {
			Color color(0xff0000ff);
			color.a = abs((frameTime % 1.0) - 0.5) * 2.0 * 255.0;
			ele.skin.draw(SS_Button, SF_Normal, absPos.padded(-5, -3), color);
		}
		drawRace(ele.skin, absPos, settings.raceName, settings.portrait, traits=settings.traits, showTraits=false);
	}
};

class CustomizeOption : GuiListElement {
	void draw(GuiListbox@ ele, uint flags, const recti& absPos) {
		const Font@ bold = ele.skin.getFont(FT_Bold);

		recti namePos = recti_area(absPos.topLeft + vec2i(8, 0), vec2i(absPos.width * 0.95, absPos.height));
		icons::Customize.draw(recti_area(absPos.topLeft + vec2i(8, 0), vec2i(absPos.height, absPos.height)));
		namePos.topLeft.x += absPos.height+8;

		bold.draw(pos=namePos, text=locale::CUSTOMIZE_RACE, color=Color(0xff8000ff));
	}
};

class TraitList : GuiIconGrid {
	array<const Trait@> traits;

	TraitList(IGuiElement@ parent, Alignment@ align) {
		super(parent, align);

		MarkupTooltip tt(350, 0.f, true, true);
		tt.Lazy = true;
		tt.LazyUpdate = false;
		tt.Padding = 4;
		@tooltipObject = tt;
	}

	uint get_length() override {
		return traits.length;
	}

	string get_tooltip() override {
		if(hovered < 0 || hovered >= int(length))
			return "";

		auto@ trait = traits[hovered];
		return format("[color=$1][b]$2[/b][/color]\n$3",
			toString(trait.color), trait.name, trait.description);
	}

	void drawElement(uint i, const recti& pos) override {
		traits[i].icon.draw(pos.aspectAligned(traits[i].icon.aspect));
	}
};

class ChangeWelfare : GuiContextOption {
	ChangeWelfare(const string& text, uint index) {
		value = int(index);
		this.text = text;
		icon = Sprite(spritesheet::ConvertIcon, index);
	}

	void call(GuiContextMenu@ menu) override {
		playerEmpire.WelfareMode = uint(value);
	}
};

const Sprite[] DIFF_SPRITES = {
	Sprite(material::HappyFace),
	Sprite(material::StatusPeace),
	Sprite(material::StatusWar),
	Sprite(material::StatusCeaseFire),
	Sprite(spritesheet::AttributeIcons, 3),
	Sprite(spritesheet::AttributeIcons, 0),
	Sprite(spritesheet::VoteIcons, 3),
	Sprite(spritesheet::VoteIcons, 3, colors::Red)
};

const Color[] DIFF_COLORS = {
	colors::Green,
	colors::White,
	colors::White,
	colors::White,
	colors::Orange,
	colors::Red,
	colors::Red,
	colors::Red
};

const string[] DIFF_TOOLTIPS = {
	locale::DIFF_PASSIVE,
	locale::DIFF_EASY,
	locale::DIFF_NORMAL,
	locale::DIFF_HARD,
	locale::DIFF_MURDEROUS,
	locale::DIFF_CHEATING,
	locale::DIFF_SAVAGE,
	locale::DIFF_BARBARIC,
};

class ChangeDifficulty : GuiMarkupContextOption {
	int level;
	EmpireSetup@ setup;

	ChangeDifficulty(EmpireSetup@ setup, int value, const string& text) {
		level = value;
		set(text);
		@this.setup = setup;
	}

	void call(GuiContextMenu@ menu) override {
		setup.settings.difficulty = level;
		setup.update();
	}
};

class ChangeTeam : GuiMarkupContextOption {
	int team;
	EmpireSetup@ setup;

	ChangeTeam(EmpireSetup@ setup, int value) {
		team = value;
		if(value >= 0)
			set(format("[b][color=$2]$1[/color][/b]", format(locale::TEAM_TEXT, toString(value)),
						toString(colorFromNumber(value))));
		else
			set(format("[b][color=#aaa]$1...[/color][/b]", locale::NO_TEAM));
		@this.setup = setup;
	}

	void call(GuiContextMenu@ menu) override {
		setup.settings.team = team;
		setup.submit();
	}
};

class Chooser : GuiIconGrid {
	Color spriteColor;
	array<Color> colors;
	array<Sprite> sprites;

	uint selected = 0;

	Chooser(IGuiElement@ parent, Alignment@ align, const vec2i& itemSize) {
		super(parent, align);
		horizAlign = 0.5;
		vertAlign = 0.0;
		iconSize = itemSize;
		updateAbsolutePosition();
	}

	void add(const Color& col) {
		colors.insertLast(col);
	}

	void add(const Sprite& sprt) {
		sprites.insertLast(sprt);
	}

	uint get_length() override {
		return max(colors.length, sprites.length);
	}

	void drawElement(uint index, const recti& pos) override {
		if(uint(selected) == index)
			drawRectangle(pos, Color(0xffffff30));
		if(uint(hovered) == index)
			drawRectangle(pos, Color(0xffffff30));
		if(index < colors.length)
			drawRectangle(pos.padded(5), colors[index]);
		if(index < sprites.length)
			sprites[index].draw(pos, spriteColor);
	}
};

class RaceChooser : GuiOverlay {
	EmpireSetup@ setup;
	GuiSkinElement@ panel;

	GuiText@ header;
	GuiPanel@ list;

	const RacePreset@ selectedRace;
	array<GuiButton@> presetButtons;
	array<const RacePreset@> racePresets;

	GuiSprite@ portrait;
	GuiSprite@ flag;
	GuiSprite@ bgDisplay;

	GuiPanel@ descScroll;
	GuiMarkupText@ description;

	GuiPanel@ loreScroll;
	GuiMarkupText@ lore;

	GuiButton@ playButton;
	GuiButton@ customizeButton;
	GuiButton@ randomizeButton;
	GuiButton@ loadButton;
	GuiButton@ backButton;
	bool isInitial;
	bool hasChosenRace = false;
	bool chosenShipset = false;

	Chooser@ flags;
	Chooser@ colors;
	ShipsetChooser@ shipsets;

	RaceChooser(EmpireSetup@ setup, bool isInitial = false) {
		@this.setup = setup;
		this.isInitial = isInitial;
		super(null);
		closeSelf = false;

		@panel = GuiSkinElement(this, Alignment(Left-4, Top+0.05f, Right+4, Bottom-0.05f), SS_Panel);

		@customizeButton = GuiButton(panel, Alignment(Right-232, Bottom-78, Width=220, Height=33));
		customizeButton.text = locale::CUSTOMIZE_RACE;
		customizeButton.setIcon(icons::Edit);

		@randomizeButton = GuiButton(panel, Alignment(Right-452, Bottom-78, Width=220, Height=33));
		randomizeButton.text = "Randomize Race";

		@loadButton = GuiButton(panel, Alignment(Right-232, Bottom-78+33, Width=220, Height=33));
		loadButton.text = locale::LOAD_CUSTOM_RACE;
		loadButton.setIcon(icons::Load);

		int w = 250, h = 140;
		int off = max((size.width - (getRacePresetCount() * w)) / 2 - 20, 0);

		GuiSkinElement listBG(panel, Alignment(Left-4, Top+12, Right+4, Top+154), SS_PlainBox);

		@list = GuiPanel(panel, Alignment(Left+off, Top+12, Right-off, Top+174));
		updateAbsolutePosition();

		vec2i pos;
		uint curSelection = 0;
		for(uint i = 0, cnt = getRacePresetCount(); i < cnt; ++i) {
			auto@ preset = getRacePreset(i);
			if(preset.dlc.length != 0 && !hasDLC(preset.dlc))
				continue;

			racePresets.insertLast(preset);

			GuiButton btn(list, recti_area(pos.x, pos.y, w, h));
			btn.toggleButton = true;
			btn.style = SS_GlowButton;
			btn.pressed = i == 0;

			GuiSprite icon(btn, recti_area(2, 2, w*0.75, h-4));
			icon.horizAlign = 0.0;
			icon.vertAlign = 1.0;
			icon.desc = getSprite(preset.portrait);

			GuiText name(btn, recti_area(0, 0, w-4, h));
			name.font = FT_Big;
			name.stroke = colors::Black;
			name.text = preset.name;
			name.vertAlign = 0.4;
			name.horizAlign = 0.9;

			GuiSkinElement tagbar(btn, recti_area(1, h-28, w-3, 24), SS_PlainBox);
			tagbar.color = Color(0xffffff80);

			GuiText tagline(btn, recti_area(0, h-30, w-4, 24));
			tagline.font = FT_Italic;
			tagline.stroke = colors::Black;
			tagline.color = Color(0xaaaaaaff);
			tagline.text = preset.tagline;
			tagline.horizAlign = 1.0;

			TraitList traits(btn, Alignment(Left, Bottom-56, Right, Bottom-28));
			traits.iconSize = vec2i(24, 24);
			traits.horizAlign = 1.0;
			traits.fallThrough = true;
			traits.traits = preset.traits;

			if(preset.equals(setup.settings)) {
				curSelection = i;
				hasChosenRace = true;
			}

			if(!setup.player && !preset.aiSupport) {
				icon.saturation = 0.f;
				traits.visible = false;
				btn.disabled = true;
				btn.color = Color(0xffffffaa);
				name.color = Color(0xaa3030ff);

				setMarkupTooltip(btn, locale::AI_CANNOT_PLAY);
			}

			presetButtons.insertLast(btn);
			pos.x += w;
		}


		BaseGuiElement leftBG(panel, Alignment(Left+12, Top+174, Left+0.33f-6, Bottom-90));
		int y = 0;

		GuiSkinElement portBG(leftBG, Alignment(Left, Top+y, Right, Bottom), SS_PlainBox);

		@bgDisplay = GuiSprite(portBG, Alignment().padded(2), Sprite(getEmpireColor(setup.settings.color).background));
		bgDisplay.color = Color(0xffffff80);
		bgDisplay.stretchOutside = true;

		@portrait = GuiSprite(portBG, Alignment(Left+2, Top, Right-2, Height=232));
		portrait.horizAlign = 0.0;
		portrait.vertAlign = 1.0;

		@flag = GuiSprite(portBG, Alignment(Right-164, Top+4, Width=160, Height=160));
		flag.horizAlign = 1.0;
		flag.vertAlign = 0.0;
		flag.color = setup.settings.color;
		flag.color.a = 0xc0;
		flag.desc = getSprite(setup.settings.flag);

		y += 220 + 12;
		GuiSkinElement colBG(leftBG, Alignment(Left, Top+y, Right, Height=34), SS_PlainBox);
		@colors = Chooser(colBG, Alignment().padded(8, 0), vec2i(48, 32));
		for(uint i = 0, cnt = getEmpireColorCount(); i < cnt; ++i) {
			Color color = getEmpireColor(i).color;
			colors.add(color);
			if(color.color == setup.settings.color.color)
				colors.selected = i;
		}
		updateAbsolutePosition();

		y += 34 + 12;
		GuiSkinElement flagBG(leftBG, Alignment(Left, Top+y, Right, Height=110), SS_PlainBox);
		@flags = Chooser(flagBG, Alignment().padded(8, 0), vec2i(48, 48));
		flags.spriteColor = setup.settings.color;
		for(uint i = 0, cnt = getEmpireFlagCount(); i < cnt; ++i) {
			string flag = getSpriteDesc(Sprite(getEmpireFlag(i).flag));
			flags.add(getSprite(flag));
			if(flag == setup.settings.flag)
				flags.selected = i;
		}
		
		y += 110 + 12;
		// DOF - Adjust shipset selection box size.
		// Trying dynamic size based on number of shipsets.  Target 6 per row, max 4 rows (don't want to get too tall for lower resolutions).
		uint SSBoxHeight = min(int(ceil(getShipsetCount()/8.0)),4) * 75;
		GuiSkinElement shipsetBG(leftBG, Alignment(Left, Top+y, Right, Height=SSBoxHeight), SS_PlainBox);
		@shipsets = ShipsetChooser(shipsetBG, Alignment().padded(8, 0), vec2i(160, 70));
		shipsets.selectedColor = setup.settings.color;
		shipsets.selected = 0;
		shipsets.horizAlign = 0.0;
		for(uint i = 0, cnt = getShipsetCount(); i < cnt; ++i) {
			auto@ ss = getShipset(i);
			if(ss.available && (ss.dlc.length == 0 || hasDLC(ss.dlc)))
				shipsets.add(ss);
			if(ss.ident == setup.settings.shipset)
				shipsets.selected = shipsets.length-1;
		}

		GuiSkinElement loreBox(panel, Alignment(Left+0.33f+6, Top+174, Left+0.66f-6, Bottom-90), SS_PlainBox);
		@loreScroll = GuiPanel(loreBox, Alignment().fill());
		@lore = GuiMarkupText(loreScroll, recti_area(12, 12, 376, 100));
		lore.fitWidth = true;

		GuiSkinElement descBox(panel, Alignment(Left+0.66f+6, Top+174, Right-12, Bottom-90), SS_PlainBox);
		@descScroll = GuiPanel(descBox, Alignment().fill());
		@description = GuiMarkupText(descScroll, recti_area(12, 12, 376, 100));
		description.fitWidth = true;

		@playButton = GuiButton(panel, Alignment(Left+0.5f-150, Bottom-78, Left+0.5f+150, Bottom-12));
		playButton.font = FT_Medium;
		playButton.color = Color(0x00c0ffff);

		@backButton = GuiButton(panel, Alignment(Left+12, Bottom-78, Left+220, Bottom-12), locale::BACK);
		backButton.font = FT_Medium;
		backButton.buttonIcon = icons::Back;

		selectRace(curSelection);
		updateAbsolutePosition();
		updateAbsolutePosition();
	}

	void updateAbsolutePosition() {
		if(shipsets !is null && shipsets.parent !is null)
			shipsets.parent.visible = screenSize.height >= 900;
		BaseGuiElement::updateAbsolutePosition();
	}

	void close() override {
		if(isInitial)
			return;
		GuiOverlay::close();
	}

	void selectRace(uint select) {
		for(uint i = 0, cnt = presetButtons.length; i < cnt; ++i)
			presetButtons[i].pressed = i == select;

		auto@ preset = racePresets[select];

		string desc;
		if(preset.isHard)
			desc += format("[font=Subtitle][color=#ffc000]$1[/color][/font]", locale::RACE_IS_HARD);
		for(uint i = 0, cnt = preset.traits.length; i < cnt; ++i) {
			if(desc.length != 0)
				desc += "\n\n";
			desc += format("[font=Medium]$1[/font][vspace=4/]\n[offset=20]", preset.traits[i].name);
			desc += preset.traits[i].description;
			desc += "[/offset]";
		}
		description.text = desc;

		string txt = format("[font=Big]$1[/font]\n", preset.name);
		txt += format("[right][font=Medium][color=#aaa]$1[/color][/font][/right]\n\n", preset.tagline);
		txt += preset.lore;
		lore.text = txt;

		if(isInitial)
			playButton.text = format(locale::PLAY_AS_RACE, preset.name);
		else
			playButton.text = format(locale::CHOOSE_A_RACE, preset.name);
		playButton.buttonIcon = getSprite(preset.portrait);

		portrait.desc = getSprite(preset.portrait);

		loreScroll.updateAbsolutePosition();
		descScroll.updateAbsolutePosition();
		@selectedRace = preset;

		if(!chosenShipset) {
			setup.settings.shipset = preset.shipset;
			for(uint i = 0, cnt = shipsets.length; i < cnt; ++i) {
				if(shipsets.items[i].ident == preset.shipset) {
					shipsets.selected = i;
					break;
				}
			}
		}
	}

	bool onGuiEvent(const GuiEvent& evt) {
		if(evt.type == GUI_Clicked) {
			if(evt.caller is flags) {
				uint sel = flags.hovered;
				if(sel != uint(-1)) {
					string sprt = getSpriteDesc(Sprite(getEmpireFlag(sel).flag));
					setup.settings.flag = sprt;
					flag.desc = getSprite(sprt);
					flags.selected = sel;
				}
				return true;
			}
			else if(evt.caller is colors) {
				uint sel = colors.hovered;
				if(sel != uint(-1)) {
					auto empCol = getEmpireColor(sel);
					Color col = empCol.color;
					setup.settings.color = col;
					bgDisplay.desc = Sprite(empCol.background);
					flag.color = col;
					flag.color.a = 0xc0;
					flags.spriteColor = col;
					shipsets.selectedColor = col;
					colors.selected = sel;
				}
				return true;
			}
			else if(evt.caller is shipsets) {
				uint sel = shipsets.hovered;
				if(sel != uint(-1)) {
					chosenShipset = true;
					setup.settings.shipset = shipsets.items[sel].ident;
					shipsets.selected = sel;
				}
				return true;
			}
			else if(evt.caller is customizeButton) {
				isInitial = false;
				if(hasChosenRace) {
					setup.applyRace(selectedRace);
					setup.submit();
				}
				setup.openRaceWindow();
				close();
				return true;
			}
			else if(evt.caller is backButton) {
				if(isInitial) {
					isInitial = false;
					new_game.backButton.emitClicked();
					close();
					return true;
				}
				close();
				return true;
			}
			else if(evt.caller is randomizeButton) {
				hasChosenRace = true;
				selectRace(randomi(0, presetButtons.length - 1));
			}
			else if(evt.caller is loadButton) {
				isInitial = false;
				LoadRaceDialog(null, setup.settings, setup);
				close();
				return true;
			}
			else if(evt.caller is playButton) {
				setup.applyRace(selectedRace);
				setup.submit();
				if(isInitial) {
					isInitial = false;
					setup.resetName();
					setup.ng.resetAIColors();
					setup.ng.resetAIRaces();
				}
				close();
				return true;
			}
			else {
				for(uint i = 0, cnt = presetButtons.length; i < cnt; ++i) {
					if(evt.caller.isChildOf(presetButtons[i])) {
						hasChosenRace = true;
						selectRace(i);
						return true;
					}
				}
			}
		}
		return GuiOverlay::onGuiEvent(evt);
	}
};

class EmpireSetup : BaseGuiElement, IGuiCallback {
	GuiButton@ portraitButton;
	GuiEmpire@ portrait;
	EmpireSettings settings;

	NewGame@ ng;
	GuiTextbox@ name;
	GuiButton@ removeButton;
	GuiText@ handicapLabel;
	GuiSpinbox@ handicap;
	GuiButton@ raceBox;
	GameAddress address;
	GuiButton@ colorButton;
	GuiButton@ flagButton;
	GuiButton@ difficulty;
	GuiButton@ aiSettings;
	GuiSprite@ aiIcon;
	GuiText@ aiText;
	GuiButton@ teamButton;
	GuiText@ raceName;
	GuiSprite@ raceFTLIcon;
	GuiText@ raceFTL;
	TraitList@ traitList;
	bool player;
	bool found = true;
	int playerId = -1;
	ChoosePopup@ popup;
	GuiSprite@ readyness;
	string defaultName;

	EmpireSetup(NewGame@ menu, Alignment@ align, bool Player = false) {
		super(menu.empirePanel, align);
		
		@portraitButton = GuiButton(this, Alignment(Left+8, Top+4, Left+EMPIRE_SETUP_HEIGHT, Bottom-4));
		portraitButton.style = SS_NULL;
		@portrait = GuiEmpire(portraitButton, Alignment().fill());
		@portrait.settings = settings;

		@ng = menu;
		@name = GuiTextbox(this, Alignment(Left+EMPIRE_SETUP_HEIGHT+8, Top+14, Right-310, Top+0.5f-4));
		name.font = FT_Subtitle;
		name.style = SS_HoverTextbox;
		name.selectionColor = Color(0xffffff40);

		@colorButton = GuiButton(this, Alignment(Right-302, Top+14, Width=50, Height=30));
		colorButton.style = SS_HoverButton;

		@flagButton = GuiButton(this, Alignment(Right-244, Top+14, Width=50, Height=30));
		flagButton.style = SS_HoverButton;

		@teamButton = GuiButton(this, Alignment(Right-186, Top+14, Width=50, Height=30));
		teamButton.style = SS_HoverButton;

		@difficulty = GuiButton(this, Alignment(Right-128, Top+14, Width=50, Height=30));
		difficulty.style = SS_HoverButton;
		difficulty.visible = false;

		@aiSettings = GuiButton(this, Alignment(Right-128, Top+10, Width=66, Height=38));
		aiSettings.style = SS_HoverButton;
		aiSettings.visible = false;

		@aiIcon = GuiSprite(aiSettings, Alignment().padded(1, 1, 1, 5));
		@aiText = GuiText(aiSettings, Alignment());
		aiText.horizAlign = 0.5;
		aiText.vertAlign = 0.2;
		aiText.font = FT_Small;
		aiText.stroke = colors::Black;

		@raceBox = GuiButton(this, Alignment(Left+EMPIRE_SETUP_HEIGHT+8, Top+0.5f+4, Right-8, Bottom-14));
		raceBox.style = SS_HoverButton;

		@raceName = GuiText(raceBox, Alignment(Left+8, Top, Left+0.35f, Bottom));
		raceName.font = FT_Bold;

		@raceFTLIcon = GuiSprite(raceBox, Alignment(Left+0.4f, Top, Left+0.4f+22, Bottom));
		@raceFTL = GuiText(raceBox, Alignment(Left+0.4f+26, Top, Right-0.3f, Bottom));

		@traitList = TraitList(raceBox, Alignment(Right-0.3f, Top+2, Right-30, Bottom));
		traitList.iconSize = vec2i(24, 24);
		traitList.horizAlign = 1.0;
		traitList.fallThrough = true;

		player = Player;
		@removeButton = GuiButton(this,
			Alignment(Right-50, Top, Right, Top+30));
		removeButton.color = colors::Red;
		removeButton.setIcon(icons::Remove);
//		if(!player) {
			removeButton.visible = true;
			aiSettings.visible = true;
//		}
//		else {
//			removeButton.visible = false;
//			playerId = 1;
//		}

		@readyness = GuiSprite(portrait, Alignment(Right-40, Bottom-40, Right, Bottom));
		readyness.visible = false;

		applyRace(getRacePreset(randomi(0, getRacePresetCount()-1)));
		updateAbsolutePosition();
	}

	void showDifficulties() {
		GuiContextMenu menu(mousePos);
		menu.itemHeight = 54;
		menu.addOption(ChangeDifficulty(this, 0, locale::DIFF_PASSIVE));
		menu.addOption(ChangeDifficulty(this, 1, locale::DIFF_EASY));
		menu.addOption(ChangeDifficulty(this, 2, locale::DIFF_NORMAL));
		menu.addOption(ChangeDifficulty(this, 3, locale::DIFF_HARD));
		menu.addOption(ChangeDifficulty(this, 4, locale::DIFF_MURDEROUS));
		menu.addOption(ChangeDifficulty(this, 5, locale::DIFF_CHEATING));
		menu.addOption(ChangeDifficulty(this, 6, locale::DIFF_SAVAGE));
		menu.addOption(ChangeDifficulty(this, 7, locale::DIFF_BARBARIC));

		menu.updateAbsolutePosition();
	}

	void showAISettings() {
		AIPopup popup(aiSettings, this);
		aiSettings.Hovered = false;
		aiSettings.Pressed = false;
	}

	void showTeams() {
		GuiContextMenu menu(mousePos);
		menu.itemHeight = 30;

		//Figure out how many distinct teams we have
		uint distinctTeams = 0;
		uint teamMask = 0;
		int maxTeam = 0;
		for(uint i = 0, cnt = ng.empires.length; i < cnt; ++i) {
			int team = ng.empires[i].settings.team;
			if(team < 0)
				continue;

			maxTeam = max(maxTeam, team);
			uint mask = 1<<(team-1);
			if(mask & teamMask == 0) {
				teamMask |= mask;
				++distinctTeams;
			}
		}

		//Add more teams than we currently have
		menu.addOption(ChangeTeam(this, -1));
		for(uint i = 1; i <= min(max(distinctTeams+5, maxTeam+1), 30); ++i)
			menu.addOption(ChangeTeam(this, i));

		menu.updateAbsolutePosition();
	}
	
	void forceAITraits(EmpireSettings& settings) {
		for(uint i = 0, cnt = settings.traits.length; i < cnt; ++i) {
			auto@ trait = settings.traits[i];
			if(!trait.aiSupport) {
				if(trait.unique.length == 0) {
					settings.traits.removeAt(i);
					--cnt; --i;
				}
				else {
					const Trait@ repl;
					uint replCount = 0;
					for(uint n = 0, ncnt = getTraitCount(); n < ncnt; ++n) {
						auto@ other = getTrait(n);
						if(other.unique == trait.unique && other.aiSupport && other.hasDLC) {
							replCount += 1;
							if(randomd() < 1.0 / double(replCount))
								@repl = other;
						}
					}

					if(repl !is null) {
						@settings.traits[i] = repl;
					}
					else {
						settings.traits.removeAt(i);
						--cnt; --i;
					}
				}
			}
		}
	}

	void applyRace(const RacePreset@ preset) {
		preset.apply(settings);
		forceAITraits(settings);
		if(!player) {
			//forceAITraits(settings);
			if(defaultName == name.text)
				resetName();
		}
		update();
	}

	void applyRace(const EmpireSettings@ custom) {
		settings.copyRaceFrom(custom);
		forceAITraits(settings);
		if(!player) {
			//settings.copyRaceFrom(custom);
			if(defaultName == name.text)
				resetName();
		}
	}

	void resetName() {
		string race = settings.raceName;
		if(race.startswith_nocase("the "))
			race = race.substr(4);
		name.text = format(localize(empireNames.generate()), race);
		defaultName = name.text;
	}

	void setPlayer(int id) {
		player = id != -1;
		playerId = id;
		name.disabled = player || !mpClient;
		removeButton.visible = !mpClient && (!player || id != CURRENT_PLAYER.id);
		aiSettings.visible = !player;
		readyness.visible = player && id != 1;

		bool editable = id == CURRENT_PLAYER.id || (!mpClient && id == -1);
		raceBox.disabled = !editable;
		colorButton.disabled = !editable;
		flagButton.disabled = !editable;
		teamButton.disabled = !editable;
		aiSettings.disabled = !editable;
	}

	void openRaceWindow() {
		TraitsWindow win(this);
	}

	void update() {
		updateTraits();

		if(difficulty.visible) {
			difficulty.color = DIFF_COLORS[settings.difficulty];
			setMarkupTooltip(difficulty, locale::TT_DIFF+"\n"+DIFF_TOOLTIPS[settings.difficulty], width=300);
			if(difficulty.color.color != colors::White.color)
				difficulty.style = SS_Button;
			else
				difficulty.style = SS_HoverButton;
		}

		if(aiSettings.visible) {
			aiIcon.desc = QDIFF_ICONS[clamp(settings.difficulty, 0, 2)];
			aiText.color = QDIFF_COLORS[clamp(settings.difficulty, 0, 2)];
			aiText.text = getAIName(settings);
		}

		name.textColor = settings.color;
		raceName.text = settings.raceName;
		for(uint i = 0, cnt = settings.traits.length; i < cnt; ++i) {
			auto@ trait = settings.traits[i];
			if(trait.unique == "FTL") {
				raceFTLIcon.desc = trait.icon;
				raceFTL.text = trait.name;
			}
		}
	}

	void updateTraits() {
		traitList.traits.length = 0;
		for(uint i = 0, cnt = getTraitCount(); i < cnt; ++i) {
			auto@ trait = getTrait(i);
			if(settings.hasTrait(trait) && trait.unique != "FTL")
				traitList.traits.insertLast(trait);
		}
		if(settings.ready) {
			readyness.tooltip = locale::MP_PLAYER_READY;
			readyness.desc = icons::Ready;
		}
		else {
			readyness.tooltip = locale::MP_PLAYER_NOT_READY;
			readyness.desc = icons::NotReady;
		}
	}

	void submit() {
		if(mpClient)
			changeEmpireSettings(settings);
		//if(!player)
			forceAITraits(settings);
		settings.delta += 1;
		update();
	}

	bool onGuiEvent(const GuiEvent& evt) {
		switch(evt.type) {
			case GUI_Clicked:
				if(evt.caller is removeButton) {
					if(player)
						mpKick(playerId);
					else
						ng.removeEmpire(this);
					return true;
				}
				else if(evt.caller is difficulty) {
					showDifficulties();
					return true;
				}
				else if(evt.caller is aiSettings) {
					showAISettings();
					return true;
				}
				else if(evt.caller is teamButton) {
					showTeams();
					return true;
				}
				else if(evt.caller is raceBox || evt.caller is portraitButton) {
					RaceChooser(this);
					raceBox.Hovered = false;
					return true;
				}
				else if(evt.caller is colorButton) {
					vec2i pos(evt.caller.absolutePosition.topLeft.x,
							evt.caller.absolutePosition.botRight.y);
					uint cnt = getEmpireColorCount();
					vec2i size(220, ceil(double(cnt)/4.0) * 38.0);
					@popup = ChoosePopup(pos, size, vec2i(48, 32));
					@popup.callback = this;
					popup.extraHeight = 60;
					ColorPicker picker(popup.overlay, recti_area(pos+vec2i(20,size.y+2), vec2i(size.x-40, 50)));
					@picker.callback = this;
					for(uint i = 0; i < cnt; ++i)
						popup.add(getEmpireColor(i).color);
					return true;
				}
				else if(evt.caller is flagButton) {
					vec2i pos(evt.caller.absolutePosition.topLeft.x,
							evt.caller.absolutePosition.botRight.y);
					uint cnt = getEmpireFlagCount();
					vec2i size(220, ceil(double(cnt)/4.0) * 52.0);
					@popup = ChoosePopup(pos, size, vec2i(48, 48));
					@popup.callback = this;
					popup.spriteColor = settings.color;
					for(uint i = 0; i < cnt; ++i)
						popup.add(Sprite(getEmpireFlag(i).flag));
					return true;
				}
				else if(evt.caller is popup) {

				}
			break;
			case GUI_Confirmed:
				if(evt.caller is popup) {
					if(popup.colors.length > 0)
						settings.color = getEmpireColor(evt.value).color;
					else if(popup.sprites.length > 0)
						settings.flag = getEmpireFlag(evt.value).flagDef;
					@popup = null;
					submit();
					return true;
				}
				if(cast<ColorPicker>(evt.caller) !is null) {
					settings.color = cast<ColorPicker>(evt.caller).picked;
					popup.remove();
					@popup = null;
					submit();
				}
			break;
		}
		return BaseGuiElement::onGuiEvent(evt);
	}

	void apply(EmpireSettings& es) {
		es = settings;
		es.name = name.text;
		es.playerId = playerId;

		//if(player || playerId != -1)
		//	es.type = ET_Player;
		//else if(es.type == ET_Player)
			es.type = ET_WeaselAI;
	}

	void load(EmpireSettings& es) {
		name.text = es.name;
		player = es.type == uint(ET_Player);
		setPlayer(es.playerId);
		settings = es;
		update();
	}

	void draw() {
		Color color = settings.color;

		skin.draw(SS_EmpireSetupItem, SF_Normal, AbsolutePosition.padded(-10, 0), color);
		BaseGuiElement::draw();

		if(colorButton.visible) {
			setClip(colorButton.absoluteClipRect);
			drawRectangle(colorButton.absolutePosition.padded(6), color);
		}

		auto@ flag = getEmpireFlag(settings.flag);
		if(flag !is null && flagButton.visible) {
			setClip(flagButton.absoluteClipRect);
			flag.flag.draw(recti_centered(flagButton.absolutePosition,
						vec2i(flagButton.size.height, flagButton.size.height)),
					color);
		}

		if(difficulty.visible) {
			setClip(difficulty.absoluteClipRect);
			DIFF_SPRITES[settings.difficulty].draw(recti_centered(difficulty.absolutePosition,
						vec2i(difficulty.size.height, difficulty.size.height)));
		}

		if(teamButton.visible) {
			setClip(teamButton.absoluteClipRect);
			if(settings.team >= 0) {
				material::TabDiplomacy.draw(recti_centered(teamButton.absolutePosition,
							vec2i(teamButton.size.height, teamButton.size.height)));
				skin.getFont(FT_Small).draw(
					pos=teamButton.absolutePosition,
					text=locale::TEAM,
					horizAlign=0.5, vertAlign=0.0,
					stroke=colors::Black,
					color=colors::White);
				skin.getFont(FT_Medium).draw(
					pos=teamButton.absolutePosition,
					text=toString(settings.team),
					horizAlign=0.5, vertAlign=1.0,
					stroke=colors::Black,
					color=colorFromNumber(settings.team));
			}
			else {
				shader::SATURATION_LEVEL = 0.f;
				material::TabDiplomacy.draw(recti_centered(teamButton.absolutePosition,
							vec2i(teamButton.size.height, teamButton.size.height)),
						Color(0xffffff80), shader::Desaturate);
			}
		}
	}
};

string getAIName(EmpireSettings& settings) {
	string text;
	text = QDIFF_NAMES[clamp(settings.difficulty, 0, 2)];

	if(settings.aiFlags & AIF_Passive != 0)
		text += "|";
	if(settings.aiFlags & AIF_Aggressive != 0)
		text += "@";
	if(settings.aiFlags & AIF_Biased != 0)
		text += "^";
	if(settings.aiFlags & AIF_CheatPrivileged != 0)
		text += "$";
	if(settings.type == ET_BumAI)
		text += "?";

	int cheatLevel = 0;
	if(settings.cheatWealth > 0)
		cheatLevel += ceil(double(settings.cheatWealth) / 10.0);
	if(settings.cheatStrength > 0)
		cheatLevel += settings.cheatStrength;
	if(settings.cheatAbundance > 0)
		cheatLevel += settings.cheatAbundance;

	if(cheatLevel > 0) {
		if(cheatLevel > 3)
			cheatLevel = 3;
		while(cheatLevel > 0) {
			text += "+";
			cheatLevel -= 1;
		}
	}

	return text;
}

class AIPopup : BaseGuiElement {
	GuiOverlay@ overlay;

	GuiListbox@ difficulties;

	GuiText@ behaveHeading;
	GuiText@ cheatHeading;

	EmpireSetup@ setup;

	GuiCheckbox@ aggressive;
	GuiCheckbox@ passive;
	GuiCheckbox@ biased;
	GuiCheckbox@ legacy;

	GuiCheckbox@ wealth;
	GuiSpinbox@ wealthAmt;
	GuiCheckbox@ strength;
	GuiSpinbox@ strengthAmt;
	GuiCheckbox@ abundance;
	GuiSpinbox@ abundanceAmt;
	GuiCheckbox@ privileged;

	GuiButton@ okButton;
	GuiButton@ applyToAllButton;

	AIPopup(IGuiElement@ around, EmpireSetup@ setup) {
		@overlay = GuiOverlay(null);
		overlay.closeSelf = false;
		overlay.fade.a = 0;
		@this.setup = setup;

		recti pos = recti_area(
				vec2i(around.absolutePosition.botRight.x, around.absolutePosition.topLeft.y),
				vec2i(600, 200));
		if(pos.botRight.y > screenSize.y)
			pos += vec2i(0, screenSize.y - pos.botRight.y);
		if(pos.botRight.x > screenSize.x)
			pos += vec2i(screenSize.x - pos.botRight.x, 0);

		super(overlay, pos);
		updateAbsolutePosition();
		setGuiFocus(this);

		@difficulties = GuiListbox(this, Alignment(Left+4, Top+4, Left+250, Bottom-4));
		difficulties.required = true;
		difficulties.itemHeight = 64;

		for(uint i = 0; i < 3; ++i) {
			difficulties.addItem(GuiMarkupListText(
				format("[color=$1][font=Medium][stroke=#000]$2[/stroke][/font][/color]\n[color=#aaa][i]$3[/i][/color]",
					toString(QDIFF_COLORS[i]), QDIFF_NAMES[i], QDIFF_DESC[i])));
		}

		@behaveHeading = GuiText(this, Alignment(Left+260, Top+6, Left+260+170, Top+36));
		behaveHeading.font = FT_Medium;
		behaveHeading.stroke = colors::Black;
		behaveHeading.text = locale::AI_BEHAVIOR;

		pos = recti_area(vec2i(260, 36), vec2i(170, 30));

		@aggressive = GuiCheckbox(this, pos, locale::AI_AGGRESSIVE);
		setMarkupTooltip(aggressive, locale::AI_AGGRESSIVE_DESC);
		pos += vec2i(0, 30);

		@passive = GuiCheckbox(this, pos, locale::AI_PASSIVE);
		setMarkupTooltip(passive, locale::AI_PASSIVE_DESC);
		pos += vec2i(0, 30);

		@biased = GuiCheckbox(this, pos, locale::AI_BIASED);
		setMarkupTooltip(biased, locale::AI_BIASED_DESC);
		pos += vec2i(0, 30);

		@legacy = GuiCheckbox(this, pos, locale::AI_LEGACY);
		legacy.textColor = Color(0xaaaaaaff);
		legacy.visible = !hasDLC("Heralds");
		setMarkupTooltip(legacy, locale::AI_LEGACY_DESC);
		pos += vec2i(0, 30);

		@cheatHeading = GuiText(this, Alignment(Left+260+165, Top+6, Right-12, Top+36));
		cheatHeading.font = FT_Medium;
		cheatHeading.stroke = colors::Black;
		cheatHeading.text = locale::AI_CHEATS;

		pos = recti_area(vec2i(260+165, 36), vec2i(170, 30));

		@wealth = GuiCheckbox(this, recti_area(pos.topLeft, vec2i(110, 30)), locale::AI_WEALTH);
		setMarkupTooltip(wealth, locale::AI_WEALTH_DESC);
		@wealthAmt = GuiSpinbox(this, recti_area(pos.topLeft+vec2i(115, 0), vec2i(50, 30)), 10, 0, 1000, 1, 0);
		pos += vec2i(0, 30);

		@strength = GuiCheckbox(this, recti_area(pos.topLeft, vec2i(110, 30)), locale::AI_STRENGTH);
		setMarkupTooltip(strength, locale::AI_STRENGTH_DESC);
		@strengthAmt = GuiSpinbox(this, recti_area(pos.topLeft+vec2i(115, 0), vec2i(50, 30)), 1, 0, 100, 1, 0);
		pos += vec2i(0, 30);

		@abundance = GuiCheckbox(this, recti_area(pos.topLeft, vec2i(110, 30)), locale::AI_ABUNDANCE);
		setMarkupTooltip(abundance, locale::AI_ABUNDANCE_DESC);
		@abundanceAmt = GuiSpinbox(this, recti_area(pos.topLeft+vec2i(115, 0), vec2i(50, 30)), 1, 0, 100, 1, 0);
		pos += vec2i(0, 30);

		@privileged = GuiCheckbox(this, pos, locale::AI_PRIVILEGED);
		setMarkupTooltip(privileged, locale::AI_PRIVILEGED_DESC);
		pos += vec2i(0, 30);

		@okButton = GuiButton(this, Alignment(Left+260+165, Bottom-34, Width=70, Height=30), locale::OK);
		@applyToAllButton = GuiButton(this, Alignment(Left+260, Bottom-34, Width=120, Height=30), "Apply to all");

		reset();
	}

	void reset() {
		difficulties.selected = clamp(setup.settings.difficulty, 0, 2);
		aggressive.checked = setup.settings.aiFlags & AIF_Aggressive != 0;
		passive.checked = setup.settings.aiFlags & AIF_Passive != 0;
		biased.checked = setup.settings.aiFlags & AIF_Biased != 0;
		privileged.checked = setup.settings.aiFlags & AIF_CheatPrivileged != 0;
		legacy.checked = setup.settings.type == ET_BumAI;
		if(legacy.checked)
			legacy.visible = true;

		wealth.checked = setup.settings.cheatWealth > 0;
		wealthAmt.visible = wealth.checked;
		if(wealth.checked)
			wealthAmt.value = setup.settings.cheatWealth;

		strength.checked = setup.settings.cheatStrength > 0;
		strengthAmt.visible = strength.checked;
		if(strength.checked)
			strengthAmt.value = setup.settings.cheatStrength;

		abundance.checked = setup.settings.cheatAbundance > 0;
		abundanceAmt.visible = abundance.checked;
		if(abundance.checked)
			abundanceAmt.value = setup.settings.cheatAbundance;
	}

	void apply(bool toAll = false) {
		uint flags = 0;
		if(aggressive.checked)
			flags |= AIF_Aggressive;
		if(passive.checked)
			flags |= AIF_Passive;
		if(biased.checked)
			flags |= AIF_Biased;
		if(privileged.checked)
			flags |= AIF_CheatPrivileged;

		if(legacy.checked)
			setup.settings.type = ET_BumAI;
		else
			setup.settings.type = ET_WeaselAI;

		wealthAmt.visible = wealth.checked;
		if(wealthAmt.visible)
			setup.settings.cheatWealth = wealthAmt.value;
		else
			setup.settings.cheatWealth = 0;

		strengthAmt.visible = strength.checked;
		if(strengthAmt.visible)
			setup.settings.cheatStrength = strengthAmt.value;
		else
			setup.settings.cheatStrength = 0;

		abundanceAmt.visible = abundance.checked;
		if(abundanceAmt.visible)
			setup.settings.cheatAbundance = abundanceAmt.value;
		else
			setup.settings.cheatAbundance = 0;

		if (toAll) {
			for (uint i = 0; i < setup.ng.empires.length; i++) {
				setup.ng.empires[i].settings.difficulty = difficulties.selected;
				setup.ng.empires[i].settings.aiFlags = flags;
				setup.ng.empires[i].submit();
			}
		} else {
			setup.settings.difficulty = difficulties.selected;
			setup.settings.aiFlags = flags;
			setup.submit();
		}
	}

	bool onGuiEvent(const GuiEvent& evt) override {
		if(evt.type == GUI_Changed) {
			if(evt.caller is passive) {
				if(passive.checked)
					aggressive.checked = false;
				apply();
				return true;
			}
			if(evt.caller is aggressive) {
				if(aggressive.checked)
					passive.checked = false;
				apply();
				return true;
			}
			apply();
		}
		if(evt.type == GUI_Clicked) {
			if(evt.caller is okButton) {
				apply();
				remove();
				return true;
			} else if(evt.caller is applyToAllButton) {
				apply(true);
				remove();
				return true;
			}
		}
		return BaseGuiElement::onGuiEvent(evt);
	}

	void remove() {
		overlay.remove();
		@overlay = null;
		BaseGuiElement::remove();
	}

	void draw() override {
		clearClip();
		skin.draw(SS_Panel, SF_Normal, AbsolutePosition);
		BaseGuiElement::draw();
	}
};

class ChoosePopup : GuiIconGrid {
	GuiOverlay@ overlay;
	int extraHeight = 0;

	Color spriteColor;
	array<Color> colors;
	array<Sprite> sprites;

	ChoosePopup(const vec2i& pos, const vec2i& size, const vec2i& itemSize) {
		@overlay = GuiOverlay(null);
		overlay.closeSelf = false;
		overlay.fade.a = 0;
		super(overlay, recti_area(pos, size));
		horizAlign = 0.5;
		vertAlign = 0.0;
		iconSize = itemSize;
		updateAbsolutePosition();
	}

	bool onGuiEvent(const GuiEvent& evt) override {
		if(evt.caller is this && evt.type == GUI_Clicked) {
			if(hovered != -1)
				emitConfirmed(uint(hovered));
			overlay.close();
			return true;
		}
		return GuiIconGrid::onGuiEvent(evt);
	}

	void remove() {
		overlay.remove();
		@overlay = null;
		GuiIconGrid::remove();
	}

	void add(const Color& col) {
		colors.insertLast(col);
	}

	void add(const Sprite& sprt) {
		sprites.insertLast(sprt);
	}

	uint get_length() override {
		return max(colors.length, sprites.length);
	}

	void drawElement(uint index, const recti& pos) override {
		if(uint(hovered) == index)
			drawRectangle(pos, Color(0xffffff30));
		if(index < colors.length)
			drawRectangle(pos.padded(5), colors[index]);
		if(index < sprites.length)
			sprites[index].draw(pos, spriteColor);
	}

	void draw() override {
		clearClip();
		skin.draw(SS_Panel, SF_Normal, AbsolutePosition.padded(0,0,0,-extraHeight));
		GuiIconGrid::draw();
	}
};

class ColorPicker : BaseGuiElement {
	Color picked;
	bool pressed = false;
	
	ColorPicker(IGuiElement@ parent, const recti& pos) {
		super(parent, pos);
		updateAbsolutePosition();
	}

	void draw() {
		shader::HSV_VALUE = 1.f;
		shader::HSV_SAT_START = 0.5f;
		shader::HSV_SAT_END = 1.f;
		drawRectangle(AbsolutePosition, material::HSVPalette, Color());
		if(AbsolutePosition.isWithin(mousePos)) {
			clearClip();
			recti area = recti_area(mousePos-vec2i(10), vec2i(20));
			drawRectangle(area.padded(-1), colors::Black);
			drawRectangle(area, getColor(mousePos-AbsolutePosition.topLeft));
		}
		BaseGuiElement::draw();
	}

	Color getColor(vec2i offset) {
		Colorf col;
		float hue = float(offset.x) / float(AbsolutePosition.width) * 360.f;
		float sat = (1.f - float(offset.y) / float(AbsolutePosition.height)) * 0.5f + 0.5f;
		col.fromHSV(hue, sat, 1.f);
		col.a = 1.f;
		return Color(col);
	}
	
	bool onMouseEvent(const MouseEvent& event, IGuiElement@ source) {
		if(event.type == MET_Button_Down || (event.type == MET_Moved && pressed)) {
			pressed = true;
			picked = getColor(mousePos - AbsolutePosition.topLeft);

			GuiEvent evt;
			@evt.caller = this;
			evt.type = GUI_Changed;
			onGuiEvent(evt);
			return true;
		}
		else if(pressed && event.type == MET_Button_Up) {
			pressed = false;
			emitConfirmed();
			return true;
		}
		return BaseGuiElement::onMouseEvent(event, source);
	}
};

class PortraitChooser : GuiIconGrid {
	array<Sprite> sprites;
	uint selected = 0;
	Color selectedColor;

	PortraitChooser(IGuiElement@ parent, Alignment@ align, const vec2i& itemSize) {
		super(parent, align);
		horizAlign = 0.5;
		vertAlign = 0.0;
		iconSize = itemSize;
		updateAbsolutePosition();
	}

	void add(const Sprite& sprt) {
		sprites.insertLast(sprt);
	}

	uint get_length() override {
		return sprites.length;
	}

	void drawElement(uint index, const recti& pos) override {
		if(selected == index)
			drawRectangle(pos, selectedColor);
		if(uint(hovered) == index)
			drawRectangle(pos, Color(0xffffff30));
		if(index < sprites.length)
			sprites[index].draw(pos);
	}
};

class ShipsetChooser : GuiIconGrid {
	array<const Shipset@> items;
	uint selected = 0;
	Color selectedColor;

	ShipsetChooser(IGuiElement@ parent, Alignment@ align, const vec2i& itemSize) {
		super(parent, align);
		horizAlign = 0.5;
		vertAlign = 0.0;
		iconSize = itemSize;
		updateAbsolutePosition();
	}

	void add(const Shipset@ shipset) {
		items.insertLast(shipset);
	}

	uint get_length() override {
		return items.length;
	}

	void drawElement(uint index, const recti& pos) override {
		if(selected == index) {
			Color col = selectedColor;
			col.a = 0x15;
			drawRectangle(pos, col);
		}
		if(uint(hovered) == index)
			drawRectangle(pos, Color(0xffffff15));
		if(index < items.length) {
			const Shipset@ shipset = items[index];
			const Hull@ hull = shipset.hulls[0];
			if(hull !is null) {
				quaterniond rot;
				rot = quaterniond_fromAxisAngle(vec3d_front(), -0.9);
				rot *= quaterniond_fromAxisAngle(vec3d_up(), 0.6);
				rot *= quaterniond_fromAxisAngle(vec3d_right(), -0.5);
				setClip(pos);
				Color lightColor = colors::White;
				if(selected == index) {
					NODE_COLOR = Colorf(selectedColor);
					lightColor = selectedColor;
				}
				else
					NODE_COLOR = Colorf(1.f, 1.f, 1.f, 1.f);
				drawLitModel(hull.model, hull.material, pos+vec2i(-4,0), rot, 1.9, lightColor=lightColor);
				clearClip();
			}

			const Font@ ft = skin.getFont(FT_Bold);
			if(selected == index || uint(hovered) == index)
				ft.draw(text=shipset.name, pos=pos.padded(0,4),
						horizAlign=0.5, vertAlign=0.0, stroke=colors::Black,
						color=(selected == index ? selectedColor : colors::White));
		}
	}
};

class WeaponSkinChooser : GuiIconGrid {
	array<const EmpireWeaponSkin@> items;
	uint selected = 0;
	Color selectedColor;

	WeaponSkinChooser(IGuiElement@ parent, Alignment@ align, const vec2i& itemSize) {
		super(parent, align);
		horizAlign = 0.5;
		vertAlign = 0.0;
		iconSize = itemSize;
		updateAbsolutePosition();
	}

	void add(const EmpireWeaponSkin@ it) {
		items.insertLast(it);
	}

	uint get_length() override {
		return items.length;
	}

	void drawElement(uint index, const recti& pos) override {
		if(selected == index) {
			Color col = selectedColor;
			col.a = 0x15;
			drawRectangle(pos, col);
		}
		if(uint(hovered) == index)
			drawRectangle(pos, Color(0xffffff15));
		if(index < items.length)
			items[index].icon.draw(pos);
	}
};

class TraitDisplay : BaseGuiElement {
	const Trait@ trait;
	GuiSprite@ icon;
	GuiMarkupText@ name;
	GuiMarkupText@ description;
	GuiText@ points;
	GuiText@ conflicts;
	GuiCheckbox@ check;
	bool hovered = false;
	bool conflict = false;

	TraitDisplay(IGuiElement@ parent) {
		super(parent, recti());

		@icon = GuiSprite(this, Alignment(Left+20, Top+12, Left+52, Bottom-12));

		@name = GuiMarkupText(this, Alignment(Left+65, Top+8, Right-168, Top+38));
		name.defaultFont = FT_Medium;
		name.defaultStroke = colors::Black;

		@description = GuiMarkupText(this, Alignment(Left+124, Top+34, Right-168, Bottom-8));

		@conflicts = GuiText(this, Alignment(Right-360, Top+8, Right-56, Bottom-8));
		conflicts.vertAlign = 0.1;
		conflicts.horizAlign = 1.0;

		@points = GuiText(this, Alignment(Right-160, Top+8, Right-56, Bottom-8));
		points.horizAlign = 1.0;
		points.font = FT_Subtitle;

		@check = GuiCheckbox(this, Alignment(Right-48, Top+0.5f-20, Right-8, Top+0.5f+20), "");
	}

	bool onGuiEvent(const GuiEvent& evt) override {
		switch(evt.type) {
			case GUI_Mouse_Entered:
				hovered = true;
			break;
			case GUI_Mouse_Left:
				hovered = false;
			break;
			case GUI_Changed:
				if(evt.caller is check) {
					check.checked = !check.checked;
					emitClicked();
					return true;
				}
			break;
		}
		return BaseGuiElement::onGuiEvent(evt);
	}

	bool onMouseEvent(const MouseEvent& evt, IGuiElement@ caller) override {
		switch(evt.type) {
			case MET_Button_Down:
				if(evt.button == 0)
					return true;
			break;
			case MET_Button_Up:
				if(evt.button == 0) {
					emitClicked();
					return true;
				}
			break;
		}
		return BaseGuiElement::onMouseEvent(evt, caller);
	}

	void set(const Trait@ trait, bool selected, bool conflict) {
		@this.trait = trait;
		this.conflict = conflict;
		description.text = trait.description;
		icon.desc = trait.icon;
		name.defaultColor = trait.color;

		if(trait.gives > 0) {
			points.text = format(locale::RACE_POINTS_POS, toString(trait.gives));
			points.color = colors::Green;
			points.visible = true;
		}
		else if(trait.cost > 0) {
			points.text = format(locale::RACE_POINTS_NEG, toString(trait.cost));
			points.color = colors::Red;
			points.visible = true;
		}
		else {
			points.text = locale::RACE_POINTS_NEU;
			points.color = Color(0xaaaaaaff);
			points.visible = false;
		}

		bool displayConflicts = false;
		if(trait.conflicts.length > 0) {
			if(conflict) {
				conflicts.color = colors::Red;
				conflicts.font = FT_Bold;
				conflicts.vertAlign = 0.2;
			}
			else {
				conflicts.color = Color(0xaaaaaaff);
				conflicts.font = FT_Italic;
				conflicts.vertAlign = 0.1;
			}
			string str = locale::CONFLICTS+" ";
			for(uint i = 0, cnt = trait.conflicts.length; i < cnt; ++i) {
				if(!trait.conflicts[i].available)
					continue;
				if(i != 0)
					str += ", ";
				str += trait.conflicts[i].name;
				displayConflicts = true;
			}

			conflicts.text = str;
		}
		if(displayConflicts) {
			conflicts.visible = true;
			points.vertAlign = 0.7;
		}
		else {
			conflicts.visible = false;
			points.vertAlign = 0.5;
		}

		if(trait.unique.length != 0) {
			check.style = SS_Radiobox;
			if(description.alignment.right.pixels != 52) {
				description.alignment.right.pixels = 52;
				description.updateAbsolutePosition();
			}
		}
		else {
			check.style = SS_Checkbox;
			if(description.alignment.right.pixels != 168) {
				description.alignment.right.pixels = 168;
				description.updateAbsolutePosition();
			}
		}

		name.text = trait.name;
		check.checked = selected;
	}

	void draw() {
		if(check.checked)
			skin.draw(SS_Glow, SF_Normal, AbsolutePosition, trait.color);
		skin.draw(SS_Panel, SF_Normal, AbsolutePosition.padded(4), trait.color);
		if(hovered)
			drawRectangle(AbsolutePosition.padded(8), Color(0xffffff10));
		BaseGuiElement::draw();
	}
};

class SaveRaceDialog : SaveDialog {
	EmpireSettings settings;
	EmpireSetup@ setup;

	SaveRaceDialog(IGuiElement@ bind, EmpireSettings@ settings, EmpireSetup@ setup) {
		this.settings = settings;
		@this.setup = setup;
		super(bind, modProfile["races"], settings.raceName+".race");
	}

	void clickConfirm() override {
		exportRace(settings, path);
	}
};

class LoadRaceDialog : LoadDialog {
	EmpireSettings settings;
	EmpireSetup@ setup;
	TraitsWindow@ win;

	LoadRaceDialog(TraitsWindow@ win, EmpireSettings@ settings, EmpireSetup@ setup) {
		this.settings = settings;
		@this.setup = setup;
		@this.win = win;
		super(win, modProfile["races"]);
	}

	void clickConfirm() override {
		importRace(setup.settings, path);
		if(win !is null)
			win.update();
		setup.submit();
	}
};

class TraitElement : GuiListElement {
	const Trait@ trait;

	void draw(GuiListbox@ ele, uint flags, const recti& absPos) {
		recti iconPos = recti_area(absPos.topLeft+vec2i(10, 5), vec2i(absPos.height-10, absPos.height-10));
		trait.icon.draw(iconPos);

		recti textPos = absPos.padded(absPos.height + 10, 0, 10, 4);
		ele.skin.getFont(FT_Medium).draw(
			text=trait.name, pos=textPos);
	}

	string get_tooltipText() {
		return format("[color=$1][b]$2[/b][/color]\n$3",
			toString(trait.color), trait.name, trait.description);
	}
};

class TraitsWindow : BaseGuiElement {
	GuiOverlay@ overlay;
	EmpireSetup@ setup;

	GuiBackgroundPanel@ bg;

	GuiListbox@ categories;
	array<const TraitCategory@> usedCategories;

	GuiPanel@ profilePanel;

	GuiText@ nameLabel;
	GuiTextbox@ name;

	GuiText@ portraitLabel;
	PortraitChooser@ portrait;

	GuiText@ shipsetLabel;
	ShipsetChooser@ shipset;

	GuiText@ weaponSkinLabel;
	WeaponSkinChooser@ weaponSkin;

	GuiText@ traitsLabel;
	GuiListbox@ traitList;

	GuiText@ pointsLabel;

	GuiPanel@ traitPanel;
	GuiText@ noTraits;
	array<TraitDisplay@> traits;

	GuiButton@ acceptButton;
	GuiButton@ saveButton;
	GuiButton@ loadButton;

	TraitsWindow(EmpireSetup@ setup) {
		@this.setup = setup;
		@overlay = GuiOverlay(null);
		overlay.closeSelf = false;
		super(overlay, Alignment(Left+0.11f, Top+0.11f, Right-0.11f, Bottom-0.11f));
		updateAbsolutePosition();

		@bg = GuiBackgroundPanel(this, Alignment().fill());
		bg.titleColor = Color(0xff8000ff);
		bg.title = locale::CUSTOMIZE_RACE;

		@categories = GuiListbox(bg, Alignment(Left+4, Top+32, Left+250, Bottom-4));
		categories.itemHeight = 44;
		categories.style = SS_PlainOverlay;
		categories.itemStyle = SS_TabButton;
		categories.addItem(GuiMarkupListText(locale::RACE_PROFILE));
		categories.required = true;

		for(uint i = 0, cnt = getTraitCategoryCount(); i < cnt; ++i) {
			auto@ cat = getTraitCategory(i);
			bool hasTraits = false;
			for(uint n = 0, ncnt = getTraitCount(); n < ncnt; ++n) {
				if(getTrait(n).category is cat && getTrait(n).available && getTrait(n).hasDLC) {
					hasTraits = true;
					break;
				}
			}
			if(hasTraits) {
				categories.addItem(GuiMarkupListText(cat.name));
				usedCategories.insertLast(cat);
			}
		}

		@acceptButton = GuiButton(bg, Alignment(Right-140, Bottom-40, Right-3, Bottom-3), locale::ACCEPT);
		@loadButton = GuiButton(bg, Alignment(Right-274, Bottom-40, Right-154, Bottom-3), locale::LOAD);
		@saveButton = GuiButton(bg, Alignment(Right-400, Bottom-40, Right-280, Bottom-3), locale::SAVE);
		@pointsLabel = GuiText(bg, Alignment(Left+264, Bottom-40, Right-410, Bottom-3));
		pointsLabel.font = FT_Medium;

		Alignment panelAlign(Left+258, Top+32, Right-4, Bottom-40);

		@profilePanel = GuiPanel(bg, panelAlign);
		@traitPanel = GuiPanel(bg, panelAlign);
		traitPanel.visible = false;

		int y = 8;

		@nameLabel = GuiText(profilePanel, Alignment(Left+12, Top+y, Left+200, Top+y+30), locale::RACE_NAME, FT_Bold);
		@name = GuiTextbox(profilePanel, Alignment(Left+200, Top+y, Right-12, Top+y+30), setup.settings.raceName);
		y += 38;

		int h = 80 + (getEmpirePortraitCount() / ((size.width - 200) / 70)) * 80;
		@portraitLabel = GuiText(profilePanel, Alignment(Left+12, Top+y, Left+200, Top+y+30), locale::PORTRAIT, FT_Bold);
		@portrait = PortraitChooser(profilePanel, Alignment(Left+200, Top+y, Right-12, Top+y+h), vec2i(70, 70));
		portrait.selectedColor = setup.settings.color;
		
		portrait.selected = randomi(0, getEmpirePortraitCount()-1);
		portrait.horizAlign = 0.0;
		for(uint i = 0, cnt = getEmpirePortraitCount(); i < cnt; ++i) {
			auto@ img = getEmpirePortrait(i);
			portrait.add(Sprite(img.portrait));
			if(img.ident == setup.settings.portrait)
				portrait.selected = i;
		}
		y += h+8;

		h = 80 + (getShipsetCount() / ((size.width - 200) / 150)) * 80;
		@shipsetLabel = GuiText(profilePanel, Alignment(Left+12, Top+y, Left+200, Top+y+30), locale::SHIPSET, FT_Bold);
		@shipset = ShipsetChooser(profilePanel, Alignment(Left+200, Top+y, Right-12, Top+y+h), vec2i(150, 70));
		shipset.selectedColor = setup.settings.color;
		shipset.selected = 0;
		shipset.horizAlign = 0.0;
		for(uint i = 0, cnt = getShipsetCount(); i < cnt; ++i) {
			auto@ ss = getShipset(i);
			if(ss.available && (ss.dlc.length == 0 || hasDLC(ss.dlc)))
				shipset.add(ss);
			if(ss.ident == setup.settings.shipset)
				shipset.selected = shipset.length-1;
		}
		y += h+8;

		@weaponSkinLabel = GuiText(profilePanel, Alignment(Left+12, Top+y, Left+200, Top+y+30), locale::WEAPON_SKIN, FT_Bold);
		@weaponSkin = WeaponSkinChooser(profilePanel, Alignment(Left+200, Top+y, Right-12, Top+y+80), vec2i(120, 70));
		weaponSkin.selectedColor = setup.settings.color;
		weaponSkin.selected = 0;
		weaponSkin.horizAlign = 0.0;
		for(uint i = 0, cnt = getEmpireWeaponSkinCount(); i < cnt; ++i) {
			auto@ skin = getEmpireWeaponSkin(i);
			weaponSkin.add(skin);
			if(skin.ident == setup.settings.effectorSkin)
				weaponSkin.selected = weaponSkin.length-1;
		}
		y += 88;

		@traitsLabel = GuiText(profilePanel, Alignment(Left+12, Top+y, Left+200, Top+y+30), locale::TRAITS, FT_Bold);
		@traitList = GuiListbox(profilePanel, Alignment(Left+200, Top+y, Right-12, Bottom-8));
		traitList.itemStyle = SS_StaticListboxItem;
		traitList.itemHeight = 50;
		addLazyMarkupTooltip(traitList);
		@noTraits = GuiText(profilePanel, Alignment(Left+240, Top+y+10, Right-12, Top+y+50), locale::NO_TRAITS);
		noTraits.color = Color(0xaaaaaaff);
		noTraits.vertAlign = 0.0;
		y += 58;

		update();
		updateAbsolutePosition();
	}

	void update() {
		int sel = categories.selected;
		profilePanel.visible = sel == 0;
		traitPanel.visible = sel != 0;

		uint index = 0;
		const TraitCategory@ cat;
		if(sel > 0)
			@cat = usedCategories[sel - 1];

		int points = STARTING_TRAIT_POINTS;
		for(uint i = 0, cnt = setup.settings.traits.length; i < cnt; ++i) {
			points += setup.settings.traits[i].gives;
			points -= setup.settings.traits[i].cost;
		}

		if(traitPanel.visible) {
			int y = 0;
			array<const Trait@> list;
			for(uint i = 0, cnt = getTraitCount(); i < cnt; ++i) {
				auto@ trait = getTrait(i);
				if(cat !is null && cat !is trait.category)
					continue;
				if(!setup.player && !trait.aiSupport)
					continue;
				if(!trait.available)
					continue;
				if(!trait.hasDLC)
					continue;
				list.insertLast(trait);
			}
			list.sortAsc();

			for(uint i = 0, cnt = list.length; i < cnt; ++i) {
				auto@ trait = list[i];
				TraitDisplay@ disp;
				if(index < traits.length) {
					@disp = traits[index];
				}
				else {
					@disp = TraitDisplay(traitPanel);
					traits.insertLast(disp);
				}

				disp.set(trait, setup.settings.hasTrait(trait), trait.hasConflicts(setup.settings.traits));
				disp.alignment.set(Left, Top+y, Right, Top+y+140);
				disp.updateAbsolutePosition();
				int needH = disp.description.renderer.height+48;
				if(needH != 140) {
					disp.alignment.set(Left, Top+y, Right, Top+y+needH);
					disp.updateAbsolutePosition();
				}

				++index;
				y += needH;
			}

			for(uint i = index, cnt = traits.length; i < cnt; ++i)
				traits[i].remove();
			traits.length = index;
			traitPanel.updateAbsolutePosition();
		}

		if(profilePanel.visible) {
			uint cnt = setup.settings.traits.length;
			traitList.removeItemsFrom(cnt);
			for(uint i = 0; i < cnt; ++i) {
				auto@ item = cast<TraitElement>(traitList.getItemElement(i));
				if(item is null) {
					@item = TraitElement();
					traitList.addItem(item);
				}

				@item.trait = setup.settings.traits[i];
			}
			noTraits.visible = cnt == 0;
		}

		if(points > 0) {
			pointsLabel.color = colors::Green;
			pointsLabel.text = format(locale::RACE_POINTS_AVAIL_POS, toString(points));
			pointsLabel.visible = true;
		}
		else if(points < 0) {
			pointsLabel.color = colors::Red;
			pointsLabel.text = format(locale::RACE_POINTS_AVAIL_NEG, toString(-points));
			pointsLabel.visible = true;
		}
		else {
			pointsLabel.color = Color(0xaaaaaaff);
			pointsLabel.text = format(locale::RACE_POINTS_AVAIL_POS, toString(points));
			pointsLabel.visible = false;
		}

		if(points >= 0 && !setup.settings.hasTraitConflicts())
			acceptButton.color = colors::Green;
		else
			acceptButton.color = colors::Red;
	}

	bool onGuiEvent(const GuiEvent& evt) override {
		if(evt.caller is acceptButton) {
			if(evt.type == GUI_Clicked) {
				overlay.close();
				return true;
			}
		}
		if(evt.caller is saveButton) {
			if(evt.type == GUI_Clicked) {
				SaveRaceDialog(this, setup.settings, setup);
				return true;
			}
		}
		else if(evt.caller is loadButton) {
			if(evt.type == GUI_Clicked) {
				LoadRaceDialog(this, setup.settings, setup);
				return true;
			}
		}
		if(evt.type == GUI_Clicked) {
			if(evt.caller is portrait) {
				int hov = portrait.hovered;
				if(hov >= 0) {
					setup.settings.portrait = getEmpirePortrait(hov).ident;
					portrait.selected = hov;
				}
				setup.submit();
				return true;
			}
			if(evt.caller is shipset) {
				int hov = shipset.hovered;
				if(hov >= 0) {
					setup.settings.shipset = shipset.items[hov].ident;
					shipset.selected = hov;
				}
				setup.submit();
				return true;
			}
			if(evt.caller is weaponSkin) {
				int hov = weaponSkin.hovered;
				if(hov >= 0) {
					setup.settings.effectorSkin = weaponSkin.items[hov].ident;
					weaponSkin.selected = hov;
				}
				setup.submit();
				return true;
			}
			
			auto@ disp = cast<TraitDisplay>(evt.caller);
			if(disp !is null) {
				if(disp.trait.unique.length != 0)
					setup.settings.chooseTrait(disp.trait);
				else if(setup.settings.hasTrait(disp.trait))
					setup.settings.removeTrait(disp.trait);
				else
					setup.settings.addTrait(disp.trait);
				update();
				setup.submit();
			}
		}
		if(evt.type == GUI_Changed) {
			if(evt.caller is name) {
				setup.settings.raceName = name.text;
				setup.submit();
				return true;
			}
			if(evt.caller is categories) {
				update();
				return true;
			}
		}
		return BaseGuiElement::onGuiEvent(evt);
	}

	void draw() override {
		BaseGuiElement::draw();
	}
};

class GalaxySetup : BaseGuiElement {
	Map@ mp;
	NewGame@ ng;
	GuiText@ name;

	GuiText@ timesLabel;
	GuiSpinbox@ timesBox;

	GuiPanel@ settings;

	GuiButton@ removeButton;
	GuiButton@ hwButton;
	GuiSprite@ hwX;

	GalaxySetup(NewGame@ menu, Alignment@ align, Map@ fromMap) {
		super(menu.galaxyPanel, align);
		@mp = fromMap.create();
		@ng = menu;

		@name = GuiText(this, Alignment(Left+6, Top+5, Right-262, Height=28));
		name.text = mp.name;
		name.font = FT_Medium;
		name.color = mp.color;
		name.stroke = colors::Black;

		@timesBox = GuiSpinbox(this, Alignment(Right-190, Top+7, Width=52, Height=22), 1.0);
		timesBox.min = 1.0;
		timesBox.max = 100.0;
		timesBox.decimals = 0;
		timesBox.color = Color(0xffffff60);

		@timesLabel = GuiText(this, Alignment(Right-135, Top+7, Width=25, Height=22), "x");

		timesBox.visible = !mp.isUnique;
		timesLabel.visible = !mp.isUnique;

		@removeButton = GuiButton(this, Alignment(Right-84, Top+4, Right-25, Top+34));
		removeButton.setIcon(icons::Remove);
		removeButton.color = colors::Red;

		@hwButton = GuiButton(this, Alignment(Right-230, Top+5, Width=26, Height=26));
		hwButton.setIcon(Sprite(spritesheet::PlanetType, 2, Color(0xffffffaa)), padding=0);
		hwButton.toggleButton = true;
		hwButton.pressed = false;
		hwButton.style = SS_IconButton;
		hwButton.color = Color(0xff0000ff);
		setMarkupTooltip(hwButton, locale::NGTT_MAP_HW);
		@hwX = GuiSprite(hwButton, Alignment(), Sprite(spritesheet::QuickbarIcons, 3, Color(0xffffff80)));
		hwX.visible = false;

		@settings = GuiPanel(this,
			Alignment(Left, Top+42, Right, Bottom-4));
		mp.create(settings);
	}

	void setHomeworlds(bool value) {
		hwButton.pressed = !value;
		hwX.visible = hwButton.pressed;
		if(hwButton.pressed)
			hwButton.fullIcon.color = Color(0xffffffff);
		else
			hwButton.fullIcon.color = Color(0xffffffaa);
	}

	void apply(MapSettings& set) {
		set.map_id = mp.id;
		set.galaxyCount = timesBox.value;
		@set.parent = ng.settings;
		set.allowHomeworlds = !hwButton.pressed;
		mp.apply(set);
	}

	void load(MapSettings& set) {
		auto@ _map = getMap(set.map_id);
		if(getClass(mp) !is getClass(_map))
			@mp = cast<Map>(getClass(_map).create());
		timesBox.value = set.galaxyCount;

		hwButton.pressed = !set.allowHomeworlds;
		hwX.visible = hwButton.pressed;
		if(hwButton.pressed)
			hwButton.fullIcon.color = Color(0xffffffff);
		else
			hwButton.fullIcon.color = Color(0xffffffaa);

		mp.load(set);
	}

	bool onGuiEvent(const GuiEvent& evt) {
		switch(evt.type) {
			case GUI_Clicked:
				if(evt.caller is removeButton) {
					ng.removeGalaxy(this);
					return true;
				}
				else if(evt.caller is hwButton) {
					setHomeworlds(!hwButton.pressed);
					return true;
				}
			break;
		}
		return BaseGuiElement::onGuiEvent(evt);
	}

	void draw() {
		recti bgPos = AbsolutePosition.padded(-5,0,-4,0);
		clipParent(bgPos);
		skin.draw(SS_GalaxySetupItem, SF_Normal, bgPos.padded(-4,0), mp.color);
		resetClip();
		auto@ icon = mapIcons[mp.index];
		if(mp.icon.length != 0 && icon.isLoaded(0)) {
			recti pos = AbsolutePosition.padded(0,42,0,0).aspectAligned(1.0, horizAlign=1.0, vertAlign=1.0);
			icon.draw(pos, Color(0xffffff80));
		}
		BaseGuiElement::draw();
	}
};

class MapElement : GuiListElement {
	Map@ mp;

	MapElement(Map@ _map) {
		@mp = _map;
	}

	void draw(GuiListbox@ ele, uint flags, const recti& absPos) override {
		const Font@ title = ele.skin.getFont(FT_Subtitle);
		const Font@ normal = ele.skin.getFont(FT_Normal);

		ele.skin.draw(SS_ListboxItem, flags, absPos, mp.color);
		auto@ icon = mapIcons[mp.index];
		if(mp.icon.length != 0 && icon.isLoaded(0)) {
			recti pos = absPos.aspectAligned(1.0, horizAlign=1.0, vertAlign=1.0);
			icon.draw(pos, Color(0xffffff80));
		}

		title.draw(pos=absPos.resized(0, 32).padded(12,4),
				text=mp.name, color=mp.color, stroke=colors::Black);
		normal.draw(pos=absPos.padded(12,36,12+absPos.height,0), offset=vec2i(),
				lineHeight=-1, text=mp.description, color=colors::White);
	}
};

class Quickstart : ConsoleCommand {
	void execute(const string& args) {
		new_game.start();
	}
};

NewGame@ new_game;
array<DynamicTexture> mapIcons;

void init() {
	@new_game = NewGame();
	new_game.visible = false;

	addConsoleCommand("quickstart", Quickstart());
}

array<Player@> connectedPlayers;
set_int connectedSet;
void tick(double time) {
	if(new_game.visible)
		new_game.tick(time);
	if(!game_running && mpServer) {
		array<Player@>@ players = getPlayers();

		//Send connect events
		for(uint i = 0, cnt = players.length; i < cnt; ++i) {
			Player@ pl = players[i];
			if(pl.id == CURRENT_PLAYER.id)
				continue;
			string name = pl.name;
			if(name.length == 0)
				continue;
			if(!connectedSet.contains(pl.id)) {
				string msg = format("[color=#aaa]* "+locale::MP_CONNECT_EVENT+"[/color]",
					format("[b]$1[/b]", bbescape(name)));
				recvMenuJoin(ALL_PLAYERS, msg);
				connectedPlayers.insertLast(pl);
				connectedSet.insert(pl.id);
			}
		}

		connectedSet.clear();
		for(uint i = 0, cnt = players.length; i < cnt; ++i)
			connectedSet.insert(players[i].id);

		//Send disconnect events
		for(uint i = 0, cnt = connectedPlayers.length; i < cnt; ++i) {
			if(!connectedSet.contains(connectedPlayers[i].id)) {
				Color color;
				string name = connectedPlayers[i].name;

				string msg = format("[color=#aaa]* "+locale::MP_DISCONNECT_EVENT+"[/color]", 
					format("[b]$2[/b]", toString(color), bbescape(name)));
				recvMenuLeave(ALL_PLAYERS, msg);
				connectedPlayers.removeAt(i);
				--i; --cnt;
			}
		}
	}
}

void showNewGame(bool fromMP = false) {
	new_game.visible = true;
	new_game.fromMP = fromMP;
	new_game.init();
	menu_container.visible = false;
	menu_container.animateOut();
	new_game.animateIn();
}

void hideNewGame(bool snap = false) {
	new_game.fromMP = false;
	menu_container.visible = true;
	if(!snap) {
		menu_container.animateIn();
		new_game.animateOut();
	}
	else {
		animate_remove(new_game);
		new_game.visible = false;
		menu_container.show();
	}
}

void changeEmpireSettings_client(Player& pl, EmpireSettings@ settings) {
	auto@ emp = new_game.findPlayer(pl.id);
	emp.settings.raceName = settings.raceName;
	emp.settings.traits = settings.traits;
	emp.settings.portrait = settings.portrait;
	emp.settings.shipset = settings.shipset;
	emp.settings.effectorSkin = settings.effectorSkin;
	emp.settings.color = settings.color;
	emp.settings.flag = settings.flag;
	emp.settings.ready = settings.ready;
	emp.settings.team = settings.team;
	emp.update();
}

bool sendPeriodic(Message& msg) {
	if(game_running)
		return false;
	new_game.apply();
	msg << new_game.settings;
	return true;
}

void recvPeriodic(Message& msg) {
	msg >> new_game.settings;
	new_game.reset();
	new_game.updateAbsolutePosition();
}

void chatMessage(Player& pl, string text) {
	auto@ emp = new_game.findPlayer(pl.id);
	Color color = emp.settings.color;
	string msg = format("[b][color=$1]$2[/color][/b] [offset=100]$3[/offset]",
		toString(color), bbescape(emp.name.text), bbescape(text));
	recvMenuChat(ALL_PLAYERS, msg);
}

void chatMessage_client(string text) {
	new_game.addChat(text);
	sound::generic_click.play();
}

void chatJoin_client(string text) {
	new_game.addChat(text);
	sound::generic_ok.play();
}

void chatLeave_client(string text) {
	new_game.addChat(text);
	sound::generic_warn.play();
}

Added scripts/server/cheats.as.














































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import orbitals;
import object_creation;
import tile_resources;
import void setInstantColonize(bool) from "planets.SurfaceComponent";
from empire import sendChatMessage;

import influence;
from bonus_effects import BonusEffect;
from generic_effects import GenericEffect;
import hooks;

bool CHEATS_ENABLED_THIS_GAME = false;
bool CHEATS_ENABLED = false;
bool getCheatsEnabled() {
	return CHEATS_ENABLED;
}

bool getCheatsEverOn() {
	return CHEATS_ENABLED_THIS_GAME;
}

void setCheatsEnabled(Player& player, bool enabled) {
	if(player != HOST_PLAYER)
		return;
	CHEATS_ENABLED = enabled;
	if(enabled)
		CHEATS_ENABLED_THIS_GAME = true;
	cheatsEnabled(ALL_PLAYERS, enabled);
	if(mpServer) {
		if(enabled)
			sendChatMessage(locale::MP_CHEATS_ENABLED, color=Color(0xaaaaaaff), offset=30);
		else
			sendChatMessage(locale::MP_CHEATS_DISABLED, color=Color(0xaaaaaaff), offset=30);
	}
}

void cheatSeeAll(Player& player, bool enabled) {
	if(!CHEATS_ENABLED || player.emp is null || !player.emp.valid)
		return;
	player.emp.visionMask = enabled ? ~0 : player.emp.mask;
}

void cheatColonize(bool enabled) {
	if(!CHEATS_ENABLED)
		return;
	setInstantColonize(enabled);
}

void cheatSpawnFlagship(Object@ spawnAt, const Design@ design, Empire@ owner) {
	if(!CHEATS_ENABLED)
		return;
	if(design.hasTag(ST_IsSupport))
		return;
	createShip(spawnAt, design, owner, free=true);
}

void cheatSpawnFlagship(vec3d spawnAt, const Design@ design, Empire@ owner) {
	if(!CHEATS_ENABLED)
		return;
	if(design.hasTag(ST_IsSupport))
		return;
	Ship@ ship = createShip(spawnAt, design, owner, free=true);
	ship.addMoveOrder(spawnAt);
}

void cheatSpawnSupports(Object@ spawnAt, const Design@ design, uint count) {
	if(!CHEATS_ENABLED)
		return;
	if(!design.hasTag(ST_IsSupport))
		return;
	if(!spawnAt.hasLeaderAI || spawnAt.owner is null || !spawnAt.owner.valid)
		return;
	for(uint i = 0; i < count; ++i)
		createShip(spawnAt, design, spawnAt.owner);
}

void cheatSpawnSupports(vec3d spawnAt, const Design@ design, uint count, Empire@ owner) {
	if(!CHEATS_ENABLED)
		return;
	if(!design.hasTag(ST_IsSupport))
		return;
	for(uint i = 0; i < count; ++i)
		createShip(spawnAt, design, owner);
}

void cheatSpawnOrbital(vec3d spawnAt, uint orbitalType, Empire@ owner) {
	if(!CHEATS_ENABLED)
		return;
	const OrbitalModule@ def = getOrbitalModule(orbitalType);
	if(def is null)
		return;
	createOrbital(spawnAt, def, owner);
}

void cheatInfluence(Player& player, int amount) {
	if(!CHEATS_ENABLED || player.emp is null || !player.emp.valid)
		return;
	player.emp.addInfluence(amount);
}

void cheatResearch(Player& player, double amount) {
	if(!CHEATS_ENABLED || player.emp is null || !player.emp.valid)
		return;
	player.emp.generatePoints(amount);
}

void cheatMoney(Player& player, int amount) {
	if(!CHEATS_ENABLED || player.emp is null || !player.emp.valid)
		return;
	player.emp.addBonusBudget(amount);
}

void cheatEnergy(Player& player, int amount) {
	if(!CHEATS_ENABLED || player.emp is null || !player.emp.valid)
		return;
	player.emp.modEnergyStored(amount);
}

void cheatFTL(Player& player, int amount) {
	if(!CHEATS_ENABLED || player.emp is null || !player.emp.valid)
		return;
	if(player.emp.FTLCapacity < amount)
		player.emp.modFTLCapacity(amount);
	player.emp.modFTLStored(amount);
}

void cheatActivateAI(Player& player) {
	if(!CHEATS_ENABLED || player.emp is null || !player.emp.valid)
		return;
	player.emp.initBasicAI();
}

void cheatDebugAI(Empire@ emp) {
	if(!CHEATS_ENABLED || emp is null)
		return;
	emp.debugAI();
}

void commandPlayerAI(string cmd) {
	playerEmpire.commandAI(cmd);
}

void cheatCommandAI(Empire@ emp, string cmd) {
	if(emp is playerEmpire) {
		if (cmd == "no achievements") {
			CHEATS_ENABLED_THIS_GAME = true;
		}
		else {
			emp.commandAI(cmd);
		}
		return;
	}
	if(!CHEATS_ENABLED || emp is null)
		return;
	emp.commandAI(cmd);
}

void cheatTrigger(Player& player, Object@ obj, Empire@ emp, string hook) {
	Empire@ plEmp = player.emp;
	if(!CHEATS_ENABLED || plEmp is null || !plEmp.valid)
		return;
	BonusEffect@ trig = cast<BonusEffect>(parseHook(hook, "bonus_effects::", required=false));
	if(trig !is null) {
		trig.activate(obj, emp);
		return;
	}
	GenericEffect@ eff = cast<GenericEffect>(parseHook(hook, "planet_effects::"));
	if(eff !is null) {
		eff.enable(obj, null);
		return;
	}
}

void cheatChangeOwner(Object@ obj, Empire@ newOwner) {
	if(!CHEATS_ENABLED || obj is null || newOwner is null)
		return;
	if(obj.isPlanet) {
		obj.takeoverPlanet(newOwner);
	}
	else if(obj.isShip) {
		if(obj.hasLeaderAI) {
			uint cnt = obj.supportCount;
			for(uint i = 0; i < cnt; ++i)
				@obj.supportShip[i].owner = newOwner;
		}

		@obj.owner = newOwner;
	}
	else {
		@obj.owner = newOwner;
	}
}

void cheatAlliance(Empire& from, Empire& to) {
	if(!CHEATS_ENABLED)
		return;
	if(from is to)
		return;
	if(!from.valid || !to.valid)
		return;
}

void cheatDestroy(Object@ obj) {
	if(!CHEATS_ENABLED || obj is null)
		return;
	obj.destroy();
}

void cheatLabor(Object@ obj, double amount) {
	if(!CHEATS_ENABLED || obj is null)
		return;
	obj.modLaborIncome(amount);
}

void syncInitial(Message& msg) {
	msg << CHEATS_ENABLED;
	msg << CHEATS_ENABLED_THIS_GAME;
}

void save(SaveFile& msg) {
	msg << CHEATS_ENABLED;
	msg << CHEATS_ENABLED_THIS_GAME;
}

void load(SaveFile& msg) {
	msg >> CHEATS_ENABLED;
	if(msg >= SV_0025)
		msg >> CHEATS_ENABLED_THIS_GAME;
	else
		CHEATS_ENABLED_THIS_GAME = CHEATS_ENABLED;
}

Added scripts/server/empire_ai/EmpireAI.as.




















































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import settings.game_settings;

import AIController@ createBumAI() from "empire_ai.BumAI";
import AIController@ createBasicAI() from "empire_ai.BasicAI";
import AIController@ createWeaselAI() from "empire_ai.weasel.WeaselAI";

interface AIController {
	void debugAI();
	void commandAI(string cmd);
	void aiPing(Empire@ fromEmpire, vec3d position, uint type);
	void init(Empire& emp, EmpireSettings& settings);
	void init(Empire& emp);
	void tick(Empire& emp, double time);
	void pause(Empire& emp);
	void resume(Empire& emp);
	void load(SaveFile& msg);
	void save(SaveFile& msg);
	int getDifficultyLevel();
	vec3d get_aiFocus();
	string getOpinionOf(Empire& emp, Empire@ other);
	int getStandingTo(Empire& emp, Empire@ other);
}

class EmpireAI : Component_EmpireAI, Savable {
	AIController@ ctrl;
	uint aiType;
	bool paused = false;
	bool override = true;

	EmpireAI() {
	}
	
	vec3d get_aiFocus() {
		if(ctrl !is null)
			return ctrl.aiFocus;
		else
			return vec3d();
	}
	
	int get_difficulty() {
		if(ctrl !is null)
			return ctrl.getDifficultyLevel();
		else
			return -1;
	}

	bool get_isAI(Empire& emp) {
		return ctrl !is null && (emp.player is null || override);
	}

	string getRelation(Player& pl, Empire& emp) {
		auto@ other = pl.emp;
		if(ctrl is null || other is null || !other.valid || other.ContactMask.value & emp.mask == 0)
			return "";
		else
			return ctrl.getOpinionOf(emp, other);
	}

	int getRelationState(Player& pl, Empire& emp) {
		auto@ other = pl.emp;
		if(ctrl is null || other is null || !other.valid || other.ContactMask.value & emp.mask == 0)
			return 0;
		else
			return ctrl.getStandingTo(emp, other);
	}

	uint getAIType() {
		return aiType;
	}
	
	void debugAI() {
		if(ctrl !is null)
			ctrl.debugAI();
	}
	
	void commandAI(string cmd) {
		if(ctrl !is null)
			ctrl.commandAI(cmd);
	}

	void load(SaveFile& msg) {
		msg >> paused;
		msg >> override;
		msg >> aiType;

		createAI(aiType);
		if(ctrl !is null)
			ctrl.load(msg);
	}

	void save(SaveFile& msg) {
		msg << paused;
		msg << override;
		msg << aiType;

		if(ctrl !is null)
			ctrl.save(msg);
	}

	void createAI(uint type) {
		aiType = type;

		//Create the controller
		switch(type) {
			case ET_Player:
				//Do nothing
			break;
			case ET_BumAI:
				@ctrl = createBasicAI();
			break;
			case ET_WeaselAI:
				@ctrl = createWeaselAI();
			break;
		}

	}

	void aiPing(Empire@ fromEmpire, vec3d position, uint type = 0) {
		if(ctrl !is null)
			ctrl.aiPing(fromEmpire, position, type);
	}

	void init(Empire& emp, EmpireSettings& settings) {
		createAI(settings.type);

		//Initialize
		if(ctrl !is null)
			ctrl.init(emp, settings);
	}

	void initBasicAI(Empire& emp) {
		override = true;
		if(ctrl !is null)
			return;

		createAI(ET_WeaselAI);

		if(ctrl !is null) {
			EmpireSettings settings;
			settings.difficulty = 2;
			settings.aiFlags |= AIF_Aggressive;
			ctrl.init(emp, settings);
			ctrl.init(emp);
		}
	}
	
	void init(Empire& emp) {
		if(ctrl !is null)
			ctrl.init(emp);
	}

	void aiTick(Empire& emp, double tick) {
		if(ctrl is null)
			return;

		if(emp.player is null || override) {
			if(paused) {
				ctrl.resume(emp);
				paused = false;
			}
			ctrl.tick(emp, tick);
		}
		else {
			if(!paused) {
				ctrl.pause(emp);
				paused = true;
			}
		}
	}
};

Added scripts/server/empire_ai/weasel/Budget.as.
















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// Budget
// ------
// Tasked with managing the empire's money and making sure we have enough to spend
// on various things, as well as dealing with prioritization and budget allocation.
//

import empire_ai.weasel.WeaselAI;

enum BudgetType {
	BT_Military,
	BT_Infrastructure,
	BT_Colonization,
	BT_Development,

	BT_COUNT
};

final class AllocateBudget {
	int id = -1;
	uint type;
	int cost = 0;
	int maintenance = 0;

	double requestTime = 0;
	double priority = 1;

	bool allocated = false;
	int opCmp(const AllocateBudget@ other) const {
		if(priority < other.priority)
			return -1;
		if(priority > other.priority)
			return 1;
		if(requestTime < other.requestTime)
			return 1;
		if(requestTime > other.requestTime)
			return -1;
		return 0;
	}

	void save(SaveFile& file) {
		file << id;
		file << type;
		file << cost;
		file << maintenance;
		file << requestTime;
		file << priority;
		file << allocated;
	}

	void load(SaveFile& file) {
		file >> id;
		file >> type;
		file >> cost;
		file >> maintenance;
		file >> requestTime;
		file >> priority;
		file >> allocated;
	}
};

final class BudgetPart {
	uint type;

	array<AllocateBudget@> allocations;

	//How much we've spent this cycle
	int spent = 0;

	//How much is remaining to be spent this cycle
	int remaining = 0;

	//How much maintenance we've gained this cycle
	int gainedMaintenance = 0;

	//How much maintenance we can still gain this cycle
	int remainingMaintenance = 0;

	void update(AI& ai, Budget& budget) {

		for(uint i = 0, cnt = allocations.length; i < cnt; ++i) {
			auto@ alloc = allocations[i];
			if(alloc.priority < 1.0) {
				if(alloc.cost >= remaining && alloc.maintenance >= remainingMaintenance) {
					budget.spend(type, alloc.cost, alloc.maintenance);
					alloc.allocated = true;
					allocations.removeAt(i);
					break;
				}
			}
			else {
				if(budget.canSpend(type, alloc.cost, alloc.maintenance, alloc.priority)) {
					budget.spend(type, alloc.cost, alloc.maintenance);
					alloc.allocated = true;
					allocations.removeAt(i);
					break;
				}
			}
		}
	}

	void turn(AI& ai, Budget& budget) {
		spent = 0;
		remaining = 0;

		gainedMaintenance = 0;
		remainingMaintenance = 0;
	}

	void save(Budget& budget, SaveFile& file) {
		file << spent;
		file << remaining;
		file << gainedMaintenance;
		file << remainingMaintenance;

		uint cnt = allocations.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			budget.saveAlloc(file, allocations[i]);
			allocations[i].save(file);
		}
	}

	void load(Budget& budget, SaveFile& file) {
		file >> spent;
		file >> remaining;
		file >> gainedMaintenance;
		file >> remainingMaintenance;

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ alloc = budget.loadAlloc(file);
			alloc.load(file);
		}
	}
};

final class Budget : AIComponent {
	//Budget thresholds
	private int _criticalThreshold = 350;
	private int _lowThreshold = 400;
	private int _mediumThreshold = 500;
	private int _highThreshold = 1000;
	private int _veryHighThreshold = 2000;

	//Focus flags
	private bool _askedFocus = false;
	private bool _focusing = false;
	//Focused budget type
	private uint _focus;

	int get_criticalThreshold() const { return _criticalThreshold; }
	int get_lowThreshold() const { return _lowThreshold; }
	int get_mediumThreshold() const { return _mediumThreshold; }
	int get_highThreshold() const { return _highThreshold; }
	int get_veryHighThreshold() const { return _veryHighThreshold; }

	array<BudgetPart@> parts;
	int NextAllocId = 0;

	int InitialBudget = 0;
	int InitialUpcoming = 0;

	double Progress = 0;
	double RemainingTime = 0;

	int FreeBudget = 0;
	int FreeMaintenance = 0;

	bool checkedMilitarySpending = false;

	void create() {
		parts.length = BT_COUNT;
		for(uint i = 0; i < BT_COUNT; ++i) {
			@parts[i] = BudgetPart();
			parts[i].type = BudgetType(i);
		}
	}

	void save(SaveFile& file) {
		file << InitialBudget;
		file << InitialUpcoming;
		file << Progress;
		file << RemainingTime;
		file << FreeBudget;
		file << FreeMaintenance;
		file << NextAllocId;
		file << checkedMilitarySpending;
		file << _askedFocus;
		file << _focusing;
		file << _focus;

		for(uint i = 0, cnt = parts.length; i < cnt; ++i)
			parts[i].save(this, file);
	}

	void load(SaveFile& file) {
		file >> InitialBudget;
		file >> InitialUpcoming;
		file >> Progress;
		file >> RemainingTime;
		file >> FreeBudget;
		file >> FreeMaintenance;
		file >> NextAllocId;
		file >> checkedMilitarySpending;
		file >> _askedFocus;
		file >> _focusing;
		file >> _focus;

		for(uint i = 0, cnt = parts.length; i < cnt; ++i)
			parts[i].load(this, file);
	}

	array<AllocateBudget@> loadIds;
	AllocateBudget@ loadAlloc(int id) {
		if(id == -1)
			return null;
		for(uint i = 0, cnt = loadIds.length; i < cnt; ++i) {
			if(loadIds[i].id == id)
				return loadIds[i];
		}
		AllocateBudget alloc;
		alloc.id = id;
		loadIds.insertLast(alloc);
		return alloc;
	}
	AllocateBudget@ loadAlloc(SaveFile& file) {
		int id = -1;
		file >> id;
		if(id == -1)
			return null;
		else
			return loadAlloc(id);
	}
	void saveAlloc(SaveFile& file, AllocateBudget@ alloc) {
		int id = -1;
		if(alloc !is null)
			id = alloc.id;
		file << id;
	}
	void postLoad(AI& ai) {
		loadIds.length = 0;
	}

	void spend(uint type, int money, int maint = 0) {
		auto@ part = parts[type];

		part.spent += money;
		part.gainedMaintenance += maint;

		if(part.remaining >= money) {
			part.remaining -= money;
		}
		else if(part.remaining >= 0) {
			money -= part.remaining;
			FreeBudget -= money;
			part.remaining = 0;
		}
		else {
			FreeBudget -= money;
		}

		if(part.remainingMaintenance >= maint) {
			part.remainingMaintenance -= maint;
		}
		else if(part.remainingMaintenance >= 0) {
			maint -= part.remainingMaintenance;
			FreeMaintenance -= maint;
			part.remainingMaintenance = 0;
		}
		else {
			FreeMaintenance -= money;
		}
	}

	bool canSpend(uint type, int money, int maint = 0, double priority = 1.0) {
		int canFree = FreeBudget;
		int canFreeMaint = FreeMaintenance;

		if (priority < 2.0) {
			//Rules for normal priority requests
			//Don't allow any spending not in our current focus
			if (_focusing) {
				if (type != _focus)
					return false;
			}
			if (type != BT_Colonization
				&& (maint > 200 && ai.empire.EstNextBudget < mediumThreshold)
				|| (maint > 100 && ai.empire.EstNextBudget < lowThreshold)
				|| (maint > 0 && ai.empire.EstNextBudget < criticalThreshold))
				//Don't allow any high maintenance cost if our estimated next budget is too low
				return false;

			//Don't allow generic spending until we've checked if we need to spend on military this cycle
			if(type == BT_Development && !checkedMilitarySpending && Progress < 0.33)
				canFree = 0;
			if(type == BT_Colonization)
				canFree += 160;
		}
		else {
			//Rules for high priority requests
			if (money > FreeBudget && ai.empire.canBorrow(money - FreeBudget))
			//Allow borrowing from next budget for high priority requests
			canFree = money;
		}

		auto@ part = parts[type];
		if(money > part.remaining + canFree)
			return false;
		if(maint != 0 && maint > part.remainingMaintenance + canFreeMaint)
			return false;
		return true;
	}

	int spendable(uint type) {
		return FreeBudget + parts[type].remaining;
	}

	int maintainable(uint type) {
		return FreeMaintenance + parts[type].remainingMaintenance;
	}

	void claim(uint type, int money, int maint = 0) {
		auto@ part = parts[type];

		FreeBudget -= money;
		part.remaining += money;

		FreeMaintenance -= maint;
		part.remainingMaintenance += maint;
	}

	void turn() {
		if(log && gameTime > 10.0) {
			ai.print("==============");
			ai.print("Unspent:");
			ai.print(" Military: "+parts[BT_Military].remaining+" / "+parts[BT_Military].remainingMaintenance);
			ai.print(" Infrastructure: "+parts[BT_Infrastructure].remaining+" / "+parts[BT_Infrastructure].remainingMaintenance);
			ai.print(" Colonization: "+parts[BT_Colonization].remaining+" / "+parts[BT_Colonization].remainingMaintenance);
			ai.print(" Development: "+parts[BT_Development].remaining+" / "+parts[BT_Development].remainingMaintenance);
			ai.print(" FREE: "+FreeBudget+" / "+FreeMaintenance);
			ai.print("==============");
			ai.print("Total Expenditures:");
			ai.print(" Military: "+parts[BT_Military].spent+" / "+parts[BT_Military].gainedMaintenance);
			ai.print(" Infrastructure: "+parts[BT_Infrastructure].spent+" / "+parts[BT_Infrastructure].gainedMaintenance);
			ai.print(" Colonization: "+parts[BT_Colonization].spent+" / "+parts[BT_Colonization].gainedMaintenance);
			ai.print(" Development: "+parts[BT_Development].spent+" / "+parts[BT_Development].gainedMaintenance);
			ai.print("==============");
		 }

		//Collect some data about this turn
		InitialBudget = ai.empire.RemainingBudget;
		InitialUpcoming = ai.empire.EstNextBudget;

		FreeBudget = InitialBudget;
		FreeMaintenance = InitialUpcoming;

		checkedMilitarySpending = false;

		//Handle focus status
		if (_focusing) {
			_focusing = false;
			if (log)
				ai.print("Budget: ending focus");
		}
		else if (_askedFocus) {
			_focusing = true;
			_askedFocus = false;
			if (log)
				ai.print("Budget: starting focus");
		}

		//Tell the budget parts to perform turns
		for(uint i = 0, cnt = parts.length; i < cnt; ++i)
			parts[i].turn(ai, this);
	}

	void remove(AllocateBudget@ alloc) {
		if(alloc is null)
			return;
		if(alloc.allocated) {
			FreeBudget += alloc.cost;
			FreeMaintenance += alloc.maintenance;
		}
		parts[alloc.type].allocations.remove(alloc);
	}

	AllocateBudget@ allocate(uint type, int cost, int maint = 0, double priority = 1.0) {
		AllocateBudget alloc;
		alloc.id = NextAllocId++;
		alloc.type = type;
		alloc.cost = cost;
		alloc.maintenance = maint;
		alloc.priority = priority;

		return allocate(alloc);
	}

	AllocateBudget@ allocate(AllocateBudget@ allocation) {
		allocation.requestTime = gameTime;
		parts[allocation.type].allocations.insertLast(allocation);
		parts[allocation.type].allocations.sortDesc();
		return allocation;
	}

	void applyNow(AllocateBudget@ alloc) {
		auto@ part = parts[alloc.type];
		spend(alloc.type, alloc.cost, alloc.maintenance);
		alloc.allocated = true;
		part.allocations.remove(alloc);
	}

	void grantBonus(int cost, int maint = 0) {
		//Spread some bonus budget across all the different parts
		FreeBudget += cost;
		FreeMaintenance += maint;
	}

	bool canFocus() {
		return !(ai.empire.EstNextBudget <= criticalThreshold || _askedFocus || _focusing);
	}

	//Focus spendings on one particular budget part for one turn
	//Only high priority requests will be considered for other parts
	//Should be called at the start of a turn for best results
	void focus(BudgetType type) {
		if (ai.empire.EstNextBudget > criticalThreshold && !_askedFocus && !_focusing) {
			_focus = type;
			//If we are still at the start of a turn, focus immediately, else wait until next turn
			//The second condition compensates for slight timing inaccuracies and execution delay
			if (Progress < 0.33 || Progress > 0.995) {
				_focusing = true;
				_askedFocus = false;
				if (log)
					ai.print("Budget: starting focus");
			}
			else
				_askedFocus = true;
		}
	}

	void tick(double time) {
		//Record some simple data
		Progress = ai.empire.BudgetTimer / ai.empire.BudgetCycle;
		RemainingTime = ai.empire.BudgetCycle - ai.empire.BudgetTimer;

		//Update one of the budget parts
		for(uint i = 0, cnt = parts.length; i < cnt; ++i) {
			auto@ part = parts[i];
			part.update(ai, this);
		}
	}

	void focusTick(double time) {
		//Detect any extra budget we need to use
		int ExpectBudget = FreeBudget;
		int ExpectMaint = FreeMaintenance;
		for(uint i = 0, cnt = parts.length; i < cnt; ++i) {
			ExpectBudget += parts[i].remaining;
			ExpectMaint += parts[i].remainingMaintenance;
		}

		int HaveBudget = ai.empire.RemainingBudget;
		int HaveMaint = ai.empire.EstNextBudget;
		if(ExpectBudget != HaveBudget || ExpectMaint != HaveMaint)
			grantBonus(HaveBudget - ExpectBudget, max(0, HaveMaint - ExpectMaint));
	}
};

AIComponent@ createBudget() {
	return Budget();
}

Added scripts/server/empire_ai/weasel/Colonization.as.


















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// Colonization
// ------------
// Deals with colonization for requested resources.
//

import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.ImportData;
import empire_ai.weasel.Resources;
import empire_ai.weasel.Planets;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Budget;
import empire_ai.weasel.Creeping;

import util.formatting;

import systems;

enum ColonizationPhase {
	CP_Expansion,
	CP_Stabilization,
};

interface RaceColonization {
	bool orderColonization(ColonizeData& data, Planet@ sourcePlanet);
	double getGenericUsefulness(const ResourceType@ type);
};

final class ColonizeData {
	int id = -1;
	Planet@ target;
	Planet@ colonizeFrom;
	bool completed = false;
	bool canceled = false;
	double checkTime = -1.0;

	void save(Colonization& colonization, SaveFile& file) {
		file << target;
		file << colonizeFrom;
		file << completed;
		file << canceled;
		file << checkTime;
	}

	void load(Colonization& colonization, SaveFile& file) {
		file >> target;
		file >> colonizeFrom;
		file >> completed;
		file >> canceled;
		file >> checkTime;
	}
};

tidy final class WaitUsed {
	ImportData@ forData;
	ExportData@ resource;

	void save(Colonization& colonization, SaveFile& file) {
		colonization.resources.saveImport(file, forData);
		colonization.resources.saveExport(file, resource);
	}

	void load(Colonization& colonization, SaveFile& file) {
		@forData = colonization.resources.loadImport(file);
		@resource = colonization.resources.loadExport(file);
	}
};

final class ColonizePenalty : Savable {
	Planet@ pl;
	double until;

	void save(SaveFile& file) {
		file << pl;
		file << until;
	}

	void load(SaveFile& file) {
		file >> pl;
		file >> until;
	}
};

final class PotentialColonize {
	Planet@ pl;
	const ResourceType@ resource;
	double weight = 0;
};

final class ColonizeLog {
	int typeId;
	double time;
};

tidy final class ColonizeQueue {
	ResourceSpec@ spec;
	Planet@ target;
	ColonizeData@ step;
	ImportData@ forData;
	ColonizeQueue@ parent;
	array<ColonizeQueue@> children;

	void save(Colonization& colonization, SaveFile& file) {
		file << spec;
		file << target;

		colonization.saveColonize(file, step);
		colonization.resources.saveImport(file, forData);

		uint cnt = children.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			children[i].save(colonization, file);
	}

	void load(Colonization& colonization, SaveFile& file) {
		@spec = ResourceSpec();
		file >> spec;
		file >> target;

		@step = colonization.loadColonize(file);
		@forData = colonization.resources.loadImport(file);

		uint cnt = 0;
		file >> cnt;
		children.length = cnt;
		for(uint i = 0; i < cnt; ++i) {
			@children[i] = ColonizeQueue();
			@children[i].parent = this;
			children[i].load(colonization, file);
		}
	}
};

final class Colonization : AIComponent {
	const ResourceClass@ foodClass, waterClass, scalableClass;

	Resources@ resources;
	Planets@ planets;
	Systems@ systems;
	Budget@ budget;
	Creeping@ creeping;
	RaceColonization@ race;

	array<ColonizeQueue@> queue;
	array<ColonizeData@> colonizing;
	array<ColonizeData@> awaitingSource;
	array<WaitUsed@> waiting;
	array<ColonizePenalty@> penalties;
	set_int penaltySet;
	int nextColonizeId = 0;
	array<ColonizeLog@> colonizeLog;

	array<PotentialSource@> sources;
	double sourceUpdate = 0;

	//Maximum colonizations that can still be done this turn
	uint remainColonizations = 0;
	//Amount of colonizations that have happened so far this budget cycle
	uint curColonizations = 0;
	//Amount of colonizations that happened the previous budget cycle
	uint prevColonizations = 0;

	//Whether to automatically find sources and order colonizations
	bool performColonization = true;
	bool queueColonization = true;

	//Colonization focus
	private uint _phase = CP_Expansion;

	//Territory request data
	private bool _needsMoreTerritory = false;
	private bool _needsNewTerritory = false;
	private uint _territoryRequests = 0;
	private Region@ _newTerritoryTarget;

	Object@ colonizeWeightObj;

	bool get_needsMoreTerritory() const { return _needsMoreTerritory; }
	bool get_needsNewTerritory() const { return _needsNewTerritory; }

	void create() {
		@resources = cast<Resources>(ai.resources);
		@planets = cast<Planets>(ai.planets);
		@systems = cast<Systems>(ai.systems);
		@budget = cast<Budget>(ai.budget);
		@creeping = cast<Creeping>(ai.creeping);
		@race = cast<RaceColonization>(ai.race);

		//Get some heuristic resource classes
		@foodClass = getResourceClass("Food");
		@waterClass = getResourceClass("WaterType");
		@scalableClass = getResourceClass("Scalable");

	}

	void save(SaveFile& file) {
		file << nextColonizeId;
		file << remainColonizations;
		file << curColonizations;
		file << prevColonizations;
		file << _phase;
		file << _needsMoreTerritory;
		file << _needsNewTerritory;
		file << _territoryRequests;
		if (_newTerritoryTarget !is null) {
			file.write1();
			file << _newTerritoryTarget;
		}
		else
			file.write0();

		uint cnt = colonizing.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			saveColonize(file, colonizing[i]);
			colonizing[i].save(this, file);
		}

		cnt = waiting.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			waiting[i].save(this, file);

		cnt = penalties.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			penalties[i].save(file);

		cnt = colonizeLog.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			file.writeIdentifier(SI_Resource, colonizeLog[i].typeId);
			file << colonizeLog[i].time;
		}

		cnt = queue.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			queue[i].save(this, file);
	}

	void load(SaveFile& file) {
		file >> nextColonizeId;
		file >> remainColonizations;
		file >> curColonizations;
		file >> prevColonizations;
		file >> _phase;
		file >> _needsMoreTerritory;
		file >> _needsNewTerritory;
		file >> _territoryRequests;
		if(file.readBit()) {
			file >> _newTerritoryTarget;
		}

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = loadColonize(file);
			if(data !is null) {
				data.load(this, file);
				if(data.target !is null) {
					colonizing.insertLast(data);
					if(data.colonizeFrom is null)
						awaitingSource.insertLast(data);
				}
				else {
					data.canceled = true;
				}
			}
			else {
				ColonizeData().load(this, file);
			}
		}

		file >> cnt;
		waiting.length = cnt;
		for(uint i = 0; i < cnt; ++i) {
			@waiting[i] = WaitUsed();
			waiting[i].load(this, file);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			ColonizePenalty pen;
			pen.load(file);
			if(pen.pl !is null) {
				penaltySet.insert(pen.pl.id);
				penalties.insertLast(pen);
			}
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			ColonizeLog logEntry;
			logEntry.typeId = file.readIdentifier(SI_Resource);
			file >> logEntry.time;
			colonizeLog.insertLast(logEntry);
		}

		file >> cnt;
		queue.length = cnt;
		for(uint i = 0; i < cnt; ++i) {
			@queue[i] = ColonizeQueue();
			queue[i].load(this, file);
		}
	}

	array<ColonizeData@> loadIds;
	ColonizeData@ loadColonize(int id) {
		if(id == -1)
			return null;
		for(uint i = 0, cnt = loadIds.length; i < cnt; ++i) {
			if(loadIds[i].id == id)
				return loadIds[i];
		}
		ColonizeData data;
		data.id = id;
		loadIds.insertLast(data);
		return data;
	}
	ColonizeData@ loadColonize(SaveFile& file) {
		int id = -1;
		file >> id;
		if(id == -1)
			return null;
		else
			return loadColonize(id);
	}
	void saveColonize(SaveFile& file, ColonizeData@ data) {
		int id = -1;
		if(data !is null)
			id = data.id;
		file << id;
	}
	void postLoad(AI& ai) {
		loadIds.length = 0;
	}

	bool canBeColonized(Planet& target) {
		if(!target.valid)
			return false;
		if(target.owner.valid)
			return false;
		return true;
	}

	bool canColonize(Planet& source) {
		if(source.level == 0)
			return false;
		if(source.owner !is ai.empire)
			return false;
		return true;
	}

	bool shouldForceExpansion() {
		uint otherColonizedSystems = 0;
		for (uint i = 0, cnt = systems.outsideBorder.length; i < cnt; ++i) {
			auto@ sys = systems.outsideBorder[i];
			//Check if any system in our tradable area is unexplored
			if (!sys.explored)
				return false;
			if (sys.planets.length > 0) {
				uint otherColonizedPlanets = 0;
				for (uint j = 0, cnt = sys.planets.length; j < cnt; ++j) {
					auto@ pl = sys.planets[j];
					int resId = pl.primaryResourceType;
					if (resId != -1) {
						//Check if any planet can still be colonized in our tradable area
						if (!pl.owner.valid && !pl.quarantined)
							return false;
						else
							++otherColonizedPlanets;
					}
				}
				//Check if all planets in the system are colonized
				if (otherColonizedPlanets == sys.planets.length)
					++otherColonizedSystems;
			}
		}
		//Check if all systems in our tradable area belong to other empires
		if (otherColonizedSystems == systems.outsideBorder.length)
			//If 0, we colonized everything!
			return false;

		return true;
	}

	double getSourceWeight(PotentialSource& source, ColonizeData& data) {
		double w = source.weight;
		w /= data.target.position.distanceTo(source.pl.position);
		return w;
	}

	void updateSources() {
		planets.getColonizeSources(sources);
	}

	void focusTick(double time) {
		if(sourceUpdate < gameTime && performColonization) {
			updateSources();
			if(sources.length == 0 && gameTime < 60.0)
				sourceUpdate = gameTime + 1.0;
			else
				sourceUpdate = gameTime + 10.0;
		}

		if (ai.behavior.forbidColonization) return;

		//Find some new colonizations we can queue up from resources
		fillQueueFromRequests();

		//If we've gained any requests, see if we can order another colonize
		if(remainColonizations > 0
				&& (budget.Progress < ai.behavior.colonizeMaxBudgetProgress || gameTime < 3.0 * 60.0)
				&& (sources.length > 0 || !performColonization) && canColonize()
				&& queueColonization) {
			//Actually go order some colonizations from the queue
			if(orderFromQueue()) {
				doColonize();
			}
			else if(awaitingSource.length == 0) {
				if(genericExpand() !is null)
					doColonize();
			}
		}

		//Find colonization sources for everything that needs them
		if(awaitingSource.length != 0 && performColonization) {
			for(uint i = 0, cnt = awaitingSource.length; i < cnt; ++i) {
				auto@ target = awaitingSource[i];

				PotentialSource@ src;
				double bestSource = 0;

				for(uint j = 0, jcnt = sources.length; j < jcnt; ++j) {
					double w = getSourceWeight(sources[j], target);
					if(w > bestSource) {
						bestSource = w;
						@src = sources[j];
					}
				}

				if(src !is null) {
					orderColonization(target, src.pl);
					sources.remove(src);
					--i; --cnt;
				}
			}
		}

		//Check if any resources we're waiting for are being used
		for(uint i = 0, cnt = waiting.length; i < cnt; ++i) {
			auto@ wait = waiting[i];
			if(wait.resource.obj is null || !wait.resource.obj.valid || wait.resource.obj.owner !is ai.empire || wait.resource.request !is null) {
				wait.forData.isColonizing = false;
				waiting.removeAt(i);
				--i; --cnt;
			}
		}

		//Prune old colonization penalties
		for(uint i = 0, cnt = penalties.length; i < cnt; ++i) {
			auto@ pen = penalties[i];
			if(pen.pl !is null && pen.pl.owner is ai.empire)
				pen.pl.forceAbandon();
			if(pen.until < gameTime) {
				if(pen.pl !is null)
					penaltySet.erase(pen.pl.id);
				penalties.removeAt(i);
				--i; --cnt;
			}
		}
	}

	void orderColonization(ColonizeData& data, Planet& sourcePlanet) {
		if(log)
			ai.print("start colonizing "+data.target.name, sourcePlanet);

		if(race !is null) {
			if(race.orderColonization(data, sourcePlanet))
				return;
		}

		@data.colonizeFrom = sourcePlanet;
		awaitingSource.remove(data);

		sourcePlanet.colonize(data.target);
	}

	void tick(double time) {
		//Check if we've finished colonizing anything
		for(uint i = 0, cnt = colonizing.length; i < cnt; ++i) {
			auto@ c = colonizing[i];

			//Remove if we can no longer colonize it
			Empire@ visOwner = c.target.visibleOwnerToEmp(ai.empire);
			if(visOwner !is ai.empire && (visOwner is null || visOwner.valid)) {
				//Fail out this colonization
				cancelColonization(c);
				--i; --cnt;
				continue;
			}

			//Check for succesful colonization
			if(visOwner is ai.empire) {
				double population = c.target.population;
				if(population >= 1.0) {
					finishColonization(c);
					colonizing.removeAt(i);
					--i; --cnt;
					continue;
				}
				else {
					if(c.checkTime == -1.0) {
						c.checkTime = gameTime;
					}
					else {
						double grace = ai.behavior.colonizeFailGraceTime;
						if(population > 0.9)
							grace *= 2.0;
						if(c.checkTime + grace < gameTime) {
							//Fail out this colonization and penalize the target
							creeping.requestClear(systems.getAI(c.target.region));
							cancelColonization(c, penalize=ai.behavior.colonizePenalizeTime);
							--i; --cnt;
							continue;
						}
					}
				}
			}

			//This colonization is still waiting for a good source
			if(c.colonizeFrom is null)
				continue;

			//Check for failed colonization
			if(!canColonize(c.colonizeFrom) || !performColonization) {
				if(c.target.owner is ai.empire && performColonization)
					c.target.stopColonizing(c.target);

				@c.colonizeFrom = null;
				awaitingSource.insertAt(0, c);
			}
		}

		//Update the colonization queue
		updateQueue();
	}

	void cancelColonization(ColonizeData@ data, double penalize = 0) {
		if(data.colonizeFrom !is null && data.colonizeFrom.owner is ai.empire)
			data.colonizeFrom.stopColonizing(data.target);
		if(data.colonizeFrom is null)
			awaitingSource.remove(data);
		if(data.target.owner is ai.empire)
			data.target.forceAbandon();
		data.canceled = true;
		sourceUpdate = 0;
		colonizing.remove(data);

		if(penalize != 0) {
			ColonizePenalty pen;
			@pen.pl = data.target;
			pen.until = gameTime + penalize;

			penaltySet.insert(pen.pl.id);
			penalties.insertLast(pen);
		}
	}

	void finishColonization(ColonizeData@ data) {
		if(data.colonizeFrom is null)
			awaitingSource.remove(data);

		//If we just colonized a new territory, reset request data
		if (data.target.region is _newTerritoryTarget) {
			_needsNewTerritory = false;
			@_newTerritoryTarget = null;
		}

		PlanetAI@ plAI = planets.register(data.target);

		ColonizeLog logEntry;
		logEntry.typeId = data.target.primaryResourceType;
		logEntry.time = gameTime;
		colonizeLog.insertLast(logEntry);

		data.completed = true;
		sourceUpdate = 0;
	}

	double timeSinceMatchingColonize(ResourceSpec& spec) {
		for(int i = colonizeLog.length - 1; i >= 0; --i) {
			auto@ res = getResource(colonizeLog[i].typeId);
			if(res !is null && spec.meets(res))
				return gameTime - colonizeLog[i].time;
		}
		return gameTime;
	}

	bool isColonizing(Planet& pl) {
		for(uint i = 0, cnt = colonizing.length; i < cnt; ++i) {
			if(colonizing[i].target is pl)
				return true;
		}
		for(uint i = 0, cnt = queue.length; i < cnt; ++i) {
			if(isColonizing(pl, queue[i]))
				return true;
		}
		return false;
	}

	bool isColonizing(Planet& pl, ColonizeQueue@ q) {
		if(q.target is pl)
			return true;
		for(uint i = 0, cnt = q.children.length; i < cnt; ++i) {
			if(isColonizing(pl, q.children[i]))
				return true;
		}
		return false;
	}

	double getGenericUsefulness(const ResourceType@ type) {
		//Return a relative value for colonizing the resource this planet has in a vacuum,
		//rather than as an explicit requirement for a planet.
		double weight = 1.0;
		if(type.level == 0) {
			weight *= 2.0;
		}
		else {
			weight /= sqr(double(1 + type.level));
			weight *= 0.001;
		}
		if(type.cls is foodClass || type.cls is waterClass)
			weight *= 10.0;
		if(type.cls is scalableClass)
			weight *= 0.0001;
		if(type.totalPressure > 0)
			weight *= double(type.totalPressure);
		if(race !is null)
			weight *= race.getGenericUsefulness(type);
		return weight;
	}

	ColonizeData@ colonize(Planet& pl) {
		if(log)
			ai.print("queue colonization", pl);

		ColonizeData data;
		data.id = nextColonizeId++;
		@data.target = pl;

		budget.spend(BT_Colonization, 0, ai.behavior.colonizeBudgetCost);

		colonizing.insertLast(data);
		awaitingSource.insertLast(data);
		return data;
	}

	ColonizeData@ colonize(ResourceSpec@ spec) {
		Planet@ newColony;
		double w;
		double bestWeight = 0.0;

		for(uint i = 0, cnt = potentials.length; i < cnt; ++i) {
			auto@ p = potentials[i];

			Region@ reg = p.pl.region;
			if(reg is null)
				continue;
			if(!spec.meets(p.resource))
				continue;
			if(isColonizing(p.pl))
				continue;
			//Skip planets out of our new territory target if we are colonizing a new one
			if (_newTerritoryTarget !is null && p.pl.region !is _newTerritoryTarget)
				continue;

			auto@ sys = systems.getAI(reg);
			w = 1.0;
			if (sys.border)
				w *= 0.25;
			if (!sys.owned && !sys.border)
				w /= 0.25;
			if (sys.obj.PlanetsMask & ~ai.mask != 0)
				w *= 0.25;
			if (w > bestWeight) {
				@newColony = p.pl;
				bestWeight = w;
			}
		}

		if(newColony !is null)
			return colonize(newColony);
		else
			return null;
	}

	array<PotentialColonize@> potentials;
	void checkSystem(SystemAI@ sys) {
		uint presentMask = sys.seenPresent;
		if(presentMask & ai.mask == 0) {
			if(!ai.behavior.colonizeEnemySystems && (presentMask & ai.enemyMask) != 0)
				return;
			if(!ai.behavior.colonizeNeutralOwnedSystems && (presentMask & ai.neutralMask) != 0)
				return;
			if(!ai.behavior.colonizeAllySystems && (presentMask & ai.allyMask) != 0)
				return;
		}

		double sysWeight = 1.0;
		if(presentMask & ai.mask == 0)
			sysWeight *= ai.behavior.weightOutwardExpand;

		uint plCnt = sys.planets.length;
		for(uint n = 0; n < plCnt; ++n) {
			Planet@ pl = sys.planets[n];
			Empire@ visOwner = pl.visibleOwnerToEmp(ai.empire);
			if(!pl.valid || visOwner.valid)
				continue;
			if(isColonizing(pl))
				continue;
			if(penaltySet.contains(pl.id))
				continue;
			if(pl.quarantined)
				continue;

			int resId = pl.primaryResourceType;
			if(resId == -1)
				continue;

			PotentialColonize p;
			@p.pl = pl;
			@p.resource = getResource(resId);
			p.weight = 1.0 * sysWeight;
			//TODO: this should be weighted according to the position of the planet,
			//we should try to colonize things in favorable positions
			potentials.insertLast(p);
		}
	}

	double nextPotentialCheck = 0.0;
	array<PotentialColonize@>@ getPotentialColonize() {
		if(gameTime < nextPotentialCheck)
			return potentials;

		potentials.length = 0;
		for(uint i = 0, cnt = systems.owned.length; i < cnt; ++i)
			checkSystem(systems.owned[i]);
		for(uint i = 0, cnt = systems.outsideBorder.length; i < cnt; ++i)
			checkSystem(systems.outsideBorder[i]);

		if(needsNewTerritory) {
			for(uint i = 0, cnt = systems.all.length; i < cnt; ++i) {
				if(systems.all[i].explored)
					checkSystem(systems.all[i]);
			}
		}

		if(systems.owned.length == 0) {
			Region@ homeSys = ai.empire.HomeSystem;
			if(homeSys !is null) {
				auto@ homeAI = systems.getAI(homeSys);
				if(homeAI !is null)
					checkSystem(homeAI);
			}
			else {
				for(uint i = 0, cnt = systems.all.length; i < cnt; ++i) {
					if(systems.all[i].visible)
						checkSystem(systems.all[i]);
				}
			}
		}

		if(potentials.length == 0 && gameTime < 60.0)
			nextPotentialCheck = gameTime + 1.0;
		else
			nextPotentialCheck = gameTime + randomd(10.0, 40.0);

		//TODO: This should be able to colonize across empires we have trade agreements with?
		return potentials;
	}

	bool canColonize() {
		if(remainColonizations == 0)
			return false;
		if(curColonizations >= ai.behavior.guaranteeColonizations) {
			if(!budget.canSpend(BT_Colonization, 0, ai.behavior.colonizeBudgetCost))
				return false;
		}
		if(ai.behavior.maxConcurrentColonizations <= colonizing.length)
			return false;
		return true;
	}

	void doColonize() {
		remainColonizations -= 1;
		curColonizations += 1;
		budget.spend(BT_Colonization, 0, ai.behavior.colonizeBudgetCost);
	}

	ColonizeData@ genericExpand() {
		auto@ potentials = getPotentialColonize();

		//Do generic expansion using any remaining colonization steps we have
		if(ai.behavior.colonizeGenericExpand) {
			PotentialColonize@ expand;
			double w;
			double bestWeight = 0.0;

			for(uint i = 0, cnt = potentials.length; i < cnt; ++i) {
				auto@ p = potentials[i];
				w = p.weight * getGenericUsefulness(p.resource);
				modPotentialWeight(p, w);

				Region@ reg = p.pl.region;
				if(reg is null)
					continue;
				if(reg.PlanetsMask & ai.mask != 0)
					continue;
				//Skip planets out of our new territory target if we are colonizing a new one
				if (_newTerritoryTarget !is null && p.pl.region !is _newTerritoryTarget)
					continue;
				if(w == 0)
					continue;
				if (w > bestWeight) {
					@expand = p;
					bestWeight = w;
				}
			}

			if(expand !is null) {
				auto@ data = colonize(expand.pl);
				potentials.remove(expand);
				if (needsNewTerritory && _newTerritoryTarget is null) {
					//Check if our target planet is outside our tradable area
					bool found = false;
					for (uint i = 0, cnt = systems.owned.length; i < cnt; ++i) {
						if (systems.owned[i].obj is expand.pl.region)
							found = true;
					}
					for (uint i = 0, cnt = systems.outsideBorder.length; i < cnt; ++i) {
						if (systems.outsideBorder[i].obj is expand.pl.region)
							found = true;
					}
					if (!found)
						@_newTerritoryTarget = expand.pl.region;
				}
				return data;
			}
		}
		return null;
	}

	void turn() {
		//Figure out how much we can colonize
		remainColonizations = ai.behavior.maxColonizations;

		//Decide colonization phase
		if (_phase == CP_Expansion) {
			if (ai.empire.EstNextBudget < budget.criticalThreshold) {
				remainColonizations = 0;
				_phase = CP_Stabilization;
				if (log)
					ai.print("Colonization: entering stabilization phase with estimated next budget: " + ai.empire.EstNextBudget);
			}
			else if (ai.empire.EstNextBudget < budget.lowThreshold) {
				remainColonizations = 1;
				if (log)
					ai.print("Colonization: continuing expansion phase with estimated next budget: " + ai.empire.EstNextBudget);
			}
			else if (ai.empire.EstNextBudget < budget.mediumThreshold) {
				remainColonizations = min(2, ai.behavior.maxColonizations);
				if (log)
					ai.print("Colonization: continuing expansion phase with estimated next budget: " + ai.empire.EstNextBudget);
			}
		}
		else if (_phase == CP_Stabilization) {
			if (ai.empire.RemainingBudget > budget.mediumThreshold) {
				_phase = CP_Expansion;
				if (log)
					ai.print("Colonization: entering expansion phase with budget: " + ai.empire.RemainingBudget);
			}
			else {
				remainColonizations = 1;
				if (log)
					ai.print("Colonization: continuing stabilization phase with budget: " + ai.empire.RemainingBudget);
			}
		}

		if (ai.empire.EstNextBudget <= 0 && !ai.behavior.forbidScuttle) {
			//We are in trouble. Abandon planets sucking budget up
			if (log)
				ai.print("Colonization: negative budget, abandoning planets");
			auto@ homeworld = ai.empire.Homeworld;
			for (uint i = 0, cnt = planets.planets.length; i < cnt; i++) {
				auto@ pl = planets.planets[i].obj;
				if (pl is homeworld)
					continue;
				int resId = pl.primaryResourceType;
				if(resId == -1)
					continue;
				const ResourceType@ type = getResource(resId);
				if ((type.cls is scalableClass || type.level > 0) && pl.resourceLevel == 0) {
					pl.forceAbandon();
				}
			}
			//If we are still in trouble, abandon more planets
			if (ai.empire.EstNextBudget <= 0) {
				for (uint i = 0, cnt = planets.planets.length; i < cnt; i++) {
					auto@ pl = planets.planets[i].obj;
					if (pl is homeworld)
						continue;
					int resId = pl.primaryResourceType;
					if(resId == -1)
						continue;
					const ResourceType@ type = getResource(resId);
					if ((type.cls is foodClass || type.cls is waterClass) && !pl.primaryResourceExported)
						pl.forceAbandon();
				}
				//More!
				if (ai.empire.EstNextBudget <= 0) {
					for (uint i = 0, cnt = planets.planets.length; i < cnt; i++) {
						auto@ pl = planets.planets[i].obj;
						if (pl is homeworld)
							continue;
						int resId = pl.primaryResourceType;
						if(resId == -1)
							continue;
						const ResourceType@ type = getResource(resId);
						if (!(type.cls is foodClass || type.cls is waterClass || type.cls is scalableClass) && type.level == 0)
							pl.forceAbandon();
					}
				}
			}
		}

		//Check if we need to push for territory
		if (shouldForceExpansion()) {
			//If we already spent at least three turns trying to extend our territory, colonize a new one
			if (needsMoreTerritory && _territoryRequests >= 3)
				_needsNewTerritory = true;
			else {
				_needsMoreTerritory = true;
				_territoryRequests++;
			}
		}
		else {
			_needsMoreTerritory = false;
			_needsNewTerritory = false;
			_territoryRequests = 0;
			@_newTerritoryTarget = null;
		}

		prevColonizations = curColonizations;
		curColonizations = 0;

		updateSources();

		if(log) {
			ai.print("Empire colonization standings at "+formatGameTime(gameTime)+":");
			for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
				Empire@ other = getEmpire(i);
				if(other.major)
					ai.print("  "+ai.pad(other.name, 20)+" - "+ai.pad(other.TotalPlanets.value+" planets", 15)+" - "+other.points.value+" points");
			}
		}
	}

	bool shouldQueueFor(const ResourceSpec@ spec, ColonizeQueue@ inside = null) {
		auto@ list = inside is null ? this.queue : inside.children;
		for(uint i = 0, cnt = list.length; i < cnt; ++i) {
			auto@ q = list[i];

			//haven't managed to resolve it fully, skip it as well
			if(spec.type == RST_Level_Specific) {
				if(q.spec.type == RST_Level_Specific && q.spec.level == spec.level) {
					if(!isResolved(q))
						return false;
				}
			}

			//Check anything inner to this tree element
			if(!shouldQueueFor(spec, q))
				return false;
		}

		return true;
	}

	bool shouldQueueFor(ImportData@ imp, ColonizeQueue@ inside = null) {
		auto@ list = inside is null ? this.queue : inside.children;
		for(uint i = 0, cnt = list.length; i < cnt; ++i) {
			auto@ q = list[i];

			//If we already have this in our queue tree, don't colonize it again
			if(imp.forLevel) {
				if(q.forData is imp)
					return false;
				if(q.parent !is null && q.parent.step !is null && q.parent.step.target is imp.obj) {
					if(q.spec == imp.spec)
						return false;
				}
			}

			//If we're already trying to get something of this level, but we
			//haven't managed to resolve it fully, skip it as well
			if(imp.spec.type == RST_Level_Specific) {
				if(q.spec.type == RST_Level_Specific && q.spec.level == imp.spec.level) {
					if(!isResolved(q))
						return false;
				}
			}

			//Check anything inner to this tree element
			if(!shouldQueueFor(imp, q))
				return false;
		}

		return true;
	}

	ColonizeQueue@ queueColonize(ResourceSpec& spec, bool place = true) {
		ColonizeQueue q;
		@q.spec = spec;

		if(place)
			queue.insertLast(q);
		return q;
	}

	bool unresolvedInQueue() {
		for(uint i = 0, cnt = queue.length; i < cnt; ++i) {
			auto@ q = queue[i];
			if(q.parent !is null)
				continue;
			if(!isResolved(q))
				return true;
		}
		return false;
	}

	bool isResolved(ColonizeQueue@ q) {
		if(q.step is null || q.step.canceled)
			return false;
		for(uint i = 0 , cnt = q.children.length; i < cnt; ++i) {
			if(!isResolved(q.children[i]))
				return false;
		}
		return true;
	}

	bool isResolved(ImportData@ req, ColonizeQueue@ inside = null) {
		auto@ list = inside is null ? this.queue : inside.children;
		for(uint i = 0, cnt = list.length; i < cnt; ++i) {
			auto@ q = list[i];
			if(q.forData is req)
				return isResolved(q);
			if(isResolved(req, inside=q))
				return true;
		}
		return false;
	}

	Planet@ resolve(ColonizeQueue@ q) {
		if(q.step !is null)
			return q.step.target;

		auto@ potentials = getPotentialColonize();
		PotentialColonize@ take;
		double takeWeight = 0.0;

		for(uint i = 0, cnt = potentials.length; i < cnt; ++i) {
			auto@ p = potentials[i];
			if(!q.spec.meets(p.resource))
				continue;

			//Skip planets out of our new territory target if we are colonizing a new one
			if (_newTerritoryTarget !is null && p.pl.region !is _newTerritoryTarget)
				continue;

			if(p.weight > takeWeight) {
				takeWeight = p.weight;
				@take = p;
			}
		}

		if(take !is null) {
			@q.target = take.pl;
			potentials.remove(take);

			array<ResourceSpec@> allReqs;
			for(uint i = 1, cnt = take.resource.level; i <= cnt; ++i) {
				const PlanetLevel@ lvl = getPlanetLevel(take.pl, i);
				if(lvl !is null) {
					array<ResourceSpec@> reqList;
					array<ResourceSpec@> curReqs;
					curReqs = allReqs;

					const ResourceRequirements@ reqs = lvl.reqs;
					for(uint i = 0, cnt = reqs.reqs.length; i < cnt; ++i) {
						auto@ need = reqs.reqs[i];

						bool found = false;
						for(uint n = 0, ncnt = curReqs.length; n < ncnt; ++n) {
							if(curReqs[n].implements(need)) {
								found = true;
								curReqs.removeAt(n);
								break;
							}
						}

						if(!found)
							reqList.insertLast(implementSpec(need));
					}

					reqList.sortDesc();

					auto@ resRace = cast<RaceResources>(race);
					if(resRace !is null)
						resRace.levelRequirements(take.pl, i, reqList);

					for(uint i = 0, cnt = reqList.length; i < cnt; ++i) {
						auto@ spec = reqList[i];
						allReqs.insertLast(spec);

						auto@ inner = queueColonize(spec, place=false);

						@inner.parent = q;
						q.children.insertLast(inner);

						resolve(inner);
					}
				}
			}

			return take.pl;
		}

		return null;
	}

	void kill(ColonizeQueue@ q) {
		for(uint i = 0, cnt = q.children.length; i < cnt; ++i)
			kill(q.children[i]);
		q.children.length = 0;
		if(q.forData !is null)
			q.forData.isColonizing = false;
		@q.parent = null;
	}

	void modPotentialWeight(PotentialColonize@ c, double& weight) {
		if(colonizeWeightObj !is null)
			weight /= c.pl.position.distanceTo(colonizeWeightObj.position)/1000.0;
	}

	bool update(ColonizeQueue@ q) {
		//See if we can find a matching import request
		if(q.forData is null && q.parent !is null && q.parent.target !is null) {
			for(uint i = 0, cnt = resources.requested.length; i < cnt; ++i) {
				auto@ req = resources.requested[i];
				if(req.isColonizing)
					continue;
				if(req.obj !is q.parent.target)
					continue;
				if(req.spec != q.spec)
					continue;

				req.isColonizing = true;
				@q.forData = req;
			}
		}

		//Cancel everything if our request is already being met
		if(q.forData !is null && q.forData.beingMet) {
			kill(q);
			return false;
		}

		//If it's not resolved, try to resolve it
		if(q.target is null)
			resolve(q);

		//If the colonization failed, try to find a new planet for it
		if((q.step !is null && q.step.canceled) || (q.step is null && q.target !is null && !canBeColonized(q.target))) {
			auto@ potentials = getPotentialColonize();
			PotentialColonize@ take;
			double takeWeight = 0.0;

			for(uint i = 0, cnt = potentials.length; i < cnt; ++i) {
				auto@ p = potentials[i];
				if(!q.spec.meets(p.resource))
					continue;

				//Skip planets out of our new territory target if we are colonizing a new one
				if (_newTerritoryTarget !is null && p.pl.region !is _newTerritoryTarget)
					continue;

				double w = p.weight;
				modPotentialWeight(p, w);

				if(w > takeWeight) {
					takeWeight = p.weight;
					@take = p;
				}
			}

			if(take !is null) {
				@q.target = take.pl;
				@q.step = null;
				potentials.remove(take);
			}
		}

		for(uint i = 0, cnt = q.children.length; i < cnt; ++i) {
			if(!update(q.children[i])) {
				@q.children[i].parent = null;
				q.children.removeAt(i);
				--i; --cnt;
			}
		}

		if(q.children.length == 0 && q.step !is null && q.step.completed) {
			if(q.forData !is null) {
				q.forData.isColonizing = false;

				PlanetAI@ plAI = planets.getAI(q.target);
				if(plAI !is null) {
					if(plAI.resources.length != 0) {
						WaitUsed wait;
						@wait.forData = q.forData;
						@wait.resource = plAI.resources[0];
						waiting.insertLast(wait);
						q.forData.isColonizing = true;
					}
				}
			}
			return false;
		}
		return true;
	}

	void updateQueue() {
		for(uint i = 0, cnt = queue.length; i < cnt; ++i) {
			auto@ q = queue[i];
			if(!update(q)) {
				queue.removeAt(i);
				--i; --cnt;
			}
		}
	}

	bool orderFromQueue(ColonizeQueue@ inside = null) {
		auto@ list = inside is null ? this.queue : inside.children;
		for(uint i = 0, cnt = list.length; i < cnt; ++i) {
			auto@ q = list[i];
			if(q.step is null && q.target !is null) {
				@q.step = colonize(q.target);
				return true;
			}

			if(orderFromQueue(q))
				return true;
		}
		return false;
	}

	void dumpQueue(ColonizeQueue@ inside = null) {
		auto@ list = inside is null ? this.queue : inside.children;

		string prefix = "";
		if(inside !is null) {
			prefix += " ";
			ColonizeQueue@ top = inside.parent;
			while(top !is null) {
				prefix += " ";
				@top = top.parent;
			}
		}

		for(uint i = 0, cnt = list.length; i < cnt; ++i) {
			auto@ q = list[i];

			string txt = "- "+q.spec.dump();
			if(q.forData !is null)
				txt += " for request "+q.forData.obj.name+"";
			else if(q.parent !is null && q.parent.target !is null)
				txt += " for parent "+q.parent.target.name+"";
			if(q.target !is null)
				txt += " ==> "+q.target.name;
			print(prefix+txt);

			dumpQueue(q);
		}
	}

	void fillQueueFromRequests() {
		for(uint i = 0, cnt = resources.requested.length; i < cnt && remainColonizations > 0; ++i) {
			auto@ req = resources.requested[i];
			if(!req.isOpen)
				continue;
			if(!req.cycled)
				continue;
			if(req.claimedFor)
				continue;
			if(req.isColonizing)
				continue;

			if(shouldQueueFor(req)) {
				auto@ q = queueColonize(req.spec);
				@q.forData = req;
				req.isColonizing = true;
			}
		}
	}

};

AIComponent@ createColonization() {
	return Colonization();
}

Added scripts/server/empire_ai/weasel/Consider.as.






























































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// Consider
// --------
// Helps AI usage hints to consider various things in the empire.
//

import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Planets;
import empire_ai.weasel.Development;
import empire_ai.weasel.Construction;
import empire_ai.weasel.Fleets;
import empire_ai.weasel.Resources;
import empire_ai.weasel.Colonization;
import empire_ai.weasel.Intelligence;

import buildings;
import ai.consider;

from ai.artifacts import ArtifactConsider;
from orbitals import OrbitalModule;

class Consider : AIComponent, Considerer {
	Systems@ systems;
	Fleets@ fleets;
	Planets@ planets;
	Construction@ construction;
	Development@ development;
	Resources@ resources;
	Intelligence@ intelligence;
	Colonization@ colonization;

	void create() {
		@systems = cast<Systems>(ai.systems);
		@fleets = cast<Fleets>(ai.fleets);
		@planets = cast<Planets>(ai.planets);
		@development = cast<Development>(ai.development);
		@construction = cast<Construction>(ai.construction);
		@resources = cast<Resources>(ai.resources);
		@intelligence = cast<Intelligence>(ai.intelligence);
		@colonization = cast<Colonization>(ai.colonization);
	}

	Empire@ get_empire() {
		return ai.empire;
	}

	Object@ secondary;
	ArtifactConsider@ artifactConsider;
	double bestWeight;
	ImportData@ request;
	const BuildingType@ bldType;
	const OrbitalModule@ _module;
	ConsiderComponent@ comp;
	ConsiderFilter@ cfilter;

	double get_selectedWeight() {
		return bestWeight;
	}

	Object@ get_currentSupplier() {
		return secondary;
	}

	ArtifactConsider@ get_artifact() {
		return artifactConsider;
	}

	void set_artifact(ArtifactConsider@ cons) {
		@artifactConsider = cons;
	}

	double get_idleTime() {
		if(request !is null)
			return gameTime - request.idleSince;
		return 0.0;
	}

	double timeSinceMatchingColonize() {
		if(request is null)
			return INFINITY;
		return colonization.timeSinceMatchingColonize(request.spec);
	}

	const BuildingType@ get_building() {
		return bldType;
	}

	void set_building(const BuildingType@ type) {
		@bldType = type;
	}
	
	const OrbitalModule@ get_module() {
		return _module;
	}
	
	void set_module(const OrbitalModule@ type) {
		@_module = type;
	}

	ConsiderComponent@ get_component() {
		return comp;
	}

	void set_component(ConsiderComponent@ comp) {
		@this.comp = comp;
	}

	void set_filter(ConsiderFilter@ filter) {
		@this.cfilter = filter;
	}

	void clear() {
		@secondary = null;
		@artifactConsider = null;
		@request = null;
		@comp = null;
		@bldType = null;
		@cfilter = null;
	}

	Object@ OwnedSystems(const ConsiderHook& hook, uint limit = uint(-1)) {
		Object@ best;
		bestWeight = 0.0;

		uint offset = randomi(0, systems.owned.length-1);
		uint cnt = min(systems.owned.length, limit);
		for(uint i = 0; i < cnt; ++i) {
			uint index = (i+offset) % systems.owned.length;
			Region@ obj = systems.owned[index].obj;
			if(obj !is null) {
				if(cfilter !is null && !cfilter.filter(obj))
					continue;
				double w = hook.consider(this, obj);
				if(w > bestWeight) {
					bestWeight = w;
					@best = obj;
				}
			}
		}

		clear();
		return best;
	}

	Object@ Fleets(const ConsiderHook& hook) {
		Object@ best;
		bestWeight = 0.0;
		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			Object@ fleet = fleets.fleets[i].obj;
			if(fleet !is null) {
				if(cfilter !is null && !cfilter.filter(fleet))
					continue;
				double w = hook.consider(this, fleet);
				if(w > bestWeight) {
					bestWeight = w;
					@best = fleet;
				}
			}
		}

		clear();
		return best;
	}

	Object@ BorderSystems(const ConsiderHook& hook) {
		Object@ best;
		bestWeight = 0.0;
		for(uint i = 0, cnt = systems.border.length; i < cnt; ++i) {
			Region@ obj = systems.border[i].obj;
			if(obj.PlanetsMask & ~ai.mask == 0)
				continue;
			if(obj !is null) {
				if(cfilter !is null && !cfilter.filter(obj))
					continue;
				double w = hook.consider(this, obj);
				if(w > bestWeight) {
					bestWeight = w;
					@best = obj;
				}
			}
		}
		for(uint i = 0, cnt = systems.outsideBorder.length; i < cnt; ++i) {
			Region@ obj = systems.outsideBorder[i].obj;
			if(obj.PlanetsMask & ~ai.mask == 0)
				continue;
			if(obj !is null) {
				if(cfilter !is null && !cfilter.filter(obj))
					continue;
				double w = hook.consider(this, obj);
				if(w > bestWeight) {
					bestWeight = w;
					@best = obj;
				}
			}
		}

		clear();
		return best;
	}

	Object@ OtherSystems(const ConsiderHook& hook) {
		Object@ best;
		bestWeight = 0.0;
		for(uint i = 0, cnt = intelligence.intel.length; i < cnt; ++i) {
			auto@ intel = intelligence.intel[i];
			if(intel is null)
				continue;

			for(uint i = 0, cnt = intel.theirOwned.length; i < cnt; ++i) {
				Region@ obj = intel.theirOwned[i].obj;
				if(obj.PlanetsMask & ~ai.mask == 0)
					continue;
				if(obj !is null) {
					if(cfilter !is null && !cfilter.filter(obj))
						continue;
					double w = hook.consider(this, obj);
					if(w > bestWeight) {
						bestWeight = w;
						@best = obj;
					}
				}
			}
		}

		clear();
		return best;
	}
	
	Object@ SystemsInTerritory(const ConsiderHook& hook, const Territory& territory, uint limit = uint(-1)) {
		Object@ best;
		bestWeight = 0.0;

		uint regionCount = territory.getRegionCount();
		uint offset = randomi(0, regionCount -1);
		uint cnt = min(regionCount, limit);
		for(uint i = 0; i < cnt; ++i) {
			uint index = (i+offset) % regionCount;
			Region@ obj = territory.getRegion(index);
			if(obj !is null) {
				if(cfilter !is null && !cfilter.filter(obj))
					continue;
				double w = hook.consider(this, obj);
				if(w > bestWeight) {
					bestWeight = w;
					@best = obj;
				}
			}
		}

		clear();
		return best;
	}

	Object@ ImportantPlanets(const ConsiderHook& hook) {
		Object@ best;
		bestWeight = 0.0;
		for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) {
			Object@ obj = development.focuses[i].obj;
			if(obj !is null) {
				if(cfilter !is null && !cfilter.filter(obj))
					continue;
				double w = hook.consider(this, obj);
				if(w > bestWeight) {
					bestWeight = w;
					@best = obj;
				}
			}
		}

		clear();
		return best;
	}
	
	Object@ ImportantPlanetsInTerritory(const ConsiderHook& hook, const Territory& territory) {
		Object@ best;;
		bestWeight = 0.0;
		for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) {
			Object@ obj = development.focuses[i].obj;
			if(obj !is null) {
				if(cfilter !is null && !cfilter.filter(obj))
					continue;
				if (obj.region !is null) {
					if (obj.region.getTerritory(ai.empire) !is territory)
						continue;
					double w = hook.consider(this, obj);
					if(w > bestWeight) {
						bestWeight = w;
						@best = obj;
					}
				}
			}
		}

		clear();
		return best;
	}

	Object@ AllPlanets(const ConsiderHook& hook) {
		Object@ best;
		bestWeight = 0.0;
		for(uint i = 0, cnt = planets.planets.length; i < cnt; ++i) {
			Object@ obj = planets.planets[i].obj;
			if(obj !is null) {
				if(cfilter !is null && !cfilter.filter(obj))
					continue;
				double w = hook.consider(this, obj);
				if(w > bestWeight) {
					bestWeight = w;
					@best = obj;
				}
			}
		}

		clear();
		return best;
	}
	
	Object@ PlanetsInTerritory(const ConsiderHook& hook, const Territory& territory) {
		Object@ best;
		bestWeight = 0.0;
		for(uint i = 0, cnt = planets.planets.length; i < cnt; ++i) {
			Object@ obj = planets.planets[i].obj;
			if(obj !is null) {
				if(cfilter !is null && !cfilter.filter(obj))
					continue;
				if (obj.region !is null) {
					if (obj.region.getTerritory(ai.empire) !is territory)
						continue;
					double w = hook.consider(this, obj);
					if(w > bestWeight) {
						bestWeight = w;
						@best = obj;
					}
				}
			}
		}

		clear();
		return best;
	}

	Object@ SomePlanets(const ConsiderHook& hook, uint count, bool alwaysImportant) {
		Object@ best;
		bestWeight = 0.0;
		if(alwaysImportant) {
			for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) {
				Object@ obj = development.focuses[i].obj;
				if(obj !is null) {
					if(cfilter !is null && !cfilter.filter(obj))
						continue;
					double w = hook.consider(this, obj);
					if(w > bestWeight) {
						bestWeight = w;
						@best = obj;
					}
				}
			}
		}

		uint planetCount = planets.planets.length;
		uint offset = randomi(0, planetCount-1);
		uint cnt = min(count, planetCount);
		for(uint i = 0; i < cnt; ++i) {
			uint index = (offset+i) % planetCount;
			Object@ obj = planets.planets[index].obj;
			if(obj !is null) {
				if(cfilter !is null && !cfilter.filter(obj))
					continue;
				double w = hook.consider(this, obj);
				if(w > bestWeight) {
					bestWeight = w;
					@best = obj;
				}
			}
		}

		clear();
		return best;
	}

	Object@ FactoryPlanets(const ConsiderHook& hook) {
		Object@ best;
		bestWeight = 0.0;
		for(uint i = 0, cnt = construction.factories.length; i < cnt; ++i) {
			Object@ obj = construction.factories[i].obj;
			if(obj !is null && obj.isPlanet) {
				if(cfilter !is null && !cfilter.filter(obj))
					continue;
				double w = hook.consider(this, obj);
				if(w > bestWeight) {
					bestWeight = w;
					@best = obj;
				}
			}
		}

		clear();
		return best;
	}

	Object@ MatchingImportRequests(const ConsiderHook& hook, const ResourceType@ type, bool considerExisting) {
		Object@ best;
		bestWeight = 0.0;
		for(uint i = 0, cnt = resources.requested.length; i < cnt; ++i) {
			ImportData@ req = resources.requested[i];
			if(!considerExisting) {
				if(req.beingMet || req.claimedFor)
					continue;
			}
			if(req.spec.meets(type, req.obj, req.obj)) {
				@secondary = null;
				@request = req;
				double w = hook.consider(this, req.obj);
				if(w > bestWeight) {
					if(cfilter !is null && !cfilter.filter(req.obj))
						continue;
					bestWeight = w;
					@best = req.obj;
				}
			}
		}
		if(considerExisting) {
			for(uint i = 0, cnt = resources.used.length; i < cnt; ++i) {
				ExportData@ res = resources.used[i];
				ImportData@ req = res.request;
				if(req !is null && req.spec.meets(type, req.obj, req.obj)) {
					@secondary = res.obj;
					@request = req;
					double w = hook.consider(this, req.obj);
					if(w > bestWeight) {
						if(cfilter !is null && !cfilter.filter(req.obj))
							continue;
						bestWeight = w;
						@best = req.obj;
					}
				}
			}
		}

		clear();
		return best;
	}
};

AIComponent@ createConsider() {
	return Consider();
}

Added scripts/server/empire_ai/weasel/Construction.as.












































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
// Construction
// ------------
// Manages factories and allows build requests for flagships, orbitals, and
// anything else that requires labor.
//

import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.Budget;
import empire_ai.weasel.Planets;
import empire_ai.weasel.Designs;
import empire_ai.weasel.Resources;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Orbitals;

import orbitals;
import saving;

import systems;
import regions.regions;

import ai.construction;

from constructible import ConstructibleType;
from constructions import ConstructionType, getConstructionType;

class AllocateConstruction : IConstruction {
	protected bool _completed = false;
	protected bool _started = false;
	
	protected int _id = -1;
	uint moneyType = BT_Development;
	Factory@ tryFactory;
	double maxTime = INFINITY;
	double completedAt = 0;
	AllocateBudget@ alloc;
	int cost = 0;
	int maintenance = 0;
	double priority = 1.0;

	AllocateConstruction() {
	}
	
	int id {
		get const { return _id; }
		set { _id = value; }
	}
	
	bool get_started() const { return _started; }
	
	bool completed {
		get const { return _completed; }
		set { _completed = value; }
	}

	void _save(Construction& construction, SaveFile& file) {
		file << moneyType;
		construction.saveFactory(file, tryFactory);
		file << maxTime;
		file << _completed;
		file << _started;
		file << completedAt;
		construction.budget.saveAlloc(file, alloc);
		file << cost;
		file << maintenance;
		file << priority;
		save(construction, file);
	}

	void save(Construction& construction, SaveFile& file) {
	}

	void _load(Construction& construction, SaveFile& file) {
		file >> moneyType;
		@tryFactory = construction.loadFactory(file);
		file >> maxTime;
		file >> _completed;
		file >> _started;
		file >> completedAt;
		@alloc = construction.budget.loadAlloc(file);
		file >> cost;
		file >> maintenance;
		file >> priority;
		load(construction, file);
	}

	void load(Construction& construction, SaveFile& file) {
	}

	bool tick(AI& ai, Construction& construction, double time) {
		if(tryFactory !is null && alloc.allocated) {
			construction.start(tryFactory, this);
			return false;
		}
		return true;
	}

	void update(AI& ai, Factory@ f) {
		@alloc = cast<Budget>(ai.budget).allocate(moneyType, cost, maintenance, priority);
	}

	double laborCost(AI& ai, Object@ obj) {
		return 0.0;
	}

	bool canBuild(AI& ai, Factory@ f) {
		return true;
	}

	void construct(AI& ai, Factory@ f) {
		_started = true;
	}

	string toString() {
		return "construction";
	}
};

class BuildFlagship : AllocateConstruction, IFlagshipConstruction {
	protected const Design@ _design;
	double baseLabor = 0.0;
	DesignTarget@ target;

	BuildFlagship() {
	}

	BuildFlagship(const Design@ dsg) {
		set(dsg);
	}

	BuildFlagship(DesignTarget@ target) {
		@this.target = target;
	}
	
	const Design@ get_design() const { return _design; }

	void save(Construction& construction, SaveFile& file) {
		file << baseLabor;
		if(_design !is null) {
			file.write1();
			file << _design;
		}
		else {
			file.write0();
		}
		construction.designs.saveDesign(file, target);
	}

	void load(Construction& construction, SaveFile& file) {
		file >> baseLabor;
		if(file.readBit())
			file >> _design;
		@target = construction.designs.loadDesign(file);
	}

	void set(const Design& dsg) {
		@_design = dsg.mostUpdated();
		baseLabor = _design.total(HV_LaborCost);
	}

	double laborCost(AI& ai, Object@ obj) {
		return baseLabor;
	}

	bool tick(AI& ai, Construction& construction, double time) override {
		if(target !is null) {
			if(target.active !is null) {
				set(target.active);
				@target = null;
			}
		}
		return AllocateConstruction::tick(ai, construction, time);
	}

	bool canBuild(AI& ai, Factory@ f) override {
		if(!f.obj.canBuildShips)
			return false;
		return _design !is null;
	}

	void update(AI& ai, Factory@ f) {
		double c = _design.total(HV_BuildCost);
		c *= double(f.obj.shipBuildCost) / 100.0;
		c *= f.obj.constructionCostMod;

		cost = ceil(c);
		maintenance = ceil(_design.total(HV_MaintainCost));

		AllocateConstruction::update(ai, f);
	}

	void construct(AI& ai, Factory@ f) {
		f.obj.buildFlagship(_design);
		AllocateConstruction::construct(ai, f);
	}

	string toString() {
		if(_design is null)
			return "flagship (design in progress)";
		return "flagship " + _design.name;
	}
};

class BuildFlagshipSourced : BuildFlagship {
	Object@ buildAt;
	Object@ buildFrom;

	BuildFlagshipSourced() {
	}

	BuildFlagshipSourced(const Design@ dsg) {
		set(dsg);
	}

	BuildFlagshipSourced(DesignTarget@ target) {
		@this.target = target;
	}

	void save(Construction& construction, SaveFile& file) override {
		BuildFlagship::save(construction, file);
		file << buildAt;
		file << buildFrom;
	}

	void load(Construction& construction, SaveFile& file) override {
		BuildFlagship::load(construction, file);
		file >> buildAt;
		file >> buildFrom;
	}

	bool canBuild(AI& ai, Factory@ f) override {
		if(buildAt !is null && f.obj !is buildAt)
			return false;
		return BuildFlagship::canBuild(ai, f);
	}

	void construct(AI& ai, Factory@ f) override {
		f.obj.buildFlagship(_design, constructFrom=buildFrom);
		AllocateConstruction::construct(ai, f);
	}
};

class BuildStation : AllocateConstruction, IStationConstruction {
	protected const Design@ _design;
	double baseLabor = 0.0;
	DesignTarget@ target;
	vec3d position;
	bool local = false;

	BuildStation() {
	}

	BuildStation(const Design@ dsg, const vec3d& position) {
		this.position = position;
		set(dsg);
	}

	BuildStation(DesignTarget@ target, const vec3d& position) {
		this.position = position;
		@this.target = target;
	}

	BuildStation(const Design@ dsg, bool local) {
		this.local = true;
		set(dsg);
	}

	BuildStation(DesignTarget@ target, bool local) {
		@this.target = target;
		this.local = true;
	}
	
	const Design@ get_design() const { return _design; }

	void save(Construction& construction, SaveFile& file) {
		file << baseLabor;
		file << position;
		file << local;
		if(_design !is null) {
			file.write1();
			file << _design;
		}
		else {
			file.write0();
		}
		construction.designs.saveDesign(file, target);
	}

	void load(Construction& construction, SaveFile& file) {
		file >> baseLabor;
		file >> position;
		file >> local;
		if(file.readBit())
			file >> _design;
		@target = construction.designs.loadDesign(file);
	}

	void set(const Design& dsg) {
		@_design = dsg.mostUpdated();
		baseLabor = _design.total(HV_LaborCost);
	}

	double laborCost(AI& ai, Object@ obj) {
		double labor = baseLabor;

		labor *= obj.owner.OrbitalLaborCostFactor;

		if(!local) {
			Region@ reg = getRegion(position);
			Region@ targReg = obj.region;
			if(reg !is null && targReg !is null) {
				int hops = cast<Systems>(ai.systems).tradeDistance(targReg, reg);
				if(hops > 0) {
					double penalty = 1.0 + config::ORBITAL_LABOR_COST_STEP * double(hops);
					baseLabor *= penalty;
				}
			}
		}
		return labor;
	}

	bool tick(AI& ai, Construction& construction, double time) override {
		if(target !is null) {
			if(target.active !is null) {
				set(target.active);
				@target = null;
			}
		}
		return AllocateConstruction::tick(ai, construction, time);
	}

	bool canBuild(AI& ai, Factory@ f) override {
		if(_design is null)
			return false;
		if(!f.obj.canBuildOrbitals)
			return false;
		Region@ targReg = f.obj.region;
		if(targReg is null)
			return false;
		if(!local) {
			Region@ reg = getRegion(position);
			if(reg is null)
				return false;
			if(!cast<Systems>(ai.systems).canTrade(targReg, reg))
				return false;
		}
		return true;
	}

	void update(AI& ai, Factory@ f) {
		double c = _design.total(HV_BuildCost);
		c *= f.obj.owner.OrbitalBuildCostFactor;
		c *= f.obj.constructionCostMod;

		cost = ceil(c);
		maintenance = ceil(_design.total(HV_MaintainCost));

		AllocateConstruction::update(ai, f);
	}

	void construct(AI& ai, Factory@ f) {
		if(local) {
			position = f.obj.position;
			vec2d offset = random2d(f.obj.radius + 10.0, f.obj.radius + 100.0);
			position.x += offset.x;
			position.z += offset.y;
		}
		f.obj.buildStation(_design, position);
		AllocateConstruction::construct(ai, f);
	}

	string toString() {
		if(_design is null)
			return "station (design in progress)";
		return "station " + _design.name;
	}
};

class BuildOrbital : AllocateConstruction, IOrbitalConstruction {
	protected const OrbitalModule@ _module;
	double baseLabor = 0.0;
	const Planet@ planet;
	bool local = false;
	vec3d position;

	BuildOrbital() {
	}

	BuildOrbital(const OrbitalModule@ module, const vec3d& position) {
		this.position = position;
		@this._module = module;
		baseLabor = module.laborCost;
	}

	BuildOrbital(const OrbitalModule@ module, bool local) {
		this.local = true;
		@this._module = module;
		baseLabor = module.laborCost;
	}

	BuildOrbital(const OrbitalModule@ module, const Planet@ planet) {
		this.local = true;
		@this.planet = planet;
		@this._module = module;
		baseLabor = module.laborCost;
	}
	
	const OrbitalModule@ get_module() const { return _module; }

	void save(Construction& construction, SaveFile& file) {
		file << baseLabor;
		file << position;
		file << local;
		file.writeIdentifier(SI_Orbital, _module.id);
	}

	void load(Construction& construction, SaveFile& file) {
		file >> baseLabor;
		file >> position;
		file >> local;
		@_module = getOrbitalModule(file.readIdentifier(SI_Orbital));
	}

	double laborCost(AI& ai, Object@ obj) {
		double labor = baseLabor;

		labor *= obj.owner.OrbitalLaborCostFactor;

		if(!local) {
			Region@ reg = getRegion(position);
			Region@ targReg = obj.region;
			if(reg !is null && targReg !is null) {
				int hops = cast<Systems>(ai.systems).tradeDistance(targReg, reg);
				if(hops > 0) {
					double penalty = 1.0 + config::ORBITAL_LABOR_COST_STEP * double(hops);
					baseLabor *= penalty;
				}
			}
		}
		return labor;
	}

	bool tick(AI& ai, Construction& construction, double time) override {
		return AllocateConstruction::tick(ai, construction, time);
	}

	bool canBuild(AI& ai, Factory@ f) override {
		if(_module is null)
			return false;
		if(!f.obj.canBuildOrbitals)
			return false;
		Region@ targReg = f.obj.region;
		if(targReg is null)
			return false;
		if(!local) {
			Region@ reg = getRegion(position);
			if(reg is null)
				return false;
			if(!cast<Systems>(ai.systems).canTrade(targReg, reg))
				return false;
		}
		return true;
	}

	void update(AI& ai, Factory@ f) {
		double c = _module.buildCost;
		c *= f.obj.owner.OrbitalBuildCostFactor;
		c *= f.obj.constructionCostMod;

		cost = ceil(c);
		maintenance = _module.maintenance;

		AllocateConstruction::update(ai, f);
	}

	void construct(AI& ai, Factory@ f) {
		if(local) {
			if (planet !is null) {
				position = planet.position;
				vec2d offset = random2d(planet.OrbitSize * 0.8, planet.OrbitSize * 0.9);
				position.x += offset.x;
				position.z += offset.y;
			}
			else {
				position = f.obj.position;
				//vec2d offset = random2d(f.obj.radius + 10.0, f.obj.radius + 100.0);
				if (f.plAI !is null) {
					vec2d offset = random2d(f.plAI.obj.OrbitSize * 0.8, f.plAI.obj.OrbitSize * 0.9);
					position.x += offset.x;
					position.z += offset.y;
				}
			}
		}
		f.obj.buildOrbital(_module.id, position);
		AllocateConstruction::construct(ai, f);
	}

	string toString() {
		return "orbital " + _module.name;
	}
};

class RetrofitShip : AllocateConstruction {
	Ship@ ship;
	double labor;

	RetrofitShip() {
	}

	RetrofitShip(Ship@ ship) {
		@this.ship = ship;
		labor = ship.getRetrofitLabor();
		cost = ship.getRetrofitCost();
	}

	void save(Construction& construction, SaveFile& file) {
		file << ship;
		file << labor;
	}

	void load(Construction& construction, SaveFile& file) {
		file >> ship;
		file >> labor;
	}

	double laborCost(AI& ai, Object@ obj) {
		return labor;
	}

	bool canBuild(AI& ai, Factory@ f) override {
		if(!f.obj.canBuildShips)
			return false;
		Region@ reg = ship.region;
		return reg !is null && reg is f.obj.region;
	}

	void construct(AI& ai, Factory@ f) {
		ship.retrofitFleetAt(f.obj);
		AllocateConstruction::construct(ai, f);
	}

	string toString() {
		return "retrofit "+ship.name;
	}
};

class BuildConstruction : AllocateConstruction {
	const ConstructionType@ consType;

	BuildConstruction() {
	}

	BuildConstruction(const ConstructionType@ consType) {
		@this.consType = consType;
	}

	void save(Construction& construction, SaveFile& file) {
		file.writeIdentifier(SI_ConstructionType, consType.id);
	}

	void load(Construction& construction, SaveFile& file) {
		@consType = getConstructionType(file.readIdentifier(SI_ConstructionType));
	}

	double laborCost(AI& ai, Object@ obj) {
		if(obj is null)
			return consType.laborCost;
		return consType.getLaborCost(obj);
	}

	bool canBuild(AI& ai, Factory@ f) override {
		return consType.canBuild(f.obj, ignoreCost=true);
	}

	void update(AI& ai, Factory@ f) {
		cost = consType.getBuildCost(f.obj);
		maintenance = consType.getMaintainCost(f.obj);

		AllocateConstruction::update(ai, f);
	}

	void construct(AI& ai, Factory@ f) {
		f.obj.buildConstruction(consType.id);
		AllocateConstruction::construct(ai, f);
	}

	string toString() {
		return "construction "+consType.name;
	}
};

class Factory {
	Object@ obj;
	PlanetAI@ plAI;

	Factory@ exportingTo;

	AllocateConstruction@ active;
	double laborAim = 0.0;
	double laborIncome = 0.0;

	double idleSince = 0.0;
	double storedLabor = 0.0;
	double laborMaxStorage = 0.0;
	double buildingPenalty = 0.0;

	bool needsSupportLabor = false;
	double waitingSupportLabor = 0.0;
	uint curConstructionType = 0;
	bool valid = true;
	bool significantLabor = true;

	uint backgrounded = 0;
	Asteroid@ bgAsteroid;

	BuildingRequest@ curBuilding;
	ImportData@ curImport;

	void save(Construction& construction, SaveFile& file) {
		construction.planets.saveAI(file, plAI);
		construction.saveConstruction(file, active);
		file << laborAim;
		file << laborIncome;
		file << idleSince;
		file << storedLabor;
		file << laborMaxStorage;
		file << buildingPenalty;
		construction.planets.saveBuildingRequest(file, curBuilding);
		construction.resources.saveImport(file, curImport);
		file << backgrounded;
		file << bgAsteroid;
		file << curConstructionType;
		file << valid;
		file << needsSupportLabor;
		file << waitingSupportLabor;
		construction.saveFactory(file, exportingTo);
	}

	void load(Construction& construction, SaveFile& file) {
		@plAI = construction.planets.loadAI(file);
		@active = construction.loadConstruction(file);
		file >> laborAim;
		file >> laborIncome;
		file >> idleSince;
		file >> storedLabor;
		file >> laborMaxStorage;
		file >> buildingPenalty;
		@curBuilding = construction.planets.loadBuildingRequest(file);
		@curImport = construction.resources.loadImport(file);
		file >> backgrounded;
		file >> bgAsteroid;
		file >> curConstructionType;
		file >> valid;
		file >> needsSupportLabor;
		file >> waitingSupportLabor;
		@exportingTo = construction.loadFactory(file);
	}

	bool get_busy() {
		return active !is null;
	}

	bool get_needsLabor() {
		if(!valid)
			return false;
		if(obj.hasOrderedSupports)
			return true;
		if(active !is null)
			return true;
		if(needsSupportLabor)
			return true;
		if(obj.constructionCount > 0 && curConstructionType != CT_Export)
			return true;
		return false;
	}

	double laborToBear(AI& ai) {
		return laborIncome * ai.behavior.constructionMaxTime + storedLabor;
	}

	bool viable(AI& ai, AllocateConstruction@ alloc) {
		double labor = obj.laborIncome;
		double estTime = (alloc.laborCost(ai, obj) - storedLabor) / labor;
		if(estTime > alloc.maxTime)
			return false;
		return true;
	}

	bool tick(AI& ai, Construction& construction, double time) {
		if(obj is null || !obj.valid || obj.owner !is ai.empire) {
			valid = false;
			return false;
		}

		if(ai.behavior.forbidConstruction) return true;

		uint curCount = obj.constructionCount;
		curConstructionType = 0;
		bool isBackground = false;
		if(curCount != 0) {
			curConstructionType = obj.constructionType;
			isBackground = curConstructionType == CT_Asteroid || curConstructionType == CT_Export;
		}
		if(active !is null) {
			if(curCount <= backgrounded || (curCount == 1 && isBackground)) {
				if(construction.log)
					ai.print("Completed construction of "+active.toString()+" "+backgrounded+" / "+curCount, obj);
				active.completed = true;
				active.completedAt = gameTime;
				@active = null;
				idleSince = gameTime;
				backgrounded = 0;
			}
		}
		else {
			if(curCount < backgrounded) {
				backgrounded = curCount;
			}
		}

		//Background constructibles we don't need to do right now
		if(curCount > 1 && curConstructionType == CT_Asteroid && bgAsteroid !is null) {
			obj.moveConstruction(obj.constructionID[0], -1);
			backgrounded += 1;
		}
		if(curCount > 1 && curConstructionType == CT_Export && exportingTo !is null) {
			obj.cancelConstruction(obj.constructionID[0]);
			@exportingTo = null;
		}
		if(bgAsteroid !is null && (bgAsteroid.owner.valid || curCount == 0)) {
			if(bgAsteroid.owner is ai.empire)
				construction.planets.register(bgAsteroid);
			@bgAsteroid = null;
		}

		//Build warehouse(s) if we've been idle
		laborIncome = obj.laborIncome;
		storedLabor = obj.currentLaborStored;
		laborMaxStorage = obj.laborStorageCapacity;
		significantLabor = laborIncome >= 0.4 * construction.bestLabor && obj.baseLaborIncome > 4.0/60.0;
		if(storedLabor < laborMaxStorage)
			idleSince = gameTime;
		if(active is null && curBuilding is null && plAI !is null && gameTime - idleSince > ai.behavior.laborStoreIdleTimer && ai.behavior.buildLaborStorage && (laborMaxStorage+50) < ai.behavior.laborStoreMaxFillTime * max(obj.baseLaborIncome, laborAim) && significantLabor) {
			auto@ bld = ai.defs.LaborStorage;
			if(bld !is null && buildingPenalty < gameTime) {
				if(construction.log)
					ai.print("Build building "+bld.name+" for labor storage", obj);

				@curBuilding = construction.planets.requestBuilding(plAI, bld);
			}
		}

		//Remove waits on completed labor gains
		if(curBuilding !is null) {
			if(curBuilding.canceled || (curBuilding.built && curBuilding.getProgress() >= 1.0)) {
				if(construction.log)
					ai.print("Building construction for labor finished", obj);
				if(curBuilding.canceled)
					buildingPenalty = gameTime + 60.0;
				@curBuilding = null;
			}
		}
		if(curImport !is null) {
			if(curImport.beingMet) {
				if(construction.log)
					ai.print("Resource import for labor finished", obj);
				@curImport = null;
			}
		}

		//See if we need a new labor gain
		if(laborIncome < laborAim) {
			if(curImport is null && plAI !is null && obj.isPressureSaturated(TR_Labor) && obj.pressureCap < uint(obj.totalPressure) && gameTime > 6.0 * 60.0 && ai.behavior.buildFactoryForLabor) {
				ResourceSpec spec;
				spec.type = RST_Pressure_Level0;
				spec.pressureType = TR_Labor;

				if(construction.log)
					ai.print("Queue resource import for labor", obj);

				@curImport = construction.resources.requestResource(obj, spec, prioritize=true);
			}
			if(curBuilding is null && plAI !is null && ai.behavior.buildLaborStorage) {
				auto@ bld = ai.defs.Factory;
				if(bld !is null && buildingPenalty < gameTime) {
					if(construction.log)
						ai.print("Build building "+bld.name+" for labor", obj);

					@curBuilding = construction.planets.requestBuilding(plAI, bld);
				}
			}
		}

		//See if we should spend our labor on a labor export somewhere else
		if(exportingTo !is null && curConstructionType == CT_Export) {
			if(!exportingTo.valid || (!exportingTo.needsLabor && exportingTo !is construction.primaryFactory)) {
				obj.cancelConstruction(obj.constructionID[0]);
				@exportingTo = null;
			}
		}
		if(ai.behavior.distributeLaborExports) {
			if(curCount == 0 && obj.canExportLabor) {
				uint offset = randomi(0, construction.factories.length-1);
				for(uint i = 0, cnt = construction.factories.length; i < cnt; ++i) {
					auto@ other = construction.factories[(i+offset) % cnt];
					if(other is this)
						continue;
					if(!other.obj.canImportLabor)
						continue;

					//Check if this is currently busy
					if(other !is construction.primaryFactory) {
						if(!other.needsLabor)
							continue;
					}

					obj.exportLaborTo(other.obj);
					@exportingTo = other;
				}
			}
		}

		//See if we should spend our labor trying to build an asteroid
		if(ai.behavior.backgroundBuildAsteroids) {
			if((curCount == 0 || (curConstructionType == CT_Export && curCount == 1)) && storedLabor >= laborMaxStorage * 0.5 && obj.canBuildAsteroids) {
				Asteroid@ roid = construction.getBackgroundAsteroid(this);
				if(roid !is null) {
					uint resCount = roid.getAvailableCount();
					if(resCount != 0) {
						uint bestIndex = 0;
						int bestId = -1;
						double bestWeight = 0.0;

						if(ai.behavior.chooseAsteroidResource) {
							for(uint i = 0; i < resCount; ++i) {
								int resourceId = roid.getAvailable(i);
								double w = asteroidResourceValue(getResource(resourceId));
								if(w > bestWeight) {
									bestWeight = w;
									bestId = resourceId;
									bestIndex = i;
								}
							}
						}
						else {
							bestIndex = randomi(0, resCount-1);
							bestId = roid.getAvailable(bestIndex);
						}

						double laborCost = roid.getAvailableCost(bestIndex);

						Region@ fromReg = obj.region;
						Region@ toReg = roid.region;
						if(fromReg !is null && toReg !is null)
							laborCost *= 1.0 + config::ASTEROID_COST_STEP * double(construction.systems.hopDistance(fromReg, toReg));

						double timeTaken = laborIncome / laborCost;
						if(timeTaken < ai.behavior.constructionMaxTime || storedLabor >= laborMaxStorage * 0.95) {
							@bgAsteroid = roid;
							obj.buildAsteroid(roid, bestId);

							if(construction.log)
								ai.print("Use background labor to mine "+roid.name+" in "+roid.region.name, obj);
						}
					}
				}
			}
		}

		return true;
	}

	void aimForLabor(double labor) {
		if(labor > laborAim)
			laborAim = labor;
	}
};

class Construction : AIComponent {
	array<Factory@> factories;
	Factory@ primaryFactory;
	double noFactoryTimer = 0.0;

	int nextAllocId = 0;
	array<AllocateConstruction@> allocations;

	double totalLabor = 0.0;
	double bestLabor = 0.0;

	BuildOrbital@ buildConsolidate;

	Budget@ budget;
	Planets@ planets;
	Orbitals@ orbitals;
	Resources@ resources;
	Designs@ designs;
	Systems@ systems;

	void create() {
		@budget = cast<Budget>(ai.budget);
		@planets = cast<Planets>(ai.planets);
		@resources = cast<Resources>(ai.resources);
		@designs = cast<Designs>(ai.designs);
		@systems = cast<Systems>(ai.systems);
		@orbitals = cast<Orbitals>(ai.orbitals);
	}

	void save(SaveFile& file) {
		file << nextAllocId;

		uint cnt = allocations.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			saveConstruction(file, allocations[i]);

		cnt = factories.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			saveFactory(file, factories[i]);
			factories[i].save(this, file);
		}

		saveFactory(file, primaryFactory);
		file << noFactoryTimer;

		saveConstruction(file, buildConsolidate);
	}

	void load(SaveFile& file) {
		file >> nextAllocId;

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ alloc = loadConstruction(file);
			if(alloc !is null)
				allocations.insertLast(alloc);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			Factory@ f = loadFactory(file);
			if(f !is null)
				f.load(this, file);
			else
				Factory().load(this, file);
		}

		@primaryFactory = loadFactory(file);
		file >> noFactoryTimer;

		@buildConsolidate = cast<BuildOrbital>(loadConstruction(file));
	}

	void saveFactory(SaveFile& file, Factory@ f) {
		if(f !is null) {
			file.write1();
			file << f.obj;
		}
		else {
			file.write0();
		}
	}

	Factory@ loadFactory(SaveFile& file) {
		if(!file.readBit())
			return null;

		Object@ obj;
		file >> obj;

		if(obj is null)
			return null;

		for(uint i = 0, cnt = factories.length; i < cnt; ++i) {
			if(factories[i].obj is obj)
				return factories[i];
		}

		Factory f;
		@f.obj = obj;
		factories.insertLast(f);
		return f;
	}

	array<AllocateConstruction@> savedConstructions;
	array<AllocateConstruction@> loadedConstructions;
	void postSave(AI& ai) {
		savedConstructions.length = 0;
	}
	void postLoad(AI& ai) {
		loadedConstructions.length = 0;
	}

	void saveConstruction(SaveFile& file, AllocateConstruction@ alloc) {
		if(alloc is null) {
			file.write0();
			return;
		}

		file.write1();
		file << alloc.id;
		if(alloc.id == -1) {
			storeConstruction(file, alloc);
		}
		else {
			bool found = false;
			for(uint i = 0, cnt = savedConstructions.length; i < cnt; ++i) {
				if(savedConstructions[i] is alloc) {
					found = true;
					break;
				}
			}

			if(!found) {
				storeConstruction(file, alloc);
				savedConstructions.insertLast(alloc);
			}
		}
	}

	AllocateConstruction@ loadConstruction(SaveFile& file) {
		if(!file.readBit())
			return null;

		int id = 0;
		file >> id;
		if(id == -1) {
			AllocateConstruction@ alloc = createConstruction(file);
			alloc.id = id;
			return alloc;
		}
		else {
			for(uint i = 0, cnt = loadedConstructions.length; i < cnt; ++i) {
				if(loadedConstructions[i].id == id)
					return loadedConstructions[i];
			}

			AllocateConstruction@ alloc = createConstruction(file);
			alloc.id = id;
			loadedConstructions.insertLast(alloc);
			return alloc;
		}
	}

	void storeConstruction(SaveFile& file, AllocateConstruction@ alloc) {
		auto@ cls = getClass(alloc);
		auto@ mod = cls.module;

		file << mod.name;
		file << cls.name;
		alloc._save(this, file);
	}

	AllocateConstruction@ createConstruction(SaveFile& file) {
		string modName;
		string clsName;

		file >> modName;
		file >> clsName;

		auto@ mod = getScriptModule(modName);
		if(mod is null) {
			error("ERROR: AI Load could not find module for alloc "+modName+"::"+clsName);
			return null;
		}

		auto@ cls = mod.getClass(clsName);
		if(cls is null) {
			error("ERROR: AI Load could not find class for alloc "+modName+"::"+clsName);
			return null;
		}

		auto@ alloc = cast<AllocateConstruction>(cls.create());
		if(alloc is null) {
			error("ERROR: AI Load could not create class instance for alloc "+modName+"::"+clsName);
			return null;
		}

		alloc._load(this, file);
		return alloc;
	}

	void start() {
		Object@ hw = ai.empire.Homeworld;
		if(hw !is null) {
			Factory f;
			@f.obj = hw;
			@f.plAI = planets.getAI(cast<Planet>(hw));

			factories.insertLast(f);
		}
	}

	Factory@ get(Object@ obj) {
		for(uint i = 0, cnt = factories.length; i < cnt; ++i) {
			if(factories[i].obj is obj)
				return factories[i];
		}
		return null;
	}

	Factory@ registerFactory(Object@ obj) {
		for(uint i = 0, cnt = factories.length; i < cnt; ++i) {
			if(factories[i].obj is obj)
				return factories[i];
		}
		Factory f;
		@f.obj = obj;
		factories.insertLast(f);
		return f;
	}

	Factory@ getFactory(Region@ region) {
		Factory@ best;
		double bestLabor = 0;
		for(uint i = 0, cnt = factories.length; i < cnt; ++i) {
			if(factories[i].obj.region !is region)
				continue;
			double l = factories[i].obj.laborIncome;
			if(l > bestLabor) {
				bestLabor = l;
				@best = factories[i];
			}
		}
		return best;
	}

	BuildConstruction@ buildConstruction(const ConstructionType@ type, double priority = 1.0, bool force = false, uint moneyType = BT_Development) {
		//Potentially build a flagship
		BuildConstruction f(type);
		f.moneyType = moneyType;
		f.priority = priority;
		build(f, force=force);
		return f;
	}

	BuildFlagship@ buildFlagship(const Design@ dsg, double priority = 1.0, bool force = false) {
		//Potentially build a flagship
		BuildFlagship f(dsg);
		f.moneyType = BT_Military;
		f.priority = priority;
		build(f, force=force);
		return f;
	}

	BuildFlagship@ buildFlagship(DesignTarget@ target, double priority = 1.0, bool force = false) {
		//Potentially build a flagship
		BuildFlagship f(target);
		f.moneyType = BT_Military;
		f.priority = priority;
		build(f, force=force);
		return f;
	}

	BuildStation@ buildStation(const Design@ dsg, const vec3d& position, double priority = 1.0, bool force = false) {
		//Potentially build a flagship
		BuildStation f(dsg, position);
		f.moneyType = BT_Military;
		f.priority = priority;
		build(f, force=force);
		return f;
	}

	BuildStation@ buildStation(DesignTarget@ target, const vec3d& position, double priority = 1.0, bool force = false) {
		//Potentially build a flagship
		BuildStation f(target, position);
		f.moneyType = BT_Military;
		f.priority = priority;
		build(f, force=force);
		return f;
	}

	BuildOrbital@ buildOrbital(const OrbitalModule@ module, const vec3d& position, double priority = 1.0, bool force = false, uint moneyType = BT_Infrastructure) {
		//Potentially build a flagship
		BuildOrbital f(module, position);
		f.moneyType = moneyType;
		f.priority = priority;
		build(f, force=force);
		return f;
	}

	BuildStation@ buildLocalStation(const Design@ dsg, double priority = 1.0, bool force = false) {
		//Potentially build a flagship
		BuildStation f(dsg, local=true);
		f.moneyType = BT_Military;
		f.priority = priority;
		build(f, force=force);
		return f;
	}

	BuildStation@ buildLocalStation(DesignTarget@ target, double priority = 1.0, bool force = false) {
		//Potentially build a flagship
		BuildStation f(target, local=true);
		f.moneyType = BT_Military;
		f.priority = priority;
		build(f, force=force);
		return f;
	}

	BuildOrbital@ buildLocalOrbital(const OrbitalModule@ module, double priority = 1.0, bool force = false, uint moneyType = BT_Infrastructure) {
		//Potentially build a flagship
		BuildOrbital f(module, local=true);
		f.moneyType = moneyType;
		f.priority = priority;
		build(f, force=force);
		return f;
	}

	BuildOrbital@ buildLocalOrbital(const OrbitalModule@ module, Planet@ planet, double priority = 1.0, bool force = false, uint moneyType = BT_Infrastructure) {
		//Potentially build a flagship
		BuildOrbital f(module, planet);
		f.moneyType = moneyType;
		f.priority = priority;
		build(f, force=force);
		return f;
	}

	RetrofitShip@ retrofit(Ship@ ship, double priority = 1.0, bool force = false) {
		//Potentially build a flagship
		RetrofitShip f(ship);
		f.moneyType = BT_Military;
		f.priority = priority;
		build(f, force=force);
		return f;
	}

	AllocateConstruction@ build(AllocateConstruction@ alloc, bool force = false) {
		//Add a construction into the potential constructions queue
		if(!force)
			alloc.maxTime = ai.behavior.constructionMaxTime;
		alloc.id = nextAllocId++;
		allocations.insertLast(alloc);

		if(log)
			ai.print("Queue construction: "+alloc.toString());

		return alloc;
	}

	AllocateConstruction@ buildNow(AllocateConstruction@ alloc, Factory@ f) {
		if(f.busy)
			return null;
		if(alloc.alloc !is null)
			budget.applyNow(alloc.alloc);
		start(f, alloc);
		allocations.remove(alloc);
		return alloc;
	}

	void cancel(AllocateConstruction@ alloc) {
		if(alloc.started || (alloc.alloc !is null && alloc.alloc.allocated))
			return; //TODO

		allocations.remove(alloc);
		for(uint i = 0, cnt = factories.length; i < cnt; ++i) {
			if(factories[i].active is alloc)
				@factories[i].active = null;
		}

		if(alloc.alloc !is null)
			budget.remove(alloc.alloc);
	}

	uint factInd = 0;
	void tick(double time) {
		//Manage factories
		if(factories.length != 0) {
			factInd = (factInd+1) % factories.length;
			auto@ f = factories[factInd];
			if(!f.tick(ai, this, time))
				factories.removeAt(factInd);
		}
	}

	void start(Factory@ f, AllocateConstruction@ c) {
		if(ai.behavior.forbidConstruction) {
			cancel(c);
			return;
		}
		//Actually construct something we've allocated budget for
		@f.active = c;
		@c.tryFactory = null;

		c.construct(ai, f);

		if(log)
			ai.print("Construct: "+c.toString(), f.obj);

		for(uint i = 0, cnt = allocations.length; i < cnt; ++i) {
			if(allocations[i].tryFactory is f)
				@allocations[i].tryFactory = null;
		}
	}

	uint plCheck = 0;
	uint orbCheck = 0;
	double consTimer = 0.0;
	void focusTick(double time) {
		//Progress the allocations
		for(uint n = 0, ncnt = allocations.length; n < ncnt; ++n) {
			if(!allocations[n].tick(ai, this, time)) {
				allocations.removeAt(n);
				--n; --ncnt;
			}
		}

		if(ai.behavior.forbidConstruction) return;

		//See if anything we can potentially construct is constructible
		totalLabor = 0.0;
		bestLabor = 0.0;
		for(uint i = 0, cnt = factories.length; i < cnt; ++i) {
			auto@ f = factories[i];
			totalLabor += f.laborIncome;
			if(f.laborIncome > bestLabor)
				bestLabor = f.laborIncome;
		}

		for(uint n = 0, ncnt = allocations.length; n < ncnt; ++n) {
			auto@ alloc = allocations[n];
			if(alloc.tryFactory !is null)
				continue;

			Factory@ bestFact;
			double bestCur = 0.0;

			for(uint i = 0, cnt = factories.length; i < cnt; ++i) {
				auto@ f = factories[i];
				if(f.busy)
					continue;
				if(!alloc.canBuild(ai, f))
					continue;
				if(!f.viable(ai, alloc))
					continue;

				double w = f.laborIncome;
				if(f is primaryFactory)
					w *= 1.5;
				if(f.exportingTo !is null)
					w /= 0.75;

				if(w > bestCur) {
					bestCur = w;
					@bestFact = f;
				}
			}

			if(bestFact !is null) {
				@alloc.tryFactory = bestFact;
				alloc.update(ai, bestFact);
			}
		}

		//Classify our primary factory
		if(primaryFactory is null) {
			//Find our best factory
			Factory@ best;
			double bestWeight = 0.0;
			for(uint i = 0, cnt = factories.length; i < cnt; ++i) {
				auto@ f = factories[i];

				double w = f.laborIncome;
				w += 0.1 * f.laborAim;
				if(f.obj.isPlanet)
					w *= 100.0;
				if(f.obj.isShip)
					w *= 0.1;

				if(w > bestWeight) {
					bestWeight = w;
					@best = f;
				}
			}

			if(best !is null) {
				@primaryFactory = best;
			}
			else {
				noFactoryTimer += time;
				if(noFactoryTimer > 3.0 * 60.0 && ai.defs.Factory !is null) {
					//Just pick our highest level planet and hope for the best
					PlanetAI@ best;
					double bestWeight = 0.0;

					for(uint i = 0, cnt = planets.planets.length; i < cnt; ++i) {
						auto@ plAI = planets.planets[i];
						double w = plAI.obj.level;
						w += 0.5 * plAI.obj.resourceLevel;

						if(w > bestWeight) {
							bestWeight = w;
							@best = plAI;
						}
					}

					if(best !is null) {
						Factory f;
						@f.obj = best.obj;
						@f.plAI = best;

						factories.insertLast(f);
						@primaryFactory = f;
					}

					noFactoryTimer = 0.0;
				}
			}
		}
		else {
			noFactoryTimer = 0.0;
		}

		//Find new factories
		if(planets.planets.length != 0) {
			plCheck = (plCheck+1) % planets.planets.length;
			PlanetAI@ plAI = planets.planets[plCheck];
			if(plAI.obj.laborIncome > 0 && plAI.obj.canBuildShips) {
				if(get(plAI.obj) is null) {
					Factory f;
					@f.obj = plAI.obj;
					@f.plAI = plAI;

					factories.insertLast(f);
				}
			}
		}
		if(orbitals.orbitals.length != 0) {
			orbCheck = (orbCheck+1) % orbitals.orbitals.length;
			OrbitalAI@ orbAI = orbitals.orbitals[orbCheck];
			if(orbAI.obj.hasConstruction && orbAI.obj.laborIncome > 0
					&& !cast<Orbital>(orbAI.obj).hasMaster()) {
				if(get(orbAI.obj) is null) {
					Factory f;
					@f.obj = orbAI.obj;

					factories.insertLast(f);
				}
			}
		}

		//See if we should switch our primary factory
		if(primaryFactory !is null) {
			if(!primaryFactory.valid) {
				@primaryFactory = null;
			}
			else {
				Factory@ best;
				double bestLabor = 0.0;
				double primaryLabor = primaryFactory.laborIncome;
				bool canImport = primaryFactory.obj.canImportLabor;
				for(uint i = 0, cnt = factories.length; i < cnt; ++i) {
					auto@ f = factories[i];
					double checkLabor = f.laborIncome;
					if(f.obj.isShip)
						checkLabor *= 0.1;
					if(f.exportingTo !is primaryFactory && canImport)
						primaryLabor += checkLabor * 0.75;
					if(checkLabor > bestLabor) {
						bestLabor = checkLabor;
						@best = f;
					}
				}

				if(best !is null && bestLabor > 1.5 * primaryLabor)
					@primaryFactory = best;
			}
		}

		//See if we should consolidate at a shipyard
		if(buildConsolidate !is null && buildConsolidate.completed) {
			@buildConsolidate = null;
			consTimer = gameTime + 60.0;
		}
		else if(ai.behavior.consolidateLaborExports && primaryFactory !is null && ai.defs.Shipyard !is null && buildConsolidate is null && !primaryFactory.obj.canImportLabor && consTimer < gameTime) {
			double totalLabor = 0.0, bestLabor = 0.0;
			for(uint i = 0, cnt = factories.length; i < cnt; ++i) {
				double inc = factories[i].obj.baseLaborIncome;
				if(factories[i].obj.canExportLabor)
					totalLabor += inc;
				if(factories[i].laborIncome > bestLabor)
					bestLabor = factories[i].laborIncome;
			}

			if(bestLabor < totalLabor * 0.6) {
				Factory@ bestConsolidate;
				double bestWeight = 0.0;
				for(uint i = 0, cnt = factories.length; i < cnt; ++i) {
					auto@ f = factories[i];
					if(!f.obj.canImportLabor)
						continue;

					double w = f.obj.baseLaborIncome;
					w /= f.obj.position.distanceTo(primaryFactory.obj.position);

					if(w > bestWeight) {
						bestWeight = w;
						@bestConsolidate = f;
					}
				}

				if(bestConsolidate !is null) {
					if(log)
						ai.print("Set shipyard for consolidate.", bestConsolidate.obj.region);
					@primaryFactory = bestConsolidate;
				}
				else {
					Region@ reg = primaryFactory.obj.region;
					if(reg !is null) {
						vec3d pos = reg.position;
						vec2d offset = random2d(reg.radius * 0.4, reg.radius * 0.8);
						pos.x += offset.x;
						pos.z += offset.y;

						if(log)
							ai.print("Build shipyard for consolidate.", reg);

						@buildConsolidate = buildOrbital(ai.defs.Shipyard, pos);
					}
				}
			}
		}
	}

	bool isGettingAsteroid(Asteroid@ asteroid) {
		for(uint i = 0, cnt = factories.length; i < cnt; ++i) {
			if(factories[i].bgAsteroid is asteroid)
				return true;
		}
		return false;
	}

	Asteroid@ getBackgroundAsteroid(Factory& f) {
		double closest = INFINITY;
		Asteroid@ best;
		Region@ reg = f.obj.region;
		if(reg is null)
			return null;

		uint cnt = systems.owned.length;
		uint offset = randomi(0, cnt-1);
		for(uint i = 0, check = min(3, cnt); i < check; ++i) {
			auto@ sys = systems.owned[(i+offset)%cnt];
			double dist = sys.obj.position.distanceToSQ(f.obj.position);
			if(dist > closest)
				continue;
			if(!sys.obj.sharesTerritory(ai.empire, reg))
				continue;

			for(uint n = 0, ncnt = sys.asteroids.length; n < ncnt; ++n) {
				Asteroid@ roid = sys.asteroids[n];
				if(roid.owner.valid)
					continue;
				if(roid.getAvailableCount() == 0)
					continue;
				if(isGettingAsteroid(roid))
					continue;

				closest = dist;
				@best = roid;
				break;
			}
		}

		cnt = systems.outsideBorder.length;
		offset = randomi(0, cnt-1);
		for(uint i = 0, check = min(3, cnt); i < check; ++i) {
			auto@ sys = systems.outsideBorder[(i+offset)%cnt];
			double dist = sys.obj.position.distanceToSQ(f.obj.position);
			if(dist > closest)
				continue;
			if(!sys.obj.sharesTerritory(ai.empire, reg))
				continue;

			for(uint n = 0, ncnt = sys.asteroids.length; n < ncnt; ++n) {
				Asteroid@ roid = sys.asteroids[n];
				if(roid.owner.valid)
					continue;
				if(roid.getAvailableCount() == 0)
					continue;
				if(isGettingAsteroid(roid))
					continue;

				closest = dist;
				@best = roid;
				break;
			}
		}

		return best;
	}
};

double asteroidResourceValue(const ResourceType@ type) {
	if(type is null)
		return 0.0;
	double w = 1.0;
	w += type.level * 10.0;
	w += type.totalPressure;
	if(type.cls !is null)
		w += 5.0;
	return w;
}

AIComponent@ createConstruction() {
	return Construction();
}

Added scripts/server/empire_ai/weasel/Creeping.as.






















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// Creeping
// --------
// Uses fleets that aren't currently doing anything to eliminate creeps.
//

import empire_ai.weasel.WeaselAI;

import empire_ai.weasel.Fleets;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Movement;
import empire_ai.weasel.searches;

import saving;
from empire import Creeps;

class CreepingMission : Mission {
	Pickup@ pickup;
	Object@ protector;

	MoveOrder@ move;

	void save(Fleets& fleets, SaveFile& file) {
		file << pickup;
		file << protector;
		fleets.movement.saveMoveOrder(file, move);
	}

	void load(Fleets& fleets, SaveFile& file) {
		file >> pickup;
		file >> protector;
		@move = fleets.movement.loadMoveOrder(file);
	}

	void start(AI& ai, FleetAI& fleet) override {
		vec3d position = pickup.position;
		double dist = fleet.radius;
		if(protector !is null && protector.valid)
			dist = fleet.obj.getEngagementRange();
		position += (fleet.obj.position - pickup.position).normalized(dist);

		@move = cast<Movement>(ai.movement).move(fleet.obj, position);
	}

	void tick(AI& ai, FleetAI& fleet, double time) {
		if(move !is null) {
			if(move.completed) {
				if(protector !is null && protector.valid) {
					if(!protector.isVisibleTo(ai.empire)) { //Yo nebulas are scary yo
						fleet.obj.addMoveOrder(protector.position);
						fleet.obj.addAttackOrder(protector, append=true);
					}
					else {
						fleet.obj.addAttackOrder(protector);
					}
				}
				@move = null;
			}
			else if(move.failed) {
				canceled = true;
				return;
			}
			else
				return;
		}
		if(protector is null || !protector.valid) {
			if(!fleet.obj.hasOrders) {
				if(pickup is null || !pickup.valid) {
					if(cast<Creeping>(ai.creeping).log)
						ai.print("Finished clearing creep camp", fleet.obj);
					completed = true;
				}
				else {
					fleet.obj.addPickupOrder(pickup);
					@protector = null;
				}
			}
		}
		else {
			if((fleet.filled < 0.3 || fleet.supplies < 0.3 || fleet.flagshipHealth < 0.4)
				&& protector.getFleetStrength() * ai.behavior.remnantOverkillFactor > fleet.strength) {
				//Holy shit what's going on? ABORT! ABORT!
				if(cast<Creeping>(ai.creeping).logCritical)
					ai.print("ABORTED CREEPING: About to lose fight", fleet.obj);
				canceled = true;
				cast<Fleets>(ai.fleets).returnToBase(fleet, MP_Critical);
			}
		}
	}
};

class ClearMission : Mission {
	Region@ region;
	Object@ eliminate;

	MoveOrder@ move;

	void save(Fleets& fleets, SaveFile& file) {
		file << region;
		file << eliminate;
		fleets.movement.saveMoveOrder(file, move);
	}

	void load(Fleets& fleets, SaveFile& file) {
		file >> region;
		file >> eliminate;
		@move = fleets.movement.loadMoveOrder(file);
	}

	void start(AI& ai, FleetAI& fleet) override {
		@move = cast<Movement>(ai.movement).move(fleet.obj, region);
	}

	void tick(AI& ai, FleetAI& fleet, double time) {
		if(move !is null) {
			if(move.completed) {
				@move = null;
			}
			else if(move.failed) {
				canceled = true;
				return;
			}
			else
				return;
		}

		if(ai.behavior.forbidCreeping) return;

		if(eliminate is null) {
			@eliminate = cast<Creeping>(ai.creeping).findRemnants(region);
			if(eliminate is null) {
				completed = true;
				return;
			}
		}

		if(eliminate !is null) {
			if(!eliminate.valid) {
				@eliminate = null;
			}
			else {
				if(!fleet.obj.hasOrders)
					fleet.obj.addAttackOrder(eliminate);

				if((fleet.filled < 0.3 || fleet.supplies < 0.3 || fleet.flagshipHealth < 0.4)
					&& eliminate.getFleetStrength() * ai.behavior.remnantOverkillFactor > fleet.strength) {
					//Holy shit what's going on? ABORT! ABORT!
					if(cast<Creeping>(ai.creeping).logCritical)
						ai.print("ABORTED CREEPING: About to lose fight", fleet.obj);
					canceled = true;
					cast<Fleets>(ai.fleets).returnToBase(fleet, MP_Critical);
				}
			}
		}
	}
};

final class CreepPenalty : Savable {
	Object@ obj;
	double until;

	void save(SaveFile& file) {
		file << obj;
		file << until;
	}

	void load(SaveFile& file) {
		file >> obj;
		file >> until;
	}
};

final class ClearSystem {
	SystemAI@ sys;
	array<Ship@> remnants;

	void save(Creeping& creeping, SaveFile& file) {
		creeping.systems.saveAI(file, sys);
		uint cnt = remnants.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << remnants[i];
	}

	void load(Creeping& creeping, SaveFile& file) {
		@sys = creeping.systems.loadAI(file);
		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			Ship@ remn;
			file >> remn;
			if(remn !is null)
				remnants.insertLast(remn);
		}
	}

	void record() {
		auto@ objs = findEnemies(sys.obj, null, Creeps.mask);
		for(uint i = 0, cnt = objs.length; i < cnt; ++i) {
			Ship@ ship = cast<Ship>(objs[i]);
			if(ship !is null)
				remnants.insertLast(ship);
		}
	}

	double getStrength() {
		double str = 0.0;
		for(uint i = 0, cnt = remnants.length; i < cnt; ++i) {
			if(remnants[i].valid)
				str += sqrt(remnants[i].getFleetStrength());
		}
		return str * str;
	}
};

class Creeping : AIComponent {
	Systems@ systems;
	Fleets@ fleets;

	array<SystemAI@> requested;
	array<CreepPenalty@> penalties;
	array<CreepingMission@> active;

	array<ClearSystem@> quarantined;

	void create() {
		@systems = cast<Systems>(ai.systems);
		@fleets = cast<Fleets>(ai.fleets);
	}

	void save(SaveFile& file) {
		uint cnt = requested.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			systems.saveAI(file, requested[i]);

		cnt = penalties.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << penalties[i];

		cnt = active.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			fleets.saveMission(file, active[i]);

		cnt = quarantined.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			quarantined[i].save(this, file);
	}

	void load(SaveFile& file) {
		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ sys = systems.loadAI(file);
			if(sys !is null)
				requested.insertLast(sys);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			CreepPenalty pen;
			file >> pen;
			if(pen.obj !is null)
				penalties.insertLast(pen);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ miss = cast<CreepingMission>(fleets.loadMission(file));
			if(miss !is null)
				active.insertLast(miss);
		}

		if(file >= SV_0151) {
			file >> cnt;
			for(uint i = 0; i < cnt; ++i) {
				ClearSystem qsys;
				qsys.load(this, file);
				quarantined.insertLast(qsys);
			}
		}
	}

	void requestClear(SystemAI@ system) {
		if(system is null)
			return;
		if(log)
			ai.print("Requested creep camp clear", system.obj);
		if(requested.find(system) == -1)
			requested.insertLast(system);
	}

	CreepingMission@ creepWithFleet(FleetAI@ fleet, Pickup@ pickup, Object@ protector = null) {
		if(protector is null)
			@protector = pickup.getProtector();

		if(log)
			ai.print("Clearing creep camp in "+pickup.region.name, fleet.obj);

		CreepingMission mission;
		@mission.pickup = pickup;
		@mission.protector = protector;

		fleets.performMission(fleet, mission);
		active.insertLast(mission);
		return mission;
	}

	Pickup@ best;
	Object@ bestProtector;
	vec3d ourPosition;
	double bestWeight;
	double ourStrength;

	void check(SystemAI@ sys, double weight = 1.0) {
		for(uint n = 0, ncnt = sys.pickups.length; n < ncnt; ++n) {
			Pickup@ pickup = sys.pickups[n];
			Object@ protector = sys.pickupProtectors[n];
			
			if(!pickup.valid)
				continue;

			double protStrength;
			if(protector !is null && protector.valid) {
				protStrength = protector.getFleetStrength();

				if(protStrength * ai.behavior.remnantOverkillFactor > ourStrength)
					continue;
			}
			else
				protStrength = 1.0;

			if(isCreeping(pickup))
				continue;

			double w = weight;
			w /= protStrength / 1000.0;
			w /= pickup.position.distanceTo(ourPosition);

			if(w > bestWeight) {
				bestWeight = w;
				@best = pickup;
				@bestProtector = protector;
			}
		}
	}

	void penalize(Object@ obj, double time) {
		for(uint i = 0, cnt = penalties.length; i < cnt; ++i) {
			if(penalties[i].obj is obj) {
				penalties[i].until = max(penalties[i].until, gameTime + time);
				return;
			}
		}

		CreepPenalty p;
		@p.obj = obj;
		p.until = gameTime + time;
		penalties.insertLast(p);
	}

	bool isPenalized(Object@ obj) {
		for(uint i = 0, cnt = penalties.length; i < cnt; ++i) {
			if(penalties[i].obj is obj)
				return true;
		}
		return false;
	}

	bool isCreeping(Pickup@ pickup) {
		for(uint i = 0, cnt = active.length; i < cnt; ++i) {
			if(active[i].pickup is pickup)
				return true;
		}
		return false;
	}

	CreepingMission@ creepWithFleet(FleetAI@ fleet) {
		@best = null;
		@bestProtector = null;
		bestWeight = 0.0;
		ourStrength = fleet.strength;
		ourPosition = fleet.obj.position;

		//Check requested systems first
		for(uint i = 0, cnt = requested.length; i < cnt; ++i) {
			auto@ sys = requested[i];

			if(sys.pickups.length == 0) {
				requested.removeAt(i);
				--i; --cnt;
				continue;
			}

			if(haveQuarantinedSystem(sys))
				continue;

			check(sys);
		}

		if(best !is null)
			return creepWithFleet(fleet, best, bestProtector);

		if(!ai.behavior.remnantAllowArbitraryClear)
			return null;

		if(log)
			ai.print("Attempted to find creep camp to clear", fleet.obj);

		//Check systems in our territory
		for(uint i = 0, cnt = systems.owned.length; i < cnt; ++i) {
			SystemAI@ sys = systems.owned[i];
			if(sys.pickups.length != 0)
				check(sys);
		}

		if(best !is null)
			return creepWithFleet(fleet, best, bestProtector);

		//Check systems just outside our border
		for(uint i = 0, cnt = systems.outsideBorder.length; i < cnt; ++i) {
			SystemAI@ sys = systems.outsideBorder[i];
			if(sys.seenPresent & ai.otherMask != 0)
				continue;
			if(haveQuarantinedSystem(sys))
				continue;
			if(sys.pickups.length != 0)
				check(sys, 1.0 / double(1.0 + sys.hopDistance));
		}

		if(best !is null)
			return creepWithFleet(fleet, best, bestProtector);

		penalize(fleet.obj, 90.0);
		return null;
	}

	Object@ findRemnants(Region@ reg) {
		for(uint i = 0, cnt = quarantined.length; i < cnt; ++i) {
			auto@ qsys = quarantined[i];
			if(qsys.sys.obj !is reg)
				continue;

			for(uint n = 0, ncnt = qsys.remnants.length; n < ncnt; ++n) {
				auto@ remn = qsys.remnants[n];
				if(remn is null || !remn.valid)
					continue;
				return remn;
			}
		}
		return null;
	}

	ClearMission@ sendToClear(FleetAI@ fleet, ClearSystem@ system) {
		ClearMission miss;
		@miss.region = system.sys.obj;

		fleets.performMission(fleet, miss);
		if(log)
			ai.print("Clear remnant defenders in "+miss.region.name, fleet.obj);
		return miss;
	}

	bool isQuarantined(SystemAI@ sys) {
		if(sys.planets.length == 0)
			return false;
		for(uint i = 0, cnt = sys.planets.length; i < cnt; ++i) {
			if(!sys.planets[i].quarantined)
				return false;
		}
		return true;
	}

	bool isQuarantined(Region@ region) {
		for(uint i = 0, cnt = quarantined.length; i < cnt; ++i) {
			if(quarantined[i].sys.obj is region)
				return true;
		}
		return false;
	}

	bool haveQuarantinedSystem(SystemAI@ sys) {
		for(uint i = 0, cnt = quarantined.length; i < cnt; ++i) {
			if(quarantined[i].sys is sys)
				return true;
		}
		return false;
	}

	void recordQuarantinedSystem(SystemAI@ sys) {
		ClearSystem qsys;
		@qsys.sys = sys;
		quarantined.insertLast(qsys);

		qsys.record();
	}

	uint ownedCheck = 0;
	uint outsideCheck = 0;
	void focusTick(double time) {
		//Manage creeping check penalties
		for(uint i = 0, cnt = penalties.length; i < cnt; ++i) {
			if(penalties[i].until < gameTime) {
				penalties.removeAt(i);
				--i; --cnt;
			}
		}

		//Manage current creeping missions
		for(uint i = 0, cnt = active.length; i < cnt; ++i) {
			if(active[i].completed || active[i].canceled) {
				active.removeAt(i);
				--i; --cnt;
			}
		}

		//Find new systems that are quarantined
		if(systems.owned.length != 0) {
			ownedCheck = (ownedCheck+1) % systems.owned.length;
			auto@ sys = systems.owned[ownedCheck];
			if(sys.explored && isQuarantined(sys)) {
				if(!haveQuarantinedSystem(sys))
					recordQuarantinedSystem(sys);
			}
		}
		if(systems.outsideBorder.length != 0) {
			outsideCheck = (outsideCheck+1) % systems.outsideBorder.length;
			auto@ sys = systems.outsideBorder[outsideCheck];
			if(sys.explored && isQuarantined(sys)) {
				if(!haveQuarantinedSystem(sys))
					recordQuarantinedSystem(sys);
			}
		}

		//Update existing quarantined systems list
		for(uint i = 0, cnt = quarantined.length; i < cnt; ++i) {
			auto@ qsys = quarantined[i];
			if(!isQuarantined(qsys.sys)) {
				quarantined.removeAt(i);
				--i; --cnt;
				continue;
			}
			for(uint n = 0, ncnt = qsys.remnants.length; n < ncnt; ++n) {
				auto@ remn = qsys.remnants[n];
				if(remn is null || !remn.valid || remn.region !is qsys.sys.obj) {
					qsys.remnants.removeAt(n);
					--n; --ncnt;
				}
			}
		}

		//See if we should try to clear a quarantined system
		bool waitingForGather = false;
		if(ai.behavior.remnantAllowArbitraryClear) {
			ClearSystem@ best;
			double bestStr = INFINITY;

			for(uint i = 0, cnt = quarantined.length; i < cnt; ++i) {
				double str = quarantined[i].getStrength();
				if(quarantined[i].remnants.length == 0)
					continue;
				if(str < bestStr) {
					bestStr = str;
					@best = quarantined[i];
				}
			}

			if(best !is null) {
				double needStr = bestStr * ai.behavior.remnantOverkillFactor;
				if(fleets.getTotalStrength(FC_Combat) > needStr) {
					waitingForGather = true;
					if(fleets.getTotalStrength(FC_Combat, readyOnly=true) > needStr) {
						//Order sufficient fleets to go clear this system
						double takeStr = sqrt(needStr);
						double haveStr = 0.0;

						uint offset = randomi(0, fleets.fleets.length-1);
						for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
							FleetAI@ fleet = fleets.fleets[(i+offset)%cnt];
							if(fleet.fleetClass != FC_Combat)
								continue;
							if(!fleet.readyForAction)
								continue;

							haveStr += sqrt(fleet.strength);
							sendToClear(fleet, best);

							if(haveStr > takeStr)
								break;
						}
					}
				}
			}
		}

		//Find new fleets to creep with
		if(!waitingForGather) {
			uint offset = randomi(0, fleets.fleets.length-1);
			for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
				FleetAI@ fleet = fleets.fleets[(i+offset)%cnt];
				if(fleet.fleetClass != FC_Combat)
					continue;
				if(!fleet.readyForAction)
					continue;
				if(isPenalized(fleet.obj))
					continue;

				creepWithFleet(fleet);
				break;
			}
		}
	}
};

AIComponent@ createCreeping() {
	return Creeping();
}

Added scripts/server/empire_ai/weasel/Designs.as.




































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;

import util.design_export;
import util.random_designs;

interface RaceDesigns {
	bool preCompose(DesignTarget@ target);
	bool postCompose(DesignTarget@ target);
	bool design(DesignTarget@ target, int size, const Design@& output);
};

enum DesignPurpose {
	DP_Scout,
	DP_Combat,
	DP_Defense,
	DP_Support,
	DP_Gate,
	DP_Slipstream,
	DP_Mothership,
	DP_Miner,

	DP_COUNT,
	DP_Unknown,
};

tidy final class DesignTarget {
	int id = -1;
	const Design@ active;
	string customName;

	array<const Design@> potential;
	array<double> scores;

	uint purpose;
	double targetBuildCost = 0;
	double targetMaintenance = 0;
	double targetLaborCost = 0;

	double dps = 0.0;
	double hp = 0.0;
	double supplyDrain = 0.0;

	double targetSize = 0;
	bool findSize = false;

	Designer@ designer;

	DesignTarget() {
	}

	DesignTarget(uint type, double targetSize) {
		this.purpose = type;
		this.targetSize = targetSize;
	}

	uint get_designType() {
		switch(purpose) {
			case DP_Scout: return DT_Flagship;
			case DP_Combat: return DT_Flagship;
			case DP_Defense: return DT_Station;
			case DP_Support: return DT_Support;
			case DP_Gate: return DT_Station;
			case DP_Slipstream: return DT_Flagship;
			case DP_Mothership: return DT_Flagship;
		}
		return DT_Flagship;
	}
	
	void save(Designs& designs, SaveFile& file) {
		if(active !is null) {
			file.write1();
			file << active;
			file << dps;
			file << supplyDrain;
			file << hp;
		}
		else {
			file.write0();
		}

		file << purpose;
		file << targetBuildCost;
		file << targetMaintenance;
		file << targetLaborCost;
		file << targetSize;
		file << findSize;
		file << customName;
	}

	void load(Designs& designs, SaveFile& file) {
		if(file.readBit()) {
			file >> active;
			file >> dps;
			file >> supplyDrain;
			file >> hp;
		}

		file >> purpose;
		file >> targetBuildCost;
		file >> targetMaintenance;
		file >> targetLaborCost;
		file >> targetSize;
		file >> findSize;
		file >> customName;
	}

	void prepare(AI& ai) {
		@designer = Designer(designType, targetSize, ai.empire, compose=false);
		designer.randomHull = true;

		switch(purpose) {
			case DP_Scout:
				designer.composeScout();
			break;
			case DP_Combat:
				designer.composeFlagship();
			break;
			case DP_Defense:
				designer.composeStation();
			break;
			case DP_Support:
				designer.composeSupport();
			break;
			case DP_Gate:
				designer.composeGate();
			break;
			case DP_Slipstream:
				designer.composeSlipstream();
			break;
			case DP_Mothership:
				designer.composeMothership();
			break;
		}
	}

	double weight(double value, double goal) {
		if(value < goal)
			return sqr(value / goal);
		else if(value > goal * 1.5)
			return goal / value;
		return 1.0;
	}

	double costWeight(double value, double goal) {
		if(findSize) {
			if(value < goal)
				return 1.0;
			else
				return 0.000001;
		}
		else {
			if(value < goal)
				return goal / value;
			else
				return pow(0.2, ((value / goal) - 1.0) * 10.0);
		}
	}

	double evaluate(AI& ai, const Design& dsg) {
		double w = 1.0;

		//Try to stick as close to our target as we can
		if(targetBuildCost != 0)
			w *= costWeight(dsg.total(HV_BuildCost), targetBuildCost);
		if(targetLaborCost != 0)
			w *= costWeight(dsg.total(HV_LaborCost), targetLaborCost);
		if(targetMaintenance != 0)
			w *= costWeight(dsg.total(HV_MaintainCost), targetMaintenance);

		double predictHP = 0.0;
		double predictDPS = 0.0;
		double predictDrain = 0.0;

		//Value support capacity where appropriate
		if(purpose == DP_Combat) {
			double supCap = dsg.total(SV_SupportCapacity);
			double avgHP = 0, avgDPS = 0, avgDrain = 0.0;
			cast<Designs>(ai.designs).getSupportAverages(avgHP, avgDPS, avgDrain);

			predictHP += supCap * avgHP;
			predictDPS += supCap * avgDPS;
			predictDrain += supCap * avgDrain;
		}

		//Value combat strength where appropriate
		if(purpose != DP_Scout && purpose != DP_Slipstream && purpose != DP_Mothership) {
			predictDPS += dsg.total(SV_DPS);
			predictHP += dsg.totalHP + dsg.total(SV_ShieldCapacity);
			predictDrain += dsg.total(SV_SupplyDrain);

			if(purpose != DP_Support) {
				w *= (predictHP * predictDPS) * 0.001;

				double supplyStores = dsg.total(SV_SupplyCapacity);
				double actionTime = supplyStores / predictDrain;
				w *= weight(actionTime, ai.behavior.fleetAimSupplyDuration);
			}
		}

		//Value acceleration on a target
		if(purpose != DP_Defense && purpose != DP_Gate) {
			double targetAccel = 2.0;
			if(purpose == DP_Support)
				targetAccel *= 1.5;
			else if(purpose == DP_Scout)
				targetAccel *= 3.0;

			w *= weight(dsg.total(SV_Thrust) / max(dsg.total(HV_Mass), 0.01), targetAccel);
		}

		//Penalties for having important systems easy to shoot down
		uint holes = 0;
		for(uint i = 0, cnt = dsg.subsystemCount; i < cnt; ++i) {
			auto@ sys = dsg.subsystem(i);
			if(!sys.type.hasTag(ST_Important))
				continue;
			//TODO: We should be able to penalize for exposed supply storage
			if(sys.type.hasTag(ST_NoCore))
				continue;

			vec2u core = sys.core;
			for(uint d = 0; d < 6; ++d) {
				if(!traceContainsArmor(dsg, core, d))
					holes += 1;
			}
		}
		
		if(holes != 0)
			w /= pow(0.9, double(holes));

		//TODO: Check FTL

		return w;
	}

	bool traceContainsArmor(const Design@ dsg, const vec2u& startPos, uint direction) {
		vec2u pos = startPos;
		while(dsg.hull.active.valid(pos)) {
			if(!dsg.hull.active.advance(pos, HexGridAdjacency(direction)))
				break;

			auto@ sys = dsg.subsystem(pos.x, pos.y);
			if(sys is null)
				continue;
			if(sys.type.hasTag(ST_IsArmor))
				return true;
		}
		return false;
	}

	bool contains(const Design& dsg) {
		if(active is null)
			return false;
		if(dsg.mostUpdated() is active.mostUpdated())
			return true;
		return false;
	}

	const Design@ design(AI& ai, Designs& designs) {
		int trySize = targetSize;
		if(findSize) {
			trySize = randomd(0.75, 1.25) * targetSize;
			trySize = 5 * round(double(designer.size) / 5.0);
		}
		if(designs.race !is null) {
			const Design@ fromRace;
			if(designs.race.design(this, trySize, fromRace))
				return fromRace;
		}
		if(designer !is null) {
			designer.size = trySize;
			return designer.design(1);
		}
		return null;
	}

	void choose(AI& ai, const Design@ dsg, bool randomizeName=true) {
		set(dsg);
		@designer = null;
		findSize = false;

		string baseName = dsg.name;
		if(customName.length != 0) {
			baseName = customName;
		}
		else if(randomizeName) {
			if(dsg.hasTag(ST_IsSupport))
				baseName = autoSupportNames[randomi(0,autoSupportNames.length-1)];
			else
				baseName = autoFlagNames[randomi(0,autoFlagNames.length-1)];
		}

		string name = baseName;
		uint try = 0;
		while(ai.empire.getDesign(name) !is null) {
			name = baseName + " ";
			appendRoman(++try, name);
		}
		if(name != dsg.name)
			dsg.rename(name);

		//Set design settings/support behavior
		if(purpose == DP_Support) {
			if(dsg.total(SV_SupportSupplyCapacity) > 0.01) {
				DesignSettings settings;
				settings.behavior = SG_Brawler;
				dsg.setSettings(settings);
			}
			else if(dsg.totalHP > 50 * dsg.size) {
				DesignSettings settings;
				settings.behavior = SG_Shield;
				dsg.setSettings(settings);
			}
			else {
				DesignSettings settings;
				settings.behavior = SG_Cannon;
				dsg.setSettings(settings);
			}
		}


		ai.empire.addDesign(ai.empire.getDesignClass("AI", true), dsg);

		if(cast<Designs>(ai.designs).log)
			ai.print("Chose design for purpose "+uint(purpose)+" at size "+dsg.size);
	}

	void step(AI& ai, Designs& designs) {
		if(active is null) {
			if(designer is null) {
				if(designs.race is null || !designs.race.preCompose(this))
					prepare(ai);
				if(designs.race !is null && designs.race.postCompose(this))
					return;
			}
			if(potential.length >= ai.behavior.designEvaluateCount) {
				//Find the best design out of all our potentials
				const Design@ best;
				double bestScore = 0.0;

				for(uint i = 0, cnt = potential.length; i < cnt; ++i) {
					double w = scores[i];
					if(w > bestScore) {
						bestScore = w;
						@best = potential[i];
					}
				}
				potential.length = 0;
				scores.length = 0;

				if(best !is null)
					choose(ai, best);
			}
			else if(designer !is null && active is null) {
				//Add a new design onto the list to be evaluated
				const Design@ dsg = design(ai, designs);
				if(dsg !is null && !dsg.hasFatalErrors()) {
					potential.insertLast(dsg);
					scores.insertLast(evaluate(ai, dsg));

					/*if(designs.log)*/
					/*	ai.print("Designed for purpose "+uint(purpose)+" at size "+dsg.size+", weight "+evaluate(ai, dsg));*/
				}
			}
		}
		else {
			set(active.mostUpdated());
		}
	}

	void set(const Design@ dsg) {
		if(active is dsg)
			return;

		@active = dsg;
		targetBuildCost = dsg.total(HV_BuildCost);
		targetMaintenance = dsg.total(HV_MaintainCost);
		targetLaborCost = dsg.total(HV_LaborCost);
		targetSize = dsg.size;

		dps = dsg.total(SV_DPS);
		hp = dsg.totalHP + dsg.total(SV_ShieldCapacity);
		supplyDrain = dsg.total(SV_SupplyDrain);
	}
};

const Design@ scaleDesign(const Design@ orig, int newSize) {
	DesignDescriptor desc;
	resizeDesign(orig, newSize, desc);
	
	return makeDesign(desc);
}

final class Designs : AIComponent {
	RaceDesigns@ race;

	int nextTargetId = 0;
	array<DesignTarget@> designing;
	array<DesignTarget@> completed;
	array<DesignTarget@> automatic;

	void create() {
		@race = cast<RaceDesigns>(ai.race);
	}

	void start() {
		//Design some basic support sizes
		design(DP_Support, 1);
		design(DP_Support, 2);
		design(DP_Support, 4);
		design(DP_Support, 8);
		design(DP_Support, 16);
	}

	void save(SaveFile& file) {
		file << nextTargetId;

		uint cnt = designing.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			saveDesign(file, designing[i]);
			designing[i].save(this, file);
		}

		cnt = automatic.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			saveDesign(file, automatic[i]);
			if(!isDesigning(automatic[i])) {
				file.write1();
				automatic[i].save(this, file);
			}
			else {
				file.write0();
			}
		}

		cnt = completed.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			saveDesign(file, completed[i]);
			if(!isDesigning(completed[i])) {
				file.write1();
				completed[i].save(this, file);
			}
			else {
				file.write0();
			}
		}
	}

	bool isDesigning(DesignTarget@ targ) {
		for(uint i = 0, cnt = designing.length; i < cnt; ++i) {
			if(designing[i] is targ)
				return true;
		}
		return false;
	}

	void load(SaveFile& file) {
		file >> nextTargetId;

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ targ = loadDesign(file);
			targ.load(this, file);
			designing.insertLast(targ);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ targ = loadDesign(file);
			if(file.readBit())
				targ.load(this, file);
			automatic.insertLast(targ);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ targ = loadDesign(file);
			if(file.readBit())
				targ.load(this, file);
			completed.insertLast(targ);
		}
	}

	array<DesignTarget@> loadIds;
	DesignTarget@ loadDesign(int id) {
		if(id == -1)
			return null;
		for(uint i = 0, cnt = loadIds.length; i < cnt; ++i) {
			if(loadIds[i].id == id)
				return loadIds[i];
		}
		DesignTarget data;
		data.id = id;
		loadIds.insertLast(data);
		return data;
	}
	DesignTarget@ loadDesign(SaveFile& file) {
		int id = -1;
		file >> id;
		if(id == -1)
			return null;
		else
			return loadDesign(id);
	}
	void saveDesign(SaveFile& file, DesignTarget@ data) {
		int id = -1;
		if(data !is null)
			id = data.id;
		file << id;
	}
	void postLoad(AI& ai) {
		loadIds.length = 0;
	}

	const Design@ get_currentSupport() {
		for(int i = automatic.length - 1; i >= 0; --i) {
			if(automatic[i].purpose == DP_Support && automatic[i].active !is null)
				return automatic[i].active;
		}
		return null;
	}

	void getSupportAverages(double& hp, double& dps, double& supDrain) {
		hp = 0;
		dps = 0;
		supDrain = 0;
		uint count = 0;
		for(uint i = 0, cnt = automatic.length; i < cnt; ++i) {
			auto@ targ = automatic[i];
			if(targ.purpose != DP_Support)
				continue;
			if(targ.active is null)
				continue;

			hp += targ.hp / double(targ.targetSize);
			dps += targ.dps / double(targ.targetSize);
			supDrain += targ.supplyDrain / double(targ.targetSize);
			count += 1;
		}

		if(count == 0) {
			hp = 40.0;
			dps = 0.30;
			supDrain = 1.0;
		}
		else {
			hp /= double(count);
			dps /= double(count);
			supDrain /= double(count);
		}
	}

	DesignPurpose classify(Object@ obj) {
		if(obj is null || !obj.isShip)
			return DP_Combat;
		Ship@ ship = cast<Ship>(obj);
		return classify(ship.blueprint.design);
	}

	DesignPurpose classify(const Design@ dsg, DesignPurpose defaultPurpose = DP_Combat) {
		if(dsg is null)
			return defaultPurpose;

		for(uint i = 0, cnt = automatic.length; i < cnt; ++i) {
			if(automatic[i].contains(dsg))
				return DesignPurpose(automatic[i].purpose);
		}

		for(uint i = 0, cnt = completed.length; i < cnt; ++i) {
			if(completed[i].contains(dsg))
				return DesignPurpose(completed[i].purpose);
		}

		if(dsg.hasTag(ST_Mothership))
			return DP_Mothership;
		if(dsg.hasTag(ST_Gate))
			return DP_Gate;
		if(dsg.hasTag(ST_Slipstream))
			return DP_Slipstream;
		if(dsg.hasTag(ST_Support))
			return DP_Support;
		if(dsg.hasTag(ST_Station))
			return DP_Defense;

		double dps = dsg.total(SV_DPS);
		if(dsg.total(SV_MiningRate) > 0)
			return DP_Miner;
		if(dsg.size == 16.0 && dsg.total(SV_DPS) < 2.0)
			return DP_Scout;
		if(dps > 0.1 * dsg.size || dsg.total(SV_SupportCapacity) > 0)
			return DP_Combat;
		return defaultPurpose;
	}

	DesignTarget@ design(uint purpose, int size, int targetCost = 0, int targetMaint = 0, double targetLabor = 0, bool findSize = false) {
		for(uint i = 0, cnt = automatic.length; i < cnt; ++i) {
			auto@ target = automatic[i];
			if(target.purpose != purpose)
				continue;
			if(target.targetSize != size)
				continue;
			if(targetCost != 0 && target.targetBuildCost > targetCost)
				continue;
			if(targetMaint != 0 && target.targetMaintenance > targetMaint)
				continue;
			if(targetLabor != 0 && target.targetLaborCost > targetLabor)
				continue;
			if(target.findSize != findSize)
				continue;
			return target;
		}

		DesignTarget targ(purpose, size);
		targ.findSize = findSize;
		targ.targetBuildCost = targetCost;
		targ.targetMaintenance = targetMaint;
		targ.targetLaborCost = targetLabor;

		automatic.insertLast(targ);
		return design(targ);
	}

	DesignTarget@ design(DesignTarget@ target) {
		target.id = nextTargetId++;
		designing.insertLast(target);
		return target;
	}

	DesignTarget@ get(const Design@ dsg) {
		for(uint i = 0, cnt = automatic.length; i < cnt; ++i) {
			if(automatic[i].contains(dsg))
				return automatic[i];
		}
		return null;
	}

	DesignTarget@ scale(const Design@ dsg, int newSize) {
		if(dsg.newer !is null) {
			auto@ newTarg = get(dsg.newest());
			if(newTarg.targetSize == newSize)
				return newTarg;
			@dsg = dsg.newest();
		}

		DesignTarget@ previous = get(dsg);

		uint purpose = DP_Combat;
		if(previous !is null)
			purpose = previous.purpose;
		else
			purpose = classify(dsg);

		DesignTarget target(purpose, newSize);
		target.id = nextTargetId++;
		@target.active = scaleDesign(dsg, newSize);

		ai.empire.changeDesign(dsg, target.active, ai.empire.getDesignClass(dsg.cls.name, true));

		if(previous !is null)
			automatic.remove(previous);
		automatic.insertLast(target);

		return target;
	}

	uint chkInd = 0;
	void tick(double time) {
		if(designing.length != 0) {
			//chkInd = (chkInd+1) % designing.length;
			// Getting 1 design first is better than getting all of them later
			chkInd = 0;
			auto@ target = designing[chkInd];
			target.step(ai, this);

			if(target.active !is null) {
				designing.removeAt(chkInd);
				if(automatic.find(target) == -1)
					completed.insertLast(target);
			}
		}
	}
};

AIComponent@ createDesigns() {
	return Designs();
}

Added scripts/server/empire_ai/weasel/Development.as.
















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.Planets;
import empire_ai.weasel.Resources;
import empire_ai.weasel.Colonization;
import empire_ai.weasel.Systems;

import planet_levels;
import buildings;

import ai.consider;
from ai.buildings import Buildings, BuildingAI, RegisterForLaborUse, AsCreatedResource, BuildingUse;
from ai.resources import AIResources, ResourceAI;

interface RaceDevelopment {
	bool shouldBeFocus(Planet& pl, const ResourceType@ resource);
};

class DevelopmentFocus {
	Object@ obj;
	PlanetAI@ plAI;
	int targetLevel = 0;
	int requestedLevel = 0;
	int maximumLevel = INT_MAX;
	array<ExportData@> managedPressure;
	double weight = 1.0;

	void tick(AI& ai, Development& dev, double time) {
		if(targetLevel != requestedLevel) {
			if(targetLevel > requestedLevel) {
				int nextLevel = min(targetLevel, min(obj.resourceLevel, requestedLevel)+1);
				if(nextLevel != requestedLevel) {
					for(int i = requestedLevel+1; i <= nextLevel; ++i)
						dev.resources.organizeImports(obj, i);
					requestedLevel = nextLevel;
				}
			}
			else {
				dev.resources.organizeImports(obj, targetLevel);
				requestedLevel = targetLevel;
			}
		}

		//Remove managed pressure resources that are no longer valid
		for(uint i = 0, cnt = managedPressure.length; i < cnt; ++i) {
			ExportData@ res = managedPressure[i];
			if(res.request !is null || res.obj is null || !res.obj.valid || res.obj.owner !is ai.empire || !res.usable || res.developUse !is obj) {
				if(res.developUse is obj)
					@res.developUse = null;
				managedPressure.removeAt(i);
				--i; --cnt;
			}
		}

		//Make sure we're not exporting our resource
		if(plAI !is null && plAI.resources !is null && plAI.resources.length != 0) {
			auto@ res = plAI.resources[0];
			res.localOnly = true;
			if(res.request !is null && res.request.obj !is res.obj)
				dev.resources.breakImport(res);
		}

		//TODO: We should be able to bump managed pressure resources back to Development for
		//redistribution if we run out of pressure capacity.
	}

	void save(Development& development, SaveFile& file) {
		file << obj;
		development.planets.saveAI(file, plAI);
		file << targetLevel;
		file << requestedLevel;
		file << maximumLevel;
		file << weight;

		uint cnt = managedPressure.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			development.resources.saveExport(file, managedPressure[i]);
	}

	void load(Development& development, SaveFile& file) {
		file >> obj;
		@plAI = development.planets.loadAI(file);
		file >> targetLevel;
		file >> requestedLevel;
		file >> maximumLevel;
		file >> weight;

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = development.resources.loadExport(file);
			managedPressure.insertLast(data);
		}
	}
};

class Development : AIComponent, Buildings, ConsiderFilter, AIResources {
	RaceDevelopment@ race;
	Planets@ planets;
	Resources@ resources;
	Colonization@ colonization;
	Systems@ systems;

	array<DevelopmentFocus@> focuses;
	array<ExportData@> managedPressure;

	array<ColonizeData@> pendingFocuses;
	array<ColonizeData@> pendingResources;

	array<BuildingRequest@> genericBuilds;
	array<ExportData@> aiResources;

	double aimFTLStorage = 0.0;
	double aimResearchRate = 0.0;

	bool managePlanetPressure = true;
	bool manageAsteroidPressure = true;
	bool buildBuildings = true;
	bool colonizeResources = true;

	void create() {
		@planets = cast<Planets>(ai.planets);
		@resources = cast<Resources>(ai.resources);
		@colonization = cast<Colonization>(ai.colonization);
		@systems = cast<Systems>(ai.systems);
		@race = cast<RaceDevelopment>(ai.race);

		//Register specialized building types
		for(uint i = 0, cnt = getBuildingTypeCount(); i < cnt; ++i) {
			auto@ type = getBuildingType(i);
			for(uint n = 0, ncnt = type.ai.length; n < ncnt; ++n) {
				auto@ hook = cast<BuildingAI>(type.ai[n]);
				if(hook !is null)
					hook.register(this, type);
			}
		}
	}

	Empire@ get_empire() {
		return ai.empire;
	}

	Considerer@ get_consider() {
		return cast<Considerer>(ai.consider);
	}

	void registerUse(BuildingUse use, const BuildingType& type) {
		switch(use) {
			case BU_Factory:
				@ai.defs.Factory = type;
			break;
			case BU_LaborStorage:
				@ai.defs.LaborStorage = type;
			break;
		}
	}

	void save(SaveFile& file) {
		file << aimFTLStorage;

		uint cnt = focuses.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ focus = focuses[i];
			focus.save(this, file);
		}

		cnt = managedPressure.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			resources.saveExport(file, managedPressure[i]);

		cnt = pendingFocuses.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			colonization.saveColonize(file, pendingFocuses[i]);

		cnt = pendingResources.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			colonization.saveColonize(file, pendingResources[i]);

		cnt = genericBuilds.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			planets.saveBuildingRequest(file, genericBuilds[i]);

		cnt = aiResources.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			resources.saveExport(file, aiResources[i]);
	}

	void load(SaveFile& file) {
		file >> aimFTLStorage;

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ focus = DevelopmentFocus();
			focus.load(this, file);

			if(focus.obj !is null)
				focuses.insertLast(focus);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = resources.loadExport(file);
			if(data !is null)
				managedPressure.insertLast(data);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = colonization.loadColonize(file);
			if(data !is null)
				pendingFocuses.insertLast(data);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = colonization.loadColonize(file);
			if(data !is null)
				pendingResources.insertLast(data);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = planets.loadBuildingRequest(file);
			if(data !is null)
				genericBuilds.insertLast(data);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = resources.loadExport(file);
			if(data !is null)
				aiResources.insertLast(data);
		}
	}

	bool requestsFTLStorage() {
		double capacity = ai.empire.FTLCapacity;
		if(aimFTLStorage <= capacity)
			return false;
		if(ai.empire.FTLStored < capacity * 0.5)
			return false;
		return true;
	}

	bool requestsResearchGeneration() {
		double rate = ai.empire.ResearchRate;
		if (aimResearchRate <= rate)
			return false;
		return true;
	}

	bool isBuilding(const BuildingType& type) {
		for(uint i = 0, cnt = genericBuilds.length; i < cnt; ++i) {
			if(genericBuilds[i].type is type)
				return true;
		}
		return false;
	}

	bool isLeveling() {
		for(uint i = 0, cnt = focuses.length; i < cnt; ++i) {
			if(focuses[i].obj.resourceLevel < uint(focuses[i].targetLevel)) {
				auto@ focus = focuses[i].obj;

				//If all our requirements are resolved, then we can safely assume it will be leveled up
				bool allResolved = true;
				for(uint n = 0, ncnt = resources.requested.length; n < ncnt; ++n) {
					auto@ req = resources.requested[n];
					if(req.obj !is focus)
						continue;
					if(req.beingMet)
						continue;

					if(!req.isColonizing) {
						allResolved = false;
						break;
					}

					if(!colonization.isResolved(req)) {
						allResolved = false;
						break;
					}
				}

				if(!allResolved)
					return true;
			}
		}
		return false;
	}

	bool isBusy() {
		if(pendingFocuses.length != 0)
			return true;
		if(pendingResources.length != 0)
			return true;
		if(isLeveling())
			return true;
		return false;
	}

	bool isFocus(Object@ obj) {
		for(uint i = 0, cnt = focuses.length; i < cnt; ++i) {
			if(focuses[i].obj is obj)
				return true;
		}
		return false;
	}

	bool isManaging(ExportData@ res) {
		for(uint i = 0, cnt = managedPressure.length; i < cnt; ++i) {
			if(managedPressure[i] is res)
				return true;
		}
		for(uint i = 0, cnt = aiResources.length; i < cnt; ++i) {
			if(aiResources[i] is res)
				return true;
		}
		for(uint n = 0, ncnt = focuses.length; n < ncnt; ++n) {
			auto@ f = focuses[n];
			if(f.obj is res.obj)
				return true;
			for(uint i = 0, cnt = f.managedPressure.length; i < cnt; ++i) {
				if(f.managedPressure[i] is res)
					return true;
			}
		}
		return false;
	}

	bool isDevelopingIn(Region@ reg) {
		if(reg is null)
			return false;
		for(uint i = 0, cnt = focuses.length; i < cnt; ++i) {
			if(focuses[i].obj.region is reg)
				return true;
		}
		return false;
	}

	void start() {
		//Level up the homeworld to level 3 to start with
		for(uint i = 0, cnt = ai.empire.planetCount; i < cnt; ++i) {
			Planet@ homeworld = ai.empire.planetList[i];
			if(homeworld !is null && homeworld.valid) {
				auto@ hwFocus = addFocus(planets.register(homeworld));
				if(homeworld.nativeResourceCount >= 2 || homeworld.primaryResourceLimitLevel >= 3 || cnt == 1)
					hwFocus.targetLevel = 3;
			}
		}
	}

	double idlePenalty = 0;
	void findSomethingToDo() {
		if(idlePenalty > gameTime)
			return;

		double totalChance =
				ai.behavior.focusDevelopWeight
			+ ai.behavior.focusColonizeNewWeight * sqr(1.0 / double(focuses.length))
			+ ai.behavior.focusColonizeHighTierWeight;
		double roll = randomd(0.0, totalChance);

		//Level up one of our existing focuses
		roll -= ai.behavior.focusDevelopWeight;
		if(roll <= 0) {
			DevelopmentFocus@ levelup;
			double totalWeight = 0.0;
			for(uint i = 0, cnt = focuses.length; i < cnt; ++i) {
				auto@ f = focuses[i];
				if(f.weight == 0)
					continue;
				if(f.targetLevel >= f.maximumLevel)
					continue;
				totalWeight += f.weight;
				if(randomd() < f.weight / totalWeight)
					@levelup = f;
			}

			if(levelup !is null) {
				levelup.targetLevel += 1;
				if(log)
					ai.print("Develop chose to level this up to "+levelup.targetLevel, levelup.obj);
				return;
			}
			else {
				if(log)
					ai.print("Develop ran out of things to level up.");
			}
		}

		if(!colonizeResources)
			return;

		//Find a scalable or high tier resource to colonize and turn into a focus
		roll -= ai.behavior.focusColonizeNewWeight * sqr(1.0 / double(focuses.length));
		if(roll <= 0) {
			Planet@ newFocus;
			double w;
			double bestWeight = 0.0;

			for(uint i = 0, cnt = colonization.potentials.length; i < cnt; ++i) {
				auto@ p = colonization.potentials[i];

				if(p.resource.level < 3 && p.resource.cls !is colonization.scalableClass)
					continue;

				Region@ reg = p.pl.region;
				if(reg is null)
					continue;

				if(colonization.isColonizing(p.pl))
					continue;

				vec2i surfaceSize = p.pl.surfaceGridSize;
				int tiles = surfaceSize.width * surfaceSize.height;
				if(tiles < 144)
					continue;

				auto@ sys = systems.getAI(reg);
				w = 1.0;
				if(sys.border)
					w *= 0.25;
				if (!sys.owned && !sys.border)
					w /= 0.25;
				if(sys.obj.PlanetsMask & ~ai.mask != 0)
					w *= 0.25;
				if(p.resource.cls is colonization.scalableClass)
					w *= 10.0;

				if (w > bestWeight) {
					@newFocus = p.pl;
					bestWeight = w;
				}
			}

			if(newFocus !is null) {
				auto@ data = colonization.colonize(newFocus);
				if(data !is null)
					pendingFocuses.insertLast(data);
				if(log)
					ai.print("Colonize to become develop focus", data.target);
				return;
			}
			else {
				if(log)
					ai.print("Develop could not find a scalable or high tier resource to make a focus.");
			}
		}

		if(focuses.length == 0)
			return;

		//Find a high tier resource to import to one of our focuses
		roll -= ai.behavior.focusColonizeHighTierWeight;
		if(roll <= 0) {
			ResourceSpec spec;
			spec.type = RST_Level_Minimum;
			spec.level = 3;
			spec.isLevelRequirement = false;

			auto@ data = colonization.colonize(spec);
			if(data !is null) {
				if(log)
					ai.print("Colonize as free resource", data.target);
				pendingResources.insertLast(data);
				return;
			}
			else {
				if(log)
					ai.print("Develop could not find a high tier resource to colonize as free resource.");
			}
		}

		//Try to find a level 2 resource if everything else failed
		{
			ResourceSpec spec;
			spec.type = RST_Level_Minimum;
			spec.level = 2;
			spec.isLevelRequirement = false;

			if(colonization.shouldQueueFor(spec)) {
				auto@ data = colonization.colonize(spec);
				if(data !is null) {
					if(log)
						ai.print("Colonize as free resource", data.target);
					pendingResources.insertLast(data);
					return;
				}
				else {
					if(log)
						ai.print("Develop could not find a level 2 resource to colonize as free resource.");
				}
			}
		}

		idlePenalty = gameTime + randomd(10.0, 40.0);
	}

	uint bldIndex = 0;
	uint aiInd = 0;
	uint presInd = 0;
	uint chkInd = 0;
	void focusTick(double time) override {
		//Remove any resources we're managing that got used
		for(uint i = 0, cnt = managedPressure.length; i < cnt; ++i) {
			ExportData@ res = managedPressure[i];
			if(res.request !is null || res.obj is null || !res.obj.valid || res.obj.owner !is ai.empire || !res.usable) {
				managedPressure.removeAt(i);
				--i; --cnt;
			}
		}

		//Find new resources that we can put in our pressure manager
		uint avCnt = resources.available.length;
		if(avCnt != 0) {
			uint index = randomi(0, avCnt-1);
			for(uint i = 0, cnt = min(avCnt, 3); i < cnt; ++i) {
				uint resInd = (index+i) % avCnt;
				ExportData@ res = resources.available[resInd];
				if(res.usable && res.request is null && res.obj !is null && res.obj.valid && res.obj.owner is ai.empire && res.developUse is null) {
					if(res.resource.ai.length != 0) {
						if(!isManaging(res))
							aiResources.insertLast(res);
					}
					else if(res.resource.totalPressure > 0 && res.resource.exportable) {
						if(!managePlanetPressure && res.obj.isPlanet)
							continue;
						if(!manageAsteroidPressure && res.obj.isAsteroid)
							continue;
						if(!isManaging(res))
							managedPressure.insertLast(res);
					}
				}
			}
		}

		//Distribute managed pressure resources
		if(managedPressure.length != 0) {
			presInd = (presInd+1) % managedPressure.length;
			ExportData@ res = managedPressure[presInd];

			int pressure = res.resource.totalPressure;

			DevelopmentFocus@ onFocus;
			double bestWeight = 0;
			bool havePressure = ai.empire.HasPressure != 0.0;

			for(uint i = 0, cnt = focuses.length; i < cnt; ++i) {
				auto@ f = focuses[i];

				int cap = f.obj.pressureCap;
				if(!havePressure)
					cap = 10000;
				int cur = f.obj.totalPressure;

				if(cur + pressure > 2 * cap)
					continue;

				double w = 1.0;
				if(cur + pressure > cap)
					w *= 0.1;

				if(w > bestWeight) {
					bestWeight = w;
					@onFocus = f;
				}
			}

			if(onFocus !is null) {
				if(res.obj !is onFocus.obj)
					res.obj.exportResourceByID(res.resourceId, onFocus.obj);
				else
					res.obj.exportResourceByID(res.resourceId, null);
				@res.developUse = onFocus.obj;

				onFocus.managedPressure.insertLast(res);
				managedPressure.removeAt(presInd);

				if(log)
					ai.print("Take "+res.resource.name+" from "+res.obj.name+" for pressure", onFocus.obj);
			}
		}

		//Use generic AI distribution hooks
		if(aiResources.length != 0) {
			aiInd = (aiInd+1) % aiResources.length;
			ExportData@ res = aiResources[aiInd];
			if(res.request !is null || res.obj is null || !res.obj.valid || res.obj.owner !is ai.empire || !res.usable) {
				aiResources.removeAt(aiInd);
			}
			else {
				Object@ newTarget = res.developUse;
				if(newTarget !is null) {
					if(!newTarget.valid || newTarget.owner !is ai.empire)
						@newTarget = null;
				}

				for(uint i = 0, cnt = res.resource.ai.length; i < cnt; ++i) {
					auto@ hook = cast<ResourceAI>(res.resource.ai[i]);
					if(hook !is null)
						@newTarget = hook.distribute(this, res.resource, newTarget);
				}

				if(newTarget !is res.developUse) {
					if(res.obj !is newTarget)
						res.obj.exportResourceByID(res.resourceId, newTarget);
					else
						res.obj.exportResourceByID(res.resourceId, null);
					@res.developUse = newTarget;
				}
			}
		}

		//Deal with focuses we're colonizing
		for(uint i = 0, cnt = pendingFocuses.length; i < cnt; ++i) {
			auto@ data = pendingFocuses[i];
			if(data.completed) {
				auto@ focus = addFocus(planets.register(data.target));
				focus.targetLevel = 3;

				pendingFocuses.removeAt(i);
				--i; --cnt;
			}
			else if(data.canceled) {
				pendingFocuses.removeAt(i);
				--i; --cnt;
			}
		}

		for(uint i = 0, cnt = pendingResources.length; i < cnt; ++i) {
			auto@ data = pendingResources[i];
			if(data.completed) {
				planets.requestLevel(planets.register(data.target), data.target.primaryResourceLevel);
				pendingResources.removeAt(i);
				--i; --cnt;
			}
			else if(data.canceled) {
				pendingResources.removeAt(i);
				--i; --cnt;
			}
		}

		//If we're not currently leveling something up, find something else to do
		if(!isBusy())
			findSomethingToDo();

		//Deal with building AI hints
		for(uint i = 0, cnt = genericBuilds.length; i < cnt; ++i) {
			auto@ build = genericBuilds[i];
			if(build.canceled) {
				genericBuilds.removeAt(i);
				--i; --cnt;
			}
			else if(build.built) {
				if(build.getProgress() >= 1.f) {
					if(build.expires < gameTime) {
						genericBuilds.removeAt(i);
						--i; --cnt;
					}
				}
				else
					build.expires = gameTime + 60.0;
			}
		}
		if(buildBuildings) {
			for(uint i = 0, cnt = getBuildingTypeCount(); i < cnt; ++i) {
				bldIndex = (bldIndex+1) % cnt;

				auto@ type = getBuildingType(bldIndex);
				if(type.ai.length == 0)
					continue;

				//If we're already generically building something of this type, wait
				bool existing = false;
				for(uint n = 0, ncnt = genericBuilds.length; n < ncnt; ++n) {
					auto@ build = genericBuilds[n];
					if(build.type is type && !build.built) {
						existing = true;
						break;
					}
				}

				if(existing)
					break;

				@filterType = type;
				@consider.filter = this;

				//See if we should generically build something of this type
				for(uint n = 0, ncnt = type.ai.length; n < ncnt; ++n) {
					auto@ hook = cast<BuildingAI>(type.ai[n]);
					if(hook !is null) {
						Object@ buildOn = hook.considerBuild(this, type);
						if(buildOn !is null && buildOn.isPlanet) {
							auto@ plAI = planets.getAI(cast<Planet>(buildOn));
							if(plAI !is null) {
								if(log)
									ai.print("AI hook generically requested building of type "+type.name, buildOn);

								double priority = 1.0;
								//Resource buildings should be built as soon as possible
								if (cast<AsCreatedResource>(hook) !is null)
									priority = 2.0;

								auto@ req = planets.requestBuilding(plAI, type, priority, expire=ai.behavior.genericBuildExpire);
								if(req !is null)
									genericBuilds.insertLast(req);
								break;
							}
						}
					}
				}
				break;
			}
		}

		//Find planets we've acquired 'somehow' that have scalable resources and should be development focuses
		if(planets.planets.length != 0) {
			chkInd = (chkInd+1) % planets.planets.length;
			auto@ plAI = planets.planets[chkInd];

			if(plAI.resources.length != 0) {
				auto@ res = plAI.resources[0];
				if(res.resource.cls is colonization.scalableClass
					|| focuses.length == 0 && res.resource.level >= 2
					|| (race !is null && race.shouldBeFocus(plAI.obj, res.resource))) {
					if(!isFocus(plAI.obj)) {
						auto@ focus = addFocus(plAI);
						focus.targetLevel = max(1, res.resource.level);
					}
				}
			}
		}
	}

	DevelopmentFocus@ addFocus(PlanetAI@ plAI) {
		DevelopmentFocus focus;
		@focus.obj = plAI.obj;
		@focus.plAI = plAI;
		focus.maximumLevel = getMaxPlanetLevel(plAI.obj);

		focuses.insertLast(focus);
		return focus;
	}

	DevelopmentFocus@ getFocus(Planet& pl) {
		for(uint i = 0, cnt = focuses.length; i < cnt; ++i) {
			if(focuses[i].obj is pl)
				return focuses[i];
		}
		return null;
	}

	void tick(double time) override {
		for(uint i = 0, cnt = focuses.length; i < cnt; ++i)
			focuses[i].tick(ai, this, time);
	}

	const BuildingType@ filterType;
	bool filter(Object@ obj) {
		for(uint i = 0, cnt = genericBuilds.length; i < cnt; ++i) {
			auto@ build = genericBuilds[i];
			if(build.type is filterType && build.plAI.obj is obj)
				return false;
		}
		return true;
	}

	Planet@ getLaborAt(Territory@ territory, double&out expires) {
		if (territory is null) {
			if (log)
				ai.print("invalid territory to get labor at");
			return null;
		}
		expires = 600.0;
		const BuildingType@ type = ai.defs.Factory;
		BuildingRequest@ request = null;
		Planet@ pl = null;
		for (uint i = 0, cnt = type.ai.length; i < cnt; ++i) {
			auto@ hook = cast<RegisterForLaborUse>(type.ai[i]);
			if (hook !is null) {
				Object@ obj = hook.considerBuild(this, type, territory);
				if (obj !is null) {
					@pl = cast<Planet>(obj);
					if (pl !is null) {
						planets.requestBuilding(planets.getAI(pl), type, 2.0, expires);
						if (log)
							ai.print("requesting building " + type.name + " at " + pl.name + " to get labor at " + addrstr(territory));
						break;
					}
				}
			}
		}
		return pl;
	}
};

AIComponent@ createDevelopment() {
	return Development();
}

Added scripts/server/empire_ai/weasel/Diplomacy.as.
























































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// Diplomacy
// ---------
// Acts as an adaptor for using the generically developed DiplomacyAI.
//

import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Planets;
import empire_ai.weasel.Development;
import empire_ai.weasel.Construction;
import empire_ai.weasel.Fleets;
import empire_ai.weasel.Resources;
import empire_ai.weasel.War;
import empire_ai.weasel.Intelligence;

import influence;
from empire_ai.DiplomacyAI import DiplomacyAI, VoteData, CardAI, VoteState;

import systems;

class Diplomacy : DiplomacyAI, IAIComponent {
	Systems@ systems;
	Fleets@ fleets;
	Planets@ planets;
	Construction@ construction;
	Development@ development;
	Resources@ resources;
	War@ war;
	Intelligence@ intelligence;

	//Adapt to AI component
	AI@ ai;
	double prevFocus = 0;
	bool logCritical = false;
	bool logErrors = true;

	double getPrevFocus() { return prevFocus; }
	void setPrevFocus(double value) { prevFocus = value; }

	void setLog() { log = true; }
	void setLogCritical() { logCritical = true; }

	void set(AI& ai) { @this.ai = ai; }
	void start() {}

	void tick(double time) {}
	void turn() {}

	void postLoad(AI& ai) {}
	void postSave(AI& ai) {}
	void loadFinalize(AI& ai) {}

	//Actual AI component implementations
	void create() {
		@systems = cast<Systems>(ai.systems);
		@fleets = cast<Fleets>(ai.fleets);
		@planets = cast<Planets>(ai.planets);
		@development = cast<Development>(ai.development);
		@construction = cast<Construction>(ai.construction);
		@resources = cast<Resources>(ai.resources);
		@war = cast<War>(ai.war);
		@intelligence = cast<Intelligence>(ai.intelligence);
	}

	//IMPLEMENTED BY DiplomacyAI
	/*void save(SaveFile& file) {}*/
	/*void load(SaveFile& file) {}*/

	uint nextStep = 0;
	void focusTick(double time) {
		summarize();

		if (ai.behavior.forbidDiplomacy) return;
		switch(nextStep++ % 3) {
			case 0:
				buyCards();
			break;
			case 1:
				considerActions();
			break;
			case 2:
				considerVotes();
			break;
		}
	}

	//Adapt to diplomacy AI
	Empire@ get_empire() {
		return ai.empire;
	}

	uint get_allyMask() {
		return ai.allyMask;
	}

	int getStanding(Empire@ emp) {
		//TODO: Use relations module for this generically
		if(emp.isHostile(ai.empire))
			return -50;
		if(ai.allyMask & emp.mask != 0)
			return 100;
		return 0;
	}

	void print(const string& str) {
		ai.print(str);
	}

	Object@ considerImportantPlanets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
		double bestWeight = 0.0;
		Object@ best;

		for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) {
			Object@ obj = development.focuses[i].obj;
			double w = hook.consider(this, targets, vote, card, obj);
			if(w > bestWeight) {
				@best = obj;
				bestWeight = w;
			}
		}

		return best;
	}

	Object@ considerOwnedPlanets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
		//Consider our important ones first
		double bestWeight = 0.0;
		Object@ best;

		for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) {
			Object@ obj = development.focuses[i].obj;
			double w = hook.consider(this, targets, vote, card, obj);
			if(w > bestWeight) {
				@best = obj;
				bestWeight = w;
			}
		}

		//Consider some random other ones
		uint planetCount = planets.planets.length;
		if(planetCount != 0) {
			uint offset = randomi(0, planetCount-1);
			for(uint n = 0; n < 5; ++n) {
				Object@ obj = planets.planets[(offset+n) % planetCount].obj;
				double w = hook.consider(this, targets, vote, card, obj);
				if(w > bestWeight) {
					@best = obj;
					bestWeight = w;
				}
			}
		}

		return best;
	}

	Object@ considerImportantSystems(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
		double bestWeight = 0.0;
		Object@ best;

		for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) {
			Object@ obj = development.focuses[i].obj.region;
			if(obj is null)
				continue;
			double w = hook.consider(this, targets, vote, card, obj);
			if(w > bestWeight) {
				@best = obj;
				bestWeight = w;
			}
		}

		return best;
	}

	Object@ considerOwnedSystems(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
		//Consider our important ones first
		double bestWeight = 0.0;
		Object@ best;

		for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) {
			Object@ obj = development.focuses[i].obj.region;
			if(obj is null)
				continue;
			double w = hook.consider(this, targets, vote, card, obj);
			if(w > bestWeight) {
				@best = obj;
				bestWeight = w;
			}
		}

		//Consider some random other ones
		uint sysCount = systems.owned.length;
		if(sysCount != 0) {
			uint offset = randomi(0, sysCount-1);
			for(uint n = 0; n < 5; ++n) {
				Object@ obj = systems.owned[(offset+n) % sysCount].obj;
				double w = hook.consider(this, targets, vote, card, obj);
				if(w > bestWeight) {
					@best = obj;
					bestWeight = w;
				}
			}
		}

		return best;
	}

	Object@ considerDefendingSystems(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
		double bestWeight = 0.0;
		Object@ best;

		for(uint i = 0, cnt = war.battles.length; i < cnt; ++i) {
			auto@ battle = war.battles[i];
			Region@ sys = battle.system.obj;
			if(sys.SiegedMask & empire.mask == 0)
				continue;

			double w = hook.consider(this, targets, vote, card, sys);
			if(w > bestWeight) {
				@best = sys;
				bestWeight = w;
			}
		}

		return best;
	}

	Object@ considerDefendingPlanets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
		double bestWeight = 0.0;
		Object@ best;

		for(uint i = 0, cnt = war.battles.length; i < cnt; ++i) {
			auto@ battle = war.battles[i];
			Region@ sys = battle.system.obj;
			if(sys.SiegedMask & empire.mask == 0)
				continue;

			for(uint n = 0, ncnt = battle.system.planets.length; n < ncnt; ++n) {
				Object@ pl = battle.system.planets[n];
				if(pl.owner !is empire)
					continue;

				double w = hook.consider(this, targets, vote, card, pl);
				if(w > bestWeight) {
					@best = pl;
					bestWeight = w;
				}
			}
		}

		return best;
	}

	Object@ considerEnemySystems(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
		double bestWeight = 0.0;
		Object@ best;

		for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
			Empire@ emp = getEmpire(i);
			if(!emp.major)
				continue;
			if(!empire.isHostile(emp))
				continue;

			auto@ intel = intelligence.get(emp);
			if(intel is null)
				continue;

			for(uint n = 0, ncnt = intel.theirBorder.length; n < ncnt; ++n) {
				auto@ sysIntel = intel.theirBorder[n];

				double w = hook.consider(this, targets, vote, card, sysIntel.obj);
				if(w > bestWeight) {
					@best = sysIntel.obj;
					bestWeight = w;
				}
			}
		}

		return best;
	}

	Object@ considerEnemyPlanets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
		double bestWeight = 0.0;
		Object@ best;

		for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
			Empire@ emp = getEmpire(i);
			if(!emp.major)
				continue;
			if(!empire.isHostile(emp))
				continue;

			auto@ intel = intelligence.get(emp);
			if(intel is null)
				continue;

			for(uint n = 0, ncnt = intel.theirBorder.length; n < ncnt; ++n) {
				auto@ sysIntel = intel.theirBorder[n];

				for(uint j = 0, jcnt = sysIntel.planets.length; j < jcnt; ++j) {
					Planet@ pl = sysIntel.planets[j];
					if(pl.visibleOwnerToEmp(empire) !is emp)
						continue;

					double w = hook.consider(this, targets, vote, card, pl);
					if(w > bestWeight) {
						@best = pl;
						bestWeight = w;
					}
				}
			}
		}

		return best;
	}

	Object@ considerFleets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
		Object@ best;
		double bestWeight = 0.0;
		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			Object@ fleet = fleets.fleets[i].obj;
			if(fleet !is null) {
				double w = hook.consider(this, targets, vote, card, fleet);
				if(w > bestWeight) {
					@best = fleet;
					bestWeight = w;
				}
			}
		}
		return best;
	}

	Object@ considerEnemyFleets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
		double bestWeight = 0.0;
		Object@ best;

		for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
			Empire@ emp = getEmpire(i);
			if(!emp.major)
				continue;
			if(!empire.isHostile(emp))
				continue;

			auto@ intel = intelligence.get(emp);
			if(intel is null)
				continue;

			for(uint n = 0, ncnt = intel.fleets.length; n < ncnt; ++n) {
				auto@ flIntel = intel.fleets[n];
				if(!flIntel.known)
					continue;

				double w = hook.consider(this, targets, vote, card, flIntel.obj);
				if(w > bestWeight) {
					@best = flIntel.obj;
					bestWeight = w;
				}
			}
		}

		return best;
	}

	Object@ considerMatchingImportRequests(const CardAI& hook, Targets& targets, VoteState@ vote, const InfluenceCard@ card, const ResourceType@ type, bool considerExisting) {
		Object@ best;
		double bestWeight = 0.0;
		for(uint i = 0, cnt = resources.requested.length; i < cnt; ++i) {
			ImportData@ req = resources.requested[i];
			if(req.spec.meets(type)) {
				double w = hook.consider(this, targets, vote, card, req.obj, null);
				if(w > bestWeight) {
					bestWeight = w;
					@best = req.obj;
				}
			}
		}
		if(considerExisting) {
			for(uint i = 0, cnt = resources.used.length; i < cnt; ++i) {
				ExportData@ res = resources.used[i];
				ImportData@ req = res.request;
				if(req !is null && req.spec.meets(type)) {
					double w = hook.consider(this, targets, vote, card, req.obj, res.obj);
					if(w > bestWeight) {
						bestWeight = w;
						@best = req.obj;
					}
				}
			}
		}
		return best;
	}
};

IAIComponent@ createDiplomacy() {
	return Diplomacy();
}

Added scripts/server/empire_ai/weasel/Energy.as.




























































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// Energy
// ------
// Manage the use of energy on artifacts and other things.
//

import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.Systems;

import ai.consider;

import artifacts;
import abilities;
import systems;

from ai.artifacts import Artifacts, ArtifactConsider, ArtifactAI;

double effCostEstimate(double cost, double freeStorage) {
	double free = min(cost, freeStorage);
	cost -= free;

	double effStep = config::ENERGY_EFFICIENCY_STEP;
	double eff = 0.0;
	double step = 1.0;
	while(cost > 0) {
		eff += (cost / effStep) * step;
		cost -= effStep;
		step *= 2.0;
	}
	return eff * effStep + free;
}

class ConsiderEnergy : ArtifactConsider {
	int id = -1;
	const ArtifactType@ type;
	Artifact@ artifact;
	Ability@ ability;
	Object@ target;
	vec3d pointTarget;
	double cost = 0.0;
	double value = 0.0;

	void save(AI& ai, SaveFile& file) {
		file << id;
		file << artifact;
		file << target;
		file << cost;
		file << value;
		file << pointTarget;
	}

	void load(AI& ai, SaveFile& file) {
		file >> id;
		file >> artifact;
		file >> target;
		file >> cost;
		file >> value;
		file >> pointTarget;

		if(artifact !is null)
			init(ai, artifact);
	}

	void setTarget(Object@ obj) {
		@target = obj;
	}

	Object@ getTarget() {
		return target;
	}

	bool canTarget(Object@ obj) {
		if(ability.targets.length != 0) {
			auto@ targ = ability.targets[0];
			@targ.obj = obj;
			targ.filled = true;
			return ability.isValidTarget(0, targ);
		}
		else
			return false;
	}

	void setTargetPosition(const vec3d& point) {
		pointTarget = point;
	}

	vec3d getTargetPosition() {
		return pointTarget;
	}

	bool canTargetPosition(const vec3d& point) {
		if(ability.targets.length != 0) {
			auto@ targ = ability.targets[0];
			targ.point = point;
			targ.filled = true;
			return ability.isValidTarget(0, targ);
		}
		else
			return false;
	}

	void init(AI& ai, Artifact@ artifact) {
		@this.artifact = artifact;
		@type = getArtifactType(artifact.ArtifactType);

		if(ability is null)
			@ability = Ability();
		if(type.secondaryChance > 0 && type.abilities.length >= 2
				&& randomd() < type.secondaryChance) {
			ability.id = 1;
			@ability.type = type.abilities[1];
		}
		else {
			ability.id = 0;
			@ability.type = type.abilities[0];
		}
		ability.targets = Targets(ability.type.targets);
		@ability.obj = artifact;
		@ability.emp = ai.empire;
	}

	bool isValid(AI& ai, Energy& energy) {
		return energy.canUse(artifact);
	}

	void considerEnergy(AI& ai, Energy& energy) {
		if(type !is null && type.abilities.length != 0) {
			value = 1.0;

			for(uint i = 0, cnt = type.ai.length; i < cnt; ++i) {
				ArtifactAI@ ai;
				if(ability.id == 0)
					@ai = cast<ArtifactAI>(type.ai[i]);
				else
					@ai = cast<ArtifactAI>(type.secondaryAI[i]);
				if(ai !is null) {
					if(!ai.consider(energy, this, value)) {
						value = 0.0;
						break;
					}
				}
			}
			if(type.ai.length == 0)
				value = 0.0;

			if(ability.targets.length != 0) {
				if(ability.targets[0].type == TT_Object) {
					@ability.targets[0].obj = target;
					ability.targets[0].filled = true;

					if(target is null)
						value = 0.0;
				}
				else if(ability.targets[0].type == TT_Point) {
					ability.targets[0].point = pointTarget;
					ability.targets[0].filled = true;
				}
			}

			if(value > 0.0) {
				if(!ability.canActivate(ability.targets, ignoreCost=true)) {
					value = 0.0;
				}
				else {
					cost = ability.getEnergyCost(ability.targets);
					if(cost != 0.0) {
						//Estimate the amount of turns it would take to trigger this,
						//and devalue it based on that. This is ceiled in order to allow
						//for artifacts of similar cost to not be affected by cost differences.
						double effCost = effCostEstimate(cost, energy.freeStorage);
						double estTime = effCost / max(energy.baseIncome, 0.01);
						double turns = ceil(estTime / (3.0 * 60.0));
						value /= turns;
					}
					else {
						value *= 1000.0;
					}
				}
			}
		}
		else {
			value = 0.0;
		}
	}

	void execute(AI& ai, Energy& energy) {
		if(artifact !is null && type.abilities.length != 0) {
			if(energy.log)
				ai.print("Activate artifact "+artifact.name, artifact.region);

			if(ability.type.targets.length != 0) {
				if(ability.type.targets[0].type == TT_Object)
					artifact.activateAbilityTypeFor(ai.empire, ability.type.id, target);
				else if(ability.type.targets[0].type == TT_Point)
					artifact.activateAbilityTypeFor(ai.empire, ability.type.id, pointTarget);
			}
			else {
				artifact.activateAbilityTypeFor(ai.empire, ability.type.id);
			}
		}
	}

	int opCmp(const ConsiderEnergy@ other) const {
		if(value < other.value)
			return -1;
		if(value > other.value)
			return 1;
		return 0;
	}
};

class Energy : AIComponent, Artifacts {
	Systems@ systems;

	double baseIncome;
	double freeStorage;

	array<ConsiderEnergy@> queue;
	int nextEnergyId = 0;

	void save(SaveFile& file) {
		file << nextEnergyId;

		uint cnt = queue.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			queue[i].save(ai, file);
	}

	void load(SaveFile& file) {
		file >> nextEnergyId;

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			ConsiderEnergy c;
			c.load(ai, file);

			if(c.artifact !is null)
				queue.insertLast(c);
		}
	}

	void create() {
		@systems = cast<Systems>(ai.systems);
	}

	Considerer@ get_consider() {
		return cast<Considerer>(ai.consider);
	}

	Empire@ get_empire() {
		return ai.empire;
	}

	bool canUse(Artifact@ artifact) {
		if(artifact is null || !artifact.valid)
			return false;
		Empire@ owner = artifact.owner;
		if(owner.valid && owner !is ai.empire)
			return false;
		Region@ reg = artifact.region;
		if(reg is null)
			return false;
		if(reg.PlanetsMask != 0)
			return reg.PlanetsMask & ai.mask != 0;
		else
			return hasTradeAdjacent(ai.empire, reg);
	}

	ConsiderEnergy@ registerArtifact(Artifact@ artifact) {
		if(!canUse(artifact))
			return null;

		for(uint i = 0, cnt = queue.length; i < cnt; ++i) {
			if(queue[i].artifact is artifact)
				return queue[i];
		}

		ConsiderEnergy c;
		c.id = nextEnergyId++;
		c.init(ai, artifact);

		if(log)
			ai.print("Detect artifact "+artifact.name, artifact.region);

		queue.insertLast(c);
		return c;
	}

	uint updateIdx = 0;
	bool update() {
		if(queue.length == 0)
			return false;
		updateIdx = (updateIdx+1) % queue.length;
		auto@ c = queue[updateIdx];
		double prevValue = c.value;

		//Make sure this is still valid
		if(!c.isValid(ai, this)) {
			queue.removeAt(updateIdx);
			return false;
		}

		//Update the current target and value
		c.considerEnergy(ai, this);

		/*if(log)*/
		/*	ai.print(c.artifact.name+": consider "+c.value+" for cost "+c.cost, c.target);*/

		//Only re-sort when needed
		bool changed = false;
		if(prevValue != c.value) {
			if(updateIdx > 0) {
				if(c.value > queue[updateIdx-1].value)
					changed = true;
			}
			if(updateIdx < queue.length-1) {
				if(c.value < queue[updateIdx+1].value)
					changed = true;
			}
		}

		return changed;
	}

	uint sysIdx = 0;
	void updateSystem() {
		uint totCnt = systems.owned.length + systems.outsideBorder.length;
		if(totCnt == 0)
			return;

		sysIdx = (sysIdx+1) % totCnt;
		SystemAI@ sys;
		if(sysIdx < systems.owned.length)
			@sys = systems.owned[sysIdx];
		else
			@sys = systems.outsideBorder[sysIdx - systems.owned.length];

		for(uint i = 0, cnt = sys.artifacts.length; i < cnt; ++i)
			registerArtifact(sys.artifacts[i]);
	}

	void tick(double time) {
		if(ai.behavior.forbidArtifact) return;
		//Update current income
		baseIncome = empire.EnergyIncome;
		freeStorage = empire.FreeEnergyStorage;

		//See if we can use anything right now
		if(queue.length != 0) {
			auto@ c = queue[0];
			if(!c.isValid(ai, this)) {
				queue.removeAt(0);
			}
			else if(c.value > 0.0 && ai.empire.EnergyStored >= c.cost) {
				c.execute(ai, this);
				queue.removeAt(0);
			}
		}
	}

	void focusTick(double time) {
		if(ai.behavior.forbidArtifact) return;
		//Consider artifact usage
		bool changed = false;
		for(uint n = 0; n < min(queue.length, max(ai.behavior.artifactFocusConsiderCount, queue.length/20)); ++n) {
			if(update())
				changed = true;
		}

		//Re-sort consideration
		if(changed)
			queue.sortDesc();

		//Try to find new artifacts
		updateSystem();
	}
};

AIComponent@ createEnergy() {
	return Energy();
}

Added scripts/server/empire_ai/weasel/Events.as.












































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// Events
// ------
// Notifies subscribed components of events raised by other components.
//
import empire_ai.weasel.WeaselAI;

import ai.events;

final class Events : AIComponent {
	//Event callbacks

	private array<EventHandler@> _onOwnedSystemAdded;
	private array<EventHandler@> _onOwnedSystemRemoved;
	private array<EventHandler@> _onBorderSystemAdded;
	private array<EventHandler@> _onBorderSystemRemoved;
	private array<EventHandler@> _onOutsideBorderSystemAdded;
	private array<EventHandler@> _onOutsideBorderSystemRemoved;
	private array<EventHandler@> _onPlanetAdded;
	private array<EventHandler@> _onPlanetRemoved;
	private array<EventHandler@> _onTradeRouteNeeded;
	private array<EventHandler@> _onOrbitalRequested;

	void create() {
	}

	//Event delegate registration

	Events@ opAddAssign(IOwnedSystemEvents& events) {
		_onOwnedSystemAdded.insertLast(EventHandler(events.onOwnedSystemAdded));
		_onOwnedSystemRemoved.insertLast(EventHandler(events.onOwnedSystemRemoved));
		return this;
	}

	Events@ opAddAssign(IBorderSystemEvents& events) {
		_onBorderSystemAdded.insertLast(EventHandler(events.onBorderSystemAdded));
		_onBorderSystemRemoved.insertLast(EventHandler(events.onBorderSystemRemoved));
		return this;
	}

	Events@ opAddAssign(IOutsideBorderSystemEvents& events) {
		_onOutsideBorderSystemAdded.insertLast(EventHandler(events.onOutsideBorderSystemAdded));
		_onOutsideBorderSystemRemoved.insertLast(EventHandler(events.onOutsideBorderSystemRemoved));
		return this;
	}

	Events@ opAddAssign(IPlanetEvents& events) {
		_onPlanetAdded.insertLast(EventHandler(events.onPlanetAdded));
		_onPlanetRemoved.insertLast(EventHandler(events.onPlanetRemoved));
		return this;
	}

	Events@ opAddAssign(ITradeRouteEvents& events) {
		_onTradeRouteNeeded.insertLast(EventHandler(events.onTradeRouteNeeded));
		return this;
	}

	Events@ opAddAssign(IOrbitalRequestEvents& events) {
		_onOrbitalRequested.insertLast(EventHandler(events.onOrbitalRequested));
		return this;
	}

	//Event notifications

	private void raiseEvent(array<EventHandler@>& subscribed, ref@ sender, EventArgs& args) {
		for (uint i = 0, cnt = subscribed.length; i < cnt; ++i)
			subscribed[i](sender, args);
	}

	void notifyOwnedSystemAdded(ref@ sender, EventArgs& args) {
		raiseEvent(_onOwnedSystemAdded, sender, args);
	}

	void notifyOwnedSystemRemoved(ref@ sender, EventArgs& args) {
		raiseEvent(_onOwnedSystemRemoved, sender, args);
	}

	void notifyBorderSystemAdded(ref@ sender, EventArgs& args) {
		raiseEvent(_onBorderSystemAdded, sender, args);
	}

	void notifyBorderSystemRemoved(ref@ sender, EventArgs& args) {
		raiseEvent(_onBorderSystemRemoved, sender, args);
	}

	void notifyOutsideBorderSystemAdded(ref@ sender, EventArgs& args) {
		raiseEvent(_onOutsideBorderSystemAdded, sender, args);
	}

	void notifyOutsideBorderSystemRemoved(ref@ sender, EventArgs& args) {
		raiseEvent(_onOutsideBorderSystemRemoved, sender, args);
	}

	void notifyPlanetAdded(ref@ sender, EventArgs& args) {
		raiseEvent(_onPlanetAdded, sender, args);
	}

	void notifyPlanetRemoved(ref@ sender, EventArgs& args) {
		raiseEvent(_onPlanetRemoved, sender, args);
	}

	void notifyTradeRouteNeeded(ref@ sender, EventArgs& args) {
		raiseEvent(_onTradeRouteNeeded, sender, args);
	}

	void notifyOrbitalRequested(ref@ sender, EventArgs& args) {
		raiseEvent(_onOrbitalRequested, sender, args);
	}

	void save(SaveFile& file) {
	}

	void load(SaveFile& file) {
	}
};

AIComponent@ createEvents() {
	return Events();
}

Added scripts/server/empire_ai/weasel/Fleets.as.














































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// Fleets
// ------
// Manages data about fleets and missions, as well as making sure fleets
// return to their station after a mission.
//

import empire_ai.weasel.WeaselAI;

import empire_ai.weasel.Systems;
import empire_ai.weasel.Designs;
import empire_ai.weasel.Movement;

enum FleetClass {
	FC_Scout,
	FC_Combat,
	FC_Slipstream,
	FC_Mothership,
	FC_Defense,

	FC_ALL
};

enum MissionPriority {
	MiP_Background,
	MiP_Normal,
	MiP_High,
	MiP_Critical,
}

class Mission {
	int id = -1;
	bool completed = false;
	bool canceled = false;
	uint priority = MiP_Normal;

	void _save(Fleets& fleets, SaveFile& file) {
		file << completed;
		file << canceled;
		file << priority;
		save(fleets, file);
	}

	void _load(Fleets& fleets, SaveFile& file) {
		file >> completed;
		file >> canceled;
		file >> priority;
		load(fleets, file);
	}

	void save(Fleets& fleets, SaveFile& file) {
	}

	void load(Fleets& fleets, SaveFile& file) {
	}

	bool get_isActive() {
		return true;
	}

	double getPerformWeight(AI& ai, FleetAI& fleet) {
		return 1.0;
	}

	void start(AI& ai, FleetAI& fleet) {
	}

	void cancel(AI& ai, FleetAI& fleet) {
	}

	void tick(AI& ai, FleetAI& fleet, double time) {
	}
};

final class FleetAI {
	uint fleetClass;
	Object@ obj;
	Mission@ mission;

	Region@ stationed;
	bool stationedFactory = true;

	double filled = 0.0;
	double idleSince = 0.0;
	double fillStaticSince = 0.0;

	void save(Fleets& fleets, SaveFile& file) {
		file << fleetClass;
		file << stationed;
		file << filled;
		file << idleSince;
		file << fillStaticSince;
		file << stationedFactory;

		fleets.saveMission(file, mission);
	}

	void load(Fleets& fleets, SaveFile& file) {
		file >> fleetClass;
		file >> stationed;
		file >> filled;
		file >> idleSince;
		file >> fillStaticSince;
		file >> stationedFactory;

		@mission = fleets.loadMission(file);
	}

	bool get_isHome() {
		if(stationed is null)
			return true;
		return obj.region is stationed;
	}

	bool get_busy() {
		return mission !is null;
	}

	double get_strength() {
		return obj.getFleetStrength();
	}

	double get_supplies() {
		Ship@ ship = cast<Ship>(obj);
		if(ship is null)
			return 1.0;
		double maxSupply = ship.MaxSupply;
		if(maxSupply <= 0)
			return 1.0;
		return ship.Supply / maxSupply;
	}

	double get_remainingSupplies() {
		Ship@ ship = cast<Ship>(obj);
		if(ship is null)
			return 0.0;
		return ship.Supply;
	}

	double get_radius() {
		return obj.getFormationRadius();
	}

	double get_fleetHealth() {
		return obj.getFleetStrength() / obj.getFleetMaxStrength();
	}

	double get_flagshipHealth() {
		Ship@ ship = cast<Ship>(obj);
		if(ship is null)
			return 1.0;
		return ship.blueprint.currentHP / ship.blueprint.design.totalHP;
	}

	bool get_actionableState() {
		if(isHome && obj.hasOrderedSupports && stationedFactory)
			return false;
		if(supplies < 0.75)
			return false;
		if(filled < 0.5)
			return false;
		if(filled < 1.0 && gameTime < fillStaticSince + 90.0)
			return false;
		return true;
	}

	bool get_readyForAction() {
		if(mission !is null)
			return false;
		if(isHome && obj.hasOrderedSupports && stationedFactory)
			return false;
		if(supplies < 0.75)
			return false;
		if(filled < 0.5)
			return false;
		if(filled < 1.0 && gameTime < fillStaticSince + 90.0)
			return false;
		if(obj.isMoving) {
			if(obj.velocity.length / obj.maxAcceleration > 16.0)
				return false;
		}
		//DOF - Do not send badly damaged flagships
		Ship@ flagship = cast<Ship>(obj);
		auto@ bp = flagship.blueprint;
		if(bp.currentHP / bp.design.totalHP < 0.75)  {
				return false;
		}
		return true;
	}

	bool tick(AI& ai, Fleets& fleets, double time) {
		//Make sure we still exist
		if(!obj.valid || obj.owner !is ai.empire) {
			if(mission !is null) {
				mission.canceled = true;
				@mission = null;
			}
			return false;
		}

		//Record data
		int supUsed = obj.SupplyUsed;
		int supCap = obj.SupplyCapacity;
		int supGhost = obj.SupplyGhost;
		int supOrdered = obj.SupplyOrdered;

		double newFill = 1.0;
		if(supCap > 0.0)
			newFill = double(supUsed - supGhost - supOrdered) / double(supCap);
		if(newFill != filled) {
			fillStaticSince = gameTime;
			filled = newFill;
		}

		//Perform our mission
		if(mission !is null) {
			if(!mission.completed && !mission.canceled)
				mission.tick(ai, this, time);
			if(mission.completed || mission.canceled) {
				@mission = null;
				idleSince = gameTime;
			}
		}

		//Return to where we're stationed if we're not doing anything
		if(mission is null && stationed !is null && fleetClass != FC_Scout) {
			if(gameTime >= idleSince + ai.behavior.fleetIdleReturnStationedTime) {
				if(obj.region !is stationed && !obj.hasOrders) {
					if(fleets.log)
						ai.print("Returning to station in "+stationed.name, obj);
					fleets.movement.move(obj, stationed, spread=true);
				}
			}
		}
		return true;
	}
};

class Fleets : AIComponent {
	Systems@ systems;
	Designs@ designs;
	Movement@ movement;

	array<FleetAI@> fleets;

	int nextMissionId = 0;
	double totalStrength = 0;
	double totalMaxStrength = 0;

	void create() {
		@systems = cast<Systems>(ai.systems);
		@designs = cast<Designs>(ai.designs);
		@movement = cast<Movement>(ai.movement);
	}

	void save(SaveFile& file) {
		file << nextMissionId;
		file << totalStrength;
		file << totalMaxStrength;

		uint cnt = fleets.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			saveAI(file, fleets[i]);
			fleets[i].save(this, file);
		}
	}

	void load(SaveFile& file) {
		file >> nextMissionId;
		file >> totalStrength;
		file >> totalMaxStrength;

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			FleetAI@ flAI = loadAI(file);
			if(flAI !is null)
				flAI.load(this, file);
			else
				FleetAI().load(this, file);
		}
	}

	void saveAI(SaveFile& file, FleetAI@ flAI) {
		if(flAI is null) {
			file.write0();
			return;
		}
		file.write1();
		file << flAI.obj;
	}

	FleetAI@ loadAI(SaveFile& file) {
		if(!file.readBit())
			return null;

		Object@ obj;
		file >> obj;

		if(obj is null)
			return null;

		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			if(fleets[i].obj is obj)
				return fleets[i];
		}

		FleetAI flAI;
		@flAI.obj = obj;
		fleets.insertLast(flAI);
		return flAI;
	}

	array<Mission@> savedMissions;
	array<Mission@> loadedMissions;
	void postSave(AI& ai) {
		savedMissions.length = 0;
	}
	void postLoad(AI& ai) {
		loadedMissions.length = 0;
	}

	void saveMission(SaveFile& file, Mission@ mission) {
		if(mission is null) {
			file.write0();
			return;
		}

		file.write1();
		file << mission.id;
		if(mission.id == -1) {
			storeMission(file, mission);
		}
		else {
			bool found = false;
			for(uint i = 0, cnt = savedMissions.length; i < cnt; ++i) {
				if(savedMissions[i] is mission) {
					found = true;
					break;
				}
			}

			if(!found) {
				storeMission(file, mission);
				savedMissions.insertLast(mission);
			}
		}
	}

	Mission@ loadMission(SaveFile& file) {
		if(!file.readBit())
			return null;

		int id = 0;
		file >> id;
		if(id == -1) {
			Mission@ miss = createMission(file);
			miss.id = id;
			return miss;
		}
		else {
			for(uint i = 0, cnt = loadedMissions.length; i < cnt; ++i) {
				if(loadedMissions[i].id == id)
					return loadedMissions[i];
			}

			Mission@ miss = createMission(file);
			miss.id = id;
			loadedMissions.insertLast(miss);
			return miss;
		}
	}

	void storeMission(SaveFile& file, Mission@ mission) {
		auto@ cls = getClass(mission);
		auto@ mod = cls.module;

		file << mod.name;
		file << cls.name;
		mission._save(this, file);
	}

	Mission@ createMission(SaveFile& file) {
		string modName;
		string clsName;

		file >> modName;
		file >> clsName;

		auto@ mod = getScriptModule(modName);
		if(mod is null) {
			error("ERROR: AI Load could not find module for mission "+modName+"::"+clsName);
			return null;
		}

		auto@ cls = mod.getClass(clsName);
		if(cls is null) {
			error("ERROR: AI Load could not find class for mission "+modName+"::"+clsName);
			return null;
		}

		auto@ miss = cast<Mission>(cls.create());
		if(miss is null) {
			error("ERROR: AI Load could not create class instance for mission "+modName+"::"+clsName);
			return null;
		}

		miss._load(this, file);
		return miss;
	}

	void checkForFleets() {
		auto@ data = ai.empire.getFlagships();
		Object@ obj;
		while(receive(data, obj)) {
			if(obj !is null)
				register(obj);
		}
		@data = ai.empire.getStations();
		while(receive(data, obj)) {
			if(obj !is null)
				register(obj);
		}
	}

	bool haveCombatReadyFleets() {
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets[i];
			if(flAI.fleetClass != FC_Combat)
				continue;
			if(!flAI.readyForAction)
				continue;
			return true;
		}
		return false;
	}

	uint countCombatReadyFleets() {
		uint count = 0;
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets[i];
			if(flAI.fleetClass != FC_Combat)
				continue;
			if(!flAI.readyForAction)
				continue;
			count += 1;
		}
		return count;
	}

	bool allFleetsCombatReady() {
		bool have = false;
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets[i];
			if(flAI.fleetClass != FC_Combat)
				continue;
			if(!flAI.readyForAction)
				return false;
			have = true;
		}
		return have;
	}

	uint prevFleetCount = 0;
	double checkTimer = 0;
	void focusTick(double time) override {
		//Check for any newly obtained fleets
		uint curFleetCount = ai.empire.fleetCount;
		checkTimer += time;
		if(curFleetCount != prevFleetCount || checkTimer > 60.0) {
			checkForFleets();
			prevFleetCount = curFleetCount;
			checkTimer = 0;
		}

		//Calculate our current strengths
		totalStrength = 0;
		totalMaxStrength = 0;
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			totalStrength += sqrt(fleets[i].obj.getFleetStrength());
			totalMaxStrength += sqrt(fleets[i].obj.getFleetMaxStrength());
		}
		totalStrength = sqr(totalStrength);
		totalMaxStrength = sqr(totalMaxStrength);
	}

	double getTotalStrength(uint checkClass, bool idleOnly = false, bool readyOnly = false) {
		double str = 0.0;
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets[i];
			if((flAI.fleetClass == checkClass || checkClass == FC_ALL)
				&& (!idleOnly || flAI.mission is null)
				&& (!readyOnly || flAI.readyForAction))
				str += sqrt(fleets[i].obj.getFleetStrength());
		}
		return str*str;
	}

	void tick(double time) override {
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			if(!fleets[i].tick(ai, this, time)) {
				fleets.removeAt(i);
				--i; --cnt;
				continue;
			}

			Region@ reg = fleets[i].obj.region;
			if(reg !is null)
				systems.focus(reg);
		}
	}

	MoveOrder@ returnToBase(FleetAI@ fleet, uint priority = MP_Normal) {
		if(fleet.stationed !is null)
			return movement.move(fleet.obj, fleet.stationed, priority, spread=true);
		return null;
	}

	FleetAI@ register(Object@ obj) {
		FleetAI@ flAI = getAI(obj);

		if(flAI is null) {
			@flAI = FleetAI();
			@flAI.obj = obj;
			@flAI.stationed = obj.region;
			obj.setHoldPosition(true);

			uint designClass = designs.classify(obj);

			if(designClass == DP_Scout)
				flAI.fleetClass = FC_Scout;
			else if(designClass == DP_Slipstream)
				flAI.fleetClass = FC_Slipstream;
			else if(designClass == DP_Mothership)
				flAI.fleetClass = FC_Mothership;
			else if(designClass == DP_Defense)
				flAI.fleetClass = FC_Defense;
			else
				flAI.fleetClass = FC_Combat;

			fleets.insertLast(flAI);
		}

		return flAI;
	}

	void register(Mission@ mission) {
		if(mission.id == -1)
			mission.id = nextMissionId++;
	}

	FleetAI@ getAI(Object@ obj) {
		if(obj is null)
			return null;
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			if(fleets[i].obj is obj)
				return fleets[i];
		}
		return null;
	}

	uint count(uint checkClass) {
		uint amount = 0;
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets[i];
			if(flAI.fleetClass == checkClass || checkClass == FC_ALL)
				amount += 1;
		}
		return amount;
	}

	bool haveIdle(uint checkClass) {
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets[i];
			if((flAI.fleetClass == checkClass || checkClass == FC_ALL) && flAI.mission is null)
				return true;
		}
		return false;
	}

	double closestIdleTo(uint checkClass, const vec3d& position) {
		double closest = INFINITY;
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets[i];
			if((flAI.fleetClass != checkClass && checkClass != FC_ALL) || flAI.mission !is null)
				continue;

			double d = flAI.obj.position.distanceTo(position);
			if(d < closest)
				closest = d;
		}
		return closest;
	}

	FleetAI@ performMission(Mission@ mission) {
		FleetAI@ perform;
		double bestWeight = 0.0;
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets[i];
			if(flAI.mission !is null)
				continue;
			double w = mission.getPerformWeight(ai, flAI);
			if(w > bestWeight) {
				bestWeight = w;
				@perform = flAI;
			}
		}

		if(perform !is null) {
			@perform.mission = mission;
			register(mission);
			mission.start(ai, perform);
		}
		return perform;
	}

	FleetAI@ performMission(FleetAI@ fleet, Mission@ mission) {
		if(fleet.mission !is null) {
			fleet.mission.cancel(ai, fleet);
			fleet.mission.canceled = true;
		}
		@fleet.mission = mission;
		register(mission);
		mission.start(ai, fleet);
		return fleet;
	}
};

AIComponent@ createFleets() {
	return Fleets();
}

Added scripts/server/empire_ai/weasel/ImportData.as.




















































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import resources;
import tile_resources;
import saving;

export ResourceSpecType;
export ResourceSpec;
export implementSpec;

export ImportData;
export ExportData;

enum ResourceSpecType {
	RST_Specific,
	RST_Level_Specific,
	RST_Level_Minimum,
	RST_Pressure_Type,
	RST_Pressure_Level0,
	RST_Class,
};

tidy final class ResourceSpec : Savable {
	uint type = RST_Specific;
	const ResourceType@ resource;
	const ResourceClass@ cls;
	uint level = 0;
	uint pressureType = 0;
	bool isLevelRequirement = false;
	bool isForImport = true;
	bool allowUniversal = true;

	void save(SaveFile& file) {
		file << type;
		if(resource !is null) {
			file.write1();
			file.writeIdentifier(SI_Resource, resource.id);
		}
		else {
			file.write0();
		}
		if(cls !is null) {
			file.write1();
			file << cls.ident;
		}
		else {
			file.write0();
		}
		file << level;
		file << pressureType;
		file << isLevelRequirement;
		file << isForImport;
		file << allowUniversal;
	}

	void load(SaveFile& file) {
		file >> type;
		if(file.readBit())
			@resource = getResource(file.readIdentifier(SI_Resource));
		if(file.readBit()) {
			string clsName;
			file >> clsName;
			@cls = getResourceClass(clsName);
		}
		file >> level;
		file >> pressureType;
		file >> isLevelRequirement;
		file >> isForImport;
		file >> allowUniversal;
	}

	bool opEquals(const ResourceSpec& other) const {
		if(type != other.type)
			return false;
		if(isLevelRequirement != other.isLevelRequirement)
			return false;
		switch(type) {
			case RST_Specific:
				return other.resource is resource;
			case RST_Level_Specific:
			case RST_Level_Minimum:
				return other.level == level;
			case RST_Pressure_Type:
			case RST_Pressure_Level0:
				return other.pressureType == pressureType;
			case RST_Class:
				return other.cls is cls;
		}
		return true;
	}

	bool meets(const ResourceType@ check, Object@ fromObj = null, Object@ toObj = null) const {
		if(check is null)
			return false;
		if(allowUniversal && isLevelRequirement) {
			if(check.mode == RM_UniversalUnique || check.mode == RM_Universal) {
				//HACK: The AI shouldn't use drugs for food and water
				switch(type) {
					case RST_Level_Specific:
					case RST_Level_Minimum:
						return level >= 2;
				}
				return false;
			}
		}
		if(isForImport && !check.exportable && (fromObj is null || fromObj !is toObj))
			return false;
		if(isLevelRequirement && check.mode == RM_NonRequirement)
			return false;
		switch(type) {
			case RST_Specific:
				return check is resource;
			case RST_Level_Specific:
				return check.level == level;
			case RST_Level_Minimum:
				return check.level >= level;
			case RST_Pressure_Type:
				return check.tilePressure[pressureType] >= max(check.totalPressure * 0.4, 1.0);
			case RST_Pressure_Level0:
				return check.level == 0 && check.tilePressure[pressureType] >= max(check.totalPressure * 0.4, 1.0);
			case RST_Class:
				return check.cls is cls;
		}
		return false;
	}

	bool implements(const ResourceRequirement& req) const {
		if(!isLevelRequirement)
			return false;
		switch(req.type) {
			case RRT_Resource:
				return this.type == RST_Specific && this.resource is req.resource;
			case RRT_Class:
			case RRT_Class_Types:
				return this.type == RST_Class && this.cls is req.cls;
			case RRT_Level:
			case RRT_Level_Types:
				return this.type == RST_Level_Specific && this.level == req.level;
		}
		return false;
	}

	string dump() {
		switch(type) {
			case RST_Specific:
				return resource.name;
			case RST_Level_Specific:
				return "Tier "+level;
			case RST_Level_Minimum:
				return "Tier "+level+"+";
			case RST_Pressure_Type:
				return "Any "+getTileResourceIdent(pressureType);
			case RST_Pressure_Level0:
				return "Level 0 "+getTileResourceIdent(pressureType);
			case RST_Class:
				return "Of "+cls.ident;
		}
		return "??";
	}

	int get_resourceLevel() const {
		switch(type) {
			case RST_Specific:
				return 0;
			case RST_Level_Specific:
				return level;
			case RST_Level_Minimum:
				return level;
			case RST_Pressure_Type:
				return 0;
			case RST_Pressure_Level0:
				return 0;
			case RST_Class:
				return 0;
		}
		return 0;
	}

	int opCmp(const ResourceSpec@ other) const {
		int level = this.resourceLevel;
		int otherLevel = other.resourceLevel;
		if(level > otherLevel)
			return 1;
		if(level < otherLevel)
			return -1;
		return 0;
	}
};

ResourceSpec@ implementSpec(const ResourceRequirement& req) {
	ResourceSpec spec;
	spec.isLevelRequirement = true;

	switch(req.type) {
		case RRT_Resource:
			spec.type = RST_Specific;
			@spec.resource = req.resource;
		break;
		case RRT_Class:
		case RRT_Class_Types:
			spec.type = RST_Class;
			@spec.cls = req.cls;
		break;
		case RRT_Level:
		case RRT_Level_Types:
			spec.type = RST_Level_Specific;
			spec.level = req.level;
		break;
	}
	return spec;
}

tidy final class ImportData : Savable {
	int id = -1;
	Object@ obj;
	ResourceSpec@ spec;
	const ResourceType@ resource;
	Object@ fromObject;
	int resourceId = -1;
	bool beingMet = false;
	bool forLevel = false;
	bool cycled = false;
	bool isColonizing = false;
	bool claimedFor = false;
	double idleSince = 0.0;

	void save(SaveFile& file) {
		file << obj;
		file << spec;
		if(resource !is null) {
			file.write1();
			file.writeIdentifier(SI_Resource, resource.id);
		}
		else {
			file.write0();
		}
		file << fromObject;
		file << resourceId;
		file << beingMet;
		file << forLevel;
		file << cycled;
		file << isColonizing;
		file << claimedFor;
		file << idleSince;
	}

	void load(SaveFile& file) {
		file >> obj;
		@spec = ResourceSpec();
		file >> spec;
		if(file.readBit())
			@resource = getResource(file.readIdentifier(SI_Resource));
		file >> fromObject;
		file >> resourceId;
		file >> beingMet;
		file >> forLevel;
		file >> cycled;
		file >> isColonizing;
		file >> claimedFor;
		file >> idleSince;
	}

	void set(ExportData@ source) {
		@fromObject = source.obj;
		resourceId = source.resourceId;
		@resource = source.resource;
	}

	int opCmp(const ImportData@ other) const {
		return spec.opCmp(other.spec);
	}

	bool get_isOpen() const {
		return !beingMet;
	}
};

tidy final class ExportData : Savable {
	int id = -1;
	Object@ obj;
	const ResourceType@ resource;
	int resourceId = -1;
	ImportData@ request;
	Object@ developUse;
	bool localOnly = false;

	bool get_usable() const {
		if(obj is null)
			return false;
		if(resourceId == obj.primaryResourceId)
			return obj.primaryResourceUsable;
		else
			return obj.getNativeResourceUsableByID(resourceId);
	}

	bool get_isPrimary() const {
		return resourceId == obj.primaryResourceId;
	}

	bool isExportedTo(Object@ check) const {
		if(check is obj)
			return true;
		if(resourceId == obj.primaryResourceId)
			return obj.isPrimaryDestination(check);
		else
			return obj.getNativeResourceDestinationByID(obj.owner, resourceId) is check;
	}

	void save(SaveFile& file) {
		//Does not save the request link, this is done by Resources
		file << obj;
		if(resource !is null) {
			file.write1();
			file.writeIdentifier(SI_Resource, resource.id);
		}
		else {
			file.write0();
		}
		file << resourceId;
		file << developUse;
		file << localOnly;
	}

	void load(SaveFile& file) {
		file >> obj;
		if(file.readBit())
			@resource = getResource(file.readIdentifier(SI_Resource));
		file >> resourceId;
		file >> developUse;
		file >> localOnly;
	}
};

Added scripts/server/empire_ai/weasel/Infrastructure.as.












































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// Infrastructure
// ------
// Manages building basic structures in newly colonized or weakened systems
// to support the Military or Colonization components.
//
import empire_ai.weasel.WeaselAI;

import empire_ai.weasel.Events;
import empire_ai.weasel.Colonization;
import empire_ai.weasel.Development;
import empire_ai.weasel.Construction;
import empire_ai.weasel.Budget;
import empire_ai.weasel.Orbitals;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Planets;
import empire_ai.weasel.Resources;

import ai.construction;
import ai.events;

from ai.orbitals import RegisterForTradeUse;

from statuses import getStatusID;
from traits import getTraitID;

enum ResourcePreference {
	RP_None,
	RP_FoodWater,
	RP_Level0,
	RP_Level1,
	RP_Level2,
	RP_Level3,
	RP_Scalable,
};

enum SystemArea {
	SA_Core,
	SA_Tradable,
};

enum SystemBuildAction {
	BA_BuildOutpost,
};

enum PlanetBuildAction {
	BA_BuildMoonBase,
};

enum SystemBuildLocation {
	BL_InSystem,
	BL_AtSystemEdge,
	BL_AtBestPlanet,
};

enum FocusType {
	FT_None,
	FT_Outpost,
}

int moonBaseStatusId = -1;

final class OwnedSystemEvents : IOwnedSystemEvents {
	Infrastructure@ infrastructure;

	OwnedSystemEvents(Infrastructure& infrastructure) {
		 @this.infrastructure = infrastructure;
	}

	void onOwnedSystemAdded(ref& sender, EventArgs& args) {
		SystemAI@ ai = cast<SystemAI>(sender);
		if (ai !is null)
			infrastructure.registerOwnedSystemAdded(ai);
	}

	void onOwnedSystemRemoved(ref& sender, EventArgs& args) {
		SystemAI@ ai = cast<SystemAI>(sender);
		if (ai !is null)
			infrastructure.registerOwnedSystemRemoved(ai);
	}
};

final class OutsideBorderSystemEvents : IOutsideBorderSystemEvents {
	Infrastructure@ infrastructure;

	OutsideBorderSystemEvents(Infrastructure& infrastructure) {
		@this.infrastructure = infrastructure;
	}

	void onOutsideBorderSystemAdded(ref& sender, EventArgs& args) {
		SystemAI@ ai = cast<SystemAI>(sender);
		if (ai !is null)
			infrastructure.registerOutsideBorderSystemAdded(ai);
	}

	void onOutsideBorderSystemRemoved(ref& sender, EventArgs& args) {
		SystemAI@ ai = cast<SystemAI>(sender);
		if (ai !is null)
			infrastructure.registerOutsideBorderSystemRemoved(ai);
	}
};

final class PlanetEvents : IPlanetEvents {
	Infrastructure@ infrastructure;

	PlanetEvents(Infrastructure& infrastructure) {
		@this.infrastructure = infrastructure;
	}

	void onPlanetAdded(ref& sender, EventArgs& args) {
		PlanetAI@ ai = cast<PlanetAI>(sender);
		if (ai !is null)
			infrastructure.registerPlanetAdded(ai);
	}

	void onPlanetRemoved(ref& sender, EventArgs& args) {
		PlanetAI@ ai = cast<PlanetAI>(sender);
		if (ai !is null)
			infrastructure.registerPlanetRemoved(ai);
	}
};

final class TradeRouteEvents : ITradeRouteEvents {
	Infrastructure@ infrastructure;

	TradeRouteEvents(Infrastructure& infrastructure) {
		@this.infrastructure = infrastructure;
	}

	void onTradeRouteNeeded(ref& sender, EventArgs& args) {
		TradeRouteNeededEventArgs@ specs = cast<TradeRouteNeededEventArgs>(args);
		if (specs !is null)
			infrastructure.establishTradeRoute(specs.territoryA, specs.territoryB);
	}
}

final class OrbitalRequestEvents : IOrbitalRequestEvents {
	Infrastructure@ infrastructure;

	OrbitalRequestEvents(Infrastructure& infrastructure) {
		@this.infrastructure = infrastructure;
	}

	void onOrbitalRequested(ref& sender, EventArgs& args) {
		OrbitalRequestedEventArgs@ specs = cast<OrbitalRequestedEventArgs>(args);
		if (specs !is null)
			infrastructure.requestOrbital(specs.region, specs.module, specs.priority, specs.expires, specs.moneyType);
	}
}

final class SystemOrder {
	private IConstruction@ _construction;

	double expires = INFINITY;

	SystemOrder() {}

	SystemOrder(IConstruction@ construction) {
		@_construction = (construction);
	}

	bool get_isValid() const { return _construction !is null; }

	bool get_isInProgress() const { return _construction.started; }

	bool get_isComplete() const { return _construction.completed; }

	IConstruction@ get_info() const { return _construction; }

	void save(Infrastructure& infrastructure, SaveFile& file) {
		file << _construction.id;
		file << expires;
	}

	void load(Infrastructure& infrastructure, SaveFile& file) {
		int id = - 1;
		file >> id;
		if (id != -1) {
			for (uint i = 0, cnt = infrastructure.construction.allocations.length; i < cnt; ++i) {
				if (infrastructure.construction.allocations[i].id == id) {
					@_construction = infrastructure.construction.allocations[i];
				}
			}
		}
		file >> expires;
	}
};

final class PlanetOrder {
	private IConstruction@ _construction;

	double expires = INFINITY;

	PlanetOrder() {}

	PlanetOrder(IConstruction@ construction) {
		@_construction = construction;
	}

	bool get_isValid() const { return _construction !is null; }

	bool get_isInProgress() const { return _construction.started; }

	bool get_isComplete() const { return _construction.completed; }

	IConstruction@ get_info() const { return _construction; }

	void save(Infrastructure& infrastructure, SaveFile& file) {
		file << _construction.id;
		file << expires;
	}

	void load(Infrastructure& infrastructure, SaveFile& file) {
		int id = - 1;
		file >> id;
		if (id != -1) {
			for (uint i = 0, cnt = infrastructure.planets.constructionRequests.length; i < cnt; ++i) {
				if (infrastructure.planets.constructionRequests[i].id == id) {
					@_construction = infrastructure.planets.constructionRequests[i];
				}
			}
		}
		file >> expires;
	}
};

abstract class NextAction {
	double priority = 1.0;
	bool force = false;
	bool critical = false;
};

final class SystemAction : NextAction {
	private SystemCheck@ _sys;
	private SystemBuildAction _action;
	private SystemBuildLocation _loc;

	SystemAction(SystemCheck& sys, SystemBuildAction action, SystemBuildLocation loc) {
		@_sys = sys;
		_action = action;
		_loc = loc;
	}

	SystemCheck@ get_sys() const { return _sys; }
	SystemBuildAction get_action() const { return _action; }
	SystemBuildLocation get_loc() const { return _loc; }
};

final class PlanetAction : NextAction {
	private PlanetCheck@ _pl;
	private PlanetBuildAction _action;

	PlanetAction(PlanetCheck& pl, PlanetBuildAction action) {
		@_pl = pl;
		_action = action;
	}

	PlanetCheck@ get_pl() const { return _pl; }
	PlanetBuildAction get_action() const { return _action; }
};

abstract class Check {
	protected double _checkInTime = 0.0;

	Check() {
		_checkInTime = gameTime;
	}

	double get_checkInTime() const { return _checkInTime; }
}

namespace SystemCheck {
	array<SystemOrder@> allOrders;
}

final class SystemCheck : Check {
	SystemAI@ ai;

	array<SystemOrder@> orders;

	private double _weight = 0.0;
	private bool _isUnderAttack = false;

	SystemCheck() {}

	SystemCheck(Infrastructure& infrastructure, SystemAI& ai) {
		super();
		@this.ai = ai;
	}

	double get_weight() const { return _weight; }
	bool get_isUnderAttack() const { return _isUnderAttack; }
	bool get_isBuilding() const { return orders.length > 0; }

	void save(Infrastructure& infrastructure, SaveFile& file) {
		infrastructure.systems.saveAI(file, ai);

		uint cnt = orders.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			orders[i].save(infrastructure, file);

		file << _checkInTime;
		file << _weight;
		file << _isUnderAttack;
	}

	void load(Infrastructure& infrastructure, SaveFile& file) {
		@ai = infrastructure.systems.loadAI(file);

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ order = SystemOrder();
			order.load(infrastructure, file);
			if (order.isValid)
				addOrder(order);
		}
		file >> _checkInTime;
		file >> _weight;
		file >> _isUnderAttack;
	}

	void tick(AI& ai, Infrastructure& infrastructure, double time) {
		OrbitalAI@ orbital;
		//Update hostile status
		_isUnderAttack = this.ai.obj.ContestedMask & ai.mask != 0;

		//Cancel all orders if attacked
		/*if (isUnderAttack && isBuilding) {
			for (uint i = 0, cnt = orders.length; i < cnt; ++i) {
				auto@ order = orders[i];
				//SoI - TODO: Cancel not fully implemented, see Construction.as
				infrastructure.construction.cancel(order.info);
				removeOrder(order);
				--i; --cnt;
			}
		}*/

		if (isBuilding) {
			for (uint i = 0, cnt = orders.length; i < cnt; ++i) {
				auto@ order = orders[i];
				if (!order.isValid) {
					removeOrder(order);
					--i; --cnt;
				}
				else if (order.isComplete) {
					if (infrastructure.log)
						ai.print("order complete");
					removeOrder(order);
					--i; --cnt;
				}
				else if (!order.isInProgress && order.expires < gameTime) {
					if (infrastructure.log)
						ai.print("order expired, gameTime = " + gameTime);
					removeOrder(order);
					--i; --cnt;
				}
			}
		}
	}

	void focusTick(AI& ai, Infrastructure& infrastructure, double time) {
	}

	double check(AI& ai) {
		_weight = 0.0;
		//Systems under attack are bottom priority for now
		if (isUnderAttack)
				return weight;
		//Hostile systems are bottom priority until cleared
		if (this.ai.seenPresent & ai.enemyMask != 0)
			return weight;
		//Start weighting
		double sysWeight = 1.0;
		//Oldest systems come first
		sysWeight /= (checkInTime + 60.0) / 60.0;
		//The home system is a priority
		if (this.ai.obj is ai.empire.HomeSystem)
			sysWeight *= 2.0;

		_weight = 1.0 * sysWeight;
		return weight;
	}

	SystemOrder@ buildInSystem(Infrastructure& infrastructure, const OrbitalModule@ module, double priority = 1.0, bool force = false, double delay = 600.0, uint moneyType = BT_Infrastructure) {
		vec3d pos = ai.obj.position;
		vec2d offset = random2d(ai.obj.radius * 0.4, ai.obj.radius * 0.7);
		pos.x += offset.x;
		pos.z += offset.y;

		BuildOrbital@ orbital = infrastructure.construction.buildOrbital(module, pos, priority, force, moneyType);
		auto@ order = SystemOrder(orbital);
		order.expires = gameTime + delay;
		addOrder(order);

		return order;
	}

	SystemOrder@ buildAtSystemEdge(Infrastructure& infrastructure, const OrbitalModule@ module, double priority = 1.0, bool force = false, double delay = 600.0, uint moneyType = BT_Infrastructure) {
		vec3d pos = ai.obj.position;
		vec2d offset = random2d(ai.obj.radius * 0.8, ai.obj.radius * 0.9);
		pos.x += offset.x;
		pos.z += offset.y;

		BuildOrbital@ orbital = infrastructure.construction.buildOrbital(module, pos, priority, force, moneyType);
		auto@ order = SystemOrder(orbital);
		order.expires = gameTime + delay;
		addOrder(order);

		return order;
	}

	SystemOrder@ buildAtPlanet(Infrastructure& infrastructure, Planet& planet, const OrbitalModule@ module, double priority = 1.0, bool force = false, double delay = 600.0, uint moneyType = BT_Infrastructure) {
		BuildOrbital@ orbital = infrastructure.construction.buildLocalOrbital(module, planet, priority, force, moneyType);
		auto@ order = SystemOrder(orbital);
		order.expires = gameTime + delay;
		addOrder(order);

		return order;
	}

	void addOrder(SystemOrder@ order) {
		orders.insertLast(order);
		SystemCheck::allOrders.insertLast(order);
	}

	void removeOrder(SystemOrder@ order) {
		orders.remove(order);
		SystemCheck::allOrders.remove(order);
		@order = null;
	}
};

namespace PlanetCheck {
	array<PlanetOrder@> allOrders;
}

final class PlanetCheck : Check {
	PlanetAI@ ai;

	array<PlanetOrder@> orders;

	private double _weight = 0.0;
	private bool _isSystemUnderAttack = false;

	PlanetCheck() {}

	PlanetCheck(Infrastructure& infrastructure, PlanetAI& ai) {
		super();
		@this.ai = ai;
	}

	double get_weight() const { return _weight; }
	bool get_isSystemUnderAttack() const { return _isSystemUnderAttack; }
	bool get_isBuilding() const { return orders.length > 0; }

	void save(Infrastructure& infrastructure, SaveFile& file) {
		infrastructure.planets.saveAI(file, ai);

		uint cnt = orders.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			orders[i].save(infrastructure, file);

		file << _checkInTime;
		file << _weight;
		file << _isSystemUnderAttack;
	}

	void load(Infrastructure& infrastructure, SaveFile& file) {
		@ai = infrastructure.planets.loadAI(file);

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ order = PlanetOrder();
			order.load(infrastructure, file);
			if (order.isValid)
				addOrder(order);
		}
		file >> _checkInTime;
		file >> _weight;
		file >> _isSystemUnderAttack;
	}

	void tick(AI& ai, Infrastructure& infrastructure, double time) {
		auto@ sysAI = infrastructure.systems.getAI(this.ai.obj.region);
		if (sysAI !is null)
			_isSystemUnderAttack = sysAI.obj.ContestedMask & ai.mask != 0;

		if (isBuilding) {
			for (uint i = 0, cnt = orders.length; i < cnt; ++i) {
				auto@ order = orders[i];
				if (!order.isValid) {
					removeOrder(order);
					--i; --cnt;
				}
				else if (order.isComplete) {
					if (infrastructure.log)
						ai.print("planet order complete");
					removeOrder(order);
					--i; --cnt;
				}
				else if (!order.isInProgress && order.expires < gameTime) {
					if (infrastructure.log)
						ai.print("planet order expired, gameTime = " + gameTime);
					removeOrder(order);
					--i; --cnt;
				}
			}
		}
	}

	void focusTick(AI& ai, Infrastructure& infrastructure, double time) {
	}

	double check(AI& ai) {
		_weight = 0.0;
		//Planets in systems under attack are bottom priority for now
		if (isSystemUnderAttack)
				return _weight;
		//Start weighting
		double plWeight = 1.0;
		//Oldest planets come first
		plWeight /= (checkInTime + 60.0) / 60.0;
		//The homeworld is a priority
		if (this.ai.obj is ai.empire.Homeworld)
			plWeight *= 2.0;

		_weight = 1.0 * plWeight;
		return _weight;
	}

	PlanetOrder@ build(Infrastructure& infrastructure, const ConstructionType@ consType, double priority = 1.0, bool force = false, bool critical = false, double delay = 600.0, uint moneyType = BT_Infrastructure) {
			ConstructionRequest@ request = infrastructure.planets.requestConstruction(ai, ai.obj, consType, priority, gameTime + delay, moneyType);
			auto@ order = PlanetOrder(request);
			order.expires = gameTime + delay;
			addOrder(order);

			return order;
	}

	void addOrder(PlanetOrder@ order) {
		orders.insertLast(order);
		PlanetCheck::allOrders.insertLast(order);
	}

	void removeOrder(PlanetOrder@ order) {
		orders.remove(order);
		PlanetCheck::allOrders.remove(order);
		@order = null;
	}
};

final class TradeRoute {
	private Territory@ _territoryA;
	private Territory@ _territoryB;
	private Region@ _endpointA;
	private Region@ _endpointB;
	private SystemOrder@ _orderA;
	private SystemOrder@ _orderB;
	private bool _isEstablishing;
	private bool _isWaitingForLabor;
	private double _delay;
	private double _sleep;

	TradeRoute() {}

	TradeRoute(Territory& territoryA, Territory& territoryB) {
		@_territoryA = territoryA;
		@_territoryB = territoryB;
		_isEstablishing = false;
		_isWaitingForLabor = false;
		_delay = 0.0;
		_sleep = 0.0;
	}

	Territory@ get_territoryA() const { return _territoryA; }
	Territory@ get_territoryB() const { return _territoryB; }
	Region@ get_endpointA() const { return _endpointA; }
	Region@ get_endpointB() const { return _endpointB; }
	SystemOrder@ get_orderA() const { return _orderA; }
	SystemOrder@ get_orderB() const { return _orderB; }
	bool get_isEstablishing() const { return _isEstablishing; }
	bool get_isWaitingForLabor() const { return _isWaitingForLabor; }

	void save(Infrastructure& infrastructure, SaveFile& file) {
	}

	void load(Infrastructure& infrastructure, SaveFile& file) {
	}

	void tick(AI& ai, Infrastructure& infrastructure, double time) {
		if (_delay > 0.0 && _delay < gameTime) {
			_isWaitingForLabor = false;
			_delay = 0.0;
		}
	}

	void focusTick(AI& ai, Infrastructure& infrastructure, double time) {
	}

	bool canEstablish(Infrastructure& infrastructure, bool&out buildAtA, bool&out canBuildAtA, bool&out buildAtB, bool&out canBuildAtB) {
		//We're still sleeping
		if (_sleep > gameTime)
			return false;
		//At least one building order is still pending
		if (orderA !is null || orderB !is null)
			return false;

		buildAtA = true;
		buildAtB = true;
		canBuildAtA = false;
		canBuildAtB = false;
		for (uint i = 0, cnt = infrastructure.checkedPlanets.length; i < cnt; ++i) {
			Planet@ pl = infrastructure.checkedPlanets[i].ai.obj;
			if (pl.region !is null) {
				Territory@ t = pl.region.getTerritory(infrastructure.ai.empire);
				if (t is territoryA) {
					//Is there a global trade node here already
					if (pl.region.GateMask & ~pl.owner.mask != 0) {
						buildAtA = false;
						@_endpointA = pl.region;
					}
					if (!canBuildAtA) {
						//Is there a labor source in this territory
						if (pl.laborIncome > 0 && pl.canBuildOrbitals)
							canBuildAtA = true;
					}
				}
				else if (t is territoryB) {
					//Is there a global trade node here already
					if (pl.region.GateMask & ~pl.owner.mask != 0) {
						buildAtB = false;
						@_endpointB = pl.region;
					}
					if (!canBuildAtB) {
						//Is there a labor source in this territory
						if (pl.laborIncome > 0 && pl.canBuildOrbitals)
							canBuildAtB = true;
					}
				}
				if (!buildAtA && !buildAtB) {
					//Should not normally happen, except if trade if somehow disrupted despite global trade nodes
					return false;
				}
				if (canBuildAtA && canBuildAtB) {
					_isWaitingForLabor = false;
					return true;
				}
			}
		}
		//These checks are expensive and don't need to be run frequently, so let's sleep for some time
		_sleep = gameTime + 10.0;
		return false;
	}

	void establish(Infrastructure& infrastructure, Region@ regionA, Region@ regionB) {
		SystemOrder@ orderA = null;
		SystemOrder@ orderB = null;
		if (regionA !is null) {
			@orderA = infrastructure.requestOrbital(regionA, infrastructure.ai.defs.TradeStation);
			@_endpointA = regionA;
		}
		if (regionB !is null) {
			@orderB = infrastructure.requestOrbital(regionB, infrastructure.ai.defs.TradeStation);
			@_endpointB = regionB;
		}
		if (orderA is null || orderB is null) {
			infrastructure.ai.print("ERROR: could not establish trade route between " + regionA.name + " and " + regionB.name);
			return;
		}
		@_orderA = orderA;
		@_orderB = orderB;
		_isEstablishing = true;
	}

	void waitForLabor(double expires) {
		_isWaitingForLabor = true;
		_delay = gameTime + expires;
	}
}

final class Infrastructure : AIComponent {
	const ResourceClass@ foodClass, waterClass, scalableClass;

	//Current focus
	private uint _focus = FT_None;

	Events@ events;
	Colonization@ colonization;
	Development@ development;
	Construction@ construction;
	Orbitals@ orbitals;
	Planets@ planets;
	Systems@ systems;
	Budget@ budget;
	Resources@ resources;

	array<SystemCheck@> checkedOwnedSystems; //Includes border systems
	array<SystemCheck@> checkedOutsideSystems;
	array<PlanetCheck@> checkedPlanets;

	array<TradeRoute@> pendingRoutes;

	SystemCheck@ homeSystem;
	NextAction@ nextAction;

	//Unlock tracking
	bool canBuildGate = false;
	bool canBuildMoonBase = true;

	void create() {
		@events = cast<Events>(ai.events);
		@colonization = cast<Colonization>(ai.colonization);
		@development = cast<Development>(ai.development);
		@construction = cast<Construction>(ai.construction);
		@orbitals = cast<Orbitals>(ai.orbitals);
		@planets = cast<Planets>(ai.planets);
		@systems = cast<Systems>(ai.systems);
		@budget = cast<Budget>(ai.budget);
		@resources = cast<Resources>(ai.resources);

		//Cache expensive lookups
		@foodClass = getResourceClass("Food");
		@waterClass = getResourceClass("WaterType");
		@scalableClass = getResourceClass("Scalable");
		moonBaseStatusId = getStatusID("MoonBase");

		events += OwnedSystemEvents(this);
		events += OutsideBorderSystemEvents(this);
		events += PlanetEvents(this);
		events += TradeRouteEvents(this);

		if (ai.empire.hasTrait(getTraitID("Gate")))
			canBuildGate = true;
		if (ai.empire.hasTrait(getTraitID("StarChildren")))
			canBuildMoonBase = false;
	}

	void save(SaveFile& file) {
		file << _focus;
		uint cnt = checkedOwnedSystems.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			checkedOwnedSystems[i].save(this, file);
		cnt = checkedOutsideSystems.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			checkedOutsideSystems[i].save(this, file);
		cnt = checkedPlanets.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			checkedPlanets[i].save(this, file);
	}

	void load(SaveFile& file) {
		file >> _focus;
		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			SystemCheck@ sys = SystemCheck();
			sys.load(this, file);
			checkedOwnedSystems.insertLast(sys);
		}
		cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			SystemCheck@ sys = SystemCheck();
			sys.load(this, file);
			checkedOutsideSystems.insertLast(sys);
		}
		cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			PlanetCheck@ pl = PlanetCheck();
			pl.load(this, file);
			checkedPlanets.insertLast(pl);
		}
	}

	void start() {
	}

	void turn() {
		if(log) {
			ai.print("==============");
			ai.print("Current owned systems checked: " + checkedOwnedSystems.length);
			for (uint i = 0, cnt = checkedOwnedSystems.length; i < cnt; ++i)
				ai.print(checkedOwnedSystems[i].ai.obj.name);
			ai.print("==============");
			ai.print("Current outside border systems checked: " + checkedOutsideSystems.length);
			for (uint i = 0, cnt = checkedOutsideSystems.length; i < cnt; ++i)
				ai.print(checkedOutsideSystems[i].ai.obj.name);
			ai.print("==============");
			ai.print("Current owned planets checked: " + checkedPlanets.length);
			for (uint i = 0, cnt = checkedPlanets.length; i < cnt; ++i)
				ai.print(checkedPlanets[i].ai.obj.name);
			ai.print("==============");
		}

		//Reset any focus
		_focus = FT_None;
		//If colonization is somehow blocked, force territory expansion by focusing on building outposts
		if (colonization.needsMoreTerritory){
			if (budget.canFocus()) {
				budget.focus(BT_Infrastructure);
				_focus = FT_Outpost;
			}
		}
	}

	void tick(double time) override {
		SystemCheck@ sys;
		PlanetCheck@ pl;
		TradeRoute@ route;
		//Perform routine duties
		for (uint i = 0, cnt = checkedOwnedSystems.length; i < cnt; ++i) {
			@sys = checkedOwnedSystems[i];
			sys.tick(ai, this, time);
		}
		for (uint i = 0, cnt = checkedOutsideSystems.length; i < cnt; ++i) {
			@sys = checkedOutsideSystems[i];
			sys.tick(ai, this, time);
		}
		for (uint i = 0, cnt = checkedPlanets.length; i < cnt; ++i) {
			@pl = checkedPlanets[i];
			pl.tick(ai, this, time);
		}
		for (uint i = 0, cnt = pendingRoutes.length; i < cnt; ++i) {
			@route = pendingRoutes[i];
			route.tick(ai, this, time);
		}
	}

	void focusTick(double time) override {
		SystemCheck@ sys;
		PlanetCheck@ pl;
		SystemBuildLocation loc;

		bool critical = false;
		double w;
		double bestWeight = 0.0;

		if(ai.behavior.forbidConstruction) return;

		//Check if owned systems need anything
		for (uint i = 0, cnt = checkedOwnedSystems.length; i < cnt; ++i) {
			@sys = checkedOwnedSystems[i];
			//Only consider anything if no critical action is underway
			if (!critical) {
				//Evaluate current weight
				w = sys.check(ai);
				if (w > bestWeight) {
					if (_focus == FT_None || _focus == FT_Outpost) {
						//Check if an outpost is needed
						if (shouldHaveOutpost(sys, SA_Core, loc)) {
							@nextAction = SystemAction(sys, BA_BuildOutpost, loc);
							bestWeight = w;
							if (log)
								ai.print("outpost considered for owned system with weight: " + w, sys.ai.obj);
						}
					}
				}
			}
			//Perform routine duties
			sys.focusTick(ai, this, time);
		}
		//Check if systems in tradable area need anything
		for (uint i = 0, cnt = checkedOutsideSystems.length; i < cnt; ++i) {
			@sys = checkedOutsideSystems[i];
			//Skip unexplored systems
			if (sys.ai.explored) {
				//Only consider anything if no critical action is underway
				if (!critical) {
					//Evaluate current weight
					w = sys.check(ai);
					if (w > bestWeight) {
						if (_focus == FT_None || _focus == FT_Outpost) {
							//Check if an outpost is needed
							if (shouldHaveOutpost(sys, SA_Tradable, loc)) {
								@nextAction = SystemAction(sys, BA_BuildOutpost, loc);
								bestWeight = w;
								if (log)
									ai.print("outpost considered for outside system with weight: " + w, sys.ai.obj);
							}
						}
					}
				}
			}
			//Perform routine duties
			sys.focusTick(ai, this, time);
		}
		//Check if owned planets need anything
		for (uint i = 0, cnt = checkedPlanets.length; i < cnt; ++i) {
			@pl = checkedPlanets[i];
			//Only consider anything if no critical action is underway
			if (!critical) {
				//Planets are their own 'factory' and can only build one construction at a time
				if (!pl.isBuilding) {
					//Evaluate current weight
					w = pl.check(ai);
					if (w > bestWeight) {
						//Check if a moon base is needed
						if (canBuildMoonBase && shouldHaveMoonBase(pl)) {
							@nextAction = PlanetAction(pl, BA_BuildMoonBase);
							bestWeight = w;
							if (log)
								ai.print("moon base considered with weight: " + w, pl.ai.obj);
						}
					}
				}
			}
			//Perform routine duties
			pl.focusTick(ai, this, time);
		}
		//Execute our next action if there is one
		if (nextAction !is null) {
			Object@ obj;
			auto@ next = cast<SystemAction>(nextAction);
			if (next !is null)
			{
				@sys = next.sys;
				switch (next.action) {
					case BA_BuildOutpost:
						switch (next.loc) {
							case BL_InSystem:
								sys.buildInSystem(this, ai.defs.TradeOutpost, next.priority, next.force);
								break;
							case BL_AtSystemEdge:
								sys.buildAtSystemEdge(this, ai.defs.TradeOutpost, next.priority, next.force);
								break;
							case BL_AtBestPlanet:
									@obj = getBestPlanet(sys);
									if (obj !is null) {
										sys.buildAtPlanet(this, cast<Planet>(obj), ai.defs.TradeOutpost, next.priority, next.force);
									}
								break;
							default:
								ai.print("ERROR: undefined infrastructure building location for outpost");
						}
						if (log)
							ai.print("outpost ordered", sys.ai.obj);
						break;
					default:
						ai.print("ERROR: undefined infrastructure building action for system");
				}
			}
			else {
				auto@ next = cast<PlanetAction>(nextAction);
				if (next !is null) {
					@pl = next.pl;
					switch (next.action) {
						case BA_BuildMoonBase:
							pl.build(this, ai.defs.MoonBase, next.priority, next.force, next.critical);
							if (log)
								ai.print("moon base ordered", pl.ai.obj);
							break;
						default:
							ai.print("ERROR: undefined infrastructure building action for planet");
					}
				}
			}

			@nextAction = null;
		}

		//Manage any pending trading routes
		TradeRoute@ route;
		for (uint i = 0, cnt = pendingRoutes.length; i < cnt; ++i) {
			@route = pendingRoutes[i];

			if (route.territoryA is null || route.territoryB is null) {
				pendingRoutes.remove(route);
				--i; --cnt;
				if (log)
					ai.print("invalid territory for pending route, route canceled");
			}

			bool buildAtA = true;
			bool canBuildAtA = false;
			bool buildAtB = true;
			bool canBuildAtB = false;

			if (route.canEstablish(this, buildAtA, canBuildAtA, buildAtB, canBuildAtB)) {
				Region@ regionA = null;
				Region@ regionB = null;
				if (buildAtA)
					@regionA = getRouteEndpoint(route.territoryA);
				if (buildAtB)
					@regionB = getRouteEndpoint(route.territoryB);
				if (regionA !is null && regionB !is null) {
					route.establish(this, regionA, regionB);
					if (log)
						ai.print("trade route establishing between " + regionA.name + " and " + regionB.name);
				}
			}
			else if (!route.isEstablishing && !route.isWaitingForLabor) {
				Region@ regionA = null;
				Region@ regionB = null;
				double expires = 0.0;
				if (!canBuildAtA)
					@regionA = getLaborAt(route.territoryA, expires);
				if (!canBuildAtB)
					@regionB = getLaborAt(route.territoryB, expires);
				route.waitForLabor(expires);
				if (log) {
					string location = "";
					if (!canBuildAtA && regionA !is null)
						location += " " + regionA.name;
					if (!canBuildAtB && regionB !is null) {
						if (location != "")
							location += ", ";
						location += " " + regionB.name;
					}
					if (location == "")
						ai.print("trade route unable to get labor");
					else
						ai.print("trade route waiting for labor at:" + location);
				}
			}
			if (route.endpointA !is null && route.endpointB !is null && resources.canTradeBetween(route.endpointA, route.endpointB)) {
				pendingRoutes.remove(route);
				--i; --cnt;
				if (log)
					ai.print("trade route established between " + addrstr(route.territoryA) + " and " + addrstr(route.territoryB));
			}
			//Perform routine duties
			route.focusTick(ai, this, time);
		}
	}

	void registerOwnedSystemAdded(SystemAI& sysAI) {
		auto@ sys = SystemCheck(this, sysAI);
		checkedOwnedSystems.insertLast(sys);
		if (log)
			ai.print("adding owned system: " + sysAI.obj.name);
	}

	void registerOwnedSystemRemoved(SystemAI& sysAI) {
		for (uint i = 0, cnt = checkedOwnedSystems.length; i < cnt; ++i) {
			if (sysAI is checkedOwnedSystems[i].ai) {
				checkedOwnedSystems.removeAt(i);
				break;
			}
		}
		if (log)
			ai.print("removing owned system: " + sysAI.obj.name);
	}

	void registerOutsideBorderSystemAdded(SystemAI& sysAI) {
		auto@ sys = SystemCheck(this, sysAI);
		checkedOutsideSystems.insertLast(sys);
		if (log)
			ai.print("adding outside system: " + sysAI.obj.name);
	}

	void registerOutsideBorderSystemRemoved(SystemAI& sysAI) {
		for (uint i = 0, cnt = checkedOutsideSystems.length; i < cnt; ++i) {
			if (sysAI is checkedOutsideSystems[i].ai) {
				checkedOutsideSystems.removeAt(i);
				break;
			}
		}
		if (log)
			ai.print("removing outside system: " + sysAI.obj.name);
	}

	void registerPlanetAdded(PlanetAI& plAI) {
		auto@ pl = PlanetCheck(this, plAI);
		checkedPlanets.insertLast(pl);
		if (log)
			ai.print("adding planet: " + plAI.obj.name);
	}

	void registerPlanetRemoved(PlanetAI& plAI) {
		for (uint i = 0, cnt = checkedPlanets.length; i < cnt; ++i) {
			if (plAI is checkedPlanets[i].ai) {
				checkedPlanets.removeAt(i);
				break;
			}
		}
		if (log)
			ai.print("removing planet: " + plAI.obj.name);
	}

	void establishTradeRoute(Territory@ territoryA, Territory@ territoryB) {
		if (canBuildGate)
			return;
		if (hasPendingTradeRoute(territoryA, territoryB)) {
			if (log)
				ai.print("pending route detected between " + addrstr(territoryA) + " and " + addrstr(territoryB) + ", establishment canceled");
			return;
		}

		if (territoryA is null || territoryB is null) {
			if (log)
				ai.print("invalid territory for pending route, establishment canceled");
			return;
		}
		pendingRoutes.insertLast(TradeRoute(territoryA, territoryB));
		if (log)
			ai.print("establishing trade route between " + addrstr(territoryA) + " and " + addrstr(territoryB));
	}

	SystemOrder@ requestOrbital(Region@ region, const OrbitalModule@ module, double priority = 1.0, double expires = INFINITY, uint moneyType = BT_Infrastructure) {
		SystemAI@ sysAI = systems.getAI(region);
		if (sysAI !is null) {
			for (uint i = 0, cnt = checkedOwnedSystems.length; i < cnt; ++i) {
				if (sysAI is checkedOwnedSystems[i].ai)
					return checkedOwnedSystems[i].buildInSystem(this, module, priority, false, expires, moneyType);
			}
			ai.print("ERROR: requestOrbital: owned system not found: " + region.name);
			return null;
		}
		return null;
	}

	bool shouldHaveOutpost(SystemCheck& sys, SystemArea area, SystemBuildLocation&out loc) const {
		loc = BL_InSystem;

		uint presentMask = sys.ai.seenPresent;
		//Make sure we did not previously built an outpost here
		if (orbitals.haveInSystem(ai.defs.TradeOutpost, sys.ai.obj))
			return false;
		//Make sure we are not already building an outpost here
		if (isBuilding(sys, ai.defs.TradeOutpost))
			return false;
		//Hostile systems should be ignored until cleared
		if (presentMask & ai.enemyMask != 0)
			return false;
		//Inhabited systems should be ignored if we're not aggressively expanding
		if(!ai.behavior.colonizeNeutralOwnedSystems && (presentMask & ai.neutralMask) != 0)
			return false;
		if(!ai.behavior.colonizeAllySystems && (presentMask & ai.allyMask) != 0)
			return false;
		else {
			Planet@ planet;
			ResourceType@ type;

			switch(area) {
				//Owned systems should have an outpost
				case SA_Core:
					if (sys.ai.planets.length > 0)
						loc = BL_AtBestPlanet;
					return true;
				//Outside systems might have an outpost if they are of some interest
				case SA_Tradable:
					@planet = getBestPlanet(sys, type);
					if (planet is null)
						break;
					loc = BL_AtBestPlanet;
					//The best planet is barren, the system needs an outpost to allow expansion
					if (int(planet.primaryResourceType) == -1)
						return true;
					//The best planet has either a scalable or level 3 or 2 resource, the system should have an outpost to dissuade other empires from colonizing it
					if (type !is null && (type.cls is scalableClass || type.level == 3 || type.level == 2))
						return true;
					return false;
				default:
					return false;
			}
		}
		return false;
	}

	bool shouldHaveMoonBase(PlanetCheck& pl) const {
		if (pl.ai.obj.moonCount == 0)
			return false;
		//If the planet is at least level 2 and short on empty developed tiles, it should have a moon base
		else if (pl.ai.obj.resourceLevel > 1 && pl.ai.obj.emptyDevelopedTiles < 9)
			return true;

		return false;
	}

	Region@ getRouteEndpoint(Territory@ territory) {
		const OrbitalModule@ module = ai.defs.TradeStation;
		Region@ region = null;
		for (uint i = 0, cnt = module.ai.length; i < cnt; ++i) {
			auto@ hook = cast<RegisterForTradeUse>(module.ai[i]);
			if (hook !is null) {
				Object@ obj = hook.considerBuild(orbitals, module, territory);
				if (obj !is null) {
					@region = cast<Region>(obj);
					break;
				}
			}
		}
		return region;
	}

	Region@ getLaborAt(Territory@ territory, double&out expires) {
		expires = 600.0;

		if (territory is null) {
			if (log)
				ai.print("invalid territory to get labor at");
			return null;
		}
		//SoI - TODO: Handle more complex cases

		//Fallback solution: build a labor generation building
		Planet@ pl = development.getLaborAt(territory, expires);
		if (pl !is null)
			return pl.region;
		return null;
	}

	bool isBuilding(const OrbitalModule@ module) {
		for (uint i = 0, cnt = SystemCheck::allOrders.length; i < cnt; ++i) {
			auto@ orbital = cast<IOrbitalConstruction>(SystemCheck::allOrders[i].info);
			if (orbital !is null) {
				if (orbital.module is module)
					return true;
			}
		}
		return false;
	}

	bool isBuilding(SystemCheck& sys, const OrbitalModule@ module) {
		for (uint i = 0, cnt = sys.orders.length; i < cnt; ++i) {
			auto@ orbital = cast<IOrbitalConstruction>(sys.orders[i].info);
			if (orbital !is null) {
				if (orbital.module is module)
					return true;
			}
		}
		return false;
	}

	bool isBuilding(const ConstructionType@ consType) {
		for (uint i = 0, cnt = PlanetCheck::allOrders.length; i < cnt; ++i) {
			auto@ generic = cast<IGenericConstruction>(PlanetCheck::allOrders[i].info);
			if (generic !is null) {
				if (generic.construction is consType)
					return true;
			}
		}
		return false;
	}

	bool isBuilding(PlanetCheck& pl, const ConstructionType@ consType) {
		for (uint i = 0, cnt = pl.orders.length; i < cnt; ++i) {
			auto@ generic = cast<IGenericConstruction>(pl.orders[i].info);
			if (generic !is null) {
				if (generic.construction is consType)
					return true;
			}
		}
		return false;
	}

	bool hasPendingTradeRoute(Territory@ territoryA, Territory@ territoryB) {
		for (uint i = 0, cnt = pendingRoutes.length; i < cnt; ++i) {
			if (pendingRoutes[i].territoryA is territoryA && pendingRoutes[i].territoryB is territoryB)
				return true;
		}
		return false;
	}

	Planet@ getBestPlanet(SystemCheck sys) {
		ResourceType@ type;
		return getBestPlanet(sys, type);
	}

	Planet@ getBestPlanet(SystemCheck sys, const ResourceType@ resourceType) {
		Planet@ bestPlanet, planet;
		ResourcePreference bestResource = RP_None;

		if (sys.ai.obj is ai.empire.HomeSystem) {
			//The homeworld if there is one
			@planet = ai.empire.Homeworld;
			if (planet !is null)
				return planet;
		}

		for (uint i = 0, cnt = sys.ai.planets.length; i < cnt; ++i) {
			@planet = sys.ai.planets[i];
			int resId = planet.primaryResourceType;
			if (resId == -1)
				continue;

			const ResourceType@ type = getResource(resId);
			//The first scalable resource
			if (type.cls is scalableClass) {
				@resourceType = type;
				return planet;
			}
			//The first level 3 resource
			if (type.level == 3) {
				bestResource = RP_Level3;
				@resourceType = type;
				@bestPlanet = planet;
			}
			//The first level 2 resource
			else if (type.level == 2 && RP_Level2 > bestResource) {
				bestResource = RP_Level2;
				@resourceType = type;
				@bestPlanet = planet;
			}
			//The first level 1 resource
			else if (type.level == 1 && RP_Level1 > bestResource) {
				bestResource = RP_Level1;
				@resourceType = type;
				@bestPlanet = planet;
			}
			//The first level 0 resource except food and water
			else if (type.level == 0 && type.cls !is foodClass && type.cls !is waterClass && RP_Level0 > bestResource) {
				bestResource = RP_Level0;
				@resourceType = type;
				@bestPlanet = planet;
			}
			//The first food or water resource
			else if ((type.cls is foodClass || type.cls is waterClass) && RP_Level0 > bestResource) {
				bestResource = RP_FoodWater;
				@resourceType = type;
				@bestPlanet = planet;
			}
			else if (i == sys.ai.planets.length - 1 && bestPlanet is null) {
				@resourceType = type;
				@bestPlanet = planet;
			}
		}

		if (bestPlanet is null)
			return planet;
		return bestPlanet;
	}
};

AIComponent@ createInfrastructure() {
	return Infrastructure();
}

Added scripts/server/empire_ai/weasel/Intelligence.as.




























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// Intelligence
// ------------
// Keeps track of the existence and movement of enemy fleets and other assets.
//

import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.Fleets;
import empire_ai.weasel.Systems;

import regions.regions;

final class FleetIntel {
	Object@ obj;
	bool known = false;
	bool visible = false;
	double lastSeen = 0;
	double seenStrength = 0;
	double predictStrength = 0;

	vec3d seenPosition;
	Region@ seenRegion;
	vec3d seenDestination;
	Region@ seenTarget;

	void save(SaveFile& file) {
		file << obj;
		file << known;
		file << visible;
		file << lastSeen;
		file << seenStrength;
		file << predictStrength;
		file << seenPosition;
		file << seenRegion;
		file << seenDestination;
		file << seenTarget;
	}

	void load(SaveFile& file) {
		file >> obj;
		file >> known;
		file >> visible;
		file >> lastSeen;
		file >> seenStrength;
		file >> predictStrength;
		file >> seenPosition;
		file >> seenRegion;
		file >> seenDestination;
		file >> seenTarget;
	}

	bool get_isSignificant() {
		return obj.getFleetStrength() > 0.1;
	}

	bool tick(AI& ai, Intelligence& intelligence, Intel& intel) {
		if(visible) {
			if(!obj.valid || obj.owner !is intel.empire)
				return false;
		}
		else {
			if(!obj.valid || obj.owner !is intel.empire) {
				if(!known || lastSeen < gameTime - 300.0)
					return false;
			}
		}
		if(obj.isVisibleTo(ai.empire)) {
			known = true;
			visible = true;
			lastSeen = gameTime;

			seenStrength = obj.getFleetStrength();
			predictStrength = obj.getFleetMaxStrength();
			int supCap = obj.SupplyCapacity;
			double fillPct = 1.0;
			if(supCap != 0) {
				double fillPct = double(obj.SupplyUsed) / double(supCap);
				if(fillPct > 0.5)
					predictStrength /= fillPct;
				else
					predictStrength *= 2.0;
			}

			seenPosition = obj.position;
			@seenRegion = obj.region;

			if(obj.isMoving) {
				seenDestination = obj.computedDestination;
				if(seenRegion !is null && inRegion(seenRegion, seenDestination))
					@seenTarget = seenRegion;
				else if(seenTarget !is null && inRegion(seenTarget, seenDestination))
					@seenTarget = seenTarget;
				else
					@seenTarget = getRegion(seenDestination);
			}
			else {
				seenDestination = seenPosition;
				@seenTarget = seenRegion;
			}
		}
		else {
			visible = false;
		}
		return true;
	}
};

final class Intel {
	Empire@ empire;
	uint borderedTo = 0;

	array<FleetIntel@> fleets;
	array<SystemAI@> shared;
	array<SystemAI@> theirBorder;
	array<SystemAI@> theirOwned;

	void save(Intelligence& intelligence, SaveFile& file) {
		uint cnt = fleets.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			fleets[i].save(file);

		cnt = shared.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			intelligence.systems.saveAI(file, shared[i]);

		cnt = theirBorder.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			intelligence.systems.saveAI(file, theirBorder[i]);

		cnt = theirOwned.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			intelligence.systems.saveAI(file, theirOwned[i]);

		file << borderedTo;
	}

	void load(Intelligence& intelligence, SaveFile& file) {
		uint cnt = 0;

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			FleetIntel flIntel;
			flIntel.load(file);
			if(flIntel.obj !is null)
				fleets.insertLast(flIntel);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ sys = intelligence.systems.loadAI(file);
			if(sys !is null)
				shared.insertLast(sys);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ sys = intelligence.systems.loadAI(file);
			if(sys !is null)
				theirBorder.insertLast(sys);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ sys = intelligence.systems.loadAI(file);
			if(sys !is null)
				theirOwned.insertLast(sys);
		}

		file >> borderedTo;
	}

	//TODO: If a fleet is going to drop out of cutoff range soon,
	// queue up a scouting mission to its last known position so we
	// can try to regain intel on it.

	double getSeenStrength(double cutOff = 600.0) {
		double total = 0.0;
		cutOff = gameTime - cutOff;
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			auto@ flIntel = fleets[i];
			if(!flIntel.known)
				continue;
			if(flIntel.lastSeen < cutOff)
				continue;
			total += sqrt(fleets[i].seenStrength);
		}
		return total * total;
	}

	double getPredictiveStrength(double cutOff = 600.0) {
		double total = 0.0;
		cutOff = gameTime - cutOff;
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			auto@ flIntel = fleets[i];
			if(!flIntel.known)
				continue;
			if(flIntel.lastSeen < cutOff)
				continue;
			total += sqrt(fleets[i].predictStrength);
		}
		return total * total;
	}

	double accuracy(AI& ai, Intelligence& intelligence, double cutOff = 600.0) {
		uint total = 0;
		uint known = 0;

		cutOff = gameTime - cutOff;
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			auto@ flIntel = fleets[i];
			if(!flIntel.isSignificant)
				continue;

			total += 1;
			if(flIntel.known && flIntel.lastSeen >= cutOff)
				known += 1;
		}

		if(total == 0)
			return 1.0;
		return double(known) / double(total);
	}

	double defeatability(AI& ai, Intelligence& intelligence, double cutOff = 600.0) {
		double acc = accuracy(ai, intelligence, cutOff);
		double ourStrength = 0, theirStrength = 0;

		if(acc < 0.6) {
			//In low-accuracy situations, base it on the empire overall strength metric
			theirStrength = empire.TotalMilitary;
			ourStrength = ai.empire.TotalMilitary;
		}
		else {
			theirStrength = getPredictiveStrength(cutOff * 10.0);
			ourStrength = intelligence.fleets.totalStrength;
		}

		if(theirStrength == 0)
			return 10.0;
		return ourStrength / theirStrength;
	}

	void tick(AI& ai, Intelligence& intelligence) {
		//Keep known fleets updated
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			if(!fleets[i].tick(ai, intelligence, this)) {
				fleets.removeAt(i);
				--i; --cnt;
			}
		}
	}

	bool isShared(AI& ai, SystemAI@ sys) {
		return sys.seenPresent & ai.empire.mask != 0 && sys.seenPresent & empire.mask != 0;
	}

	bool isBorder(AI& ai, SystemAI@ sys) {
		return sys.outsideBorder && sys.seenPresent & empire.mask != 0;
	}

	void focusTick(AI& ai, Intelligence& intelligence) {
		//Detect newly created fleets
		auto@ data = empire.getFlagships();
		Object@ obj;
		while(receive(data, obj)) {
			if(obj is null)
				continue;

			bool found = false;
			for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
				if(fleets[i].obj is obj) {
					found = true;
					break;
				}
			}

			if(!found) {
				FleetIntel flIntel;
				@flIntel.obj = obj;
				fleets.insertLast(flIntel);
			}
		}

		//Remove no longer shared and border systems
		for(uint i = 0, cnt = shared.length; i < cnt; ++i) {
			if(!isShared(ai, shared[i])) {
				shared.removeAt(i);
				--i; --cnt;
			}
		}
		for(uint i = 0, cnt = theirBorder.length; i < cnt; ++i) {
			if(!isBorder(ai, theirBorder[i])) {
				theirBorder.removeAt(i);
				--i; --cnt;
			}
		}

		borderedTo = 0;
		for(uint i = 0, cnt = theirOwned.length; i < cnt; ++i) {
			auto@ sys = theirOwned[i];
			uint seen = sys.seenPresent;
			if(seen & empire.mask == 0) {
				theirOwned.removeAt(i);
				--i; --cnt;
				continue;
			}

			for(uint n = 0, ncnt = sys.desc.adjacent.length; n < ncnt; ++n) {
				auto@ other = intelligence.systems.getAI(sys.desc.adjacent[n]);
				if(other !is null)
					borderedTo |= other.seenPresent & ~empire.mask;
			}

			for(uint n = 0, ncnt = sys.desc.wormholes.length; n < ncnt; ++n) {
				auto@ other = intelligence.systems.getAI(sys.desc.wormholes[n]);
				if(other !is null)
					borderedTo |= other.seenPresent & ~empire.mask;
			}
		}

		//Detect shared and border systems
		for(uint i = 0, cnt = intelligence.systems.owned.length; i < cnt; ++i) {
			auto@ sys = intelligence.systems.owned[i];
			if(isShared(ai, sys)) {
				if(shared.find(sys) == -1)
					shared.insertLast(sys);
			}
		}
		for(uint i = 0, cnt = intelligence.systems.outsideBorder.length; i < cnt; ++i) {
			auto@ sys = intelligence.systems.outsideBorder[i];
			if(isBorder(ai, sys)) {
				if(theirBorder.find(sys) == -1)
					theirBorder.insertLast(sys);
			}
		}
		for(uint i = 0, cnt = intelligence.systems.all.length; i < cnt; ++i) {
			auto@ sys = intelligence.systems.all[i];
			if(sys.seenPresent & empire.mask != 0) {
				if(theirOwned.find(sys) == -1)
					theirOwned.insertLast(sys);
			}
		}
	
		//Try to update some stuff
		SystemAI@ check;
		double lru = 0;

		for(uint i = 0, cnt = shared.length; i < cnt; ++i) {
			auto@ sys = shared[i];
			double update = sys.lastStrengthCheck;
			if(update < lru && sys.visible) {
				@check = sys;
				lru = update;
			}
		}

		for(uint i = 0, cnt = theirBorder.length; i < cnt; ++i) {
			auto@ sys = theirBorder[i];
			double update = sys.lastStrengthCheck;
			if(update < lru && sys.visible) {
				@check = sys;
				lru = update;
			}
		}

		if(check !is null)
			check.strengthCheck(ai);
	}
};

class Intelligence : AIComponent {
	Fleets@ fleets;
	Systems@ systems;

	array<Intel@> intel;

	void create() {
		@fleets = cast<Fleets>(ai.fleets);
		@systems = cast<Systems>(ai.systems);
	}

	void start() {
		for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
			Empire@ emp = getEmpire(i);
			if(emp is ai.empire)
				continue;
			if(!emp.major)
				continue;

			Intel empIntel;
			@empIntel.empire = emp;

			if(intel.length <= uint(emp.index))
				intel.length = uint(emp.index)+1;
			@intel[emp.index] = empIntel;
		}
	}

	void save(SaveFile& file) {
		uint cnt = intel.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			if(intel[i] is null) {
				file.write0();
				continue;
			}

			file.write1();
			intel[i].save(this, file);
		}
	}

	void load(SaveFile& file) {
		uint cnt = 0;
		file >> cnt;
		intel.length = cnt;

		for(uint i = 0; i < cnt; ++i) {
			if(!file.readBit())
				continue;

			@intel[i] = Intel();
			@intel[i].empire = getEmpire(i);
			intel[i].load(this, file);
		}
	}

	Intel@ get(Empire@ emp) {
		if(emp is null)
			return null;
		if(!emp.major)
			return null;
		if(uint(emp.index) >= intel.length)
			return null;
		return intel[emp.index];
	}

	uint ind = 0;
	void tick(double time) override {
		if(intel.length == 0)
			return;
		ind = (ind+1)%intel.length;
		if(intel[ind] !is null)
			intel[ind].tick(ai, this);
	}

	uint fInd = 0;
	void focusTick(double time) override {
		if(intel.length == 0)
			return;
		fInd = (fInd+1)%intel.length;
		if(intel[fInd] !is null)
			intel[fInd].focusTick(ai, this);
	}

	string strdisplay(double str) {
		return standardize(str * 0.001, true);
	}

	double defeatability(Empire@ emp) {
		auto@ empIntel = get(emp);
		if(empIntel is null)
			return 0.0;
		return empIntel.defeatability(ai, this);
	}

	double defeatability(uint theirMask, uint myMask = 0, double cutOff = 600.0) {
		if(myMask == 0)
			myMask = ai.empire.mask;

		double minAcc = 1.0;
		for(uint i = 0, cnt = intel.length; i < cnt; ++i) {
			auto@ itl = intel[i];
			if(itl is null || itl.empire is null)
				continue;
			if((theirMask | myMask) & itl.empire.mask == 0)
				continue;
			minAcc = min(itl.accuracy(ai, this, cutOff), minAcc);
		}

		double ourStrength = 0, theirStrength = 0;
		for(uint i = 0, cnt = intel.length; i < cnt; ++i) {
			auto@ itl = intel[i];
			if(itl is null || itl.empire is null)
				continue;
			if((theirMask | myMask) & itl.empire.mask == 0)
				continue;

			double str = 0.0;
			if(minAcc < 0.6)
				str = itl.empire.TotalMilitary;
			else
				str = itl.getPredictiveStrength(cutOff * 10.0);

			if(itl.empire.mask & theirMask != 0)
				theirStrength += str;
			if(itl.empire.mask & myMask != 0)
				ourStrength += str;
		}

		if(myMask & ai.empire.mask != 0) {
			if(minAcc < 0.6)
				ourStrength += ai.empire.TotalMilitary;
			else
				ourStrength += fleets.totalStrength;
		}
		if(theirMask & ai.empire.mask != 0) {
			if(minAcc < 0.6)
				theirStrength += ai.empire.TotalMilitary;
			else
				theirStrength += fleets.totalStrength;
		}

		if(theirStrength == 0)
			return 10.0;
		return ourStrength / theirStrength;
	}

	void turn() override {
		if(log) {
			ai.print("Intelligence Report on Empires:");
			ai.print(ai.pad(" Our strength: ", 18)+strdisplay(fleets.totalStrength)+" / "+strdisplay(fleets.totalMaxStrength));
			for(uint i = 0, cnt = intel.length; i < cnt; ++i) {
				auto@ empIntel = intel[i];
				if(empIntel is null)
					continue;
				ai.print(" "+ai.pad(empIntel.empire.name, 16)
						+ai.pad(" "+strdisplay(empIntel.getSeenStrength())
						+" / "+strdisplay(empIntel.getPredictiveStrength()), 20)
						+" defeatability "+toString(empIntel.defeatability(ai, this), 2)
						+"   accuracy "+toString(empIntel.accuracy(ai, this), 2));
			}
		}
	}
};

AIComponent@ createIntelligence() {
	return Intelligence();
}

Added scripts/server/empire_ai/weasel/Military.as.


























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// Military
// --------
// Military construction logic. Builds and restores fleets and defensive stations,
// but does not deal with actually using those fleets to fight - that is the purview of
// the War component.
//

import empire_ai.weasel.WeaselAI;

import empire_ai.weasel.Designs;
import empire_ai.weasel.Construction;
import empire_ai.weasel.Budget;
import empire_ai.weasel.Fleets;
import empire_ai.weasel.Development;
import empire_ai.weasel.Movement;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Orbitals;

import resources;

class SupportOrder {
	DesignTarget@ design;
	Object@ onObject;
	AllocateBudget@ alloc;
	bool isGhostOrder = false;
	double expires = INFINITY;
	uint count = 0;

	void save(Military& military, SaveFile& file) {
		military.designs.saveDesign(file, design);
		file << onObject;
		military.budget.saveAlloc(file, alloc);
		file << isGhostOrder;
		file << expires;
		file << count;
	}

	void load(Military& military, SaveFile& file) {
		@design = military.designs.loadDesign(file);
		file >> onObject;
		@alloc = military.budget.loadAlloc(file);
		file >> isGhostOrder;
		file >> expires;
		file >> count;
	}

	bool tick(AI& ai, Military& military, double time) {
		if(alloc !is null) {
			if(alloc.allocated) {
				if(isGhostOrder)
					onObject.rebuildAllGhosts();
				else
					onObject.orderSupports(design.active.mostUpdated(), count);
				if(military.log && design !is null)
					ai.print("Support order completed for "+count+"x "+design.active.name+" ("+design.active.size+")", onObject);
				return false;
			}
		}
		else if(design !is null) {
			if(design.active !is null)
				@alloc = military.budget.allocate(BT_Military, getBuildCost(design.active.mostUpdated()) * count);
		}
		if(expires < gameTime) {
			if(alloc !is null && !alloc.allocated)
				military.budget.remove(alloc);
			if(isGhostOrder)
				onObject.clearAllGhosts();
			if(military.log)
				ai.print("Support order expired", onObject);
			return false;
		}
		return true;
	}
};

class StagingBase {
	Region@ region;
	array<FleetAI@> fleets;

	double idleTime = 0.0;
	double occupiedTime = 0.0;

	OrbitalAI@ shipyard;
	BuildOrbital@ shipyardBuild;
	Factory@ factory;

	bool isUnderAttack = false;

	void save(Military& military, SaveFile& file) {
		file << region;
		file << idleTime;
		file << occupiedTime;
		file << isUnderAttack;

		military.orbitals.saveAI(file, shipyard);
		military.construction.saveConstruction(file, shipyardBuild);

		uint cnt = fleets.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			military.fleets.saveAI(file, fleets[i]);
	}

	void load(Military& military, SaveFile& file) {
		file >> region;
		file >> idleTime;
		file >> occupiedTime;
		file >> isUnderAttack;

		@shipyard = military.orbitals.loadAI(file);
		@shipyardBuild = cast<BuildOrbital>(military.construction.loadConstruction(file));

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			if(i > 200 && file < SV_0158) {
				//Something went preeeetty wrong in an old save
				if(file.readBit()) {
					Object@ obj;
					file >> obj;
				}
			}
			else {
				auto@ fleet = military.fleets.loadAI(file);
				if(fleet !is null)
					fleets.insertLast(fleet);
			}
		}
	}

	bool tick(AI& ai, Military& military, double time) {
		if(fleets.length == 0) {
			occupiedTime = 0.0;
			idleTime += time;
		}
		else {
			occupiedTime += time;
			idleTime = 0.0;
		}

		isUnderAttack = region.ContestedMask & ai.mask != 0;

		//Manage building our shipyard
		if(shipyardBuild !is null) {
			if(shipyardBuild.completed) {
				@shipyard = military.orbitals.getInSystem(ai.defs.Shipyard, region);
				if(shipyard !is null)
					@shipyardBuild = null;
			}
		}
		if(shipyard !is null) {
			if(!shipyard.obj.valid) {
				@shipyard = null;
				@shipyardBuild = null;
			}
		}

		if(factory !is null && (!factory.valid || factory.obj.region !is region))
			@factory = null;
		if(factory is null)
			@factory = military.construction.getFactory(region);

		if(factory !is null) {
			factory.needsSupportLabor = false;
			factory.waitingSupportLabor = 0.0;
			if(factory.obj.hasOrderedSupports) {
				factory.needsSupportLabor = true;
				factory.waitingSupportLabor += double(factory.obj.SupplyOrdered) * ai.behavior.estSizeSupportLabor;
			}
			for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
				if(fleets[i].isHome && fleets[i].obj.hasOrderedSupports) {
					factory.needsSupportLabor = true;
					factory.waitingSupportLabor += double(fleets[i].obj.SupplyOrdered) * ai.behavior.estSizeSupportLabor;
					break;
				}
			}
			if(factory.waitingSupportLabor > 0)
				factory.aimForLabor(factory.waitingSupportLabor / ai.behavior.constructionMaxTime);
		}

		bool isFactorySufficient = false;
		if(factory !is null) {
			if(factory.waitingSupportLabor <= factory.laborIncome * ai.behavior.constructionMaxTime
					|| factory.obj.canImportLabor || factory !is military.construction.primaryFactory)
				isFactorySufficient = true;
		}

		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			Object@ obj = fleets[i].obj;
			if(obj is null || !obj.valid) {
				fleets.removeAt(i);
				--i; --cnt;
				continue;
			}
			fleets[i].stationedFactory = isFactorySufficient;
		}

		if(occupiedTime >= 3.0 * 60.0 && ai.defs.Shipyard !is null && shipyard is null && shipyardBuild is null
				&& !isUnderAttack && (!isFactorySufficient && factory !is military.construction.primaryFactory)) {
			//If any fleets need construction try to queue up a shipyard
			bool needYard = false;
			for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
				auto@ flt = fleets[i];
				if(flt.obj.hasOrderedSupports || flt.filled < 0.8) {
					needYard = true;
					break;
				}
			}

			if(needYard) {
				@shipyard = military.orbitals.getInSystem(ai.defs.Shipyard, region);
				if(shipyard is null) {
					vec3d pos = region.position;
					vec2d offset = random2d(region.radius * 0.4, region.radius * 0.8);
					pos.x += offset.x;
					pos.z += offset.y;

					@shipyardBuild = military.construction.buildOrbital(ai.defs.Shipyard, pos);
				}
			}
		}

		if((idleTime >= 10.0 * 60.0 || region.PlanetsMask & ai.mask == 0) && (shipyardBuild is null || shipyard !is null) && (factory is null || (shipyard !is null && factory.obj is shipyard.obj)) && military.stagingBases.length >= 2) {
			if(shipyard !is null && !ai.behavior.forbidScuttle) {
				cast<Orbital>(shipyard.obj).scuttle();
			}
			else {
				if(factory !is null) {
					factory.needsSupportLabor = false;
					@factory = null;
				}
				return false;
			}
		}
		return true;
	}
};

class Military : AIComponent {
	Fleets@ fleets;
	Development@ development;
	Designs@ designs;
	Construction@ construction;
	Budget@ budget;
	Systems@ systems;
	Orbitals@ orbitals;

	array<SupportOrder@> supportOrders;
	array<StagingBase@> stagingBases;

	AllocateConstruction@ mainWait;
	bool spentMoney = true;

	void create() {
		@fleets = cast<Fleets>(ai.fleets);
		@development = cast<Development>(ai.development);
		@designs = cast<Designs>(ai.designs);
		@construction = cast<Construction>(ai.construction);
		@budget = cast<Budget>(ai.budget);
		@systems = cast<Systems>(ai.systems);
		@orbitals = cast<Orbitals>(ai.orbitals);
	}

	void save(SaveFile& file) {
		uint cnt = supportOrders.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			supportOrders[i].save(this, file);

		construction.saveConstruction(file, mainWait);
		file << spentMoney;

		cnt = stagingBases.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			saveStaging(file, stagingBases[i]);
			stagingBases[i].save(this, file);
		}
	}

	void load(SaveFile& file) {
		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			SupportOrder ord;
			ord.load(this, file);
			if(ord.onObject !is null)
				supportOrders.insertLast(ord);
		}

		@mainWait = construction.loadConstruction(file);
		file >> spentMoney;

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			StagingBase@ base = loadStaging(file);
			if(base !is null) {
				base.load(this, file);
				if(stagingBases.find(base) == -1)
					stagingBases.insertLast(base);
			}
			else {
				StagingBase().load(this, file);
			}
		}
	}

	void loadFinalize(AI& ai) override {
		for(uint i = 0, cnt = stagingBases.length; i < cnt; ++i) {
			auto@ base = stagingBases[i];
			for(uint n = 0, ncnt = base.fleets.length; n < ncnt; ++n) {
				Object@ obj = base.fleets[n].obj;
				if(obj is null || !obj.valid || !obj.initialized) {
					base.fleets.removeAt(n);
					--n; --ncnt;
				}
			}
		}
	}

	StagingBase@ loadStaging(SaveFile& file) {
		Region@ reg;
		file >> reg;

		if(reg is null)
			return null;

		StagingBase@ base = getBase(reg);
		if(base is null) {
			@base = StagingBase();
			@base.region = reg;
			stagingBases.insertLast(base);
		}
		return base;
	}

	void saveStaging(SaveFile& file, StagingBase@ base) {
		Region@ reg;
		if(base !is null)
			@reg = base.region;
		file << reg;
	}

	Region@ getClosestStaging(Region& targetRegion) {
		//Check if we have anything close enough
		StagingBase@ best;
		int minHops = INT_MAX;
		for(uint i = 0, cnt = stagingBases.length; i < cnt; ++i) {
			int d = systems.hopDistance(stagingBases[i].region, targetRegion);
			if(d < minHops) {
				minHops = d;
				@best = stagingBases[i];
			}
		}
		if(best !is null)
			return best.region;
		return null;
	}

	Region@ getStagingFor(Region& targetRegion) {
		//Check if we have anything close enough
		StagingBase@ best;
		int minHops = INT_MAX;
		for(uint i = 0, cnt = stagingBases.length; i < cnt; ++i) {
			int d = systems.hopDistance(stagingBases[i].region, targetRegion);
			if(d < minHops) {
				minHops = d;
				@best = stagingBases[i];
			}
		}
		if(minHops < ai.behavior.stagingMaxHops)
			return best.region;

		//Create a new staging base for this
		Region@ bestNew;
		minHops = INT_MAX;
		for(uint i = 0, cnt = systems.border.length; i < cnt; ++i) {
			auto@ sys = systems.border[i].obj;
			int d = systems.hopDistance(sys, targetRegion);
			if(d < minHops) {
				minHops = d;
				@bestNew = sys;
			}
		}

		if(minHops > ai.behavior.stagingMaxHops && best !is null)
			return best.region;

		auto@ base = getBase(bestNew);
		if(base !is null)
			return base.region;
		else
			return createStaging(bestNew).region;
	}

	StagingBase@ createStaging(Region@ region) {
		if(region is null)
			return null;

		if(log)
			ai.print("Create new staging base.", region);

		StagingBase newBase;
		@newBase.region = region;

		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			if(fleets.fleets[i].stationed is region)
				newBase.fleets.insertLast(fleets.fleets[i]);
		}

		stagingBases.insertLast(newBase);
		return newBase;
	}

	StagingBase@ getBase(Region@ inRegion) {
		if(inRegion is null)
			return null;
		for(uint i = 0, cnt = stagingBases.length; i < cnt; ++i) {
			if(stagingBases[i].region is inRegion)
				return stagingBases[i];
		}
		return null;
	}

	vec3d getStationPosition(Region& inRegion, double distance = 100.0) {
		auto@ base = getBase(inRegion);
		if(base !is null) {
			if(base.shipyard !is null) {
				vec3d pos = base.shipyard.obj.position;
				vec2d offset = random2d(distance * 0.5, distance * 1.5);
				pos.x += offset.x;
				pos.z += offset.y;

				return pos;
			}
		}

		vec3d pos = inRegion.position;
		vec2d offset = random2d(inRegion.radius * 0.4, inRegion.radius * 0.8);
		pos.x += offset.x;
		pos.z += offset.y;
		return pos;
	}

	void stationFleet(FleetAI@ fleet, Region@ inRegion) {
		if(inRegion is null || fleet.stationed is inRegion)
			return;

		auto@ prevBase = getBase(fleet.stationed);
		if(prevBase !is null)
			prevBase.fleets.remove(fleet);

		auto@ base = getBase(inRegion);
		if(base !is null)
			base.fleets.insertLast(fleet);

		@fleet.stationed = inRegion;
		fleet.stationedFactory = construction.getFactory(inRegion) !is null;
		if(fleet.mission is null)
			fleets.returnToBase(fleet);
	}

	void orderSupportsOn(Object& obj, double expire = 60.0) {
		if(obj.SupplyGhost > 0) {
			if(ai.behavior.fleetsRebuildGhosts) {
				//Try to rebuild the fleet's ghosts
				SupportOrder ord;
				@ord.onObject = obj;
				@ord.alloc = budget.allocate(BT_Military, obj.rebuildGhostsCost());
				ord.expires = gameTime + expire;
				ord.isGhostOrder = true;

				supportOrders.insertLast(ord);

				if(log)
					ai.print("Attempting to rebuild ghosts", obj);
				return;
			}
			else {
				obj.clearAllGhosts();
			}
		}

		int supCap = obj.SupplyCapacity;
		int supHave = obj.SupplyUsed - obj.SupplyGhost;

		//Build some supports
		int supSize = pow(2, round(::log(double(supCap) * randomd(0.005, 0.03))/::log(2.0)));
		supSize = max(min(supSize, supCap - supHave), 1);

		SupportOrder ord;
		@ord.onObject = obj;
		@ord.design = designs.design(DP_Support, supSize);
		ord.expires = gameTime + expire;
		ord.count = clamp((supCap - supHave)/supSize, 1, int(ceil((randomd(0.01, 0.1)*supCap)/double(supSize))));

		if(log)
			ai.print("Attempting to build supports: "+ord.count+"x size "+supSize, obj);

		supportOrders.insertLast(ord);
	}

	void findSomethingToDo() {
		//See if we should retrofit anything
		if(mainWait is null && !spentMoney && gameTime > ai.behavior.flagshipBuildMinGameTime) {
			int availMoney = budget.spendable(BT_Military);
			int moneyTargetSize = floor(double(availMoney) * ai.behavior.shipSizePerMoney);

			//See if one of our fleets is old enough that we can retrofit it
			for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
				FleetAI@ fleet = fleets.fleets[i];
				if(fleet.mission !is null && fleet.mission.isActive)
					continue;
				if(fleet.fleetClass != FC_Combat)
					continue;
				if(fleet.obj.hasOrders)
					continue;

				Ship@ ship = cast<Ship>(fleet.obj);
				if(ship is null)
					continue;

				//Don't retrofit free fleets
				if(ship.isFree && !ai.behavior.retrofitFreeFleets)
					continue;

				//Find the factory assigned to this
				Factory@ factory;
				if(fleet.isHome) {
					Region@ reg = fleet.obj.region;
					@factory = construction.getFactory(reg);
				}
				if(factory is null)
					continue;
				if(factory.busy)
					continue;

				//Find how large we can make this flagship
				const Design@ dsg = ship.blueprint.design;
				int targetSize = min(int(moneyTargetSize * 1.2), int(factory.laborToBear(ai) * 1.3 * ai.behavior.shipSizePerLabor));
				targetSize = 5 * floor(double(targetSize) / 5.0);

				//See if we should retrofit this
				int size = ship.blueprint.design.size;
				if(size > targetSize)
					continue;

				double pctDiff = (double(targetSize) / double(size)) - 1.0;
				if(pctDiff < ai.behavior.shipRetrofitThreshold)
					continue;

				DesignTarget@ newDesign = designs.scale(dsg, targetSize);
				spentMoney = true;

				auto@ retrofit = construction.retrofit(ship);
				@mainWait = construction.buildNow(retrofit, factory);

				if(log)
					ai.print("Retrofitting to size "+targetSize, fleet.obj);

				//TODO: This should mark the fleet as occupied for missions while we retrofit

				return;
			}

			//See if we should build a new fleet
			Factory@ factory = construction.primaryFactory;
			if(factory !is null && !factory.busy) {
				//Figure out how large our flagship would be if we built one
				factory.aimForLabor((double(moneyTargetSize) / ai.behavior.shipSizePerLabor) / ai.behavior.constructionMaxTime);
				int targetSize = min(moneyTargetSize, int(factory.laborToBear(ai) * ai.behavior.shipSizePerLabor));
				targetSize = 5 * floor(double(targetSize) / 5.0);

				int expMaint = double(targetSize) * ai.behavior.maintenancePerShipSize;
				int expCost = double(targetSize) / ai.behavior.shipSizePerMoney;
				if(budget.canSpend(BT_Military, expCost, expMaint)) {
					//Make sure we're building an adequately sized flagship
					uint count = 0;
					double avgSize = 0.0;
					for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
						FleetAI@ fleet = fleets.fleets[i];
						Ship@ ship = cast<Ship>(fleet.obj);
						if(ship !is null && fleet.fleetClass == FC_Combat) {
							avgSize += ship.blueprint.design.size;
							count += 1;
						}
					}
					if(count != 0)
						avgSize /= double(count);

					if(count < ai.behavior.maxActiveFleets && targetSize >= avgSize * ai.behavior.flagshipBuildMinAvgSize) {
						//Build the flagship
						DesignTarget@ design = designs.design(DP_Combat, targetSize,
								availMoney, budget.maintainable(BT_Military),
								factory.laborToBear(ai),
								findSize=true);
						@mainWait = construction.buildFlagship(design);
						mainWait.maxTime *= 1.5;
						spentMoney = true;

						if(log)
							ai.print("Ordering a new fleet at size "+targetSize);

						return;
					}
				}
			}
		}

		//See if any of our fleets need refilling
		//TODO: Aim for labor on the factory so that the supports are built in reasonable time
		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			FleetAI@ fleet = fleets.fleets[i];
			if(fleet.mission !is null && fleet.mission.isActive)
				continue;
			if(fleet.fleetClass != FC_Combat)
				continue;
			if(fleet.obj.hasOrders)
				continue;
			if(fleet.filled >= 1.0)
				continue;
			if(hasSupportOrderFor(fleet.obj))
				continue;
			if(!fleet.isHome)
				continue;

			//Re-station to our factory if we're idle and need refill without being near a factory
			Factory@ f = construction.getFactory(fleet.obj.region);
			if(f is null) {
				if(fleet.filled < ai.behavior.stagingToFactoryFill && construction.primaryFactory !is null)
					stationFleet(fleet, construction.primaryFactory.obj.region);
				continue;
			}

			//Don't order if the factory has support orders, it'll just make everything take longer
			if(f !is null && ai.behavior.supportOrderWaitOnFactory && fleet.filled < 0.9 && fleet.obj.SupplyGhost == 0) {
				if(f.obj.hasOrderedSupports && f.obj.SupplyUsed < f.obj.SupplyCapacity)
					continue;
			}

			int supCap = fleet.obj.SupplyCapacity;
			int supHave = fleet.obj.SupplyUsed - fleet.obj.SupplyGhost;
			if(supHave < supCap) {
				orderSupportsOn(fleet.obj);
				spentMoney = true;
				return;
			}
		}

		budget.checkedMilitarySpending = spentMoney;

		//TODO: Build defense stations
	}

	bool hasSupportOrderFor(Object& obj) {
		for(uint i = 0, cnt = supportOrders.length; i < cnt; ++i) {
			if(supportOrders[i].onObject is obj)
				return true;
		}
		return false;
	}

	void tick(double time) override {
		//Manage our orders for support ships
		for(uint i = 0, cnt = supportOrders.length; i < cnt; ++i) {
			if(!supportOrders[i].tick(ai, this, time)) {
				supportOrders.removeAt(i);
				--i; --cnt;
			}
		}
	}

	void focusTick(double time) override {
		//Find something for us to do
		findSomethingToDo();

		//If we're far into the budget, spend our money on building supports at our factories
		if(budget.Progress > 0.9 && budget.canSpend(BT_Military, 10)) {
			for(uint i = 0, cnt = construction.factories.length; i < cnt; ++i) {
				//TODO: Build on planets in the system if this is full
				auto@ f = construction.factories[i];
				if(f.obj.SupplyUsed < f.obj.SupplyCapacity && !hasSupportOrderFor(f.obj)) {
					orderSupportsOn(f.obj, expire=budget.RemainingTime);
					break;
				}
			}
		}

		//Check if we should re-station any of our fleets
		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets.fleets[i];
			if(flAI.stationed is null) {
				Region@ reg = flAI.obj.region;
				if(reg !is null && reg.PlanetsMask & ai.mask != 0)
					stationFleet(flAI, reg);
			}
		}

		//Make sure all our major factories are considered staging bases
		for(uint i = 0, cnt = construction.factories.length; i < cnt; ++i) {
			auto@ f = construction.factories[i];
			if(f.obj.isShip)
				continue;
			Region@ reg = f.obj.region;
			if(reg is null)
				continue;
			auto@ base = getBase(reg);
			if(base is null)
				createStaging(reg);
		}

		//If we don't have any staging bases, make one at a focus
		if(stagingBases.length == 0 && development.focuses.length != 0) {
			Region@ reg = development.focuses[0].obj.region;
			if(reg !is null)
				createStaging(reg);
		}

		//Update our staging bases
		for(uint i = 0, cnt = stagingBases.length; i < cnt; ++i) {
			auto@ base = stagingBases[i];
			if(!base.tick(ai, this, time)) {
				stagingBases.removeAt(i);
				--i; --cnt;
			}
		}
	}

	void turn() override {
		//Fleet construction happens in the beginning of the turn, because we want
		//to use our entire military budget on it.
		if(mainWait !is null) {
			if(mainWait.completed) {
				@mainWait = null;
			}
			else if(!mainWait.started) {
				if(log)
					ai.print("Failed current main construction wait.");
				construction.cancel(mainWait);
				@mainWait = null;
			}
		}
		spentMoney = false;
	}
};

AIComponent@ createMilitary() {
	return Military();
}

Added scripts/server/empire_ai/weasel/Movement.as.




































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// Movement
// --------
// Manages FTL travel modes, expenditure of FTL energy, and general movement patterns.
//

import empire_ai.weasel.WeaselAI;

import oddity_navigation;
import ftl;

enum FTLReturn {
	F_Pass,
	F_Continue,
	F_Done,
	F_Kill,
};

class FTL : AIComponent {
	uint order(MoveOrder& order) { return F_Pass; }
	uint tick(MoveOrder& order, double time) { return F_Pass; }
};

bool getNearPosition(Object& obj, Object& target, vec3d& pos, bool spread = false) {
	if(target !is null) {
		if(target.isPlanet) {
			Planet@ toPl = cast<Planet>(target);
			vec3d dir = obj.position - toPl.position;
			dir = dir.normalized(toPl.OrbitSize * 0.9);
			if(spread)
				dir = quaterniond_fromAxisAngle(vec3d_up(), randomd(-0.15,0.15) * pi) * dir;
			pos = toPl.position + dir;
			return true;
		}
		else if(obj.hasLeaderAI && (target.isShip || target.isOrbital)) {
			vec3d dir = obj.position - target.position;
			dir = dir.normalized(obj.getEngagementRange());
			pos = target.position + dir;
			return true;
		}
		else {
			Region@ reg = cast<Region>(target);
			if(reg is null)
				@reg = target.region;
			if(reg !is null) {
				vec3d dir = obj.position - reg.position;
				dir = dir.normalized(reg.radius * 0.85);
				if(spread)
					dir = quaterniond_fromAxisAngle(vec3d_up(), randomd(-0.15,0.15) * pi) * dir;
				pos = reg.position + dir;
				return true;
			}
		}
	}
	return false;
}

bool targetPosition(MoveOrder& ord, vec3d& toPosition) {
	if(ord.target !is null) {
		return getNearPosition(ord.obj, ord.target, toPosition);
	}
	else {
		toPosition = ord.position;
		return true;
	}
}

double usableFTL(AI& ai, MoveOrder& ord) {
	double storage = ai.empire.FTLCapacity;
	double avail = ai.empire.FTLStored;

	double reserved = 0.0;
	if(ord.priority < MP_Critical)
		reserved += ai.behavior.ftlReservePctCritical;
	if(ord.priority < MP_Normal)
		reserved += ai.behavior.ftlReservePctNormal;
	avail -= reserved * storage;

	return avail;
}

enum MovePriority {
	MP_Background,
	MP_Normal,
	MP_Critical
};

class MoveOrder {
	int id = -1;
	uint priority = MP_Normal;
	Object@ obj;
	Object@ target;
	vec3d position;
	bool completed = false;
	bool failed = false;

	void save(Movement& movement, SaveFile& file) {
		file << priority;
		file << obj;
		file << target;
		file << position;
		file << completed;
		file << failed;
	}

	void load(Movement& movement, SaveFile& file) {
		file >> priority;
		file >> obj;
		file >> target;
		file >> position;
		file >> completed;
		file >> failed;
	}

	void cancel() {
		failed = true;
		obj.clearOrders();
	}

	bool tick(AI& ai, Movement& movement, double time) {
		//Check if we still exist
		if(obj is null || !obj.valid || obj.owner !is ai.empire) {
			failed = true;
			return false;
		}

		uint ftlMode = F_Pass;
		if(movement.ftl !is null) {
			ftlMode = movement.ftl.tick(this, time);
			if(ftlMode == F_Kill)
				return false;
			if(ftlMode == F_Done)
				return true;
		}

		//Check if we've arrived
		if(target !is null) {
			if(!target.valid) {
				failed = true;
				return false;
			}
			double targDist = target.radius + 45.0 + obj.radius;
			if(target.isRegion)
				targDist = target.radius * 0.86 + obj.radius;
			if(target.position.distanceTo(obj.position) < targDist) {
				completed = true;
				return false;
			}
		}
		else {
			double targDist = obj.radius * 2.0;
			if(obj.position.distanceTo(position) < targDist) {
				completed = true;
				return false;
			}
		}

		//Fail out if our order failed
		if(ftlMode == F_Pass) {
			if(!obj.hasOrders) {
				failed = true;
				return false;
			}
		}

		return true;
	}
};

class Movement : AIComponent {
	int nextMoveOrderId = 0;
	array<MoveOrder@> moveOrders;

	array<Oddity@> oddities;

	FTL@ ftl;

	void create() {
		@ftl = cast<FTL>(ai.ftl);
	}

	void save(SaveFile& file) {
		file << nextMoveOrderId;

		uint cnt = moveOrders.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			saveMoveOrder(file, moveOrders[i]);
			moveOrders[i].save(this, file);
		}
	}

	void load(SaveFile& file) {
		file >> nextMoveOrderId;

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ ord = loadMoveOrder(file);
			if(ord !is null) {
				ord.load(this, file);
				if(ord.obj !is null)
					moveOrders.insertLast(ord);
			}
			else {
				MoveOrder().load(this, file);
			}
		}
	}

	array<MoveOrder@> loadIds;
	MoveOrder@ loadMoveOrder(SaveFile& file) {
		int id = -1;
		file >> id;
		bool failed = false, completed = false;
		file >> failed;
		file >> completed;
		if(id == -1) {
			return null;
		}
		else {
			for(uint i = 0, cnt = loadIds.length; i < cnt; ++i) {
				if(loadIds[i].id == id)
					return loadIds[i];
			}
			MoveOrder data;
			data.id = id;
			data.failed = failed;
			data.completed = completed;
			loadIds.insertLast(data);
			return data;
		}
	}

	void saveMoveOrder(SaveFile& file, MoveOrder@ data) {
		int id = -1;
		bool failed = false, completed = false;
		if(data !is null) {
			id = data.id;
			failed = data.failed;
			completed = data.completed;
		}
		file << id;
		file << failed;
		file << completed;
	}

	void postLoad(AI& ai) {
		loadIds.length = 0;
		getOddityGates(oddities);
	}

	array<PathNode@> path;
	double getPathDistance(const vec3d& fromPosition, const vec3d& toPosition, double accel = 1.0) {
		pathOddityGates(oddities, ai.empire, path, fromPosition, toPosition, accel);
		return ::getPathDistance(fromPosition, toPosition, path);
	}

	double eta(Object& obj, Object& toObject, uint priority = MP_Normal) {
		return eta(obj, toObject.position, priority);
	}

	double eta(Object& obj, const vec3d& position, uint priority = MP_Normal) {
		//TODO: Use FTL
		//TODO: Path through gates/wormholes
		return newtonArrivalTime(obj.maxAcceleration, position - obj.position, obj.velocity);
	}

	void order(MoveOrder& ord) {
		if(ord.target !is null && ord.target is ord.obj.region)
			return;

		bool madeOrder = false;

		if(ftl !is null) {
			uint mode = ftl.order(ord);
			if(mode == F_Kill || mode == F_Done)
				return;
			madeOrder = (mode == F_Continue);
		}

		if(ord.target !is null) {
			ord.obj.addGotoOrder(ord.target, append=madeOrder);
			ord.position = ord.target.position;
		}
		else
			ord.obj.addMoveOrder(ord.position, append=madeOrder);
	}

	void add(MoveOrder& ord) {
		for(uint i = 0, cnt = moveOrders.length; i < cnt; ++i) {
			if(moveOrders[i].obj is ord.obj) {
				moveOrders[i].failed = true;
				moveOrders.removeAt(i);
				--i; --cnt;
			}
		}

		moveOrders.insertLast(ord);
		order(ord);
	}

	MoveOrder@ move(Object& obj, Object& toObject, uint priority = MP_Normal, bool spread = false, bool nearOnly = false) {
		if(toObject.isRegion) {
			if(obj.region is toObject)
				nearOnly = false;
			else
				nearOnly = true;
		}
		if(nearOnly) {
			vec3d pos;
			bool canNear = getNearPosition(obj, toObject, pos, spread);
			if(canNear)
				return move(obj, pos, priority);
		}

		MoveOrder ord;
		ord.id = nextMoveOrderId++;
		@ord.obj = obj;
		@ord.target = toObject;
		ord.priority = priority;

		add(ord);
		return ord;
	}

	MoveOrder@ move(Object& obj, const vec3d& position, uint priority = MP_Normal, bool spread = false) {
		MoveOrder ord;
		ord.id = nextMoveOrderId++;
		@ord.obj = obj;
		ord.position = position;
		ord.priority = priority;

		add(ord);
		return ord;
	}

	void tick(double time) override {
		for(uint i = 0, cnt = moveOrders.length; i < cnt; ++i) {
			if(!moveOrders[i].tick(ai, this, time)) {
				moveOrders.removeAt(i);
				--i; --cnt;
			}
		}
	}

	void focusTick(double time) override {
		//Update our gate navigation list
		getOddityGates(oddities);
	}
};

AIComponent@ createMovement() {
	return Movement();
}

Added scripts/server/empire_ai/weasel/Orbitals.as.










































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.Budget;
import empire_ai.weasel.Systems;

from ai.orbitals import AIOrbitals, OrbitalAIHook, OrbitalUse;
import ai.consider;
import ai.construction;

import orbitals;
import saving;

final class OrbitalAI {
	Object@ obj;
	const OrbitalModule@ type;
	double prevTick = 0;
	Object@ around;

	void init(AI& ai, Orbitals& orbitals) {
		if(obj.isOrbital)
			@type = getOrbitalModule(cast<Orbital>(obj).coreModule);
	}

	void save(Orbitals& orbitals, SaveFile& file) {
		file << obj;
	}

	void load(Orbitals& orbitals, SaveFile& file) {
		file >> obj;
	}

	void remove(AI& ai, Orbitals& orbitals) {
	}

	void tick(AI& ai, Orbitals& orbitals, double time) {
		//Deal with losing planet ownership
		if(obj is null || !obj.valid || obj.owner !is ai.empire) {
			orbitals.remove(this);
			return;
		}

		//Record what we're orbiting around
		if(around !is null) {
			if(!obj.isOrbitingAround(around))
				@around = obj.getOrbitingAround();
		}
		else {
			if(obj.hasOrbitCenter)
				@around = obj.getOrbitingAround();
		}
	}
};

class Orbitals : AIComponent, AIOrbitals {
	Budget@ budget;
	Systems@ systems;

	array<OrbitalAI@> orbitals;
	uint orbIdx = 0;
	
	array<IOrbitalConstruction@> genericBuilds;
	
	bool buildOrbitals = true;

	void create() {
		@budget = cast<Budget>(ai.budget);
		@systems = cast<Systems>(ai.systems);

		//Register specialized orbital types
		for(uint i = 0, cnt = getOrbitalModuleCount(); i < cnt; ++i) {
			auto@ type = getOrbitalModule(i);
			for(uint n = 0, ncnt = type.ai.length; n < ncnt; ++n) {
				auto@ hook = cast<OrbitalAIHook>(type.ai[n]);
				if(hook !is null)
					hook.register(this, type);
			}
		}
	}

	Empire@ get_empire() {
		return ai.empire;
	}

	Considerer@ get_consider() {
		return cast<Considerer>(ai.consider);
	}

	OrbitalAI@ getInSystem(const OrbitalModule@ module, Region@ reg) {
		if(module is null)
			return null;
		for(uint i = 0, cnt = orbitals.length; i < cnt; ++i) {
			if(orbitals[i].type is module) {
				if(orbitals[i].obj.region is reg)
					return orbitals[i];
			}
		}
		return null;
	}

	bool haveInSystem(const OrbitalModule@ module, Region@ reg) {
		if(module is null)
			return false;
		for(uint i = 0, cnt = orbitals.length; i < cnt; ++i) {
			if(orbitals[i].type is module) {
				if(orbitals[i].obj.region is reg)
					return true;
			}
		}
		return false;
	}

	bool haveAround(const OrbitalModule@ module, Object@ around) {
		if(module is null)
			return false;
		for(uint i = 0, cnt = orbitals.length; i < cnt; ++i) {
			if(orbitals[i].type is module) {
				if(orbitals[i].around is around)
					return true;
			}
		}
		return false;
	}

	void registerUse(OrbitalUse use, const OrbitalModule& type) {
		switch(use) {
			case OU_Shipyard:
				@ai.defs.Shipyard = type;
				break;
			case OU_TradeOutpost:
				@ai.defs.TradeOutpost = type;
				break;
			case OU_TradeStation:
				@ai.defs.TradeStation = type;
				break;
		}
	}

	void save(SaveFile& file) {
		uint cnt = orbitals.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = orbitals[i];
			saveAI(file, data);
			data.save(this, file);
		}
	}

	void load(SaveFile& file) {
		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = loadAI(file);
			if(data !is null)
				data.load(this, file);
			else
				OrbitalAI().load(this, file);
		}
	}

	OrbitalAI@ loadAI(SaveFile& file) {
		Object@ obj;
		file >> obj;

		if(obj is null)
			return null;

		OrbitalAI@ data = getAI(obj);
		if(data is null) {
			@data = OrbitalAI();
			@data.obj = obj;
			data.prevTick = gameTime;
			orbitals.insertLast(data);
			data.init(ai, this);
		}
		return data;
	}

	void saveAI(SaveFile& file, OrbitalAI@ ai) {
		Object@ obj;
		if(ai !is null)
			@obj = ai.obj;
		file << obj;
	}

	void start() {
		checkForOrbitals();
	}

	void checkForOrbitals() {
		auto@ data = ai.empire.getOrbitals();
		Object@ obj;
		while(receive(data, obj)) {
			if(obj !is null)
				register(obj);
		}
	}

	bool isBuilding(const OrbitalModule& type) {
		for(uint i = 0, cnt = genericBuilds.length; i < cnt; ++i) {
			if(genericBuilds[i].module is type)
				return true;
		}
		return false;
	}

	void tick(double time) {
		double curTime = gameTime;

		if(orbitals.length != 0) {
			orbIdx = (orbIdx+1) % orbitals.length;

			auto@ data = orbitals[orbIdx];
			data.tick(ai, this, curTime - data.prevTick);
			data.prevTick = curTime;
		}
	}

	uint prevCount = 0;
	double checkTimer = 0;
	void focusTick(double time) override {
		//Check for any newly obtained planets
		uint curCount = ai.empire.orbitalCount;
		checkTimer += time;
		if(curCount != prevCount || checkTimer > 60.0) {
			checkForOrbitals();
			prevCount = curCount;
			checkTimer = 0;
		}
		
		//Deal with building AI hints
		
	}

	OrbitalAI@ getAI(Object& obj) {
		for(uint i = 0, cnt = orbitals.length; i < cnt; ++i) {
			if(orbitals[i].obj is obj)
				return orbitals[i];
		}
		return null;
	}

	OrbitalAI@ register(Object& obj) {
		OrbitalAI@ data = getAI(obj);
		if(data is null) {
			@data = OrbitalAI();
			@data.obj = obj;
			data.prevTick = gameTime;
			orbitals.insertLast(data);
			data.init(ai, this);
		}
		return data;
	}

	void remove(OrbitalAI@ data) {
		data.remove(ai, this);
		orbitals.remove(data);
	}
};

AIComponent@ createOrbitals() {
	return Orbitals();
}

Added scripts/server/empire_ai/weasel/Planets.as.
















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;

import empire_ai.weasel.Events;
import empire_ai.weasel.Resources;
import empire_ai.weasel.Budget;
import empire_ai.weasel.Systems;

import ai.construction;
import ai.events;

import planets.PlanetSurface;

import void relationRecordLost(AI& ai, Empire& emp, Object@ obj) from "empire_ai.weasel.Relations";

from constructions import ConstructionType, getConstructionTypeCount, getConstructionType;
from ai.constructions import AIConstructions, ConstructionAIHook, ConstructionUse;

import ai.consider;

import buildings;
import saving;

final class BuildingRequest : IBuildingConstruction {
	protected int _id = -1;
	PlanetAI@ plAI;
	AllocateBudget@ alloc;
	const BuildingType@ type;
	double expires = INFINITY;
	bool built = false;
	bool canceled = false;
	bool scatter = false;
	vec2i builtAt;

	BuildingRequest(Budget& budget, const BuildingType@ type, double priority, uint moneyType) {
		@this.type = type;
		@alloc = budget.allocate(moneyType, type.buildCostEst, type.maintainCostEst, priority=priority);
	}

	BuildingRequest() {
	}
	
	int id {
		get const { return _id; }
		set { _id = value; }
	}
	
	bool get_started() const { return built; }
	
	bool completed {
		get const { return getProgress() >= 1.0; }
		set { }
	}
	
	const BuildingType@ get_building() const { return type; }

	void save(Planets& planets, SaveFile& file) {
		planets.saveAI(file, plAI);
		planets.budget.saveAlloc(file, alloc);
		file.writeIdentifier(SI_Building, type.id);
		file << expires;
		file << built;
		file << canceled;
		file << builtAt;
		file << scatter;
	}

	void load(Planets& planets, SaveFile& file) {
		@plAI = planets.loadAI(file);
		@alloc = planets.budget.loadAlloc(file);
		@type = getBuildingType(file.readIdentifier(SI_Building));
		file >> expires;
		file >> built;
		file >> canceled;
		file >> builtAt;
		if(file >= SV_0153)
			file >> scatter;
	}

	double cachedProgress = 0.0;
	double nextProgressCache = 0.0;
	double getProgress() {
		if(!built)
			return 0.0;
		if(gameTime < nextProgressCache)
			return cachedProgress;

		cachedProgress = plAI.obj.getBuildingProgressAt(builtAt.x, builtAt.y);
		if(cachedProgress > 0.95)
			nextProgressCache = gameTime + 1.0;
		else if(cachedProgress < 0.5)
			nextProgressCache = gameTime + 30.0;
		else
			nextProgressCache = gameTime + 10.0;

		return cachedProgress;
	}

	bool tick(AI& ai, Planets& planets, double time) {
		if(expires < gameTime) {
			if(planets.log)
				ai.print(type.name+" build request expired", plAI.obj);
			canceled = true;
			return false;
		}

		if(alloc is null || alloc.allocated) {
			builtAt = plAI.buildBuilding(ai, planets, type, scatter=scatter);
			if(builtAt == vec2i(-1,-1)) {
				planets.budget.remove(alloc);
				canceled = true;
			}
			else
				built = true;
			return false;
		}
		return true;
	}
};

final class ConstructionRequest : IGenericConstruction {
	protected int _id = -1;
	PlanetAI@ plAI;
	AllocateBudget@ alloc;
	const ConstructionType@ type;
	double expires = INFINITY;
	bool built = false;
	bool canceled = false;
	vec2i builtAt;

	ConstructionRequest(Budget& budget, Object@ buildAt, const ConstructionType@ type, double priority, uint moneyType) {
		@this.type = type;
		@alloc = budget.allocate(moneyType, type.getBuildCost(buildAt), type.getMaintainCost(buildAt), priority=priority);
	}

	ConstructionRequest() {
	}
	
	int id {
		get const { return _id; }
		set { _id = value; }
	}
	
	bool get_started() const { return built; }
	
	bool completed {
		get const {
			double progress = getProgress();
			return progress == -1.0 || progress >= 1.0;
		}
		set { }
	}
	
	const ConstructionType@ get_construction() const { return type; }

	void save(Planets& planets, SaveFile& file) {
		planets.saveAI(file, plAI);
		planets.budget.saveAlloc(file, alloc);
		file.writeIdentifier(SI_ConstructionType, type.id);
		file << expires;
		file << built;
		file << canceled;
		file << builtAt;
	}

	void load(Planets& planets, SaveFile& file) {
		@plAI = planets.loadAI(file);
		@alloc = planets.budget.loadAlloc(file);
		@type = getConstructionType(file.readIdentifier(SI_ConstructionType));
		file >> expires;
		file >> built;
		file >> canceled;
		file >> builtAt;
	}

	double cachedProgress = 0.0;
	double nextProgressCache = 0.0;
	double getProgress() {
		if(!built)
			return 0.0;
		if(gameTime < nextProgressCache)
			return cachedProgress;

		cachedProgress = plAI.obj.get_constructionProgress();
		if(cachedProgress > 0.95)
			nextProgressCache = gameTime + 1.0;
		else if(cachedProgress < 0.5)
			nextProgressCache = gameTime + 30.0;
		else
			nextProgressCache = gameTime + 10.0;

		return cachedProgress;
	}

	bool tick(AI& ai, Planets& planets, double time) {
		if(expires < gameTime) {
			if(planets.log)
				ai.print(type.name+" construction request expired", plAI.obj);
			canceled = true;
			return false;
		}

		if(alloc is null || alloc.allocated) {
			if(!plAI.buildConstruction(ai, planets, type)) {
				planets.budget.remove(alloc);
				canceled = true;

			}
			else
				built = true;
			return false;
		}
		return true;
	}
};

final class PlanetAI {
	Planet@ obj;

	int targetLevel = 0;
	int requestedLevel = 0;
	double prevTick = 0;

	array<ExportData@>@ resources;
	ImportData@ claimedChain;

	void init(AI& ai, Planets& planets) {
		@resources = planets.resources.availableResource(obj);
		planets.events.notifyPlanetAdded(this, EventArgs());
	}

	void save(Planets& planets, SaveFile& file) {
		file << obj;
		file << targetLevel;
		file << requestedLevel;
		file << prevTick;

		uint cnt = 0;
		if(resources !is null)
			cnt = resources.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			planets.resources.saveExport(file, resources[i]);
		planets.resources.saveImport(file, claimedChain);
	}

	void load(Planets& planets, SaveFile& file) {
		file >> obj;
		file >> targetLevel;
		file >> requestedLevel;
		file >> prevTick;
		uint cnt = 0;
		file >> cnt;
		@resources = array<ExportData@>();
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = planets.resources.loadExport(file);
			if(data !is null)
				resources.insertLast(data);
		}
		@claimedChain = planets.resources.loadImport(file);
	}

	void remove(AI& ai, Planets& planets) {
		if(claimedChain !is null) {
			claimedChain.claimedFor = false;
			@claimedChain = null;
		}
		@resources = null;
		planets.events.notifyPlanetRemoved(this, EventArgs());
	}

	void tick(AI& ai, Planets& planets, double time) {
		//Deal with losing planet ownership
		if(obj is null || !obj.valid || obj.owner !is ai.empire) {
			if(obj.owner !is ai.empire)
				relationRecordLost(ai, obj.owner, obj);
			planets.remove(this);
			return;
		}

		//Handle when the planet's native resources change
		if(obj.nativeResourceCount != resources.length || (resources.length != 0 && obj.primaryResourceId != resources[0].resourceId))
			planets.updateResourceList(obj, resources);

		//Level up resources if we need them
		if(resources.length != 0 && claimedChain is null) {
			int resLevel = resources[0].resource.level;
			if(resLevel > 0 && !resources[0].resource.exportable)
				resLevel += 1;
			if(targetLevel < resLevel) {
				//See if we need it for anything first
				@claimedChain = planets.resources.findUnclaimed(resources[0]);
				if(claimedChain !is null)
					claimedChain.claimedFor = true;

				//Chain the levelup before what needs it
				planets.requestLevel(this, resLevel, before=claimedChain);
			}
		}

		//Request imports if the planet needs to level up
		if(targetLevel > requestedLevel) {
			int nextLevel = min(targetLevel, min(obj.resourceLevel, requestedLevel)+1);
			if(nextLevel != requestedLevel) {
				planets.resources.organizeImports(obj, nextLevel);
				requestedLevel = nextLevel;
			}
		}
		else if(targetLevel < requestedLevel) {
			planets.resources.organizeImports(obj, targetLevel);
			requestedLevel = targetLevel;
		}
	}

	double get_colonizeWeight() {
		if(obj.isColonizing)
			return 0.0;
		if(obj.level == 0)
			return 0.0;
		if(!obj.canSafelyColonize)
			return 0.0;
		double w = 1.0;
		double pop = obj.population;
		double maxPop = obj.maxPopulation;
		if(pop < maxPop-0.1) {
			if(obj.resourceLevel > 1 && pop/maxPop < 0.9)
				return 0.0;
			w *= 0.01 * (pop / maxPop);
		}
		return w;
	}

	vec2i buildBuilding(AI& ai, Planets& planets, const BuildingType@ type, bool scatter = true) {
		if(type is null || !type.canBuildOn(obj))
			return vec2i(-1,-1);

		if(planets.log)
			ai.print("Attempt to construct "+type.name, obj);

		PlanetSurface@ surface = planets.surface;
		receive(obj.getPlanetSurface(), surface);

		//Find the best place to build this building
		int bestPenalty = INT_MAX;
		int possibs = 0;
		vec2i best;
		vec2i center = vec2i(type.getCenter());

		for(int x = 0, w = surface.size.x; x < w; ++x) {
			for(int y = 0, h = surface.size.y; y < h; ++y) {
				vec2i pos(x, y);

				bool valid = true;
				int penalty = 0;

				for(int xoff = 0; xoff < int(type.size.x); ++xoff) {
					for(int yoff = 0; yoff < int(type.size.y); ++yoff) {
						vec2i rpos = pos - center + vec2i(xoff, yoff);

						if(rpos.x < 0 || rpos.y < 0 || rpos.x >= w || rpos.y >= h) {
							valid = false;
							break;
						}

						auto@ biome = surface.getBiome(rpos.x, rpos.y);
						if(biome is null || !biome.buildable) {
							valid = false;
							break;
						}

						uint flags = surface.getFlags(rpos.x, rpos.y);
						if(flags & SuF_Usable == 0) {
							bool affinity = false;
							if(type.buildAffinities.length != 0) {
								for(uint i = 0, cnt = type.buildAffinities.length; i < cnt; ++i) {
									if(biome is type.buildAffinities[i].biome) {
										affinity = true;
										break;
									}
								}
							}
							if(!affinity && type.tileBuildCost > 0) {
								penalty += 1;

								if(biome.buildCost > 1.0)
									penalty += ceil((biome.buildCost - 1.0) / 0.1);
							}
							affinity = false;
							if(type.maintainAffinities.length != 0) {
								for(uint i = 0, cnt = type.maintainAffinities.length; i < cnt; ++i) {
									if(biome is type.maintainAffinities[i].biome) {
										affinity = true;
										break;
									}
								}
							}
							if(!affinity && type.tileMaintainCost > 0)
								penalty += 2;
						}

						auto@ bld = surface.getBuilding(rpos.x, rpos.y);
						if(bld !is null) {
							if(bld.type.civilian) {
								penalty += 2;
							}
							else {
								valid = false;
								break;
							}
						}
					}
					if(!valid)
						break;
				}

				if(valid) {
					if(penalty < bestPenalty) {
						possibs = 1;
						bestPenalty = penalty;
						best = pos;
					}
					else if(penalty == bestPenalty && scatter) {
						possibs += 1;
						if(randomd() < 1.0 / double(possibs))
							best = pos;
					}
				}
			}
		}

		if(bestPenalty != INT_MAX) {
			if(planets.log)
				ai.print("Construct "+type.name+" at "+best+" with penalty "+bestPenalty, obj);
			obj.buildBuilding(type.id, best);
			return best;
		}

		if(planets.log)
			ai.print("Could not find place to construct "+type.name, obj);
		return vec2i(-1,-1);
	}

	bool buildConstruction(AI& ai, Planets& planets, const ConstructionType@ type) {
		if(type is null || !type.canBuild(obj))
			return false;

			if(planets.log)
				ai.print("Construct "+type.name);
			obj.buildConstruction(type.id);

			return true;
	}
}

final class PotentialSource {
	Planet@ pl;
	double weight = 0;
};

final class AsteroidData {
	Asteroid@ asteroid;
	array<ExportData@>@ resources;

	void save(Planets& planets, SaveFile& file) {
		file << asteroid;

		uint cnt = 0;
		if(resources !is null)
			cnt = resources.length;

		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			planets.resources.saveExport(file, resources[i]);
	}

	void load(Planets& planets, SaveFile& file) {
		file >> asteroid;

		uint cnt = 0;
		file >> cnt;
		if(cnt != 0)
			@resources = array<ExportData@>();
		for(uint i = 0; i < cnt; ++i) {
			auto@ res = planets.resources.loadExport(file);
			if(res !is null)
				resources.insertLast(res);
		}
	}

	bool tick(AI& ai, Planets& planets) {
		if(asteroid is null || !asteroid.valid || asteroid.owner !is ai.empire) {
			planets.resources.killImportsTo(asteroid);
			planets.resources.killResourcesFrom(asteroid);
			return false;
		}
		if(resources is null) {
			@resources = planets.resources.availableResource(asteroid);
		}
		else {
			if(asteroid.nativeResourceCount != resources.length || (resources.length != 0 && asteroid.primaryResourceId != resources[0].resourceId))
				planets.updateResourceList(asteroid, resources);
		}
		return true;
	}
};

class Planets : AIComponent, AIConstructions {
	Events@ events;
	Resources@ resources;
	Budget@ budget;
	Systems@ systems;

	PlanetSurface surface;

	array<AsteroidData@> ownedAsteroids;
	array<PlanetAI@> planets;
	array<PlanetAI@> bumped;
	uint planetIdx = 0;


	array<BuildingRequest@> building;
	int nextBuildingRequestId = 0;
	array<ConstructionRequest@> constructionRequests;
	int nextConstructionRequestId = 0;

	void create() {
		@events = cast<Events>(ai.events);
		@resources = cast<Resources>(ai.resources);
		@budget = cast<Budget>(ai.budget);
		@systems = cast<Systems>(ai.systems);

		//Register specialized construction types
		for(uint i = 0, cnt = getConstructionTypeCount(); i < cnt; ++i) {
			auto@ type = getConstructionType(i);
			for(uint n = 0, ncnt = type.ai.length; n < ncnt; ++n) {
				auto@ hook = cast<ConstructionAIHook>(type.ai[n]);
				if(hook !is null)
					hook.register(this, type);
			}
		}
	}

	Empire@ get_empire() {
		return ai.empire;
	}

	Considerer@ get_consider() {
		return cast<Considerer>(ai.consider);
	}

	void registerUse(ConstructionUse use, const ConstructionType& type) {
		switch(use) {
			case CU_MoonBase:
				@ai.defs.MoonBase = type;
				break;
		}
	}

	void save(SaveFile& file) {
		file << nextBuildingRequestId;
		file << nextConstructionRequestId;

		uint cnt = planets.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ plAI = planets[i];
			saveAI(file, plAI);
			plAI.save(this, file);
		}

		cnt = building.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			saveBuildingRequest(file, building[i]);
			building[i].save(this, file);
		}

		cnt = constructionRequests.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			saveConstructionRequest(file, constructionRequests[i]);
			constructionRequests[i].save(this, file);
		}

		cnt = ownedAsteroids.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			ownedAsteroids[i].save(this, file);
	}

	void load(SaveFile& file) {
		file >> nextBuildingRequestId;
		file >> nextConstructionRequestId;

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ plAI = loadAI(file);
			if(plAI !is null)
				plAI.load(this, file);
			else
				PlanetAI().load(this, file);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ req = loadBuildingRequest(file);
			if(req !is null) {
				req.load(this, file);
				building.insertLast(req);
			}
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ req = loadConstructionRequest(file);
			if (req !is null) {
				req.load(this, file);
				constructionRequests.insertLast(req);
			}
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			AsteroidData data;
			data.load(this, file);
			if(data.asteroid !is null)
				ownedAsteroids.insertLast(data);
		}
	}

	PlanetAI@ loadAI(SaveFile& file) {
		Planet@ obj;
		file >> obj;

		if(obj is null)
			return null;

		PlanetAI@ plAI = getAI(obj);
		if(plAI is null) {
			@plAI = PlanetAI();
			@plAI.obj = obj;
			plAI.prevTick = gameTime;
			planets.insertLast(plAI);
		}
		return plAI;
	}

	void saveAI(SaveFile& file, PlanetAI@ ai) {
		Planet@ pl;
		if(ai !is null)
			@pl = ai.obj;
		file << pl;
	}

	array<BuildingRequest@> buildingLoadIds;
	BuildingRequest@ loadBuildingRequest(int id) {
		if(id == -1)
			return null;
		for(uint i = 0, cnt = buildingLoadIds.length; i < cnt; ++i) {
			if(buildingLoadIds[i].id == id)
				return buildingLoadIds[i];
		}
		BuildingRequest data;
		data.id = id;
		buildingLoadIds.insertLast(data);
		return data;
	}
	BuildingRequest@ loadBuildingRequest(SaveFile& file) {
		int id = -1;
		file >> id;
		if(id == -1)
			return null;
		else
			return loadBuildingRequest(id);
	}
	void saveBuildingRequest(SaveFile& file, BuildingRequest@ data) {
		int id = -1;
		if(data !is null)
			id = data.id;
		file << id;
	}
	array<ConstructionRequest@> constructionLoadIds;
	ConstructionRequest@ loadConstructionRequest(int id) {
		if(id == -1)
			return null;
		for(uint i = 0, cnt = constructionLoadIds.length; i < cnt; ++i) {
			if(constructionLoadIds[i].id == id)
				return constructionLoadIds[i];
		}
		ConstructionRequest data;
		data.id = id;
		constructionLoadIds.insertLast(data);
		return data;
	}
	ConstructionRequest@ loadConstructionRequest(SaveFile& file) {
		int id = -1;
		file >> id;
		if(id == -1)
			return null;
		else
			return loadConstructionRequest(id);
	}
	void saveConstructionRequest(SaveFile& file, ConstructionRequest@ data) {
		int id = -1;
		if(data !is null)
			id = data.id;
		file << id;
	}
	void postLoad(AI& ai) {
		buildingLoadIds.length = 0;
		constructionLoadIds.length= 0;
	}

	void start() {
		checkForPlanets();
	}

	void checkForPlanets() {
		auto@ data = ai.empire.getPlanets();
		Object@ obj;
		while(receive(data, obj)) {
			Planet@ pl = cast<Planet>(obj);
			if(pl !is null)
				register(cast<Planet>(obj));
		}
	}

	uint roidIdx = 0;
	void tick(double time) {
		double curTime = gameTime;

		if(planets.length != 0) {
			planetIdx = (planetIdx+1) % planets.length;

			auto@ plAI = planets[planetIdx];
			plAI.tick(ai, this, curTime - plAI.prevTick);
			plAI.prevTick = curTime;
		}

		for(int i = bumped.length-1; i >= 0; --i) {
			auto@ plAI = bumped[i];
			double tickTime = curTime - plAI.prevTick;
			if(tickTime != 0) {
				plAI.tick(ai, this, tickTime);
				plAI.prevTick = curTime;
			}
		}
		bumped.length = 0;

		if(ownedAsteroids.length != 0) {
			roidIdx = (roidIdx+1) % ownedAsteroids.length;
			if(!ownedAsteroids[roidIdx].tick(ai, this))
				ownedAsteroids.removeAt(roidIdx);
		}

		//Construct any buildings we are waiting on
		for(uint i = 0, cnt = building.length; i < cnt; ++i) {
			if(!building[i].tick(ai, this, time)) {
				building.removeAt(i);
				--i; --cnt;
				break;
			}
		}

		//Construct any constructions we are waiting on
		for(uint i = 0, cnt = constructionRequests.length; i < cnt; ++i) {
			if(!constructionRequests[i].tick(ai, this, time)) {
				constructionRequests.removeAt(i);
				--i; --cnt;
				break;
			}
		}
	}

	uint prevCount = 0;
	double checkTimer = 0;
	uint sysIdx = 0, ownedIdx = 0;
	void focusTick(double time) override {
		//Check for any newly obtained planets
		uint curCount = ai.empire.planetCount;
		checkTimer += time;
		if(curCount != prevCount || checkTimer > 60.0) {
			checkForPlanets();
			prevCount = curCount;
			checkTimer = 0;
		}

		//Find any asteroids we've gained
		if(systems.all.length != 0) {
			sysIdx = (sysIdx+1) % systems.all.length;
			auto@ sys = systems.all[sysIdx];
			for(uint i = 0, cnt = sys.asteroids.length; i < cnt; ++i)
				register(sys.asteroids[i]);
		}
		if(systems.owned.length != 0) {
			ownedIdx = (ownedIdx+1) % systems.owned.length;
			auto@ sys = systems.owned[ownedIdx];
			for(uint i = 0, cnt = sys.asteroids.length; i < cnt; ++i)
				register(sys.asteroids[i]);
		}
	}

	void bump(Planet@ pl) {
		if(pl !is null)
			bump(getAI(pl));
	}

	void bump(PlanetAI@ plAI) {
		if(plAI !is null)
			bumped.insertLast(plAI);
	}

	PlanetAI@ getAI(Planet& obj) {
		for(uint i = 0, cnt = planets.length; i < cnt; ++i) {
			if(planets[i].obj is obj)
				return planets[i];
		}
		return null;
	}

	PlanetAI@ register(Planet& obj) {
		PlanetAI@ plAI = getAI(obj);
		if(plAI is null) {
			@plAI = PlanetAI();
			@plAI.obj = obj;
			plAI.prevTick = gameTime;
			planets.insertLast(plAI);
			plAI.init(ai, this);
		}
		return plAI;
	}

	AsteroidData@ register(Asteroid@ obj) {
		if(obj is null || !obj.valid || obj.owner !is ai.empire)
			return null;
		for(uint i = 0, cnt = ownedAsteroids.length; i < cnt; ++i) {
			if(ownedAsteroids[i].asteroid is obj)
				return ownedAsteroids[i];
		}

		AsteroidData data;
		@data.asteroid = obj;
		ownedAsteroids.insertLast(data);

		if(log)
			ai.print("Detected asteroid: "+obj.name, obj.region);

		return data;
	}

	void remove(PlanetAI@ plAI) {
		resources.killImportsTo(plAI.obj);
		resources.killResourcesFrom(plAI.obj);
		plAI.remove(ai, this);
		planets.remove(plAI);
		bumped.remove(plAI);
	}

	void requestLevel(PlanetAI@ plAI, int toLevel, ImportData@ before = null) {
		if(plAI is null)
			return;
		plAI.targetLevel = toLevel;
		if(before !is null) {
			for(int lv = max(plAI.requestedLevel, 1); lv <= toLevel; ++lv)
				resources.organizeImports(plAI.obj, lv, before);
			plAI.requestedLevel = toLevel;
		}
		else {
			bump(plAI);
		}
	}

	BuildingRequest@ requestBuilding(PlanetAI@ plAI, const BuildingType@ type, double priority = 1.0, double expire = INFINITY, bool scatter = true, uint moneyType = BT_Development) {
		if(plAI is null)
			return null;

		if(log)
			ai.print("Requested building of type "+type.name, plAI.obj);

		BuildingRequest req(budget, type, priority, moneyType);
		req.scatter = scatter;
		req.id = nextBuildingRequestId++;
		req.expires = gameTime + expire;
		@req.plAI = plAI;

		building.insertLast(req);
		return req;
	}

	ConstructionRequest@ requestConstruction(PlanetAI@ plAI, Object@ buildAt, const ConstructionType@ type, double priority = 1.0, double expire = INFINITY, uint moneyType = BT_Development) {
		if(plAI is null)
			return null;

		if(log)
			ai.print("Requested construction of type "+type.name, plAI.obj);

		ConstructionRequest req(budget, buildAt, type, priority, moneyType);
		req.id = nextConstructionRequestId++;
		req.expires = gameTime + expire;
		@req.plAI = plAI;

		constructionRequests.insertLast(req);
		return req;
	}

	bool isBuilding(Planet@ planet, const BuildingType@ type) {
		for(uint i = 0, cnt = building.length; i < cnt; ++i) {
			if(building[i].type is type && building[i].plAI.obj is planet)
				return true;
		}
		return false;
	}

	bool isBuilding(Planet@ planet, const ConstructionType@ type) {
		for(uint i = 0, cnt = constructionRequests.length; i < cnt; ++i) {
			if(constructionRequests[i].type is type && constructionRequests[i].plAI.obj is planet)
				return true;
		}
		return false;
	}

	void getColonizeSources(array<PotentialSource@>& sources) {
		sources.length = 0;
		for(uint i = 0, cnt = planets.length; i < cnt; ++i) {
			auto@ plAI = planets[i];
			if(!plAI.obj.valid)
				continue;

			double w = plAI.colonizeWeight;
			if(w == 0)
				continue;
			if(plAI.obj.owner !is ai.empire)
				continue;

			PotentialSource src;
			@src.pl = planets[i].obj;
			src.weight = w;
			sources.insertLast(src);
		}
	}

	array<ExportData@> newResources;
	array<ExportData@> removedResources;
	array<Resource> checkResources;
	void updateResourceList(Object@ obj, array<ExportData@>& resList) {
		newResources.length = 0;
		removedResources = resList;

		checkResources.syncFrom(obj.getNativeResources());

		uint nativeCnt = checkResources.length;
		for(uint i = 0; i < nativeCnt; ++i) {
			int id = checkResources[i].id;

			bool found = false;
			for(uint n = 0, ncnt = removedResources.length; n < ncnt; ++n) {
				if(removedResources[n].resourceId == id) {
					removedResources.removeAt(n);
					found = true;
					break;
				}
			}

			if(!found) {
				auto@ type = checkResources[i].type;
				auto@ res = resources.availableResource(obj, type, id);

				if(i == 0)
					resList.insertAt(0, res);
				else
					resList.insertLast(res);
				newResources.insertLast(res);
			}
			else if(i == 0 && resList.length > 1 && resList[0].resourceId != id) {
				for(uint n = 0, ncnt = resList.length; n < ncnt; ++n) {
					if(resList[n].resourceId == id) {
						auto@ res = resList[n];
						resList.removeAt(n);
						resList.insertAt(0, res);
						break;
					}
				}
			}
		}

		//Get rid of resources we no longer have
		for(uint i = 0, cnt = removedResources.length; i < cnt; ++i) {
			resources.removeResource(removedResources[i]);
			resList.remove(removedResources[i]);
		}

		//Tell the resources component to try to immediately use the new resources
		for(uint i = 0, cnt = newResources.length; i < cnt; ++i)
			resources.checkReplaceCurrent(newResources[i]);
	}
};

AIComponent@ createPlanets() {
	return Planets();
}

Added scripts/server/empire_ai/weasel/Relations.as.










































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// Relations
// ---------
// Manages the relationships we have with other empires, including treaties, hatred, and wars.
//

import empire_ai.weasel.WeaselAI;

import empire_ai.weasel.Intelligence;
import empire_ai.weasel.Fleets;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Planets;

import warandpeace;
import influence;
from influence_global import activeTreaties, influenceLock, joinTreaty, leaveTreaty, declineTreaty, Treaty, sendPeaceOffer, createTreaty, offerSurrender, demandSurrender, leaveTreatiesWith;

enum HateType {
	HT_SystemPresence,
	HT_FleetPresence,
	HT_COUNT
};

class Hate {
	uint type;
	double amount = 0.0;
	Object@ obj;
	SystemAI@ sys;

	void save(Relations& relations, SaveFile& file) {
		file << type;
		file << amount;
		file << obj;
		relations.systems.saveAI(file, sys);
	}

	void load(Relations& relations, SaveFile& file) {
		file >> type;
		file >> amount;
		file >> obj;
		@sys = relations.systems.loadAI(file);
	}

	bool get_valid() {
		if(type == HT_SystemPresence)
			return sys !is null;
		if(type == HT_FleetPresence)
			return obj !is null && sys !is null;
		return true;
	}

	bool update(AI& ai, Relations& relations, Relation& rel, double time) {
		if(type == HT_SystemPresence) {
			amount = 0.25;
			if(sys.seenPresent & rel.empire.mask == 0)
				return false;
			if(sys.seenPresent & ai.empire.mask == 0)
				return false;
		}
		else if(type == HT_FleetPresence) {
			if(!obj.valid || obj.owner !is rel.empire)
				return false;
			if(sys.seenPresent & ai.empire.mask == 0)
				return false;
			if(obj.region !is sys.obj)
				return false;
			if(obj.getFleetStrength() < 1000.0)
				amount = 0.1;
			else
				amount = 0.5;
		}

		rel.hate += amount * time;
		return true;
	}

	string dump() {
		switch(type) {
			case HT_SystemPresence:
				return "system presence in "+sys.obj.name;
			case HT_FleetPresence:
				return "fleet presence "+obj.name+" in "+sys.obj.name;
		}
		return "unknown";
	}
};

final class Relation {
	Empire@ empire;

	//Whether we've met this empire
	bool contacted = false;

	//Whether we're currently at war
	bool atWar = false;

	//Last time we tried to make peace
	double lastPeaceTry = 0;

	//Whether this is our war of aggression
	bool aggressive = false;

	//Whether this is our ally
	bool allied = false;

	//Our relationship data
	double hate = 0.0;
	array<Hate@> hates;

	//Masks
	uint borderedTo = 0;
	uint alliedTo = 0;

	//Whether we consider this empire a threat to us
	bool isThreat = false;

	//How much we would value having this empire as an ally
	double allyValue = 0.0;
	//How much we think we can beat this empire and all its allies
	double defeatable = 0.0;
	//Relative strength of this empire to us in a vacuum
	double relStrength = 0.0;

	//How much we've lost to them in this recent war
	double warLost = 0.0;
	//How much we've taken from them in this recent war
	double warTaken = 0.0;

	void save(Relations& relations, SaveFile& file) {
		file << contacted;
		file << atWar;
		file << aggressive;
		file << allied;

		file << hate;
		uint cnt = hates.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			hates[i].save(relations, file);

		file << borderedTo;
		file << alliedTo;
		file << isThreat;
		file << allyValue;
		file << defeatable;
		file << relStrength;
		file << warTaken;
		file << warLost;
		file << lastPeaceTry;
	}

	void load(Relations& relations, SaveFile& file) {
		file >> contacted;
		file >> atWar;
		file >> aggressive;
		file >> allied;

		file >> hate;
		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			Hate ht;
			ht.load(relations, file);
			if(ht.valid)
				hates.insertLast(ht);
		}

		file >> borderedTo;
		file >> alliedTo;
		file >> isThreat;
		file >> allyValue;
		file >> defeatable;
		file >> relStrength;
		file >> warTaken;
		file >> warLost;
		file >> lastPeaceTry;
	}

	void trackSystem(AI& ai, Relations& relations, SystemAI@ sys) {
		for(uint i = 0, cnt = hates.length; i < cnt; ++i) {
			auto@ ht = hates[i];
			if(ht.type != HT_SystemPresence)
				continue;
			if(ht.sys is sys)
				return;
		}

		Hate ht;
		ht.type = HT_SystemPresence;
		@ht.sys = sys;
		hates.insertLast(ht);

		if(relations.log)
			ai.print("Gain hate of "+empire.name+": "+ht.dump());
	}

	void trackFleet(AI& ai, Relations& relations, FleetIntel@ intel, SystemAI@ sys) {
		for(uint i = 0, cnt = hates.length; i < cnt; ++i) {
			auto@ ht = hates[i];
			if(ht.type != HT_FleetPresence)
				continue;
			if(ht.obj is intel.obj && ht.sys is sys)
				return;
		}

		Hate ht;
		ht.type = HT_FleetPresence;
		@ht.sys = sys;
		@ht.obj = intel.obj;
		hates.insertLast(ht);

		if(relations.log)
			ai.print("Gain hate of "+empire.name+": "+ht.dump());
	}

	void tick(AI& ai, Relations& relations, double time) {
		if(!contacted) {
			if(ai.empire.ContactMask & empire.mask != 0)
				contacted = true;
		}

		bool curWar = ai.empire.isHostile(empire);
		if(curWar != atWar)
			atWar = curWar;
		if(!atWar) {
			aggressive = false;
			warLost = 0.0;
			warTaken = 0.0;
			lastPeaceTry = 0.0;
		}

		borderedTo = relations.intelligence.get(empire).borderedTo;
		alliedTo = empire.mask | empire.mutualDefenseMask | empire.ForcedPeaceMask.value;

		defeatable = relations.intelligence.defeatability(alliedTo, ai.mask | ai.allyMask);
		relStrength = relations.intelligence.defeatability(ai.mask, empire.mask);
		isThreat = defeatable < 0.8 && (borderedTo & ai.empire.mask) != 0;

		//Check how valuable of an ally this empire would make
		allyValue = 1.0;
		for(uint i = 0, cnt = relations.relations.length; i < cnt; ++i) {
			auto@ other = relations.relations[i];
			if(other is null || other is this || other.empire is null)
				continue;
			if(other.borderedTo & empire.mask == 0)
				continue;
			if(alliedTo & empire.mask != 0)
				continue;

			if(other.atWar)
				allyValue *= 3.0;
			else if(other.isThreat)
				allyValue *= 1.5;
		}

		//Become aggressive here if we're aggressive against one of its allies
		if(atWar && !aggressive) {
			for(uint i = 0, cnt = relations.relations.length; i < cnt; ++i) {
				auto@ other = relations.relations[i];
				if(other is null || other is this || other.empire is null)
					continue;
				if(other.aggressive && this.alliedTo & other.empire.mask != 0) {
					aggressive = true;
					break;
				}
			}
		}

		//Update our hatred of them
		for(uint i = 0, cnt = hates.length; i < cnt; ++i) {
			if(!hates[i].update(ai, relations, this, time)) {
				if(relations.log)
					ai.print("Hate with "+empire.name+" expired: "+hates[i].dump());
				hates.removeAt(i);
				--i; --cnt;
			}
		}

		hate *= pow(ai.behavior.hateDecayRate, time / 60.0);
		if(ai.behavior.biased && !empire.isAI)
			hate += 1.0;

		if(ai.behavior.forbidDiplomacy) return;

		//If we really really hate them, declare war
		if(!atWar || !aggressive) {
			double reqHate = 100.0;
			if(defeatable < 1.0)
				reqHate *= sqr(1.0 / defeatable);
			reqHate *= pow(2.0, relations.warCount());

			if(hate > reqHate && (!ai.behavior.passive || atWar) && defeatable >= ai.behavior.hatredWarOverkill) {
				//Make sure our other requirements for war are met
				if(relations.fleets.haveCombatReadyFleets()) {
					if(canDeclareWar(ai)) {
						if(relations.log)
							ai.print("Declaring hatred war on "+empire.name+": "+hate+" / "+reqHate);
						if(atWar)
							aggressive = true;
						else
							relations.declareWar(empire, aggressive=true);
					}
				}
			}
		}
	}

	bool isAllied(AI& ai) {
		return alliedTo & ai.empire.mask != 0;
	}

	bool canDeclareWar(AI& ai) {
		if(empire.SubjugatedBy !is null)
			return false;
		if(ai.empire.SubjugatedBy !is null)
			return false;
		if(!contacted)
			return false;
		if(ai.empire.ForcedPeaceMask & empire.mask != 0)
			return false;
		return true;
	}
};

class Relations : AIComponent {
	Intelligence@ intelligence;
	Systems@ systems;
	Fleets@ fleets;
	Planets@ planets;

	array<Relation@> relations;

	bool expansionLocked = false;
	double treatyRespond = 0;
	double treatyConsider = 0;

	double warPoints = 0.0;

	void create() {
		@intelligence = cast<Intelligence>(ai.intelligence);
		@fleets = cast<Fleets>(ai.fleets);
		@systems = cast<Systems>(ai.systems);
		@planets = cast<Planets>(ai.planets);
	}

	void start() {
		for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
			Empire@ emp = getEmpire(i);
			if(emp is ai.empire)
				continue;
			if(!emp.major)
				continue;

			Relation r;
			@r.empire = emp;

			if(relations.length <= uint(emp.index))
				relations.length = uint(emp.index)+1;
			@relations[emp.index] = r;
		}
	}

	void save(SaveFile& file) {
		uint cnt = relations.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			if(relations[i] is null) {
				file.write0();
				continue;
			}

			file.write1();
			relations[i].save(this, file);
		}

		file << expansionLocked;
		file << treatyRespond;
		file << treatyConsider;
	}

	void load(SaveFile& file) {
		uint cnt = 0;
		file >> cnt;
		relations.length = cnt;

		for(uint i = 0; i < cnt; ++i) {
			if(!file.readBit())
				continue;

			@relations[i] = Relation();
			@relations[i].empire = getEmpire(i);
			relations[i].load(this, file);
		}

		file >> expansionLocked;
		file >> treatyRespond;
		file >> treatyConsider;
	}

	double getPointValue(Object@ obj) {
		if(obj is null)
			return 0.0;
		if(obj.isShip) {
			auto@ dsg = cast<Ship>(obj).blueprint.design;
			if(dsg !is null)
				return dsg.size;
		}
		else if(obj.isPlanet) {
			return 10.0 * pow(3.0, double(obj.level));
		}
		return 0.0;
	}

	void recordTakenFrom(Empire& emp, double amount) {
		if(!emp.valid)
			return;
		if(log)
			ai.print("Taken value "+amount+" from "+emp.name);
		auto@ rel = get(emp);
		if(rel !is null)
			rel.warTaken += amount;
	}

	void recordLostTo(Empire& emp, double amount) {
		if(!emp.valid)
			return;
		if(log)
			ai.print("Lost value "+amount+" to "+emp.name);
		auto@ rel = get(emp);
		if(rel !is null)
			rel.warLost += amount;
	}

	void recordLostTo(Empire& emp, Object@ obj) {
		recordLostTo(emp, getPointValue(obj));
	}

	void recordTakenFrom(Empire& emp, Object@ obj) {
		recordTakenFrom(emp, getPointValue(obj));
	}

	Relation@ get(Empire@ emp) {
		if(emp is null)
			return null;
		if(!emp.major)
			return null;
		if(uint(emp.index) >= relations.length)
			return null;
		return relations[emp.index];
	}

	bool isFightingWar(bool aggressive = false) {
		for(uint i = 0, cnt = relations.length; i < cnt; ++i) {
			if(relations[i] is null)
				continue;
			if(relations[i].atWar) {
				if(!aggressive || relations[i].aggressive)
					return true;
			}
		}
		return false;
	}

	uint warCount() {
		uint count = 0;
		for(uint i = 0, cnt = relations.length; i < cnt; ++i) {
			if(relations[i] is null)
				continue;
			if(relations[i].atWar)
				count += 1;
		}
		return count;
	}

	void declareWar(Empire@ onEmpire, bool aggressive = true) {
		//Break all treaties
		leaveTreatiesWith(ai.empire, onEmpire.mask);

		//Declare actual war
		auto@ rel = get(onEmpire);
		rel.aggressive = aggressive;
		::declareWar(ai.empire, onEmpire);
	}

	uint sysIdx = 0;
	uint relIdx = 0;
	void tick(double time) override {
		//Find new ways to hate other empires
		if(systems.all.length != 0) {
			sysIdx = (sysIdx+1) % systems.all.length;
			auto@ sys = systems.all[sysIdx];
			if(sys.owned && sys.seenPresent & ~ai.mask != 0) {
				for(uint i = 0, cnt = relations.length; i < cnt; ++i) {
					auto@ rel = relations[i];
					if(rel is null)
						continue;
					if(sys.seenPresent & rel.empire.mask != 0)
						rel.trackSystem(ai, this, sys);
				}
			}
		}

		if(relations.length != 0) {
			relIdx = (relIdx+1) % relations.length;
			auto@ rel = relations[relIdx];
			auto@ itl = intelligence.intel[relIdx];
			if(rel !is null && itl !is null) {
				for(uint i = 0, cnt = itl.fleets.length; i < cnt; ++i) {
					if(!itl.fleets[i].visible)
						continue;

					auto@ inSys = systems.getAI(itl.fleets[i].obj.region);
					if(inSys !is null && inSys.owned)
						rel.trackFleet(ai, this, itl.fleets[i], inSys);
				}
			}
		}
	}

	uint relInd = 0;
	void focusTick(double time) override {
		//Update our current relations
		if(relations.length != 0) {
			relInd = (relInd+1) % relations.length;
			if(relations[relInd] !is null)
				relations[relInd].tick(ai, this, time);
		}

		//Compute how many points we have in total that can be taken
		warPoints = 0.0;
		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			Ship@ ship = cast<Ship>(fleets.fleets[i].obj);
			if(ship !is null && ship.valid && ship.owner is ai.empire)
				warPoints += getPointValue(ship);
		}
		for(uint i = 0, cnt = planets.planets.length; i < cnt; ++i) {
			Planet@ pl = cast<Planet>(planets.planets[i].obj);
			if(pl !is null && pl.valid && pl.owner is ai.empire)
				warPoints += getPointValue(pl);
		}

		//Become aggressive if we cannot expand anywhere anymore
		expansionLocked = true;
		for(uint i = 0, cnt = systems.outsideBorder.length; i < cnt; ++i) {
			auto@ sys = systems.outsideBorder[i];
			if(sys.seenPresent == 0) {
				bool havePlanets = false;
				for(uint n = 0, ncnt = sys.planets.length; n < ncnt; ++n) {
					if(sys.planets[n].quarantined)
						continue;
					havePlanets = true;
					break;
				}
				if(havePlanets) {
					expansionLocked = false;
					break;
				}
			}
		}

		if(ai.behavior.forbidDiplomacy) return;

		//Deal with our AI's aggressive behavior
		if(ai.behavior.aggressive || (expansionLocked && ai.behavior.aggressiveWhenBoxedIn && !ai.behavior.passive)) {
			//Try to make sure we're always fighting at least one aggressive war
			bool atWar = false, aggro = false;
			for(uint i = 0, cnt = relations.length; i < cnt; ++i) {
				if(relations[i] is null)
					continue;
				if(relations[i].atWar) {
					atWar = true;
					if(relations[i].aggressive)
						aggro = true;
				}
			}

			if(!atWar) {
				if(fleets.haveCombatReadyFleets()) {
					//Declare war on people who share our border and are defeatable
					Empire@ best;
					double bestWeight = 0;

					for(uint i = 0, cnt = relations.length; i < cnt; ++i) {
						auto@ rel = relations[i];
						if(rel is null)
							continue;

						auto@ intel = intelligence.get(rel.empire);
						if(intel.shared.length == 0 && intel.theirBorder.length == 0)
							continue;
						if(!rel.canDeclareWar(ai))
							continue;
						if(!ai.behavior.biased || rel.empire.isAI) {
							if(rel.defeatable < ai.behavior.aggressiveWarOverkill)
								continue;
						}

						double w = rel.defeatable * rel.hate;
						if(rel.isAllied(ai))
							w *= 0.01;
						if(w > bestWeight) {
							bestWeight = w;
							@best = rel.empire;
						}
					}

					if(best !is null) {
						if(log)
							ai.print("Declare aggressive war against "+best.name);
						declareWar(best, aggressive=true);
					}
				}
			}
			else if(!aggro) {
				//Start going aggressive on someone defeatable we are already at war with
				Empire@ best;
				double bestWeight = 0;

				for(uint i = 0, cnt = relations.length; i < cnt; ++i) {
					auto@ rel = relations[i];
					if(rel is null)
						continue;
					if(!rel.atWar)
						continue;
					if(rel.defeatable < ai.behavior.aggressiveWarOverkill)
						continue;

					double w = rel.defeatable * rel.hate;
					if(w > bestWeight) {
						bestWeight = w;
						@best = rel.empire;
					}
				}

				if(best !is null) {
					//Go aggressive then!
					if(log)
						ai.print("Become aggressive against "+best.name);
					get(best).aggressive = true;
				}
			}
		}

		//Respond to treaties
		if(gameTime > treatyRespond) {
			treatyRespond = gameTime + randomd(8.0, 20.0);

			Treaty@ respondTreaty;

			{
				Lock lck(influenceLock);
				for(uint i = 0, cnt = activeTreaties.length; i < cnt; ++i) {
					auto@ trty = activeTreaties[i];
					if(trty.inviteMask & ai.mask != 0 && trty.presentMask & ai.mask == 0) {
						Message msg;
						trty.write(msg);

						@respondTreaty = Treaty();
						respondTreaty.read(msg);
						break;
					}
				}
			}

			if(respondTreaty !is null) {
				bool accept = false;
				Empire@ invitedBy = respondTreaty.leader;
				if(invitedBy is null)
					@invitedBy = respondTreaty.joinedEmpires[0];
				Relation@ other = get(invitedBy);

				if(respondTreaty.hasClause("SubjugateClause")) {
					//This is a surrender offer or demand
					if(respondTreaty.leader is null) {
						//An offer
						accept = true;
					}
					else if(respondTreaty.joinedEmpires.length != 0) {
						//A demand
						auto@ other = get(respondTreaty.joinedEmpires[0]);
						if(other.defeatable < ai.behavior.surrenderMinStrength) {
							if(warPoints / (other.warLost + warPoints) < ai.behavior.acceptSurrenderRatio) {
								accept = true;
							}
						}
					}
				}
				else if(respondTreaty.hasClause("MutualDefenseClause")
						|| respondTreaty.hasClause("AllianceClause")) {
					//This is an alliance treaty
					if(other.atWar) {
						//Need to be at peace first
						accept = false;
					}
					else {
						//See if this empire can help us defeat someone
						if(other.allyValue >= 3.0 && other.relStrength >= 0.5)
							accept = true;
					}
				}
				else if(respondTreaty.hasClause("PeaceClause")) {
					//This is a peace offering
					accept = shouldPeace(other);
				}
				else if(respondTreaty.hasClause("VisionClause")) {
					//This is a vision sharing treaty
					if(other !is null)
						accept = !other.isThreat && !other.atWar && other.hate <= 50.0;
				}
				else if(respondTreaty.hasClause("TradeClause")) {
					//This is a trade sharing treaty
					if(other !is null)
						accept = !other.isThreat && !other.atWar && other.hate <= 10.0;
				}

				if(accept) {
					if(log)
						ai.print("Accept treaty: "+respondTreaty.name, emp=invitedBy);
					joinTreaty(ai.empire, respondTreaty.id);
				}
				else {
					if(log)
						ai.print("Reject treaty: "+respondTreaty.name, emp=invitedBy);
					declineTreaty(ai.empire, respondTreaty.id);
				}
			}
		}

		//See if we should send a treaty over to someone
		if(gameTime > treatyConsider) {
			treatyConsider = gameTime + randomd(100.0, 300.0);

			uint offset = randomi(0, relations.length-1);
			for(uint i = 0, cnt = relations.length; i < cnt; ++i) {
				auto@ other = relations[(i+offset) % cnt];
				if(other is null)
					continue;

				//Check if we should make peace with them
				if(other.atWar) {
					if(other.lastPeaceTry < gameTime - 600.0 && shouldPeace(other, isOffer=true)) {
						if(other.aggressive)
							other.aggressive = false;
						if(log)
							ai.print("Send peace offer.", emp=other.empire);
						other.lastPeaceTry = gameTime;
						sendPeaceOffer(ai.empire, other.empire);
						break;
					}
				}

				if(other.atWar) {
					//Check if we should try to surrender to them
					if(other.defeatable < ai.behavior.surrenderMinStrength) {
						if(warPoints / (other.warLost + warPoints) < ai.behavior.offerSurrenderRatio) {
							if(log)
								ai.print("Send surrender offer.", emp=other.empire);
							offerSurrender(ai.empire, other.empire);
							break;
						}
					}

					//Check if we should try to demand their surrender
					if(other.defeatable >= 1.0 / ai.behavior.surrenderMinStrength && other.warTaken >= warPoints * 0.1) {
						if(log)
							ai.print("Demand surrender.", emp=other.empire);
						demandSurrender(ai.empire, other.empire);
						break;
					}
				}

				//Check if we should try to ally with them
				if(!other.atWar && !other.isThreat && other.allyValue >= 3.0) {
					Treaty treaty;
					treaty.addClause(getInfluenceClauseType("AllianceClause"));
					treaty.addClause(getInfluenceClauseType("VisionClause"));
					treaty.addClause(getInfluenceClauseType("MutualDefenseClause"));

					if(treaty.canInvite(ai.empire, other.empire)) {
						treaty.inviteMask = other.empire.mask;

						//Generate treaty name
						string genName;
						uint genCount = 0;
						for(uint i = 0, cnt = systemCount; i < cnt; ++i) {
							auto@ reg = getSystem(i).object;
							if(reg.TradeMask & (ai.mask | other.empire.mask) != 0) {
								genCount += 1;
								if(randomd() < 1.0 / double(genCount))
									genName = reg.name;
							}
						}
						treaty.name = format(locale::TREATY_NAME_GEN, genName);

						if(log)
							ai.print("Send alliance offer.", emp=other.empire);
						createTreaty(ai.empire, treaty);
					}
				}
			}
		}
	}

	bool shouldPeace(Relation@ other, bool isOffer = false) {
		bool accept = false;
		if(other.aggressive) {
			//We're trying to conquer these people, don't accept peace unless
			//we're fighting someone scarier or we're losing
			double otherWar = 0.0;
			uint otherInd = uint(-1);
			for(uint i = 0, cnt = relations.length; i < cnt; ++i) {
				auto@ rel = relations[i];
				if(rel is null || rel is other)
					continue;
				if(rel.empire.mask & other.alliedTo != 0)
					continue;
				if(!rel.atWar)
					continue;
				otherWar = max(otherWar, rel.defeatable);
				otherInd = i;
			}

			if(otherInd != uint(-1) && otherWar < other.defeatable) {
				accept = true;
				if(!relations[otherInd].aggressive)
					relations[otherInd].aggressive = otherWar >= ai.behavior.aggressiveWarOverkill;
			}
			else if(other.defeatable < 0.25) {
				accept = true;
			}
		}
		else {
			//We don't have any ~particular qualms with these people, peace should be good
			if(!isOffer) {
				if(other.defeatable < 0.5 || other.hate < 50.0)
					accept = true;
			}
		}
		return accept;
	}

	void turn() override {
		if(log) {
			ai.print("Relations Report on Empires:");
			ai.print(" war points: "+warPoints);
			for(uint i = 0, cnt = relations.length; i < cnt; ++i) {
				auto@ rel = relations[i];
				if(rel is null)
					continue;
				ai.print(" "+ai.pad(rel.empire.name, 15)
						+" war: "+ai.pad(rel.atWar+" / "+rel.aggressive, 15)
						+" threat: "+ai.pad(""+rel.isThreat, 8)
						+" defeatable: "+ai.pad(toString(rel.defeatable,2), 8)
						+" hate: "+ai.pad(toString(rel.hate,0), 8)
						+" ally value: "+ai.pad(toString(rel.allyValue,1), 8)
						+" taken: "+ai.pad(toString(rel.warTaken,1), 8)
						+" lost: "+ai.pad(toString(rel.warLost,1), 8)
				);
			}
		}
	}
};

AIComponent@ createRelations() {
	return Relations();
}

void relationRecordLost(AI& ai, Empire& emp, Object@ obj) {
	cast<Relations>(ai.relations).recordLostTo(emp, obj);
}

Added scripts/server/empire_ai/weasel/Research.as.














































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// Research
// --------
// Spends research points to unlock and improve things in the research grid.
//

import empire_ai.weasel.WeaselAI;

import empire_ai.weasel.Development;

import research;

const double baseAimResearchRate = 2.0;

class Research : AIComponent {
	Development@ development;

	TechnologyGrid grid;
	array<TechnologyNode@> immediateQueue;

	void create() {
		@development = cast<Development>(ai.development);
	}

	void save(SaveFile& file) {
		uint cnt = immediateQueue.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << immediateQueue[i].id;
	}

	void load(SaveFile& file) {
		updateGrid();

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			int id = 0;
			file >> id;

			for(uint i = 0, cnt = grid.nodes.length; i < cnt; ++i) {
				if(grid.nodes[i].id == id) {
					immediateQueue.insertLast(grid.nodes[i]);
					break;
				}
			}
		}

	}

	void updateGrid() {
		//Receive the full grid from the empire to path on
		grid.nodes.length = 0;

		DataList@ recvData = ai.empire.getTechnologyNodes();
		TechnologyNode@ node = TechnologyNode();
		while(receive(recvData, node)) {
			grid.nodes.insertLast(node);
			@node = TechnologyNode();
		}

		grid.regenBounds();
	}

	double getEndPointWeight(const TechnologyType& tech) {
		//TODO: Might want to make this configurable by data file
		return 1.0;
	}

	bool isEndPoint(const TechnologyType& tech) {
		return tech.cls >= Tech_BigUpgrade;
	}

	double findResearch(int atIndex, array<TechnologyNode@>& path, array<bool>& visited, bool initial = false) {
		if(visited[atIndex])
			return 0.0;
		visited[atIndex] = true;

		auto@ node = grid.nodes[atIndex];
		if(!initial) {
			if(node.bought)
				return 0.0;
			if(!node.hasRequirements(ai.empire))
				return 0.0;

			path.insertLast(node);

			if(isEndPoint(node.type))
				return getEndPointWeight(node.type);
		}

		vec2i startPos = node.position;
		double totalWeight = 0.0;

		array<TechnologyNode@> tmp;
		array<TechnologyNode@> chosen;
		tmp.reserve(20);
		chosen.reserve(20);

		for(uint d = 0; d < 6; ++d) {
			vec2i otherPos = startPos;
			if(grid.doAdvance(otherPos, HexGridAdjacency(d))) {
				int otherIndex = grid.getIndex(otherPos);
				if(otherIndex != -1) {
					tmp.length = 0;
					double w = findResearch(otherIndex, tmp, visited);
					if(w != 0.0) {
						totalWeight += w;
						if(randomd() < w / totalWeight) {
							chosen = tmp;
						}
					}
				}
			}
		}

		for(uint i = 0, cnt = chosen.length; i < cnt; ++i)
			path.insertLast(chosen[i]);
		return max(totalWeight, 0.01);
	}

	void queueNewResearch() {
		if(log)
			ai.print("Attempted to find new research to queue");

		//Update our grid representation
		updateGrid();

		//Find a good path to do
		array<bool> visited(grid.nodes.length, false);

		double totalWeight = 0.0;

		auto@ path = array<TechnologyNode@>();
		auto@ tmp = array<TechnologyNode@>();
		path.reserve(20);
		tmp.reserve(20);

		for(int i = 0, cnt = grid.nodes.length; i < cnt; ++i) {
			if(grid.nodes[i].bought) {
				tmp.length = 0;
				double weight = findResearch(i, tmp, visited, initial=true);
				if(weight != 0.0) {
					totalWeight += weight;
					if(randomd() < weight / totalWeight) {
						auto@ swp = path;
						@path = tmp;
						@tmp = swp;
					}
				}
			}
		}

		if(path.length != 0) {
			for(uint i = 0, cnt = path.length; i < cnt; ++i) {
				if(log)
					ai.print("Queue research: "+path[i].type.name+" at "+path[i].position);
				immediateQueue.insertLast(path[i]);
			}
		}
	}

	double immTimer = randomd(10.0, 60.0);
	void focusTick(double time) override {
		if (ai.behavior.forbidResearch) return;

		//Queue some new research if we have to
		if(immediateQueue.length == 0) {
			immTimer -= time;
			if(immTimer <= 0.0) {
				immTimer = 60.0;
				queueNewResearch();
			}
		}
		else {
			immTimer = 0.0;
		}

		//Deal with current queued research
		if(immediateQueue.length != 0) {
			auto@ node = immediateQueue[0];
			if(!receive(ai.empire.getTechnologyNode(node.id), node)) {
				immediateQueue.removeAt(0);
			}
			else if(!node.available || node.bought) {
				immediateQueue.removeAt(0);
			}
			else {
				double cost = node.getPointCost(ai.empire);
				if(cost == 0) {
					//Try it once and then give up
					ai.empire.research(node.id, secondary=true);
					immediateQueue.removeAt(0);

					if(log)
						ai.print("Attempt secondary research: "+node.type.name+" at "+node.position);
				}
				else if(cost <= ai.empire.ResearchPoints) {
					//If we have enough to buy it, buy it
					ai.empire.research(node.id);
					immediateQueue.removeAt(0);

					if(log)
						ai.print("Purchase research: "+node.type.name+" at "+node.position);
				}
			}
		}

		//Update research generation rate goal
		development.aimResearchRate = clamp(gameTime / (20.0 * 60.0) - 0.5, 0.0, baseAimResearchRate);
	}
};

AIComponent@ createResearch() {
	return Research();
}

Added scripts/server/empire_ai/weasel/Resources.as.


















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;

import empire_ai.weasel.Events;

import empire_ai.weasel.ImportData;

import ai.events;

import resources;
import planet_levels;
import system_pathing;
import systems;

from orbitals import OrbitalModule;

interface RaceResources {
	void levelRequirements(Object& obj, int targetLevel, array<ResourceSpec@>& specs);
};

final class Resources : AIComponent {
	Events@ events;
	
	RaceResources@ race;

	array<ImportData@> requested;
	array<ImportData@> active;
	int nextImportId = 0;

	array<ExportData@> available;
	array<ExportData@> used;
	int nextExportId = 0;

	void create() {
		@events = cast<Events>(ai.events);
		@race = cast<RaceResources>(ai.race);
	}

	void save(SaveFile& file) {
		file << nextImportId;
		file << nextExportId;

		uint cnt = 0;

		cnt = requested.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			saveImport(file, requested[i]);
			file << requested[i];
		}

		cnt = active.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			saveImport(file, active[i]);
			file << active[i];
		}

		cnt = available.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			saveExport(file, available[i]);
			file << available[i];
			saveImport(file, available[i].request);
		}

		cnt = used.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			saveExport(file, used[i]);
			file << used[i];
			saveImport(file, used[i].request);
		}
	}

	void load(SaveFile& file) {
		file >> nextImportId;
		file >> nextExportId;

		uint cnt = 0;

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = loadImport(file);
			file >> data;
			requested.insertLast(data);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = loadImport(file);
			file >> data;
			active.insertLast(data);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = loadExport(file);
			file >> data;
			@data.request = loadImport(file);
			available.insertLast(data);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = loadExport(file);
			file >> data;
			@data.request = loadImport(file);
			used.insertLast(data);
		}
	}

	array<ImportData@> importIds;
	ImportData@ loadImport(int id) {
		if(id == -1)
			return null;
		for(uint i = 0, cnt = importIds.length; i < cnt; ++i) {
			if(importIds[i].id == id)
				return importIds[i];
		}
		ImportData data;
		data.id = id;
		importIds.insertLast(data);
		return data;
	}
	ImportData@ loadImport(SaveFile& file) {
		int id = -1;
		file >> id;
		if(id == -1)
			return null;
		else
			return loadImport(id);
	}
	void saveImport(SaveFile& file, ImportData@ data) {
		int id = -1;
		if(data !is null)
			id = data.id;
		file << id;
	}
	array<ExportData@> exportIds;
	ExportData@ loadExport(int id) {
		if(id == -1)
			return null;
		for(uint i = 0, cnt = exportIds.length; i < cnt; ++i) {
			if(exportIds[i].id == id)
				return exportIds[i];
		}
		ExportData data;
		data.id = id;
		exportIds.insertLast(data);
		return data;
	}
	ExportData@ loadExport(SaveFile& file) {
		int id = -1;
		file >> id;
		if(id == -1)
			return null;
		else
			return loadExport(id);
	}
	void saveExport(SaveFile& file, ExportData@ data) {
		int id = -1;
		if(data !is null)
			id = data.id;
		file << id;
	}
	void postLoad(AI& ai) {
		importIds.length = 0;
		exportIds.length = 0;
	}

	void start() {
		focusTick(0);
	}

	void tick(double time) {
	}

	uint checkIdx = 0;
	void focusTick(double time) {
		//Do a check to make sure our resource export setup is still correct
		if(used.length != 0) {
			checkIdx = (checkIdx+1) % used.length;
			ExportData@ res = used[checkIdx];
			if(res.request !is null && res.request.obj !is null && !res.isExportedTo(res.request.obj)) {
				if(log)
					ai.print("Break export to "+res.request.obj.name+": link changed underfoot", res.obj);
				breakImport(res);
			}
			else {
				bool valid = true;
				if(res.obj is null || res.obj.owner !is ai.empire || !res.obj.valid)
					valid = false;
				//Don't break these imports, we want to wait for the decay to happen
				else if((res.request is null || !res.request.obj.hasSurfaceComponent || res.request.obj.decayTime <= 0) && !res.obj.isAsteroid && !res.usable) {
					valid = false;
				}
				else if(res.request !is null) {
					if(res.request.obj.owner !is ai.empire || !res.request.obj.valid)
						valid = false;
				}
				if(!valid)
					breakImport(res);
			}

		}

		//TODO: Make sure universal unique only applies once per planet

		//Match requested with available
		for(uint i = 0, cnt = requested.length; i < cnt; ++i) {
			auto@ req = requested[i];
			req.cycled = true;

			if(req.obj is null)
				continue;
			if(req.beingMet) {
				ai.print("Error: Requested is being met", req.obj);
				continue;
			}

			ExportData@ source;
			double sourceWeight = 0.0;

			for(uint j = 0, jcnt = available.length; j < jcnt; ++j) {
				auto@ av = available[j];
				if(av.request !is null) {
					ai.print("Error: Available is being used", av.obj);
					continue;
				}

				if(!req.spec.meets(av.resource, av.obj, req.obj))
					continue;
				if(!av.usable || av.obj is null || !av.obj.valid || av.obj.owner !is ai.empire)
					continue;
				//Check if a trade route exists between the two locations
				if(!canTradeBetween(av.obj, req.obj) && av.obj.region !is null && req.obj.region !is null) {
					auto@ territoryA = av.obj.region.getTerritory(ai.empire);
					auto@ territoryB = req.obj.region.getTerritory(ai.empire);
					if (territoryA !is territoryB) {
						if (log)
							ai.print("trade route requested between " + addrstr(territoryA) + " and " + addrstr(territoryB));
						events.notifyTradeRouteNeeded(this, TradeRouteNeededEventArgs(territoryA, territoryB));
					}
					continue;
				}
				if(av.localOnly && av.obj !is req.obj)
					continue;

				double weight = 1.0;
				if(req.obj is av.obj)
					weight = INFINITY;

				if(weight > sourceWeight) {
					sourceWeight = weight;
					@source = av;
				}
			}

			if(source !is null) {
				link(req, source);
				--i; --cnt;
			}
		}
	}

	void turn() {
	}

	bool get_hasOpenRequests() {
		for(uint i = 0, cnt = requested.length; i < cnt; ++i) {
			auto@ req = requested[i];
			if(req.isOpen)
				return true;
		}
		return false;
	}

	TradePath tradePather;
	int tradeDistance(Region& fromRegion, Region& toRegion) {
		@tradePather.forEmpire = ai.empire;
		tradePather.generate(getSystem(fromRegion), getSystem(toRegion), keepCache=true);
		if(!tradePather.valid)
			return -1;
		return tradePather.pathSize - 1;
	}

	bool canTradeBetween(Object& fromObj, Object& toObj) {
		Region@ fromRegion = fromObj.region;
		if(fromRegion is null)
			return false;
		Region@ toRegion = toObj.region;
		if(toRegion is null)
			return false;
		return canTradeBetween(fromRegion, toRegion);
	}

	bool canTradeBetween(Region& fromRegion, Region& toRegion) {
		if(fromRegion.sharesTerritory(ai.empire, toRegion))
			return true;
		int dist = tradeDistance(fromRegion, toRegion);
		if(dist < 0)
			return false;
		return true;
	}

	void link(ImportData@ req, ExportData@ source) {
		//Manage the data
		@source.request = req;
		@source.developUse = null;
		req.set(source);

		requested.remove(req);
		active.insertLast(req);

		req.beingMet = true;

		available.remove(source);
		used.insertLast(source);

		if(log)
			ai.print("link "+source.resource.name+" from "+source.obj.name+" to "+req.obj.name);

		//Perform the actual export
		if(source.obj !is req.obj)
			source.obj.exportResourceByID(source.resourceId, req.obj);
		else
			source.obj.exportResourceByID(source.resourceId, null);
	}

	ImportData@ requestResource(Object& toObject, ResourceSpec& spec, bool forLevel = false, bool activate = true, bool prioritize = false) {
		ImportData data;
		data.idleSince = gameTime;
		data.id = nextImportId++;
		@data.obj = toObject;
		@data.spec = spec;
		data.forLevel = forLevel;

		if(log)
			ai.print("requested resource: "+spec.dump(), toObject);

		if(activate) {
			if(prioritize)
				requested.insertAt(0, data);
			else
				requested.insertLast(data);
		}
		return data;
	}

	ExportData@ availableResource(Object& fromObject, const ResourceType& resource, int id) {
		ExportData data;
		data.id = nextExportId++;
		@data.obj = fromObject;
		@data.resource = resource;
		data.resourceId = id;

		if(log)
			ai.print("available resource: "+resource.name, fromObject);

		available.insertLast(data);
		return data;
	}

	void checkReplaceCurrent(ExportData@ res) {
		//If the planet that this resource is on is currently importing this same resource, switch it around
		if(res.request !is null)
			return;

		for(uint i = 0, cnt = used.length; i < cnt; ++i) {
			auto@ other = used[i];
			auto@ request = other.request;
			if(request is null)
				continue;
			if(request.obj !is res.obj)
				continue;

			if(request.spec.meets(res.resource, res.obj, res.obj)) {
				//Swap the import with using the local resource
				if(other.resource.exportable) {
					breakImport(other);
					link(request, res);
					return;
				}
			}
		}
	}

	array<Resource> checkResources;
	array<ExportData@>@ availableResource(Object& fromObject) {
		array<ExportData@> list;

		checkResources.syncFrom(fromObject.getNativeResources());

		uint nativeCount = checkResources.length;
		for(uint i = 0; i < nativeCount; ++i) {
			auto@ r = checkResources[i].type;
			if(r !is null)
				list.insertLast(availableResource(fromObject, r, checkResources[i].id));
		}

		return list;
	}

	ExportData@ findResource(Object@ obj, int resourceId) {
		for(uint i = 0, cnt = available.length; i < cnt; ++i) {
			if(available[i].obj is obj && available[i].resourceId == resourceId)
				return available[i];
		}
		for(uint i = 0, cnt = used.length; i < cnt; ++i) {
			if(used[i].obj is obj && used[i].resourceId == resourceId)
				return used[i];
		}
		return null;
	}

	ImportData@ findUnclaimed(ExportData@ forResource) {
		for(uint i = 0, cnt = requested.length; i < cnt; ++i) {
			auto@ req = requested[i];
			if(req.claimedFor)
				continue;
			if(req.beingMet)
				continue;

			if(!req.spec.meets(forResource.resource, forResource.obj, req.obj))
				continue;
			if(!canTradeBetween(req.obj, forResource.obj))
				continue;

			return req;
		}
		return null;
	}

	void breakImport(ImportData@ data) {
		if(data.fromObject !is null) {
			auto@ source = findResource(data.fromObject, data.resourceId);
			if(source !is null) {
				breakImport(source);
				return;
			}
		}

		@data.fromObject = null;
		data.resourceId = -1;
		data.beingMet = false;
		data.idleSince = gameTime;

		active.remove(data);
		requested.insertAt(0, data);
	}

	void breakImport(ExportData@ data) {
		if(data.request !is null) {
			if(data.request.obj !is data.obj)
				data.obj.exportResource(data.resourceId, null);

			data.request.beingMet = false;
			@data.request.fromObject = null;
			data.request.resourceId = -1;
			data.request.idleSince = gameTime;

			active.remove(data.request);
			requested.insertAt(0, data.request);

			@data.request = null;
		}

		used.remove(data);
		available.insertLast(data);
	}

	void cancelRequest(ImportData@ data) {
		if(data.beingMet) {
			breakImport(data);
			active.remove(data);
		}
		else {
			requested.remove(data);
		}
	}

	void removeResource(ExportData@ data) {
		if(data.request !is null) {
			breakImport(data);
			used.remove(data);
			@data.obj = null;
		}
		else {
			available.remove(data);
			@data.obj = null;
		}
	}

	ImportData@ claimImport(ImportData@ data) {
		data.beingMet = true;
		requested.remove(data);
		active.insertLast(data);
		return data;
	}

	void relinquishImport(ImportData@ data) {
		data.beingMet = false;
		active.remove(data);
		requested.insertLast(data);
	}

	void organizeImports(Object& obj, int targetLevel, ImportData@ before = null) {
		//Organize any imports for this object so it tries to get to a particular target level
		if(log)
			ai.print("Organizing imports for level", obj, targetLevel);

		//Get the requirement list
		const PlanetLevel@ lvl = getPlanetLevel(obj, targetLevel);
		if(lvl is null) {
			ai.print("Error: could not find planet level", obj, targetLevel);
			return; //Welp, can't do nothing here
		}

		//Collect all the requests this planet currently has outstanding
		array<ImportData@> activeRequests;
		for(uint i = 0, cnt = requested.length; i < cnt; ++i) {
			auto@ req = requested[i];
			if(req.obj !is obj)
				continue;
			if(!req.forLevel)
				continue;

			activeRequests.insertLast(req);
		}
		for(uint i = 0, cnt = active.length; i < cnt; ++i) {
			auto@ req = active[i];
			if(req.obj !is obj)
				continue;
			if(!req.forLevel)
				continue;

			activeRequests.insertLast(req);
		}

		//TODO: This needs to be able to deal with dummy resources

		//Match import requests with level requirements
		array<ResourceSpec@> addSpecs;
		const ResourceRequirements@ reqs = lvl.reqs;

		for(uint i = 0, cnt = reqs.reqs.length; i < cnt; ++i) {
			auto@ need = reqs.reqs[i];
			for(uint n = 0; n < need.amount; ++n)
				addSpecs.insertLast(implementSpec(need));
		}

		if(race !is null)
			race.levelRequirements(obj, targetLevel, addSpecs);

		for(uint i = 0, cnt = addSpecs.length; i < cnt; ++i) {
			auto@ spec = addSpecs[i];

			bool foundMatch = false;
			for(uint j = 0, jcnt = activeRequests.length; j < jcnt; ++j) {
				if(activeRequests[j].spec == spec) {
					foundMatch = true;
					activeRequests.removeAt(j);
					break;
				}
			}

			if(foundMatch) {
				addSpecs.removeAt(i);
				--i; --cnt;
			}
		}

		//Cancel any import requests that we don't need anymore
		for(uint i = 0, cnt = activeRequests.length; i < cnt; ++i)
			cancelRequest(activeRequests[i]);

		//Insert any imports above any imports of the planet we're exporting to
		int place = -1;
		if(before !is null) {
			for(uint i = 0, cnt = requested.length; i < cnt; ++i) {
				if(requested[i] is before) {
					place = int(i);
					break;
				}
			}
		}

		//Insert everything we need to add
		addSpecs.sortDesc();
		for(uint i = 0, cnt = addSpecs.length; i < cnt; ++i) {
			ImportData@ req = requestResource(obj, addSpecs[i], forLevel=true, activate=false);
			if(place == -1) {
				requested.insertLast(req);
			}
			else {
				requested.insertAt(place, req);
				place += 1;
			}
		}
	}

	void killImportsTo(Object& obj) {
		for(uint i = 0, cnt = requested.length; i < cnt; ++i) {
			if(requested[i].obj is obj) {
				cancelRequest(requested[i]);
				--i; --cnt;
			}
		}
		for(uint i = 0, cnt = active.length; i < cnt; ++i) {
			if(active[i].obj is obj) {
				cancelRequest(active[i]);
				--i; --cnt;
			}
		}
	}

	void killResourcesFrom(Object& obj) {
		for(uint i = 0, cnt = available.length; i < cnt; ++i) {
			if(available[i].obj is obj) {
				removeResource(available[i]);
				--i; --cnt;
			}
		}
		for(uint i = 0, cnt = used.length; i < cnt; ++i) {
			if(used[i].obj is obj) {
				removeResource(used[i]);
				--i; --cnt;
			}
		}
	}

	ImportData@ getImport(const string& fromName, uint index = 0) {
		for(uint i = 0, cnt = requested.length; i < cnt; ++i) {
			if(requested[i].obj.name.equals_nocase(fromName)) {
				if(index == 0)
					return requested[i];
				else
					index -= 1;
			}
		}
		for(uint i = 0, cnt = active.length; i < cnt; ++i) {
			if(active[i].obj.name.equals_nocase(fromName)) {
				if(index == 0)
					return active[i];
				else
					index -= 1;
			}
		}
		return null;
	}

	ExportData@ getExport(const string& fromName, uint index = 0) {
		for(uint i = 0, cnt = available.length; i < cnt; ++i) {
			if(available[i].obj.name.equals_nocase(fromName)) {
				if(index == 0)
					return available[i];
				else
					index -= 1;
			}
		}
		for(uint i = 0, cnt = used.length; i < cnt; ++i) {
			if(used[i].obj.name.equals_nocase(fromName)) {
				if(index == 0)
					return used[i];
				else
					index -= 1;
			}
		}
		return null;
	}

	void getImportsOf(array<ImportData@>& output, uint resType, Planet@ toPlanet = null) {
		for(uint i = 0, cnt = active.length; i < cnt; ++i) {
			auto@ req = active[i];
			if(req.spec.type != RST_Specific)
				continue;
			if(req.spec.resource.id != resType)
				continue;
			if(toPlanet !is null && req.obj !is toPlanet)
				continue;
			output.insertLast(req);
		}
		for(uint i = 0, cnt = requested.length; i < cnt; ++i) {
			auto@ req = requested[i];
			if(req.spec.type != RST_Specific)
				continue;
			if(req.spec.resource.id != resType)
				continue;
			if(toPlanet !is null && req.obj !is toPlanet)
				continue;
			output.insertLast(req);
		}
	}

	void dumpRequests(Object@ forObject = null) {
		for(uint i = 0, cnt = requested.length; i < cnt; ++i) {
			if(forObject !is null && requested[i].obj !is forObject)
				continue;
			print(requested[i].obj.name+" requests "+requested[i].spec.dump());
		}
		if(forObject !is null) {
			for(uint i = 0, cnt = used.length; i < cnt; ++i) {
				if(used[i].request is null || used[i].request.obj !is forObject)
					continue;
				print(used[i].request.obj.name+" is getting "+used[i].request.spec.dump()+" from "+used[i].obj.name);
			}
		}
	}
};

AIComponent@ createResources() {
	return Resources();
}

Added scripts/server/empire_ai/weasel/Scouting.as.






















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// Scouting
// --------
// Orders the construction of scouts, explores the galaxy with them and makes
// sure we have vision where we need vision, as well as scanning anomalies.
//

import empire_ai.weasel.WeaselAI;

import empire_ai.weasel.Fleets;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Designs;
import empire_ai.weasel.Construction;
import empire_ai.weasel.Movement;
import empire_ai.weasel.Creeping;

final class ScoutingMission : Mission {
	Region@ region;
	MoveOrder@ move;

	void save(Fleets& fleets, SaveFile& file) override {
		file << region;
		fleets.movement.saveMoveOrder(file, move);
	}

	void load(Fleets& fleets, SaveFile& file) override {
		file >> region;
		@move = fleets.movement.loadMoveOrder(file);
	}

	double getPerformWeight(AI& ai, FleetAI& fleet) {
		if(fleet.fleetClass != FC_Scout) {
			if(fleet.fleetClass == FC_Mothership)
				return 0.0;
			if(gameTime > ai.behavior.scoutAllTimer)
				return 0.0;
		}
		return 1.0 / region.position.distanceTo(fleet.obj.position);
	}

	void start(AI& ai, FleetAI& fleet) override {
		uint mprior = MP_Background;
		if(gameTime < 6.0 * 60.0)
			mprior = MP_Critical;
		else if(priority > MiP_Normal)
			mprior = MP_Normal;
		@move = cast<Movement>(ai.movement).move(fleet.obj, region, mprior);
	}

	void tick(AI& ai, FleetAI& fleet, double time) {
		if(move.failed)
			canceled = true;
		if(move.completed) {
			//We managed to scout this system
			if(fleet.obj.region !is region) {
				@move = cast<Movement>(ai.movement).move(fleet.obj, region.position + random3d(400.0));
				return;
			}
			completed = true;

			//Detect any anomalies and put them into the scanning queue
			//TODO: Detect newly created anomalies in systems we already have vision over?
			if(region.anomalyCount != 0) {
				auto@ list = region.getAnomalies();
				Object@ obj;
				while(receive(list, obj)) {
					Anomaly@ anom = cast<Anomaly>(obj);
					if(anom !is null)
						cast<Scouting>(ai.scouting).recordAnomaly(anom);
				}
			}
		}
	}
};

final class ScanningMission : Mission {
	Anomaly@ anomaly;
	MoveOrder@ move;

	void save(Fleets& fleets, SaveFile& file) override {
		file << anomaly;
		fleets.movement.saveMoveOrder(file, move);
	}

	void load(Fleets& fleets, SaveFile& file) override {
		file >> anomaly;
		@move = fleets.movement.loadMoveOrder(file);
	}

	double getPerformWeight(AI& ai, FleetAI& fleet) {
		if(fleet.fleetClass != FC_Scout) {
			if(gameTime > ai.behavior.scoutAllTimer)
				return 0.0;
		}
		return 1.0 / anomaly.position.distanceTo(fleet.obj.position);
	}

	void start(AI& ai, FleetAI& fleet) override {
		uint mprior = MP_Background;
		if(priority > MiP_Normal)
			mprior = MP_Normal;
		@move = cast<Movement>(ai.movement).move(fleet.obj, anomaly, mprior);
	}

	void tick(AI& ai, FleetAI& fleet, double time) {
		if(move !is null) {
			if(move.failed) {
				canceled = true;
				return;
			}
			if(move.completed)
				@move = null;
		}
		if(move is null) {
			if(anomaly is null || !anomaly.valid) {
				completed = true;
				return;
			}

			if(anomaly.getEmpireProgress(ai.empire) >= 1.f) {
				uint choose = 0;
				uint possibs = 0;
				uint optCnt = anomaly.getOptionCount();
				for(uint i = 0; i < optCnt; ++i) {
					if(anomaly.isOptionSafe[i]) {
						possibs += 1;
						if(randomd() < 1.0 / double(possibs))
							choose = i;
					}
				}

				if(!ai.behavior.forbidAnomalyChoice && possibs != 0) {
					anomaly.choose(ai.empire, choose);
				}
				else {
					completed = true;
				}
			}
			else {
				if(!fleet.obj.hasOrders)
					fleet.obj.addScanOrder(anomaly);
			}
		}
	}
};

class Scouting : AIComponent {
	Fleets@ fleets;
	Systems@ systems;
	Designs@ designs;
	Construction@ construction;
	Movement@ movement;
	Creeping@ creeping;

	DesignTarget@ scoutDesign;

	array<ScoutingMission@> queue;
	array<ScoutingMission@> active;

	array<Anomaly@> anomalies;
	array<ScanningMission@> scanQueue;
	array<ScanningMission@> scanActive;

	array<BuildFlagship@> constructing;

	int exploreHops = 0;
	bool buildScouts = true;

	void create() {
		@fleets = cast<Fleets>(ai.fleets);
		@systems = cast<Systems>(ai.systems);
		@designs = cast<Designs>(ai.designs);
		@construction = cast<Construction>(ai.construction);
		@movement = cast<Movement>(ai.movement);
		@creeping = cast<Creeping>(ai.creeping);
	}

	void save(SaveFile& file) {
		designs.saveDesign(file, scoutDesign);
		file << exploreHops;

		uint cnt = queue.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			fleets.saveMission(file, queue[i]);

		cnt = active.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			fleets.saveMission(file, active[i]);

		cnt = anomalies.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << anomalies[i];

		cnt = scanQueue.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			fleets.saveMission(file, scanQueue[i]);

		cnt = scanActive.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			fleets.saveMission(file, scanActive[i]);

		cnt = constructing.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			construction.saveConstruction(file, constructing[i]);
	}

	void load(SaveFile& file) {
		@scoutDesign = designs.loadDesign(file);
		file >> exploreHops;

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ miss = cast<ScoutingMission>(fleets.loadMission(file));
			if(miss !is null)
				queue.insertLast(miss);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ miss = cast<ScoutingMission>(fleets.loadMission(file));
			if(miss !is null)
				active.insertLast(miss);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			Anomaly@ anom;
			file >> anom;
			if(anom !is null)
				anomalies.insertLast(anom);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ miss = cast<ScanningMission>(fleets.loadMission(file));
			if(miss !is null)
				scanQueue.insertLast(miss);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ miss = cast<ScanningMission>(fleets.loadMission(file));
			if(miss !is null)
				scanActive.insertLast(miss);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ cons = cast<BuildFlagship>(construction.loadConstruction(file));
			if(cons !is null)
				constructing.insertLast(cons);
		}
	}

	void start() {
		@scoutDesign = DesignTarget(DP_Scout, 16);
		scoutDesign.targetMaintenance = 40;
		designs.design(scoutDesign);
	}

	bool isScouting(Region@ region) {
		for(uint i = 0, cnt = queue.length; i < cnt; ++i) {
			if(queue[i].region is region)
				return true;
		}
		for(uint i = 0, cnt = active.length; i < cnt; ++i) {
			if(active[i].region is region)
				return true;
		}
		return false;
	}

	ScoutingMission@ scout(Region@ region, uint priority = MiP_High) {
		for(uint i = 0, cnt = queue.length; i < cnt; ++i) {
			if(queue[i].region is region)
				return queue[i];
		}

		for(uint i = 0, cnt = active.length; i < cnt; ++i) {
			if(active[i].region is region)
				return active[i];
		}

		if(log)
			ai.print("Queue scouting mission", region);

		ScoutingMission mission;
		@mission.region = region;
		mission.priority = priority;
		fleets.register(mission);

		queue.insertLast(mission);
		return mission;
	}

	void recordAnomaly(Anomaly@ anom) {
		for(uint i = 0, cnt = scanActive.length; i < cnt; ++i) {
			if(scanActive[i].anomaly is anom)
				return;
		}

		if(anomalies.find(anom) == -1)
			anomalies.insertLast(anom);
	}

	ScanningMission@ scan(Anomaly& anomaly, uint priority = MiP_Normal) {
		for(uint i = 0, cnt = scanActive.length; i < cnt; ++i) {
			if(scanActive[i].anomaly is anomaly)
				return scanActive[i];
		}

		if(log)
			ai.print("Queue scanning mission on "+anomaly.name, anomaly.region);

		ScanningMission mission;
		@mission.anomaly = anomaly;
		mission.priority = priority;
		fleets.register(mission);
		anomalies.remove(anomaly);

		scanQueue.insertLast(mission);
		return mission;
	}

	void focusTick(double time) {
		//Remove completed scouting missions
		for(uint i = 0, cnt = active.length; i < cnt; ++i) {
			if(active[i].completed || active[i].canceled) {
				active.removeAt(i);
				--i; --cnt;
			}
		}

		//Remove completed scanning missions
		for(uint i = 0, cnt = scanActive.length; i < cnt; ++i) {
			if(scanActive[i].completed || scanActive[i].canceled) {
				scanActive.removeAt(i);
				--i; --cnt;
			}
		}

		if(ai.behavior.forbidScouting) return;

		//Make sure we have enough scouts active and scouting
		if(fleets.count(FC_Scout) + constructing.length < ai.behavior.scoutsActive && buildScouts)
			constructing.insertLast(construction.buildFlagship(scoutDesign));
		for(uint i = 0, cnt = constructing.length; i < cnt; ++i) {
			if(constructing[i].completed && constructing[i].completedAt + 30.0 < gameTime) {
				constructing.removeAt(i);
				--i; --cnt;
			}
		}

		//See if we can fill the scouting queue with something nice
		uint scoutClass = FC_Scout;
		if(gameTime < ai.behavior.scoutAllTimer)
			scoutClass = FC_ALL;
		bool haveIdle = fleets.haveIdle(scoutClass);

		//See if we should queue up a new anomaly scan
		if(scanQueue.length == 0 && anomalies.length != 0 && scanActive.length < ai.behavior.maxScanningMissions && haveIdle && (!ai.behavior.prioritizeScoutOverScan || active.length > 0)) {
			Anomaly@ best;
			double bestDist = INFINITY;
			for(uint i = 0, cnt = anomalies.length; i < cnt; ++i) {
				auto@ anom = anomalies[i];
				if(anom is null || !anom.valid) {
					anomalies.removeAt(i);
					--i; --cnt;
					continue;
				}
				if(creeping.isQuarantined(anom.region))
					continue;

				double d = fleets.closestIdleTo(scoutClass, anom.position);
				if(d < bestDist) {
					@best = anom;
					bestDist = d;
				}
			}

			if(best !is null)
				scan(best);
		}

		//Scan anomalies in our scan queue
		if(scanQueue.length != 0) {
			auto@ mission = scanQueue[0];
			if(mission.anomaly is null || !mission.anomaly.valid) {
				scanQueue.removeAt(0);
			}
			else {
				auto@ flAI = fleets.performMission(mission);
				if(flAI !is null) {
					if(log)
						ai.print("Perform scanning mission with "+flAI.obj.name, mission.anomaly.region);

					scanQueue.remove(mission);
					scanActive.insertLast(mission);
				}
			}
		}

		//TODO: In large maps we should probably devote scouts to scouting enemies even before the map is fully explored
		if(queue.length == 0 && active.length < ai.behavior.maxScoutingMissions && haveIdle) {
			//Explore systems from the inside out
			if(exploreHops != -1) {
				double bestDist = INFINITY;
				bool remainingHops = false;
				bool emptyHops = true;
				Region@ best;

				for(uint i = 0, cnt = systems.all.length; i < cnt; ++i) {
					auto@ sys = systems.all[i];

					if(sys.hopDistance == exploreHops)
						emptyHops = false;

					if(sys.explored || isScouting(sys.obj))
						continue;

					if(sys.hopDistance == exploreHops)
						remainingHops = true;

					double d = fleets.closestIdleTo(scoutClass, sys.obj.position);
					if(sys.hopDistance != exploreHops)
						d *= pow(ai.behavior.exploreBorderWeight, double(sys.hopDistance - exploreHops));

					if(d < bestDist) {
						bestDist = d;
						@best = sys.obj;
					}
				}

				if(best !is null)
					scout(best, priority=MiP_Normal);

				if(emptyHops)
					exploreHops = -1;
				else if(!remainingHops)
					exploreHops += 1;
			}
			else {
				//Gain vision over systems we haven't recently seen
				Region@ best;
				double bestWeight = 0;
				double curTime = gameTime;

				for(uint i = 0, cnt = systems.all.length; i < cnt; ++i) {
					auto@ sys = systems.all[i];
					if(sys.visible || sys.visibleNow(ai))
						continue;
					if(isScouting(sys.obj))
						continue;

					double timer = curTime - sys.lastVisible;
					if(timer < ai.behavior.minScoutingInterval)
						continue;

					double w = 1.0;
					w *= timer / ai.behavior.minScoutingInterval;
					w /= fleets.closestIdleTo(scoutClass, sys.obj.position);

					if(!sys.explored)
						w *= 10.0;
					if(sys.seenPresent & ~ai.visionMask != 0)
						w *= 2.0;
					if(sys.seenPresent & ai.enemyMask != 0) {
						if(sys.hopDistance < 2)
							w *= 4.0;
						w *= 4.0;
					}

					if(w > bestWeight) {
						bestWeight = w;
						@best = sys.obj;
					}
				}

				if(best !is null)
					scout(best, priority=MiP_Normal);
			}
		}

		//Try to find a scout to perform our top scouting mission from the queue
		if(queue.length != 0) {
			auto@ mission = queue[0];
			auto@ flAI = fleets.performMission(mission);
			if(flAI !is null) {
				if(log)
					ai.print("Perform scouting mission with "+flAI.obj.name, mission.region);

				active.insertLast(mission);
				queue.removeAt(0);
			}
		}
	}
};

AIComponent@ createScouting() {
	return Scouting();
}

Added scripts/server/empire_ai/weasel/Systems.as.




























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;

import empire_ai.weasel.Events;

import empire_ai.weasel.searches;

import ai.events;

import systems;
import system_pathing;

final class SystemAI {
	const SystemDesc@ desc;
	Region@ obj;

	double prevTick = 0.0;

	array<Planet@> planets;
	array<Pickup@> pickups;
	array<Object@> pickupProtectors;
	array<Artifact@> artifacts;
	array<Asteroid@> asteroids;

	bool explored = false;
	bool owned = false;
	bool visible = false;

	int hopDistance = 0;
	bool visited = false;

	bool border = false;
	bool bordersEmpires = false;
	bool outsideBorder = false;

	double lastVisible = 0;
	uint seenPresent = 0;

	double focusDuration = 0;

	double enemyStrength = 0;
	double lastStrengthCheck = 0;

	double nextDetailed = 0;

	SystemAI() {
	}

	SystemAI(const SystemDesc@ sys) {
		@desc = sys;
		@obj = desc.object;
	}

	void save(SaveFile& file) {
		file << obj;
		file << prevTick;

		uint cnt = planets.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << planets[i];

		cnt = pickups.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << pickups[i];

		cnt = pickupProtectors.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << pickupProtectors[i];

		cnt = artifacts.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << artifacts[i];

		cnt = asteroids.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << asteroids[i];

		file << explored;
		file << owned;
		file << visible;
		file << hopDistance;
		file << border;
		file << bordersEmpires;
		file << outsideBorder;
		file << lastVisible;
		file << seenPresent;
		file << enemyStrength;
		file << lastStrengthCheck;
	}

	void load(SaveFile& file) {
		file >> obj;
		file >> prevTick;

		uint cnt = 0;
		file >> cnt;
		planets.length = cnt;
		for(uint i = 0; i < cnt; ++i)
			file >> planets[i];

		file >> cnt;
		pickups.length = cnt;
		for(uint i = 0; i < cnt; ++i)
			file >> pickups[i];

		file >> cnt;
		pickupProtectors.length = cnt;
		for(uint i = 0; i < cnt; ++i)
			file >> pickupProtectors[i];

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			Artifact@ artif;
			file >> artif;
			if(artif !is null)
				artifacts.insertLast(artif);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			Asteroid@ roid;
			file >> roid;
			if(roid !is null)
				asteroids.insertLast(roid);
		}

		file >> explored;
		file >> owned;
		file >> visible;
		file >> hopDistance;
		file >> border;
		file >> bordersEmpires;
		file >> outsideBorder;
		file >> lastVisible;
		file >> seenPresent;
		file >> enemyStrength;
		file >> lastStrengthCheck;
	}

	bool visibleNow(AI& ai) {
		return obj.VisionMask & ai.visionMask != 0;
	}

	void strengthCheck(AI& ai, double minInterval = 30.0) {
		if(lastStrengthCheck + minInterval > gameTime)
			return;
		if(!visible && lastVisible < gameTime - 30.0)
			return;
		lastStrengthCheck = gameTime;
		enemyStrength = getTotalFleetStrength(obj, ai.enemyMask);
	}

	void tick(AI& ai, Systems& systems, double time) {
		//Check if we should be visible
		bool shouldVisible = obj.VisionMask & ai.visionMask != 0;
		if(visible != shouldVisible) {
			if(visible)
				lastVisible = gameTime;
			visible = shouldVisible;
		}

		//Check if we should be owned
		bool shouldOwned = obj.TradeMask & ai.mask != 0;
		if(owned != shouldOwned) {
			if(shouldOwned) {
				systems.owned.insertLast(this);
				systems.hopsChanged = true;
				hopDistance = 0;
				systems.events.notifyOwnedSystemAdded(this, EventArgs());
			}
			else {
				hopDistance = 1;
				systems.owned.remove(this);
				systems.hopsChanged = true;
				systems.events.notifyOwnedSystemRemoved(this, EventArgs());
			}
			owned = shouldOwned;
		}

		//Check if we should be border
		bool shouldBorder = false;
		bordersEmpires = false;
		if(owned) {
			for(uint i = 0, cnt = desc.adjacent.length; i < cnt; ++i) {
				auto@ other = systems.getAI(desc.adjacent[i]);
				if(other !is null && !other.owned) {
					if(other.seenPresent & ~ai.teamMask != 0)
						bordersEmpires = true;
					shouldBorder = true;
					break;
				}
			}
			for(uint i = 0, cnt = desc.wormholes.length; i < cnt; ++i) {
				auto@ other = systems.getAI(desc.wormholes[i]);
				if(other !is null && !other.owned) {
					if(other.seenPresent & ~ai.teamMask != 0)
						bordersEmpires = true;
					shouldBorder = true;
					break;
				}
			}
		}

		if(border != shouldBorder) {
			if(shouldBorder) {
				systems.border.insertLast(this);
				systems.events.notifyBorderSystemAdded(this, EventArgs());
			}
			else {
				systems.border.remove(this);
				systems.events.notifyBorderSystemRemoved(this, EventArgs());
			}
			border = shouldBorder;
		}

		//Check if we should be outsideBorder
		bool shouldOutsideBorder = !owned && hopDistance == 1;
		if(outsideBorder != shouldOutsideBorder) {
			if(shouldOutsideBorder) {
				systems.outsideBorder.insertLast(this);
				systems.events.notifyOutsideBorderSystemAdded(this, EventArgs());
			}
			else {
				systems.outsideBorder.remove(this);
				systems.events.notifyOutsideBorderSystemRemoved(this, EventArgs());
			}
			outsideBorder = shouldOutsideBorder;
		}

		//Check if we've been explored
		if(visible && !explored) {
			//Find all remnants in this system
			auto@ objs = findType(obj, null, OT_Pickup);
			for(uint i = 0, cnt = objs.length; i < cnt; ++i) {
				Pickup@ p = cast<Pickup>(objs[i]);
				if(p !is null) {
					pickups.insertLast(p);
					pickupProtectors.insertLast(p.getProtector());
				}
			}

			explored = true;
		}

		//Deal with recording new data on this system
		if(explored) {
			uint plCnt = obj.planetCount;
			if(plCnt != planets.length) {
				auto@ objs = findType(obj, null, OT_Planet);
				planets.length = 0;
				planets.reserve(objs.length);
				for(uint i = 0, cnt = objs.length; i < cnt; ++i) {
					Planet@ pl = cast<Planet>(objs[i]);
					if(pl !is null)
						planets.insertLast(pl);
				}
			}
		}

		if(visible) {
			seenPresent = obj.PlanetsMask;

			uint astrCount = obj.asteroidCount;
			if(astrCount != asteroids.length) {
				auto@ objs = findType(obj, null, OT_Asteroid);
				asteroids.length = 0;
				asteroids.reserve(objs.length);
				for(uint i = 0, cnt = objs.length; i < cnt; ++i) {
					Asteroid@ a = cast<Asteroid>(objs[i]);
					if(a !is null)
						asteroids.insertLast(a);
				}
			}

			for(uint i = 0, cnt = pickups.length; i < cnt; ++i) {
				if(!pickups[i].valid) {
					pickups.removeAt(i);
					pickupProtectors.removeAt(i);
					break;
				}
			}

			if(nextDetailed < gameTime) {
				nextDetailed = gameTime + randomd(40.0, 100.0);

				auto@ objs = findType(obj, null, OT_Artifact);
				artifacts.length = 0;
				artifacts.reserve(objs.length);
				for(uint i = 0, cnt = objs.length; i < cnt; ++i) {
					Artifact@ a = cast<Artifact>(objs[i]);
					if(a !is null)
						artifacts.insertLast(a);
				}
			}
		}
	}
};

class Systems : AIComponent {
	Events@ events;
	
	//All owned systems
	array<SystemAI@> owned;

	//All owned systems that are considered our empire's border
	array<SystemAI@> border;

	//All systems just outside our border
	array<SystemAI@> outsideBorder;

	//All systems
	array<SystemAI@> all;

	//Systems that need to be processed soon
	array<SystemAI@> bumped;
	array<SystemAI@> focused;

	uint sysIdx = 0;
	bool hopsChanged = false;
	
	void create() {
		@events = cast<Events>(ai.events);
	}

	void save(SaveFile& file) {
		uint cnt = all.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			all[i].save(file);

		cnt = owned.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			saveAI(file, owned[i]);

		cnt = border.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			saveAI(file, border[i]);

		cnt = outsideBorder.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			saveAI(file, outsideBorder[i]);
	}

	void load(SaveFile& file) {
		uint cnt = 0;
		file >> cnt;
		all.length = max(all.length, cnt);
		for(uint i = 0; i < cnt; ++i) {
			if(all[i] is null)
				@all[i] = SystemAI();
			all[i].load(file);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = loadAI(file);
			if(data !is null)
				owned.insertLast(data);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = loadAI(file);
			if(data !is null)
				border.insertLast(data);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ data = loadAI(file);
			if(data !is null)
				outsideBorder.insertLast(data);
		}
	}

	void loadFinalize(AI& ai) override {
		for(uint i = 0, cnt = all.length; i < cnt; ++i) {
			auto@ sys = getSystem(i);
			@all[i].desc = sys;
			@all[i].obj = sys.object;
		}
	}

	void saveAI(SaveFile& file, SystemAI@ ai) {
		Region@ reg;
		if(ai !is null)
			@reg = ai.obj;
		file << reg;
	}

	SystemAI@ loadAI(SaveFile& file) {
		Region@ reg;
		file >> reg;

		if(reg is null)
			return null;

		uint id = reg.SystemId;
		if(id >= all.length) {
			all.length = id+1;
			@all[id] = SystemAI();
			@all[id].obj = reg;
		}

		return all[id];
	}

	void focusTick(double time) {
		if(all.length != systemCount) {
			uint prevCount = all.length;
			all.length = systemCount;
			for(uint i = prevCount, cnt = all.length; i < cnt; ++i)
				@all[i] = SystemAI(getSystem(i));
		}
		if(hopsChanged)
			calculateHops();
	}

	void tick(double time) override {
		double curTime = gameTime;

		if(all.length != 0) {
			uint tcount = max(ceil(time / 0.2), double(all.length)/20.0);
			for(uint n = 0; n < tcount; ++n) {
				sysIdx = (sysIdx+1) % all.length;

				auto@ sys = all[sysIdx];
				sys.tick(ai, this, curTime - sys.prevTick);
				sys.prevTick = curTime;
			}
		}

		for(uint i = 0, cnt = bumped.length; i < cnt; ++i) {
			auto@ sys = bumped[i];
			double tickTime = curTime - sys.prevTick;
			if(tickTime != 0) {
				sys.tick(ai, this, tickTime);
				sys.prevTick = curTime;
			}
		}
		bumped.length = 0;

		for(uint i = 0, cnt = focused.length; i < cnt; ++i) {
			auto@ sys = focused[i];
			sys.focusDuration -= time;

			double tickTime = curTime - sys.prevTick;
			if(tickTime != 0) {
				sys.tick(ai, this, tickTime);
				sys.prevTick = curTime;
			}

			if(sys.focusDuration <= 0) {
				focused.removeAt(i);
				--i; --cnt;
			}
		}
	}

	void calculateHops() {
		if(!hopsChanged)
			return;
		hopsChanged = false;
		priority_queue q;
		for(uint i = 0, cnt = all.length; i < cnt; ++i) {
			auto@ sys = all[i];
			sys.visited = false;
			if(sys.owned) {
				sys.hopDistance = 0;
				q.push(int(i), 0);
			}
			else
				sys.hopDistance = INT_MAX;
		}

		while(!q.empty()) {
			uint index = uint(q.top());
			q.pop();

			auto@ sys = all[index];
			if(sys.visited)
				continue;

			int dist = sys.hopDistance;
			sys.visited = true;

			for(uint i = 0, cnt = sys.desc.adjacent.length; i < cnt; ++i) {
				uint otherInd = sys.desc.adjacent[i];
				if(otherInd < all.length) {
					auto@ other = all[otherInd];
					if(other.hopDistance > dist+1) {
						other.hopDistance = dist+1;
						q.push(otherInd, -other.hopDistance);
					}
				}
			}
			for(uint i = 0, cnt = sys.desc.wormholes.length; i < cnt; ++i) {
				uint otherInd = sys.desc.wormholes[i];
				if(otherInd < all.length) {
					auto@ other = all[otherInd];
					if(other.hopDistance > dist+1) {
						other.hopDistance = dist+1;
						q.push(otherInd, -other.hopDistance);
					}
				}
			}
		}
	}

	void focus(Region@ reg, double duration = 30.0) {
		bool found = false;
		for(uint i = 0, cnt = focused.length; i < cnt; ++i) {
			if(focused[i].obj is reg) {
				focused[i].focusDuration = max(focused[i].focusDuration, duration);
				found = true;
				break;
			}
		}

		if(!found) {
			auto@ sys = getAI(reg);
			if(sys !is null) {
				sys.focusDuration = duration;
				focused.insertLast(sys);
			}
		}
	}

	void bump(Region@ sys) {
		if(sys !is null)
			bump(getAI(sys));
	}

	void bump(SystemAI@ sys) {
		if(sys !is null)
			bumped.insertLast(sys);
	}

	SystemAI@ getAI(uint idx) {
		if(idx < all.length)
			return all[idx];
		return null;
	}

	SystemAI@ getAI(Region@ region) {
		if(region is null)
			return null;
		uint idx = region.SystemId;
		if(idx < all.length)
			return all[idx];
		return null;
	}

	SystemPath pather;
	int hopDistance(Region& fromRegion, Region& toRegion){
		pather.generate(getSystem(fromRegion), getSystem(toRegion), keepCache=true);
		if(!pather.valid)
			return INT_MAX;
		return pather.pathSize - 1;
	}

	TradePath tradePather;
	int tradeDistance(Region& fromRegion, Region& toRegion) {
		@tradePather.forEmpire = ai.empire;
		tradePather.generate(getSystem(fromRegion), getSystem(toRegion), keepCache=true);
		if(!tradePather.valid)
			return -1;
		return tradePather.pathSize - 1;
	}

	bool canTrade(Region& fromRegion, Region& toRegion) {
		if(fromRegion.sharesTerritory(ai.empire, toRegion))
			return true;
		int dist = tradeDistance(fromRegion, toRegion);
		if(dist < 0)
			return false;
		return true;
	}

	SystemAI@ getAI(const string& name) {
		for(uint i = 0, cnt = all.length; i < cnt; ++i) {
			if(all[i].obj.name.equals_nocase(name))
				return all[i];
		}
		return null;
	}

	uint index(const string& name) {
		for(uint i = 0, cnt = all.length; i < cnt; ++i) {
			if(all[i].obj.name.equals_nocase(name))
				return i;
		}
		return uint(-1);
	}
};

AIComponent@ createSystems() {
	return Systems();
}

Added scripts/server/empire_ai/weasel/War.as.


















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
// War
// ---
// Attacks and defends from enemy attacks during situations of war.
//

import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.Intelligence;
import empire_ai.weasel.Relations;
import empire_ai.weasel.Fleets;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Movement;
import empire_ai.weasel.Scouting;
import empire_ai.weasel.Military;
import empire_ai.weasel.searches;

import regions.regions;

class BattleMission : Mission {
	Region@ battleIn;
	FleetAI@ fleet;
	MoveOrder@ move;
	Object@ defending;
	Planet@ capturing;
	Empire@ captureFrom;
	bool arrived = false;

	void save(Fleets& fleets, SaveFile& file) override {
		file << battleIn;
		fleets.saveAI(file, fleet);
		fleets.movement.saveMoveOrder(file, move);
		file << defending;
		file << capturing;
		file << captureFrom;
		file << arrived;
	}

	void load(Fleets& fleets, SaveFile& file) override {
		file >> battleIn;
		@fleet = fleets.loadAI(file);
		@move = fleets.movement.loadMoveOrder(file);
		file >> defending;
		file >> capturing;
		file >> captureFrom;
		file >> arrived;
	}
};

double captureSupply(Empire& emp, Object& check) {
	double loy = check.getLoyaltyFacing(emp);
	double cost = config::SIEGE_LOYALTY_SUPPLY_COST * loy;
	cost *= emp.CaptureSupplyFactor;
	cost *= check.owner.CaptureSupplyDifficulty;
	return cost;
}

class Battle {
	SystemAI@ system;
	Region@ staging;
	array<BattleMission@> fleets;
	uint curPriority = MiP_Critical;
	bool isAttack = false;

	double enemyStrength;
	double ourStrength;
	double lastCombat = 0;
	double bestCapturePct;
	double lastHadFleets = 0;
	bool inCombat = false;
	bool isUnderSiege = false;

	Planet@ defendPlanet;
	Object@ eliminate;

	Battle() {
		lastHadFleets = gameTime;
		lastCombat = gameTime;
	}

	void save(War& war, SaveFile& file) {
		war.systems.saveAI(file, system);

		uint cnt = fleets.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			war.fleets.saveMission(file, fleets[i]);

		file << curPriority;
		file << isAttack;
		file << lastCombat;
		file << inCombat;
		file << defendPlanet;
		file << eliminate;
		file << isUnderSiege;
		file << bestCapturePct;
	}

	void load(War& war, SaveFile& file) {
		@system = war.systems.loadAI(file);

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ miss = cast<BattleMission>(war.fleets.loadMission(file));
			if(miss !is null)
				fleets.insertLast(miss);
		}

		file >> curPriority;
		file >> isAttack;
		file >> lastCombat;
		file >> inCombat;
		file >> defendPlanet;
		file >> eliminate;
		file >> isUnderSiege;
		file >> bestCapturePct;
	}

	BattleMission@ join(AI& ai, War& war, FleetAI@ flAI) {
		BattleMission mission;
		@mission.fleet = flAI;
		@mission.battleIn = system.obj;
		mission.priority = curPriority;

		Object@ moveTo = system.obj;
		if(defendPlanet !is null)
			@moveTo = defendPlanet;
		else if(eliminate !is null && eliminate.isShip)
			@moveTo = eliminate;
		@mission.move = war.movement.move(flAI.obj, moveTo, MP_Critical, spread=true, nearOnly=true);

		//Station this fleet nearby after the battle is over
		if(staging !is null)
			war.military.stationFleet(flAI, staging);

		if(war.log)
			ai.print("Assign to battle at "+system.obj.name
					+" for strength "+standardize(ourStrength * 0.001, true)
					+ " vs their "+standardize(enemyStrength * 0.001, true), flAI.obj);

		fleets.insertLast(mission);
		war.fleets.performMission(flAI, mission);
		return mission;
	}

	bool stayingHere(Object@ other) {
		if(other is null || !other.hasMover)
			return true;
		if(!inRegion(system.obj, other.position))
			return false;
		double acc = other.maxAcceleration;
		if(acc <= 0.0001)
			return true;
		vec3d compDest = other.computedDestination;
		if(inRegion(system.obj, compDest))
			return true;
		if(inRegion(system.obj, other.position + other.velocity * 10.0))
			return true;
		return false;
	}

	bool tick(AI& ai, War& war, double time) {
		//Compute strength values
		enemyStrength = getTotalFleetStrength(system.obj, ai.enemyMask, planets=true);
		ourStrength = 0;
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i)
			ourStrength += sqrt(fleets[i].fleet.strength);
		ourStrength *= ourStrength;
		inCombat = false;
		bool ourPlanetsPresent = system.obj.PlanetsMask & (ai.allyMask | ai.mask) != 0;

		if((enemyStrength < 0.01 || !ourPlanetsPresent) && defendPlanet is null)
			isUnderSiege = false;

		//Remove lost fleets
		bool anyArrived = false;
		bestCapturePct = 0.0;
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			auto@ miss = fleets[i];
			miss.priority = curPriority;
			if(!miss.fleet.obj.valid || miss.canceled) {
				if(!miss.fleet.obj.valid) {
					for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
						Empire@ other = getEmpire(i);
						if(!other.major || !ai.empire.isHostile(other))
							continue;
						if(system.obj.ContestedMask & other.mask != 0)
							war.relations.recordLostTo(other, miss.fleet.obj);
					}
				}
				miss.canceled = true;
				if(war.log)
					ai.print("BATTLE: lost fleet "+miss.fleet.obj.name, system.obj);
				fleets.removeAt(i);
				--i; --cnt;
				if(fleets.length == 0)
					lastHadFleets = gameTime;
				continue;
			}
			if(miss.move !is null) {
				if(miss.move.failed) {
					miss.canceled = true;
					if(war.log)
						ai.print("BATTLE: move failed on lost fleet "+miss.fleet.obj.name, system.obj);
					fleets.removeAt(i);
					--i; --cnt;
					if(fleets.length == 0)
						lastHadFleets = gameTime;
					continue;
				}
				if(miss.move.completed) {
					miss.arrived = true;
					@miss.move = null;
				}
			}
			if(miss.arrived) {
				anyArrived = true;

				bool shouldRetreat = false;
				if(miss.fleet.supplies < 0.05) {
					if(isCapturingAny && eliminate is null)
						shouldRetreat = true;
					else if(ourStrength < enemyStrength * 0.75)
						shouldRetreat = true;
				}
				if(miss.fleet.fleetHealth < 0.25) {
					if(ourStrength < enemyStrength * 0.5)
						shouldRetreat = true;
				}
				//DOF - Adding some leader based checks
				if(miss.fleet.flagshipHealth < 0.5)  {
					if(ourStrength < enemyStrength * 0.75)
						shouldRetreat = true;
				}
				if(shouldRetreat) {
					war.fleets.returnToBase(miss.fleet);
					fleets.removeAt(i);
					miss.canceled = true;
					--i; --cnt;
					if(fleets.length == 0)
						lastHadFleets = gameTime;
					continue;
				}
			}
			if(miss.capturing !is null)
				bestCapturePct = max(bestCapturePct, miss.capturing.capturePct);
		}

		//Defend our planets
		if(defendPlanet is null) {
			Planet@ defPl;
			double bestWeight = 0.0;
			for(uint i = 0, cnt = system.planets.length; i < cnt; ++i) {
				Planet@ pl = system.planets[i];
				double w = 1.0;
				if(pl.owner is ai.empire)
					w *= 2.0;
				else if(pl.owner.mask & ai.allyMask != 0)
					w *= 0.5;
				else
					continue;
				double capt = pl.capturePct;
				if(capt <= 0.01)
					continue;
				w *= capt;

				if(!pl.enemiesInOrbit)
					continue;
				if(w > bestWeight) {
					bestWeight = w;
					@defPl = pl;
				}
			}

			if(defPl !is null) {
				if(war.log)
					ai.print("BATTLE: protect planet "+defPl.name, system.obj);

				@defendPlanet = defPl;
				for(uint i = 0, cnt = fleets.length; i < cnt; ++i)
					moveTo(fleets[i], defendPlanet, force=true);
			}
		}
		else {
			//Check if there are still enemies in orbit
			if(!defendPlanet.enemiesInOrbit || !defendPlanet.valid || defendPlanet.owner.isHostile(ai.empire))
				@defendPlanet = null;
		}
		if(defendPlanet !is null) {
			//Make sure one fleet is in orbit
			inCombat = true;
			isUnderSiege = true;
			for(uint i = 0, cnt = fleets.length; i < cnt; ++i)
				moveTo(fleets[i], defendPlanet);
		}

		//Eliminate any remaining threats
		if(!inCombat) {
			//Eliminate any hostile targets in the system
			if(eliminate !is null) {
				//Make sure this is still a valid target to eliminate
				bool valid = true;
				if(!eliminate.valid) {
					valid = false;
					war.relations.recordTakenFrom(eliminate.owner, eliminate);
				}
				else if(!stayingHere(eliminate))
					valid = false;
				else if(!eliminate.isVisibleTo(ai.empire))
					valid = false;
				else if(!ai.empire.isHostile(eliminate.owner))
					valid = false;

				if(!valid) {
					@eliminate = null;
					clearOrders();
				}
				else {
					for(uint i = 0, cnt = fleets.length; i < cnt; ++i)
						attack(fleets[i], eliminate);
					inCombat = true;
				}
			}
			else {
				//Find a new target to eliminate
				Object@ check = findEnemy(system.obj, ai.empire, ai.enemyMask);
				if(check !is null) {
					if(stayingHere(check)) {
						@eliminate = check;
						for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
							auto@ obj = fleets[i].fleet.obj;
							if(!fleets[i].arrived)
								continue;
							obj.addAttackOrder(eliminate);
						}

						if(war.log)
							ai.print("BATTLE: Eliminate "+eliminate.name, system.obj);

						inCombat = true;
						for(uint i = 0, cnt = fleets.length; i < cnt; ++i)
							attack(fleets[i], eliminate, force=true);
					}
				}
			}
		}
		else {
			@eliminate = null;
		}

		//Capture enemy planets if possible
		//TODO: Respond to defense by abandoning all but 1 capture and swarming around the best one
		if(!inCombat) {
			for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
				auto@ miss = fleets[i];
				if(miss.capturing !is null) {
					if(canCapture(ai, miss, miss.capturing) && miss.fleet.remainingSupplies > captureSupply(ai.empire, miss.capturing) && miss.fleet.obj.hasOrders) {
						inCombat = true;
						continue;
					}
					else {
						if(miss.capturing.owner is ai.empire && miss.captureFrom !is null)
							war.relations.recordTakenFrom(miss.captureFrom, miss.capturing);
						@miss.capturing = null;
						@miss.captureFrom = null;
					}
				}
				if(!miss.arrived)
					continue;

				Planet@ bestCapture;
				double totalWeight = 0;

				for(uint i = 0, cnt = system.planets.length; i < cnt; ++i) {
					Planet@ check = system.planets[i];
					if(!canCapture(ai, miss, check))
						continue;

					//Don't send two fleets to the same thing
					if(isCapturing(check))
						continue;

					//Make sure we have the supplies remaining to capture
					if(miss.fleet.remainingSupplies < captureSupply(ai.empire, check) * ai.behavior.captureSupplyEstimate)
						continue;

					double str = check.getFleetStrength();
					double w = 1.0;
					w *= check.getLoyaltyFacing(ai.empire);
					if(str != 0)
						w /= str;

					totalWeight += w;
					if(randomd() < w / totalWeight)
						@bestCapture = check;
				}

				if(bestCapture !is null) {
					if(war.log)
						ai.print("BATTLE: Capture "+bestCapture.name+" with "+miss.fleet.obj.name, system.obj);

					@miss.capturing = bestCapture;
					@miss.captureFrom = bestCapture.owner;
					miss.fleet.obj.addCaptureOrder(bestCapture);
					inCombat = true;
				}
			}
		}
		else {
			for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
				auto@ miss = fleets[i];
				@miss.capturing = null;
				@miss.captureFrom = null;
			}
		}

		//Keep fleets here in non-critical mode for a few minutes
		if(!inCombat && (anyArrived || !isAttack)) {
			//TODO: Don't start this countdown until we've actually arrived
			if(gameTime > lastCombat + 90.0) {
				for(uint i = 0, cnt = fleets.length; i < cnt; ++i)
					fleets[i].completed = true;
				if(war.log)
					ai.print("BATTLE: ended", system.obj);
				return false;
			}
			else if(gameTime > lastCombat + 30.0) {
				curPriority = MiP_Normal;
			}
			else {
				curPriority = MiP_High;
			}
		}
		else {
			if(ourPlanetsPresent && isUnderSiege) {
				curPriority = MiP_Critical;
				if(war.winnability(this) < 0.5)
					curPriority = MiP_High;
			}
			else if(bestCapturePct > 0.75)
				curPriority = MiP_Critical;
			else
				curPriority = MiP_High;
			lastCombat = gameTime;
		}

		//If needed, claim fleets
		if(ourStrength < enemyStrength * ai.behavior.battleStrengthOverkill) {
			FleetAI@ claim;
			double bestWeight = 0;

			for(uint i = 0, cnt = war.fleets.fleets.length; i < cnt; ++i) {
				auto@ fleet = war.fleets.fleets[i];
				double w = war.assignable(this, fleet);

				if(w > bestWeight) {
					bestWeight = w;
					@claim = fleet;
				}
			}

			if(claim !is null)
				join(ai, war, claim);
		}

		//Give up the battle when we should
		if(fleets.length == 0) {
			if(!ourPlanetsPresent && !isAttack) {
				//We lost all our planets before we could respond with anything.
				// We might be able to use an attack to claim it back later, but for now we just give up on it.
				if(war.log)
					ai.print("BATTLE: aborted defense, no fleets and no planets left", system.obj);
				for(uint i = 0, cnt = fleets.length; i < cnt; ++i)
					fleets[i].canceled = true;
				return false;
			}
			if(isAttack) {
				//We haven't been able to find any fleets to assign here for a while,
				//so just abort the attack
				if(gameTime - lastHadFleets > 60.0) {
					if(war.log)
						ai.print("BATTLE: aborted attack, no fleets available", system.obj);
					for(uint i = 0, cnt = fleets.length; i < cnt; ++i)
						fleets[i].canceled = true;
					return false;
				}
			}
		}

		return true;
	}

	void clearOrders() {
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			auto@ obj = fleets[i].fleet.obj;
			if(!fleets[i].arrived)
				continue;
			if(obj.hasOrders)
				obj.clearOrders();
		}
	}

	bool canCapture(AI& ai, BattleMission@ miss, Planet@ check) {
		if(!ai.empire.isHostile(check.owner))
			return false;
		//TODO: Wait around a while maybe?
		if(check.isProtected(ai.empire))
			return false;
		return true;
	}

	void moveTo(BattleMission@ miss, Planet@ defPl, bool force = false) {
		if(!miss.arrived)
			return;
		if(!force) {
			if(miss.fleet.obj.hasOrders)
				return;
			double dist = miss.fleet.obj.position.distanceTo(defPl.position);
			if(dist < defPl.OrbitSize)
				return;
		}
		vec3d pos = defPl.position;
		vec2d offset = random2d(defPl.OrbitSize * 0.85);
		pos.x += offset.x;
		pos.z += offset.y;
		miss.fleet.obj.addMoveOrder(pos);
	}

	void attack(BattleMission@ miss, Object@ target, bool force = false) {
		//TODO: make this not chase stuff out of the system like a madman?
		// (in attack logic as well)
		if(!miss.arrived)
			return;
		if(!force) {
			if(miss.fleet.obj.hasOrders)
				return;
		}
		miss.fleet.obj.addAttackOrder(target);
	}

	bool isCapturing(Planet@ pl) {
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			if(fleets[i].capturing is pl)
				return true;
		}
		return false;
	}

	bool get_isCapturingAny() {
		for(uint i = 0, cnt = fleets.length; i < cnt; ++i) {
			if(fleets[i].capturing !is null)
				return true;
		}
		return false;
	}
};

class War : AIComponent {
	Fleets@ fleets;
	Intelligence@ intelligence;
	Relations@ relations;
	Movement@ movement;
	Scouting@ scouting;
	Systems@ systems;
	Military@ military;

	array<Battle@> battles;

	ScoutingMission@ currentScout;

	void create() {
		@fleets = cast<Fleets>(ai.fleets);
		@intelligence = cast<Intelligence>(ai.intelligence);
		@relations = cast<Relations>(ai.relations);
		@movement = cast<Movement>(ai.movement);
		@scouting = cast<Scouting>(ai.scouting);
		@systems = cast<Systems>(ai.systems);
		@military = cast<Military>(ai.military);
	}

	void save(SaveFile& file) {
		uint cnt = battles.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			battles[i].save(this, file);

		fleets.saveMission(file, currentScout);
	}

	void load(SaveFile& file) {
		uint cnt = 0;
		file >> cnt;
		battles.length = cnt;
		for(uint i = 0; i < cnt; ++i) {
			@battles[i] = Battle();
			battles[i].load(this, file);
		}

		@currentScout = cast<ScoutingMission>(fleets.loadMission(file));
		ai.behavior.remnantAllowArbitraryClear = false;
	}

	double getCombatReadyStrength() {
		double str = 0;
		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets.fleets[i];
			if(flAI.fleetClass != FC_Combat)
				continue;
			if(!flAI.readyForAction)
				continue;
			str += flAI.strength;
		}
		return str * str;
	}

	Battle@ attack(SystemAI@ sys) {
		Battle atk;
		@atk.system = sys;
		atk.isAttack = true;
		atk.curPriority = MiP_High;
		@atk.staging = military.getStagingFor(sys.obj);

		if(log)
			ai.print("Organizing an attack against "+sys.obj.name);

		claimFleetsFor(atk);
		battles.insertLast(atk);
		return atk;
	}

	Battle@ defend(SystemAI@ sys) {
		Battle def;
		@def.system = sys;
		@def.staging = military.getClosestStaging(sys.obj);

		if(log)
			ai.print("Organizing defense for "+sys.obj.name);

		battles.insertLast(def);
		return def;
	}

	void claimFleetsFor(Battle@ atk) {
		//TODO: This currently claims everything not in use, should it
		//leave some reserves for defense? Is that good?
		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets.fleets[i];
			if(flAI.fleetClass != FC_Combat)
				continue;
			if(!flAI.readyForAction)
				continue;
			atk.join(ai, this, flAI);
		}
	}

	void sendFleetToJoin(Battle@ atk) {
		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets.fleets[i];
			if(flAI.fleetClass != FC_Combat)
				continue;
			if(!flAI.readyForAction)
				continue;
			atk.join(ai, this, flAI);
			break;
		}
	}

	bool isFightingIn(Region@ reg) {
		for(uint i = 0, cnt = battles.length; i < cnt; ++i) {
			if(battles[i].system.obj is reg)
				return true;
		}
		return false;
	}

	void tick(double time) override {
		for(uint i = 0, cnt = battles.length; i < cnt; ++i) {
			if(!battles[i].tick(ai, this, time)) {
				battles.removeAt(i);
				--i; --cnt;
			}
		}
	}

	double ourStrength;
	double curTime;
	SystemAI@ best;
	double totalWeight;
	SystemAI@ scout;
	uint scoutCount;
	void check(SystemAI@ sys, double baseWeight) {
		if(isFightingIn(sys.obj))
			return;

		if(!sys.visible) {
			sys.strengthCheck(ai, minInterval=5*60.0);
			if(sys.lastStrengthCheck < curTime - 5 * 60.0) {
				scoutCount += 1;
				if(randomd() < 1.0 / double(scoutCount))
					@scout = sys;
				return;
			}
		}
		else {
			sys.strengthCheck(ai, minInterval=60.0);
		}

		double theirStrength = sys.enemyStrength;
		if(ourStrength < theirStrength * ai.behavior.attackStrengthOverkill)
			return;

		double w = baseWeight;

		//Try to capture less important systems at first
		//TODO: This should flip when we go from border skirmishes to subjugation war
		uint capturable = 0;
		for(uint i = 0, cnt = sys.planets.length; i < cnt; ++i) {
			Planet@ pl = sys.planets[i];
			if(!ai.empire.isHostile(pl.owner))
				continue;
			if(pl.isProtected(ai.empire))
				continue;
			w /= 1.0 + double(pl.level);
			capturable += 1;
		}

		//Ignore protected systems
		if(capturable == 0)
			return;

		//See where their defenses are low
		if(theirStrength != 0) {
			double defRatio = ourStrength / theirStrength;
			if(defRatio > 4.0) {
				//We prefer destroying some minor assets over fighting an entirely undefended system,
				//because it hurts more to lose stuff.
				w *= 6.0;
			}
		}
		else {
			w *= 2.0;
		}

		totalWeight += w;
		if(randomd() < w / totalWeight)
			@best = sys;
	}

	int totalEnemySize(SystemAI@ sys) {
		int size = 0;
		for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
			Empire@ other = getEmpire(i);
			if(other.major && ai.empire.isHostile(other))
				size += sys.obj.getStrength(other);
		}
		return size;
	}

	bool isUnderAttack(SystemAI@ sys) {
		if(sys.obj.ContestedMask & ai.mask == 0)
			return false;
		if(totalEnemySize(sys) < 100) {
			if(sys.obj.SiegedMask & ai.mask == 0)
				return false;
		}
		return true;
	}

	double assignable(Battle& battle, FleetAI& fleet) {
		if(fleet.fleetClass != FC_Combat)
			return 0.0;
		double w = 1.0;
		if(fleet.mission !is null) {
			w *= 0.1;
			if(fleet.mission.priority >= MiP_High)
				w *= 0.1;
			if(fleet.mission.priority >= battle.curPriority)
				return 0.0;

			auto@ miss = cast<BattleMission>(fleet.mission);
			if(miss !is null && miss.battleIn is battle.system.obj)
				return 0.0;
		}
		else if(fleet.isHome && fleet.stationed is battle.system.obj) {
			//This should be allowed to fight always
		}
		else if(battle.curPriority >= MiP_Critical) {
			if(fleet.supplies < 0.25)
				return 0.0;
			if(fleet.fleetHealth < 0.25)
				return 0.0;
			if(fleet.filled < 0.2)
				return 0.0;
			//DOF - Do not send badly damaged flagships
			if(fleet.flagshipHealth < 0.5)
				return 0.0;

			if(fleet.obj.isMoving) {
				if(fleet.obj.velocity.length / fleet.obj.maxAcceleration > 16.0)
					w *= 0.1;
			}
		}
		else {
			if(!fleet.readyForAction)
				return 0.0;
		}
		double fleetStrength = fleet.strength;
		if(battle.ourStrength + fleetStrength < battle.enemyStrength * ai.behavior.battleStrengthOverkill)
			w *= 0.25;
		return w;
	}

	double winnability(Battle& battle) {
		double ours = sqrt(battle.ourStrength);
		double theirs = battle.enemyStrength;
		if(theirs <= 0.0)
			return 10.0;

		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			auto@ fleet = fleets.fleets[i];
			double w = assignable(battle, fleet);
			if(w != 0.0)
				ours += sqrt(fleet.strength);
		}
		ours *= ours;

		return ours / theirs;
	}

	void focusTick(double time) override {
		if(currentScout !is null) {
			if(currentScout.canceled || currentScout.completed)
				@currentScout = null;
		}

		//Change our behavior a little depending on the state
		ai.behavior.remnantAllowArbitraryClear = !relations.isFightingWar(aggressive=true) && battles.length == 0;

		//Find any systems that need defending
		//TODO: Defend allies at lowered priority
		if(ai.behavior.forbidDefense) {
			for(uint i = 0, cnt = systems.owned.length; i < cnt; ++i) {
				SystemAI@ sys = systems.owned[i];
				if(!isUnderAttack(sys))
					continue;
				if(isFightingIn(sys.obj))
					continue;
				defend(sys);
				return;
			}
		}

		if(ai.behavior.forbidAttack)
			return;
		//Do attacks
		uint ready = fleets.countCombatReadyFleets();
		if(ready != 0) {
			//See if we can start a new attack
			if(battles.length < ai.behavior.maxBattles && relations.isFightingWar(aggressive=true)
					&& (battles.length == 0 || ready > ai.behavior.battleReserveFleets)) {
				//Determine our own strength
				ourStrength = getCombatReadyStrength();

				//Evaluate systems to attack in our aggressive war
				@best = null;
				totalWeight = 0;
				curTime = gameTime;
				@scout = null;
				scoutCount = 0;
				//TODO: Consider aggressive wars against an empire to also be against that empire's vassals
				for(uint i = 0, cnt = intelligence.intel.length; i < cnt; ++i) {
					auto@ intel = intelligence.intel[i];
					if(intel is null)
						continue;

					auto@ relation = relations.get(intel.empire);
					if(!relation.atWar || !relation.aggressive)
						continue;

					for(uint n = 0, ncnt = intel.shared.length; n < ncnt; ++n) {
						auto@ sys = intel.shared[n];
						check(sys, 20.0);
					}

					for(uint n = 0, ncnt = intel.theirBorder.length; n < ncnt; ++n) {
						auto@ sys = intel.theirBorder[n];
						check(sys, 1.0);
					}
				}

				//Make the attack with our fleets
				if(best !is null)
					attack(best);
				else if(scout !is null && currentScout is null) {
					if(log)
						ai.print("War requests scout to flyby "+scout.obj.name);
					@currentScout = scouting.scout(scout.obj);
				}
			}
		}
	}
};

AIComponent@ createWar() {
	return War();
}

Added scripts/server/empire_ai/weasel/WeaselAI.as.






























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import settings.game_settings;
from empire_ai.EmpireAI import AIController;

import AIComponent@ createEvents() from "empire_ai.weasel.Events";
import AIComponent@ createColonization() from "empire_ai.weasel.Colonization";
import AIComponent@ createResources() from "empire_ai.weasel.Resources";
import AIComponent@ createPlanets() from "empire_ai.weasel.Planets";
import AIComponent@ createSystems() from "empire_ai.weasel.Systems";
import AIComponent@ createFleets() from "empire_ai.weasel.Fleets";
import AIComponent@ createScouting() from "empire_ai.weasel.Scouting";
import AIComponent@ createDevelopment() from "empire_ai.weasel.Development";
import AIComponent@ createDesigns() from "empire_ai.weasel.Designs";
import AIComponent@ createBudget() from "empire_ai.weasel.Budget";
import AIComponent@ createConstruction() from "empire_ai.weasel.Construction";
import AIComponent@ createMilitary() from "empire_ai.weasel.Military";
import AIComponent@ createMovement() from "empire_ai.weasel.Movement";
import AIComponent@ createCreeping() from "empire_ai.weasel.Creeping";
import AIComponent@ createRelations() from "empire_ai.weasel.Relations";
import AIComponent@ createIntelligence() from "empire_ai.weasel.Intelligence";
import AIComponent@ createWar() from "empire_ai.weasel.War";
import AIComponent@ createResearch() from "empire_ai.weasel.Research";
import AIComponent@ createEnergy() from "empire_ai.weasel.Energy";
import IAIComponent@ createDiplomacy() from "empire_ai.weasel.Diplomacy";
import AIComponent@ createConsider() from "empire_ai.weasel.Consider";
import AIComponent@ createOrbitals() from "empire_ai.weasel.Orbitals";
import AIComponent@ createInfrastructure() from "empire_ai.weasel.Infrastructure";

import AIComponent@ createHyperdrive() from "empire_ai.weasel.ftl.Hyperdrive";
import AIComponent@ createGate() from "empire_ai.weasel.ftl.Gate";
import AIComponent@ createFling() from "empire_ai.weasel.ftl.Fling";
import AIComponent@ createSlipstream() from "empire_ai.weasel.ftl.Slipstream";
import AIComponent@ createJumpdrive() from "empire_ai.weasel.ftl.Jumpdrive";

import AIComponent@ createVerdant() from "empire_ai.weasel.race.Verdant";
import AIComponent@ createMechanoid() from "empire_ai.weasel.race.Mechanoid";
import AIComponent@ createStarChildren() from "empire_ai.weasel.race.StarChildren";
import AIComponent@ createExtragalactic() from "empire_ai.weasel.race.Extragalactic";
import AIComponent@ createLinked() from "empire_ai.weasel.race.Linked";
import AIComponent@ createDevout() from "empire_ai.weasel.race.Devout";
import AIComponent@ createAncient() from "empire_ai.weasel.race.Ancient";

import AIComponent@ createInvasion() from "empire_ai.weasel.misc.Invasion";
import bool hasInvasionMap() from "Invasion.InvasionMap";

from buildings import BuildingType;
from orbitals import OrbitalModule;
from constructions import ConstructionType;
import util.formatting;

from empire import ai_full_speed;

from traits import getTraitID;

export IAIComponent, AIComponent, AI;
uint GUARD = 0xDEADBEEF;

enum AddedComponent {
	AC_Invasion = 0x1,
};

interface IAIComponent : Savable {
	void set(AI& ai);
	void setLog();
	void setLogCritical();

	double getPrevFocus();
	void setPrevFocus(double value);

	void create();
	void start();

	void tick(double time);
	void focusTick(double time);
	void turn();

	void save(SaveFile& file);
	void load(SaveFile& file);
	void postLoad(AI& ai);
	void postSave(AI& ai);
	void loadFinalize(AI& ai);
};

class AIComponent : IAIComponent, Savable {
	AI@ ai;
	double prevFocus = 0;
	bool log = false;
	bool logCritical = false;
	bool logErrors = true;

	double getPrevFocus() { return prevFocus; }
	void setPrevFocus(double value) { prevFocus = value; }

	void setLog() { log = true; }
	void setLogCritical() { logCritical = true; }

	void set(AI& ai) { @this.ai = ai; }
	void create() {}
	void start() {}

	void tick(double time) {}
	void focusTick(double time) {}
	void turn() {}

	void save(SaveFile& file) {}
	void load(SaveFile& file) {}
	void postLoad(AI& ai) {}
	void postSave(AI& ai) {}
	void loadFinalize(AI& ai) {}
};

class ProfileData {
	double tickPeak = 0.0;
	double tickAvg = 0.0;
	double tickCount = 0.0;

	double focusPeak = 0.0;
	double focusAvg = 0.0;
	double focusCount = 0.0;
};

final class AIBehavior {
	//AIEmpire controls
	bool forbidDiplomacy = false;
	bool forbidColonization = false;
	bool forbidCreeping = false;
	bool forbidResearch = false;
	bool forbidDefense = false;
	bool forbidAttack = false;
	bool forbidConstruction = false;
	bool forbidScouting = false;
	bool forbidAnomalyChoice = false;
	bool forbidArtifact = false;
	bool forbidScuttle = false;

	//How many focuses we can manage in a tick
	uint focusPerTick = 2;

	//The maximum colonizations this AI can do in one turn
	uint maxColonizations = UINT_MAX;
	//How many colonizations we're guaranteed to be able to do in one turn regardless of finances
	uint guaranteeColonizations = 2;
	//How many colonizations at most we can be doing at once
	uint maxConcurrentColonizations = UINT_MAX;
	//Whether this AI will colonize planets in systems owned by someone else
	// TODO: This should be partially ignored for border systems, so it can try to aggressively expand into the border
	bool colonizeEnemySystems = false;
	bool colonizeNeutralOwnedSystems = false;
	bool colonizeAllySystems = false;
	//How much this AI values claiming new systems instead of colonizing stuff in its existing ones
	double weightOutwardExpand = 2.0;
	//How much money this AI considers a colonization event to cost out of the budget
	int colonizeBudgetCost = 80;
	//Whether to do any generic expansion beyond any requests
	bool colonizeGenericExpand = true;
	//Latest percentage into a budget cycle that we still allow colonization
	double colonizeMaxBudgetProgress = 0.66;
	//Time after initial ownership change that an incomplete colonization is canceled
	double colonizeFailGraceTime = 100.0;
	//Time a planet that we failed to colonize is disregarded for colonization
	double colonizePenalizeTime = 9.0 * 60.0;

	//Maximum amount of scouting missions that can be performed simultaneously
	uint maxScoutingMissions = UINT_MAX;
	//Minimum time after losing vision over a system that we can scout it again
	double minScoutingInterval = 3.0 * 60.0;
	//Weight that it gives to exploring things near our empire instead of greedily exploring nearby things
	double exploreBorderWeight = 2.0;
	//How long we consider all fleets viable for scouting with
	double scoutAllTimer = 3.0 * 60.0;
	//How many scouts we want to have active
	uint scoutsActive = 2;
	//How many scanning missions we can do at once
	uint maxScanningMissions = 1;
	//Whether to prioritize scouting over scanning if we only have one scout
	bool prioritizeScoutOverScan = true;

	//Weights for what to do in generic planet development
	//  Leveling up an existing development focus
	double focusDevelopWeight = 1.0;
	//  Colonizing a new scalable or high tier to focus on
	double focusColonizeNewWeight = 4.0;
	//  Colonizing a new high tier resource to import to one of our focuses
	double focusColonizeHighTierWeight = 1.0;

	//How many potential designs are evaluated before choosing the best one
	uint designEvaluateCount = 10;
	//How long a fleet has to be fully idle before it returns to its stationed system
	double fleetIdleReturnStationedTime = 60.0;
	//How long we try to have a fleet be capable of firing before running out of supplies
	double fleetAimSupplyDuration = 2.0 * 60.0;

	//How long a potential construction can take at most before we consider it unreasonable
	double constructionMaxTime = 10.0 * 60.0;
	//How long a factory has to have been idle for us to consider constructing labor storage
	double laborStoreIdleTimer = 60.0;
	//Maximum amount of time worth of labor we want to store in our warehouses
	double laborStoreMaxFillTime = 60.0 * 10.0;
	//Whether to use labor to build asteroids in the background
	bool backgroundBuildAsteroids = true;
	//Whether to choose the best resource on an asteroid, instead of doing it randomly
	bool chooseAsteroidResource = true;
	//Whether to distribute labor to shipyards when planets are idle
	bool distributeLaborExports = true;
	//Whether to build a shipyard to consolidate multiple planets of labor where possible
	bool consolidateLaborExports = true;
	//Estimate amount of labor spent per point of support ship size
	double estSizeSupportLabor = 0.25;

	//Maximum combat fleets we can have in service at once (counts starting fleet(s))
	uint maxActiveFleets = UINT_MAX;
	//How much flagship size we try to make per available money
	double shipSizePerMoney = 1.0 / 3.5;
	//How much flagship size we try to make per available labor
	double shipSizePerLabor = 1.0 / 0.33;
	//How much maintenance we expect per ship size
	double maintenancePerShipSize = 2.0;
	//Minimum percentage increase in size before we decide to retrofit a flagship to be bigger
	double shipRetrofitThreshold = 0.5;
	//Whether to retrofit our free starting fleet if appropriate
	bool retrofitFreeFleets = false;
	//Minimum percentage of average current flagship size new fleets should be
	double flagshipBuildMinAvgSize = 1.00;
	//Minimum game time before we consider constructing new flagships
	double flagshipBuildMinGameTime = 4.0 * 60.0;
	//Whether to build factories when we need labor
	bool buildFactoryForLabor = true;
	//Whether to build warehouses when we're not using labor
	bool buildLaborStorage = true;
	//Whether factories can queue labor resource imports when needed
	bool allowRequestLaborImports = true;
	//Whether fleets with ghosted supports attempt to rebuild the ghosts or just clear them
	bool fleetsRebuildGhosts = true;
	//When trying to order supports on a fleet, wait for the planet to construct its supports so we can claim them
	bool supportOrderWaitOnFactory = true;

	//How much stronger we need to be than a remnant fleet to clear it
	double remnantOverkillFactor = 1.5;
	//Whether to allow idle fleets to be sent to clear remnants
	// Modified by Relations
	bool remnantAllowArbitraryClear = true;

	//Whether we should aggressively try to take out enemies
	bool aggressive = false;
	//Whether to become aggressive after we get boxed in and can no longer expand anywhere
	bool aggressiveWhenBoxedIn = false;
	//Whether we should never declare war ourselves
	bool passive = false;
	//Whether to hate human players the most
	bool biased = false;
	//How much stronger we need to be than someone to declare war out of hatred
	double hatredWarOverkill = 0.5;
	//How much stronger we need to be than someone to try to take them out in an aggressive war
	double aggressiveWarOverkill = 1.5;
	//How much stronger we want to be before we attack a system
	double attackStrengthOverkill = 1.5;
	//How many battles we can be performing at once
	uint maxBattles = UINT_MAX;
	//How much we try to overkill while fighting
	double battleStrengthOverkill = 1.5;
	//How many fleets we don't commit to attacks when we're already currently fighting
	uint battleReserveFleets = 1;
	//How much extra supply we try to have before starting a capture, to make sure we can actually do it
	double captureSupplyEstimate = 1.5;
	//Maximum hop distance we use as staging areas for our attacks
	int stagingMaxHops = 5;
	//If our fleet fill is less than this, immediately move back to factory from staging
	double stagingToFactoryFill = 0.6;

	//How much ftl is reserved for critical applications
	double ftlReservePctCritical = 0.25;
	//How much ftl is reserved to not be used for background applications
	double ftlReservePctNormal = 0.25;

	//How many artifacts we consider where to use per focus turn
	uint artifactFocusConsiderCount = 2;

	//How long after trying to build a generically requested building we give up
	double genericBuildExpire = 3.0 * 60.0;

	//How much the hate in a relationship decays to every minute
	double hateDecayRate = 0.9;
	//How much weaker we need to be to even consider surrender
	double surrenderMinStrength = 0.5;
	//How many of our total war points have to be taken by an empire for us to surrender
	double acceptSurrenderRatio = 0.75;
	double offerSurrenderRatio = 0.5;

	void setDifficulty(int diff, uint flags) {
		//This changes the behavior values based on difficulty and flags
		if(flags & AIF_Aggressive != 0)
			aggressive = true;
		if(flags & AIF_Passive != 0)
			passive = true;
		if(flags & AIF_Biased != 0)
			biased = true;

		//Low difficulties can't colonize as many things at once
		if(diff <= 0) {
			maxConcurrentColonizations = 1;
			guaranteeColonizations = 1;
			weightOutwardExpand = 0.5;
		}
		else if(diff <= 1) {
			maxConcurrentColonizations = 2;
			weightOutwardExpand = 1.0;
		}

		//Hard AI becomes aggressive when it gets boxed in
		aggressiveWhenBoxedIn = diff >= 2;

		//Easy difficulty can't attack and defend at the same time
		if(diff <= 0)
			maxBattles = 1;

		//Low difficulties aren't as good at managing labor
		if(diff <= 0) {
			distributeLaborExports = false;
			consolidateLaborExports = false;
			buildLaborStorage = false;
		}
		else if(diff <= 1) {
			consolidateLaborExports = false;
		}

		//Low difficulties aren't as good at managing fleets
		if(diff <= 0) {
			maxActiveFleets = 2;
			retrofitFreeFleets = true;
		}

		//Low difficulties aren't as good at scouting
		if(diff <= 1)
			scoutAllTimer = 0.0;

		//Low difficulties are worse at designing
		if(diff <= 0)
			designEvaluateCount = 3;
		else if(diff <= 1)
			designEvaluateCount = 8;
		else
			designEvaluateCount = 12;

		//Easy is a bit slow
		if(diff <= 0)
			focusPerTick = 1;
		else if(diff >= 2)
			focusPerTick = 3;
	}
};

final class AIDefs {
	const BuildingType@ Factory;
	const BuildingType@ LaborStorage;
	const ConstructionType@ MoonBase;
	const OrbitalModule@ Shipyard;
	const OrbitalModule@ TradeOutpost;
	const OrbitalModule@ TradeStation;
};

final class AI : AIController, Savable {
	Empire@ empire;
	AIBehavior behavior;
	AIDefs defs;

	int cycleId = -1;
	uint componentCycle = 0;
	uint addedComponents = 0;

	uint majorMask = 0;
	uint difficulty = 0;
	uint flags = 0;
	bool isLoading = false;

	array<IAIComponent@> components;
	array<ProfileData> profileData;
	IAIComponent@ events;
	IAIComponent@ fleets;
	IAIComponent@ budget;
	IAIComponent@ colonization;
	IAIComponent@ resources;
	IAIComponent@ planets;
	IAIComponent@ systems;
	IAIComponent@ scouting;
	IAIComponent@ development;
	IAIComponent@ designs;
	IAIComponent@ construction;
	IAIComponent@ military;
	IAIComponent@ movement;
	IAIComponent@ creeping;
	IAIComponent@ relations;
	IAIComponent@ intelligence;
	IAIComponent@ war;
	IAIComponent@ research;
	IAIComponent@ energy;
	IAIComponent@ diplomacy;
	IAIComponent@ consider;
	IAIComponent@ orbitals;
	IAIComponent@ infrastructure;

	IAIComponent@ ftl;
	IAIComponent@ race;

	IAIComponent@ invasion;

	void createComponents() {
		//NOTE: This is also save/load order, so
		//make sure to add loading logic when changing this list
		@events = add(createEvents());
		@budget = add(createBudget());
		@planets = add(createPlanets());
		@resources = add(createResources());
		@systems = add(createSystems());
		@colonization = add(createColonization());
		@fleets = add(createFleets());
		@scouting = add(createScouting());
		@development = add(createDevelopment());
		@designs = add(createDesigns());
		@construction = add(createConstruction());
		@military = add(createMilitary());
		@movement = add(createMovement());
		@creeping = add(createCreeping());
		@relations = add(createRelations());
		@intelligence = add(createIntelligence());
		@war = add(createWar());
		@research = add(createResearch());
		@energy = add(createEnergy());
		@diplomacy = add(createDiplomacy());
		@consider = add(createConsider());
		@orbitals = add(createOrbitals());
		@infrastructure = add(createInfrastructure());

		//Make FTL component
		if(empire.hasTrait(getTraitID("Hyperdrive")))
			@ftl = add(createHyperdrive());
		else if(empire.hasTrait(getTraitID("Gate")))
			@ftl = add(createGate());
		else if(empire.hasTrait(getTraitID("Fling")))
			@ftl = add(createFling());
		else if(empire.hasTrait(getTraitID("Slipstream")))
			@ftl = add(createSlipstream());
		else if(empire.hasTrait(getTraitID("Jumpdrive")))
			@ftl = add(createJumpdrive());
		/* Not implemented yet.
		else if(empire.hasTrait(getTraitID("Flux")))
			@ftl = add(createFlux());
		*/

		//Make racial component
		if(empire.hasTrait(getTraitID("Verdant")))
			@race = add(createVerdant());
		else if(empire.hasTrait(getTraitID("Mechanoid")))
			@race = add(createMechanoid());
		else if(empire.hasTrait(getTraitID("StarChildren")))
			@race = add(createStarChildren());
		else if(empire.hasTrait(getTraitID("Extragalactic")))
			@race = add(createExtragalactic());
		else if(empire.hasTrait(getTraitID("Linked")))
			@race = add(createLinked());
		else if(empire.hasTrait(getTraitID("Devout")))
			@race = add(createDevout());
		else if(empire.hasTrait(getTraitID("Ancient")))
			@race = add(createAncient());
		/* Not implemented yet.
		else if(empire.hasTrait(getTraitID("Technicists")))
			@race = add(createResearchers());
		else if(empire.hasTrait(getTraitID("Progenitors")))
			@race = add(createProgenitors());
		else if(empire.hasTrait(getTraitID("Berserkers")))
			@race = add(createBerserkers());
		else if(empire.hasTrait(getTraitID("Pacifists")))
			@race = add(createPacifists());
		*/

		//Misc components
		if(hasInvasionMap() || addedComponents & AC_Invasion != 0) {
			@invasion = add(createInvasion());
			addedComponents |= AC_Invasion;
		}

		//if(empire is playerEmpire) {
			//log(race);
		//	log(colonization);
		//	log(resources);
		//	log(construction);
		//}
		//log(intelligence);
		//logAll();
		logCritical();

		profileData.length = components.length;
		for(uint i = 0, cnt = components.length; i < cnt; ++i)
			components[i].create();
	}

	void createGeneral() {
	}

	void init(Empire& emp, EmpireSettings& settings) {
		@this.empire = emp;
		flags = settings.aiFlags;
		difficulty = settings.difficulty;
		behavior.setDifficulty(difficulty, flags);

		createComponents();
	}

	int getDifficultyLevel() {
		return difficulty;
	}

	void load(SaveFile& file) {
		file >> empire;
		file >> cycleId;
		file >> majorMask;
		file >> difficulty;
		file >> flags;
		if(file >= SV_0153)
			file >> addedComponents;
		behavior.setDifficulty(difficulty, flags);
		createComponents();
		createGeneral();

		uint loadCnt = 0;
		file >> loadCnt;
		loadCnt = loadCnt;
		for(uint i = 0; i < loadCnt; ++i) {
			double prevFocus = 0;
			file >> prevFocus;
			components[i].setPrevFocus(prevFocus);
			file >> components[i];

			uint check = 0;
			file >> check;
			if(check != GUARD)
				error("ERROR: AI Load error detected in component "+addrstr(components[i])+" of "+empire.name);
		}
		for(uint i = 0, cnt = components.length; i < cnt; ++i)
			components[i].postLoad(this);
		isLoading = true;
	}

	void save(SaveFile& file) {
		file << empire;
		file << cycleId;
		file << majorMask;
		file << difficulty;
		file << flags;
		file << addedComponents;
		uint saveCnt = components.length;
		file << saveCnt;
		for(uint i = 0; i < saveCnt; ++i) {
			file << components[i].getPrevFocus();
			file << components[i];
			file << GUARD;
		}
		for(uint i = 0, cnt = components.length; i < cnt; ++i)
			components[i].postSave(this);
	}

	void log(IAIComponent@ comp) {
		if(comp is null)
			return;
		comp.setLog();
		comp.setLogCritical();
	}

	void logCritical() {
		for(uint i = 0, cnt = components.length; i < cnt; ++i)
			components[i].setLogCritical();
	}

	void logAll() {
		for(uint i = 0, cnt = components.length; i < cnt; ++i) {
			components[i].setLog();
			components[i].setLogCritical();
		}
	}

	IAIComponent@ add(IAIComponent& component) {
		component.set(this);
		components.insertLast(component);
		return component;
	}

	void init(Empire& emp) {
		majorMask = 0;
		for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
			Empire@ emp = getEmpire(i);
			if(emp.major)
				majorMask |= emp.mask;
		}

		createGeneral();
	}

	bool hasStarted = false;
	void tick(Empire& emp, double time) {
		if(isLoading) {
			for(uint i = 0, cnt = components.length; i < cnt; ++i)
				components[i].loadFinalize(this);
			isLoading = false;
			hasStarted = true;
		}
		else if(!hasStarted) {
			for(uint i = 0, cnt = components.length; i < cnt; ++i)
				components[i].start();
			hasStarted = true;
		}
		else if(emp.Victory == -1) {
			//Don't do anything when actually defeated
			return;
		}

		//Manage gametime-specific behaviors
		behavior.colonizeGenericExpand = gameTime >= 6.0 * 60.0;

		//Find cycled turns
		int curCycle = emp.BudgetCycleId;
		if(curCycle != cycleId) {
			for(uint i = 0, cnt = components.length; i < cnt; ++i)
				components[i].turn();
			cycleId = curCycle;
		}

		//Generic ticks
		double startTime = getExactTime();
		for(uint i = 0, cnt = components.length; i < cnt; ++i) {
			auto@ comp = components[i];
			comp.tick(time);

			double endTime = getExactTime();
			//double ms = 1000.0 * (endTime - startTime);
			startTime = endTime;

			//auto@ dat = profileData[i];
			//dat.tickPeak = max(dat.tickPeak, ms);
			//dat.tickAvg += ms;
			//dat.tickCount += 1.0;
		}

		//Do focuseds tick on components
		uint focusCount = behavior.focusPerTick;
		if(ai_full_speed.value == 1.0)
			focusCount = max(uint(round((time / 0.25) * behavior.focusPerTick)), behavior.focusPerTick);
		double allocStart = startTime;

		for(uint n = 0; n < focusCount; ++n) {
			componentCycle = (componentCycle+1) % components.length;
			auto@ focusComp = components[componentCycle];
			focusComp.focusTick(gameTime - focusComp.getPrevFocus());
			focusComp.setPrevFocus(gameTime);

			double endTime = getExactTime();
			//double ms = 1000.0 * (endTime - startTime);
			startTime = endTime;
			if(endTime - allocStart > 4000.0)
				break;

			//auto@ dat = profileData[componentCycle];
			//dat.focusPeak = max(dat.focusPeak, ms);
			//dat.focusAvg += ms;
			//dat.focusCount += 1.0;
		}
	}

	void dumpProfile() {
		for(uint i = 0, cnt = components.length; i < cnt; ++i) {
			auto@ c = profileData[i];
			print(pad(addrstr(components[i]), 40)+" tick peak "+toString(c.tickPeak,2)+"    tick avg "+toString(c.tickAvg/c.tickCount, 2)
				+"    focus peak "+toString(c.focusPeak,2)+"    focus avg "+toString(c.focusAvg/c.focusCount, 2));
		}
	}

	void resetProfile() {
		for(uint i = 0, cnt = profileData.length; i < cnt; ++i) {
			auto@ c = profileData[i];
			c.tickPeak = 0.0;
			c.tickAvg = 0.0;
			c.tickCount = 0.0;
			c.focusPeak = 0.0;
			c.focusAvg = 0.0;
			c.focusCount = 0.0;
		}
	}

	uint get_mask() {
		return empire.mask;
	}

	uint get_teamMask() {
		//TODO
		return empire.mask;
	}

	uint get_visionMask() {
		return majorMask & empire.visionMask;
	}

	uint get_allyMask() {
		return empire.mutualDefenseMask | empire.ForcedPeaceMask.value;
	}

	uint get_enemyMask() {
		return empire.hostileMask & majorMask;
	}

	uint get_neutralMask() {
		return majorMask & ~allyMask & ~mask & ~enemyMask;
	}

	uint get_otherMask() {
		return majorMask & ~mask;
	}

	string pad(const string& input, uint width) {
		string str = input;
		while(str.length < width)
			str += " ";
		return str;
	}

	void print(const string& info, Object@ related = null, double value = INFINITY, bool flag = false, Empire@ emp = null) {
		string str = info;
		if(related !is null)
			str = pad(related.name, 16)+" | "+str;
		str = pad("["+empire.index+": "+empire.name+" AI] ", 20)+str;
		str = formatGameTime(gameTime) + " " + str;
		if(value != INFINITY)
			str += " | Value = "+standardize(value, true);
		if(flag)
			str += " | FLAGGED On";
		if(emp !is null)
			str += " | Target = "+emp.name;
		::print(str);
	}

	void debugAI() {}
	void commandAI(string cmd) {
		if (cmd == "forbid all") {
			behavior.forbidDiplomacy = true;
			behavior.forbidColonization = true;
			behavior.forbidCreeping = true;
			behavior.forbidResearch = true;
			behavior.forbidDefense = true;
			behavior.forbidAttack = true;
			behavior.forbidConstruction = true;
			behavior.forbidScouting = true;
			behavior.forbidAnomalyChoice = true;
			behavior.forbidArtifact = true;
			behavior.forbidScuttle = true;
		} else if (cmd == "allow all") {
			behavior.forbidDiplomacy = false;
			behavior.forbidColonization = false;
			behavior.forbidCreeping = false;
			behavior.forbidResearch = false;
			behavior.forbidDefense = false;
			behavior.forbidAttack = false;
			behavior.forbidConstruction = false;
			behavior.forbidScouting = false;
			behavior.forbidAnomalyChoice = false;
			behavior.forbidArtifact = false;
			behavior.forbidScuttle = false;
		} else if (cmd == "forbid Diplomacy") { behavior.forbidDiplomacy = true;
		} else if (cmd == "allow Diplomacy") { behavior.forbidDiplomacy = false;
		} else if (cmd == "forbid Colonization") { behavior.forbidColonization = true;
		} else if (cmd == "allow Colonization") { behavior.forbidColonization = false;
		} else if (cmd == "forbid Creeping") { behavior.forbidCreeping = true;
		} else if (cmd == "allow Creeping") { behavior.forbidCreeping = false;
		} else if (cmd == "forbid Research") { behavior.forbidResearch = true;
		} else if (cmd == "allow Research") { behavior.forbidResearch = false;
		} else if (cmd == "forbid Defense") { behavior.forbidDefense = true;
		} else if (cmd == "allow Defense") { behavior.forbidDefense = false;
		} else if (cmd == "forbid Attack") { behavior.forbidAttack = true;
		} else if (cmd == "allow Attack") { behavior.forbidAttack = false;
		} else if (cmd == "forbid Construction") { behavior.forbidConstruction = true;
		} else if (cmd == "allow Construction") { behavior.forbidConstruction = false;
		} else if (cmd == "forbid Scouting") { behavior.forbidScouting = true;
		} else if (cmd == "allow Scouting") { behavior.forbidScouting = false;
		} else if (cmd == "forbid AnomalyChoice") { behavior.forbidAnomalyChoice = true;
		} else if (cmd == "allow AnomalyChoice") { behavior.forbidAnomalyChoice = false;
		} else if (cmd == "forbid Artifact") { behavior.forbidArtifact = true;
		} else if (cmd == "allow Artifact") { behavior.forbidArtifact = false;
		} else if (cmd == "forbid Scuttle") { behavior.forbidScuttle = true;
		} else if (cmd == "allow Scuttle") { behavior.forbidScuttle = false;
		} else {
			print("WeaselAI: got unhandled AI command: " + cmd);
		}
	}
	void aiPing(Empire@ fromEmpire, vec3d position, uint type) {}
	void pause(Empire& emp) {}
	void resume(Empire& emp) {}
	vec3d get_aiFocus() { return vec3d(); }
	string getOpinionOf(Empire& emp, Empire@ other) { return ""; }
	int getStandingTo(Empire& emp, Empire@ other) { return 0; }
};

AIController@ createWeaselAI() {
	return AI();
}

Added scripts/server/empire_ai/weasel/debug.as.














































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.Colonization;
import empire_ai.weasel.Construction;
import empire_ai.weasel.Budget;
import empire_ai.weasel.Designs;
import empire_ai.weasel.Development;
import empire_ai.weasel.Fleets;
import empire_ai.weasel.Military;
import empire_ai.weasel.Planets;
import empire_ai.weasel.Resources;
import empire_ai.weasel.Scouting;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Creeping;
import empire_ai.weasel.Movement;
import empire_ai.weasel.Relations;
import empire_ai.weasel.Intelligence;
import empire_ai.weasel.War;
import empire_ai.weasel.Research;
import empire_ai.weasel.Energy;
import empire_ai.weasel.Diplomacy;
import empire_ai.weasel.ftl.Gate;
import empire_ai.weasel.ftl.Hyperdrive;
import empire_ai.weasel.ftl.Fling;
import empire_ai.weasel.ftl.Slipstream;
import empire_ai.weasel.ftl.Jumpdrive;
import empire_ai.weasel.race.Verdant;
import empire_ai.weasel.race.Mechanoid;
import empire_ai.weasel.race.StarChildren;
import empire_ai.weasel.race.Extragalactic;
import empire_ai.weasel.race.Linked;
import empire_ai.weasel.race.Devout;
import empire_ai.weasel.race.Ancient;
import empire_ai.weasel.misc.Invasion;
import empire_ai.EmpireAI;

AI@ ai(uint index) {
	Empire@ emp = getEmpire(index);
	return cast<AI>(cast<EmpireAI>(emp.EmpireAI).ctrl);
}

Colonization@ colonization(uint index) {
	return cast<Colonization>(ai(index).colonization);
}

Construction@ construction(uint index) {
	return cast<Construction>(ai(index).construction);
}

Budget@ budget(uint index) {
	return cast<Budget>(ai(index).budget);
}

Designs@ designs(uint index) {
	return cast<Designs>(ai(index).designs);
}

Development@ development(uint index) {
	return cast<Development>(ai(index).development);
}

Fleets@ fleets(uint index) {
	return cast<Fleets>(ai(index).fleets);
}

Military@ military(uint index) {
	return cast<Military>(ai(index).military);
}

Planets@ planets(uint index) {
	return cast<Planets>(ai(index).planets);
}

Resources@ resources(uint index) {
	return cast<Resources>(ai(index).resources);
}

Scouting@ scouting(uint index) {
	return cast<Scouting>(ai(index).scouting);
}

Systems@ systems(uint index) {
	return cast<Systems>(ai(index).systems);
}

Movement@ movement(uint index) {
	return cast<Movement>(ai(index).movement);
}

Creeping@ creeping(uint index) {
	return cast<Creeping>(ai(index).creeping);
}

Relations@ relations(uint index) {
	return cast<Relations>(ai(index).relations);
}

Intelligence@ intelligence(uint index) {
	return cast<Intelligence>(ai(index).intelligence);
}

War@ war(uint index) {
	return cast<War>(ai(index).war);
}

Research@ research(uint index) {
	return cast<Research>(ai(index).research);
}

Energy@ energy(uint index) {
	return cast<Energy>(ai(index).energy);
}

Diplomacy@ diplomacy(uint index) {
	return cast<Diplomacy>(ai(index).diplomacy);
}

Gate@ gate(uint index) {
	return cast<Gate>(ai(index).ftl);
}

Hyperdrive@ hyperdrive(uint index) {
	return cast<Hyperdrive>(ai(index).ftl);
}

Fling@ fling(uint index) {
	return cast<Fling>(ai(index).ftl);
}

Slipstream@ slipstream(uint index) {
	return cast<Slipstream>(ai(index).ftl);
}

Jumpdrive@ jumpdrive(uint index) {
	return cast<Jumpdrive>(ai(index).ftl);
}

Mechanoid@ mechanoid(uint index) {
	return cast<Mechanoid>(ai(index).race);
}

Verdant@ verdant(uint index) {
	return cast<Verdant>(ai(index).race);
}

StarChildren@ starchildren(uint index) {
	return cast<StarChildren>(ai(index).race);
}

Extragalactic@ extragalactic(uint index) {
	return cast<Extragalactic>(ai(index).race);
}

Linked@ linked(uint index) {
	return cast<Linked>(ai(index).race);
}

Devout@ devout(uint index) {
	return cast<Devout>(ai(index).race);
}

Ancient@ ancient(uint index) {
	return cast<Ancient>(ai(index).race);
}

Invasion@ invasion(uint index) {
	return cast<Invasion>(ai(index).invasion);
}

Added scripts/server/empire_ai/weasel/ftl/Fling.as.




































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.Movement;
import empire_ai.weasel.Military;
import empire_ai.weasel.Construction;
import empire_ai.weasel.Designs;
import empire_ai.weasel.Development;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Budget;
import empire_ai.weasel.Fleets;

import ftl;
from orbitals import getOrbitalModuleID;

const double FLING_MIN_DISTANCE_STAGE = 10000;
const double FLING_MIN_DISTANCE_DEVELOP = 20000;
const double FLING_MIN_TIMER = 3.0 * 60.0;

int flingModule = -1;

void init() {
	flingModule = getOrbitalModuleID("FlingCore");
}

class FlingRegion : Savable {
	Region@ region;
	Object@ obj;
	bool installed = false;
	vec3d destination;

	void save(SaveFile& file) {
		file << region;
		file << obj;
		file << installed;
		file << destination;
	}

	void load(SaveFile& file) {
		file >> region;
		file >> obj;
		file >> installed;
		file >> destination;
	}
};

class Fling : FTL {
	Military@ military;
	Designs@ designs;
	Construction@ construction;
	Development@ development;
	Systems@ systems;
	Budget@ budget;
	Fleets@ fleets;

	array<FlingRegion@> tracked;
	array<Object@> unused;

	BuildOrbital@ buildFling;
	double nextBuildTry = 15.0 * 60.0;
	bool wantToBuild = false;

	void create() override {
		@military = cast<Military>(ai.military);
		@designs = cast<Designs>(ai.designs);
		@construction = cast<Construction>(ai.construction);
		@development = cast<Development>(ai.development);
		@systems = cast<Systems>(ai.systems);
		@budget = cast<Budget>(ai.budget);
		@fleets = cast<Fleets>(ai.fleets);
	}

	void save(SaveFile& file) override {
		construction.saveConstruction(file, buildFling);
		file << nextBuildTry;
		file << wantToBuild;

		uint cnt = tracked.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << tracked[i];

		cnt = unused.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << unused[i];
	}

	void load(SaveFile& file) override {
		@buildFling = cast<BuildOrbital>(construction.loadConstruction(file));
		file >> nextBuildTry;
		file >> wantToBuild;

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			FlingRegion fr;
			file >> fr;
			tracked.insertLast(fr);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			Object@ obj;
			file >> obj;
			if(obj !is null)
				unused.insertLast(obj);
		}
	}

	uint order(MoveOrder& ord) override {
		if(!canFling(ord.obj))
			return F_Pass;

		//Find the position to fling to
		vec3d toPosition;
		if(!targetPosition(ord, toPosition))
			return F_Pass;

		//Don't fling if we're saving our FTL for a new beacon
		double avail = usableFTL(ai, ord);
		if((buildFling !is null && !buildFling.started) || wantToBuild)
			avail = min(avail, ai.empire.FTLStored - 250.0);

		//Make sure we have the ftl to fling
		if(flingCost(ord.obj, toPosition) > avail)
			return F_Pass;

		//Make sure we're in range of a beacon
		Object@ beacon = getClosest(ord.obj.position);
		if(beacon is null || beacon.position.distanceTo(ord.obj.position) > FLING_BEACON_RANGE)
			return F_Pass;

		ord.obj.addFlingOrder(beacon, toPosition);
		return F_Continue;
	}

	FlingRegion@ get(Region@ reg) {
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			if(tracked[i].region is reg)
				return tracked[i];
		}
		return null;
	}

	void remove(FlingRegion@ gt) {
		if(gt.obj !is null && gt.obj.valid && gt.obj.owner is ai.empire)
			unused.insertLast(gt.obj);
		tracked.remove(gt);
	}

	Object@ getClosest(const vec3d& position) {
		Object@ closest;
		double minDist = INFINITY;
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			Object@ obj = tracked[i].obj;
			if(obj is null)
				continue;
			double d = obj.position.distanceTo(position);
			if(d < minDist) {
				minDist = d;
				@closest = obj;
			}
		}
		for(uint i = 0, cnt = unused.length; i < cnt; ++i) {
			Object@ obj = unused[i];
			if(obj is null)
				continue;
			double d = obj.position.distanceTo(position);
			if(d < minDist) {
				minDist = d;
				@closest = obj;
			}
		}
		return closest;
	}

	FlingRegion@ getClosestRegion(const vec3d& position) {
		FlingRegion@ closest;
		double minDist = INFINITY;
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			double d = tracked[i].region.position.distanceTo(position);
			if(d < minDist) {
				minDist = d;
				@closest = tracked[i];
			}
		}
		return closest;
	}

	void assignTo(FlingRegion@ track, Object@ closest) {
		unused.remove(closest);
		@track.obj = closest;
	}

	bool trackingBeacon(Object@ obj) {
		for(uint i = 0, cnt = unused.length; i < cnt; ++i) {
			if(unused[i] is obj)
				return true;
		}
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			if(tracked[i].obj is obj)
				return true;
		}
		return false;
	}

	bool shouldHaveBeacon(Region@ reg, bool always = false) {
		if(military.getBase(reg) !is null)
			return true;
		if(development.isDevelopingIn(reg))
			return true;
		return false;
	}

	void focusTick(double time) override {
		//Manage unused beacons list
		for(uint i = 0, cnt = unused.length; i < cnt; ++i) {
			Object@ obj = unused[i];
			if(obj is null || !obj.valid || obj.owner !is ai.empire) {
				unused.removeAt(i);
				--i; --cnt;
			}
		}

		if(ai.behavior.forbidConstruction) return;

		//Detect new beacons
		auto@ data = ai.empire.getFlingBeacons();
		Object@ obj;
		while(receive(data, obj)) {
			if(obj is null)
				continue;
			if(!trackingBeacon(obj))
				unused.insertLast(obj);
		}

		//Update existing beacons for staging bases
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			auto@ reg = tracked[i];
			bool checkAlways = false;
			if(reg.obj !is null) {
				if(!reg.obj.valid || reg.obj.owner !is ai.empire || reg.obj.region !is reg.region) {
					@reg.obj = null;
					checkAlways = true;
				}
			}
			if(!shouldHaveBeacon(reg.region, checkAlways)) {
				remove(tracked[i]);
				--i; --cnt;
			}
		}

		//Detect new staging bases to build beacons at
		for(uint i = 0, cnt = military.stagingBases.length; i < cnt; ++i) {
			auto@ base = military.stagingBases[i];
			if(base.occupiedTime < FLING_MIN_TIMER)
				continue;

			if(get(base.region) is null) {
				FlingRegion@ closest = getClosestRegion(base.region.position);
				if(closest !is null && closest.region.position.distanceTo(base.region.position) < FLING_MIN_DISTANCE_STAGE)
					continue;

				FlingRegion gt;
				@gt.region = base.region;
				tracked.insertLast(gt);
				break;
			}
		}

		//Detect new important planets to build beacons at
		for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) {
			auto@ focus = development.focuses[i];
			Region@ reg = focus.obj.region;
			if(reg is null)
				continue;

			if(get(reg) is null) {
				FlingRegion@ closest = getClosestRegion(reg.position);
				if(closest !is null && closest.region.position.distanceTo(reg.position) < FLING_MIN_DISTANCE_DEVELOP)
					continue;

				FlingRegion gt;
				@gt.region = reg;
				tracked.insertLast(gt);
				break;
			}
		}

		//Destroy beacons if we're having ftl trouble
		if(ai.empire.FTLShortage && !ai.behavior.forbidScuttle) {
			Orbital@ leastImportant;
			double leastWeight = INFINITY;

			for(uint i = 0, cnt = unused.length; i < cnt; ++i) {
				Orbital@ obj = cast<Orbital>(unused[i]);
				if(obj is null || !obj.valid)
					continue;

				@leastImportant = obj;
				leastWeight = 0.0;
				break;
			}

			if(leastImportant !is null) {
				if(log)
					ai.print("Scuttle unused beacon for ftl", leastImportant.region);
				leastImportant.scuttle();
			}
			else {
				for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
					Orbital@ obj = cast<Orbital>(tracked[i].obj);
					if(obj is null || !obj.valid)
						continue;

					double weight = 1.0;
					auto@ base = military.getBase(tracked[i].region);
					if(base is null) {
						weight *= 5.0;
					}
					else if(base.idleTime >= 1) {
						weight *= 1.0 + (base.idleTime / 60.0);
					}
					else {
						weight /= 2.0;
					}

					if(weight < leastWeight) {
						@leastImportant = obj;
						leastWeight = weight;
					}
				}

				if(leastImportant !is null) {
					if(log)
						ai.print("Scuttle unimportant beacon for ftl", leastImportant.region);
					leastImportant.scuttle();
				}
			}
		}

		//See if we should build a new gate
		if(buildFling !is null) {
			if(buildFling.completed) {
				@buildFling = null;
				nextBuildTry = gameTime + 60.0;
			}
		}
		wantToBuild = false;
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			auto@ gt = tracked[i];
			if(gt.obj is null && gt.region.ContestedMask & ai.mask == 0 && gt.region.BlockFTLMask & ai.mask == 0) {
				Object@ found;
				for(uint n = 0, ncnt = unused.length; n < ncnt; ++n) {
					Object@ obj = unused[n];
					if(obj.region is gt.region) {
						@found = obj;
						break;
					}
				}

				if(found !is null) {
					if(log)
						ai.print("Assign beacon to => "+gt.region.name, found.region);
					assignTo(gt, found);
				} else if(buildFling is null && gameTime > nextBuildTry && !ai.empire.isFTLShortage(0.15)) {
					if(ai.empire.FTLStored >= 250) {
						if(log)
							ai.print("Build beacon for this system", gt.region);

						@buildFling = construction.buildOrbital(getOrbitalModule(flingModule), military.getStationPosition(gt.region));
					}
					else {
						wantToBuild = true;
					}
				}
			}
		}

		//Scuttle anything unused if we don't need beacons in those regions
		for(uint i = 0, cnt = unused.length; i < cnt; ++i) {
			if(get(unused[i].region) is null && unused[i].isOrbital) {
				cast<Orbital>(unused[i]).scuttle();
				unused.removeAt(i);
				--i; --cnt;
			}
		}

		//Try to get enough ftl storage that we can fling our largest fleet and have some remaining
		double highestCost = 0.0;
		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets.fleets[i];
			if(flAI.fleetClass != FC_Combat)
				continue;
			highestCost = max(highestCost, double(flingCost(flAI.obj, vec3d())));
		}
		development.aimFTLStorage = highestCost / (1.0 - ai.behavior.ftlReservePctCritical - ai.behavior.ftlReservePctNormal);
	}
};

AIComponent@ createFling() {
	return Fling();
}

Added scripts/server/empire_ai/weasel/ftl/Gate.as.




























































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.Movement;
import empire_ai.weasel.Military;
import empire_ai.weasel.Construction;
import empire_ai.weasel.Designs;
import empire_ai.weasel.Development;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Budget;

from statuses import getStatusID;
from abilities import getAbilityID;

const double GATE_MIN_DISTANCE_STAGE = 10000;
const double GATE_MIN_DISTANCE_DEVELOP = 20000;
const double GATE_MIN_DISTANCE_BORDER = 30000;
const double GATE_MIN_TIMER = 3.0 * 60.0;
const int GATE_BUILD_MOVE_HOPS = 5;

int packAbility = -1;
int unpackAbility = -1;

int packedStatus = -1;
int unpackedStatus = -1;

void init() {
	packAbility = getAbilityID("GatePack");
	unpackAbility = getAbilityID("GateUnpack");

	packedStatus = getStatusID("GatePacked");
	unpackedStatus = getStatusID("GateUnpacked");
}

class GateRegion : Savable {
	Region@ region;
	Object@ gate;
	bool installed = false;
	vec3d destination;

	void save(SaveFile& file) {
		file << region;
		file << gate;
		file << installed;
		file << destination;
	}

	void load(SaveFile& file) {
		file >> region;
		file >> gate;
		file >> installed;
		file >> destination;
	}
};

class Gate : FTL {
	Military@ military;
	Designs@ designs;
	Construction@ construction;
	Development@ development;
	Systems@ systems;
	Budget@ budget;

	DesignTarget@ gateDesign;

	array<GateRegion@> tracked;
	array<Object@> unassigned;

	BuildStation@ buildGate;
	double nextBuildTry = 15.0 * 60.0;

	void create() override {
		@military = cast<Military>(ai.military);
		@designs = cast<Designs>(ai.designs);
		@construction = cast<Construction>(ai.construction);
		@development = cast<Development>(ai.development);
		@systems = cast<Systems>(ai.systems);
		@budget = cast<Budget>(ai.budget);
	}

	void save(SaveFile& file) override {
		designs.saveDesign(file, gateDesign);
		construction.saveConstruction(file, buildGate);
		file << nextBuildTry;

		uint cnt = tracked.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << tracked[i];

		cnt = unassigned.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << unassigned[i];
	}

	void load(SaveFile& file) override {
		@gateDesign = designs.loadDesign(file);
		@buildGate = cast<BuildStation>(construction.loadConstruction(file));
		file >> nextBuildTry;

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			GateRegion gt;
			file >> gt;
			tracked.insertLast(gt);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			Object@ obj;
			file >> obj;
			if(obj !is null)
				unassigned.insertLast(obj);
		}
	}

	GateRegion@ get(Region@ reg) {
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			if(tracked[i].region is reg)
				return tracked[i];
		}
		return null;
	}

	void remove(GateRegion@ gt) {
		if(gt.gate !is null && gt.gate.valid && gt.gate.owner is ai.empire)
			unassigned.insertLast(gt.gate);
		tracked.remove(gt);
	}

	Object@ getClosestGate(const vec3d& position) {
		Object@ closest;
		double minDist = INFINITY;
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			Object@ gate = tracked[i].gate;
			if(gate is null)
				continue;
			if(!tracked[i].installed)
				continue;
			double d = gate.position.distanceTo(position);
			if(d < minDist) {
				minDist = d;
				@closest = gate;
			}
		}
		return closest;
	}

	GateRegion@ getClosestGateRegion(const vec3d& position) {
		GateRegion@ closest;
		double minDist = INFINITY;
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			double d = tracked[i].region.position.distanceTo(position);
			if(d < minDist) {
				minDist = d;
				@closest = tracked[i];
			}
		}
		return closest;
	}

	void assignTo(GateRegion@ gt, Object@ closest) {
		unassigned.remove(closest);
		@gt.gate = closest;
		gt.installed = false;

		if(closest.region is gt.region) {
			if(closest.hasStatusEffect(unpackedStatus)) {
				gt.installed = true;
			}
		}

		if(!gt.installed) {
			gt.destination = military.getStationPosition(gt.region);
			closest.activateAbilityTypeFor(ai.empire, packAbility);
			closest.addMoveOrder(gt.destination);
		}
	}

	bool trackingGate(Object@ obj) {
		for(uint i = 0, cnt = unassigned.length; i < cnt; ++i) {
			if(unassigned[i] is obj)
				return true;
		}
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			if(tracked[i].gate is obj)
				return true;
		}
		return false;
	}

	bool shouldHaveGate(Region@ reg, bool always = false) {
		if(military.getBase(reg) !is null)
			return true;
		if(development.isDevelopingIn(reg))
			return true;
		if(!always) {
			auto@ sys = systems.getAI(reg);
			if(sys !is null) {
				if(sys.border && sys.bordersEmpires)
					return true;
			}
		}
		return false;
	}

	void turn() override {
		if(gateDesign !is null && gateDesign.active !is null) {
			int newSize = round(double(budget.spendable(BT_Military)) * 0.5 * ai.behavior.shipSizePerMoney / 64.0) * 64;
			if(newSize < 128)
				newSize = 128;
			if(newSize != gateDesign.targetSize) {
				@gateDesign = designs.design(DP_Gate, newSize);
				gateDesign.customName = "Gate";
			}
		}
	}

	void focusTick(double time) override {
		if(ai.behavior.forbidConstruction) return;

		//Design a gate
		if(gateDesign is null) {
			@gateDesign = designs.design(DP_Gate, 128);
			gateDesign.customName = "Gate";
		}

		//Manage unassigned gates list
		for(uint i = 0, cnt = unassigned.length; i < cnt; ++i) {
			Object@ obj = unassigned[i];
			if(obj is null || !obj.valid || obj.owner !is ai.empire) {
				unassigned.removeAt(i);
				--i; --cnt;
			}
		}

		//Detect new gates
		auto@ data = ai.empire.getStargates();
		Object@ obj;
		while(receive(data, obj)) {
			if(obj is null)
				continue;
			if(!trackingGate(obj))
				unassigned.insertLast(obj);
		}

		//Update existing gates for staging bases
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			auto@ gt = tracked[i];
			bool checkAlways = false;
			if(gt.gate !is null) {
				if(!gt.gate.valid || gt.gate.owner !is ai.empire || (gt.installed && gt.gate.region !is gt.region)) {
					@gt.gate = null;
					gt.installed = false;
					checkAlways = true;
				}
				else if(!gt.installed && !gt.gate.hasOrders) {
					if(gt.destination.distanceTo(gt.gate.position) < 10.0) {
						gt.gate.activateAbilityTypeFor(ai.empire, unpackAbility, gt.destination);
						gt.installed = true;
					}
					else {
						gt.gate.activateAbilityTypeFor(ai.empire, packAbility);
						gt.gate.addMoveOrder(gt.destination);
					}
				}
			}
			if(!shouldHaveGate(gt.region, checkAlways)) {
				remove(tracked[i]);
				--i; --cnt;
			}
		}

		//Detect new staging bases to build gates at
		for(uint i = 0, cnt = military.stagingBases.length; i < cnt; ++i) {
			auto@ base = military.stagingBases[i];
			if(base.occupiedTime < GATE_MIN_TIMER)
				continue;

			if(get(base.region) is null) {
				GateRegion@ closest = getClosestGateRegion(base.region.position);
				if(closest !is null && closest.region.position.distanceTo(base.region.position) < GATE_MIN_DISTANCE_STAGE)
					continue;

				GateRegion gt;
				@gt.region = base.region;
				tracked.insertLast(gt);
				break;
			}
		}

		//Detect new important planets to build gates at
		for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) {
			auto@ focus = development.focuses[i];
			Region@ reg = focus.obj.region;
			if(reg is null)
				continue;

			if(get(reg) is null) {
				GateRegion@ closest = getClosestGateRegion(reg.position);
				if(closest !is null && closest.region.position.distanceTo(reg.position) < GATE_MIN_DISTANCE_DEVELOP)
					continue;

				GateRegion gt;
				@gt.region = reg;
				tracked.insertLast(gt);
				break;
			}
		}

		//Detect new border systems to build gates at
		uint offset = randomi(0, systems.border.length-1);
		for(uint i = 0, cnt = systems.border.length; i < cnt; ++i) {
			auto@ sys = systems.border[(i+offset)%cnt];
			Region@ reg = sys.obj;
			if(reg is null)
				continue;
			if(!sys.bordersEmpires)
				continue;

			if(get(reg) is null) {
				GateRegion@ closest = getClosestGateRegion(reg.position);
				if(closest !is null && closest.region.position.distanceTo(reg.position) < GATE_MIN_DISTANCE_DEVELOP)
					continue;

				GateRegion gt;
				@gt.region = reg;
				tracked.insertLast(gt);
				break;
			}
		}

		//Destroy gates if we're having ftl trouble
		if(ai.empire.FTLShortage && !ai.behavior.forbidScuttle) {
			Ship@ leastImportant;
			double leastWeight = INFINITY;

			for(uint i = 0, cnt = unassigned.length; i < cnt; ++i) {
				Ship@ ship = cast<Ship>(unassigned[i]);
				if(ship is null || !ship.valid)
					continue;

				double weight = ship.blueprint.design.size;
				weight *= 10.0;

				if(weight < leastWeight) {
					@leastImportant = ship;
					leastWeight = weight;
				}
			}

			if(leastImportant !is null) {
				if(log)
					ai.print("Scuttle unassigned gate for ftl", leastImportant.region);
				leastImportant.scuttle();
			}
			else {
				for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
					Ship@ ship = cast<Ship>(tracked[i].gate);
					if(ship is null || !ship.valid)
						continue;

					double weight = ship.blueprint.design.size;
					auto@ base = military.getBase(tracked[i].region);
					if(base is null) {
						weight *= 5.0;
					}
					else if(base.idleTime >= 1) {
						weight *= 1.0 + (base.idleTime / 60.0);
					}
					else {
						weight /= 2.0;
					}

					if(weight < leastWeight) {
						@leastImportant = ship;
						leastWeight = weight;
					}
				}

				if(leastImportant !is null) {
					if(log)
						ai.print("Scuttle unimportant gate for ftl", leastImportant.region);
					leastImportant.scuttle();
				}
			}
		}

		//See if we should build a new gate
		if(buildGate !is null) {
			if(buildGate.completed) {
				@buildGate = null;
				nextBuildTry = gameTime + 60.0;
			}
		}
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			auto@ gt = tracked[i];
			if(gt.gate is null && gt.region.ContestedMask & ai.mask == 0 && gt.region.BlockFTLMask & ai.mask == 0) {
				Object@ closest;
				double closestDist = INFINITY;
				for(uint n = 0, ncnt = unassigned.length; n < ncnt; ++n) {
					Object@ obj = unassigned[n];
					if(obj.region is gt.region) {
						@closest = obj;
						break;
					}
					if(!obj.hasMover)
						continue;
					if(buildGate is null && gameTime > nextBuildTry) {
						double d = obj.position.distanceTo(gt.region.position);
						if(d < closestDist) {
							closestDist = d;
							@closest = obj;
						}
					}
				}

				if(closest !is null) {
					if(log)
						ai.print("Assign gate to => "+gt.region.name, closest.region);
					assignTo(gt, closest);
				} else if(buildGate is null && gameTime > nextBuildTry && !ai.empire.isFTLShortage(0.15)) {
					if(log)
						ai.print("Build gate for this system", gt.region);

					bool buildLocal = true;
					auto@ factory = construction.primaryFactory;
					if(factory !is null) {
						Region@ factRegion = factory.obj.region;
						if(factRegion !is null && systems.hopDistance(gt.region, factRegion) < GATE_BUILD_MOVE_HOPS)
							buildLocal = false;
					}

					if(buildLocal)
						@buildGate = construction.buildLocalStation(gateDesign);
					else
						@buildGate = construction.buildStation(gateDesign, military.getStationPosition(gt.region));
				}
			}
		}
	}
};

AIComponent@ createGate() {
	return Gate();
}

Added scripts/server/empire_ai/weasel/ftl/Hyperdrive.as.






































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.Movement;
import empire_ai.weasel.Development;
import empire_ai.weasel.Fleets;

import ftl;

from orders import OrderType;

const double REJUMP_MIN_DIST = 8000.0;
const double STORAGE_AIM_DISTANCE = 40000;

class Hyperdrive : FTL {
	Development@ development;
	Fleets@ fleets;

	void create() override {
		@development = cast<Development>(ai.development);
		@fleets = cast<Fleets>(ai.fleets);
	}

	double hdETA(Object& obj, const vec3d& position) {
		double charge = HYPERDRIVE_CHARGE_TIME;
		if(obj.owner.HyperdriveNeedCharge == 0)
			charge = 0.0;
		double dist = position.distanceTo(obj.position);
		double speed = hyperdriveMaxSpeed(obj);
		return charge + dist / speed;
	}

	double subETA(Object& obj, const vec3d& position) {
		return newtonArrivalTime(obj.maxAcceleration, position - obj.position, vec3d());
	}
	
	bool shouldHD(Object& obj, const vec3d& position, uint priority) {
		//This makes me sad
		if(position.distanceTo(obj.position) < 3000)
			return false;
		double pathDist = cast<Movement>(ai.movement).getPathDistance(obj.position, position, obj.maxAcceleration);
		double straightDist = position.distanceTo(obj.position);
		return pathDist >= straightDist * 0.6;
	}

	uint order(MoveOrder& ord) override {
		if(!canHyperdrive(ord.obj))
			return F_Pass;

		double avail = usableFTL(ai, ord);
		if(avail > 0) {
			vec3d toPosition;
			if(targetPosition(ord, toPosition)) {
				if(shouldHD(ord.obj, toPosition, ord.priority)) {
					double cost = hyperdriveCost(ord.obj, toPosition);
					if(avail >= cost) {
						ord.obj.addHyperdriveOrder(toPosition);
						return F_Continue;
					}
				}
			}
		}

		return F_Pass;
	}

	uint tick(MoveOrder& ord, double time) {
		if(ord.priority == MP_Critical && canHyperdrive(ord.obj) && ord.obj.firstOrderType != OT_Hyperdrive) {
			vec3d toPosition;
			if(targetPosition(ord, toPosition)) {
				double dist = ord.obj.position.distanceToSQ(toPosition);
				if(dist > REJUMP_MIN_DIST * REJUMP_MIN_DIST) {
					double avail = usableFTL(ai, ord);
					double cost = hyperdriveCost(ord.obj, toPosition);
					if(avail >= cost && shouldHD(ord.obj, toPosition, ord.priority)) {
						cast<Movement>(ai.movement).order(ord);
						return F_Continue;
					}
				}
			}
		}
		return F_Pass;
	}

	void focusTick(double time) override {
		//Try to get enough ftl storage that we can ftl our largest fleet a fair distance and have some remaining
		double highestCost = 0.0;
		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets.fleets[i];
			if(flAI.fleetClass != FC_Combat)
				continue;
			vec3d toPosition = flAI.obj.position + vec3d(0, 0, STORAGE_AIM_DISTANCE);
			highestCost = max(highestCost, double(hyperdriveCost(flAI.obj, toPosition)));
		}
		development.aimFTLStorage = highestCost / (1.0 - ai.behavior.ftlReservePctCritical - ai.behavior.ftlReservePctNormal);
	}
};

AIComponent@ createHyperdrive() {
	return Hyperdrive();
}

Added scripts/server/empire_ai/weasel/ftl/Jumpdrive.as.
































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.Movement;
import empire_ai.weasel.Development;
import empire_ai.weasel.Fleets;

import ftl;
import system_flags;
import regions.regions;
import systems;

from orders import OrderType;

const double REJUMP_MIN_DIST = 8000.0;

class Jumpdrive : FTL {
	Development@ development;
	Fleets@ fleets;

	int safetyFlag = -1;
	array<Region@> safeRegions;

	void create() override {
		@development = cast<Development>(ai.development);
		@fleets = cast<Fleets>(ai.fleets);

		safetyFlag = getSystemFlag("JumpdriveSafety");
	}

	void save(SaveFile& file) {
		uint cnt = safeRegions.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << safeRegions[i];
	}

	void load(SaveFile& file) {
		uint cnt = 0;
		file >> cnt;
		safeRegions.length = cnt;
		for(uint i = 0; i < cnt; ++i)
			file >> safeRegions[i];
	}

	double jdETA(Object& obj, const vec3d& position) {
		double charge = JUMPDRIVE_CHARGE_TIME;
		return charge;
	}

	double subETA(Object& obj, const vec3d& position) {
		return newtonArrivalTime(obj.maxAcceleration, position - obj.position, vec3d());
	}
	
	bool shouldJD(Object& obj, const vec3d& position, uint priority) {
		//This makes me sad
		if(position.distanceTo(obj.position) < 3000)
			return false;
		return true;

		/*double factor = 0.8;*/
		/*if(priority == MP_Critical)*/
		/*	factor = 1.0;*/
		/*return jdETA(obj, position) <= factor * subETA(obj, position);*/
	}

	uint order(MoveOrder& ord) override {
		return order(ord, ord.obj.position, false);
	}

	uint order(MoveOrder& ord, const vec3d& fromPos, bool secondary) {
		if(!canJumpdrive(ord.obj))
			return F_Pass;

		double avail = usableFTL(ai, ord);
		if(avail > 0) {
			vec3d toPosition;
			if(targetPosition(ord, toPosition)) {
				double maxRange = jumpdriveRange(ord.obj);
				double dist = toPosition.distanceTo(fromPos);

				bool isSafe = false;
				Region@ reg = getRegion(toPosition);
				if(reg !is null)
					isSafe = reg.getSystemFlag(ai.empire, safetyFlag);

				if(dist > maxRange && !isSafe) {
					//See if we should jump to a safe region first
					if(!secondary) {
						double bestHop = INFINITY;
						Region@ hopRegion;
						vec3d bestPos;
						for(uint i = 0, cnt = safeRegions.length; i < cnt; ++i) {
							if(!safeRegions[i].getSystemFlag(ai.empire, safetyFlag))
								continue;
							vec3d hopPos = safeRegions[i].position;
							hopPos = hopPos + (fromPos-hopPos).normalized(safeRegions[i].radius * 0.85);
							double d = hopPos.distanceTo(toPosition);
							if(d < bestHop) {
								bestHop = d;
								@hopRegion = safeRegions[i];
								bestPos = hopPos;
							}
						}

						if(bestHop < dist * 0.8) {
							double cost = jumpdriveCost(ord.obj, fromPos, bestPos);
							if(avail >= cost) {
								ord.obj.addJumpdriveOrder(bestPos);
								order(ord, bestPos, true);
								return F_Continue;
							}
						}
					}

					//Shorten our jump
					if(ord.priority < MP_Normal)
						return F_Pass;
					toPosition = fromPos + (toPosition - fromPos).normalized(maxRange);
				}

				if(shouldJD(ord.obj, toPosition, ord.priority)) {
					double cost = jumpdriveCost(ord.obj, toPosition);
					if(avail >= cost) {
						ord.obj.addJumpdriveOrder(toPosition, append=secondary);
						return F_Continue;
					}
				}
			}
		}

		return F_Pass;
	}

	uint tick(MoveOrder& ord, double time) {
		if(ord.priority == MP_Critical && canJumpdrive(ord.obj) && ord.obj.firstOrderType != OT_Jumpdrive) {
			vec3d toPosition;
			if(targetPosition(ord, toPosition)) {
				double dist = ord.obj.position.distanceToSQ(toPosition);
				if(dist > REJUMP_MIN_DIST * REJUMP_MIN_DIST) {
					double maxRange = jumpdriveRange(ord.obj);
					dist = sqrt(dist);

					bool isSafe = false;
					Region@ reg = getRegion(toPosition);
					if(reg !is null)
						isSafe = reg.getSystemFlag(ai.empire, safetyFlag);

					if(dist > maxRange && !isSafe)
						toPosition = ord.obj.position + (toPosition - ord.obj.position).normalized(maxRange);

					if(shouldJD(ord.obj, toPosition, ord.priority)) {
						double avail = usableFTL(ai, ord);
						double cost = jumpdriveCost(ord.obj, toPosition);
						if(avail >= cost) {
							cast<Movement>(ai.movement).order(ord);
							return F_Continue;
						}
					}
				}
			}
		}
		return F_Pass;
	}

	uint sysChk = 0;
	void start() {
		for(uint i = 0, cnt = systemCount; i < cnt; ++i) {
			Region@ reg = getSystem(i).object;
			if(reg.getSystemFlag(ai.empire, safetyFlag))
				safeRegions.insertLast(reg);
		}
	}

	void focusTick(double time) override {
		//Try to get enough ftl storage that we can ftl our largest fleet a fair distance and have some remaining
		double highestCost = 0.0;
		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets.fleets[i];
			if(flAI.fleetClass != FC_Combat)
				continue;
			double dist = jumpdriveRange(flAI.obj);
			vec3d toPosition = flAI.obj.position + vec3d(0, 0, dist);
			highestCost = max(highestCost, double(jumpdriveCost(flAI.obj, toPosition)));
		}
		development.aimFTLStorage = highestCost / (1.0 - ai.behavior.ftlReservePctCritical - ai.behavior.ftlReservePctNormal);

		//Disable systems that are no longer safe
		for(uint i = 0, cnt = safeRegions.length; i < cnt; ++i) {
			if(!safeRegions[i].getSystemFlag(ai.empire, safetyFlag)) {
				safeRegions.removeAt(i);
				--i; --cnt;
			}
		}

		//Try to find regions that are safe for us
		{
			sysChk = (sysChk+1) % systemCount;
			auto@ reg = getSystem(sysChk).object;
			if(reg.getSystemFlag(ai.empire, safetyFlag)) {
				if(safeRegions.find(reg) == -1)
					safeRegions.insertLast(reg);
			}
		}
	}
};

AIComponent@ createJumpdrive() {
	return Jumpdrive();
}

Added scripts/server/empire_ai/weasel/ftl/Slipstream.as.
































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.Movement;
import empire_ai.weasel.Military;
import empire_ai.weasel.Construction;
import empire_ai.weasel.Designs;
import empire_ai.weasel.Development;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Budget;
import empire_ai.weasel.Fleets;

from statuses import getStatusID;
from abilities import getAbilityID;

from oddity_navigation import hasOddityLink;

const double SS_MIN_DISTANCE_STAGE = 0;
const double SS_MIN_DISTANCE_DEVELOP = 10000;
const double SS_MIN_TIMER = 3.0 * 60.0;
const double SS_MAX_DISTANCE = 3000.0;

class SSRegion : Savable {
	Region@ region;
	Object@ obj;
	bool arrived = false;
	vec3d destination;

	void save(SaveFile& file) {
		file << region;
		file << obj;
		file << arrived;
		file << destination;
	}

	void load(SaveFile& file) {
		file >> region;
		file >> obj;
		file >> arrived;
		file >> destination;
	}
};

class Slipstream : FTL {
	Military@ military;
	Designs@ designs;
	Construction@ construction;
	Development@ development;
	Systems@ systems;
	Budget@ budget;
	Fleets@ fleets;

	DesignTarget@ ssDesign;

	array<SSRegion@> tracked;
	array<Object@> unassigned;

	BuildFlagship@ buildSS;
	double nextBuildTry = 15.0 * 60.0;

	void create() override {
		@military = cast<Military>(ai.military);
		@designs = cast<Designs>(ai.designs);
		@construction = cast<Construction>(ai.construction);
		@development = cast<Development>(ai.development);
		@systems = cast<Systems>(ai.systems);
		@budget = cast<Budget>(ai.budget);
		@fleets = cast<Fleets>(ai.fleets);
	}

	void save(SaveFile& file) override {
		designs.saveDesign(file, ssDesign);
		construction.saveConstruction(file, buildSS);
		file << nextBuildTry;

		uint cnt = tracked.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << tracked[i];

		cnt = unassigned.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << unassigned[i];
	}

	void load(SaveFile& file) override {
		@ssDesign = designs.loadDesign(file);
		@buildSS = cast<BuildFlagship>(construction.loadConstruction(file));
		file >> nextBuildTry;

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			SSRegion gt;
			file >> gt;
			tracked.insertLast(gt);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			Object@ obj;
			file >> obj;
			if(obj !is null)
				unassigned.insertLast(obj);
		}
	}

	uint order(MoveOrder& ord) override {
		//Find the position to fling to
		vec3d toPosition;
		if(!targetPosition(ord, toPosition))
			return F_Pass;

		//Check if we have a slipstream generator in this region
		auto@ gt = get(ord.obj.region);
		if(gt is null || gt.obj is null || !gt.arrived)
			return F_Pass;

		//Make sure our generator is usable
		Object@ ssGen = gt.obj;
		if(!canSlipstream(ssGen))
			return F_Pass;

		//Check if we already have a link
		if(hasOddityLink(gt.region, toPosition, SS_MAX_DISTANCE, minDuration=60.0))
			return F_Pass;

		//See if we have the FTL to make a link
		double avail = usableFTL(ai, ord);
		if(!canSlipstreamTo(ssGen, toPosition))
			return F_Pass;
		if(slipstreamCost(ssGen, 0, toPosition.distanceTo(ssGen.position)) >= avail)
			return F_Pass;

		ssGen.addSlipstreamOrder(toPosition, append=true);
		if(ssGen !is ord.obj) {
			ord.obj.addWaitOrder(ssGen, moveTo=true);
			ssGen.addSecondaryToSlipstream(ord.obj);
		}
		else {
			ord.obj.addMoveOrder(toPosition, append=true);
		}

		return F_Continue;
	}

	SSRegion@ get(Region@ reg) {
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			if(tracked[i].region is reg)
				return tracked[i];
		}
		return null;
	}

	void remove(SSRegion@ gt) {
		if(gt.obj !is null && gt.obj.valid && gt.obj.owner is ai.empire)
			unassigned.insertLast(gt.obj);
		tracked.remove(gt);
	}

	Object@ getClosest(const vec3d& position) {
		Object@ closest;
		double minDist = INFINITY;
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			Object@ obj = tracked[i].obj;
			if(obj is null)
				continue;
			if(!tracked[i].arrived)
				continue;
			double d = obj.position.distanceTo(position);
			if(d < minDist) {
				minDist = d;
				@closest = obj;
			}
		}
		return closest;
	}

	SSRegion@ getClosestRegion(const vec3d& position) {
		SSRegion@ closest;
		double minDist = INFINITY;
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			double d = tracked[i].region.position.distanceTo(position);
			if(d < minDist) {
				minDist = d;
				@closest = tracked[i];
			}
		}
		return closest;
	}

	void assignTo(SSRegion@ gt, Object@ closest) {
		unassigned.remove(closest);
		@gt.obj = closest;
		gt.arrived = false;
		military.stationFleet(fleets.getAI(closest), gt.region);

		if(closest.region is gt.region)
			gt.arrived = true;

		if(!gt.arrived) {
			gt.destination = military.getStationPosition(gt.region);
			closest.addMoveOrder(gt.destination);
		}
	}

	bool trackingGen(Object@ obj) {
		for(uint i = 0, cnt = unassigned.length; i < cnt; ++i) {
			if(unassigned[i] is obj)
				return true;
		}
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			if(tracked[i].obj is obj)
				return true;
		}
		return false;
	}

	bool shouldHaveGen(Region@ reg, bool always = false) {
		if(military.getBase(reg) !is null)
			return true;
		if(development.isDevelopingIn(reg))
			return true;
		return false;
	}

	void turn() override {
		if(ssDesign !is null && ssDesign.active !is null) {
			int newSize = round(double(budget.spendable(BT_Military)) * 0.2 * ai.behavior.shipSizePerMoney / 64.0) * 64;
			if(newSize < 128)
				newSize = 128;
			if(newSize != ssDesign.targetSize) {
				@ssDesign = designs.design(DP_Slipstream, newSize);
				ssDesign.customName = "Slipstream";
			}
		}
	}

	void focusTick(double time) override {
		if(ai.behavior.forbidConstruction) return;

		//Design a generator
		if(ssDesign is null) {
			@ssDesign = designs.design(DP_Slipstream, 128);
			ssDesign.customName = "Slipstream";
		}

		//Manage unassigned gens list
		for(uint i = 0, cnt = unassigned.length; i < cnt; ++i) {
			Object@ obj = unassigned[i];
			if(obj is null || !obj.valid || obj.owner !is ai.empire) {
				unassigned.removeAt(i);
				--i; --cnt;
			}
		}

		//Detect new gens
		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets.fleets[i];
			if(flAI.fleetClass != FC_Slipstream)
				continue;
			if(!trackingGen(flAI.obj))
				unassigned.insertLast(flAI.obj);
		}

		//Update existing gens for staging bases
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			auto@ gt = tracked[i];
			bool checkAlways = false;
			if(gt.obj !is null) {
				if(!gt.obj.valid || gt.obj.owner !is ai.empire || (gt.arrived && gt.obj.region !is gt.region)) {
					@gt.obj = null;
					gt.arrived = false;
					checkAlways = true;
				}
				else if(!gt.arrived && !gt.obj.hasOrders) {
					if(gt.destination.distanceTo(gt.obj.position) < 10.0)
						gt.arrived = true;
					else
						assignTo(gt, gt.obj);
				}
			}
			if(!shouldHaveGen(gt.region, checkAlways)) {
				remove(tracked[i]);
				--i; --cnt;
			}
		}

		//Detect new staging bases to build gens at
		for(uint i = 0, cnt = military.stagingBases.length; i < cnt; ++i) {
			auto@ base = military.stagingBases[i];
			if(base.occupiedTime < SS_MIN_TIMER)
				continue;

			if(get(base.region) is null) {
				SSRegion@ closest = getClosestRegion(base.region.position);
				if(closest !is null && closest.region.position.distanceTo(base.region.position) < SS_MIN_DISTANCE_STAGE)
					continue;

				SSRegion gt;
				@gt.region = base.region;
				tracked.insertLast(gt);
				break;
			}
		}

		//Detect new important planets to build generator at
		for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) {
			auto@ focus = development.focuses[i];
			Region@ reg = focus.obj.region;
			if(reg is null)
				continue;

			if(get(reg) is null) {
				SSRegion@ closest = getClosestRegion(reg.position);
				if(closest !is null && closest.region.position.distanceTo(reg.position) < SS_MIN_DISTANCE_DEVELOP)
					continue;

				SSRegion gt;
				@gt.region = reg;
				tracked.insertLast(gt);
				break;
			}
		}

		//See if we should build a new generator
		if(buildSS !is null) {
			if(buildSS.completed) {
				@buildSS = null;
				nextBuildTry = gameTime + 60.0;
			}
		}
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			auto@ gt = tracked[i];
			if(gt.obj is null && gt.region.ContestedMask & ai.mask == 0 && gt.region.BlockFTLMask & ai.mask == 0) {
				Object@ closest;
				double closestDist = INFINITY;
				for(uint n = 0, ncnt = unassigned.length; n < ncnt; ++n) {
					Object@ obj = unassigned[n];
					if(obj.region is gt.region) {
						@closest = obj;
						break;
					}
					if(!obj.hasMover)
						continue;
					if(buildSS is null && gameTime > nextBuildTry) {
						double d = obj.position.distanceTo(gt.region.position);
						if(d < closestDist) {
							closestDist = d;
							@closest = obj;
						}
					}
				}

				if(closest !is null) {
					if(log)
						ai.print("Assign slipstream gen to => "+gt.region.name, closest.region);
					assignTo(gt, closest);
				} else if(buildSS is null && gameTime > nextBuildTry) {
					if(log)
						ai.print("Build slipstream gen for this system", gt.region);

					@buildSS = construction.buildFlagship(ssDesign);
				}
			}
		}

		//Try to get enough ftl storage that we can permanently open a slipstream with each of generators
		double mostCost = 0.0;
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			Ship@ obj = cast<Ship>(tracked[i].obj);
			if(obj is null)
				continue;

			double baseCost = obj.blueprint.design.average(SV_SlipstreamCost);
			double duration = obj.blueprint.design.average(SV_SlipstreamDuration);
			mostCost += baseCost / duration;
		}
		development.aimFTLStorage = mostCost;
	}
};

AIComponent@ createSlipstream() {
	return Slipstream();
}

Added scripts/server/empire_ai/weasel/misc/Invasion.as.


























































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;

import empire_ai.weasel.Fleets;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Movement;
import empire_ai.weasel.searches;

import systems;
from empire import Pirates;

class InvasionDefendMission : Mission {
	FleetAI@ fleet;
	Region@ targRegion;
	MoveOrder@ move;
	bool pending = false;

	Object@ eliminate;

	void save(Fleets& fleets, SaveFile& file) override {
		fleets.saveAI(file, fleet);
		file << targRegion;
		fleets.movement.saveMoveOrder(file, move);
		file << pending;
	}

	void load(Fleets& fleets, SaveFile& file) override {
		@fleet = fleets.loadAI(file);
		file >> targRegion;
		@move = fleets.movement.loadMoveOrder(file);
		file >> pending;
	}

	bool get_isActive() override {
		return targRegion !is null;
	}

	void tick(AI& ai, FleetAI& fleet, double time) override {
		if(targRegion is null)
			return;
		if(move !is null)
			return;

		//Find stuff to fight
		if(eliminate is null)
			@eliminate = findEnemy(targRegion, null, ai.empire.hostileMask);

		if(eliminate !is null) {
			if(!eliminate.valid) {
				@eliminate = null;
			}
			else {
				if(!fleet.obj.hasOrders)
					fleet.obj.addAttackOrder(eliminate);
				if((fleet.filled < 0.3 || fleet.supplies < 0.3 || fleet.flagshipHealth < 0.5)
					&& eliminate.getFleetStrength() * 2.0 > fleet.strength
					&& !pending) {
					@targRegion = null;
					@eliminate = null;
					@move = cast<Fleets>(ai.fleets).returnToBase(fleet, MP_Critical);
				}
			}
		}
	}

	void update(AI& ai, Invasion& invasion) {
		//Manage movement
		if(move !is null) {
			if(move.failed || move.completed)
				@move = null;
		}

		//Find new regions to go to
		if(targRegion is null || (!pending && move is null && !invasion.isFighting(targRegion))) {
			bool ready = fleet.actionableState && move is null;

			DefendSystem@ bestDef;
			double bestWeight = 0.0;

			for(uint i = 0, cnt = invasion.defending.length; i < cnt; ++i) {
				auto@ def = invasion.defending[i];
				double w = randomd(0.9, 1.1);
				if(!def.fighting) {
					if(!ready)
						continue;
					else
						w *= 0.1;
				}

				if(!def.winning) {
					w *= 10.0;
				}
				else {
					if(!ready)
						continue;
				}

				if(def.obj is targRegion)
					w *= 1.5;

				if(w > bestWeight) {
					bestWeight = w;
					@bestDef = def;
				}
			}

			if(bestDef !is null && fleet.supplies >= 0.25 && fleet.filled >= 0.2 && fleet.fleetHealth >= 0.2) {
				@targRegion = bestDef.obj;
				invasion.pend(targRegion, fleet);
				pending = true;
			}
		}

		//Move to the region we want to go to
		if(targRegion !is null) {
			if(move is null) {
				if(fleet.obj.region !is targRegion) {
					@eliminate = findEnemy(targRegion, null, ai.empire.hostileMask);
					if(eliminate is null) {
						vec3d targPos = targRegion.position;
						targPos += (targRegion.position - ai.empire.HomeSystem.position).normalized(targRegion.radius * 0.85);

						@move = invasion.movement.move(fleet.obj, targPos, MP_Critical);
					}
					else {
						@move = invasion.movement.move(fleet.obj, eliminate, MP_Critical, nearOnly=true);
					}
				}
				else {
					//Remove from pending list
					if(pending) {
						invasion.unpend(targRegion, fleet);
						pending = false;
					}

					//See if we should return to base
					if(!invasion.isFighting(targRegion) && (fleet.supplies < 0.25 || fleet.filled < 0.5)) {
						@targRegion = null;
						@move = invasion.fleets.returnToBase(fleet, MP_Critical);
					}
				}
			}
		}
	}
};

class DefendSystem {
	Region@ obj;
	array<FleetAI@> pending;

	double enemyStrength = 0.0;
	double ourStrength = 0.0;
	double remnantStrength = 0.0;
	double pendingStrength = 0.0;

	void save(Invasion& invasion, SaveFile& file) {
		file << obj;

		uint cnt = pending.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			invasion.fleets.saveAI(file, pending[i]);

		file << enemyStrength;
		file << ourStrength;
		file << remnantStrength;
		file << pendingStrength;
	}

	void load(Invasion& invasion, SaveFile& file) {
		file >> obj;

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ fleet = invasion.fleets.loadAI(file);
			if(fleet !is null && fleet.obj !is null)
				pending.insertLast(fleet);
		}

		file >> enemyStrength;
		file >> ourStrength;
		file >> remnantStrength;
		file >> pendingStrength;
	}

	void update(AI& ai, Invasion& invasion) {
		enemyStrength = getTotalFleetStrength(obj, ai.empire.hostileMask);

		ourStrength = getTotalFleetStrength(obj, ai.mask);
		remnantStrength = getTotalFleetStrength(obj, Pirates.mask);
		if(gameTime < 10.0 * 60.0)
			ourStrength += remnantStrength;
		else if(gameTime < 30.0 * 60.0)
			ourStrength += remnantStrength * 0.5;

		pendingStrength = 0.0;
		for(uint i = 0, cnt = pending.length; i < cnt; ++i)
			pendingStrength += sqrt(pending[i].strength);
		pendingStrength *= pendingStrength;

		if(obj.PlanetsMask & ai.empire.mask != 0)
			ai.empire.setDefending(obj, true);
	}

	bool get_fighting() {
		return enemyStrength > 0;
	}

	bool get_winning() {
		return ourStrength + pendingStrength > enemyStrength;
	}
};

class Invasion : AIComponent {
	Fleets@ fleets;
	Movement@ movement;

	array<DefendSystem@> defending;
	array<InvasionDefendMission@> tracked;

	void create() {
		@fleets = cast<Fleets>(ai.fleets);
		@movement = cast<Movement>(ai.movement);

		ai.behavior.maintenancePerShipSize = 0.0;
	}

	void save(SaveFile& file) {
		uint cnt = defending.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			defending[i].save(this, file);

		cnt = tracked.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			fleets.saveMission(file, tracked[i]);
	}

	void load(SaveFile& file) {
		uint cnt = 0;

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			DefendSystem def;
			def.load(this, file);
			defending.insertLast(def);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			InvasionDefendMission@ miss = cast<InvasionDefendMission>(fleets.loadMission(file));
			if(miss !is null)
				tracked.insertLast(miss);
		}
	}

	void start() {
		//Find systems to defend
		Region@ home = ai.empire.HomeSystem;
		const SystemDesc@ sys = getSystem(home);
		for(uint i = 0, cnt = sys.adjacent.length; i < cnt; ++i) {
			auto@ otherSys = getSystem(sys.adjacent[i]);
			if(findEnemy(otherSys.object, null, Pirates.mask, fleets=false, stations=true) !is null) {
				DefendSystem def;
				@def.obj = otherSys.object;
				defending.insertLast(def);
			}
		}
	}

	bool isManaging(FleetAI& fleet) {
		if(fleet.mission is null)
			return false;
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			if(tracked[i] is fleet.mission)
				return true;
		}
		return false;
	}

	void manage(FleetAI& fleet) {
		InvasionDefendMission miss;
		@miss.fleet = fleet;

		fleets.performMission(fleet, miss);
		tracked.insertLast(miss);
	}

	void pend(Region@ region, FleetAI& fleet) {
		for(uint i = 0, cnt = defending.length; i < cnt; ++i ){
			if(defending[i].obj is region) {
				defending[i].pending.insertLast(fleet);
				break;
			}
		}
	}

	void unpend(Region@ region, FleetAI& fleet) {
		for(uint i = 0, cnt = defending.length; i < cnt; ++i ){
			if(defending[i].obj is region) {
				defending[i].pending.remove(fleet);
				break;
			}
		}
	}

	DefendSystem@ getDefending(Region@ region) {
		for(uint i = 0, cnt = defending.length; i < cnt; ++i ){
			if(defending[i].obj is region)
				return defending[i];
		}
		return null;
	}

	bool isFighting(Region@ region) {
		for(uint i = 0, cnt = defending.length; i < cnt; ++i ){
			if(defending[i].obj is region)
				return defending[i].fighting;
		}
		return false;
	}

	uint sysUpd = 0;
	void focusTick(double time) {
		//All your fleets are belong to us
		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets.fleets[i];
			if(flAI.fleetClass != FC_Combat)
				continue;
			if(!isManaging(flAI))
				manage(flAI);
		}

		//Update systems we're defending
		if(defending.length != 0) {
			sysUpd = (sysUpd+1) % defending.length;
			defending[sysUpd].update(ai, this);
		}

		//Make sure our fleets are in the right places
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i)
			tracked[i].update(ai, this);
	}
};

AIComponent@ createInvasion() {
	return Invasion();
}

Added scripts/server/empire_ai/weasel/race/Ancient.as.
















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.race.Race;

import empire_ai.weasel.Colonization;
import empire_ai.weasel.Construction;
import empire_ai.weasel.Resources;
import empire_ai.weasel.Development;
import empire_ai.weasel.Movement;
import empire_ai.weasel.Planets;
import empire_ai.weasel.Orbitals;

from orbitals import getOrbitalModule, OrbitalModule;
from buildings import getBuildingType, BuildingType;
from resources import ResourceType, getResource, getResourceID;
from statuses import getStatusID;
from biomes import getBiomeID;

enum PlanetClass {
	PC_Empty,
	PC_Core,
	PC_Mine,
	PC_Transmute,
}

class TrackReplicator {
	Object@ obj;
	Planet@ target;
	bool arrived = false;
	MoveOrder@ move;
	BuildingRequest@ build;
	uint intention = PC_Empty;

	bool get_busy() {
		if(target is null)
			return false;
		if(!arrived || move !is null || build !is null)
			return true;
		return false;
	}

	void save(Ancient& ancient, SaveFile& file) {
		file << obj;
		file << target;
		file << arrived;
		ancient.movement.saveMoveOrder(file, move);
		ancient.planets.saveBuildingRequest(file, build);
		file << intention;
	}

	void load(Ancient& ancient, SaveFile& file) {
		file >> obj;
		file >> target;
		file >> arrived;
		@move = ancient.movement.loadMoveOrder(file);
		@build = ancient.planets.loadBuildingRequest(file);
		file >> intention;
	}
};

class Ancient : Race, RaceResources, RaceColonization {
	Colonization@ colonization;
	Construction@ construction;
	Resources@ resources;
	Planets@ planets;
	Development@ development;
	Movement@ movement;
	Orbitals@ orbitals;

	array<TrackReplicator@> replicators;

	const OrbitalModule@ replicatorMod;

	const BuildingType@ core;
	const BuildingType@ miner;
	const BuildingType@ transmuter;

	const BuildingType@ foundry;

	const BuildingType@ depot;
	const BuildingType@ refinery;
	const BuildingType@ reinforcer;
	const BuildingType@ developer;
	const BuildingType@ compressor;

	int claimStatus = -1;
	int replicatorStatus = -1;

	int mountainsBiome = -1;

	int oreResource = -1;
	int baseMatResource = -1;

	bool foundFirstT2 = false;

	void create() {
		@colonization = cast<Colonization>(ai.colonization);
		colonization.performColonization = false;

		@resources = cast<Resources>(ai.resources);
		@construction = cast<Construction>(ai.construction);
		@movement = cast<Movement>(ai.movement);
		@planets = cast<Planets>(ai.planets);
		@orbitals = cast<Orbitals>(ai.orbitals);
		@planets = cast<Planets>(ai.planets);

		@development = cast<Development>(ai.development);
		development.managePlanetPressure = false;
		development.buildBuildings = false;
		development.colonizeResources = false;

		@replicatorMod = getOrbitalModule("AncientReplicator");

		@transmuter = getBuildingType("AncientTransmuter");
		@miner = getBuildingType("AncientMiner");
		@core = getBuildingType("AncientCore");

		@foundry = getBuildingType("AncientFoundry");

		@depot = getBuildingType("AncientDepot");
		@refinery = getBuildingType("AncientRefinery");
		@reinforcer = getBuildingType("AncientReinforcer");
		@developer = getBuildingType("AncientDeveloper");
		@compressor = getBuildingType("Compressor");

		claimStatus = getStatusID("AncientClaim");
		replicatorStatus = getStatusID("AncientReplicator");

		mountainsBiome = getBiomeID("Mountains");

		oreResource = getResourceID("OreRate");
		baseMatResource = getResourceID("BaseMaterial");

		@ai.defs.Factory = null;
		@ai.defs.LaborStorage = null;
	}

	void save(SaveFile& file) override {
		file << foundFirstT2;
		uint cnt = replicators.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			replicators[i].save(this, file);
	}

	void load(SaveFile& file) override {
		file >> foundFirstT2;
		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			TrackReplicator t;
			t.load(this, file);
			if(t.obj !is null)
				replicators.insertLast(t);
		}
	}

	void levelRequirements(Object& obj, int targetLevel, array<ResourceSpec@>& specs) {
		//YOLO
		specs.length = 0;
	}

	bool orderColonization(ColonizeData& data, Planet@ sourcePlanet) {
		return true;
	}

	double getGenericUsefulness(const ResourceType@ type) {
		return 1.0;
	}

	bool hasReplicator(Planet& pl) {
		for(uint i = 0, cnt = replicators.length; i < cnt; ++i) {
			if(replicators[i].target is pl)
				return true;
		}
		return false;
	}

	bool isTracking(Object& obj) {
		for(uint i = 0, cnt = replicators.length; i < cnt; ++i) {
			if(replicators[i].obj is obj)
				return true;
		}
		return false;
	}

	void trackReplicator(Object& obj) {
		TrackReplicator t;
		@t.obj = obj;

		replicators.insertLast(t);
	}

	void updateRequests(Planet& pl) {
		//Handle requests for base materials
		uint baseMatReqs = 0;
		baseMatReqs += pl.getBuildingCount(depot.id);
		baseMatReqs += pl.getBuildingCount(refinery.id);
		baseMatReqs += pl.getBuildingCount(reinforcer.id);
		baseMatReqs += pl.getBuildingCount(developer.id);
		baseMatReqs += pl.getBuildingCount(compressor.id);

		array<ImportData@> curBaseMat;
		resources.getImportsOf(curBaseMat, baseMatResource, pl);

		if(curBaseMat.length < baseMatReqs) {
			for(uint i = curBaseMat.length, cnt = baseMatReqs; i < cnt; ++i) {
				ResourceSpec spec;
				spec.type = RST_Specific;
				@spec.resource = getResource(baseMatResource);

				resources.requestResource(pl, spec);
			}
		}
		else if(curBaseMat.length > baseMatReqs) {
			for(uint i = baseMatReqs, cnt = curBaseMat.length; i < cnt; ++i)
				resources.cancelRequest(curBaseMat[i]);
		}

		//Handle requests for ore
		uint oreReqs = 0;
		oreReqs += pl.getBuildingCount(foundry.id);

		array<ImportData@> curOre;
		resources.getImportsOf(curOre, oreResource, pl);

		if(curOre.length < oreReqs) {
			for(uint i = curOre.length, cnt = oreReqs; i < cnt; ++i) {
				ResourceSpec spec;
				spec.type = RST_Specific;
				@spec.resource = getResource(oreResource);

				resources.requestResource(pl, spec);
			}
		}
		else if(curOre.length > oreReqs) {
			for(uint i = oreReqs, cnt = curOre.length; i < cnt; ++i)
				resources.cancelRequest(curOre[i]);
		}
	}

	uint plInd = 0;
	void focusTick(double time) {
		if(ai.behavior.forbidColonization) return;

		//Find new replicators
		for(uint i = 0, cnt = orbitals.orbitals.length; i < cnt; ++i) {
			auto@ orb = cast<Orbital>(orbitals.orbitals[i].obj);
			if(orb.coreModule == replicatorMod.id) {
				if(!isTracking(orb))
					trackReplicator(orb);
			}
		}

		//Update requests for planets
		if(planets.planets.length != 0) {
			for(uint n = 0, ncnt = min(planets.planets.length, 10); n < ncnt; ++n) {
				plInd = (plInd+1) % planets.planets.length;
				Planet@ pl = planets.planets[plInd].obj;

				if(classify(pl) == PC_Core)
					updateRequests(pl);
			}
		}

		//Manage existing replicators
		for(uint i = 0, cnt = replicators.length; i < cnt; ++i) {
			auto@ t = replicators[i];
			if(t.obj is null || !t.obj.valid || t.obj.owner !is ai.empire) {
				replicators.removeAt(i);
				--i; --cnt;
				continue;
			}

			if(t.target !is null) {
				if(!t.target.valid) {
					@t.target = null;
					if(!t.arrived)
						t.obj.stopMoving();
					t.arrived = false;
				}
				else if(t.target.owner !is ai.empire && t.target.owner.valid) {
					@t.target = null;
					if(!t.arrived)
						t.obj.stopMoving();
					t.arrived = false;
				}
			}

			if(t.move !is null) {
				if(t.move.failed) {
					@t.move = null;
					t.arrived = false;
				}
				else if(t.move.completed) {
					if(t.obj.isOrbitingAround(t.target)) {
						@t.move = null;
						t.arrived = true;
					}
					else if(t.obj.inOrbit) {
						@t.move = null;
						t.arrived = false;
						@t.target = null;
					}
				}
			}
			else if(t.target !is null && !t.arrived) {
				@t.move = movement.move(t.obj, t.target);
			}

			if(t.build !is null) {
				if(t.build.canceled) {
					//A build failed, give up on this planet
					if(log)
						ai.print("Failed building build", t.target);
					@t.target = null;
					@t.build = null;
					t.arrived = false;
				}
				else if(t.build.built) {
					float progress = t.build.getProgress();
					if(progress >= 1.f) {
						if(log)
							ai.print("Completed building build", t.target);
						@t.build = null;
					}
					else if(progress < -0.5f) {
						if(log)
							ai.print("Failed building build location "+t.build.builtAt, t.target);
						@t.build = null;
						@t.target = null;
						t.arrived = false;
					}
				}
			}

			if(t.arrived || t.target is null) {
				if(!t.busy)
					useReplicator(t);
			}
		}
	}
	
	uint classify(Planet& pl) {
		int resType = pl.primaryResourceType;
		if(resType == oreResource)
			return PC_Mine;
		if(resType == baseMatResource)
			return PC_Transmute;
		uint claims = pl.getStatusStackCountAny(claimStatus);
		if(claims <= 1)
			return PC_Empty;
		if(pl.getBuildingCount(core.id) >= 1)
			return PC_Core;
		if(pl.getBuildingCount(transmuter.id) >= 1)
			return PC_Transmute;
		if(pl.getBuildingCount(miner.id) >= 1)
			return PC_Mine;
		return PC_Empty;
	}

	bool shouldBeCore(const ResourceType@ type) {
		if(type.level >= 1)
			return true;
		if(type.totalPressure >= 8)
			return true;
		return false;
	}

	int openOreRequests(TrackReplicator@ discount = null) {
		int reqs = 0;
		for(uint i = 0, cnt = resources.requested.length; i < cnt; ++i) {
			auto@ req = resources.requested[i];
			if(req.beingMet)
				continue;
			if(req.spec.type != RST_Specific)
				continue;
			if(req.spec.resource.id != uint(oreResource))
				continue;
			reqs += 1;
		}
		for(uint i = 0, cnt = replicators.length; i < cnt; ++i) {
			auto@ t = replicators[i];
			if(t is discount)
				continue;
			if(t.target is null)
				continue;
			if(t.intention == PC_Mine && (t.build is null || t.build.type is miner))
				reqs -= 1;
		}
		return reqs;
	}

	int openBaseMatRequests(TrackReplicator@ discount = null) {
		int reqs = 0;
		for(uint i = 0, cnt = resources.requested.length; i < cnt; ++i) {
			auto@ req = resources.requested[i];
			if(req.beingMet)
				continue;
			if(req.spec.type != RST_Specific)
				continue;
			if(req.spec.resource.id != uint(baseMatResource))
				continue;
			reqs += 1;
		}
		for(uint i = 0, cnt = replicators.length; i < cnt; ++i) {
			auto@ t = replicators[i];
			if(t is discount)
				continue;
			if(t.target is null)
				continue;
			if(t.intention == PC_Transmute && (t.build is null || t.build.type is transmuter))
				reqs -= 1;
		}
		return reqs;
	}

	void build(TrackReplicator& t, const BuildingType@ building) {
		auto@ plAI = planets.getAI(t.target);
		if(plAI is null)
			return;
		if(!t.target.hasStatusEffect(replicatorStatus))
			return;

		//bool scatter = building is miner || building is transmuter;
		bool scatter = false;
		@t.build = planets.requestBuilding(plAI, building, scatter=scatter, moneyType=BT_Colonization);

		if(log)
			ai.print("Build "+building.name, t.target);
	}

	void useReplicator(TrackReplicator& t) {
		if(t.target !is null) {
			uint type = classify(t.target);
			switch(type) {
				case PC_Empty: {
					const ResourceType@ res = getResource(t.target.primaryResourceType);
					if(res is null) {
						@t.target = null;
						t.arrived = false;
						return;
					}

					if(shouldBeCore(res)) {
						build(t, core);
					}
					else if(openBaseMatRequests(t) >= openOreRequests(t) || gameTime < 6.0 * 60.0 || !t.target.hasBiome(mountainsBiome)) {
						build(t, transmuter);
					}
					else {
						build(t, miner);
					}
					return;
				}
				case PC_Transmute:
					@t.target = null;
					t.arrived = false;
				break;
				case PC_Mine:
					@t.target = null;
					t.arrived = false;
				break;
				case PC_Core:
					build(t, refinery);
					return;
			}
		}

		//Find a new planet to colonize
		PotentialColonize@ best;
		double bestWeight = 0.0;

		uint getType = PC_Core;
		if(openBaseMatRequests() >= 1)
			getType = PC_Transmute;
		else if(openOreRequests() >= 1 && gameTime > 6.0 * 60.0)
			getType = PC_Mine;

		auto@ potentials = colonization.getPotentialColonize();
		for(uint i = 0, cnt = potentials.length; i < cnt; ++i) {
			PotentialColonize@ p = potentials[i];
			if(hasReplicator(p.pl))
				continue;

			double w = p.weight;
			if(!foundFirstT2 && p.resource.level >= 2)
				w *= 100.0;
			else if((getType == PC_Core) != shouldBeCore(p.resource))
				w *= 0.6;
			if(getType == PC_Core && p.resource.level >= 2)
				w *= 4.0;
			if(getType == PC_Core && p.resource.level >= 3)
				w *= 6.0;
			if(getType == PC_Mine && !p.pl.hasBiome(mountainsBiome))
				w *= 0.1;
			if(getType == PC_Core)
				w *= double(p.pl.totalSurfaceTiles) / 100.0;
			w /= p.pl.position.distanceTo(t.obj.position)/1000.0;

			if(w > bestWeight) {
				bestWeight = w;
				@best = p;
			}
		}

		if(best !is null) {
			@t.target = best.pl;
			t.intention = shouldBeCore(best.resource) ? uint(PC_Core) : getType;
			t.arrived = false;
			if(!foundFirstT2) {
				if(best.resource.level == 2)
					foundFirstT2 = true; 
			}
		}
	}
};

AIComponent@ createAncient() {
	return Ancient();
}

Added scripts/server/empire_ai/weasel/race/Devout.as.
















































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.race.Race;

import empire_ai.weasel.Development;
import empire_ai.weasel.Planets;
import empire_ai.weasel.Budget;

import resources;
import buildings;
import attributes;

class Devout : Race, RaceDevelopment {
	Development@ development;
	Planets@ planets;
	Budget@ budget;

	const ResourceType@ altarResource;
	const BuildingType@ altar;

	int coverAttrib = -1;

	BuildingRequest@ altarBuild;
	Planet@ focusAltar;

	double considerTimer = 0.0;

	void save(SaveFile& file) {
		planets.saveBuildingRequest(file, altarBuild);
		file << focusAltar;
		file << considerTimer;
	}

	void load(SaveFile& file) {
		@altarBuild = planets.loadBuildingRequest(file);
		file >> focusAltar;
		file >> considerTimer;
	}

	void create() {
		@planets = cast<Planets>(ai.planets);
		@development = cast<Development>(ai.development);
		@budget = cast<Budget>(ai.budget);

		@altarResource = getResource("Altar");

		@altar = getBuildingType("Altar");

		coverAttrib = getEmpAttribute("AltarSupportedPopulation");
	}

	void start() {
		auto@ data = ai.empire.getPlanets();
		Object@ obj;
		while(receive(data, obj)) {
			Planet@ pl = cast<Planet>(obj);
			if(pl !is null){
				if(pl.primaryResourceType == altarResource.id) {
					@focusAltar = pl;
					break;
				}
			}
		}
	}

	bool shouldBeFocus(Planet& pl, const ResourceType@ resource) override {
		if(resource is altarResource)
			return true;
		return false;
	}

	void focusTick(double time) override {
		if(ai.behavior.forbidConstruction) return;

		//Handle our current altar build
		if(altarBuild !is null) {
			if(altarBuild.built) {
				@focusAltar = altarBuild.plAI.obj;
				@altarBuild = null;
			}
			else if(altarBuild.canceled) {
				@altarBuild = null;
			}
		}

		//Handle our focused altar
		if(focusAltar !is null) {
			if(!focusAltar.valid || focusAltar.owner !is ai.empire || focusAltar.primaryResourceType != altarResource.id) {
				@focusAltar = null;
			}
		}

		//If we aren't covering our entire population, find new planets to make into altars
		double coverage = ai.empire.getAttribute(coverAttrib);
		double population = ai.empire.TotalPopulation;

		if(coverage >= population || altarBuild !is null)
			return;

		bool makeNewAltar = true;
		if(focusAltar !is null) {
			auto@ foc = development.getFocus(focusAltar);
			if(foc !is null && int(foc.obj.level) >= foc.targetLevel) {
				foc.targetLevel += 1;
				considerTimer = gameTime + 180.0;
				makeNewAltar = false;
			}
			else {
				makeNewAltar = gameTime > considerTimer;
			}
		}

		if(makeNewAltar) {
			if(budget.canSpend(BT_Development, 300)) {
				//Turn our most suitable planet into an altar
				PlanetAI@ bestBuild;
				double bestWeight = 0.0;

				for(uint i = 0, cnt = planets.planets.length; i < cnt; ++i) {
					auto@ plAI = planets.planets[i];
					double w = randomd(0.9, 1.1);

					if(plAI.resources !is null && plAI.resources.length != 0) {
						auto@ res = plAI.resources[0].resource;
						if(res.level == 0 && !res.limitlessLevel)
							w *= 5.0;
						if(res.cls !is null)
							w *= 0.5;
						if(res.level > 0)
							w /= pow(2.0, res.level);
					}
					else {
						w *= 100.0;
					}

					if(w > bestWeight) {
						bestWeight = w;
						@bestBuild = plAI;
					}
				}

				if(bestBuild !is null) {
					@altarBuild = planets.requestBuilding(bestBuild, altar, expire=60.0);
					considerTimer = gameTime + 120.0;
				}
			}
		}
	}
};

AIComponent@ createDevout() {
	return Devout();
}

Added scripts/server/empire_ai/weasel/race/Extragalactic.as.


















































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.race.Race;

import empire_ai.weasel.Colonization;
import empire_ai.weasel.Construction;
import empire_ai.weasel.Resources;
import empire_ai.weasel.Scouting;
import empire_ai.weasel.Orbitals;
import empire_ai.weasel.Budget;

from orbitals import getOrbitalModuleID;
from constructions import ConstructionType, getConstructionType;

class Extragalactic : Race {
	Colonization@ colonization;
	Construction@ construction;
	Scouting@ scouting;
	Orbitals@ orbitals;
	Resources@ resources;
	Budget@ budget;

	array<OrbitalAI@> beacons;
	OrbitalAI@ masterBeacon;

	int beaconMod = -1;

	array<ImportData@> imports;
	array<const ConstructionType@> beaconBuilds;

	void create() {
		@colonization = cast<Colonization>(ai.colonization);
		colonization.performColonization = false;
		colonization.queueColonization = false;

		@scouting = cast<Scouting>(ai.scouting);
		scouting.buildScouts = false;

		@orbitals = cast<Orbitals>(ai.orbitals);
		beaconMod = getOrbitalModuleID("Beacon");

		@construction = cast<Construction>(ai.construction);
		@resources = cast<Resources>(ai.resources);
		@budget = cast<Budget>(ai.budget);

		beaconBuilds.insertLast(getConstructionType("BeaconHealth"));
		beaconBuilds.insertLast(getConstructionType("BeaconWeapons"));
		beaconBuilds.insertLast(getConstructionType("BeaconLabor"));
	}

	void save(SaveFile& file) override {
		uint cnt = beacons.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			orbitals.saveAI(file, beacons[i]);
		orbitals.saveAI(file, masterBeacon);

		cnt = imports.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			resources.saveImport(file, imports[i]);
	}

	void load(SaveFile& file) override {
		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ b = orbitals.loadAI(file);
			if(b !is null && b.obj !is null)
				beacons.insertLast(b);
		}
		@masterBeacon = orbitals.loadAI(file);

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ imp = resources.loadImport(file);
			if(imp !is null)
				imports.insertLast(imp);
		}
	}

	uint prevBeacons = 0;
	void focusTick(double time) {
		if(ai.behavior.forbidConstruction) return;

		//Find our beacons
		for(uint i = 0, cnt = beacons.length; i < cnt; ++i) {
			auto@ b = beacons[i];
			if(b is null || b.obj is null || !b.obj.valid || b.obj.owner !is ai.empire) {
				if(b.obj !is null)
					resources.killImportsTo(b.obj);
				beacons.removeAt(i);
				--i; --cnt;
			}
		}

		for(uint i = 0, cnt = orbitals.orbitals.length; i < cnt; ++i) {
			auto@ orb = orbitals.orbitals[i];
			Orbital@ obj = cast<Orbital>(orb.obj);
			if(obj !is null && obj.coreModule == uint(beaconMod)) {
				if(beacons.find(orb) == -1)
					beacons.insertLast(orb);
			}
		}

		//Find our master beacon
		if(masterBeacon !is null) {
			Orbital@ obj = cast<Orbital>(masterBeacon.obj);
			if(obj is null || !obj.valid || obj.owner !is ai.empire || obj.hasMaster())
				@masterBeacon = null;
		}
		else {
			for(uint i = 0, cnt = beacons.length; i < cnt; ++i) {
				auto@ b = beacons[i];
				Orbital@ obj = cast<Orbital>(b.obj);
				if(!obj.hasMaster()) {
					@masterBeacon = b;
					ai.empire.setDefending(obj, true);
					break;
				}
			}
		}

		scouting.buildScouts = gameTime > 5.0 * 60.0;
		if(prevBeacons < beacons.length && masterBeacon !is null && gameTime > 10.0) {
			for(int i = beacons.length-1; i >= int(prevBeacons); --i) {
				//Make sure we order a scout at each beacon
				if(!scouting.buildScouts) {
					BuildFlagshipSourced build(scouting.scoutDesign);
					build.moneyType = BT_Military;
					@build.buildAt = masterBeacon.obj;
					if(beacons[i] !is masterBeacon)
						@build.buildFrom = beacons[i].obj;

					construction.build(build, force=true);
				}

				//Set the beacon to fill up other stuff
				beacons[i].obj.allowFillFrom = true;
			}
			prevBeacons = beacons.length;
		}

		//Handle with importing labor and defense to our master beacon
		if(masterBeacon !is null) {
			if(imports.length == 0) {
				//Request labor and defense at our beacon
				{
					ResourceSpec spec;
					spec.type = RST_Pressure_Type;
					spec.pressureType = TR_Labor;

					imports.insertLast(resources.requestResource(masterBeacon.obj, spec));
				}
				{
					ResourceSpec spec;
					spec.type = RST_Pressure_Type;
					spec.pressureType = TR_Defense;

					imports.insertLast(resources.requestResource(masterBeacon.obj, spec));
				}
				{
					ResourceSpec spec;
					spec.type = RST_Pressure_Level0;
					spec.pressureType = TR_Research;

					imports.insertLast(resources.requestResource(masterBeacon.obj, spec));
				}
			}
			else {
				//When our requests are met, make more requests!
				for(uint i = 0, cnt = imports.length; i < cnt; ++i) {
					if(imports[i].beingMet || imports[i].obj !is masterBeacon.obj) {
						ResourceSpec spec;
						spec = imports[i].spec;
						@imports[i] = resources.requestResource(masterBeacon.obj, spec);
					}
				}
			}

			//Build stuff on our beacon if we have enough stuff
			if(budget.canSpend(BT_Development, 300)) {
				uint offset = randomi(0, beaconBuilds.length-1);
				for(uint i = 0, cnt = beaconBuilds.length; i < cnt; ++i) {
					uint ind = (i+offset) % cnt;
					auto@ type = beaconBuilds[ind];
					if(type is null)
						continue;

					if(type.canBuild(masterBeacon.obj, ignoreCost=false)) {
						masterBeacon.obj.buildConstruction(type.id);
						break;
					}
				}
			}
		}
	}
};

AIComponent@ createExtragalactic() {
	return Extragalactic();
}

Added scripts/server/empire_ai/weasel/race/Linked.as.






































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.race.Race;

import empire_ai.weasel.Movement;
import empire_ai.weasel.Military;
import empire_ai.weasel.Construction;
import empire_ai.weasel.Designs;
import empire_ai.weasel.Development;
import empire_ai.weasel.Systems;
import empire_ai.weasel.Budget;

from orbitals import getOrbitalModuleID;

const double MAINFRAME_MIN_DISTANCE_STAGE = 15000;
const double MAINFRAME_MIN_DISTANCE_DEVELOP = 20000;
const double MAINFRAME_MIN_TIMER = 3.0 * 60.0;
const int MAINFRAME_BUILD_MOVE_HOPS = 5;

class LinkRegion : Savable {
	Region@ region;
	Object@ obj;
	bool arrived = false;
	vec3d destination;

	void save(SaveFile& file) {
		file << region;
		file << obj;
		file << arrived;
		file << destination;
	}

	void load(SaveFile& file) {
		file >> region;
		file >> obj;
		file >> arrived;
		file >> destination;
	}
};

class Linked : Race {
	Military@ military;
	Designs@ designs;
	Construction@ construction;
	Development@ development;
	Systems@ systems;
	Budget@ budget;

	array<LinkRegion@> tracked;
	array<Object@> unassigned;

	BuildOrbital@ buildMainframe;
	int mainframeId = -1;

	double nextBuildTry = 15.0 * 60.0;

	void create() override {
		@military = cast<Military>(ai.military);
		@designs = cast<Designs>(ai.designs);
		@construction = cast<Construction>(ai.construction);
		@development = cast<Development>(ai.development);
		@systems = cast<Systems>(ai.systems);
		@budget = cast<Budget>(ai.budget);

		mainframeId = getOrbitalModuleID("Mainframe");
	}

	void save(SaveFile& file) override {
		construction.saveConstruction(file, buildMainframe);
		file << nextBuildTry;

		uint cnt = tracked.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << tracked[i];

		cnt = unassigned.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			file << unassigned[i];
	}

	void load(SaveFile& file) override {
		@buildMainframe = cast<BuildOrbital>(construction.loadConstruction(file));
		file >> nextBuildTry;

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			LinkRegion gt;
			file >> gt;
			tracked.insertLast(gt);
		}

		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			Object@ obj;
			file >> obj;
			if(obj !is null)
				unassigned.insertLast(obj);
		}
	}

	LinkRegion@ get(Region@ reg) {
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			if(tracked[i].region is reg)
				return tracked[i];
		}
		return null;
	}

	void remove(LinkRegion@ gt) {
		if(gt.obj !is null && gt.obj.valid && gt.obj.owner is ai.empire)
			unassigned.insertLast(gt.obj);
		tracked.remove(gt);
	}

	Object@ getClosestMainframe(const vec3d& position) {
		Object@ closest;
		double minDist = INFINITY;
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			Object@ obj = tracked[i].obj;
			if(obj is null)
				continue;
			if(!tracked[i].arrived)
				continue;
			double d = obj.position.distanceTo(position);
			if(d < minDist) {
				minDist = d;
				@closest = obj;
			}
		}
		return closest;
	}

	LinkRegion@ getClosestLinkRegion(const vec3d& position) {
		LinkRegion@ closest;
		double minDist = INFINITY;
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			double d = tracked[i].region.position.distanceTo(position);
			if(d < minDist) {
				minDist = d;
				@closest = tracked[i];
			}
		}
		return closest;
	}

	void assignTo(LinkRegion@ gt, Object@ closest) {
		unassigned.remove(closest);
		@gt.obj = closest;
		gt.arrived = false;

		if(closest.region is gt.region)
			gt.arrived = true;
		if(!gt.arrived) {
			gt.destination = military.getStationPosition(gt.region);
			closest.addMoveOrder(gt.destination);
		}
	}

	bool trackingMainframe(Object@ obj) {
		for(uint i = 0, cnt = unassigned.length; i < cnt; ++i) {
			if(unassigned[i] is obj)
				return true;
		}
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			if(tracked[i].obj is obj)
				return true;
		}
		return false;
	}

	bool shouldHaveMainframe(Region@ reg, bool always = false) {
		if(military.getBase(reg) !is null)
			return true;
		if(development.isDevelopingIn(reg))
			return true;
		return false;
	}

	void focusTick(double time) override {
		if(ai.behavior.forbidConstruction) return;

		//Manage unassigned mainframes list
		for(uint i = 0, cnt = unassigned.length; i < cnt; ++i) {
			Object@ obj = unassigned[i];
			if(obj is null || !obj.valid || obj.owner !is ai.empire) {
				unassigned.removeAt(i);
				--i; --cnt;
			}
		}

		//Detect new gates
		auto@ data = ai.empire.getOrbitals();
		Object@ obj;
		while(receive(data, obj)) {
			if(obj is null)
				continue;
			Orbital@ orb = cast<Orbital>(obj);
			if(orb is null || orb.coreModule != uint(mainframeId))
				continue;
			if(!trackingMainframe(obj))
				unassigned.insertLast(obj);
		}

		//Update existing gates for staging bases
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			auto@ gt = tracked[i];
			bool checkAlways = false;
			if(gt.obj !is null) {
				if(!gt.obj.valid || gt.obj.owner !is ai.empire || (gt.arrived && gt.obj.region !is gt.region)) {
					@gt.obj = null;
					gt.arrived = false;
					checkAlways = true;
				}
				else if(!gt.arrived && !gt.obj.hasOrders) {
					if(gt.destination.distanceTo(gt.obj.position) < 10.0)
						gt.arrived = true;
					else
						gt.obj.addMoveOrder(gt.destination);
				}
			}
			if(!shouldHaveMainframe(gt.region, checkAlways)) {
				remove(tracked[i]);
				--i; --cnt;
			}
		}

		//Detect new staging bases to build mainframes at
		for(uint i = 0, cnt = military.stagingBases.length; i < cnt; ++i) {
			auto@ base = military.stagingBases[i];
			if(base.occupiedTime < MAINFRAME_MIN_TIMER)
				continue;

			if(get(base.region) is null) {
				LinkRegion@ closest = getClosestLinkRegion(base.region.position);
				if(closest !is null && closest.region.position.distanceTo(base.region.position) < MAINFRAME_MIN_DISTANCE_STAGE)
					continue;

				LinkRegion gt;
				@gt.region = base.region;
				tracked.insertLast(gt);
				break;
			}
		}

		//Detect new important planets to build mainframes at
		for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i) {
			auto@ focus = development.focuses[i];
			Region@ reg = focus.obj.region;
			if(reg is null)
				continue;

			if(get(reg) is null) {
				LinkRegion@ closest = getClosestLinkRegion(reg.position);
				if(closest !is null && closest.region.position.distanceTo(reg.position) < MAINFRAME_MIN_DISTANCE_DEVELOP)
					continue;

				LinkRegion gt;
				@gt.region = reg;
				tracked.insertLast(gt);
				break;
			}
		}

		//See if we should build a new mainframe
		if(buildMainframe !is null) {
			if(buildMainframe.completed) {
				@buildMainframe = null;
				nextBuildTry = gameTime + 60.0;
			}
		}
		for(uint i = 0, cnt = tracked.length; i < cnt; ++i) {
			auto@ gt = tracked[i];
			if(gt.obj is null) {
				Object@ closest;
				double closestDist = INFINITY;
				for(uint n = 0, ncnt = unassigned.length; n < ncnt; ++n) {
					Object@ obj = unassigned[n];
					if(obj.region is gt.region) {
						@closest = obj;
						break;
					}
					if(!obj.hasMover)
						continue;
					if(buildMainframe is null && gameTime > nextBuildTry) {
						double d = obj.position.distanceTo(gt.region.position);
						if(d < closestDist) {
							closestDist = d;
							@closest = obj;
						}
					}
				}

				if(closest !is null) {
					if(log)
						ai.print("Assign mainframe to => "+gt.region.name, closest.region);
					assignTo(gt, closest);
				} else if(buildMainframe is null && gameTime > nextBuildTry) {
					if(log)
						ai.print("Build mainframe for this system", gt.region);

					bool buildLocal = true;
					auto@ factory = construction.primaryFactory;
					if(factory !is null) {
						Region@ factRegion = factory.obj.region;
						if(factRegion !is null && systems.hopDistance(gt.region, factRegion) < MAINFRAME_BUILD_MOVE_HOPS)
							buildLocal = false;
					}

					if(buildLocal)
						@buildMainframe = construction.buildLocalOrbital(getOrbitalModule(mainframeId));
					else
						@buildMainframe = construction.buildOrbital(getOrbitalModule(mainframeId), military.getStationPosition(gt.region));
				}
			}
		}
	}
};

AIComponent@ createLinked() {
	return Linked();
}

Added scripts/server/empire_ai/weasel/race/Mechanoid.as.




























































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.race.Race;

import empire_ai.weasel.Resources;
import empire_ai.weasel.Colonization;
import empire_ai.weasel.Construction;
import empire_ai.weasel.Movement;
import empire_ai.weasel.Planets;
import empire_ai.weasel.Budget;

import resources;
import abilities;
import planet_levels;
from constructions import getConstructionType, ConstructionType;
from abilities import getAbilityID;
import oddity_navigation;

const double MAX_POP_BUILDTIME = 3.0 * 60.0;

class Mechanoid : Race, RaceResources, RaceColonization {
	Colonization@ colonization;
	Construction@ construction;
	Movement@ movement;
	Budget@ budget;
	Planets@ planets;

	const ResourceType@ unobtanium;
	const ResourceType@ crystals;
	int unobtaniumAbl = -1;

	const ResourceClass@ foodClass;
	const ResourceClass@ waterClass;
	const ResourceClass@ scalableClass;
	const ConstructionType@ buildPop;

	int colonizeAbl = -1;

	array<Planet@> popRequests;
	array<Planet@> popSources;
	array<Planet@> popFactories;

	void create() {
		@colonization = cast<Colonization>(ai.colonization);
		@construction = cast<Construction>(ai.construction);
		@movement = cast<Movement>(ai.movement);
		@planets = cast<Planets>(ai.planets);
		@budget = cast<Budget>(ai.budget);

		@ai.defs.Shipyard = null;

		@crystals = getResource("FTL");
		@unobtanium = getResource("Unobtanium");
		unobtaniumAbl = getAbilityID("UnobtaniumMorph");

		@foodClass = getResourceClass("Food");
		@waterClass = getResourceClass("WaterType");
		@scalableClass = getResourceClass("Scalable");

		colonizeAbl = getAbilityID("MechanoidColonize");
		colonization.performColonization = false;

		@buildPop = getConstructionType("MechanoidPopulation");
	}

	void start() {
		//Oh yes please can we have some ftl crystals sir
		if(crystals !is null) {
			ResourceSpec spec;
			spec.type = RST_Specific;
			@spec.resource = crystals;
			spec.isLevelRequirement = false;
			spec.isForImport = false;

			colonization.queueColonize(spec);
		}
	}

	void levelRequirements(Object& obj, int targetLevel, array<ResourceSpec@>& specs) override {
		//Remove all food and water resources
		if(obj.levelChain != baseLevelChain.id)
			return;
		for(int i = specs.length-1; i >= 0; --i) {
			auto@ spec = specs[i];
			if(spec.type == RST_Class && (spec.cls is foodClass || spec.cls is waterClass))
				specs.removeAt(i);
		}
	}

	double transferCost(double dist) {
		return 20 + dist * 0.002;
	}

	bool orderColonization(ColonizeData& data, Planet@ sourcePlanet) {
		return false;
	}

	double getGenericUsefulness(const ResourceType@ type) override {
		if(type.cls is foodClass || type.cls is waterClass)
			return 0.00001;
		if(type.level == 1)
			return 100.0;
		return 1.0;
	}

	bool canBuildPopulation(Planet& pl, double factor=1.0) {
		if(buildPop is null)
			return false;
		if(!buildPop.canBuild(pl, ignoreCost=true))
			return false;
		auto@ primFact = construction.primaryFactory;
		if(primFact !is null && pl is primFact.obj)
			return true;

		double laborCost = buildPop.getLaborCost(pl);
		double laborIncome = pl.laborIncome;
		return laborCost < laborIncome * MAX_POP_BUILDTIME * factor;
	}

	bool requiresPopulation(Planet& pl, double mod = 0.0) {
		double curPop = pl.population + mod;
		double maxPop = pl.maxPopulation;
		return curPop < maxPop;
	}

	bool canSendPopulation(Planet& pl, double mod = 0.0) {
		double curPop = pl.population + mod;
		double maxPop = pl.maxPopulation;
		if(curPop >= maxPop + 1)
			return true;
		//auto@ primFact = construction.primaryFactory;
		//if(primFact !is null && pl is primFact.obj) {
		//	uint minFacts = 2;
		//	if(popFactories.find(pl) == -1)
		//		minFacts -= 1;
		//	if(popFactories.length >= minFacts)
		//		return false;
		//}
		//if(canBuildPopulation(pl)) {
		//	if(curPop >= maxPop)
		//		return true;
		//}
		return false;
	}

	uint chkInd = 0;
	array<Planet@> availSources;
	void focusTick(double time) override {
		if(ai.behavior.forbidColonization) return;

		//Check existing lists
		for(uint i = 0, cnt = popFactories.length; i < cnt; ++i) {
			auto@ obj = popFactories[i];
			if(obj is null || !obj.valid || obj.owner !is ai.empire) {
				popFactories.removeAt(i);
				--i; --cnt;
				continue;
			}
			if(!canBuildPopulation(popFactories[i])) {
				popFactories.removeAt(i);
				--i; --cnt;
				continue;
			}
		}

		for(uint i = 0, cnt = popSources.length; i < cnt; ++i) {
			auto@ obj = popSources[i];
			if(obj is null || !obj.valid || obj.owner !is ai.empire) {
				popSources.removeAt(i);
				--i; --cnt;
				continue;
			}
			if(!canSendPopulation(popSources[i])) {
				popSources.removeAt(i);
				--i; --cnt;
				continue;
			}
		}

		for(uint i = 0, cnt = popRequests.length; i < cnt; ++i) {
			auto@ obj = popRequests[i];
			if(obj is null || !obj.valid || obj.owner !is ai.empire) {
				popRequests.removeAt(i);
				--i; --cnt;
				continue;
			}
			if(!requiresPopulation(popRequests[i])) {
				popRequests.removeAt(i);
				--i; --cnt;
				continue;
			}
		}

		//Find new planets to add to our lists
		bool checkMorph = false;
		Planet@ hw = ai.empire.Homeworld;
		if(hw !is null && hw.valid && hw.owner is ai.empire && unobtanium !is null) {
			if(hw.primaryResourceType == unobtanium.id)
				checkMorph = true;
		}

		uint plCnt = planets.planets.length;
		for(uint n = 0, cnt = min(15, plCnt); n < cnt; ++n) {
			chkInd = (chkInd+1) % plCnt;
			auto@ plAI = planets.planets[chkInd];

			//Find planets that can build population reliably
			if(canBuildPopulation(plAI.obj)) {
				if(popFactories.find(plAI.obj) == -1)
					popFactories.insertLast(plAI.obj);
			}

			//Find planets that need population
			if(requiresPopulation(plAI.obj)) {
				if(popRequests.find(plAI.obj) == -1)
					popRequests.insertLast(plAI.obj);
			}

			//Find planets that have extra population
			if(canSendPopulation(plAI.obj)) {
				if(popSources.find(plAI.obj) == -1)
					popSources.insertLast(plAI.obj);
			}

			if(plAI.resources !is null && plAI.resources.length != 0) {
				auto@ res = plAI.resources[0];

				//Get rid of food and water we don't need
				if(res.resource.cls is foodClass || res.resource.cls is waterClass) {
					if(res.request is null && !ai.behavior.forbidScuttle) {
						Region@ reg = res.obj.region;
						if(reg !is null && reg.getPlanetCount(ai.empire) >= 2) {
							plAI.obj.abandon();
						}
					}
				}

				//See if we have anything useful to morph our homeworld too
				if(checkMorph) {
					bool morph = false;
					if(res.resource is crystals)
						morph = true;
					else if(res.resource.level >= 2 && res.resource.tilePressure[TR_Labor] >= 5)
						morph = true;
					else if(res.resource.level >= 3 && res.resource.totalPressure > 10)
						morph = true;
					else if(res.resource.cls is scalableClass && gameTime > 30.0 * 60.0)
						morph = true;
					else if(res.resource.level >= 2 && res.resource.totalPressure >= 5 && gameTime > 60.0 * 60.0)
						morph = true;

					if(morph) {
						if(log)
							ai.print("Morph homeworld to "+res.resource.name+" from "+res.obj.name, hw);
						hw.activateAbilityTypeFor(ai.empire, unobtaniumAbl, plAI.obj);
					}
				}
			}
		}

		//See if we can find something to send population to
		availSources = popSources;

		for(uint i = 0, cnt = popRequests.length; i < cnt; ++i) {
			Planet@ dest = popRequests[i];
			if(canBuildPopulation(dest, factor=(availSources.length == 0 ? 2.5 : 1.5))) {
				Factory@ f = construction.get(dest);
				if(f !is null) {
					if(f.active is null) {
						auto@ build = construction.buildConstruction(buildPop);
						construction.buildNow(build, f);
						if(log)
							ai.print("Build population", f.obj);
						continue;
					}
					else {
						auto@ cons = cast<BuildConstruction>(f.active);
						if(cons !is null && cons.consType is buildPop) {
							if(double(dest.maxPopulation) <= dest.population + 0.0)
								continue;
						}
					}
				}
			}
			transferBest(dest, availSources);
		}

		if(availSources.length != 0) {
			//If we have any population left, do stuff from our colonization queue
			for(uint i = 0, cnt = colonization.awaitingSource.length; i < cnt && availSources.length != 0; ++i) {
				Planet@ dest = colonization.awaitingSource[i].target;
				Planet@ source = transferBest(dest, availSources);
				if(source !is null) {
					@colonization.awaitingSource[i].colonizeFrom = source;
					colonization.awaitingSource.removeAt(i);
					--i; --cnt;
				}
			}
		}

		//Build population on idle planets
		if(budget.canSpend(BT_Development, 100)) {
			for(int i = popFactories.length-1; i >= 0; --i) {
				Planet@ dest = popFactories[i];
				Factory@ f = construction.get(dest);
				if(f is null || f.active !is null)
					continue;
				if(dest.population >= double(dest.maxPopulation) + 1.0)
					continue;

				auto@ build = construction.buildConstruction(buildPop);
				construction.buildNow(build, f);
				if(log)
					ai.print("Build population for idle", f.obj);
				break;
			}
		}
	}

	Planet@ transferBest(Planet& dest, array<Planet@>& availSources) {
		//Find closest source
		Planet@ bestSource;
		double bestDist = INFINITY;
		for(uint j = 0, jcnt = availSources.length; j < jcnt; ++j) {
			double d = movement.getPathDistance(availSources[j].position, dest.position);
			if(d < bestDist) {
				bestDist = d;
				@bestSource = availSources[j];
			}
		}

		if(bestSource !is null) {
			double cost = transferCost(bestDist);
			if(cost <= ai.empire.FTLStored) {
				if(log)
					ai.print("Transfering population to "+dest.name, bestSource);
				availSources.remove(bestSource);
				bestSource.activateAbilityTypeFor(ai.empire, colonizeAbl, dest);
				return bestSource;
			}
		}
		return null;
	}

	void tick(double time) override {
	}
};

AIComponent@ createMechanoid() {
	return Mechanoid();
}

Added scripts/server/empire_ai/weasel/race/Race.as.








>
>
>
>
1
2
3
4
import empire_ai.weasel.WeaselAI;

class Race : AIComponent {
};

Added scripts/server/empire_ai/weasel/race/StarChildren.as.
























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.race.Race;

import empire_ai.weasel.Colonization;
import empire_ai.weasel.Resources;
import empire_ai.weasel.Construction;
import empire_ai.weasel.Development;
import empire_ai.weasel.Fleets;
import empire_ai.weasel.Movement;
import empire_ai.weasel.Planets;
import empire_ai.weasel.Designs;

import oddity_navigation;
from abilities import getAbilityID;
from statuses import getStatusID;

class HabitatMission : Mission {
	Planet@ target;
	MoveOrder@ move;
	double timer = 0.0;

	void save(Fleets& fleets, SaveFile& file) override {
		file << target;
		file << timer;
		fleets.movement.saveMoveOrder(file, move);
	}

	void load(Fleets& fleets, SaveFile& file) override {
		file >> target;
		file >> timer;
		@move = fleets.movement.loadMoveOrder(file);
	}

	void start(AI& ai, FleetAI& fleet) override {
		uint prior = MP_Normal;
		if(gameTime < 30.0 * 60.0)
			prior = MP_Critical;
		@move = cast<Movement>(ai.movement).move(fleet.obj, target, prior);
	}

	void tick(AI& ai, FleetAI& fleet, double time) override {
		if(move !is null) {
			if(move.failed) {
				canceled = true;
				return;
			}
			if(move.completed) {
				int ablId = cast<StarChildren>(ai.race).habitatAbl;
				fleet.obj.activateAbilityTypeFor(ai.empire, ablId, target);

				@move = null;
				timer = gameTime + 60.0;
			}
		}
		else {
			if(target is null || !target.valid || target.quarantined
					|| (target.owner !is ai.empire && target.owner.valid)
					|| target.inCombat) {
				canceled = true;
				return;
			}

			double maxPop = max(double(target.maxPopulation), double(getPlanetLevel(target, target.primaryResourceLevel).population));
			double curPop = target.population;
			if(curPop >= maxPop) {
				completed = true;
				return;
			}

			if(gameTime >= timer) {
				int popStatus = cast<StarChildren>(ai.race).popStatus;
				if(target.getStatusStackCountAny(popStatus) >= 5) {
					canceled = true;
					return;
				}
			}
		}
	}
};

class LaborMission : Mission {
	Planet@ target;
	MoveOrder@ move;
	double timer = 0.0;

	void save(Fleets& fleets, SaveFile& file) override {
		file << target;
		file << timer;
		fleets.movement.saveMoveOrder(file, move);
	}

	void load(Fleets& fleets, SaveFile& file) override {
		file >> target;
		file >> timer;
		@move = fleets.movement.loadMoveOrder(file);
	}

	void start(AI& ai, FleetAI& fleet) override {
		@move = cast<Movement>(ai.movement).move(fleet.obj, target);
	}

	void tick(AI& ai, FleetAI& fleet, double time) override {
		if(move !is null) {
			if(move.failed) {
				canceled = true;
				return;
			}
			if(move.completed) {
				@move = null;
				timer = gameTime + 10.0;
			}
		}
		else {
			if(target is null || !target.valid || target.quarantined
					|| target.owner !is ai.empire) {
				canceled = true;
				return;
			}

			if(gameTime >= timer) {
				int popStatus = cast<StarChildren>(ai.race).popStatus;
				timer = gameTime + 10.0;
				if(target.getStatusStackCountAny(popStatus) >= 10) {
					completed = true;
					return;
				}
			}
		}
	}
};

class StarChildren : Race {
	Colonization@ colonization;
	Construction@ construction;
	Development@ development;
	Movement@ movement;
	Planets@ planets;
	Fleets@ fleets;
	Designs@ designs;

	DesignTarget@ mothershipDesign;
	double idleSince = 0;

	array<FleetAI@> motherships;

	int habitatAbl = -1;
	int popStatus = -1;

	array<Planet@> popRequests;
	array<Planet@> laborPlanets;

	BuildFlagship@ mcBuild;
	BuildOrbital@ yardBuild;

	void save(SaveFile& file) override {
		designs.saveDesign(file, mothershipDesign);
		file << idleSince;
		construction.saveConstruction(file, mcBuild);
		construction.saveConstruction(file, yardBuild);

		uint cnt = motherships.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i)
			fleets.saveAI(file, motherships[i]);
	}

	void load(SaveFile& file) override {
		@mothershipDesign = designs.loadDesign(file);
		file >> idleSince;
		@mcBuild = cast<BuildFlagship>(construction.loadConstruction(file));
		@yardBuild = cast<BuildOrbital>(construction.loadConstruction(file));

		uint cnt = 0;
		file >> cnt;
		for(uint i = 0; i < cnt; ++i) {
			auto@ flAI = fleets.loadAI(file);
			if(flAI !is null)
				motherships.insertLast(flAI);
		}
	}

	void create() override {
		@colonization = cast<Colonization>(ai.colonization);
		colonization.performColonization = false;

		@development = cast<Development>(ai.development);
		development.managePlanetPressure = false;
		development.buildBuildings = false;

		@fleets = cast<Fleets>(ai.fleets);
		@construction = cast<Construction>(ai.construction);
		@planets = cast<Planets>(ai.planets);
		@designs = cast<Designs>(ai.designs);
		@movement = cast<Movement>(ai.movement);

		@ai.defs.Factory = null;
		@ai.defs.LaborStorage = null;

		habitatAbl = getAbilityID("MothershipColonize");
		popStatus = getStatusID("MothershipPopulation");
	}

	void start() override {
		//Get the Tier 1 in our home system
		{
			ResourceSpec spec;
			spec.type = RST_Level_Specific;
			spec.level = 1;
			spec.isForImport = false;
			spec.isLevelRequirement = false;

			colonization.queueColonize(spec);
		}

		//Then find a Tier 2 to get
		{
			ResourceSpec spec;
			spec.type = RST_Level_Specific;
			spec.level = 2;
			spec.isForImport = false;
			spec.isLevelRequirement = false;

			colonization.queueColonize(spec);
		}

		//Design a mothership
		@mothershipDesign = designs.design(DP_Mothership, 500);
		mothershipDesign.targetMaintenance = 300;
		mothershipDesign.targetLaborCost = 110;
		mothershipDesign.customName = "Mothership";
	}

	bool requiresPopulation(Planet& target) {
		double maxPop = max(double(target.maxPopulation), double(getPlanetLevel(target, target.primaryResourceLevel).population));
		double curPop = target.population;
		return curPop < maxPop;
	}

	uint chkInd = 0;
	void focusTick(double time) override {
		if(ai.behavior.forbidColonization) return;

		//Detect motherships
		for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) {
			auto@ flAI = fleets.fleets[i];
			if(flAI.fleetClass != FC_Mothership)
				continue;

			if(motherships.find(flAI) == -1) {
				//Add to our tracking list
				flAI.obj.autoFillSupports = false;
				flAI.obj.allowFillFrom = true;
				motherships.insertLast(flAI);

				//Add as a factory
				construction.registerFactory(flAI.obj);
			}
		}

		for(uint i = 0, cnt = motherships.length; i < cnt; ++i) {
			Object@ obj = motherships[i].obj;
			if(obj is null || !obj.valid || obj.owner !is ai.empire) {
				motherships.removeAt(i);
				--i; --cnt;
			}
		}

		//Detect planets that require more population
		for(uint i = 0, cnt = popRequests.length; i < cnt; ++i) {
			auto@ obj = popRequests[i];
			if(obj is null || !obj.valid || obj.owner !is ai.empire) {
				popRequests.removeAt(i);
				--i; --cnt;
				continue;
			}
			if(!requiresPopulation(obj)) {
				popRequests.removeAt(i);
				--i; --cnt;
				continue;
			}
		}

		for(uint i = 0, cnt = laborPlanets.length; i < cnt; ++i) {
			auto@ obj = laborPlanets[i];
			if(obj is null || !obj.valid || obj.owner !is ai.empire) {
				laborPlanets.removeAt(i);
				--i; --cnt;
				continue;
			}
			if(obj.laborIncome < 3.0/60.0) {
				laborPlanets.removeAt(i);
				--i; --cnt;
				continue;
			}
		}

		uint plCnt = planets.planets.length;
		for(uint n = 0, cnt = min(15, plCnt); n < cnt; ++n) {
			chkInd = (chkInd+1) % plCnt;
			auto@ plAI = planets.planets[chkInd];

			//Find planets that need population
			if(requiresPopulation(plAI.obj)) {
				if(popRequests.find(plAI.obj) == -1)
					popRequests.insertLast(plAI.obj);
			}

			//Find planets that have labor
			if(plAI.obj.laborIncome >= 3.0/60.0) {
				if(laborPlanets.find(plAI.obj) == -1)
					laborPlanets.insertLast(plAI.obj);
			}
		}

		//Send motherships to do colonization
		uint totalCount = popRequests.length + colonization.awaitingSource.length;
		uint motherCount = idleMothershipCount();

		/*if(motherCount > totalCount) {*/
			for(uint i = 0, cnt = popRequests.length; i < cnt; ++i) {
				Planet@ dest = popRequests[i];
				if(isColonizing(dest))
					continue;
				if(dest.inCombat)
					continue;

				colonizeBest(dest);
			}

			for(uint i = 0, cnt = colonization.awaitingSource.length; i < cnt; ++i) {
				Planet@ dest = colonization.awaitingSource[i].target;
				if(isColonizing(dest))
					continue;

				colonizeBest(dest);
			}
		/*}*/
		/*else {*/
		/*	for(uint i = 0, cnt = motherships.length; i < cnt; ++i) {*/
		/*		auto@ flAI = motherships[i];*/
		/*		if(flAI.mission !is null)*/
		/*			continue;*/
		/*		if(isBuildingWithLabor(flAI))*/
		/*			continue;*/

		/*		colonizeBest(flAI);*/
		/*	}*/
		/*}*/

		if(totalCount != 0)
			idleSince = gameTime;

		//See if we should build new motherships
		uint haveMC = motherships.length;
		uint wantMC = 1;
		if(gameTime > 20.0 * 60.0)
			wantMC += 1;
		wantMC = max(wantMC, min(laborPlanets.length, uint(gameTime/(30.0*60.0))));

		if(mcBuild !is null && mcBuild.completed)
			@mcBuild = null;
		if(wantMC > haveMC && mcBuild is null)
			@mcBuild = construction.buildFlagship(mothershipDesign, force=true);

		if(yardBuild is null && haveMC > 0 && gameTime > 60 && gameTime < 180 && ai.defs.Shipyard !is null) {
			Region@ reg = motherships[0].obj.region;
			if(reg !is null) {
				vec3d pos = reg.position;
				vec2d offset = random2d(reg.radius * 0.4, reg.radius * 0.8);
				pos.x += offset.x;
				pos.z += offset.y;

				@yardBuild = construction.buildOrbital(ai.defs.Shipyard, pos);
			}
		}

		if(motherships.length == 1)
			@colonization.colonizeWeightObj = motherships[0].obj;
		else
			@colonization.colonizeWeightObj = null;

		//Idle motherships should be sent to go collect labor from labor planets
		if(laborPlanets.length != 0) {
			for(uint i = 0, cnt = motherships.length; i < cnt; ++i) {
				auto@ flAI = motherships[i];
				if(flAI.mission !is null)
					continue;
				if(isAtLaborPlanet(flAI))
					continue;
				if(i == 0 && idleSince < gameTime-60.0)
					continue;

				double bestDist = INFINITY;
				Planet@ best;
				for(uint n = 0, ncnt = laborPlanets.length; n < ncnt; ++n) {
					Planet@ check = laborPlanets[n];
					if(hasMothershipAt(check))
						continue;

					double d = movement.getPathDistance(flAI.obj.position, check.position);
					if(d < bestDist) {
						@best = check;
						bestDist = d;
					}
				}

				if(best !is null) {
					LaborMission miss;
					@miss.target = best;

					fleets.performMission(flAI, miss);
				}
			}
		}
	}

	bool isAtLaborPlanet(FleetAI& flAI) {
		auto@ miss = cast<LaborMission>(flAI);
		if(miss !is null)
			return true;

		for(uint i = 0, cnt = laborPlanets.length; i < cnt; ++i) {
			if(flAI.obj.isLockedOrbit(laborPlanets[i]))
				return true;
		}
		return false;
	}

	bool isBuildingWithLabor(FleetAI& flAI) {
		auto@ f = construction.get(flAI.obj);
		if(f !is null && f.active !is null)
			return false;
		if(isAtLaborPlanet(flAI))
			return true;
		return false;
	}

	bool hasMothershipAt(Planet& pl) {
		for(uint i = 0, cnt = motherships.length; i < cnt; ++i) {
			auto@ flAI = motherships[i];

			auto@ miss = cast<LaborMission>(flAI);
			if(miss !is null && miss.target is pl)
				return true;

			if(flAI.obj.isLockedOrbit(pl))
				return true;
		}
		return false;
	}

	uint idleMothershipCount() {
		uint count = 0;
		for(uint i = 0, cnt = motherships.length; i < cnt; ++i) {
			if(motherships[i].mission is null)
				count += 1;
		}
		return count;
	}

	bool isColonizing(Planet& dest) {
		for(uint i = 0, cnt = motherships.length; i < cnt; ++i) {
			auto@ flAI = motherships[i];
			auto@ miss = cast<HabitatMission>(flAI.mission);
			if(miss !is null && miss.target is dest)
				return true;
		}
		return false;
	}

	Planet@ colonizeBest(FleetAI& flAI) {
		Planet@ best;
		double bestDist = INFINITY;
		for(uint i = 0, cnt = popRequests.length; i < cnt; ++i) {
			Planet@ dest = popRequests[i];
			if(isColonizing(dest))
				continue;
			if(dest.inCombat)
				continue;

			double d = movement.getPathDistance(flAI.obj.position, dest.position);
			if(d < bestDist) {
				@best = dest;
				bestDist = d;
			}
		}

		if(best is null) {
			for(uint i = 0, cnt = colonization.awaitingSource.length; i < cnt; ++i) {
				Planet@ dest = colonization.awaitingSource[i].target;
				if(isColonizing(dest))
					continue;

				double d = movement.getPathDistance(flAI.obj.position, dest.position);
				if(d < bestDist) {
					@best = dest;
					bestDist = d;
				}
			}
		}

		if(best !is null) {
			HabitatMission miss;
			@miss.target = best;

			fleets.performMission(flAI, miss);
		}
		return best;
	}

	FleetAI@ colonizeBest(Planet& dest) {
		FleetAI@ best;
		double bestDist = INFINITY;
		for(uint i = 0, cnt = motherships.length; i < cnt; ++i) {
			auto@ flAI = motherships[i];
			if(flAI.mission !is null)
				continue;
			if(isBuildingWithLabor(flAI))
				continue;

			double d = movement.getPathDistance(flAI.obj.position, dest.position);
			if(d < bestDist) {
				@best = flAI;
				bestDist = d;
			}
		}

		if(best !is null) {
			HabitatMission miss;
			@miss.target = dest;

			fleets.performMission(best, miss);
		}
		return best;
	}
};

AIComponent@ createStarChildren() {
	return StarChildren();
}

Added scripts/server/empire_ai/weasel/race/Verdant.as.


























































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
import empire_ai.weasel.WeaselAI;
import empire_ai.weasel.race.Race;

import empire_ai.weasel.Designs;
import empire_ai.weasel.Development;
import empire_ai.weasel.Planets;

import buildings;

class Verdant : Race, RaceDesigns {
	Designs@ designs;
	Development@ development;
	Planets@ planets;

	array<const Design@> defaultDesigns;
	array<uint> defaultGoals;

	const SubsystemDef@ sinewSubsystem;
	const SubsystemDef@ supportSinewSubsystem;

	const BuildingType@ stalk;

	void create() override {
		@designs = cast<Designs>(ai.designs);
		@development = cast<Development>(ai.development);
		@planets = cast<Planets>(ai.planets);

		@sinewSubsystem = getSubsystemDef("VerdantSinew");
		@supportSinewSubsystem = getSubsystemDef("VerdantSupportSinew");
		@stalk = getBuildingType("Stalk");
	}

	void start() override {
		ReadLock lock(ai.empire.designMutex);
		for(uint i = 0, cnt = ai.empire.designCount; i < cnt; ++i) {
			const Design@ dsg = ai.empire.getDesign(i);
			if(dsg.newer !is null)
				continue;
			if(dsg.updated !is null)
				continue;

			uint goal = designs.classify(dsg, DP_Unknown);
			if(goal == DP_Unknown)
				continue;

			defaultDesigns.insertLast(dsg);
			defaultGoals.insertLast(goal);
		}
	}

	void save(SaveFile& file) override {
		uint cnt = defaultDesigns.length;
		file << cnt;
		for(uint i = 0; i < cnt; ++i) {
			file << defaultDesigns[i];
			file << defaultGoals[i];
		}
	}

	void load(SaveFile& file) override {
		uint cnt = 0;
		file >> cnt;
		defaultDesigns.length = cnt;
		defaultGoals.length = cnt;
		for(uint i = 0; i < cnt; ++i) {
			file >> defaultDesigns[i];
			file >> defaultGoals[i];
		}
	}

	uint plCheck = 0;
	void focusTick(double time) override {
		if(ai.behavior.forbidConstruction) return;

		//Check if we need to build stalks anywhere
		for(uint i = 0, cnt = development.focuses.length; i < cnt; ++i)
			checkForStalk(development.focuses[i].plAI);

		uint plCnt = planets.planets.length;
		if(plCnt != 0) {
			for(uint n = 0; n < min(plCnt, 5); ++n) {
				plCheck = (plCheck+1) % plCnt;
				auto@ plAI = planets.planets[plCheck];
				checkForStalk(plAI);
			}
		}
	}

	void checkForStalk(PlanetAI@ plAI) {
		if(plAI is null)
			return;
		Planet@ pl = plAI.obj;
		if(pl.pressureCap <= 0 && pl.totalPressure >= 1) {
			if(planets.isBuilding(plAI.obj, stalk))
				return;
			if(pl.getBuildingCount(stalk.id) != 0)
				return;

			planets.requestBuilding(plAI, stalk, expire=180.0);
		}
	}

	bool preCompose(DesignTarget@ target) override {
		return false;
	}

	bool postCompose(DesignTarget@ target) override {
	//	auto@ d = target.designer;

	//	//Add an extra engine
	//	if(target.purpose == DP_Combat)
	//		d.composition.insertAt(0, Exhaust(tag("Engine") & tag("GivesThrust"), 0.25, 0.35));

	//	//Remove armor layers we don't need
	//	for(uint i = 0, cnt = d.composition.length; i < cnt; ++i) {
	//		if(cast<ArmorLayer>(d.composition[i]) !is null) {
	//			d.composition.removeAt(i);
	//			--i; --cnt;
	//		}
	//	}

		return false;
	}

	bool design(DesignTarget@ target, int size, const Design@& output) {
		//All designs are rescales of default designs
		const Design@ baseDesign;
		uint possible = 0;
		for(uint i = 0, cnt = defaultDesigns.length; i < cnt; ++i) {
			if(defaultGoals[i] == target.purpose) {
				possible += 1;
				if(randomd() < 1.0 / double(possible))
					@baseDesign = defaultDesigns[i];
			}
		}

		if(baseDesign is null)
			return false;

		//if(target.designer !is null) {
		//	@target.designer.baseOffDesign = baseDesign;
		//	if(target.purpose != DP_Support)
		//		@target.designer.baseOffSubsystem = sinewSubsystem;
		//	else
		//		@target.designer.baseOffSubsystem = supportSinewSubsystem;
		//	@output = target.designer.design();
		//}

		if(output is null)
			@output = scaleDesign(baseDesign, size);
		return true;
	}
};

AIComponent@ createVerdant() {
	return Verdant();
}

Added scripts/server/empire_ai/weasel/searches.as.








































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
Object@ findEnemy(Region@ region, Empire@ emp, uint empireMask, bool fleets = true, bool stations = true, bool planets = false) {
	array<Object@>@ objs = findInBox(region.position - vec3d(region.radius), region.position + vec3d(region.radius), empireMask);
	uint offset = randomi(0, objs.length-1);
	uint cnt = objs.length;
	for(uint i = 0; i < cnt; ++i) {
		Object@ obj = objs[(i+offset)%cnt];
		Empire@ owner = obj.owner;

		if(!obj.valid) {
			continue;
		}
		else if(owner is null || owner.mask & empireMask == 0) {
			continue;
		}
		else if(emp !is null && !obj.isVisibleTo(emp)) {
			continue;
		}
		else if(obj.region !is region) {
			continue;
		}
		else {
			uint type = obj.type;
			switch(type) {
				case OT_Ship:
					if(!obj.hasLeaderAI)
						continue;
					if(cast<Ship>(obj).isStation) {
						if(!stations)
							continue;
					}
					else {
						if(!fleets)
							continue;
					}
					if(obj.getFleetStrength() < 100.0)
						continue;
				break;
				case OT_Orbital:
					if(!stations)
						continue;
				break;
				case OT_Planet:
					if(!planets)
						continue;
				break;
				default:
					continue;
			}
		}

		return obj;
	}
	return null;
}

array<Object@>@ findEnemies(Region@ region, Empire@ emp, uint empireMask, bool fleets = true, bool stations = true, bool planets = false) {
	array<Object@>@ objs = findInBox(region.position - vec3d(region.radius), region.position + vec3d(region.radius), empireMask);
	array<Object@> outObjs;
	for(int i = objs.length-1; i >= 0; --i) {
		Object@ obj = objs[i];
		Empire@ owner = obj.owner;

		bool remove = false;
		if(!obj.valid) {
			remove = true;
		}
		else if(owner is null || owner.mask & empireMask == 0) {
			remove = true;
		}
		else if(emp !is null && !obj.isVisibleTo(emp)) {
			remove = true;
		}
		else if(obj.region !is region) {
			remove = true;
		}
		else {
			uint type = obj.type;
			switch(type) {
				case OT_Ship:
					if(!obj.hasLeaderAI)
						remove = true;
					if(cast<Ship>(obj).isStation) {
						if(!stations)
							remove = true;
					}
					else {
						if(!fleets)
							remove = true;
					}
					if(obj.getFleetStrength() < 100.0)
						remove = true;
				break;
				case OT_Orbital:
					if(!stations)
						remove = true;
				break;
				case OT_Planet:
					if(!planets)
						remove = true;
				break;
				default:
					remove = true;
			}
		}

		if(!remove)
			outObjs.insertLast(obj);
	}
	return outObjs;
}

array<Object@>@ findType(Region@ region, Empire@ emp, uint objectType, uint empireMask = ~0) {
	// Specialized for safe object buckets
	array<Object@>@ objs;
	DataList@ data;
	switch(objectType)
	{
		case OT_Planet:
			@data = region.getPlanets();
		break;
		case OT_Pickup:
			@data = region.getPickups();
		break;
		case OT_Anomaly:
			@data = region.getAnomalies();
		break;
		case OT_Artifact:
			@data = region.getArtifacts();
		break;
		case OT_Asteroid:
			@data = region.getAsteroids();
		break;
	}

	if(data !is null)
	{
		@objs = array<Object@>();
		Object@ obj;
		while(receive(data, obj)) {
			if(obj !is null)
				objs.insertLast(obj);
		}
	}
	else {
		// No object bucket retrieval mechanism, do a full physics search
		@objs = findInBox(region.position - vec3d(region.radius), region.position + vec3d(region.radius), empireMask);
	}

	// Generic search using physics system
	array<Object@> outObjs;
	for(int i = objs.length-1; i >= 0; --i) {
		Object@ obj = objs[i];
		Empire@ owner = obj.owner;

		bool remove = false;
		if(!obj.valid) {
			remove = true;
		}
		else if(owner is null || owner.mask & empireMask == 0) {
			remove = true;
		}
		else if(emp !is null && !obj.isVisibleTo(emp)) {
			remove = true;
		}
		else if(obj.region !is region) {
			remove = true;
		}
		else {
			uint type = obj.type;
			if(type != objectType)
				remove = true;
		}

		if(!remove)
			outObjs.insertLast(obj);
	}
	return outObjs;
}

array<Object@>@ findAll(Region@ region, uint empireMask = ~0) {
	return findInBox(region.position - vec3d(region.radius), region.position + vec3d(region.radius), empireMask);
}

double getTotalFleetStrength(Region@ region, uint empireMask, bool fleets = true, bool stations = true, bool planets = true) {
	auto@ objs = findAll(region, empireMask);
	double str = 0.0;
	for(uint i = 0, cnt = objs.length; i < cnt; ++i) {
		Object@ obj = objs[i];
		Empire@ owner = obj.owner;
		if(!obj.valid)
			continue;
		if(owner is null || owner.mask & empireMask == 0)
			continue;
		if(obj.region !is region)
			continue;

		uint type = obj.type;
		switch(type) {
			case OT_Ship:
				if(!obj.hasLeaderAI)
					continue;
				if(cast<Ship>(obj).isStation) {
					if(!stations)
						continue;
				}
				else {
					if(!fleets)
						continue;
				}
				if(obj.getFleetStrength() < 100.0)
					continue;
			break;
			case OT_Orbital:
				if(!stations)
					continue;
			break;
			case OT_Planet:
				if(!planets)
					continue;
			break;
			default:
				continue;
		}

		str += sqrt(obj.getFleetStrength());
	}
	return str * str;
}

Added scripts/server/game_start.as.


























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
#priority init 1000
#priority sync 10
import empire_ai.EmpireAI;
import settings.map_lib;
import settings.game_settings;
import maps;
import map_systems;
import regions.regions;
import artifacts;
from map_generation import generatedSystems, generatedGalaxyGas, GasData;
from empire import Creeps, majorEmpireCount, initEmpireDesigns, sendChatMessage;

import void createWormhole(SystemDesc@ from, SystemDesc@ to) from "objects.Oddity";
import Artifact@ makeArtifact(SystemDesc@ system, uint type = uint(-1)) from "map_effects";

//Galaxy positioning
Map@[] galaxies;

vec3d mapLeft;
vec3d mapRight;
double galaxyRadius = 0;

const double GALAXY_MIN_SPACING = 60000.0;
const double GALAXY_MAX_SPACING = 120000.0;
const double GALAXY_HEIGHT_MARGIN = 50000.0;

bool overlaps(Map@ from, vec3d point, Map@ to) {
	return point.distanceTo(from.origin) < GALAXY_MIN_SPACING + from.radius + to.radius;
}

//Homeworld searches
class HomeworldSearch {
	ScriptThread@ thread;
	vec3d goal;
	SystemData@ result;
	Map@ map;
	Empire@ emp;
};

double findHomeworld(double time, ScriptThread& thread) {
	HomeworldSearch@ search;
	thread.getObject(@search);
	
	@search.result = search.map.findHomeworld(search.emp, search.goal);
	thread.stop();
	return 0;
}

class QualityCalculation {
	array<Map@> galaxies;
	array<SystemData@>@ homeworlds;
}

void calculateQuality(QualityCalculation@ data) {
	uint homeworldCount = data.homeworlds.length;
	array<double> dists(homeworldCount);
	
	for(uint g = 0, gcnt = data.galaxies.length; g < gcnt; ++g) {
		Map@ mp = data.galaxies[g];
		mp.calculateHomeworldDistances();
		
		for(uint i = 0, end = mp.systemData.length; i < end; ++i) {
			SystemData@ system = mp.systemData[i];
			mp.calculateQuality(system, data.homeworlds, dists);
		}
	}
}

void init() {
	soundScale = 500.f;
	if(isLoadedSave)
		return;

	double start = getExactTime(), end = start;
	uint hwGalaxies = 0;

	//Create galaxy map instances
	for(uint i = 0, cnt = gameSettings.galaxies.length; i < cnt; ++i) {
		Map@ desc = getMap(gameSettings.galaxies[i].map_id);

		if(desc !is null) {
			for(uint n = 0; n < gameSettings.galaxies[i].galaxyCount; ++n) {
				Map@ mp = cast<Map>(desc.create());
				@mp.settings = gameSettings.galaxies[i];
				mp.allowHomeworlds = gameSettings.galaxies[i].allowHomeworlds;
				if(mp.allowHomeworlds)
					hwGalaxies += 1;

				galaxies.insertLast(mp);
			}
		}
		else {
			error("Error: Could not find map "+gameSettings.galaxies[i].map_id);
		}
	}

	if(galaxies.length == 0) {
		auto@ _map = cast<Map>(getMap("Spiral.SpiralMap").create());
		@_map.settings = MapSettings();
		galaxies.insertLast(_map);
	}

	if(hwGalaxies == 0) {
		hwGalaxies += 1;
		galaxies[0].allowHomeworlds = true;
	}

	//Place all the systems
	for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) {
		galaxies[i].preInit();
		if(galaxies[i].allowHomeworlds)
			galaxies[i].estPlayerCount = ceil(double(majorEmpireCount) / double(hwGalaxies));
		else
			galaxies[i].estPlayerCount = 0;
		galaxies[i].universePlayerCount = majorEmpireCount;
		galaxies[i].preGenerate();
	}

	//Place the galaxies
	for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) {
		vec3d origin;

		if(i != 0) {
			double startRad = galaxies[0].radius + galaxies[i].radius + GALAXY_MIN_SPACING;
			double endRad = startRad - GALAXY_MIN_SPACING + GALAXY_MAX_SPACING;

			bool overlap = false;
			do {
				vec2d pos = random2d(startRad, endRad);

				origin = vec3d(pos.x, randomd(-GALAXY_HEIGHT_MARGIN, GALAXY_HEIGHT_MARGIN), pos.y);
				overlap = false;

				for(uint j = 0; j < i; ++j) {
					if(overlaps(galaxies[j], origin, galaxies[i])) {
						overlap = true;
						endRad += GALAXY_MIN_SPACING;
						break;
					}
				}
			}
			while(overlap);
		}

		galaxies[i].setOrigin(origin);
		galaxyRadius = max(galaxyRadius, origin.length + galaxies[i].radius * 1.4);
	}

	//Search for homeworld starting positions in multiple threads (one per empire)
	array<SystemData@> globalHomeworlds;
	{	
		array<TeamSorter> sortedEmps;
		for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
			Empire@ emp = getEmpire(i);
			if(!emp.major)
				continue;
			sortedEmps.insertLast(TeamSorter(emp));
		}
		sortedEmps.sortAsc();

		array<HomeworldSearch> homeworlds(sortedEmps.length);
		uint mapCnt = galaxies.length;
		uint mapN = randomi(0, mapCnt - 1), mapC = 0;

		for(uint i = 0; i < homeworlds.length; ++i) {
			HomeworldSearch@ search = homeworlds[i];
			Empire@ emp = sortedEmps[i].emp;

			//Find a galaxy willing to host this empire
			uint j = 0;
			do {
				@search.map = galaxies[(mapN + mapC) % mapCnt];
				++mapC;
				++j;
			}
			while((!search.map.allowHomeworlds || !search.map.canHaveHomeworld(emp)) && j < mapCnt);

			if(mapC >= mapCnt) {
				mapN = randomi(0, mapCnt - 1);
				mapC = 0;
			}

			//Suggested place for this empire
			double angle = double(i) * twopi / double(majorEmpireCount);
			double rad = search.map.radius * 0.8;
			search.goal = vec3d(rad * cos(angle), 0, rad * sin(angle));
			search.goal += search.map.origin;

			//Start the search
			@search.emp = emp;
			if(search.map.possibleHomeworlds.length == 0)
				@search.thread = ScriptThread("game_start::findHomeworld", @search);
			else
				@search.result = search.map.findHomeworld(search.emp, search.goal);
		}
		
		for(uint i = 0; i < homeworlds.length; ++i) {
			HomeworldSearch@ search = homeworlds[i];
			while(search.thread !is null && search.thread.running) sleep(0);
			if(search.result !is null) {
				search.result.addHomeworld(search.emp);
				search.map.markHomeworld(search.result);
			}
			globalHomeworlds.insertLast(search.result);
		}
	}

	//Calculate system quality in threads
	{
		array<QualityCalculation> calcs(6);
		for(uint i = 0, cnt = galaxies.length; i < cnt; ++i)
			galaxies[i].calculateGalaxyQuality(globalHomeworlds);

		uint n = 0, step = int(ceil(double(galaxies.length) / double(calcs.length)));
		for(uint i = 0; i < calcs.length; ++i) {
			QualityCalculation@ calc = calcs[i];
			@calc.homeworlds = @globalHomeworlds;
			for(uint j = 0; j < step && n < galaxies.length; ++j) {
				calc.galaxies.insertLast(galaxies[n]);
				n += 1;
			}
			calculateQuality(calc);
		}	
	}

	//Generate physics
	double gridSize = max(modSpacing(7500.0), (galaxyRadius * 2.0) / 150.0);
	int gridAmount = (galaxyRadius * 2.0) / gridSize;
	setupPhysics(gridSize, gridSize / 8.0, gridAmount);

	//Generate region objects
	for(uint i = 0, cnt = galaxies.length; i < cnt; ++i)
		galaxies[i].generateRegions();
	for(uint i = 0, cnt = generatedSystems.length; i < cnt; ++i)
		generatedSystems[i].object.finalizeCreation();

	//Actually generate maps
	for(uint i = 0, cnt = galaxies.length; i < cnt; ++i)
		galaxies[i].generate();

	//Regenerate the region lookup tree with the actual sizes
	regenerateRegionGroups();

	//Generate wormholes in case of multiple galaxies
	if(galaxies.length > 1) {
		uint totalSystems = 0;
		for(uint i = 0, cnt = galaxies.length; i < cnt; ++i)
			totalSystems += galaxies[i].systems.length;
		uint wormholes = max(config::GALAXY_MIN_WORMHOLES * galaxies.length,
				totalSystems / config::SYSTEMS_PER_WORMHOLE);
		if(wormholes % 2 != 0)
			wormholes += 1;
		uint generated = 0;

		for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) {
			auto@ glx = galaxies[i];

			//Figure out how many wormhole endpoints this galaxy should have
			double pct = double(glx.systems.length) / double(totalSystems);
			uint amount = max(uint(config::GALAXY_MIN_WORMHOLES), uint(round(pct * wormholes)));

			//Tell the galaxy to distribute them
			glx.placeWormholes(amount);

			generated += amount;
		}

		//Make a circle of wormhole endpoints
		for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) {
			auto@ glx = galaxies[i];
			auto@ nextGlx = galaxies[(i+1)%cnt];

			auto@ from = glx.getWormhole();
			auto@ to = nextGlx.getWormhole();
			if(from is null || to is null)
				continue;

			createWormhole(from, to);
			glx.addWormhole(from, to);
			nextGlx.addWormhole(to, from);
		}

		//Randomly spread the remaining wormholes
		for(uint i = 0, cnt = galaxies.length; i < cnt; ++i) {
			auto@ glx = galaxies[i], otherGlx;
			SystemDesc@ hole = glx.getWormhole();
			SystemDesc@ other;
			while(hole !is null) {
				uint index = randomi(0, cnt - 1);
				for(uint n = 0; n < cnt; ++n) {
					@otherGlx = galaxies[n];
					@other = otherGlx.getWormhole();

					if(other !is null)
						break;
				}

				if(other !is null) {
					createWormhole(hole, other);
					glx.addWormhole(hole, other);
					otherGlx.addWormhole(other, hole);
				}

				@hole = glx.getWormhole();
				@other = null;
				@otherGlx = null;
			}
		}
	}

	end = getExactTime();
	info("Map generation: "+toString((end - start)*1000,1)+"ms");
	start = end;

	end = getExactTime();
	info("Link generation: "+toString((end - start)*1000,1)+"ms");
	start = end;

	//Deal with generating unique spread artifacts
	if(generatedSystems.length > 1 && config::ENABLE_UNIQUE_SPREADS != 0) {
		for(uint i = 0, cnt = getArtifactTypeCount(); i < cnt; ++i) {
			auto@ type = getArtifactType(i);
			if(type.spreadVariable.length == 0)
				continue;
			if(config::get(type.spreadVariable) <= 0.0)
				continue;

			SystemDesc@ sys;
			if(type.requireContestation > 0)
				@sys = getRandomSystemAboveContestation(type.requireContestation);
			if(sys is null)
				@sys = getRandomSystem();

			if(sys !is null)
				makeArtifact(sys, type.id);
		}
	}

	//Initialization for map code
	for(uint i = 0, cnt = galaxies.length; i < cnt; ++i)
		galaxies[i].initDefs();
	for(uint i = 0, cnt = galaxies.length; i < cnt; ++i)
		galaxies[i].init();

	//Explore entire map if indicated
	if(config::START_EXPLORED_MAP != 0.0) {
		for(uint i = 0, cnt = systemCount; i < cnt; ++i)
			getSystem(i).object.ExploredMask = int(~0);
	}

	//Assign already connected players to empires
	{
		if(playerEmpire !is null && playerEmpire.valid)
			CURRENT_PLAYER.linkEmpire(playerEmpire);
		uint empInd = 0, empCnt = getEmpireCount();
		array<Player@>@ players = getPlayers();

		//First pass: players into player empires
		for(uint i = 0, plCnt = players.length; i < plCnt && empInd < empCnt; ++i) {
			Player@ pl = players[i];
			connectedPlayers.insertLast(pl);
			connectedSet.insert(pl.id);
			if(pl.emp is null) {
				for(; empInd < empCnt; ++empInd) {
					Empire@ emp = getEmpire(empInd);
					if(!emp.major)
						continue;
					if(emp.player !is null)
						continue;
					//if(emp.getAIType() != ET_Player)
					//	continue;

					pl.linkEmpire(emp);
					++empInd;
					break;
				}
			}
		}

		//Second pass: take over AIs
		empInd = 0;
		for(uint i = 0, plCnt = players.length; i < plCnt && empInd < empCnt; ++i) {
			Player@ pl = players[i];
			if(pl.emp is null) {
				for(; empInd < empCnt; ++empInd) {
					Empire@ emp = getEmpire(empInd);
					if(!emp.major)
						continue;
					if(emp.player !is null)
						continue;

					pl.linkEmpire(emp);
					if(pl.name.length != 0)
						emp.name = pl.name;
					++empInd;
					break;
				}
			}
		}
	}
}

class TeamSorter {
	Empire@ emp;
	TeamSorter() {}
	TeamSorter(Empire@ empire) {
		@emp = empire;
	}

	int opCmp(const TeamSorter& other) const {
		if(emp.team == -1) {
			if(other.emp.team == -1)
				return 0;
			return 1;
		}
		if(other.emp.team == -1)
			return -1;
		if(emp.team > other.emp.team)
			return 1;
		if(emp.team < other.emp.team)
			return 1;
		return 0;
	}
};

uint get_systemCount() {
	return generatedSystems.length;
}

SystemDesc@ getSystem(uint index) {
	if(index >= generatedSystems.length)
		return null;
	return generatedSystems[index];
}

SystemDesc@ getSystem(Region@ region) {
	if(region is null || region.SystemId == -1)
		return null;
	return generatedSystems[region.SystemId];
}

SystemDesc@ getSystem(const string& name) {
	//TODO: Use dictionary
	uint cnt = systemCount;
	for(uint i = 0; i < cnt; ++i) {
		if(getSystem(i).name == name)
			return getSystem(i);
	}
	return null;
}

SystemDesc@ getRandomSystem() {
	return generatedSystems[randomi(0, generatedSystems.length-1)];
}

SystemDesc@ getRandomSystemAboveContestation(double contest) {
	double roll = randomd();
	double total = 0.0;
	SystemDesc@ chosen;
	for(uint i = 0, cnt = generatedSystems.length; i < cnt; ++i) {
		auto@ sys = generatedSystems[i];
		if(sys.contestation < contest)
			continue;

		total += 1.0;
		double chance = 1.0 / total;
		if(roll < chance) {
			@chosen = sys;
			roll /= chance;
		}
		else {
			roll = (roll - chance) / (1.0 - chance);
		}
	}
	return chosen;
}

SystemDesc@ getClosestSystem(const vec3d& point) {
	SystemDesc@ closest;
	double dist = INFINITY;
	for(uint i = 0, cnt = generatedSystems.length; i < cnt; ++i) {
		double d = generatedSystems[i].position.distanceToSQ(point);
		if(d < dist) {
			dist = d;
			@closest = generatedSystems[i];
		}
	}
	return closest;
}

void syncInitial(Message& msg) {
	uint cnt = generatedSystems.length;
	msg << cnt;
	for(uint i = 0; i < cnt; ++i)
		generatedSystems[i].write(msg);

	cnt = galaxies.length;
	msg << cnt;
	for(uint i = 0; i < cnt; ++i)
		msg << galaxies[i].id;
	
	cnt = generatedGalaxyGas.length;
	msg << cnt;
	for(uint i = 0; i < cnt; ++i) {
		GasData@ gas = generatedGalaxyGas[i];
		
		msg.writeSmallVec3(gas.position);
		msg << float(gas.scale);
		
		if(gas.gdat.cullingNode !is null) {
			msg.write1();
			msg.writeSmallVec3(gas.gdat.cullingNode.position);
			msg << float(gas.gdat.cullingNode.scale);
		}
		else {
			msg.write0();
		}
		
		uint sCnt = gas.sprites.length;
		msg.writeSmall(sCnt);
		for(uint s = 0; s < sCnt; ++s) {
			GasSprite@ sprite = gas.sprites[s];
			msg.writeSmallVec3(sprite.pos);
			msg << float(sprite.scale);
			msg << sprite.color;
			msg.writeBit(sprite.structured);
		}
	}
}

bool doSystemSync = false;
bool sendPeriodic(Message& msg) {
	if(!doSystemSync)
		return false;

	doSystemSync = false;
	uint cnt = generatedSystems.length;
	msg << cnt;
	for(uint i = 0; i < cnt; ++i)
		generatedSystems[i].write(msg);
	return true;
}

array<Player@> connectedPlayers;
set_int connectedSet;
double timer = 0.0;
void tick(double time) {
	for(uint i = 0, cnt = galaxies.length; i < cnt; ++i)
		galaxies[i].tick(time);

	timer += time;
	if(timer >= 1.0) {
		timer = 0.0;
		array<Player@>@ players = getPlayers();

		//Send connect events
		for(uint i = 0, cnt = players.length; i < cnt; ++i) {
			Player@ pl = players[i];
			string name = pl.name;
			if(name.length == 0)
				continue;
			if(!connectedSet.contains(pl.id)) {
				string msg = format("[color=#aaa]"+locale::MP_CONNECT_EVENT+"[/color]", 
					format("[b]$1[/b]", bbescape(name)));
				sendChatMessage(msg, offset=30);
				connectedPlayers.insertLast(pl);
				connectedSet.insert(pl.id);
			}
		}

		connectedSet.clear();
		for(uint i = 0, cnt = players.length; i < cnt; ++i)
			connectedSet.insert(players[i].id);

		//Send disconnect events
		for(uint i = 0, cnt = connectedPlayers.length; i < cnt; ++i) {
			if(!connectedSet.contains(connectedPlayers[i].id)) {
				Color color;
				string name = connectedPlayers[i].name;
				Empire@ emp = connectedPlayers[i].emp;
				if(emp !is null)
					color = emp.color;

				string msg = format("[color=#aaa]"+locale::MP_DISCONNECT_EVENT+"[/color]", 
					format("[b][color=$1]$2[/color][/b]", toString(color), bbescape(name)));
				sendChatMessage(msg, offset=30);
				connectedPlayers.removeAt(i);
				--i; --cnt;
			}
		}
	}
}

void getSystems() {
	uint cnt = generatedSystems.length;
	for(uint i = 0; i < cnt; ++i)
		yield(generatedSystems[i]);
}

void generateNewSystem(const vec3d& pos, double radius, const string& name = "", bool makeLinks = true) {
	generateNewSystem(pos, radius, null, name, makeLinks);
}

void generateNewSystem(const vec3d& pos, double radius, SystemGenerateHook@ hook, const string& name = "", bool makeLinks = true, const string& type = "") {
	//Because things access the generated systems list from outside of a locked context for performance, and
	//creating new systems is a very very rare thing, we just use an isolation hook here, which pauses the
	//execution of the entire game, runs the hook, then resumes.
	SystemGenerator sys;
	sys.position = pos;
	sys.radius = radius;
	sys.makeLinks = makeLinks;
	sys.makeType = type;
	sys.name = name;
	@sys.hook = hook;
	isolate_run(sys);
}

interface SystemGenerateHook {
	void call(SystemDesc@ desc);
}

class SystemGenerator : IsolateHook {
	vec3d position;
	double radius;
	string name;
	SystemGenerateHook@ hook;
	bool makeLinks = true;
	string makeType;

	void call() {
		if(name.length == 0) {
			NameGenerator sysNames;
			sysNames.read("data/system_names.txt");
			name = sysNames.generate();
		}

		ObjectDesc sysDesc;
		sysDesc.type = OT_Region;
		sysDesc.name = name;
		sysDesc.flags |= objNoPhysics;
		sysDesc.flags |= objNoDamage;
		sysDesc.delayedCreation = true;
		sysDesc.position = position;

		Region@ region = cast<Region>(makeObject(sysDesc));
		region.alwaysVisible = true;
		region.InnerRadius = radius / 1.5;
		region.OuterRadius = radius;
		region.radius = region.OuterRadius;

		SystemData dat;
		dat.index = generatedSystems.length;
		dat.position = position;
		dat.quality = 100;
		@dat.systemCode = SystemCode();

		SystemDesc desc;
		desc.index = generatedSystems.length;
		region.SystemId = desc.index;
		desc.name = region.name;
		desc.position = position;
		desc.radius = region.OuterRadius;
		@desc.object = region;

		generatedSystems.insertLast(desc);
		addRegion(desc.object);

		region.finalizeCreation();

		//Run the type
		auto@ sysType = getSystemType(makeType);
		if(sysType !is null) {
			dat.systemType = sysType.id;

			sysType.generate(dat, desc);

			region.InnerRadius = desc.radius;
			region.OuterRadius = desc.radius * 1.5;
			region.radius = region.OuterRadius;
			desc.radius = region.OuterRadius;

			sysType.postGenerate(dat, desc);

			MapGeneration gen;
			gen.finalizeSystem(dat, desc);
		}

		//Make trade lines to nearby systems
		if(makeLinks) {
			SystemDesc@ closest;
			array<SystemDesc@> nearby;
			double closestDist = INFINITY;
			for(uint i = 0, cnt = generatedSystems.length; i < cnt; ++i) {
				double d = generatedSystems[i].position.distanceTo(desc.position);
				if(generatedSystems[i] is desc)
					continue;
				if(d < 13000.0)
					nearby.insertLast(generatedSystems[i]);
				if(d < closestDist) {
					closestDist = d;
					@closest = generatedSystems[i];
				}
			}

			if(nearby.length == 0) {
				closest.adjacent.insertLast(desc.index);
				closest.adjacentDist.insertLast(closest.position.distanceTo(desc.position));
				desc.adjacent.insertLast(closest.index);
				desc.adjacentDist.insertLast(closest.position.distanceTo(desc.position));
			}
			else {
				for(uint i = 0, cnt = nearby.length; i < cnt; ++i) {
					nearby[i].adjacent.insertLast(desc.index);
					nearby[i].adjacentDist.insertLast(nearby[i].position.distanceTo(desc.position));
					desc.adjacent.insertLast(nearby[i].index);
					desc.adjacentDist.insertLast(nearby[i].position.distanceTo(desc.position));
				}
			}

			if(desc.adjacent.length == 0 || config::START_EXPLORED_MAP != 0.0) {
				desc.object.ExploredMask.value = int(~0);
			}
			else {
				for(uint i = 0, cnt = desc.adjacent.length; i < cnt; ++i)
					desc.object.ExploredMask |= getSystem(desc.adjacent[i]).object.SeenMask;
			}
		}

		//Create the system node
		Node@ snode = bindCullingNode(region, desc.position, 1000.0);
		snode.scale = region.radius + 128.0;
		snode.rebuildTransform();

		calcGalaxyExtents();

		if(hook !is null)
			hook.call(desc);

		//Notify clients of changes
		refreshClientSystems(CURRENT_PLAYER);
		doSystemSync = true;
	}
};

void save(SaveFile& data) {
	data << uint(generatedSystems.length);
	for(uint i = 0; i < generatedSystems.length; ++i)
		generatedSystems[i].save(data);
	data << uint(generatedGalaxies.length);
	for(uint i = 0; i < generatedGalaxies.length; ++i)
		generatedGalaxies[i].save(data);
	data << uint(generatedGalaxyGas.length);
	for(uint i = 0; i < generatedGalaxyGas.length; ++i)
		generatedGalaxyGas[i].save(data);
	data << uint(galaxies.length);
	for(uint i = 0; i < galaxies.length; ++i) {
		data << galaxies[i].id;
		galaxies[i].save(data);
	}
}

void load(SaveFile& data) {
	uint count = 0;
	data >> count;
	generatedSystems.length = count;
	for(uint i = 0; i < generatedSystems.length; ++i) {
		SystemDesc desc;
		desc.load(data);
		@generatedSystems[i] = desc;
	}

	data >> count;
	generatedGalaxies.length = count;
	for(uint i = 0; i < count; ++i) {
		@generatedGalaxies[i] = GalaxyData();
		generatedGalaxies[i].load(data);
	}

	data >> count;
	generatedGalaxyGas.length = count;
	for(uint i = 0; i < count; ++i) {
		@generatedGalaxyGas[i] = GasData();
		generatedGalaxyGas[i].load(data);
	}

	if(data >= SV_0040) {
		data >> count;
		galaxies.length = count;

		for(uint i = 0; i < galaxies.length; ++i) {
			string ident;
			data >> ident;
			@galaxies[i] = getMap(ident).create();
			galaxies[i].load(data);
		}
	}
}