Introduces PhysicsBodyQueue to safely defer physics body creation to the main thread, addressing thread-safety issues with Farseer Physics during parallel updates. Updates LevelResource, TriggerComponent, BallastFloraBehavior, and MapEntity to use the queue for all physics body creation and refresh operations, ensuring they are processed outside of parallel loops. Also adds cleanup of the queue at round end.
1704 lines
77 KiB
C#
1704 lines
77 KiB
C#
#nullable enable
|
|
|
|
using Barotrauma.IO;
|
|
using Barotrauma.Items.Components;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
using Barotrauma.Networking;
|
|
using Barotrauma.Extensions;
|
|
using Barotrauma.PerkBehaviors;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
internal readonly record struct PerkCollection(
|
|
ImmutableArray<DisembarkPerkPrefab> Team1Perks,
|
|
ImmutableArray<DisembarkPerkPrefab> Team2Perks)
|
|
{
|
|
public static readonly PerkCollection Empty = new PerkCollection(ImmutableArray<DisembarkPerkPrefab>.Empty, ImmutableArray<DisembarkPerkPrefab>.Empty);
|
|
|
|
public void ApplyAll(IReadOnlyCollection<Character> team1Characters, IReadOnlyCollection<Character> team2Characters)
|
|
{
|
|
// Usually there should only be 1 mission active on pvp and mission modes
|
|
bool anyMissionDoesNotLoadSubs = GameMain.GameSession.Missions.Any(static m => !m.Prefab.LoadSubmarines);
|
|
|
|
foreach (var team1Perk in Team1Perks)
|
|
{
|
|
GameAnalyticsManager.AddDesignEvent("DisembarkPerk:" + team1Perk.Identifier);
|
|
foreach (PerkBase behavior in team1Perk.PerkBehaviors)
|
|
{
|
|
if (anyMissionDoesNotLoadSubs && !behavior.CanApplyWithoutSubmarine()) { continue; }
|
|
#if CLIENT
|
|
if (behavior.Simulation == PerkSimulation.ServerOnly) { continue; }
|
|
#endif
|
|
behavior.ApplyOnRoundStart(team1Characters, Submarine.MainSubs[0]);
|
|
}
|
|
}
|
|
|
|
if (Submarine.MainSubs[1] is not null && GameMain.GameSession.GameMode is PvPMode)
|
|
{
|
|
foreach (var team2Perk in Team2Perks)
|
|
{
|
|
GameAnalyticsManager.AddDesignEvent("DisembarkPerk:" + team2Perk.Identifier);
|
|
foreach (PerkBase behavior in team2Perk.PerkBehaviors)
|
|
{
|
|
if (anyMissionDoesNotLoadSubs && !behavior.CanApplyWithoutSubmarine()) { continue; }
|
|
#if CLIENT
|
|
if (behavior.Simulation == PerkSimulation.ServerOnly) { continue; }
|
|
#endif
|
|
behavior.ApplyOnRoundStart(team2Characters, Submarine.MainSubs[1]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
partial class GameSession
|
|
{
|
|
#if DEBUG
|
|
public static float MinimumLoadingTime;
|
|
#endif
|
|
|
|
public enum InfoFrameTab { Crew, Mission, MyCharacter, Traitor };
|
|
|
|
public Version LastSaveVersion { get; set; } = GameMain.Version;
|
|
|
|
public readonly EventManager EventManager;
|
|
|
|
public GameMode? GameMode;
|
|
|
|
//two locations used as the start and end in the MP mode
|
|
private Location[]? dummyLocations;
|
|
public CrewManager? CrewManager;
|
|
|
|
public float RoundDuration
|
|
{
|
|
get; private set;
|
|
}
|
|
|
|
public double TimeSpentCleaning, TimeSpentPainting;
|
|
|
|
private readonly List<Mission> missions = new List<Mission>();
|
|
public IEnumerable<Mission> Missions { get { return missions; } }
|
|
|
|
private readonly HashSet<Character> casualties = new HashSet<Character>();
|
|
public IEnumerable<Character> Casualties { get { return casualties; } }
|
|
|
|
/// <summary>
|
|
/// Permadeaths per MP account are stored currently just for an achievement ("getoutalive").
|
|
/// The dictionary stores Option<AccountId> directly just to keep the code using it simpler and leaner, but if
|
|
/// this is ever used for something else too, feel free to refactor it to use actual AccountIds.
|
|
/// </summary>
|
|
private Dictionary<Option<AccountId>, int> permadeathsPerAccount = new Dictionary<Option<AccountId>, int>();
|
|
public void IncrementPermadeath(Option<AccountId> accountId)
|
|
{
|
|
permadeathsPerAccount[accountId] = permadeathsPerAccount.GetValueOrDefault(accountId, 0) + 1;
|
|
}
|
|
public int PermadeathCountForAccount(Option<AccountId> accountId)
|
|
{
|
|
return permadeathsPerAccount.GetValueOrDefault(accountId, 0);
|
|
}
|
|
|
|
public CharacterTeamType? WinningTeam;
|
|
|
|
/// <summary>
|
|
/// Is a round currently running?
|
|
/// </summary>
|
|
public bool IsRunning { get; private set; }
|
|
|
|
public bool RoundEnding { get; private set; }
|
|
|
|
public Level? Level { get; private set; }
|
|
public LevelData? LevelData { get; private set; }
|
|
|
|
public bool MirrorLevel { get; private set; }
|
|
|
|
public Map? Map
|
|
{
|
|
get
|
|
{
|
|
return (GameMode as CampaignMode)?.Map;
|
|
}
|
|
}
|
|
|
|
public CampaignMode? Campaign
|
|
{
|
|
get
|
|
{
|
|
return GameMode as CampaignMode;
|
|
}
|
|
}
|
|
|
|
|
|
public Location StartLocation
|
|
{
|
|
get
|
|
{
|
|
if (Map != null) { return Map.CurrentLocation; }
|
|
if (dummyLocations == null)
|
|
{
|
|
dummyLocations = LevelData == null ? CreateDummyLocations(seed: string.Empty) : CreateDummyLocations(LevelData);
|
|
}
|
|
if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); }
|
|
return dummyLocations[0];
|
|
}
|
|
}
|
|
|
|
public Location EndLocation
|
|
{
|
|
get
|
|
{
|
|
if (Map != null) { return Map.SelectedLocation; }
|
|
if (dummyLocations == null)
|
|
{
|
|
dummyLocations = LevelData == null ? CreateDummyLocations(seed: string.Empty) : CreateDummyLocations(LevelData);
|
|
}
|
|
if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); }
|
|
return dummyLocations[1];
|
|
}
|
|
}
|
|
|
|
public SubmarineInfo SubmarineInfo { get; set; }
|
|
public SubmarineInfo EnemySubmarineInfo { get; set; }
|
|
|
|
public SubmarineInfo? ForceOutpostModule;
|
|
|
|
public List<SubmarineInfo> OwnedSubmarines = new List<SubmarineInfo>();
|
|
|
|
public Submarine? Submarine { get; set; }
|
|
|
|
private readonly HashSet<(CharacterTeamType team, Identifier identifier)> unlockedRecipes = new HashSet<(CharacterTeamType, Identifier)>();
|
|
public IEnumerable<(CharacterTeamType, Identifier)> UnlockedRecipes => unlockedRecipes;
|
|
|
|
public CampaignDataPath DataPath { get; set; }
|
|
|
|
public bool TraitorsEnabled =>
|
|
GameMain.NetworkMember?.ServerSettings != null &&
|
|
GameMain.NetworkMember.ServerSettings.TraitorProbability > 0.0f;
|
|
|
|
partial void InitProjSpecific();
|
|
|
|
private GameSession(SubmarineInfo submarineInfo)
|
|
{
|
|
InitProjSpecific();
|
|
SubmarineInfo = submarineInfo;
|
|
EnemySubmarineInfo = SubmarineInfo;
|
|
GameMain.GameSession = this;
|
|
EventManager = new EventManager();
|
|
}
|
|
|
|
private GameSession(SubmarineInfo submarineInfo, SubmarineInfo enemySubmarineInfo)
|
|
: this(submarineInfo)
|
|
{
|
|
EnemySubmarineInfo = enemySubmarineInfo;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start a new GameSession. Will be saved to the specified save path (if playing a game mode that can be saved).
|
|
/// </summary>
|
|
public GameSession(SubmarineInfo submarineInfo, Option<SubmarineInfo> enemySub, CampaignDataPath dataPath, GameModePreset gameModePreset, CampaignSettings settings, string? seed = null, IEnumerable<Identifier>? missionTypes = null)
|
|
: this(submarineInfo)
|
|
{
|
|
DataPath = dataPath;
|
|
CrewManager = new CrewManager(gameModePreset.IsSinglePlayer);
|
|
GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, settings, missionTypes: missionTypes);
|
|
EnemySubmarineInfo = enemySub.TryUnwrap(out var enemySubmarine) ? enemySubmarine : submarineInfo;
|
|
InitOwnedSubs(submarineInfo);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start a new GameSession with a specific pre-selected mission.
|
|
/// </summary>
|
|
public GameSession(SubmarineInfo submarineInfo, Option<SubmarineInfo> enemySub, GameModePreset gameModePreset, string? seed = null, IEnumerable<MissionPrefab>? missionPrefabs = null)
|
|
: this(submarineInfo)
|
|
{
|
|
CrewManager = new CrewManager(gameModePreset.IsSinglePlayer);
|
|
GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, CampaignSettings.Empty, missionPrefabs: missionPrefabs);
|
|
EnemySubmarineInfo = enemySub.TryUnwrap(out var enemySubmarine) ? enemySubmarine : submarineInfo;
|
|
InitOwnedSubs(submarineInfo);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load a game session from the specified XML document. The session will be saved to the specified path.
|
|
/// </summary>
|
|
public GameSession(SubmarineInfo submarineInfo, List<SubmarineInfo> ownedSubmarines, XDocument doc, CampaignDataPath campaignData) : this(submarineInfo)
|
|
{
|
|
DataPath = campaignData;
|
|
GameMain.GameSession = this;
|
|
XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null.");
|
|
|
|
LastSaveVersion = doc.Root.GetAttributeVersion("version", GameMain.Version);
|
|
|
|
foreach (var subElement in rootElement.Elements())
|
|
{
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
case "gamemode": //legacy support
|
|
case "singleplayercampaign":
|
|
#if CLIENT
|
|
CrewManager = new CrewManager(true);
|
|
var campaign = SinglePlayerCampaign.Load(subElement);
|
|
campaign.LoadNewLevel();
|
|
GameMode = campaign;
|
|
InitOwnedSubs(submarineInfo, ownedSubmarines);
|
|
#else
|
|
throw new Exception("The server cannot load a single player campaign.");
|
|
#endif
|
|
break;
|
|
case "multiplayercampaign":
|
|
CrewManager = new CrewManager(false);
|
|
var mpCampaign = MultiPlayerCampaign.LoadNew(subElement);
|
|
GameMode = mpCampaign;
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
|
{
|
|
mpCampaign.LoadNewLevel();
|
|
InitOwnedSubs(submarineInfo, ownedSubmarines);
|
|
//save to ensure the campaign ID in the save file matches the one that got assigned to this campaign instance
|
|
SaveUtil.SaveGame(campaignData, isSavingOnLoading: true);
|
|
}
|
|
break;
|
|
case "permadeaths":
|
|
permadeathsPerAccount = new Dictionary<Option<AccountId>, int>();
|
|
foreach (XElement accountElement in subElement.Elements("account"))
|
|
{
|
|
if (accountElement.Attribute("id") is XAttribute accountIdAttr &&
|
|
accountElement.Attribute("permadeathcount") is XAttribute permadeathCountAttr)
|
|
{
|
|
try
|
|
{
|
|
permadeathsPerAccount[AccountId.Parse(accountIdAttr.Value)] = int.Parse(permadeathCountAttr.Value);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
DebugConsole.AddWarning($"Exception while trying to load permadeath counts!\n{e}\n id: {accountIdAttr}\n permadeathcount: {permadeathCountAttr}");
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
//NOTE: if you're adding something that's supposed to load something that's persistent in a campaign,
|
|
//this is probably not the correct place! This is where the GameSession itself is initialized,
|
|
//and if you let's say quit to the server lobby and reload, this method won't be called again.
|
|
//You should probably add it to CampaignMode.LoadSaveSharedSingleAndMultiplayer
|
|
}
|
|
}
|
|
}
|
|
|
|
private void InitOwnedSubs(SubmarineInfo submarineInfo, List<SubmarineInfo>? ownedSubmarines = null)
|
|
{
|
|
OwnedSubmarines = ownedSubmarines ?? new List<SubmarineInfo>();
|
|
if (submarineInfo != null && !OwnedSubmarines.Any(s => s.Name == submarineInfo.Name))
|
|
{
|
|
OwnedSubmarines.Add(submarineInfo);
|
|
}
|
|
}
|
|
|
|
private GameMode InstantiateGameMode(GameModePreset gameModePreset, string? seed, SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable<MissionPrefab>? missionPrefabs = null, IEnumerable<Identifier>? missionTypes = null)
|
|
{
|
|
if (gameModePreset.GameModeType == typeof(CoOpMode))
|
|
{
|
|
return missionPrefabs != null ?
|
|
new CoOpMode(gameModePreset, missionPrefabs) :
|
|
new CoOpMode(gameModePreset, missionTypes, seed ?? ToolBox.RandomSeed(8));
|
|
}
|
|
else if (gameModePreset.GameModeType == typeof(PvPMode))
|
|
{
|
|
return missionPrefabs != null ?
|
|
new PvPMode(gameModePreset, missionPrefabs) :
|
|
new PvPMode(gameModePreset, missionTypes, seed ?? ToolBox.RandomSeed(8));
|
|
}
|
|
else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign))
|
|
{
|
|
var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings);
|
|
if (selectedSub != null)
|
|
{
|
|
campaign.Bank.Deduct(selectedSub.Price);
|
|
campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, 0);
|
|
#if SERVER
|
|
if (GameMain.Server?.ServerSettings?.NewCampaignDefaultSalary is { } salary)
|
|
{
|
|
campaign.Bank.SetRewardDistribution((int)Math.Round(salary, digits: 0));
|
|
}
|
|
#endif
|
|
}
|
|
return campaign;
|
|
}
|
|
#if CLIENT
|
|
else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign))
|
|
{
|
|
var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings);
|
|
if (selectedSub != null)
|
|
{
|
|
campaign.Bank.TryDeduct(selectedSub.Price);
|
|
campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, 0);
|
|
}
|
|
return campaign;
|
|
}
|
|
else if (gameModePreset.GameModeType == typeof(TutorialMode))
|
|
{
|
|
return new TutorialMode(gameModePreset);
|
|
}
|
|
else if (gameModePreset.GameModeType == typeof(TestGameMode))
|
|
{
|
|
return new TestGameMode(gameModePreset);
|
|
}
|
|
#endif
|
|
else if (gameModePreset.GameModeType == typeof(GameMode))
|
|
{
|
|
return new GameMode(gameModePreset);
|
|
}
|
|
else
|
|
{
|
|
throw new Exception($"Could not find a game mode of the type \"{gameModePreset.GameModeType}\"");
|
|
}
|
|
}
|
|
|
|
public static Location[] CreateDummyLocations(LevelData levelData, LocationType? forceLocationType = null)
|
|
{
|
|
MTRandom rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
|
|
var forceParams = levelData?.ForceOutpostGenerationParams;
|
|
if (forceLocationType == null &&
|
|
forceParams != null && forceParams.AllowedLocationTypes.Any() && !forceParams.AllowedLocationTypes.Contains("Any".ToIdentifier()))
|
|
{
|
|
forceLocationType =
|
|
LocationType.Prefabs.Where(lt => forceParams.AllowedLocationTypes.Contains(lt.Identifier)).GetRandom(rand);
|
|
}
|
|
var dummyLocations = CreateDummyLocations(rand, forceLocationType);
|
|
List<Faction> factions = new List<Faction>();
|
|
foreach (var factionPrefab in FactionPrefab.Prefabs)
|
|
{
|
|
factions.Add(new Faction(new CampaignMetadata(), factionPrefab));
|
|
}
|
|
foreach (var location in dummyLocations)
|
|
{
|
|
if (location.Type.HasOutpost)
|
|
{
|
|
location.Faction = CampaignMode.GetRandomFaction(factions, rand, secondary: false);
|
|
location.SecondaryFaction = CampaignMode.GetRandomFaction(factions, rand, secondary: true);
|
|
}
|
|
}
|
|
return dummyLocations;
|
|
}
|
|
|
|
public static Location[] CreateDummyLocations(string seed, LocationType? forceLocationType = null)
|
|
{
|
|
return CreateDummyLocations(new MTRandom(ToolBox.StringToInt(seed)), forceLocationType);
|
|
}
|
|
|
|
private static Location[] CreateDummyLocations(Random rand, LocationType? forceLocationType = null)
|
|
{
|
|
var dummyLocations = new Location[2];
|
|
for (int i = 0; i < 2; i++)
|
|
{
|
|
dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), zone: null, biomeId: null, rand, requireOutpost: true, forceLocationType);
|
|
}
|
|
return dummyLocations;
|
|
}
|
|
|
|
public static bool ShouldApplyDisembarkPoints(GameModePreset? preset)
|
|
{
|
|
if (preset is null) { return true; } // sure I guess?
|
|
|
|
return preset == GameModePreset.Sandbox ||
|
|
preset == GameModePreset.Mission ||
|
|
preset == GameModePreset.PvP;
|
|
}
|
|
|
|
public void LoadPreviousSave()
|
|
{
|
|
GameMain.LuaCs.Hook.Call("roundEnd");
|
|
AchievementManager.OnRoundEnded(this, roundInterrupted: true);
|
|
Submarine.Unload();
|
|
SaveUtil.LoadGame(DataPath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Switch to another submarine. The sub is loaded when the next round starts.
|
|
/// </summary>
|
|
public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, Client? client = null)
|
|
{
|
|
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
|
|
{
|
|
OwnedSubmarines.Add(newSubmarine);
|
|
}
|
|
else
|
|
{
|
|
// Fetch owned submarine data as the newSubmarine is just the base submarine
|
|
for (int i = 0; i < OwnedSubmarines.Count; i++)
|
|
{
|
|
if (OwnedSubmarines[i].Name == newSubmarine.Name)
|
|
{
|
|
newSubmarine = OwnedSubmarines[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
Campaign!.PendingSubmarineSwitch = newSubmarine;
|
|
Campaign!.TransferItemsOnSubSwitch = transferItems;
|
|
}
|
|
|
|
public bool TryPurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null)
|
|
{
|
|
if (Campaign is null) { return false; }
|
|
int price = newSubmarine.GetPrice();
|
|
if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, price)) { return false; }
|
|
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
|
|
{
|
|
GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name);
|
|
OwnedSubmarines.Add(newSubmarine);
|
|
#if SERVER
|
|
(Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.SubList);
|
|
#endif
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public bool IsSubmarineOwned(SubmarineInfo query)
|
|
{
|
|
return
|
|
Submarine.MainSub?.Info.Name == query.Name ||
|
|
(OwnedSubmarines != null && OwnedSubmarines.Any(os => os.Name == query.Name));
|
|
}
|
|
|
|
public bool IsCurrentLocationRadiated()
|
|
{
|
|
if (Map?.CurrentLocation == null || Campaign == null) { return false; }
|
|
|
|
bool isRadiated = Map.CurrentLocation.IsRadiated();
|
|
|
|
if (Level.Loaded?.EndLocation is { } endLocation)
|
|
{
|
|
isRadiated |= endLocation.IsRadiated();
|
|
}
|
|
|
|
return isRadiated;
|
|
}
|
|
|
|
public void StartRound(string levelSeed, float? difficulty = null, LevelGenerationParams? levelGenerationParams = null, Identifier forceBiome = default)
|
|
{
|
|
if (GameMode == null) { return; }
|
|
|
|
LevelData? randomLevel = null;
|
|
bool pvpOnly = GameMode is PvPMode;
|
|
foreach (Mission mission in Missions.Union(GameMode.Missions))
|
|
{
|
|
MissionPrefab missionPrefab = mission.Prefab;
|
|
if (missionPrefab != null &&
|
|
missionPrefab.AllowedLocationTypes.Any() &&
|
|
!missionPrefab.AllowedConnectionTypes.Any())
|
|
{
|
|
Random rand = new MTRandom(ToolBox.StringToInt(levelSeed));
|
|
LocationType locationType = LocationType.Prefabs
|
|
.OrderBy(lt => lt.UintIdentifier)
|
|
.Where(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier))
|
|
.GetRandom(rand)!;
|
|
dummyLocations = CreateDummyLocations(levelSeed, locationType);
|
|
|
|
if (!tryCreateFaction(mission.Prefab.RequiredLocationFaction, dummyLocations, static (loc, fac) => loc.Faction = fac))
|
|
{
|
|
tryCreateFaction(locationType.Faction, dummyLocations, static (loc, fac) => loc.Faction = fac);
|
|
tryCreateFaction(locationType.SecondaryFaction, dummyLocations, static (loc, fac) => loc.SecondaryFaction = fac);
|
|
}
|
|
static bool tryCreateFaction(Identifier factionIdentifier, Location[] locations, Action<Location, Faction> setter)
|
|
{
|
|
if (factionIdentifier.IsEmpty) { return false; }
|
|
if (!FactionPrefab.Prefabs.TryGet(factionIdentifier, out var prefab)) { return false; }
|
|
if (locations.Length == 0) { return false; }
|
|
|
|
var newFaction = new Faction(metadata: null, prefab);
|
|
for (int i = 0; i < locations.Length; i++)
|
|
{
|
|
setter(locations[i], newFaction);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true, biomeId: forceBiome, pvpOnly: pvpOnly);
|
|
break;
|
|
}
|
|
}
|
|
randomLevel ??= LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, biomeId: forceBiome, pvpOnly: pvpOnly);
|
|
StartRound(randomLevel);
|
|
}
|
|
|
|
private bool TryGenerateStationAroundModule(SubmarineInfo? moduleInfo, out Submarine? outpostSub)
|
|
{
|
|
outpostSub = null;
|
|
if (moduleInfo == null) { return false; }
|
|
|
|
var allSuitableOutpostParams = OutpostGenerationParams.OutpostParams
|
|
.Where(outpostParam => IsOutpostParamsSuitable(outpostParam));
|
|
|
|
// allow for fallback when there are no options with allowed location types defined
|
|
var suitableOutpostParams =
|
|
allSuitableOutpostParams.Where(p => p.AllowedLocationTypes.Any()).GetRandomUnsynced() ??
|
|
allSuitableOutpostParams.GetRandomUnsynced();
|
|
|
|
bool IsOutpostParamsSuitable(OutpostGenerationParams outpostParams)
|
|
{
|
|
bool moduleWorksWithOutpostParams = outpostParams.ModuleCounts.Any(moduleCount => moduleInfo.OutpostModuleInfo.ModuleFlags.Contains(moduleCount.Identifier));
|
|
if (!moduleWorksWithOutpostParams) { return false; }
|
|
|
|
// is there a location that these outpostParams are suitable for, and which this module is suitable for
|
|
return LocationType.Prefabs.Any(locationType => IsSuitableLocationType(moduleInfo.OutpostModuleInfo.AllowedLocationTypes, locationType.Identifier)
|
|
&& IsSuitableLocationType(outpostParams.AllowedLocationTypes, locationType.Identifier));
|
|
|
|
bool IsSuitableLocationType(IEnumerable<Identifier> allowedLocationTypes, Identifier locationType)
|
|
{
|
|
return allowedLocationTypes.None() || allowedLocationTypes.Contains("Any".ToIdentifier()) || allowedLocationTypes.Contains(locationType);
|
|
}
|
|
}
|
|
|
|
if (suitableOutpostParams == null)
|
|
{
|
|
DebugConsole.AddWarning("No suitable generation parameters found for ForceOutpostModule, skipping outpost generation!");
|
|
return false;
|
|
}
|
|
|
|
var suitableLocationType = LocationType.Prefabs.Where(locationType =>
|
|
suitableOutpostParams.AllowedLocationTypes.Contains(locationType.Identifier)).GetRandomUnsynced();
|
|
|
|
if (suitableLocationType == null)
|
|
{
|
|
DebugConsole.AddWarning("No suitable location type found for ForceOutpostModule, skipping outpost generation!");
|
|
return false;
|
|
}
|
|
|
|
// try to find a required faction id matching our module
|
|
var requiredFactionModuleCount = suitableOutpostParams.ModuleCounts.FirstOrDefault(mc => !mc.RequiredFaction.IsEmpty && moduleInfo.OutpostModuleInfo.ModuleFlags.Contains(mc.Identifier));
|
|
Identifier requiredFactionId = requiredFactionModuleCount?.RequiredFaction ?? Identifier.Empty;
|
|
|
|
if (requiredFactionId.IsEmpty)
|
|
{
|
|
// no matching faction requirements, generate normally from location type
|
|
outpostSub = OutpostGenerator.Generate(suitableOutpostParams, suitableLocationType);
|
|
return outpostSub != null;
|
|
}
|
|
|
|
// if there is a faction requirement for the module, create a dummy location and augment its factions to match
|
|
var dummyLocations = CreateDummyLocations("1337", suitableLocationType);
|
|
var dummyLocation = dummyLocations[0];
|
|
|
|
if (FactionPrefab.Prefabs.TryGet(requiredFactionId, out FactionPrefab? factionPrefab))
|
|
{
|
|
if (factionPrefab.ControlledOutpostPercentage > factionPrefab.SecondaryControlledOutpostPercentage)
|
|
{
|
|
dummyLocation.Faction = new Faction(null, factionPrefab);
|
|
}
|
|
else
|
|
{
|
|
dummyLocation.SecondaryFaction = new Faction(null, factionPrefab);
|
|
}
|
|
|
|
outpostSub = OutpostGenerator.Generate(suitableOutpostParams, dummyLocation);
|
|
return outpostSub != null;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void StartRound(LevelData? levelData, bool mirrorLevel = false, SubmarineInfo? startOutpost = null, SubmarineInfo? endOutpost = null)
|
|
{
|
|
#if DEBUG
|
|
DateTime startTime = DateTime.Now;
|
|
#endif
|
|
RoundDuration = 0.0f;
|
|
AfflictionPrefab.LoadAllEffectsAndTreatmentSuitabilities();
|
|
|
|
MirrorLevel = mirrorLevel;
|
|
if (SubmarineInfo == null)
|
|
{
|
|
DebugConsole.ThrowError("Couldn't start game session, submarine not selected.");
|
|
return;
|
|
}
|
|
if (SubmarineInfo.IsFileCorrupted)
|
|
{
|
|
DebugConsole.ThrowError("Couldn't start game session, submarine file corrupted.");
|
|
return;
|
|
}
|
|
if (SubmarineInfo.SubmarineElement.Elements().Count() == 0)
|
|
{
|
|
DebugConsole.ThrowError("Couldn't start game session, saved submarine is empty. The submarine file may be corrupted.");
|
|
return;
|
|
}
|
|
|
|
Submarine.LockX = Submarine.LockY = false;
|
|
|
|
LevelData = levelData;
|
|
|
|
Submarine.Unload();
|
|
|
|
bool loadSubmarine = GameMode!.Missions.None(m => !m.Prefab.LoadSubmarines);
|
|
|
|
// attempt to generate an outpost for the main sub, with the forced module inside it
|
|
if (loadSubmarine)
|
|
{
|
|
if (TryGenerateStationAroundModule(ForceOutpostModule, out Submarine? outpostSub))
|
|
{
|
|
Submarine = Submarine.MainSub = outpostSub ?? new Submarine(SubmarineInfo);
|
|
}
|
|
else
|
|
{
|
|
Submarine = Submarine.MainSub = new Submarine(SubmarineInfo);
|
|
}
|
|
foreach (Submarine sub in Submarine.GetConnectedSubs())
|
|
{
|
|
sub.TeamID = CharacterTeamType.Team1;
|
|
foreach (Item item in Item.ItemList)
|
|
{
|
|
if (item.Submarine != sub) { continue; }
|
|
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
|
|
{
|
|
wifiComponent.TeamID = sub.TeamID;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Submarine = Submarine.MainSub = null;
|
|
}
|
|
|
|
GameMode!.AddExtraMissions(LevelData);
|
|
foreach (Mission mission in GameMode!.Missions)
|
|
{
|
|
// setting level for missions that may involve difficulty-related submarine creation
|
|
mission.SetLevel(levelData);
|
|
}
|
|
|
|
if (Submarine.MainSubs[1] == null && loadSubmarine)
|
|
{
|
|
var enemySubmarineInfo = GameMode is PvPMode ? EnemySubmarineInfo : GameMode.Missions.FirstOrDefault(m => m.EnemySubmarineInfo != null)?.EnemySubmarineInfo;
|
|
if (enemySubmarineInfo != null)
|
|
{
|
|
Submarine.MainSubs[1] = new Submarine(enemySubmarineInfo);
|
|
}
|
|
}
|
|
|
|
if (GameMain.NetworkMember?.ServerSettings is { LockAllDefaultWires: true } &&
|
|
Submarine.MainSubs[0] != null)
|
|
{
|
|
List<Item> items = new List<Item>();
|
|
items.AddRange(Submarine.MainSubs[0].GetItems(alsoFromConnectedSubs: true));
|
|
if (Submarine.MainSubs[1] != null)
|
|
{
|
|
items.AddRange(Submarine.MainSubs[1].GetItems(alsoFromConnectedSubs: true));
|
|
}
|
|
foreach (Item item in items)
|
|
{
|
|
if (item.GetComponent<CircuitBox>() is { } cb)
|
|
{
|
|
cb.TemporarilyLocked = true;
|
|
}
|
|
|
|
Wire wire = item.GetComponent<Wire>();
|
|
if (wire != null && !wire.NoAutoLock && wire.Connections.Any(c => c != null)) { wire.Locked = true; }
|
|
}
|
|
}
|
|
|
|
Level? level = null;
|
|
if (levelData != null)
|
|
{
|
|
level = Level.Generate(levelData, mirrorLevel, StartLocation, EndLocation, startOutpost, endOutpost);
|
|
}
|
|
|
|
InitializeLevel(level);
|
|
|
|
//Clear out the cached grids and force update
|
|
Powered.Grids.Clear();
|
|
|
|
casualties.Clear();
|
|
|
|
#if DEBUG
|
|
double startDuration = (DateTime.Now - startTime).TotalSeconds;
|
|
if (startDuration < MinimumLoadingTime)
|
|
{
|
|
int sleepTime = (int)((MinimumLoadingTime - startDuration) * 1000);
|
|
DebugConsole.NewMessage($"Stalling round start by {sleepTime / 1000.0f} s (minimum loading time set to {MinimumLoadingTime})...", Color.Magenta);
|
|
System.Threading.Thread.Sleep(sleepTime);
|
|
}
|
|
#endif
|
|
#if CLIENT
|
|
var existingRoundSummary = GUIMessageBox.MessageBoxes.Find(mb => mb.UserData is RoundSummary)?.UserData as RoundSummary;
|
|
if (existingRoundSummary?.ContinueButton != null)
|
|
{
|
|
existingRoundSummary.ContinueButton.Visible = true;
|
|
}
|
|
|
|
CharacterHUD.ClearBossProgressBars();
|
|
|
|
RoundSummary = new RoundSummary(GameMode, Missions, StartLocation, EndLocation);
|
|
|
|
if (GameMode is not TutorialMode && GameMode is not TestGameMode)
|
|
{
|
|
GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false);
|
|
if (EndLocation != null && levelData != null)
|
|
{
|
|
GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false);
|
|
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.DisplayName), Color.CadetBlue, playSound: false);
|
|
var missionsToShow = missions.Where(m => m.Prefab.ShowStartMessage);
|
|
if (missionsToShow.Count() > 1)
|
|
{
|
|
string joinedMissionNames = string.Join(", ", missions.Where(static m => m.Prefab.ShowInMenus).Select(static m => m.Name));
|
|
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), joinedMissionNames), Color.CadetBlue, playSound: false);
|
|
}
|
|
else
|
|
{
|
|
var mission = missionsToShow.FirstOrDefault();
|
|
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), mission?.Name ?? TextManager.Get("None")), Color.CadetBlue, playSound: false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Location"), StartLocation.DisplayName), Color.CadetBlue, playSound: false);
|
|
}
|
|
}
|
|
|
|
ReadyCheck.ReadyCheckCooldown = DateTime.MinValue;
|
|
GUI.PreventPauseMenuToggle = false;
|
|
HintManager.OnRoundStarted();
|
|
|
|
GameMain.LuaCs.Hook.Call("roundStart");
|
|
EnableEventLogNotificationIcon(enabled: false);
|
|
|
|
LogStartRoundStats();
|
|
|
|
#endif
|
|
var campaignMode = GameMode as CampaignMode;
|
|
if (campaignMode is { ItemsRelocatedToMainSub: true })
|
|
{
|
|
#if SERVER
|
|
GameMain.Server.SendChatMessage(TextManager.Get("itemrelocated").Value, ChatMessageType.ServerMessageBoxInGame);
|
|
#else
|
|
if (campaignMode.IsSinglePlayer)
|
|
{
|
|
new GUIMessageBox(string.Empty, TextManager.Get("itemrelocated"));
|
|
}
|
|
#endif
|
|
campaignMode.ItemsRelocatedToMainSub = false;
|
|
}
|
|
|
|
EventManager?.EventLog?.Clear();
|
|
if (campaignMode is { DivingSuitWarningShown: false } &&
|
|
Level.Loaded != null && Level.Loaded.GetRealWorldDepth(0) > 4000)
|
|
{
|
|
#if CLIENT
|
|
CoroutineManager.Invoke(() => new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("hint.upgradedivingsuits")), delay: 5.0f);
|
|
#endif
|
|
campaignMode.DivingSuitWarningShown = true;
|
|
}
|
|
}
|
|
|
|
private void InitializeLevel(Level? level)
|
|
{
|
|
//make sure no status effects have been carried on from the next round
|
|
//(they should be stopped in EndRound, this is a safeguard against cases where the round is ended ungracefully)
|
|
StatusEffect.StopAll();
|
|
|
|
bool forceDocking = false;
|
|
#if CLIENT
|
|
GameMain.LightManager.LosEnabled = (GameMain.Client == null || GameMain.Client.CharacterInfo != null) && !GameMain.DevMode;
|
|
if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; }
|
|
if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameSettings.CurrentConfig.Graphics.LosMode; }
|
|
forceDocking = GameMode is TutorialMode;
|
|
#endif
|
|
LevelData = level?.LevelData;
|
|
Level = level;
|
|
|
|
PlaceSubAtInitialPosition(Submarine, Level, placeAtStart: true, forceDocking: forceDocking);
|
|
|
|
foreach (var sub in Submarine.Loaded)
|
|
{
|
|
// TODO: Currently there's no need to check these on ruins, but that might change -> Could maybe just check if the body is static?
|
|
if (sub.Info.IsOutpost || sub.Info.IsBeacon || sub.Info.IsWreck)
|
|
{
|
|
sub.DisableObstructedWayPoints();
|
|
}
|
|
}
|
|
|
|
Entity.Spawner = new EntitySpawner();
|
|
|
|
if (GameMode != null)
|
|
{
|
|
missions.Clear();
|
|
missions.AddRange(GameMode.Missions);
|
|
GameMode.Start();
|
|
foreach (Mission mission in missions)
|
|
{
|
|
int prevEntityCount = Entity.GetEntities().Count;
|
|
mission.Start(Level.Loaded);
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count != prevEntityCount)
|
|
{
|
|
DebugConsole.ThrowError(
|
|
$"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " +
|
|
"The clients should not instantiate entities themselves when starting the mission," +
|
|
" but instead the server should inform the client of the spawned entities using Mission.ServerWriteInitial.");
|
|
}
|
|
}
|
|
|
|
#if CLIENT
|
|
ObjectiveManager.ResetObjectives();
|
|
#endif
|
|
EventManager?.StartRound(Level.Loaded);
|
|
AchievementManager.OnStartRound(Level?.LevelData.Biome);
|
|
|
|
GameMode.ShowStartMessage();
|
|
|
|
if (GameMain.NetworkMember == null)
|
|
{
|
|
//only place items and corpses here in single player
|
|
//the server does this after loading the respawn shuttle
|
|
if (Level != null)
|
|
{
|
|
if (GameMain.GameSession.Missions.None(m => !m.Prefab.AllowOutpostNPCs))
|
|
{
|
|
Level.SpawnNPCs();
|
|
}
|
|
Level.SpawnCorpses();
|
|
Level.PrepareBeaconStation();
|
|
}
|
|
else
|
|
{
|
|
// Spawn npcs in the sub editor test mode.
|
|
foreach (Submarine sub in Submarine.Loaded)
|
|
{
|
|
if (sub?.Info?.OutpostGenerationParams != null)
|
|
{
|
|
OutpostGenerator.SpawnNPCs(StartLocation, sub);
|
|
}
|
|
}
|
|
}
|
|
AutoItemPlacer.SpawnItems(Campaign?.Settings.StartItemSet);
|
|
}
|
|
if (GameMode is MultiPlayerCampaign mpCampaign)
|
|
{
|
|
mpCampaign.UpgradeManager.ApplyUpgrades();
|
|
mpCampaign.UpgradeManager.SanityCheckUpgrades();
|
|
}
|
|
}
|
|
|
|
CreatureMetrics.RecentlyEncountered.Clear();
|
|
|
|
GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub?.WorldPosition ?? Submarine.Loaded.First().WorldPosition;
|
|
RoundDuration = 0.0f;
|
|
GameMain.ResetFrameTime();
|
|
IsRunning = true;
|
|
}
|
|
|
|
public static void PlaceSubAtInitialPosition(Submarine? sub, Level? level, bool placeAtStart = true, bool forceDocking = false)
|
|
{
|
|
if (level == null || sub == null)
|
|
{
|
|
sub?.SetPosition(Vector2.Zero);
|
|
return;
|
|
}
|
|
|
|
Submarine outpost = placeAtStart ? level.StartOutpost : level.EndOutpost;
|
|
|
|
var originalSubPos = sub.WorldPosition;
|
|
var spawnPoint = WayPoint.WayPointList.Find(wp => wp.SpawnType.HasFlag(SpawnType.Submarine) && wp.Submarine == outpost);
|
|
if (spawnPoint != null)
|
|
{
|
|
//pre-determined spawnpoint, just use it directly
|
|
sub.SetPosition(spawnPoint.WorldPosition);
|
|
sub.NeutralizeBallast();
|
|
sub.EnableMaintainPosition();
|
|
}
|
|
else if (outpost != null)
|
|
{
|
|
//start by placing the sub below the outpost
|
|
Rectangle outpostBorders = outpost.GetDockedBorders();
|
|
Rectangle subBorders = sub.GetDockedBorders();
|
|
|
|
sub.SetPosition(
|
|
outpost.WorldPosition -
|
|
new Vector2(0.0f, outpostBorders.Height / 2 + subBorders.Height / 2));
|
|
|
|
//find the port that's the nearest to the outpost and dock if one is found
|
|
float closestDistance = 0.0f;
|
|
DockingPort? myPort = null, outPostPort = null;
|
|
foreach (DockingPort port in DockingPort.List)
|
|
{
|
|
if (port.IsHorizontal || port.Docked) { continue; }
|
|
if (port.Item.Submarine == outpost)
|
|
{
|
|
if (port.DockingTarget == null || (outPostPort != null && !outPostPort.MainDockingPort && port.MainDockingPort))
|
|
{
|
|
outPostPort = port;
|
|
}
|
|
continue;
|
|
}
|
|
if (port.Item.Submarine != sub) { continue; }
|
|
|
|
//the submarine port has to be at the top of the sub
|
|
if (port.Item.WorldPosition.Y < sub.WorldPosition.Y) { continue; }
|
|
|
|
float dist = Vector2.DistanceSquared(port.Item.WorldPosition, outpost.WorldPosition);
|
|
if ((myPort == null || dist < closestDistance || port.MainDockingPort) && !(myPort?.MainDockingPort ?? false))
|
|
{
|
|
myPort = port;
|
|
closestDistance = dist;
|
|
}
|
|
}
|
|
|
|
if (myPort != null && outPostPort != null)
|
|
{
|
|
Vector2 portDiff = myPort.Item.WorldPosition - sub.WorldPosition;
|
|
Vector2 spawnPos = (outPostPort.Item.WorldPosition - portDiff) - Vector2.UnitY * outPostPort.DockedDistance;
|
|
|
|
bool startDocked = level.Type == LevelData.LevelType.Outpost || forceDocking;
|
|
if (startDocked)
|
|
{
|
|
sub.SetPosition(spawnPos);
|
|
myPort.Dock(outPostPort);
|
|
myPort.Lock(isNetworkMessage: true, applyEffects: false);
|
|
foreach (var item in sub.GetItems(alsoFromConnectedSubs: true))
|
|
{
|
|
//need to refresh position to maintain since the sub was moved to the docking port
|
|
if (item.GetComponent<Steering>() is { MaintainPos: true } steering)
|
|
{
|
|
steering.RefreshPosToMaintain();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sub.SetPosition(spawnPos - Vector2.UnitY * 100.0f);
|
|
sub.NeutralizeBallast();
|
|
sub.EnableMaintainPosition();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sub.NeutralizeBallast();
|
|
sub.EnableMaintainPosition();
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
sub.SetPosition(sub.FindSpawnPos(placeAtStart ? level.StartPosition : level.EndPosition));
|
|
sub.NeutralizeBallast();
|
|
sub.EnableMaintainPosition();
|
|
}
|
|
|
|
// Make sure that linked subs which are NOT docked to the main sub
|
|
// (but still close enough to NOT be considered as 'left behind')
|
|
// are also moved to keep their relative position to the main sub
|
|
var linkedSubs = MapEntity.MapEntityList.FindAll(me => me is LinkedSubmarine);
|
|
foreach (LinkedSubmarine ls in linkedSubs)
|
|
{
|
|
if (ls.Sub == null || ls.Submarine != sub) { continue; }
|
|
if (!ls.LoadSub || ls.Sub.DockedTo.Contains(sub)) { continue; }
|
|
if (sub.Info.LeftBehindDockingPortIDs.Contains(ls.OriginalLinkedToID)) { continue; }
|
|
if (ls.Sub.Info.SubmarineElement.Attribute("location") != null) { continue; }
|
|
ls.SetPositionRelativeToMainSub();
|
|
}
|
|
}
|
|
|
|
public void Update(float deltaTime)
|
|
{
|
|
RoundDuration += deltaTime;
|
|
EventManager?.Update(deltaTime);
|
|
GameMode?.Update(deltaTime);
|
|
//backwards for loop because the missions may get completed and removed from the list in Update()
|
|
for (int i = missions.Count - 1; i >= 0; i--)
|
|
{
|
|
missions[i].Update(deltaTime);
|
|
}
|
|
UpdateProjSpecific(deltaTime);
|
|
}
|
|
|
|
public Mission? GetMission(int index)
|
|
{
|
|
if (index < 0 || index >= missions.Count) { return null; }
|
|
return missions[index];
|
|
}
|
|
|
|
public int GetMissionIndex(Mission mission)
|
|
{
|
|
return missions.IndexOf(mission);
|
|
}
|
|
|
|
public void EnforceMissionOrder(List<Identifier> missionIdentifiers)
|
|
{
|
|
List<Mission> sortedMissions = new List<Mission>();
|
|
foreach (Identifier missionId in missionIdentifiers)
|
|
{
|
|
var matchingMission = missions.Find(m => m.Prefab.Identifier == missionId);
|
|
if (matchingMission == null) { continue; }
|
|
sortedMissions.Add(matchingMission);
|
|
missions.Remove(matchingMission);
|
|
}
|
|
missions.AddRange(sortedMissions);
|
|
}
|
|
|
|
partial void UpdateProjSpecific(float deltaTime);
|
|
|
|
/// <summary>
|
|
/// Returns a list of crew characters currently in the game with a given filter.
|
|
/// </summary>
|
|
/// <param name="type">Character type filter</param>
|
|
/// <returns></returns>
|
|
/// <remarks>
|
|
/// In singleplayer mode the CharacterType.Player returns the currently controlled player.
|
|
/// </remarks>
|
|
public static ImmutableHashSet<Character> GetSessionCrewCharacters(CharacterType type)
|
|
{
|
|
var result = GameMain.LuaCs.Hook.Call<Character[]?>("getSessionCrewCharacters", type);
|
|
if (result != null) return ImmutableHashSet.Create(result);
|
|
|
|
if (GameMain.GameSession?.CrewManager is not { } crewManager) { return ImmutableHashSet<Character>.Empty; }
|
|
|
|
IEnumerable<Character> players;
|
|
IEnumerable<Character> bots;
|
|
HashSet<Character> characters = new HashSet<Character>();
|
|
|
|
#if SERVER
|
|
players = GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead);
|
|
bots = crewManager.GetCharacterInfos()
|
|
//filter out players in case a player has been given control of a bot using console commands
|
|
.Where(characterInfo => GameMain.Server.ConnectedClients.None(c => c.CharacterInfo == characterInfo))
|
|
.Select(characterInfo => characterInfo.Character)
|
|
.NotNull();
|
|
#elif CLIENT
|
|
players = crewManager.GetCharacters().Where(static c => c.IsPlayer);
|
|
bots = crewManager.GetCharacters().Where(static c => c.IsBot);
|
|
#endif
|
|
if (type.HasFlag(CharacterType.Bot))
|
|
{
|
|
foreach (Character bot in bots) { characters.Add(bot); }
|
|
}
|
|
|
|
if (type.HasFlag(CharacterType.Player))
|
|
{
|
|
foreach (Character player in players) { characters.Add(player); }
|
|
}
|
|
|
|
return characters.ToImmutableHashSet();
|
|
}
|
|
|
|
#if SERVER
|
|
private double LastEndRoundErrorMessageTime;
|
|
#endif
|
|
|
|
public void EndRound(string endMessage, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults = null, bool createRoundSummary = true)
|
|
{
|
|
RoundEnding = true;
|
|
|
|
#if CLIENT
|
|
GameMain.LuaCs.Hook.Call("roundEnd");
|
|
#endif
|
|
//Clear the grids to allow for garbage collection
|
|
Powered.Grids.Clear();
|
|
Powered.ClearChangedConnections();
|
|
|
|
try
|
|
{
|
|
EventManager?.TriggerOnEndRoundActions();
|
|
|
|
ImmutableHashSet<Character> crewCharacters = GetSessionCrewCharacters(CharacterType.Both);
|
|
int prevMoney = GetAmountOfMoney(crewCharacters);
|
|
|
|
EndMissions();
|
|
|
|
foreach (Character character in crewCharacters)
|
|
{
|
|
character.CheckTalents(AbilityEffectType.OnRoundEnd);
|
|
}
|
|
|
|
GameMain.LuaCs.Hook.Call("missionsEnded", missions);
|
|
|
|
#if CLIENT
|
|
if (GUI.PauseMenuOpen)
|
|
{
|
|
GUI.TogglePauseMenu();
|
|
}
|
|
if (IsTabMenuOpen)
|
|
{
|
|
ToggleTabMenu();
|
|
}
|
|
DeathPrompt?.Close();
|
|
DeathPrompt.CloseBotPanel();
|
|
|
|
GUI.PreventPauseMenuToggle = true;
|
|
|
|
if (createRoundSummary &&
|
|
GameMode is not TestGameMode &&
|
|
Screen.Selected == GameMain.GameScreen && RoundSummary != null &&
|
|
transitionType != CampaignMode.TransitionType.End)
|
|
{
|
|
GUI.ClearMessages();
|
|
GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary);
|
|
GUIFrame summaryFrame = RoundSummary.CreateSummaryFrame(this, endMessage, transitionType, traitorResults);
|
|
GUIMessageBox.MessageBoxes.Add(summaryFrame);
|
|
RoundSummary.ContinueButton.OnClicked = (_, __) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; };
|
|
}
|
|
|
|
if (GameMain.NetLobbyScreen != null) { GameMain.NetLobbyScreen.OnRoundEnded(); }
|
|
TabMenu.OnRoundEnded();
|
|
GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb));
|
|
ObjectiveManager.ResetUI();
|
|
CharacterHUD.ClearBossProgressBars();
|
|
#endif
|
|
AchievementManager.OnRoundEnded(this);
|
|
|
|
#if SERVER
|
|
GameMain.Server?.TraitorManager?.EndRound();
|
|
#endif
|
|
GameMode?.End(transitionType);
|
|
EventManager?.EndRound();
|
|
StatusEffect.StopAll();
|
|
AfflictionPrefab.ClearAllEffects();
|
|
PhysicsBodyQueue.Clear();
|
|
IsRunning = false;
|
|
|
|
#if CLIENT
|
|
bool success = CrewManager!.GetCharacters().Any(c => !c.IsDead);
|
|
#else
|
|
bool success =
|
|
GameMain.Server != null &&
|
|
GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
|
|
#endif
|
|
GameAnalyticsManager.AddProgressionEvent(
|
|
success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail,
|
|
GameMode?.Preset.Identifier.Value ?? "none",
|
|
RoundDuration);
|
|
string eventId = "EndRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":";
|
|
LogEndRoundStats(eventId, traitorResults);
|
|
if (GameMode is CampaignMode campaignMode)
|
|
{
|
|
GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney);
|
|
campaignMode.TotalPlayTime += RoundDuration;
|
|
}
|
|
#if CLIENT
|
|
HintManager.OnRoundEnded();
|
|
#endif
|
|
missions.Clear();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
string errorMsg = "Unknown error while ending the round.";
|
|
DebugConsole.ThrowError(errorMsg, e);
|
|
GameAnalyticsManager.AddErrorEventOnce("GameSession.EndRound:UnknownError", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + e.StackTrace);
|
|
#if SERVER
|
|
if (Timing.TotalTime > LastEndRoundErrorMessageTime + 1.0)
|
|
{
|
|
GameMain.Server?.SendChatMessage(errorMsg + "\n" + e.StackTrace, Networking.ChatMessageType.Error);
|
|
LastEndRoundErrorMessageTime = Timing.TotalTime;
|
|
}
|
|
#endif
|
|
}
|
|
finally
|
|
{
|
|
RoundEnding = false;
|
|
}
|
|
|
|
int GetAmountOfMoney(IEnumerable<Character> crew)
|
|
{
|
|
if (GameMode is not CampaignMode campaign) { return 0; }
|
|
|
|
return GameMain.NetworkMember switch
|
|
{
|
|
null => campaign.Bank.Balance,
|
|
_ => crew.Sum(c => c.Wallet.Balance) + campaign.Bank.Balance
|
|
};
|
|
}
|
|
}
|
|
|
|
public void EndMissions()
|
|
{
|
|
ImmutableHashSet<Character> crewCharacters = GetSessionCrewCharacters(CharacterType.Both);
|
|
foreach (Mission mission in missions)
|
|
{
|
|
mission.End();
|
|
}
|
|
|
|
if (missions.Any())
|
|
{
|
|
if (missions.Any(m => m.Completed))
|
|
{
|
|
foreach (Character character in crewCharacters)
|
|
{
|
|
character.CheckTalents(AbilityEffectType.OnAnyMissionCompleted);
|
|
}
|
|
}
|
|
|
|
if (missions.All(m => m.Completed))
|
|
{
|
|
foreach (Character character in crewCharacters)
|
|
{
|
|
character.CheckTalents(AbilityEffectType.OnAllMissionsCompleted);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static PerkCollection GetPerks()
|
|
{
|
|
if (GameMain.NetworkMember?.ServerSettings is not { } serverSettings)
|
|
{
|
|
return PerkCollection.Empty;
|
|
}
|
|
|
|
var team1Builder = ImmutableArray.CreateBuilder<DisembarkPerkPrefab>();
|
|
var team2Builder = ImmutableArray.CreateBuilder<DisembarkPerkPrefab>();
|
|
|
|
foreach (Identifier coalitionPerk in serverSettings.SelectedCoalitionPerks)
|
|
{
|
|
if (!DisembarkPerkPrefab.Prefabs.TryGet(coalitionPerk, out DisembarkPerkPrefab? disembarkPerk)) { continue; }
|
|
team1Builder.Add(disembarkPerk);
|
|
}
|
|
|
|
foreach (Identifier separatistsPerk in serverSettings.SelectedSeparatistsPerks)
|
|
{
|
|
if (!DisembarkPerkPrefab.Prefabs.TryGet(separatistsPerk, out DisembarkPerkPrefab? disembarkPerk)) { continue; }
|
|
team2Builder.Add(disembarkPerk);
|
|
}
|
|
|
|
return new PerkCollection(team1Builder.ToImmutable(), team2Builder.ToImmutable());
|
|
}
|
|
|
|
public static bool ValidatedDisembarkPoints(GameModePreset preset, IEnumerable<Identifier> missionTypes)
|
|
{
|
|
if (GameMain.NetworkMember?.ServerSettings is not { } settings) { return false; }
|
|
|
|
bool checkBothTeams = preset == GameModePreset.PvP;
|
|
|
|
PerkCollection perks = GetPerks();
|
|
|
|
int team1TotalCost = GetTotalCost(perks.Team1Perks);
|
|
if (team1TotalCost > settings.DisembarkPointAllowance)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (checkBothTeams)
|
|
{
|
|
int team2TotalCost = GetTotalCost(perks.Team2Perks);
|
|
if (team2TotalCost > settings.DisembarkPointAllowance)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
|
|
int GetTotalCost(ImmutableArray<DisembarkPerkPrefab> perksToCheck)
|
|
{
|
|
if (preset == GameModePreset.Mission || preset == GameModePreset.PvP)
|
|
{
|
|
if (ShouldIgnorePerksThatCanNotApplyWithoutSubmarine(preset, missionTypes))
|
|
{
|
|
perksToCheck = perksToCheck.Where(static p => p.PerkBehaviors.All(static b => b.CanApplyWithoutSubmarine())).ToImmutableArray();
|
|
}
|
|
}
|
|
return perksToCheck.Sum(static p => p.Cost);
|
|
}
|
|
}
|
|
|
|
public static bool ShouldIgnorePerksThatCanNotApplyWithoutSubmarine(GameModePreset preset, IEnumerable<Identifier> missionTypes)
|
|
{
|
|
if (preset == GameModePreset.Mission || preset == GameModePreset.PvP)
|
|
{
|
|
var missionTypesToCheck = MissionMode.ValidateMissionTypes(missionTypes, preset == GameModePreset.PvP ? MissionPrefab.PvPMissionClasses : MissionPrefab.CoOpMissionClasses);
|
|
foreach (var missionType in missionTypesToCheck)
|
|
{
|
|
foreach (var missionPrefab in MissionPrefab.Prefabs.Where(mp => mp.Type == missionType))
|
|
{
|
|
if (missionPrefab.LoadSubmarines)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void LogStartRoundStats()
|
|
{
|
|
#if !UNSTABLE
|
|
if (!GameAnalyticsManager.ShouldLogRandomSample())
|
|
{
|
|
return;
|
|
}
|
|
#endif
|
|
GameAnalyticsManager.AddProgressionEvent(
|
|
GameAnalyticsManager.ProgressionStatus.Start,
|
|
GameMode?.Preset?.Identifier.Value ?? "none");
|
|
|
|
string eventId = "StartRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":";
|
|
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
|
|
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier.Value ?? "none"));
|
|
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.GetCharacterInfos()?.Count() ?? 0));
|
|
foreach (Mission mission in missions)
|
|
{
|
|
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier);
|
|
}
|
|
if (Level.Loaded != null)
|
|
{
|
|
Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ?
|
|
Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier :
|
|
Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier();
|
|
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + Level.Loaded.Type.ToString() + ":" + levelId);
|
|
}
|
|
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"));
|
|
#if CLIENT
|
|
if (GameMode is TutorialMode tutorialMode)
|
|
{
|
|
GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier);
|
|
if (GameMain.IsFirstLaunch)
|
|
{
|
|
GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier);
|
|
}
|
|
}
|
|
GameAnalyticsManager.AddDesignEvent($"{eventId}HintManager:{(HintManager.Enabled ? "Enabled" : "Disabled")}");
|
|
#endif
|
|
var campaignMode = GameMode as CampaignMode;
|
|
if (campaignMode != null)
|
|
{
|
|
GameAnalyticsManager.AddDesignEvent("CampaignSettings:RadiationEnabled:" + campaignMode.Settings.RadiationEnabled);
|
|
GameAnalyticsManager.AddDesignEvent("CampaignSettings:WorldHostility:" + campaignMode.Settings.WorldHostility);
|
|
GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShowHuskWarning:" + campaignMode.Settings.ShowHuskWarning);
|
|
GameAnalyticsManager.AddDesignEvent("CampaignSettings:StartItemSet:" + campaignMode.Settings.StartItemSet);
|
|
GameAnalyticsManager.AddDesignEvent("CampaignSettings:MaxMissionCount:" + campaignMode.Settings.MaxMissionCount);
|
|
//log the multipliers as integers to reduce the number of distinct values
|
|
GameAnalyticsManager.AddDesignEvent("CampaignSettings:RepairFailMultiplier:" + (int)(campaignMode.Settings.RepairFailMultiplier * 100));
|
|
GameAnalyticsManager.AddDesignEvent("CampaignSettings:FuelMultiplier:" + (int)(campaignMode.Settings.FuelMultiplier * 100));
|
|
GameAnalyticsManager.AddDesignEvent("CampaignSettings:MissionRewardMultiplier:" + (int)(campaignMode.Settings.MissionRewardMultiplier * 100));
|
|
GameAnalyticsManager.AddDesignEvent("CampaignSettings:CrewVitalityMultiplier:" + (int)(campaignMode.Settings.CrewVitalityMultiplier * 100));
|
|
GameAnalyticsManager.AddDesignEvent("CampaignSettings:NonCrewVitalityMultiplier:" + (int)(campaignMode.Settings.NonCrewVitalityMultiplier * 100));
|
|
GameAnalyticsManager.AddDesignEvent("CampaignSettings:OxygenMultiplier:" + (int)(campaignMode.Settings.OxygenMultiplier * 100));
|
|
GameAnalyticsManager.AddDesignEvent("CampaignSettings:RepairFailMultiplier:" + (int)(campaignMode.Settings.RepairFailMultiplier * 100));
|
|
GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShipyardPriceMultiplier:" + (int)(campaignMode.Settings.ShipyardPriceMultiplier * 100));
|
|
GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShopPriceMultiplier:" + (int)(campaignMode.Settings.ShopPriceMultiplier * 100));
|
|
|
|
bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData!.Biome);
|
|
if (firstTimeInBiome)
|
|
{
|
|
GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime);
|
|
GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels);
|
|
}
|
|
if (GameMain.NetworkMember?.ServerSettings is { } serverSettings)
|
|
{
|
|
GameAnalyticsManager.AddDesignEvent("ServerSettings:RespawnMode:" + serverSettings.RespawnMode);
|
|
GameAnalyticsManager.AddDesignEvent("ServerSettings:IronmanMode:" + serverSettings.IronmanModeActive);
|
|
GameAnalyticsManager.AddDesignEvent("ServerSettings:AllowBotTakeoverOnPermadeath:" + serverSettings.AllowBotTakeoverOnPermadeath);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public void LogEndRoundStats(string eventId, TraitorManager.TraitorResults? traitorResults = null)
|
|
{
|
|
#if !UNSTABLE
|
|
//only collect the stats from a random sample of round ends
|
|
if (!GameAnalyticsManager.ShouldLogRandomSample())
|
|
{
|
|
return;
|
|
}
|
|
#endif
|
|
if (Submarine.MainSub?.Info?.IsVanillaSubmarine() ?? false)
|
|
{
|
|
//don't log modded subs, that's a ton of extra data to collect
|
|
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), RoundDuration);
|
|
}
|
|
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), RoundDuration);
|
|
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.GetCharacterInfos()?.Count() ?? 0), RoundDuration);
|
|
foreach (Mission mission in missions)
|
|
{
|
|
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), RoundDuration);
|
|
}
|
|
if (!ContentPackageManager.ModsEnabled)
|
|
{
|
|
if (Level.Loaded != null)
|
|
{
|
|
Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ?
|
|
Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier :
|
|
Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier();
|
|
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), RoundDuration);
|
|
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), RoundDuration);
|
|
}
|
|
|
|
//disabled for now, we're collecting too many events and this is information we don't need atm
|
|
/*if (Submarine.MainSub != null)
|
|
{
|
|
Dictionary<ItemPrefab, int> submarineInventory = new Dictionary<ItemPrefab, int>();
|
|
foreach (Item item in Item.ItemList)
|
|
{
|
|
var rootContainer = item.RootContainer ?? item;
|
|
if (rootContainer.Submarine?.Info == null || rootContainer.Submarine.Info.Type != SubmarineType.Player) { continue; }
|
|
if (rootContainer.Submarine != Submarine.MainSub && !Submarine.MainSub.DockedTo.Contains(rootContainer.Submarine)) { continue; }
|
|
|
|
var holdable = item.GetComponent<Holdable>();
|
|
if (holdable == null || holdable.Attached) { continue; }
|
|
var wire = item.GetComponent<Wire>();
|
|
if (wire != null && wire.Connections.Any(c => c != null)) { continue; }
|
|
|
|
if (!submarineInventory.ContainsKey(item.Prefab))
|
|
{
|
|
submarineInventory.Add(item.Prefab, 0);
|
|
}
|
|
submarineInventory[item.Prefab]++;
|
|
}
|
|
foreach (var subItem in submarineInventory)
|
|
{
|
|
GameAnalyticsManager.AddDesignEvent(eventId + "SubmarineInventory:" + subItem.Key.Identifier, subItem.Value);
|
|
}
|
|
}*/
|
|
}
|
|
|
|
if (traitorResults.HasValue)
|
|
{
|
|
GameAnalyticsManager.AddDesignEvent($"TraitorEvent:{traitorResults.Value.TraitorEventIdentifier}:{traitorResults.Value.ObjectiveSuccessful}");
|
|
GameAnalyticsManager.AddDesignEvent($"TraitorEvent:{traitorResults.Value.TraitorEventIdentifier}:{(traitorResults.Value.VotedCorrectTraitor ? "TraitorIdentifier" : "TraitorUnidentified")}");
|
|
}
|
|
|
|
//disabled to reduce the amount of data we collect through GA
|
|
/*foreach (Character c in GetSessionCrewCharacters(CharacterType.Both))
|
|
{
|
|
foreach (var itemSelectedDuration in c.ItemSelectedDurations)
|
|
{
|
|
string characterType = "Unknown";
|
|
if (c.IsBot)
|
|
{
|
|
characterType = "Bot";
|
|
}
|
|
else if (c.IsPlayer)
|
|
{
|
|
characterType = "Player";
|
|
}
|
|
GameAnalyticsManager.AddDesignEvent("TimeSpentOnDevices:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":" + characterType + ":" + (c.Info?.Job?.Prefab.Identifier.Value ?? "NoJob") + ":" + itemSelectedDuration.Key.Identifier, itemSelectedDuration.Value);
|
|
}
|
|
}*/
|
|
#if CLIENT
|
|
if (GameMode is TutorialMode tutorialMode)
|
|
{
|
|
GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier);
|
|
if (GameMain.IsFirstLaunch)
|
|
{
|
|
GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier);
|
|
}
|
|
}
|
|
//disabled to reduce the amount of data we collect through GA
|
|
//GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentCleaning", TimeSpentCleaning);
|
|
//GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentPainting", TimeSpentPainting);
|
|
TimeSpentCleaning = TimeSpentPainting = 0.0;
|
|
#endif
|
|
}
|
|
|
|
public void KillCharacter(Character character)
|
|
{
|
|
if (CrewManager != null &&
|
|
CrewManager.GetCharacterInfos().Contains(character.Info))
|
|
{
|
|
casualties.Add(character);
|
|
}
|
|
#if CLIENT
|
|
CrewManager?.KillCharacter(character);
|
|
#endif
|
|
}
|
|
|
|
public void ReviveCharacter(Character character)
|
|
{
|
|
casualties.Remove(character);
|
|
#if CLIENT
|
|
CrewManager?.ReviveCharacter(character);
|
|
#endif
|
|
}
|
|
|
|
public void UnlockRecipe(CharacterTeamType team, Identifier identifier, bool showNotifications)
|
|
{
|
|
if (unlockedRecipes.Add((team, identifier)))
|
|
{
|
|
#if CLIENT
|
|
if (showNotifications)
|
|
{
|
|
foreach (var character in GetSessionCrewCharacters(CharacterType.Both))
|
|
{
|
|
if (character.TeamID != team) { continue; }
|
|
LocalizedString recipeName = TextManager.Get($"entityname.{identifier}").Fallback(identifier.Value);
|
|
character.AddMessage(TextManager.GetWithVariable("recipeunlockednotification", "[name]", recipeName).Value, GUIStyle.Yellow, playSound: true);
|
|
}
|
|
}
|
|
#else
|
|
GameMain.Server.UnlockRecipe(team, identifier);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public bool HasUnlockedRecipe(Character character, Identifier itemIdentifier)
|
|
{
|
|
if (character == null) { return false; }
|
|
return unlockedRecipes.Contains((character.TeamID, itemIdentifier));
|
|
}
|
|
|
|
public static bool IsCompatibleWithEnabledContentPackages(IList<string> contentPackageNames, out LocalizedString errorMsg)
|
|
{
|
|
errorMsg = "";
|
|
//no known content packages, must be an older save file
|
|
if (!contentPackageNames.Any()) { return true; }
|
|
|
|
List<string> missingPackages = new List<string>();
|
|
foreach (string packageName in contentPackageNames)
|
|
{
|
|
if (!ContentPackageManager.EnabledPackages.All.Any(cp => cp.NameMatches(packageName)))
|
|
{
|
|
missingPackages.Add(packageName);
|
|
}
|
|
}
|
|
List<string> excessPackages = new List<string>();
|
|
foreach (ContentPackage cp in ContentPackageManager.EnabledPackages.All)
|
|
{
|
|
if (!cp.HasMultiplayerSyncedContent) { continue; }
|
|
if (!contentPackageNames.Any(p => cp.NameMatches(p)))
|
|
{
|
|
excessPackages.Add(cp.Name);
|
|
}
|
|
}
|
|
|
|
bool orderMismatch = false;
|
|
if (missingPackages.Count == 0 && missingPackages.Count == 0)
|
|
{
|
|
var enabledPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).ToImmutableArray();
|
|
for (int i = 0; i < contentPackageNames.Count && i < enabledPackages.Length; i++)
|
|
{
|
|
if (!enabledPackages[i].NameMatches(contentPackageNames[i]))
|
|
{
|
|
orderMismatch = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!orderMismatch && missingPackages.Count == 0 && excessPackages.Count == 0) { return true; }
|
|
|
|
if (missingPackages.Count == 1)
|
|
{
|
|
errorMsg = TextManager.GetWithVariable("campaignmode.missingcontentpackage", "[missingcontentpackage]", missingPackages[0]);
|
|
}
|
|
else if (missingPackages.Count > 1)
|
|
{
|
|
errorMsg = TextManager.GetWithVariable("campaignmode.missingcontentpackages", "[missingcontentpackages]", string.Join(", ", missingPackages));
|
|
}
|
|
if (excessPackages.Count == 1)
|
|
{
|
|
if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; }
|
|
errorMsg += TextManager.GetWithVariable("campaignmode.incompatiblecontentpackage", "[incompatiblecontentpackage]", excessPackages[0]);
|
|
}
|
|
else if (excessPackages.Count > 1)
|
|
{
|
|
if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; }
|
|
errorMsg += TextManager.GetWithVariable("campaignmode.incompatiblecontentpackages", "[incompatiblecontentpackages]", string.Join(", ", excessPackages));
|
|
}
|
|
if (orderMismatch)
|
|
{
|
|
if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; }
|
|
errorMsg += TextManager.GetWithVariable("campaignmode.contentpackageordermismatch", "[loadorder]", string.Join(", ", contentPackageNames));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void Save(string filePath, bool isSavingOnLoading)
|
|
{
|
|
if (GameMode is not CampaignMode campaign)
|
|
{
|
|
throw new NotSupportedException("GameSessions can only be saved when playing in a campaign mode.");
|
|
}
|
|
|
|
XDocument doc = new XDocument(new XElement("Gamesession"));
|
|
XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null.");
|
|
|
|
rootElement.Add(new XAttribute("savetime", SerializableDateTime.UtcNow.ToUnixTime()));
|
|
#warning TODO: after this gets on main, replace savetime with the commented line
|
|
//rootElement.Add(new XAttribute("savetime", SerializableDateTime.LocalNow));
|
|
|
|
rootElement.Add(new XAttribute("currentlocation", Map?.CurrentLocation?.NameIdentifier.Value ?? string.Empty));
|
|
rootElement.Add(new XAttribute("currentlocationnameformatindex", Map?.CurrentLocation?.NameFormatIndex ?? -1));
|
|
rootElement.Add(new XAttribute("locationtype", Map?.CurrentLocation?.Type?.Identifier ?? Identifier.Empty));
|
|
|
|
rootElement.Add(new XAttribute("nextleveltype", campaign.NextLevel?.Type ?? LevelData?.Type ?? LevelData.LevelType.Outpost));
|
|
|
|
rootElement.Add(new XAttribute("ismultiplayer", campaign is MultiPlayerCampaign));
|
|
|
|
LastSaveVersion = GameMain.Version;
|
|
rootElement.Add(new XAttribute("version", GameMain.Version));
|
|
if (Submarine?.Info != null && !Submarine.Removed && Campaign != null)
|
|
{
|
|
bool hasNewPendingSub = Campaign.PendingSubmarineSwitch != null &&
|
|
Campaign.PendingSubmarineSwitch.MD5Hash.StringRepresentation != Submarine.Info.MD5Hash.StringRepresentation;
|
|
if (hasNewPendingSub)
|
|
{
|
|
Campaign.SwitchSubs();
|
|
}
|
|
}
|
|
rootElement.Add(new XAttribute("submarine", SubmarineInfo == null ? "" : SubmarineInfo.Name));
|
|
if (OwnedSubmarines != null)
|
|
{
|
|
List<string> ownedSubmarineNames = new List<string>();
|
|
var ownedSubsElement = new XElement("ownedsubmarines");
|
|
rootElement.Add(ownedSubsElement);
|
|
foreach (var ownedSub in OwnedSubmarines)
|
|
{
|
|
ownedSubsElement.Add(new XElement("sub", new XAttribute("name", ownedSub.Name)));
|
|
}
|
|
}
|
|
|
|
if (Map != null) { rootElement.Add(new XAttribute("mapseed", Map.Seed)); }
|
|
rootElement.Add(new XAttribute("selectedcontentpackagenames",
|
|
string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).Select(cp => cp.Name.Replace("|", @"\|")))));
|
|
|
|
|
|
XElement permadeathsElement = new XElement("permadeaths");
|
|
foreach (var kvp in permadeathsPerAccount)
|
|
{
|
|
if (kvp.Key.TryUnwrap(out AccountId? accountId))
|
|
{
|
|
permadeathsElement.Add(
|
|
new XElement("account",
|
|
new XAttribute("id", accountId.StringRepresentation),
|
|
new XAttribute("permadeathcount", kvp.Value)));
|
|
}
|
|
}
|
|
rootElement.Add(permadeathsElement);
|
|
|
|
rootElement.Add(new XAttribute("respawnmode", GameMain.NetworkMember?.ServerSettings?.RespawnMode ?? RespawnMode.None));
|
|
|
|
((CampaignMode)GameMode).Save(doc.Root, isSavingOnLoading);
|
|
|
|
doc.SaveSafe(filePath, throwExceptions: true);
|
|
}
|
|
|
|
/*public void Load(XElement saveElement)
|
|
{
|
|
foreach (var subElement in saveElement.Elements())
|
|
{
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
#if CLIENT
|
|
case "gamemode": //legacy support
|
|
case "singleplayercampaign":
|
|
GameMode = SinglePlayerCampaign.Load(subElement);
|
|
break;
|
|
#endif
|
|
case "multiplayercampaign":
|
|
if (!(GameMode is MultiPlayerCampaign mpCampaign))
|
|
{
|
|
DebugConsole.ThrowError("Error while loading a save file: the save file is for a multiplayer campaign but the current gamemode is " + GameMode.GetType().ToString());
|
|
break;
|
|
}
|
|
|
|
mpCampaign.Load(subElement);
|
|
break;
|
|
}
|
|
}
|
|
}*/
|
|
|
|
}
|
|
}
|