5 using Microsoft.Xna.Framework;
7 using System.Collections.Generic;
8 using System.Collections.Immutable;
10 using System.Xml.Linq;
17 internal readonly record
struct PerkCollection(
18 ImmutableArray<DisembarkPerkPrefab> Team1Perks,
19 ImmutableArray<DisembarkPerkPrefab> Team2Perks)
21 public static readonly PerkCollection
Empty =
new PerkCollection(ImmutableArray<DisembarkPerkPrefab>.Empty, ImmutableArray<DisembarkPerkPrefab>.Empty);
23 public void ApplyAll(IReadOnlyCollection<Character> team1Characters, IReadOnlyCollection<Character> team2Characters)
26 bool anyMissionDoesNotLoadSubs = GameMain.GameSession.Missions.Any(
static m => !m.Prefab.LoadSubmarines);
28 foreach (var team1Perk
in Team1Perks)
30 GameAnalyticsManager.AddDesignEvent(
"DisembarkPerk:" + team1Perk.Identifier);
31 foreach (PerkBase behavior
in team1Perk.PerkBehaviors)
33 if (anyMissionDoesNotLoadSubs && !behavior.CanApplyWithoutSubmarine()) {
continue; }
35 if (behavior.Simulation == PerkSimulation.ServerOnly) {
continue; }
37 behavior.ApplyOnRoundStart(team1Characters,
Submarine.MainSubs[0]);
43 foreach (var team2Perk
in Team2Perks)
45 GameAnalyticsManager.AddDesignEvent(
"DisembarkPerk:" + team2Perk.Identifier);
46 foreach (PerkBase behavior
in team2Perk.PerkBehaviors)
48 if (anyMissionDoesNotLoadSubs && !behavior.CanApplyWithoutSubmarine()) {
continue; }
50 if (behavior.Simulation == PerkSimulation.ServerOnly) {
continue; }
52 behavior.ApplyOnRoundStart(team2Characters,
Submarine.MainSubs[1]);
62 public static float MinimumLoadingTime;
84 private readonly List<Mission> missions =
new List<Mission>();
85 public IEnumerable<Mission>
Missions {
get {
return missions; } }
87 private readonly HashSet<Character> casualties =
new HashSet<Character>();
88 public IEnumerable<Character>
Casualties {
get {
return casualties; } }
95 private Dictionary<Option<AccountId>,
int> permadeathsPerAccount =
new Dictionary<Option<AccountId>,
int>();
98 permadeathsPerAccount[accountId] = permadeathsPerAccount.GetValueOrDefault(accountId, 0) + 1;
102 return permadeathsPerAccount.GetValueOrDefault(accountId, 0);
141 if (dummyLocations ==
null)
145 if (dummyLocations ==
null) {
throw new NullReferenceException(
"dummyLocations is null somehow!"); }
146 return dummyLocations[0];
155 if (dummyLocations ==
null)
159 if (dummyLocations ==
null) {
throw new NullReferenceException(
"dummyLocations is null somehow!"); }
160 return dummyLocations[1];
179 partial
void InitProjSpecific();
191 : this(submarineInfo)
200 : this(submarineInfo)
204 GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, settings, missionTypes: missionTypes);
205 EnemySubmarineInfo = enemySub.TryUnwrap(out var enemySubmarine) ? enemySubmarine : submarineInfo;
206 InitOwnedSubs(submarineInfo);
213 : this(submarineInfo)
216 GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, CampaignSettings.Empty, missionPrefabs: missionPrefabs);
217 EnemySubmarineInfo = enemySub.TryUnwrap(out var enemySubmarine) ? enemySubmarine : submarineInfo;
218 InitOwnedSubs(submarineInfo);
228 XElement rootElement = doc.Root ??
throw new NullReferenceException(
"Game session XML element is invalid: document is null.");
232 foreach (var subElement
in rootElement.Elements())
234 switch (subElement.Name.ToString().ToLowerInvariant())
237 case "singleplayercampaign":
240 var campaign = SinglePlayerCampaign.Load(subElement);
241 campaign.LoadNewLevel();
243 InitOwnedSubs(submarineInfo, ownedSubmarines);
245 throw new Exception(
"The server cannot load a single player campaign.");
248 case "multiplayercampaign":
254 mpCampaign.LoadNewLevel();
255 InitOwnedSubs(submarineInfo, ownedSubmarines);
257 SaveUtil.SaveGame(campaignData, isSavingOnLoading:
true);
261 permadeathsPerAccount =
new Dictionary<Option<AccountId>,
int>();
262 foreach (XElement accountElement
in subElement.Elements(
"account"))
264 if (accountElement.Attribute(
"id") is XAttribute accountIdAttr &&
265 accountElement.Attribute(
"permadeathcount") is XAttribute permadeathCountAttr)
269 permadeathsPerAccount[AccountId.Parse(accountIdAttr.Value)] =
int.Parse(permadeathCountAttr.Value);
273 DebugConsole.AddWarning($
"Exception while trying to load permadeath counts!\n{e}\n id: {accountIdAttr}\n permadeathcount: {permadeathCountAttr}");
282 private void InitOwnedSubs(
SubmarineInfo submarineInfo, List<SubmarineInfo>? ownedSubmarines =
null)
291 private GameMode InstantiateGameMode(GameModePreset gameModePreset,
string? seed,
SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable<MissionPrefab>? missionPrefabs =
null, IEnumerable<Identifier>? missionTypes =
null)
293 if (gameModePreset.GameModeType == typeof(CoOpMode))
295 return missionPrefabs !=
null ?
296 new CoOpMode(gameModePreset, missionPrefabs) :
297 new CoOpMode(gameModePreset, missionTypes, seed ?? ToolBox.RandomSeed(8));
299 else if (gameModePreset.GameModeType == typeof(PvPMode))
301 return missionPrefabs !=
null ?
302 new PvPMode(gameModePreset, missionPrefabs) :
303 new PvPMode(gameModePreset, missionTypes, seed ?? ToolBox.RandomSeed(8));
305 else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign))
307 var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings);
308 if (selectedSub !=
null)
310 campaign.Bank.Deduct(selectedSub.Price);
311 campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, 0);
313 if (GameMain.Server?.ServerSettings?.NewCampaignDefaultSalary is { } salary)
315 campaign.Bank.SetRewardDistribution((
int)Math.Round(salary, digits: 0));
322 else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign))
324 var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings);
325 if (selectedSub !=
null)
327 campaign.Bank.TryDeduct(selectedSub.Price);
328 campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, 0);
332 else if (gameModePreset.GameModeType == typeof(TutorialMode))
334 return new TutorialMode(gameModePreset);
336 else if (gameModePreset.GameModeType == typeof(TestGameMode))
338 return new TestGameMode(gameModePreset);
341 else if (gameModePreset.GameModeType == typeof(
GameMode))
343 return new GameMode(gameModePreset);
347 throw new Exception($
"Could not find a game mode of the type \"{gameModePreset.GameModeType}\"");
355 if (forceLocationType ==
null &&
356 forceParams !=
null && forceParams.AllowedLocationTypes.Any() && !forceParams.AllowedLocationTypes.Contains(
"Any".ToIdentifier()))
359 LocationType.
Prefabs.Where(lt => forceParams.AllowedLocationTypes.Contains(lt.Identifier)).GetRandom(rand);
362 List<Faction> factions =
new List<Faction>();
363 foreach (var factionPrefab
in FactionPrefab.Prefabs)
365 factions.Add(
new Faction(
new CampaignMetadata(), factionPrefab));
367 foreach (var location
in dummyLocations)
369 if (location.Type.HasOutpost)
375 return dummyLocations;
385 var dummyLocations =
new Location[2];
386 for (
int i = 0; i < 2; i++)
388 dummyLocations[i] =
Location.
CreateRandom(
new Vector2((
float)rand.NextDouble() * 10000.0f, (
float)rand.NextDouble() * 10000.0f),
null, rand, requireOutpost:
true, forceLocationType);
390 return dummyLocations;
395 if (preset is
null) {
return true; }
435 if (
Campaign is
null) {
return false; }
436 int price = newSubmarine.
GetPrice();
440 GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.
Name);
458 if (
Map?.CurrentLocation ==
null ||
Campaign ==
null) {
return false; }
464 isRadiated |= endLocation.IsRadiated();
479 if (missionPrefab !=
null &&
491 tryCreateFaction(locationType.
Faction, dummyLocations, static (loc, fac) => loc.Faction = fac);
492 tryCreateFaction(locationType.
SecondaryFaction, dummyLocations, static (loc, fac) => loc.SecondaryFaction = fac);
494 static bool tryCreateFaction(Identifier factionIdentifier,
Location[] locations, Action<Location, Faction> setter)
496 if (factionIdentifier.IsEmpty) {
return false; }
497 if (!FactionPrefab.Prefabs.TryGet(factionIdentifier, out var prefab)) {
return false; }
498 if (locations.Length == 0) {
return false; }
500 var newFaction =
new Faction(metadata:
null, prefab);
501 for (
int i = 0; i < locations.Length; i++)
503 setter(locations[i], newFaction);
509 randomLevel =
LevelData.
CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost:
true, biomeId: forceBiome, pvpOnly: pvpOnly);
513 randomLevel ??=
LevelData.
CreateRandom(levelSeed, difficulty, levelGenerationParams, biomeId: forceBiome, pvpOnly: pvpOnly);
520 if (moduleInfo ==
null) {
return false; }
522 var allSuitableOutpostParams = OutpostGenerationParams.OutpostParams
523 .Where(outpostParam => IsOutpostParamsSuitable(outpostParam));
526 var suitableOutpostParams =
527 allSuitableOutpostParams.Where(p => p.AllowedLocationTypes.Any()).GetRandomUnsynced() ??
528 allSuitableOutpostParams.GetRandomUnsynced();
530 bool IsOutpostParamsSuitable(OutpostGenerationParams outpostParams)
532 bool moduleWorksWithOutpostParams = outpostParams.ModuleCounts.Any(moduleCount => moduleInfo.
OutpostModuleInfo.
ModuleFlags.Contains(moduleCount.Identifier));
533 if (!moduleWorksWithOutpostParams) {
return false; }
537 && IsSuitableLocationType(outpostParams.AllowedLocationTypes, locationType.Identifier));
539 bool IsSuitableLocationType(IEnumerable<Identifier> allowedLocationTypes, Identifier locationType)
541 return allowedLocationTypes.None() || allowedLocationTypes.Contains(
"Any".ToIdentifier()) || allowedLocationTypes.Contains(locationType);
545 if (suitableOutpostParams ==
null)
547 DebugConsole.AddWarning(
"No suitable generation parameters found for ForceOutpostModule, skipping outpost generation!");
551 var suitableLocationType = LocationType.Prefabs.Where(locationType =>
552 suitableOutpostParams.AllowedLocationTypes.Contains(locationType.Identifier)).GetRandomUnsynced();
554 if (suitableLocationType ==
null)
556 DebugConsole.AddWarning(
"No suitable location type found for ForceOutpostModule, skipping outpost generation!");
561 var requiredFactionModuleCount = suitableOutpostParams.ModuleCounts.FirstOrDefault(mc => !mc.RequiredFaction.IsEmpty && moduleInfo.
OutpostModuleInfo.
ModuleFlags.Contains(mc.Identifier));
562 Identifier requiredFactionId = requiredFactionModuleCount?.RequiredFaction ?? Identifier.Empty;
564 if (requiredFactionId.IsEmpty)
567 outpostSub = OutpostGenerator.Generate(suitableOutpostParams, suitableLocationType);
568 return outpostSub !=
null;
573 var dummyLocation = dummyLocations[0];
575 if (FactionPrefab.Prefabs.TryGet(requiredFactionId, out FactionPrefab? factionPrefab))
577 if (factionPrefab.ControlledOutpostPercentage > factionPrefab.SecondaryControlledOutpostPercentage)
579 dummyLocation.Faction =
new Faction(
null, factionPrefab);
583 dummyLocation.SecondaryFaction =
new Faction(
null, factionPrefab);
586 outpostSub = OutpostGenerator.Generate(suitableOutpostParams, dummyLocation);
587 return outpostSub !=
null;
596 DateTime startTime = DateTime.Now;
604 DebugConsole.ThrowError(
"Couldn't start game session, submarine not selected.");
609 DebugConsole.ThrowError(
"Couldn't start game session, submarine file corrupted.");
614 DebugConsole.ThrowError(
"Couldn't start game session, saved submarine is empty. The submarine file may be corrupted.");
624 bool loadSubmarine =
GameMode!.
Missions.None(m => !m.Prefab.LoadSubmarines);
665 if (enemySubmarineInfo !=
null)
674 List<Item> items =
new List<Item>();
680 foreach (
Item item
in items)
682 if (item.GetComponent<CircuitBox>() is { } cb)
684 cb.TemporarilyLocked =
true;
687 Wire wire = item.GetComponent<
Wire>();
693 if (levelData !=
null)
698 InitializeLevel(level);
705 GameAnalyticsManager.AddProgressionEvent(
706 GameAnalyticsManager.ProgressionStatus.Start,
711 GameAnalyticsManager.AddDesignEvent(eventId +
"GameMode:" + (
GameMode?.Preset?.Identifier.Value ??
"none"));
712 GameAnalyticsManager.AddDesignEvent(eventId +
"CrewSize:" + (
CrewManager?.GetCharacterInfos()?.Count() ?? 0));
713 foreach (
Mission mission
in missions)
715 GameAnalyticsManager.AddDesignEvent(eventId +
"MissionType:" + (mission.
Prefab.
Type.ToString() ??
"none") +
":" + mission.
Prefab.
Identifier);
722 GameAnalyticsManager.AddDesignEvent(eventId +
"LevelType:" +
Level.
Loaded.
Type.ToString() +
":" + levelId);
726 if (
GameMode is TutorialMode tutorialMode)
728 GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier);
731 GameAnalyticsManager.AddDesignEvent(
"FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier);
734 GameAnalyticsManager.AddDesignEvent($
"{eventId}HintManager:{(HintManager.Enabled ? "Enabled
" : "Disabled")}");
737 if (campaignMode !=
null)
739 GameAnalyticsManager.AddDesignEvent(
"CampaignSettings:RadiationEnabled:" + campaignMode.Settings.RadiationEnabled);
740 GameAnalyticsManager.AddDesignEvent(
"CampaignSettings:WorldHostility:" + campaignMode.Settings.WorldHostility);
741 GameAnalyticsManager.AddDesignEvent(
"CampaignSettings:ShowHuskWarning:" + campaignMode.Settings.ShowHuskWarning);
742 GameAnalyticsManager.AddDesignEvent(
"CampaignSettings:StartItemSet:" + campaignMode.Settings.StartItemSet);
743 GameAnalyticsManager.AddDesignEvent(
"CampaignSettings:MaxMissionCount:" + campaignMode.Settings.MaxMissionCount);
745 GameAnalyticsManager.AddDesignEvent(
"CampaignSettings:RepairFailMultiplier:" + (
int)(campaignMode.Settings.RepairFailMultiplier * 100));
746 GameAnalyticsManager.AddDesignEvent(
"CampaignSettings:FuelMultiplier:" + (
int)(campaignMode.Settings.FuelMultiplier * 100));
747 GameAnalyticsManager.AddDesignEvent(
"CampaignSettings:MissionRewardMultiplier:" + (
int)(campaignMode.Settings.MissionRewardMultiplier * 100));
748 GameAnalyticsManager.AddDesignEvent(
"CampaignSettings:CrewVitalityMultiplier:" + (
int)(campaignMode.Settings.CrewVitalityMultiplier * 100));
749 GameAnalyticsManager.AddDesignEvent(
"CampaignSettings:NonCrewVitalityMultiplier:" + (
int)(campaignMode.Settings.NonCrewVitalityMultiplier * 100));
750 GameAnalyticsManager.AddDesignEvent(
"CampaignSettings:OxygenMultiplier:" + (
int)(campaignMode.Settings.OxygenMultiplier * 100));
751 GameAnalyticsManager.AddDesignEvent(
"CampaignSettings:RepairFailMultiplier:" + (
int)(campaignMode.Settings.RepairFailMultiplier * 100));
752 GameAnalyticsManager.AddDesignEvent(
"CampaignSettings:ShipyardPriceMultiplier:" + (
int)(campaignMode.Settings.ShipyardPriceMultiplier * 100));
753 GameAnalyticsManager.AddDesignEvent(
"CampaignSettings:ShopPriceMultiplier:" + (
int)(campaignMode.Settings.ShopPriceMultiplier * 100));
756 if (firstTimeInBiome)
763 GameAnalyticsManager.AddDesignEvent(
"ServerSettings:RespawnMode:" + serverSettings.RespawnMode);
764 GameAnalyticsManager.AddDesignEvent(
"ServerSettings:IronmanMode:" + serverSettings.IronmanModeActive);
765 GameAnalyticsManager.AddDesignEvent(
"ServerSettings:AllowBotTakeoverOnPermadeath:" + serverSettings.AllowBotTakeoverOnPermadeath);
770 double startDuration = (DateTime.Now - startTime).TotalSeconds;
771 if (startDuration < MinimumLoadingTime)
773 int sleepTime = (int)((MinimumLoadingTime - startDuration) * 1000);
774 DebugConsole.NewMessage($
"Stalling round start by {sleepTime / 1000.0f} s (minimum loading time set to {MinimumLoadingTime})...", Color.Magenta);
775 System.Threading.Thread.Sleep(sleepTime);
779 var existingRoundSummary = GUIMessageBox.MessageBoxes.Find(mb => mb.UserData is RoundSummary)?.UserData as RoundSummary;
780 if (existingRoundSummary?.ContinueButton !=
null)
782 existingRoundSummary.ContinueButton.Visible =
true;
791 GUI.AddMessage(
"", Color.Transparent, 3.0f, playSound:
false);
794 GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound:
false);
795 GUI.AddMessage(TextManager.AddPunctuation(
':', TextManager.Get(
"Destination"),
EndLocation.
DisplayName), Color.CadetBlue, playSound:
false);
796 var missionsToShow = missions.Where(m => m.Prefab.ShowStartMessage);
797 if (missionsToShow.Count() > 1)
799 string joinedMissionNames =
string.Join(
", ", missions.Select(m => m.Name));
800 GUI.AddMessage(TextManager.AddPunctuation(
':', TextManager.Get(
"Mission"), joinedMissionNames), Color.CadetBlue, playSound:
false);
804 var mission = missionsToShow.FirstOrDefault();
805 GUI.AddMessage(TextManager.AddPunctuation(
':', TextManager.Get(
"Mission"), mission?.Name ?? TextManager.Get(
"None")), Color.CadetBlue, playSound:
false);
810 GUI.AddMessage(TextManager.AddPunctuation(
':', TextManager.Get(
"Location"),
StartLocation.
DisplayName), Color.CadetBlue, playSound:
false);
814 ReadyCheck.ReadyCheckCooldown = DateTime.MinValue;
815 GUI.PreventPauseMenuToggle =
false;
816 HintManager.OnRoundStarted();
818 GameMain.LuaCs.Hook.Call(
"roundStart");
819 EnableEventLogNotificationIcon(enabled:
false);
821 if (campaignMode is { ItemsRelocatedToMainSub:
true })
824 GameMain.Server.SendChatMessage(TextManager.Get(
"itemrelocated").Value,
ChatMessageType.ServerMessageBoxInGame);
826 if (campaignMode.IsSinglePlayer)
828 new GUIMessageBox(
string.Empty, TextManager.Get(
"itemrelocated"));
831 campaignMode.ItemsRelocatedToMainSub =
false;
835 if (campaignMode is { DivingSuitWarningShown:
false } &&
839 CoroutineManager.Invoke(() =>
new GUIMessageBox(TextManager.Get(
"warning"), TextManager.Get(
"hint.upgradedivingsuits")), delay: 5.0f);
841 campaignMode.DivingSuitWarningShown =
true;
845 private void InitializeLevel(Level? level)
849 StatusEffect.StopAll();
851 bool forceDocking =
false;
853 GameMain.LightManager.LosEnabled = (GameMain.Client ==
null || GameMain.Client.CharacterInfo !=
null) && !GameMain.DevMode;
854 if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; }
855 if (GameMain.Client ==
null) { GameMain.LightManager.LosMode = GameSettings.CurrentConfig.Graphics.LosMode; }
856 forceDocking = GameMode is TutorialMode;
858 LevelData = level?.LevelData;
861 PlaceSubAtInitialPosition(Submarine, Level, placeAtStart:
true, forceDocking: forceDocking);
866 if (sub.Info.IsOutpost || sub.Info.IsBeacon || sub.Info.IsWreck)
868 sub.DisableObstructedWayPoints();
872 Entity.Spawner =
new EntitySpawner();
874 if (GameMode !=
null)
877 missions.AddRange(GameMode.
Missions);
879 foreach (Mission mission
in missions)
881 int prevEntityCount = Entity.GetEntities().Count;
882 mission.Start(Level.Loaded);
883 if (GameMain.NetworkMember !=
null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count != prevEntityCount)
885 DebugConsole.ThrowError(
886 $
"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " +
887 "The clients should not instantiate entities themselves when starting the mission," +
888 " but instead the server should inform the client of the spawned entities using Mission.ServerWriteInitial.");
893 ObjectiveManager.ResetObjectives();
895 EventManager?.StartRound(Level.Loaded);
896 AchievementManager.OnStartRound(Level?.LevelData.Biome);
900 if (GameMain.NetworkMember ==
null)
906 if (GameMain.GameSession.Missions.None(m => !m.Prefab.AllowOutpostNPCs))
910 Level.SpawnCorpses();
911 Level.PrepareBeaconStation();
916 foreach (Submarine sub
in Submarine.Loaded)
918 if (sub?.Info?.OutpostGenerationParams !=
null)
920 OutpostGenerator.SpawnNPCs(StartLocation, sub);
924 AutoItemPlacer.SpawnItems(Campaign?.Settings.StartItemSet);
926 if (GameMode is MultiPlayerCampaign mpCampaign)
928 mpCampaign.UpgradeManager.ApplyUpgrades();
929 mpCampaign.UpgradeManager.SanityCheckUpgrades();
933 CreatureMetrics.RecentlyEncountered.Clear();
935 GameMain.GameScreen.Cam.Position =
Character.Controlled?.WorldPosition ??
Submarine.MainSub?.WorldPosition ??
Submarine.Loaded.First().WorldPosition;
936 RoundDuration = 0.0f;
937 GameMain.ResetFrameTime();
943 if (level ==
null || sub ==
null)
953 if (spawnPoint !=
null)
960 else if (outpost !=
null)
968 new Vector2(0.0f, outpostBorders.Height / 2 + subBorders.Height / 2));
971 float closestDistance = 0.0f;
990 if ((myPort ==
null || dist < closestDistance || port.
MainDockingPort) && !(myPort?.MainDockingPort ??
false))
993 closestDistance = dist;
997 if (myPort !=
null && outPostPort !=
null)
1000 Vector2 spawnPos = (outPostPort.Item.WorldPosition - portDiff) - Vector2.UnitY * outPostPort.DockedDistance;
1006 myPort.Dock(outPostPort);
1007 myPort.Lock(isNetworkMessage:
true, applyEffects:
false);
1011 sub.
SetPosition(spawnPos - Vector2.UnitY * 100.0f);
1036 if (ls.
Sub ==
null || ls.
Submarine != sub) {
continue; }
1046 RoundDuration += deltaTime;
1050 for (
int i = missions.Count - 1; i >= 0; i--)
1052 missions[i].Update(deltaTime);
1054 UpdateProjSpecific(deltaTime);
1059 if (index < 0 || index >= missions.Count) {
return null; }
1060 return missions[index];
1065 return missions.IndexOf(mission);
1070 List<Mission> sortedMissions =
new List<Mission>();
1071 foreach (Identifier missionId
in missionIdentifiers)
1073 var matchingMission = missions.Find(m => m.Prefab.Identifier == missionId);
1074 if (matchingMission ==
null) {
continue; }
1075 sortedMissions.Add(matchingMission);
1076 missions.Remove(matchingMission);
1078 missions.AddRange(sortedMissions);
1081 partial
void UpdateProjSpecific(
float deltaTime);
1094 if (result !=
null)
return ImmutableHashSet.
Create(result);
1098 IEnumerable<Character> players;
1099 IEnumerable<Character> bots;
1100 HashSet<Character> characters =
new HashSet<Character>();
1103 players = GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info !=
null && !c.IsDead);
1104 bots = crewManager.GetCharacters().Where(c => !c.IsRemotePlayer);
1106 players = crewManager.GetCharacters().Where(
static c => c.IsPlayer);
1107 bots = crewManager.GetCharacters().Where(
static c => c.IsBot);
1111 foreach (Character bot
in bots) { characters.Add(bot); }
1116 foreach (Character player
in players) { characters.Add(player); }
1119 return characters.ToImmutableHashSet();
1123 private double LastEndRoundErrorMessageTime;
1141 ImmutableHashSet<Character> crewCharacters = GetSessionCrewCharacters(
CharacterType.Both);
1142 int prevMoney = GetAmountOfMoney(crewCharacters);
1143 foreach (
Mission mission
in missions)
1148 foreach (
Character character
in crewCharacters)
1155 if (missions.Any(m => m.Completed))
1157 foreach (
Character character
in crewCharacters)
1163 if (missions.All(m => m.Completed))
1165 foreach (
Character character
in crewCharacters)
1175 if (GUI.PauseMenuOpen)
1177 GUI.TogglePauseMenu();
1183 DeathPrompt?.Close();
1184 DeathPrompt.CloseBotPanel();
1186 GUI.PreventPauseMenuToggle =
true;
1190 GUI.ClearMessages();
1191 GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary);
1192 GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(
this, endMessage, transitionType, traitorResults);
1193 GUIMessageBox.MessageBoxes.Add(summaryFrame);
1194 RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame);
return true; };
1198 TabMenu.OnRoundEnded();
1199 GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as
string ==
"ConversationAction" || ReadyCheck.IsReadyCheck(mb));
1200 ObjectiveManager.ResetUI();
1203 AchievementManager.OnRoundEnded(
this);
1221 GameAnalyticsManager.AddProgressionEvent(
1222 success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail,
1226 LogEndRoundStats(eventId, traitorResults);
1229 GameAnalyticsManager.AddDesignEvent(eventId +
"MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney);
1230 campaignMode.TotalPlayTime += RoundDuration;
1233 HintManager.OnRoundEnded();
1239 string errorMsg =
"Unknown error while ending the round.";
1240 DebugConsole.ThrowError(errorMsg, e);
1241 GameAnalyticsManager.AddErrorEventOnce(
"GameSession.EndRound:UnknownError", GameAnalyticsManager.ErrorSeverity.Error, errorMsg +
"\n" + e.StackTrace);
1243 if (Timing.TotalTime > LastEndRoundErrorMessageTime + 1.0)
1246 LastEndRoundErrorMessageTime = Timing.TotalTime;
1252 RoundEnding =
false;
1255 int GetAmountOfMoney(IEnumerable<Character> crew)
1261 null => campaign.Bank.Balance,
1262 _ => crew.Sum(c => c.Wallet.Balance) + campaign.Bank.Balance
1271 return PerkCollection.Empty;
1274 var team1Builder = ImmutableArray.CreateBuilder<DisembarkPerkPrefab>();
1275 var team2Builder = ImmutableArray.CreateBuilder<DisembarkPerkPrefab>();
1277 foreach (Identifier coalitionPerk
in serverSettings.SelectedCoalitionPerks)
1279 if (!DisembarkPerkPrefab.Prefabs.TryGet(coalitionPerk, out DisembarkPerkPrefab? disembarkPerk)) {
continue; }
1280 team1Builder.Add(disembarkPerk);
1283 foreach (Identifier separatistsPerk
in serverSettings.SelectedSeparatistsPerks)
1285 if (!DisembarkPerkPrefab.Prefabs.TryGet(separatistsPerk, out DisembarkPerkPrefab? disembarkPerk)) {
continue; }
1286 team2Builder.Add(disembarkPerk);
1289 return new PerkCollection(team1Builder.ToImmutable(), team2Builder.ToImmutable());
1298 PerkCollection perks = GetPerks();
1300 int team1TotalCost = GetTotalCost(perks.Team1Perks);
1301 if (team1TotalCost > settings.DisembarkPointAllowance)
1308 int team2TotalCost = GetTotalCost(perks.Team2Perks);
1309 if (team2TotalCost > settings.DisembarkPointAllowance)
1317 int GetTotalCost(ImmutableArray<DisembarkPerkPrefab> perksToCheck)
1321 if (ShouldIgnorePerksThatCanNotApplyWithoutSubmarine(preset, missionTypes))
1323 perksToCheck = perksToCheck.Where(
static p => p.PerkBehaviors.All(
static b => b.CanApplyWithoutSubmarine())).ToImmutableArray();
1326 return perksToCheck.Sum(
static p => p.Cost);
1335 foreach (var missionType
in missionTypesToCheck)
1339 if (missionPrefab.LoadSubmarines)
1354 GameAnalyticsManager.AddDesignEvent(eventId +
"Submarine:" + (
Submarine.
MainSub?.
Info?.
Name ??
"none"), RoundDuration);
1356 GameAnalyticsManager.AddDesignEvent(eventId +
"GameMode:" + (
GameMode?.Name.Value ??
"none"), RoundDuration);
1357 GameAnalyticsManager.AddDesignEvent(eventId +
"CrewSize:" + (
CrewManager?.GetCharacterInfos()?.Count() ?? 0), RoundDuration);
1358 foreach (
Mission mission
in missions)
1360 GameAnalyticsManager.AddDesignEvent(eventId +
"MissionType:" + (mission.
Prefab.
Type.ToString() ??
"none") +
":" + mission.
Prefab.
Identifier +
":" + (mission.
Completed ?
"Completed" :
"Failed"), RoundDuration);
1362 if (!ContentPackageManager.ModsEnabled)
1369 GameAnalyticsManager.AddDesignEvent(eventId +
"LevelType:" + (
Level.
Loaded?.
Type.ToString() ??
"none" +
":" + levelId), RoundDuration);
1401 if (traitorResults.HasValue)
1403 GameAnalyticsManager.AddDesignEvent($
"TraitorEvent:{traitorResults.Value.TraitorEventIdentifier}:{traitorResults.Value.ObjectiveSuccessful}");
1404 GameAnalyticsManager.AddDesignEvent($
"TraitorEvent:{traitorResults.Value.TraitorEventIdentifier}:{(traitorResults.Value.VotedCorrectTraitor ? "TraitorIdentifier
" : "TraitorUnidentified
")}");
1411 string characterType =
"Unknown";
1414 characterType =
"Bot";
1418 characterType =
"Player";
1420 GameAnalyticsManager.AddDesignEvent(
"TimeSpentOnDevices:" + (
GameMode?.Preset?.Identifier.Value ??
"none") +
":" + characterType +
":" + (c.
Info?.
Job?.
Prefab.
Identifier.Value ??
"NoJob") +
":" + itemSelectedDuration.Key.Identifier, itemSelectedDuration.Value);
1424 if (
GameMode is TutorialMode tutorialMode)
1426 GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier);
1429 GameAnalyticsManager.AddDesignEvent(
"FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier);
1432 GameAnalyticsManager.AddDesignEvent(eventId +
"TimeSpentCleaning", TimeSpentCleaning);
1433 GameAnalyticsManager.AddDesignEvent(eventId +
"TimeSpentPainting", TimeSpentPainting);
1434 TimeSpentCleaning = TimeSpentPainting = 0.0;
1442 casualties.Add(character);
1451 casualties.Remove(character);
1461 if (!contentPackageNames.Any()) {
return true; }
1463 List<string> missingPackages =
new List<string>();
1464 foreach (
string packageName
in contentPackageNames)
1466 if (!ContentPackageManager.EnabledPackages.All.Any(cp => cp.NameMatches(packageName)))
1468 missingPackages.Add(packageName);
1471 List<string> excessPackages =
new List<string>();
1472 foreach (
ContentPackage cp
in ContentPackageManager.EnabledPackages.All)
1475 if (!contentPackageNames.Any(p => cp.
NameMatches(p)))
1477 excessPackages.Add(cp.
Name);
1481 bool orderMismatch =
false;
1482 if (missingPackages.Count == 0 && missingPackages.Count == 0)
1485 for (
int i = 0; i < contentPackageNames.Count && i < enabledPackages.Length; i++)
1487 if (!enabledPackages[i].NameMatches(contentPackageNames[i]))
1489 orderMismatch =
true;
1495 if (!orderMismatch && missingPackages.Count == 0 && excessPackages.Count == 0) {
return true; }
1497 if (missingPackages.Count == 1)
1499 errorMsg = TextManager.GetWithVariable(
"campaignmode.missingcontentpackage",
"[missingcontentpackage]", missingPackages[0]);
1501 else if (missingPackages.Count > 1)
1503 errorMsg = TextManager.GetWithVariable(
"campaignmode.missingcontentpackages",
"[missingcontentpackages]",
string.Join(
", ", missingPackages));
1505 if (excessPackages.Count == 1)
1507 if (!errorMsg.IsNullOrEmpty()) { errorMsg +=
"\n"; }
1508 errorMsg += TextManager.GetWithVariable(
"campaignmode.incompatiblecontentpackage",
"[incompatiblecontentpackage]", excessPackages[0]);
1510 else if (excessPackages.Count > 1)
1512 if (!errorMsg.IsNullOrEmpty()) { errorMsg +=
"\n"; }
1513 errorMsg += TextManager.GetWithVariable(
"campaignmode.incompatiblecontentpackages",
"[incompatiblecontentpackages]",
string.Join(
", ", excessPackages));
1517 if (!errorMsg.IsNullOrEmpty()) { errorMsg +=
"\n"; }
1518 errorMsg += TextManager.GetWithVariable(
"campaignmode.contentpackageordermismatch",
"[loadorder]",
string.Join(
", ", contentPackageNames));
1524 public void Save(
string filePath,
bool isSavingOnLoading)
1528 throw new NotSupportedException(
"GameSessions can only be saved when playing in a campaign mode.");
1531 XDocument doc =
new XDocument(
new XElement(
"Gamesession"));
1532 XElement rootElement = doc.Root ??
throw new NullReferenceException(
"Game session XML element is invalid: document is null.");
1535 #warning TODO: after this gets on main, replace savetime with the commented line
1538 rootElement.Add(
new XAttribute(
"currentlocation",
Map?.CurrentLocation?.NameIdentifier.Value ??
string.Empty));
1539 rootElement.Add(
new XAttribute(
"currentlocationnameformatindex",
Map?.CurrentLocation?.NameFormatIndex ?? -1));
1540 rootElement.Add(
new XAttribute(
"locationtype",
Map?.CurrentLocation?.Type?.Identifier ?? Identifier.Empty));
1548 bool hasNewPendingSub = Campaign.PendingSubmarineSwitch !=
null &&
1550 if (hasNewPendingSub)
1552 Campaign.SwitchSubs();
1556 if (OwnedSubmarines !=
null)
1558 List<string> ownedSubmarineNames =
new List<string>();
1559 var ownedSubsElement =
new XElement(
"ownedsubmarines");
1560 rootElement.Add(ownedSubsElement);
1561 foreach (var ownedSub
in OwnedSubmarines)
1563 ownedSubsElement.Add(
new XElement(
"sub",
new XAttribute(
"name", ownedSub.Name)));
1566 if (
Map !=
null) { rootElement.Add(
new XAttribute(
"mapseed",
Map.
Seed)); }
1567 rootElement.Add(
new XAttribute(
"selectedcontentpackagenames",
1570 XElement permadeathsElement =
new XElement(
"permadeaths");
1571 foreach (var kvp
in permadeathsPerAccount)
1573 if (kvp.Key.TryUnwrap(out AccountId? accountId))
1575 permadeathsElement.Add(
1576 new XElement(
"account"),
1577 new XAttribute(
"id", accountId.StringRepresentation),
1578 new XAttribute(
"permadeathcount", kvp.Value));
1581 rootElement.Add(permadeathsElement);
1585 doc.SaveSafe(filePath, throwExceptions:
true);
AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character....
static void ClearAllEffects()
Removes all the effects of the prefab (including the sounds and other assets defined in them)....
static void LoadAllEffectsAndTreatmentSuitabilities()
Should be called before each round: loads all StatusEffects and refreshes treatment suitabilities.
virtual bool TryPurchase(Client client, int price)
Faction GetRandomFaction(Rand.RandSync randSync, bool allowEmpty=true)
Returns a random faction based on their ControlledOutpostPercentage
SubmarineInfo PendingSubmarineSwitch
bool TransferItemsOnSubSwitch
void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, bool hasAi=true, RagdollParams ragdoll=null, bool spawnInitialItems=true)
Create a new character
Dictionary< ItemPrefab, double > ItemSelectedDurations
bool NameMatches(Identifier name)
bool HasMultiplayerSyncedContent
Does the content package include some content that needs to match between all players in multiplayer.
Responsible for keeping track of the characters in the player crew, saving and loading their orders,...
IEnumerable< Character > GetCharacters()
void TriggerOnEndRoundActions()
void Update(float deltaTime)
readonly EventLog EventLog
static NetLobbyScreen NetLobbyScreen
static readonly Version Version
static GameScreen GameScreen
static NetworkMember NetworkMember
static GameSession GameSession
virtual void ShowStartMessage()
virtual IEnumerable< Mission > Missions
virtual void AddExtraMissions(LevelData levelData)
virtual void End(CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None)
virtual void Update(float deltaTime)
static GameModePreset Mission
static GameModePreset PvP
readonly bool IsSinglePlayer
static GameModePreset Sandbox
readonly Identifier Identifier
Mission? GetMission(int index)
void StartRound(LevelData? levelData, bool mirrorLevel=false, SubmarineInfo? startOutpost=null, SubmarineInfo? endOutpost=null)
List< SubmarineInfo > OwnedSubmarines
CharacterTeamType? WinningTeam
static void PlaceSubAtInitialPosition(Submarine? sub, Level? level, bool placeAtStart=true, bool forceDocking=false)
bool IsRunning
Is a round currently running?
static Location[] CreateDummyLocations(string seed, LocationType? forceLocationType=null)
void LogEndRoundStats(string eventId, TraitorManager.TraitorResults? traitorResults=null)
GameSession(SubmarineInfo submarineInfo, Option< SubmarineInfo > enemySub, GameModePreset gameModePreset, string? seed=null, IEnumerable< MissionPrefab >? missionPrefabs=null)
Start a new GameSession with a specific pre-selected mission.
GameSession(SubmarineInfo submarineInfo, Option< SubmarineInfo > enemySub, CampaignDataPath dataPath, GameModePreset gameModePreset, CampaignSettings settings, string? seed=null, IEnumerable< Identifier >? missionTypes=null)
Start a new GameSession. Will be saved to the specified save path (if playing a game mode that can be...
static bool ShouldIgnorePerksThatCanNotApplyWithoutSubmarine(GameModePreset preset, IEnumerable< Identifier > missionTypes)
static PerkCollection GetPerks()
bool IsSubmarineOwned(SubmarineInfo query)
void EnforceMissionOrder(List< Identifier > missionIdentifiers)
static ImmutableHashSet< Character > GetSessionCrewCharacters(CharacterType type)
Returns a list of crew characters currently in the game with a given filter.
CampaignDataPath DataPath
void Save(string filePath, bool isSavingOnLoading)
IEnumerable< Mission > Missions
static bool IsCompatibleWithEnabledContentPackages(IList< string > contentPackageNames, out LocalizedString errorMsg)
bool IsCurrentLocationRadiated()
bool TryPurchaseSubmarine(SubmarineInfo newSubmarine, Client? client=null)
static bool ShouldApplyDisembarkPoints(GameModePreset? preset)
SubmarineInfo SubmarineInfo
int PermadeathCountForAccount(Option< AccountId > accountId)
void ReviveCharacter(Character character)
void KillCharacter(Character character)
IEnumerable< Character > Casualties
SubmarineInfo EnemySubmarineInfo
static Location[] CreateDummyLocations(LevelData levelData, LocationType? forceLocationType=null)
static bool ValidatedDisembarkPoints(GameModePreset preset, IEnumerable< Identifier > missionTypes)
readonly EventManager EventManager
int GetMissionIndex(Mission mission)
void StartRound(string levelSeed, float? difficulty=null, LevelGenerationParams? levelGenerationParams=null, Identifier forceBiome=default)
void EndRound(string endMessage, CampaignMode.TransitionType transitionType=CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults=null)
SubmarineInfo? ForceOutpostModule
void Update(float deltaTime)
GameSession(SubmarineInfo submarineInfo, List< SubmarineInfo > ownedSubmarines, XDocument doc, CampaignDataPath campaignData)
Load a game session from the specified XML document. The session will be saved to the specified path.
void IncrementPermadeath(Option< AccountId > accountId)
void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, Client? client=null)
Switch to another submarine. The sub is loaded when the next round starts.
static readonly List< Item > ItemList
static IEnumerable< DockingPort > List
DockingPort DockingTarget
static readonly Dictionary< int, GridInfo > Grids
static readonly HashSet< Connection > ChangedConnections
static LevelData CreateRandom(string seed="", float? difficulty=null, LevelGenerationParams generationParams=null, Identifier biomeId=default, bool requireOutpost=false, bool pvpOnly=false)
OutpostGenerationParams ForceOutpostGenerationParams
LevelGenerationParams GenerationParams
readonly LevelData LevelData
float GetRealWorldDepth(float worldPositionY)
Calculate the "real" depth in meters from the surface of Europa (the value you see on the nav termina...
static Level Generate(LevelData levelData, bool mirror, Location startLocation, Location endLocation, SubmarineInfo startOutpost=null, SubmarineInfo endOutpost=null)
void SetPositionRelativeToMainSub()
ushort OriginalLinkedToID
static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType forceLocationType=null, IEnumerable< Location > existingLocations=null)
LocalizedString DisplayName
Identifier Faction
If set, forces the location to be assigned to this faction. Set to "None" if you don't want the locat...
Identifier SecondaryFaction
If set, forces the location to be assigned to this secondary faction. Set to "None" if you don't want...
static readonly PrefabCollection< LocationType > Prefabs
object Call(string name, params object[] args)
Mersenne Twister based random
static readonly List< MapEntity > MapEntityList
Map(CampaignSettings settings)
Location SelectedLocation
List< LocationConnection > Connections
readonly string StringRepresentation
readonly MissionPrefab Prefab
virtual void SetLevel(LevelData level)
void End()
End the mission and give a reward if it was completed successfully
static IEnumerable< Identifier > ValidateMissionTypes(IEnumerable< Identifier > missionTypes, Dictionary< Identifier, Type > missionClasses)
Returns the mission types that are valid for the given mission classes (e.g. all mission types suitab...
static readonly Dictionary< Identifier, Type > CoOpMissionClasses
The keys here are for backwards compatibility, tying the old mission types to the appropriate class....
static readonly PrefabCollection< MissionPrefab > Prefabs
static readonly Dictionary< Identifier, Type > PvPMissionClasses
The keys here are for backwards compatibility, tying the old mission types to the appropriate class....
readonly List<(Identifier from, Identifier to)> AllowedConnectionTypes
The mission can only be received when travelling from a location of the first type to a location of t...
readonly Identifier RequiredLocationFaction
The mission can only happen in locations owned by this faction. In the mission mode,...
readonly List< Identifier > AllowedLocationTypes
The mission can only be received in these location types
static MultiPlayerCampaign LoadNew(XElement element)
override IReadOnlyList< Client > ConnectedClients
TraitorManager TraitorManager
void SendChatMessage(string message, ChatMessageType? type=null, Client senderClient=null, Character senderCharacter=null, PlayerConnectionChangeType changeType=PlayerConnectionChangeType.None, ChatMode chatMode=ChatMode.None)
Add the message to the chatbox and pass it to all clients who can receive it
IEnumerable< Identifier > AllowedLocationTypes
IEnumerable< Identifier > ModuleFlags
readonly Identifier Identifier
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
List< Item > GetItems(bool alsoFromConnectedSubs)
static readonly Submarine[] MainSubs
override Vector2? WorldPosition
void EnableMaintainPosition()
IEnumerable< Submarine > DockedTo
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
Rectangle GetDockedBorders(bool allowDifferentTeam=true)
Returns a rect that contains the borders of this sub and all subs docked to it, excluding outposts
void SetPosition(Vector2 position, List< Submarine > checkd=null, bool forceUndockFromStaticSubmarines=true)
Vector2 FindSpawnPos(Vector2 spawnPos, Point? submarineSize=null, float subDockingPortOffset=0.0f, int verticalMoveDir=0)
Attempt to find a spawn position close to the specified position where the sub doesn't collide with w...
bool IsVanillaSubmarine()
XElement SubmarineElement
OutpostGenerationParams OutpostGenerationParams
int GetPrice(Location location=null, ImmutableHashSet< Character > characterList=null)
OutpostModuleInfo OutpostModuleInfo
static List< WayPoint > WayPointList
@ Character
Characters only
DateTime wrapper that tries to offer a reliable string representation that's also human-friendly
static SerializableDateTime UtcNow