File size: 29,300 Bytes
b6a38d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
DefineClass.ExitZoneInteractable = {
	__parents = { "EditorVisibleObject", "Interactable", "Object", "GridMarker" },
	properties = {
		{ category = "Enabled Logic", editor = "bool", id = "HideVisualWhenDisabled", default = false },
		{ category = "Travel", editor = "combo", id = "SectorOverride", items = function() return GetCampaignSectorsCombo() end, default = false },
		{ category = "Travel", editor = "bool", id = "IsUnderground", name = "IsUndergroundExit", default = false },
		{ category = "Travel", editor = "bool", id = "RetreatInConflictOnlyIfCameFromHere", default = false },
		{ category = "Travel", editor = "choice", name = "Entity", id = "entity", items = function() return table.get(Presets, "EntityVariation", "Default", "TravelObject", "Entities") or { "UITravelObject_01" } end, default = "UITravelObject_01" },
	},

	-- Change in GridMarkerType editor
	AreaWidth = 5,
	AreaHeight = 5,
	
	BadgePosition = "average",
	Type = "ExitZoneInteractable",
	
	fake_visual_obj = false, -- Object to be used when one isn't explicitly placed
	discovered = true
}

function ExitZoneInteractable:InitFakeVO()
	if not self.fake_visual_obj then
		local obj = PlaceObject(self.entity)
		obj:SetColorizationPalette(g_DefaultColorsPalette)
		obj:SetEnumFlags(const.efSelectable)
		obj:ClearGameFlags(const.gofPermanent)
		obj:SetCollision(false)
		obj.spawner = self
		self.fake_visual_obj = obj
		if self.visuals_cache then
			self.visuals_cache[#self.visuals_cache + 1] = self.fake_visual_obj
		end
	end
end

function ExitZoneInteractable:GameInit()
	self:InitFakeVO()
	self:EvaluateNeedForFakeVisual()
end

function ExitZoneInteractable:Done()
	if IsValid(self.fake_visual_obj) then
		DoneObject(self.fake_visual_obj)
		self.fake_visual_obj = false
	end
end

function ExitZoneInteractable:Setentity(value)
	self:ChangeEntity(value)
	if IsValid(self.fake_visual_obj) then
		self.fake_visual_obj:ChangeEntity(value)
		self.fake_visual_obj:SetColorizationPalette(g_DefaultColorsPalette)
	end
	self:SetColorizationPalette(g_DefaultColorsPalette)
	self.entity = value
end

function ExitZoneInteractable:PopulateVisualCache()
	Interactable.PopulateVisualCache(self)
	table.remove_value(self.visuals_cache, self) -- ExitZoneInteractable is never its own visual.
	
	local visuals = self.visuals_cache
	for i, obj in ipairs(visuals) do
		if IsKindOf(obj, "Interactable") then
			obj.visual_of_interactable = self
		end
	end
	
--[[	if IsValid(self.fake_visual_obj) then
		self.visuals_cache[#self.visuals_cache + 1] = self.fake_visual_obj
	end]]
end

local function lUpdateVisualsOfExitZoneInteractables()
	FireNetSyncEventOnHost("UpdateVisualsOfExitZoneInteractables")
end

function NetSyncEvents.UpdateVisualsOfExitZoneInteractables()
	local sector = gv_Sectors[gv_CurrentSectorId]
	local inConflict = sector and sector.conflict
	
	MapForEach("map", "ExitZoneInteractable", function(o)
		 o:EvaluateNeedForFakeVisual()
		 
		 if o.RetreatInConflictOnlyIfCameFromHere and inConflict then return end
		 
		 if o:IsMarkerEnabled() then
			 -- Mark all sectors that can be accessed from this sector as discovered
			 local nextSector = o:GetNextSector()
			 if nextSector then
				nextSector.discovered = true
			 end
		 end
	end)
end

OnMsg.ExplorationStart = lUpdateVisualsOfExitZoneInteractables
OnMsg.DeploymentStarted = lUpdateVisualsOfExitZoneInteractables
OnMsg.CombatEnd = lUpdateVisualsOfExitZoneInteractables

function ExitZoneInteractable:EvaluateNeedForFakeVisual()
	self:InitFakeVO()

	local visualObjects = ResolveInteractableVisualObjects(self)
	local shouldHave
	if #visualObjects == 0 then
		shouldHave = true
	elseif #visualObjects <= 2 then
		local o1, o2 = visualObjects[1], visualObjects[2]
		if (not o1 or o1 == self or o1 == self.fake_visual_obj) and
			(not o2 or o2 == self or o2 == self.fake_visual_obj) then
			shouldHave = true
		end
	end

	if shouldHave and self.HideVisualWhenDisabled and not self:IsMarkerEnabled() then
		shouldHave = false
	end
	
	local nextSector = self:GetNextSector()
	if not nextSector then
		shouldHave = false
	end
	
	shouldHave = shouldHave or IsEditorActive()
	if shouldHave then
		if self.visuals_cache and not table.find(self.visuals_cache, self.fake_visual_obj) then
			self.visuals_cache[#self.visuals_cache + 1] = self.fake_visual_obj
		end
		self.fake_visual_obj:SetEnumFlags(const.efVisible)
		self.fake_visual_obj:SetCollision(true)
	else
		if self.visuals_cache then
			table.remove_value(self.visuals_cache, self.fake_visual_obj)
		end
		self.fake_visual_obj:ClearEnumFlags(const.efVisible)
		self.fake_visual_obj:SetCollision(false)
	end
	
	local dirs = {
		point(const.SlabSizeX, 0, 0),
		point(const.SlabSizeX, 0, 0),
		point(0, const.SlabSizeY, 0),
		point(0, -const.SlabSizeY, 0)
	}
	
	local spotForFakeInteractable = false
	for i, d in ipairs(dirs) do
		local voxel = self:GetPos() + d
		local bbox = GetVoxelBBox(voxel, false, true)
		local boxHasZ = bbox:minz()
		local any = MapGetFirst(bbox, "GridMarker", function(obj) -- NOTE: enumerating in the voxel may be faster than all GridMarkers
			if not boxHasZ then return true end
			
			local x, y, z = obj:GetPosXYZ()
			if not z then z = terrain.GetHeight(x, y) end

			return bbox:PointInside(x, y, z)
		end)
		if not any then
			spotForFakeInteractable = voxel
			break
		end
	end
	
	self.fake_visual_obj:SetPos(spotForFakeInteractable)
	self.fake_visual_obj:SetAngle(self:GetAngle())
end

function ExitZoneInteractable:EditorEnter()
	EditorVisibleObject.EditorEnter(self)
	self:EvaluateNeedForFakeVisual()
end

function ExitZoneInteractable:EditorExit()
	EditorVisibleObject.EditorExit(self)
	Interactable.EditorExit(self)
	self:EvaluateNeedForFakeVisual()
end

function ExitZoneInteractable:EditorCallbackMove()
	self:EvaluateNeedForFakeVisual()
end

function ExitZoneInteractable:EditorCallbackRotate()
	self:EvaluateNeedForFakeVisual()
end

function ExitZoneInteractable:EditorCallbackPlace()

end

function ExitZoneInteractable:GetNextSector()
	if not gv_CurrentSectorId then return false end

	local sectorId, underground = false, false
	if self:IsUndergroundExit() then
		sectorId = gv_Sectors[gv_CurrentSectorId].GroundSector or (gv_CurrentSectorId .. "_Underground")
		underground = self.Groups[1]
	else
		local selfIsUnderground = not not gv_Sectors[gv_CurrentSectorId].GroundSector
		
		for _, dir in ipairs(const.WorldDirections) do
			if self:IsInGroup(dir) then
				local neighSectorId = GetNeighborSector(gv_CurrentSectorId, dir)
				
				-- Underground sectors exits in world directions can only lead to
				-- other underground sectors
				if selfIsUnderground and not IsSectorUnderground(neighSectorId) then
					neighSectorId = false
				end
				
				if neighSectorId and
					not IsTravelBlocked(gv_CurrentSectorId, neighSectorId) and
					not GetDirectionProperty(neighSectorId, gv_CurrentSectorId, "BlockTravelRiver") and
					gv_Sectors[neighSectorId].Map then
					
					sectorId = neighSectorId
					break
				end
			end
		end
	end
	
	if self.SectorOverride then
		sectorId = self.SectorOverride
	end

	return gv_Sectors[sectorId], underground
end

function ExitZoneInteractable:IsUndergroundExit()
	return self:IsInGroup("Underground") or self.IsUnderground
end

function ExitZoneInteractable:BadgeTextUpdate()
	local withCursor = table.find(self.highlight_reasons, "cursor")
	local badgeInstance = self.interactable_badge
	if not badgeInstance or badgeInstance.ui.window_state == "destroying" then return end
	
	if IsUnitPartOfAnyActiveBanter(self) then
		badgeInstance.ui.idText:SetVisible(false)
		return
	end
	
	local unit = UIFindInteractWith(self)
	if unit then	
		local currentSect = gv_Sectors[gv_CurrentSectorId]
		if not currentSect then return end

		local nextSect, underground = self:GetNextSector()
		local nextMapName = nextSect and GetSectorText(nextSect)
		local action = self:GetInteractionCombatAction(unit)
		badgeInstance.ui.idText:SetContext(unit)
		
		local text = action:GetActionDisplayName({unit, self})
		if g_TestExploration then
			text = Untranslated("Cant Travel in Exploration Test")
		elseif underground then
			if IsSectorUnderground(gv_CurrentSectorId) then
				text = T(705526346094, "Exit")
			else
				text = T(749506366915, "Go Underground")
			end
		elseif currentSect.conflict then
			text = T(482029101969, "Retreat To <Map>")
		else
			text = T(500843659226, "Exit To <Map>")
		end

		badgeInstance.ui.idText:SetText(T{text, Map = nextMapName})
	end
	badgeInstance.ui.idText:SetVisible(withCursor)
end

function ExitZoneInteractable:GetInteractionCombatAction(unit)
	if not self:IsMarkerEnabled() then return false end
	if gv_DeploymentStarted then return false end
	
	if self.RetreatInConflictOnlyIfCameFromHere then
		local group = self.Groups
		group = group and group[1]
		
		local sector = gv_Sectors[gv_CurrentSectorId]
		if sector and sector.conflict and unit and unit.arrival_dir ~= group then
			return false
		end
	end

	local sector = gv_Sectors[gv_CurrentSectorId]
	if sector and sector.conflict and sector.conflict.disable_travel then return false end
	
	local nextSector = self:GetNextSector()
	if not nextSector then
		return false
	end

	if unit and (unit:IsDowned() or unit:IsDead()) then
		return false
	end

	return CombatActions.Interact_Exit
end

MapVar("g_RetreatThread", false)

function ExitZoneInteractable:UnitLeaveSector(unit)
	if IsValidThread(g_RetreatThread) then return end
	g_RetreatThread = CreateRealTimeThread(ExitZoneInteractable.UnitLeaveSectorInternal, self, unit)
end

function ExitZoneInteractable:IsUnitInside(u)
	local entranceMarker = MapGetMarkers("Entrance", self.Groups and self.Groups[1])
	entranceMarker = entranceMarker and entranceMarker[1] or self
	return self:IsInsideArea(u) or entranceMarker:IsInsideArea(u)
end

-- Original Spec: http://mantis.haemimontgames.com/view.php?id=147486
-- Cases
-- 1. Conflict All Units
--	2. Conflict Partial Units
--	3. No Conflict All Units
--	4. No Conflict Partial Units
-- Subcases, apply these to to each of the above cases.
--	1. Going towards a sector with travel time 0 (cities/roads)
--	2. Going towards a sector with travel time above 0
--	3. To Underground
--	4. To Overground
function ExitZoneInteractable:UnitLeaveSectorInternal(unit)
	if not unit:CanBeControlled() then return end

	local sector, underground = self:GetNextSector()
	if not sector then return end
	local sector_id = sector.Id
	
	local playerSquads = GetSquadsOnMap()
	local leavingSquads = {}
	for i, squadId in ipairs(playerSquads) do
		local squad = gv_Squads[squadId]
		if not squad then goto continue end
	
		-- Find which units can leave
		local thisSquadHasLeavingUnit = false
		for _, id in ipairs(squad.units or empty_table) do
			local u = g_Units[id]
			if u and u:IsLocalPlayerControlled() and self:IsUnitInside(u) and self:IsMarkerEnabled() then
				thisSquadHasLeavingUnit = true
				break
			end
		end
		if thisSquadHasLeavingUnit then
			leavingSquads[#leavingSquads + 1] = squadId
		end
	
		::continue::
	end
	
	local leavingUnits = {}
	for i, squadId in ipairs(leavingSquads) do
		local squad = gv_Squads[squadId]
		if not squad then goto continue end
		
		-- Check if the squad has any tired units
		local exhausted = GetSquadTiredUnits(squad, "Exhausted")
		if exhausted then
			local exhausted_ids = ShowExhaustedUnitsQuestion(squad, exhausted)
			if exhausted_ids then
				-- This needs to be sync so that the split happens completely before we proceed with the exit.
				-- This function it self isn't sync due to various UI popups etc.
				SyncSplitSquad(squad.UniqueId, exhausted_ids)
				ObjModified("hud_squads")
			else
				goto continue
			end
		end
	
		-- Find which units can leave
		for _, id in ipairs(squad.units or empty_table) do
			local u = g_Units[id]
			if u and u:CanBeControlled() and self:IsUnitInside(u) and self:IsMarkerEnabled() then
				table.insert(leavingUnits, u.session_id)
			end
		end
		
		::continue::
	end
	
	local spawned_units = 0
	for i, squadId in ipairs(playerSquads) do
		local squad = gv_Squads[squadId]
		if not squad then goto continue end
		
		for _, id in ipairs(squad.units or empty_table) do
			local u = g_Units[id]
			if u then
				spawned_units = spawned_units + 1
			end
		end
		
		::continue::
	end
	
	-- All were filtered out.
	if #leavingUnits == 0 then return end

	if gv_Sectors[gv_CurrentSectorId].conflict then
		LeaveSectorConflict(sector_id, leavingUnits, underground, spawned_units, unit)
	else
		LeaveSectorExploration(sector_id, leavingUnits, underground, nil, unit:IsLocalPlayerControlled())
	end
end

function GetExitZoneInteractableFromMarker(marker)
	if not marker then return end
	local exitInteractable = MapGetMarkers("ExitZoneInteractable", marker.Groups and marker.Groups[1])
	return exitInteractable and exitInteractable[1]
end

function LeaveSectorConflict(sectorId, units, underground, totalPlayerUnits, initiatingUnit)
	local names = {}
	for _, u in ipairs(units) do
		local unitData = gv_UnitData[u]
		names[#names + 1] = _InternalTranslate(unitData.Nick)
	end
	names = table.concat(names, ", ")
	
	local initiatedByLocalPlayer = initiatingUnit:IsLocalPlayerControlled()
	local state_func = nil
	if not initiatedByLocalPlayer then
		state_func = function() return "disabled" end
	end
	local three_choices = #units > 1
	local res = WaitPopupChoice(GetInGameInterfaceModeDlg(), {
		translate = true,
		text = T{867511434762, "Do you want to retreat the following mercs - <u(names)>?", names = names},
		choice1 = three_choices and T(288455844681, "Retreat All") or T(1138, "Yes"),
		choice1_state_func = state_func,
		choice1_gamepad_shortcut = "ButtonX",
		choice2 = three_choices and T{162642612318, "Retreat <merc>", merc = initiatingUnit.Nick} or T(967444875712, "Cancel"),
		choice2_state_func = three_choices and state_func or nil,
		choice2_gamepad_shortcut = three_choices and "ButtonY" or "ButtonB",
		choice3 = three_choices and T(1000246, "Cancel") or nil,
		choice3_gamepad_shortcut = "ButtonB",
		sync_close = initiatedByLocalPlayer,
	})
	
	if res == 1 then

	elseif res == 2 and three_choices then
		units = {initiatingUnit.session_id}
	else
		return
	end
	
	if #units < totalPlayerUnits then
		NetSyncEvent("RetreatUnits", units, sectorId, underground, totalPlayerUnits - #units)
	else
		LeaveSectorExploration(sectorId, units, underground, true)
	end
end

function WaitQuestion_ZuluSync(parent, caption, text, ok_text, cancel_text, obj, localPlayer)
	--creates a question box that has it's ok enabled only for localPlayer == true
	--cancel is enabled for all clients to evade failure states
	--cancel/ok on localPlayer == true closes box on all clients
	assert(type(parent) == "table" and parent.IsKindOf and parent:IsKindOf("XWindow"), "The first argument must be a parent window. Don't just create 'global' messages, attach them to the correct parent so they'd share their lifetimes.", 1)
	local dialog
	if IsKindOf(caption, "XDialog") then
		dialog = caption
	else
		local func = nil
		if not localPlayer then
			func = function() return "disabled" end
		end
		dialog = CreateQuestionBox(parent, caption, text, ok_text, cancel_text, obj, func, nil, nil, localPlayer)
	end
	local result, dataset, xInputStateAtClose = dialog:Wait() 
	return result, dataset, xInputStateAtClose
end

function LeaveSectorExploration(sectorId, units, underground, skipNotify, localPlayer)
	if not skipNotify then
		local popupText
		if underground then
			if IsSectorUnderground(gv_CurrentSectorId) then
				popupText = T(528652976882, "Are you sure you want to exit?")
			else
				popupText = T(261972368205, "Are you sure you want to go underground?")
			end
		else
			popupText = T{397573113952, "Are you sure you want to leave sector <SectorName(current_sector)> and enter sector <SectorName(next_sector)>?", 
				current_sector = gv_Sectors[gv_CurrentSectorId],
				next_sector = gv_Sectors[sectorId],
			}
		end
		
		if WaitQuestion_ZuluSync(GetInGameInterfaceModeDlg(), T(814633909510, "Confirm"), popupText, T(689884995409, "Yes"), T(782927325160, "No"), nil, localPlayer) ~= "ok" then
			return
		end
	end
			
	local special_entrance = underground
	
	local squads = {}
	local playerSquads = GetSquadsOnMap()
	for i, squadId in ipairs(playerSquads) do
		local squad = gv_Squads[squadId]
		if not squad then goto continue end
		
		-- This squad initiated retreat but all the non-retreating units died.
		local thisSquadHasLeavingUnit = false
		for _, id in ipairs(squad.units or empty_table) do
			local u = g_Units[id]
			local ud = gv_UnitData[id]
			if not u and ud and ud.retreat_to_sector then
				thisSquadHasLeavingUnit = true
			end
		end
		if thisSquadHasLeavingUnit then
			table.insert_unique(squads, squadId)
		end
		
		::continue::
	end
	
	for i, u in ipairs(units) do
		local unit = g_Units[u] or gv_UnitData[u]
		local squadId = unit.Squad
		table.insert_unique(squads, squadId)
	end
	
	-- Check for busy squads
	local squadsToMove = {}
	for i, sqId in ipairs(squads) do
		local squad = gv_Squads[sqId]
		local squadToMove = CheckSquadBusy(sqId)
		if squadToMove then 
			squadsToMove[#squadsToMove + 1] = squadToMove
		end
	end
	if #squadsToMove == 0 then return end

	NetSyncEvent("LeaveSectorMap", sectorId, false, special_entrance, squadsToMove)
end

-- Map retreat from non-adjacent sectors is possible when
-- an exit zone interactable has its destination overriden.
-- In these cases travel instantly.
function AreAdjacentSectors(s1Id, s2Id)
	return GetSectorDistance(s1Id, s2Id) <= 1
end

function RetreatMoveWholeSquad(squad_id, to_sector_id, from_sector_id)
	local squad = gv_Squads[squad_id]

	local route = GenerateRouteDijkstra(from_sector_id, to_sector_id)
	if not route then route = {to_sector_id} end
	route = {route} -- waypointify
	
	local time = GetSectorTravelTime(from_sector_id, to_sector_id, route, squad.units)
	local instant = not time or time <= 0
	
	-- When the link is multiple sectors teleport between them (186068)
	if route and route[1] and #route[1] > 1 then instant = true end
	
	local from_sector = gv_Sectors[from_sector_id]
	local to_sector = gv_Sectors[to_sector_id]
	
	if not instant then
		-- Tick needs to be considered passed in order for the squad to be considered travelling.
		route.satellite_tick_passed = true
		SetSatelliteSquadRetreatRoute(squad, route, "keepJoiningSquads", "from_map")
	else
		squad.Retreat = false
		if not gv_SatelliteView then SyncUnitProperties("map") end
		SetSatelliteSquadCurrentSector(squad, to_sector_id, "update_pos", "teleported")
		-- For player units we need to sync back to the unit as they will sync back to the unit data in the despawn function
		if not gv_SatelliteView then SyncUnitProperties("session") end
	end
	return instant
end

local function lMoveWholeSquadTacticalView(squad_id, sector_id)
	return RetreatMoveWholeSquad(squad_id, sector_id, gv_CurrentSectorId)
end

function RetreatUnit(unit, sector_id)
	Msg("UnitRetreat", unit)
	local team = unit.team
	unit:Despawn()
	gv_UnitData[unit.session_id].retreat_to_sector = sector_id
	if g_Combat then
		if g_Teams[g_CurrentTeam] == team then
			g_Combat:NextUnit(team, "force")
		end
		g_Combat:CheckEndTurn()
	end
	ObjModified(Game)
	ObjModified(Selection)
	ObjModified("hud_squads")
end

function CancelUnitRetreat(ud)
	-- Try to get the units in the direction they retreated to
	if IsSectorUnderground(ud.retreat_to_sector) then
		ud.arrival_dir = "Underground"
	else
		local dirToRetreatSector = GetSectorDirection(gv_CurrentSectorId, ud.retreat_to_sector)
		ud.arrival_dir = dirToRetreatSector
	end

	ud.retreat_to_sector = false
	ud.already_spawned_on_map = false
end

-- Used for resuming retreat if the last units on the map die
GameVar("gv_LastRetreatedUnit", false)
GameVar("gv_LastRetreatedEntrance", false)

function NetSyncEvents.RetreatUnits(session_ids, sector_id, underground, remaining)
	local units = {}
	for _, id in ipairs(session_ids) do
		local unit = g_Units[id]
		if not unit then
			assert(false, "Trying to retreat non existent unit")
			return
		end
		units[#units + 1] = unit
	end

	-- When retreating in conflict force cancel operations of units. (219850)
	SectorOperation_CancelByGame(units)

	-- Record in case the rest of the units die.
	-- We need to call LeaveSectorExploration then.
	gv_LastRetreatedUnit = #session_ids > 0 and session_ids[1]
	gv_LastRetreatedEntrance = { sector_id, underground }

	local check_squads = {}
	for _, unit in ipairs(units) do
		RetreatUnit(unit, sector_id)
		table.insert_unique(check_squads, unit.Squad)
	end
	
	-- check if there are new squads with all mercs retreated
	local squadsToMove = {}
	for _, id in ipairs(check_squads) do
		local retreat_whole_squad = true
		local squad = gv_Squads[id]
		for _, unit in ipairs(squad.units or empty_table) do
			if not gv_UnitData[unit].retreat_to_sector then
				retreat_whole_squad = false
				break
			end
		end
		if retreat_whole_squad then
			lMoveWholeSquadTacticalView(squad.UniqueId, sector_id)
			table.insert(squadsToMove, squad.UniqueId)
		end
	end
	EnsureCurrentSquad()
	ShowTacticalNotification("allyRetreat", nil, T(312444150797, "Retreated successfully"), { number = remaining })
	
	if #squadsToMove > 0 and #GetAllPlayerUnitsOnMap() <= 0 then --no guys left on map
		NetSyncEvents.LeaveSectorMap(sector_id, false, underground, squadsToMove)
	end
end

function SyncSplitSquad(squad_id, available)
	assert(CanYield())
	NetSyncEvent("SplitSquad", squad_id, available)
	local err, newSquad, oldSquad
	while oldSquad ~= squad_id do
		err, newSquad, oldSquad = WaitMsg("SyncSplitSquad", 1000)
		if err then
			break
		end
	end
	return newSquad
end

function CheckSquadBusy(squad_id)
	local busy, available = GetSquadBusyAvailable(squad_id)
	if next(busy) then
		local res = GetSplitMoveChoice(busy, available)
		if res == "split" then
			return SyncSplitSquad(squad_id, available)
		elseif res == "cancel" then
			return false
		end
	end
	return squad_id
end

function NetSyncEvents.LeaveSectorMap(dest_sector_id, spawn_mode, special_entrance, squad_ids)
	if g_Combat and not g_Combat.combat_started then return end
	if IsSetpiecePlaying() then return end

	SectorOperation_SquadOnMove(gv_CurrentSectorId, squad_ids)

	local squads = GetSquadsWithIds(squad_ids)
	local curSector = gv_Sectors[gv_CurrentSectorId]

	-- Apply unaware to non-player units when leaving the map.
	-- This will be synced to the unit data inside MoveWholeSquad
	for _, unit in ipairs(g_Units) do
		if unit.team and not unit.team.player_team then
			unit:AddStatusEffect("Unaware")
		else
			if unit:HasStatusEffect("ManningEmplacement") then
				unit:LeaveEmplacement("instant")
			elseif unit:HasStatusEffect("StationedMachineGun") then
				unit:MGPack()
			end
		end
	end

	-- travel or teleport to other sector instantly
	local satellite = false
	for i, squad in ipairs(squads) do
		-- stop operations
		if dest_sector_id ~= squad.CurrentSector then
			local units = squad.units
			SectorOperation_CancelByGame(units, false, true)
		end

		-- move
		local instant = lMoveWholeSquadTacticalView(squad.UniqueId, dest_sector_id)
		satellite = satellite or not instant
		
		for _, id in ipairs(squad.units) do
			local unit = g_Units[id]
			if unit then
				RetreatUnit(unit, dest_sector_id)
			end
		end
		
		-- Clear retreat flag from units
		for _, u in ipairs(squad.units) do
			local ud = gv_UnitData[u]
			ud.retreat_to_sector = false
		end
		
		-- Overwrite arrival direction with special entrance if any.
		-- This will affect the deployment on the new sector.
		if special_entrance then
			for _, u in ipairs(squad.units) do
				local ud = gv_UnitData[u]
				ud.arrival_dir = special_entrance
			end
		end
	end
	
	local conflict = curSector.conflict
	local bRetreat = false
	for i, squad in ipairs(squads) do
		if conflict then
			if satellite then squad.Retreat = true end
			bRetreat = true
		end
	end
	
	if conflict then
		ResolveConflict(curSector, bRetreat and "no_voice", false, bRetreat)
	end
	
	-- Retreat triggered while in satellite mode, such as by a merc release.
	-- In this case don't force an explore as it can be jarring.
	if gv_SatelliteView then return end
	
	if satellite then
		ForceReloadSectorMap = true
		LocalCheckUnitsMapPresence()
		CreateRealTimeThread(function()
			OpenSatelliteView()
			SetCampaignSpeed(Game.CampaignTimeFactor, "UI")
		end)
	else
		if not spawn_mode then
			local destSector = gv_Sectors[dest_sector_id]
			spawn_mode = destSector and destSector.conflict and "attack" or "explore"
		end
		CreateGameTimeThread(function()
			LoadSector(dest_sector_id, spawn_mode) --pause wants to yield, but it can't
		end)
	end
end

-- Promote half-way retreating squads into full retreating squads should all their units have been released.
function OnMsg.MercReleased(_, squadId)
	local squad = gv_Squads[squadId]
	if not squad then return end
	
	local squadUnitsLeft = squad.units
	local allRetreating, retreatingTo = true, false
	for i, u in ipairs(squadUnitsLeft) do
		local ud = gv_UnitData[u]
		if not ud.retreat_to_sector then
			allRetreating = false
		elseif not retreatingTo then
			retreatingTo = ud.retreat_to_sector
		end
	end
	if allRetreating then
		local currentSector = squad.CurrentSector
		local underground = currentSector .. "_Underground" == retreatingTo or retreatingTo .. "_Underground" == currentSector
		LeaveSectorExploration(retreatingTo, squadUnitsLeft, underground, true)
	end
end

MapVar("gv_RetreatOrTravelOption", false)

function CheckRetreatButtonVisibility()
	local selectedUnit = Selection and Selection[1]
	if not selectedUnit or (IsKindOf(selectedUnit, "Unit") and not selectedUnit:CanBeControlled()) then
		gv_RetreatOrTravelOption = false
		ObjModified("gv_RetreatOrTravelOption")
		return
	end

	local markers = MapGetMarkers("Entrance")
	for i, m in ipairs(markers) do
		if m:IsMarkerEnabled() and m:IsInsideArea(selectedUnit) then
			local exitInteractable = MapGetMarkers("ExitZoneInteractable", m.Groups and m.Groups[1])
			exitInteractable = exitInteractable and exitInteractable[1]
			
			if exitInteractable and exitInteractable:GetInteractionCombatAction(selectedUnit) then
				gv_RetreatOrTravelOption = exitInteractable
				ObjModified("gv_RetreatOrTravelOption")
				return
			end
		end
	end
	
	gv_RetreatOrTravelOption = false
	ObjModified("gv_RetreatOrTravelOption")
end

OnMsg.ExplorationTick = CheckRetreatButtonVisibility
OnMsg.CombatGotoStep = CheckRetreatButtonVisibility
OnMsg.SelectedObjChange = CheckRetreatButtonVisibility
OnMsg.SelectionChange = CheckRetreatButtonVisibility
OnMsg.TurnStart = CheckRetreatButtonVisibility
OnMsg.RepositionEnd = CheckRetreatButtonVisibility

function GetClosestExitZoneInteractable(pos_or_obj)
	local closestExitZone = false
	MapForEach("map", "ExitZoneInteractable", function(o)
		if not closestExitZone then
			closestExitZone = o
			return
		end
		closestExitZone = closestExitZone and IsCloser(pos_or_obj, o, closestExitZone) and o or closestExitZone
	end)
	
	return closestExitZone
end

if Platform.developer then
local function lCheckMapEntrances(campaign_preset, sector, errors)
	errors = errors or {}

	local sectors = campaign_preset.Sectors or empty_table
	local directions = { }
	for _, dir in ipairs(const.WorldDirections) do
		local neighSectorId = GetNeighborSector(sector.Id, dir, campaign_preset)
		if neighSectorId then
			local sector = table.find_value(sectors, "Id", neighSectorId)
			if sector and sector.Passability ~= "Blocked" then
				directions[#directions + 1] = dir
			end
		end
	end
	
	local blockedTravel = sector.BlockTravel or empty_table
	for i, dir in ipairs(directions) do
		if not blockedTravel[dir] and not next(MapGetMarkers("ExitZoneInteractable", dir)) then
			errors[#errors + 1] = string.format("No ExitZoneInteractable on map '%s' for direction '%s'", GetMapName(), dir)
		end
	end
	
	return errors
end

function OnMsg.SaveMap()
	local campaign = Game and Game.Campaign or rawget(_G, "DefaultCampaign") or "HotDiamonds"
	local campaign_presets = rawget(_G, "CampaignPresets") or empty_table
	local campaign_preset = campaign_presets[campaign]

	local sectors = campaign_preset and campaign_preset.Sectors or empty_table
	
	local sector = false
	for i, s in ipairs(sectors) do
		if s.Map == CurrentMap then
			sector = s
			break
		end
	end
	if not sector or sector.GroundSector then return end
	
	local errors = lCheckMapEntrances(campaign_preset, sector)
	for i, err in ipairs(errors) do
		StoreErrorSource(i, err)
	end
end

function CheckEntrancesOfAllMaps()
	if not CanYield() then
		CreateRealTimeThread(CheckEntrancesOfAllMaps)
		return
	end

	local campaign = Game and Game.Campaign or rawget(_G, "DefaultCampaign") or "HotDiamonds"
	local campaign_presets = rawget(_G, "CampaignPresets") or empty_table
	local campaign_preset = campaign_presets[campaign]

	local sectors = campaign_preset and campaign_preset.Sectors or empty_table
	local maps = {}
	local mapToSector = {}
	for i, s in ipairs(sectors) do
		if s.Map and not s.GroundSector then
			maps[#maps + 1] = s.Map
			mapToSector[s.Map] = s
		end
	end
	
	local errors = {}
	ForEachMap(maps, function()
		local sector = mapToSector[CurrentMap]
		errors = lCheckMapEntrances(campaign_preset, sector, errors)
	end)

	while IsChangingMap() do Sleep(100) end

	for i, err in ipairs(errors) do
		StoreErrorSource(false, err)
	end
	Inspect(errors)
end
end