5 using Microsoft.Xna.Framework;
7 using System.Collections.Generic;
8 using System.Collections.Immutable;
10 using System.Xml.Linq;
17 public readonly record
struct SaveInfo(
21 ImmutableArray<string> EnabledContentPackageNames) : INetSerializableStruct;
29 const float FirstRoundEventDelay = 0.0f;
45 private List<Faction> factions;
46 public IReadOnlyList<Faction>
Factions => factions;
56 private readonly List<Mission> extraMissions =
new List<Mission>();
58 public readonly NamedEvent<WalletChangedEvent>
OnMoneyChanged =
new NamedEvent<WalletChangedEvent>();
66 ProgressToNextLocation,
68 ReturnToPreviousLocation,
70 ReturnToPreviousEmptyLocation,
72 ProgressToNextEmptyLocation,
93 private readonly Dictionary<string, double> dialogLastSpoken =
new Dictionary<string, double>();
120 yield
return mission;
124 foreach (
Mission mission
in extraMissions)
126 yield
return mission;
161 c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } &&
162 (IsOwner(c) || c.HasPermission(permissions)));
166 return GameMain.
NetworkMember.ConnectedClients.None(c => IsOwner(c) || c.HasPermission(permissions));
175 Balance = settings.InitialMoney
181 Identifier messageIdentifier =
new Identifier(
"money");
184 OnMoneyChanged.RegisterOverwriteExisting(
new Identifier(
"CampaignMoneyChangeNotification"), e =>
186 if (!e.ChangedData.BalanceChanged.TryUnwrap(out var changed)) { return; }
188 if (changed == 0) {
return; }
190 bool isGain = changed > 0;
191 Color clr = isGain ? GUIStyle.Yellow : GUIStyle.Red;
193 if (e.Owner.TryUnwrap(out var owner))
195 owner.AddMessage(FormatMessage(), clr, playSound: Character.Controlled == owner, messageIdentifier, changed);
202 string FormatMessage() => TextManager.GetWithVariable(isGain ?
"moneygainformat" :
"moneyloseformat",
"[money]", TextManager.FormatCurrency(Math.Abs(changed))).ToString();
214 return price == 0 ||
GetWallet(client).TryDeduct(price);
247 !leavingSub.
DockedTo.Contains(sub) &&
269 dialogLastSpoken.Clear();
270 characterOutOfBoundsTimer.Clear();
275 foreach (var faction
in factions)
277 faction.Reputation.ReputationAtRoundStart = faction.Reputation.Value;
280 if (PurchasedHullRepairsInLatestSave)
289 wall.
SetDamage(i, 0, createNetworkEvent:
false, createExplosionEffect:
false);
295 if (PurchasedItemRepairsInLatestSave)
302 if (item.GetComponent<Items.
Components.Repairable>() !=
null)
318 float totalDamage = 0;
335 float totalRepairDuration = 0.0f;
341 var repairable = item.GetComponent<
Repairable>();
342 if (repairable ==
null) {
continue; }
346 return (
int)Math.Min(totalRepairDuration * ItemRepairCostPerRepairDuration, MaxItemRepairCost);
351 factions =
new List<Faction>();
352 foreach (FactionPrefab factionPrefab
in FactionPrefab.Prefabs)
370 if (levelData ==
null)
372 throw new ArgumentException(
"Level data was null.");
375 extraMissions.Clear();
378 if (currentLocation ==
null)
380 throw new InvalidOperationException(
"Current location was null.");
385 foreach (var availableMission
in currentLocation.AvailableMissions)
387 if (availableMission.Locations[0] == currentLocation && availableMission.Locations[1] == currentLocation)
389 currentLocation.SelectMission(availableMission);
395 foreach (
Mission mission
in currentLocation.SelectedMissions.ToList())
398 if (mission.
Locations[0] == currentLocation &&
401 currentLocation.DeselectMission(mission);
407 if (beaconMissionPrefabs.Any())
409 var filteredMissions = beaconMissionPrefabs.Where(m => levelData.
Difficulty >= m.MinLevelDifficulty && levelData.
Difficulty <= m.MaxLevelDifficulty);
410 if (filteredMissions.None())
412 DebugConsole.AddWarning($
"No suitable beacon mission found matching the level difficulty {levelData.Difficulty}. Ignoring the restriction.");
416 beaconMissionPrefabs = filteredMissions;
419 var beaconMissionPrefab = ToolBox.SelectWeightedRandom(beaconMissionPrefabs, p => p.Commonness, rand);
425 var huntingGroundsMissionPrefabs =
MissionPrefab.
Prefabs.Where(m => m.IsSideObjective && m.Tags.Contains(
"huntinggrounds")).OrderBy(m => m.UintIdentifier);
426 if (!huntingGroundsMissionPrefabs.Any())
428 DebugConsole.AddWarning(
"Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggrounds\" found.");
434 var prefabs = huntingGroundsMissionPrefabs.ToList();
435 var weights = prefabs.Select(p => (
float)Math.Max(p.Commonness, 1)).ToList();
436 for (
int i = 0; i < prefabs.Count; i++)
438 var prefab = prefabs[i];
439 var weight = weights[i];
440 if (prefab.Tags.Contains(
"easy"))
444 else if (prefab.Tags.Contains(
"hard"))
450 var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(prefabs, weights, rand);
451 if (!
Missions.Any(m => m.Prefab.Tags.Contains(
"huntinggrounds")))
457 foreach (
Faction faction
in factions.OrderBy(f => f.Prefab.MenuOrder))
459 foreach (var automaticMission
in faction.
Prefab.AutomaticMissions)
463 if (automaticMission.DisallowBetweenOtherFactionOutposts && levelData.
Type ==
LevelData.
LevelType.LocationConnection)
470 if (automaticMission.MaxDistanceFromFactionOutpost <
int.MaxValue)
474 automaticMission.MaxDistanceFromFactionOutpost,
475 loc => loc.Faction == faction))
481 if (levelData.
Type != automaticMission.LevelType) {
continue; }
484 automaticMission.MinProbability,
485 automaticMission.MaxProbability,
486 MathUtils.InverseLerp(automaticMission.MinReputation, automaticMission.MaxReputation, faction.
Reputation.
Value));
487 if (rand.NextDouble() < probability)
489 var missionPrefabs =
MissionPrefab.
Prefabs.Where(m => m.Tags.Any(t => t == automaticMission.MissionTag)).OrderBy(m => m.UintIdentifier);
490 if (missionPrefabs.Any())
492 var missionPrefab = ToolBox.SelectWeightedRandom(missionPrefabs, p => p.Commonness, rand);
499 extraMissions.Add(missionPrefab.Instantiate(
new Location[] { currentLocation, currentLocation },
Submarine.
MainSub));
512 Identifier endMissionTag = Identifier.Empty;
516 if (locationIndex > -1)
518 endMissionTag = (
"endlevel_locationconnection_" + locationIndex).ToIdentifier();
524 if (locationIndex > -1)
526 endMissionTag = (
"endlevel_location_" + locationIndex).ToIdentifier();
529 if (!endMissionTag.IsEmpty)
531 var endLevelMissionPrefabs =
MissionPrefab.
Prefabs.Where(m => m.Tags.Contains(endMissionTag)).OrderBy(m => m.UintIdentifier);
532 if (endLevelMissionPrefabs.Any())
535 var endLevelMissionPrefab = ToolBox.SelectWeightedRandom(endLevelMissionPrefabs, p => p.Commonness, rand);
536 if (
Missions.All(m => m.Prefab.Type != endLevelMissionPrefab.Type))
544 extraMissions.Add(endLevelMissionPrefab.Instantiate(
new Location[] { map.CurrentLocation, map.CurrentLocation },
Submarine.
MainSub));
560 if (CoroutineManager.IsCoroutineRunning(
"LevelTransition"))
562 DebugConsole.ThrowError(
"Level transition already running.\n" + Environment.StackTrace.CleanupStackTrace());
579 DebugConsole.ThrowErrorLocalized(
"Failed to load a new campaign level. No available level transitions " +
582 "leaving sub: " + (leavingSub?.Info?.Name ??
"null") +
", " +
583 "at start: " + (leavingSub?.AtStartExit.ToString() ??
"null") +
", " +
584 "at end: " + (leavingSub?.AtEndExit.ToString() ??
"null") +
")\n" +
585 Environment.StackTrace.CleanupStackTrace());
588 if (nextLevel ==
null)
590 DebugConsole.ThrowErrorLocalized(
"Failed to load a new campaign level. No available level transitions " +
591 "(transition type: " + availableTransition +
", " +
594 "leaving sub: " + (leavingSub?.Info?.Name ??
"null") +
", " +
595 "at start: " + (leavingSub?.AtStartExit.ToString() ??
"null") +
", " +
596 "at end: " + (leavingSub?.AtEndExit.ToString() ??
"null") +
")\n" +
597 Environment.StackTrace.CleanupStackTrace());
603 DebugConsole.NewMessage(
"Transitioning to " + (nextLevel?.Seed ??
"null") +
606 "leaving sub: " + (leavingSub?.Info?.Name ??
"null") +
", " +
607 "at start: " + (leavingSub?.AtStartExit.ToString() ??
"null") +
", " +
608 "at end: " + (leavingSub?.AtEndExit.ToString() ??
"null") +
", " +
609 "transition type: " + availableTransition +
")");
613 CoroutineManager.StartCoroutine(
DoLevelTransition(availableTransition, nextLevel, leavingSub, mirror),
"LevelTransition");
635 leavingSub = GetLeavingSub();
636 if (leavingSub ==
null)
645 if (leavingSub.AtEndExit)
663 else if (leavingSub.AtStartExit)
691 if (currentEndLocationIndex > -1)
699 else if (leavingSub.AtEndExit && currentEndLocationIndex <
map.
EndLocations.Count - 1)
719 throw new NotImplementedException();
742 Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers, submarineTeam);
743 Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers, submarineTeam);
745 int playersInSubAtStart = leavingSubAtStart ==
null || !leavingSubAtStart.
AtStartExit ? 0 :
747 int playersInSubAtEnd = leavingSubAtEnd ==
null || !leavingSubAtEnd.
AtEndExit ? 0 :
750 if (playersInSubAtStart == 0 && playersInSubAtEnd == 0)
755 return playersInSubAtStart > playersInSubAtEnd ? leavingSubAtStart : leavingSubAtEnd;
757 static Submarine GetLeavingSubAtStart(IEnumerable<Character> leavingPlayers,
CharacterTeamType submarineTeam)
759 if (Level.Loaded.StartOutpost ==
null)
761 Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts:
true, ignoreRespawnShuttle:
true, teamType: submarineTeam);
762 if (closestSub ==
null) {
return null; }
763 return closestSub.DockedTo.Contains(
Submarine.MainSub) ?
Submarine.MainSub : closestSub;
768 if (Level.Loaded.StartOutpost.DockedTo.Any())
770 foreach (var dockedSub
in Level.Loaded.StartOutpost.DockedTo)
772 if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) {
continue; }
773 return dockedSub.DockedTo.Contains(
Submarine.MainSub) ?
Submarine.MainSub : dockedSub;
778 if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.StartOutpost)) {
return null; }
779 Submarine closestSub =
Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts:
true, ignoreRespawnShuttle:
true, teamType: submarineTeam);
780 if (closestSub ==
null || !closestSub.AtStartExit) {
return null; }
781 return closestSub.DockedTo.Contains(
Submarine.MainSub) ?
Submarine.MainSub : closestSub;
787 if (Level.Loaded.EndOutpost !=
null && Level.Loaded.EndOutpost.ExitPoints.Any())
789 Submarine closestSub =
Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts:
true, ignoreRespawnShuttle:
true, teamType: submarineTeam);
790 if (closestSub ==
null || !closestSub.AtEndExit) {
return null; }
791 return closestSub.DockedTo.Contains(
Submarine.MainSub) ?
Submarine.MainSub : closestSub;
794 if (Level.Loaded.Type == LevelData.LevelType.Outpost)
799 if (Level.Loaded.EndOutpost ==
null)
801 Submarine closestSub =
Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts:
true, ignoreRespawnShuttle:
true, teamType: submarineTeam);
802 if (closestSub ==
null) {
return null; }
803 return closestSub.DockedTo.Contains(
Submarine.MainSub) ?
Submarine.MainSub : closestSub;
808 if (Level.Loaded.EndOutpost.DockedTo.Any())
810 foreach (var dockedSub
in Level.Loaded.EndOutpost.DockedTo)
812 if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) {
continue; }
813 return dockedSub.DockedTo.Contains(
Submarine.MainSub) ?
Submarine.MainSub : dockedSub;
818 if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.EndOutpost)) {
return null; }
819 Submarine closestSub =
Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts:
true, ignoreRespawnShuttle:
true, teamType: submarineTeam);
820 if (closestSub ==
null || !closestSub.AtEndExit) {
return null; }
821 return closestSub.DockedTo.Contains(
Submarine.MainSub) ?
Submarine.MainSub : closestSub;
828 List<Item> takenItems =
new List<Item>();
837 takenItems.Add(item);
870 List<Character> killedCharacters =
new List<Character>();
874 killedCharacters.Add(c);
893 if (item.HasTag(Tags.IdCardTag) &&
894 (item.Container?.HasTag(Tags.DespawnContainer) ??
false))
910 if (port.
Door !=
null &
922 if (!sub.Info.IsPlayer) {
continue; }
924 if (item.GetComponent<
Reactor>() is
Reactor reactor && reactor.
LastAIUser !=
null && reactor.LastUser == reactor.LastAIUser)
928 reactor.AutoTemp =
true;
971 IsBeaconActive =
false,
982 location.
Reset(
this);
997 faction.Reputation.SetReputation(faction.Prefab.InitialReputation);
1010 GameAnalyticsManager.AddProgressionEvent(
1011 GameAnalyticsManager.ProgressionStatus.Complete,
1013 string eventId =
"FinishCampaign:";
1015 GameAnalyticsManager.AddDesignEvent(eventId +
"CrewSize:" + (
CrewManager?.GetCharacterInfos()?.Count() ?? 0));
1016 GameAnalyticsManager.AddDesignEvent(eventId +
"Money",
Bank.Balance);
1017 GameAnalyticsManager.AddDesignEvent(eventId +
"Playtime",
TotalPlayTime);
1018 GameAnalyticsManager.AddDesignEvent(eventId +
"PassedLevels",
TotalPassedLevels);
1041 public static Faction GetRandomFaction(IEnumerable<Faction> factions, Rand.RandSync randSync,
bool secondary =
false,
bool allowEmpty =
true)
1043 return GetRandomFaction(factions, Rand.GetRNG(randSync), secondary, allowEmpty);
1046 public static Faction GetRandomFaction(IEnumerable<Faction> factions, Random random,
bool secondary =
false,
bool allowEmpty =
true)
1048 List<Faction> factionsList = factions.OrderBy(f => f.Prefab.Identifier).ToList();
1049 List<float> weights = factionsList.Select(f => secondary ? f.Prefab.SecondaryControlledOutpostPercentage : f.Prefab.ControlledOutpostPercentage).ToList();
1050 float percentageSum = weights.Sum();
1051 if (percentageSum < 100.0f && allowEmpty)
1054 factionsList.Add(
null);
1055 weights.Add(100.0f - percentageSum);
1057 return ToolBox.SelectWeightedRandom(factionsList, weights, random);
1062 if (characterInfo ==
null) {
return false; }
1063 if (characterInfo.MinReputationToHire.factionId != Identifier.Empty)
1065 if (MathF.Round(
GetReputation(characterInfo.MinReputationToHire.factionId)) < characterInfo.MinReputationToHire.reputation)
1071 if (takeMoney && !
TryPurchase(client, price)) {
return false; }
1074 characterInfo.
Title =
null;
1077 GameAnalyticsManager.AddMoneySpentEvent(characterInfo.
Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.
Job?.
Prefab.
Identifier.Value ??
"unknown");
1083 float characterCostPercentage =
GameMain.
NetworkMember?.ServerSettings.ReplaceCostPercentage ?? 100f;
1095 NPCInteractProjSpecific(npc, interactor);
1096 string coroutineName =
"DoCharacterWait." + (npc?.
ID ?? Entity.NullEntityID);
1097 if (!CoroutineManager.IsCoroutineRunning(coroutineName))
1099 CoroutineManager.StartCoroutine(DoCharacterWait(npc, interactor), coroutineName);
1103 private IEnumerable<CoroutineStatus> DoCharacterWait(Character npc, Character interactor)
1105 if (npc ==
null || interactor ==
null) { yield
return CoroutineStatus.Failure; }
1107 HumanAIController humanAI = npc.AIController as HumanAIController;
1108 if (humanAI ==
null) { yield
return CoroutineStatus.Success; }
1110 var waitOrder = OrderPrefab.Prefabs[
"wait"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
1111 humanAI.SetForcedOrder(waitOrder);
1112 var waitObjective = humanAI.ObjectiveManager.ForcedOrder;
1113 humanAI.FaceTarget(interactor);
1115 while (!npc.Removed && !interactor.Removed &&
1116 Vector2.DistanceSquared(npc.WorldPosition, interactor.WorldPosition) < 300.0f * 300.0f &&
1117 humanAI.ObjectiveManager.ForcedOrder == waitObjective &&
1118 humanAI.AllowCampaignInteraction() &&
1119 !interactor.IsIncapacitated)
1121 yield
return CoroutineStatus.Running;
1129 humanAI.ClearForcedOrder();
1131 yield
return CoroutineStatus.Success;
1134 partial
void NPCInteractProjSpecific(Character npc, Character interactor);
1141 character.
HumanPrefab is { Identifier: var merchantId })
1160 hudText: TextManager.GetWithVariable(
"CampaignInteraction." + interactionType,
"[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(
InputType.Use)));
1162 hudText: TextManager.Get(
"CampaignInteraction." + interactionType));
1166 private readonly Dictionary<Character, float> characterOutOfBoundsTimer =
new Dictionary<Character, float>();
1170 const float MaxDist = 3000.0f;
1171 const float MinDist = 2500.0f;
1183 if (characterOutOfBoundsTimer.ContainsKey(c))
1186 characterOutOfBoundsTimer.Remove(c);
1191 if (c.
WorldPosition.Y < worldBorders.Y - worldBorders.Height - MaxDist)
1193 if (!characterOutOfBoundsTimer.ContainsKey(c))
1195 characterOutOfBoundsTimer.Add(c, 0.0f);
1199 characterOutOfBoundsTimer[c] += deltaTime;
1202 else if (c.
WorldPosition.Y > worldBorders.Y - worldBorders.Height - MinDist)
1204 if (characterOutOfBoundsTimer.ContainsKey(c))
1207 characterOutOfBoundsTimer.Remove(c);
1212 foreach (KeyValuePair<Character, float> character
in characterOutOfBoundsTimer)
1214 if (character.Value <= 0.0f)
1220 TextManager.Get(
"RadioAnnouncerName"),
1221 TextManager.Get(
"TooFarFromOutpostWarning"),
1222 Networking.ChatMessageType.Default,
1229 foreach (Networking.Client c in
GameMain.Server.ConnectedClients)
1232 GameMain.Server.SendDirectChatMessage(Networking.ChatMessage.Create(
1233 TextManager.Get(
"RadioAnnouncerName").Value,
1234 TextManager.Get(
"TooFarFromOutpostWarning").Value, Networking.ChatMessageType.Default,
null), c);
1239 character.Key.OverrideMovement = Vector2.UnitY * 10.0f;
1244 if (character.Value > 10.0f)
1246 Vector2 teleportPos = character.Key.WorldPosition;
1248 character.Key.AnimController.SetPosition(ConvertUnits.ToSimUnits(teleportPos));
1272 return factions.Find(f => f.Prefab.Identifier == identifier);
1278 factionIdentifier ==
"location".ToIdentifier() ?
1280 factions.Find(f => f.Prefab.Identifier == factionIdentifier);
1281 return faction?.Reputation?.Value ?? 0.0f;
1290 public abstract void Save(XElement element);
1301 return new XElement(
"stats",
1309 DebugConsole.NewMessage(
"********* CAMPAIGN STATUS *********", Color.White);
1310 DebugConsole.NewMessage(
" Money: " +
Bank.Balance, Color.White);
1313 DebugConsole.NewMessage(
" Available destinations: ", Color.White);
1319 DebugConsole.NewMessage(
" " + i +
". " + destination.
DisplayName +
" [SELECTED]", Color.White);
1323 DebugConsole.NewMessage(
" " + i +
". " + destination.
DisplayName, Color.White);
1331 DebugConsole.NewMessage(
" Selected mission: " + mission.
Name, Color.White);
1332 DebugConsole.NewMessage(
"\n" + mission.
Description, Color.White);
1351 foreach (
Location location
in currentLocation.
Connections.Select(c => c.OtherLocation(currentLocation)))
1355 DebugConsole.AddWarning($
"Client {sender.Name} had too many missions selected for location {location.DisplayName}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions.");
1356 foreach (
Mission mission
in currentLocation.SelectedMissions.Where(m => m.Locations[1] == location).Skip(
Settings.TotalMaxMissionCount).ToList())
1358 currentLocation.DeselectMission(mission);
1371 leavingSub.
Info.
FilePath = System.IO.Path.Combine(SaveUtil.TempPath, leavingSub.
Info.
Name +
".sub");
1374 foreach (
Submarine sub
in subsToLeaveBehind)
1400 if (currentSub ==
null || currentSub.
Removed)
1402 DebugConsole.ThrowError(
"Cannot transfer items between subs, because the current sub is null or removed!");
1405 var itemsToTransfer =
new List<(Item item, Item container)>();
1412 if (item.
Removed) {
continue; }
1415 if (!connectedSubs.Contains(item.
Submarine)) {
continue; }
1417 if (AnyParentInventoryDisableTransfer(item)) {
continue; }
1419 if (rootOwner is
Character) {
continue; }
1421 if (item.GetComponent<
Door>() !=
null) {
continue; }
1425 itemsToTransfer.Add((item, item.
Container));
1428 static bool AnyParentInventoryDisableTransfer(
Item item)
1431 return HasProblematicComponent(parentOwner) || AnyParentInventoryDisableTransfer(parentOwner);
1433 static bool HasProblematicComponent(
Item it)
1434 => it.
Components.Any(
static c => c.DontTransferInventoryBetweenSubs);
1437 foreach (var (item, container) in itemsToTransfer)
1442 item.
Drop(
null, createNetworkEvent:
false, setTransform:
false);
1445 foreach (var itemContainer
in item.GetComponents<
ItemContainer>())
1447 itemContainer.Inventory.FindAllItems((_) =>
true, recursive:
true).ForEach(it => it.Submarine =
null);
1451 System.Diagnostics.Debug.Assert(itemsToTransfer.None(it => it.item.Submarine !=
null),
"Item that was set to be transferred was not removed from the sub!");
1460 var connectedSubs = newSub.GetConnectedSubs().Where(s => s.Info.Type ==
SubmarineType.Player);
1463 if (spawnHull ==
null)
1465 DebugConsole.AddWarning($
"Failed to transfer items between subs. No cargo waypoint or dry hulls found in the new sub.");
1469 var cargoContainers = itemsToTransfer.Where(it => it.item.HasTag(Tags.Crate)).ToHashSet();
1470 foreach (var (item, _) in cargoContainers)
1473 item.
SetTransform(simPos, 0.0f, findNewHull:
false, setPrevTransform:
false);
1479 foreach (var (item, oldContainer) in itemsToTransfer)
1481 if (cargoContainers.Contains((item, oldContainer))) {
continue; }
1482 Item newContainer =
null;
1486 newContainer = newSub.FindContainerFor(item, onlyPrimary:
true, checkTransferConditions:
true, allowConnectedSubs:
true);
1488 string newContainerName = newContainer ==
null ?
"(null)" : $
"{newContainer.Prefab.Identifier} ({newContainer.Tags})";
1492 if (cargoContainer ==
null || !cargoContainer.Inventory.TryPutItem(item, user:
null, createNetworkEvent:
false))
1495 item.
SetTransform(simPos, 0.0f, findNewHull:
false, setPrevTransform:
false);
1499 if (cargoContainer.Item.Submarine is
Submarine containerSub)
1504 newContainerName = cargoContainer.Item.Prefab.Identifier.
ToString();
1508 if (oldContainer !=
null)
1510 if (newContainer ==
null && oldContainer == item.
Container)
1512 msg = $
"Transferred {item.Prefab.Identifier} ({item.ID}) contained inside {oldContainer.Prefab.Identifier} ({oldContainer.ID})";
1516 msg = $
"Transferred {item.Prefab.Identifier} ({item.ID}) from {oldContainer.Prefab.Identifier} ({oldContainer.Tags}) to {newContainerName}";
1521 msg = $
"Transferred {item.Prefab.Identifier} ({item.ID}) to {newContainerName}";
1524 DebugConsole.NewMessage(msg);
1526 DebugConsole.Log(msg);
1530 foreach (var (item, _) in itemsToTransfer)
1535 PropagateSubmarineProperty(item);
1538 static void PropagateSubmarineProperty(
Item item)
1540 foreach (var ownedContainer
in item.GetComponents<
ItemContainer>())
1542 foreach (var containedItem
in ownedContainer.Inventory.AllItems)
1544 containedItem.Submarine = item.
Submarine;
1545 PropagateSubmarineProperty(containedItem);
1550 newSub.Info.NoItems =
false;
float AdjustedMaxDifficulty
void SaveActiveOrders(XElement parentElement=null)
TransitionType GetAvailableTransition(out LevelData nextLevel, out Submarine leavingSub)
Which type of transition between levels is currently possible (if any)
UpgradeManager UpgradeManager
bool DivingSuitWarningShown
void LoadStats(XElement element)
virtual bool TryPurchase(Client client, int price)
Action BeforeLevelLoading
Automatically cleared after triggering -> no need to unregister
static int GetHullRepairCost()
static Faction GetRandomFaction(IEnumerable< Faction > factions, Random random, bool secondary=false, bool allowEmpty=true)
virtual int GetBalance(Client client=null)
Faction GetRandomFaction(Rand.RandSync randSync, bool allowEmpty=true)
Returns a random faction based on their ControlledOutpostPercentage
Faction GetFaction(Identifier identifier)
CampaignMode(GameModePreset preset, CampaignSettings settings)
TransitionType GetAvailableTransition()
void TransferItemsBetweenSubs()
Also serializes the current sub.
bool PurchasedLostShuttlesInLatestSave
int NumberOfMissionsAtLocation(Location location)
const int MaxHullRepairCost
bool SwitchedSubsThisRound
bool CanAfford(int cost, Client client=null)
MedicalClinic MedicalClinic
readonly CampaignMetadata CampaignMetadata
float GetReputation(Identifier factionIdentifier)
bool??????? ShowCampaignUI
virtual Wallet GetWallet(Client client=null)
virtual Wallet Wallet
Gets the current personal wallet In singleplayer this is the campaign bank and in multiplayer this is...
FactionAffiliation GetFactionAffiliation(Identifier factionIdentifier)
void AssignNPCMenuInteraction(Character character, InteractionType interactionType)
virtual bool PurchasedItemRepairs
void SavePets(XElement parentElement=null)
CampaignSettings Settings
SubmarineInfo PendingSubmarineSwitch
readonly NamedEvent< WalletChangedEvent > OnMoneyChanged
IReadOnlyList< Faction > Factions
bool CanAffordNewCharacter(CharacterInfo characterInfo)
Faction GetRandomSecondaryFaction(Rand.RandSync randSync, bool allowEmpty=true)
Returns a random faction based on their SecondaryControlledOutpostPercentage
void HandleSaveAndQuit()
Handles updating store stock, registering event history and relocating items (i.e....
abstract IEnumerable< CoroutineStatus > DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror)
Location GetCurrentDisplayLocation()
The location that's displayed as the "current one" in the map screen. Normally the current outpost or...
override IEnumerable< Mission > Missions
Action OnSaveAndQuit
Triggers when saving and quitting mid-round (as in, not just transferring to a new level)....
override void End(CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None)
int NewCharacterCost(CharacterInfo characterInfo)
void CheckTooManyMissions(Location currentLocation, Client sender)
virtual void EndCampaignProjSpecific()
static bool BlocksInteraction(InteractionType interactionType)
XElement ActiveOrdersElement
abstract void LoadInitialLevel()
Load the first level and start the round after loading a save file
SubmarineInfo GetPredefinedStartOutpost()
virtual bool PurchasedHullRepairs
abstract void Save(XElement element)
readonly record struct SaveInfo(string FilePath, Option< SerializableDateTime > SaveTime, string SubmarineName, ImmutableArray< string > EnabledContentPackageNames) const int MaxMoney
static Faction GetRandomFaction(IEnumerable< Faction > factions, Rand.RandSync randSync, bool secondary=false, bool allowEmpty=true)
const float EndTransitionDuration
void KeepCharactersCloseToOutpost(float deltaTime)
const int ShuttleReplaceCost
static void LeaveUnconnectedSubs(Submarine leavingSub)
virtual bool PurchasedLostShuttles
void RefreshOwnedSubmarines()
void OutpostNPCAttacked(Character npc, Character attacker, AttackResult attackResult)
bool TransferItemsOnSubSwitch
void UpdateStoreStock()
Updates store stock before saving the game
const float HullRepairCostPerDamage
bool TryHireCharacter(Location location, CharacterInfo characterInfo, bool takeMoney=true, Client client=null, bool buyingNewCharacter=false)
override void AddExtraMissions(LevelData levelData)
static List< Submarine > GetSubsToLeaveBehind(Submarine leavingSub)
bool ItemsRelocatedToMainSub
readonly CargoManager CargoManager
static int GetItemRepairCost()
Dictionary< Identifier, List< PurchasedItem > > PurchasedItems
void ClearItemsInBuyCrate()
static ItemContainer GetOrCreateCargoContainerFor(ItemPrefab item, ISpatialEntity cargoRoomOrSpawnPoint, ref List< ItemContainer > availableContainers)
void ClearItemsInSellCrate()
static Vector2 GetCargoPos(Hull hull, ItemPrefab itemPrefab)
void ClearSoldItemsProjSpecific()
void ClearItemsInSellFromSubCrate()
Dictionary< Identifier, List< SoldItem > > SoldItems
static IEnumerable< ItemContainer > FindReusableCargoContainers(IEnumerable< Submarine > subs, IEnumerable< Hull > cargoRooms=null)
void RemoveNegativeAfflictions()
Vector2? OverrideMovement
void SetCustomInteract(Action< Character, Character > onCustomInteract, LocalizedString hudText)
Set an action that's invoked when another character interacts with this one.
CharacterHealth CharacterHealth
CampaignMode.InteractionType CampaignInteractionType
static readonly List< Character > CharacterList
void AddMessage(string rawText, Color color, bool playSound, Identifier identifier=default, int? value=null, float lifetime=3.0f)
static bool DisableControls
static Character? Controlled
Identifier MerchantIdentifier
void DespawnNow(bool createNetworkEvents=true)
bool IsRemotePlayer
Is the character controlled by another human player (should always be false in single player)
Stores information about the Character that is needed between rounds in the menu etc....
bool IsNewHire
Newly hired bot that hasn't spawned yet
CauseOfDeath CauseOfDeath
Responsible for keeping track of the characters in the player crew, saving and loading their orders,...
void RemoveCharacterInfo(CharacterInfo characterInfo)
Remove info of a selected character. The character will not be visible in any menus or the round summ...
IEnumerable< CharacterInfo > GetCharacterInfos()
Note: this only returns AI characters' infos in multiplayer. The infos are used to manage hiring/firi...
void AddCharacterInfo(CharacterInfo characterInfo)
void LoadActiveOrders(XElement element)
void AddSinglePlayerChatMessage(LocalizedString senderName, LocalizedString text, ChatMessageType messageType, Entity sender)
Adds the message to the single player chatbox.
void SaveActiveOrders(XElement element)
virtual Vector2 WorldPosition
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
void RegisterEventHistory(bool registerFinishedOnly=false)
Registers the exhaustible events in the level as exhausted, and adds the current events to the event ...
static FactionAffiliation GetPlayerAffiliationStatus(Faction faction)
Get what kind of affiliation this faction has towards the player depending on who they chose to side ...
static GameSession?? GameSession
static NetworkMember NetworkMember
readonly Identifier Identifier
List< SubmarineInfo > OwnedSubmarines
SubmarineInfo SubmarineInfo
readonly EventManager EventManager
static int GetSalaryFor(IReadOnlyCollection< CharacterInfo > hires)
static readonly List< Hull > HullList
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
ItemInventory OwnInventory
bool NonPlayerTeamInteractable
Use IsPlayerInteractable to also check NonInteractable
Inventory ParentInventory
void SetTransform(Vector2 simPosition, float rotation, bool findNewHull=true, bool setPrevTransform=true)
bool?? SpawnedInCurrentOutpost
static readonly List< Item > ItemList
List< ItemComponent > Components
Entity GetRootInventoryOwner()
override bool TryPutItem(Item item, Character user, IEnumerable< InvSlotType > allowedSlots=null, bool createNetworkEvent=true, bool ignoreCondition=false)
If there is room, puts the item in the inventory and returns true, otherwise returns false
bool DontTransferBetweenSubs
static IEnumerable< DockingPort > List
DockingPort DockingTarget
float FixDurationHighSkill
OutpostGenerationParams ForceOutpostGenerationParams
readonly float Difficulty
const float HuntingGroundsDifficultyThreshold
Minimum difficulty of the level before hunting grounds can appear.
readonly LevelData LevelData
static bool IsLoadedFriendlyOutpost
Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1). Does not take reput...
static bool IsLoadedOutpost
Is there a loaded level set and is it an outpost?
static LinkedSubmarine CreateDummy(Submarine mainSub, Submarine linkedSub)
void SetMerchantFaction(Identifier factionIdentifier)
void RemoveHireableCharacter(CharacterInfo character)
void AddStock(Dictionary< Identifier, List< SoldItem >> items)
readonly List< LocationConnection > Connections
StoreInfo GetStore(Identifier identifier)
void Reset(CampaignMode campaign)
IEnumerable< Mission > SelectedMissions
void RegisterTakenItems(IEnumerable< Item > items)
Mark the items that have been taken from the outpost to prevent them from spawning when re-entering t...
LocalizedString DisplayName
void RemoveStock(Dictionary< Identifier, List< PurchasedItem >> items)
void RegisterKilledCharacters(IEnumerable< Character > characters)
Mark the characters who have been killed to prevent them from spawning when re-entering the outpost
Mersenne Twister based random
int OriginalModuleIndex
The index of the outpost module this entity originally spawned in (-1 if not an outpost item)
static readonly List< MapEntity > MapEntityList
bool IsHidden
Is the entity hidden due to HiddenInGame being enabled or the layer the entity is in being hidden?
IReadOnlyList< Location > EndLocations
List< Location > Locations
static bool LocationOrConnectionWithinDistance(Location startLocation, int maxDistance, Func< Location, bool > criteria, Func< LocationConnection, bool > connectionCriteria=null)
void SetLocation(int index)
void ClearLocationHistory()
Location SelectedLocation
List< LocationConnection > Connections
LocationConnection SelectedConnection
void SelectLocation(int index)
virtual LocalizedString Name
virtual LocalizedString Description
readonly Location[] Locations
static readonly PrefabCollection< MissionPrefab > Prefabs
Marks fields and properties as to be serialized and deserialized by INetSerializableStruct....
ContentPath OutpostFilePath
static void LoadPets(XElement petsElement)
static void SavePets(XElement petsElement)
readonly Identifier Identifier
const float MaxReputationLossFromNPCDamage
Maximum amount of reputation loss you can get from damaging outpost NPCs per round
void AddReputation(float reputationChange, float maxReputationChangePerRound=float.MaxValue)
const float ReputationLossPerNPCDamage
void SetDamage(int sectionIndex, float damage, Character attacker=null, bool createNetworkEvent=true, bool isNetworkEvent=true, bool createExplosionEffect=true, bool createWallDamageProjectiles=false)
float SectionDamage(int sectionIndex)
static List< Structure > WallList
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=null)
override Vector2? WorldPosition
override string ToString()
IEnumerable< Submarine > DockedTo
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
static List< Submarine > Loaded
Rectangle GetDockedBorders(bool allowDifferentTeam=true)
Returns a rect that contains the borders of this sub and all subs docked to it, excluding outposts
readonly Dictionary< Identifier, List< Character > > OutpostNPCs
This class handles all upgrade logic. Storing, applying, checking and validation of upgrades.
static List< WayPoint > WayPointList
DateTime wrapper that tries to offer a reliable string representation that's also human-friendly