1702 lines
78 KiB
C#
1702 lines
78 KiB
C#
using Barotrauma.Extensions;
|
|
using Barotrauma.IO;
|
|
using Barotrauma.Networking;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
partial class MultiPlayerCampaign : CampaignMode
|
|
{
|
|
private readonly List<CharacterCampaignData> characterData = new List<CharacterCampaignData>();
|
|
private readonly Dictionary<ushort, Wallet> walletsToCheck = new Dictionary<ushort, Wallet>();
|
|
private readonly HashSet<NetWalletTransaction> transactions = new HashSet<NetWalletTransaction>();
|
|
private const float clientCheckInterval = 10;
|
|
private float clientCheckTimer = clientCheckInterval;
|
|
|
|
/// <summary>
|
|
/// Temporary backup storage for characters that have been overwritten by SaveSingleCharacter, this will be gone
|
|
/// once the round ends or the server closes. Currently needed to enable the console command "revive" in ironman mode.
|
|
/// </summary>
|
|
public List<CharacterCampaignData> replacedCharacterDataBackup = new List<CharacterCampaignData>();
|
|
|
|
public override Wallet GetWallet(Client client = null)
|
|
{
|
|
if (client is null) { throw new ArgumentNullException(nameof(client), "Client should not be null in multiplayer"); }
|
|
|
|
if (client.Character is { } character)
|
|
{
|
|
return character.Wallet;
|
|
}
|
|
|
|
return Wallet.Invalid;
|
|
}
|
|
|
|
private bool forceMapUI;
|
|
public bool ForceMapUI
|
|
{
|
|
get { return forceMapUI; }
|
|
set
|
|
{
|
|
if (forceMapUI == value) { return; }
|
|
forceMapUI = value;
|
|
IncrementLastUpdateIdForFlag(NetFlags.MapAndMissions);
|
|
}
|
|
}
|
|
|
|
public bool GameOver { get; private set; }
|
|
|
|
class SavedExperiencePoints
|
|
{
|
|
public readonly Option<AccountId> AccountId;
|
|
public readonly Address Address;
|
|
public readonly int ExperiencePoints;
|
|
|
|
public SavedExperiencePoints(Client client)
|
|
{
|
|
AccountId = client.AccountId;
|
|
Address = client.Connection.Endpoint.Address;
|
|
ExperiencePoints = client.Character?.Info?.ExperiencePoints ?? 0;
|
|
}
|
|
|
|
public SavedExperiencePoints(XElement element)
|
|
{
|
|
AccountId = Networking.AccountId.Parse(
|
|
element.GetAttributeString("accountid", null)
|
|
?? element.GetAttributeString("steamid", ""));
|
|
Address = Address.Parse(
|
|
element.GetAttributeString("address", null)
|
|
?? element.GetAttributeString("endpoint", ""))
|
|
.Fallback(new UnknownAddress());
|
|
ExperiencePoints = element.GetAttributeInt("points", 0);
|
|
}
|
|
}
|
|
|
|
private readonly List<SavedExperiencePoints> savedExperiencePoints = new List<SavedExperiencePoints>();
|
|
|
|
public override bool Paused
|
|
{
|
|
get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition"); }
|
|
}
|
|
|
|
private bool purchasedHullRepairs, purchasedLostShuttles, purchasedItemRepairs;
|
|
public override bool PurchasedHullRepairs
|
|
{
|
|
get { return purchasedHullRepairs; }
|
|
set
|
|
{
|
|
if (purchasedHullRepairs == value) { return; }
|
|
purchasedHullRepairs = value;
|
|
PurchasedHullRepairsInLatestSave |= value;
|
|
IncrementLastUpdateIdForFlag(NetFlags.Misc);
|
|
DebugConsole.NewMessage("Set PurchasedHullRepairs to " + PurchasedHullRepairs, Color.Cyan);
|
|
}
|
|
}
|
|
public override bool PurchasedLostShuttles
|
|
{
|
|
get { return purchasedLostShuttles; }
|
|
set
|
|
{
|
|
if (purchasedLostShuttles == value) { return; }
|
|
purchasedLostShuttles = value;
|
|
PurchasedLostShuttlesInLatestSave |= value;
|
|
IncrementLastUpdateIdForFlag(NetFlags.Misc);
|
|
}
|
|
}
|
|
public override bool PurchasedItemRepairs
|
|
{
|
|
get { return purchasedItemRepairs; }
|
|
set
|
|
{
|
|
if (purchasedItemRepairs == value) { return; }
|
|
purchasedItemRepairs = value;
|
|
PurchasedItemRepairsInLatestSave |= value;
|
|
IncrementLastUpdateIdForFlag(NetFlags.Misc);
|
|
}
|
|
}
|
|
|
|
public static void StartNewCampaign(string savePath, string subPath, string seed, CampaignSettings startingSettings)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(savePath)) { return; }
|
|
|
|
GameMain.GameSession = new GameSession(new SubmarineInfo(subPath), Option.None, CampaignDataPath.CreateRegular(savePath), GameModePreset.MultiPlayerCampaign, startingSettings, seed);
|
|
GameMain.NetLobbyScreen.ToggleCampaignMode(true);
|
|
SaveUtil.SaveGame(GameMain.GameSession.DataPath);
|
|
|
|
DebugConsole.NewMessage("Campaign started!", Color.Cyan);
|
|
DebugConsole.NewMessage("Current location: " + GameMain.GameSession.Map.CurrentLocation.DisplayName, Color.Cyan);
|
|
((MultiPlayerCampaign)GameMain.GameSession.GameMode).LoadInitialLevel();
|
|
}
|
|
|
|
public static void LoadCampaign(CampaignDataPath path, Client client)
|
|
{
|
|
GameMain.NetLobbyScreen.ToggleCampaignMode(true);
|
|
try
|
|
{
|
|
SaveUtil.LoadGame(path);
|
|
if (GameMain.GameSession.GameMode is MultiPlayerCampaign mpCampaign)
|
|
{
|
|
mpCampaign.LastSaveID++;
|
|
}
|
|
else
|
|
{
|
|
DebugConsole.ThrowError("Failed to load a campaign. Unexpected game mode: " + GameMain.GameSession.GameMode ?? "none");
|
|
return;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
string errorMsg = $"Error while loading the save {path.LoadPath}";
|
|
if (client != null)
|
|
{
|
|
GameMain.Server?.SendDirectChatMessage($"{errorMsg}: {e.Message}\n{e.StackTrace}", client, ChatMessageType.Error);
|
|
}
|
|
DebugConsole.ThrowError(errorMsg, e);
|
|
return;
|
|
}
|
|
DebugConsole.NewMessage("Campaign loaded!", Color.Cyan);
|
|
DebugConsole.NewMessage(
|
|
GameMain.GameSession.Map.SelectedLocation == null ?
|
|
GameMain.GameSession.Map.CurrentLocation.DisplayName :
|
|
GameMain.GameSession.Map.CurrentLocation.DisplayName + " -> " + GameMain.GameSession.Map.SelectedLocation.DisplayName, Color.Cyan);
|
|
}
|
|
|
|
protected override void LoadInitialLevel()
|
|
{
|
|
NextLevel = map.SelectedConnection?.LevelData ?? map.CurrentLocation.LevelData;
|
|
MirrorLevel = false;
|
|
GameMain.Server.TryStartGame();
|
|
}
|
|
|
|
public static void StartCampaignSetup()
|
|
{
|
|
DebugConsole.NewMessage("********* CAMPAIGN SETUP *********", Color.White);
|
|
DebugConsole.ShowQuestionPrompt("Do you want to start a new campaign? Y/N", (string arg) =>
|
|
{
|
|
if (arg.Equals("y", StringComparison.OrdinalIgnoreCase) || arg.Equals("yes", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
DebugConsole.ShowQuestionPrompt("Enter a save name for the campaign:", (string saveName) =>
|
|
{
|
|
string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName);
|
|
StartNewCampaign(savePath, GameMain.NetLobbyScreen.SelectedSub.FilePath, GameMain.NetLobbyScreen.LevelSeed, CampaignSettings.Empty);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
var saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false).ToArray();
|
|
if (saveFiles.Length == 0)
|
|
{
|
|
DebugConsole.ThrowError("No save files found.");
|
|
return;
|
|
}
|
|
DebugConsole.NewMessage("Saved campaigns:", Color.White);
|
|
for (int i = 0; i < saveFiles.Length; i++)
|
|
{
|
|
DebugConsole.NewMessage(" " + i + ". " + saveFiles[i].FilePath, Color.White);
|
|
}
|
|
DebugConsole.ShowQuestionPrompt("Select a save file to load (0 - " + (saveFiles.Length - 1) + "):", (string selectedSave) =>
|
|
{
|
|
int saveIndex = -1;
|
|
if (!int.TryParse(selectedSave, out saveIndex)) { return; }
|
|
|
|
if (saveIndex < 0 || saveIndex >= saveFiles.Length)
|
|
{
|
|
DebugConsole.ThrowError("Invalid save file index.");
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
LoadCampaign(CampaignDataPath.CreateRegular(saveFiles[saveIndex].FilePath), client: null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
DebugConsole.ThrowError("Failed to load the campaign.", ex);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
public override void Start()
|
|
{
|
|
base.Start();
|
|
IncrementAllLastUpdateIds();
|
|
}
|
|
|
|
private static bool IsOwner(Client client) => client != null && client.Connection == GameMain.Server.OwnerConnection;
|
|
|
|
public void SaveExperiencePoints(Client client)
|
|
{
|
|
ClearSavedExperiencePoints(client);
|
|
savedExperiencePoints.Add(new SavedExperiencePoints(client));
|
|
}
|
|
public int GetSavedExperiencePoints(Client client)
|
|
{
|
|
return savedExperiencePoints.Find(s => client.AccountId == s.AccountId || client.Connection.Endpoint.Address == s.Address)?.ExperiencePoints ?? 0;
|
|
}
|
|
public void ClearSavedExperiencePoints(Client client)
|
|
{
|
|
savedExperiencePoints.RemoveAll(s => client.AccountId == s.AccountId || client.Connection.Endpoint.Address == s.Address);
|
|
}
|
|
|
|
public void RefreshCharacterCampaignData(Character character, bool refreshHealthData)
|
|
{
|
|
var matchingData = characterData.FirstOrDefault(c => c.CharacterInfo == character.Info);
|
|
matchingData?.Refresh(character, refreshHealthData: refreshHealthData);
|
|
}
|
|
|
|
public void SavePlayers()
|
|
{
|
|
//refresh the character data of clients who are still in the server
|
|
foreach (Client c in GameMain.Server.ConnectedClients)
|
|
{
|
|
//ignore if the character is controlling a monster
|
|
//(we'll just use the previously saved campaign data if there's any)
|
|
if (c.Character != null && c.Character.Info == null)
|
|
{
|
|
c.Character = null;
|
|
}
|
|
//use the info of the character the client is currently controlling
|
|
// or the previously saved info if not (e.g. if the client has been spectating or died)
|
|
var characterInfo = c.Character?.Info;
|
|
var matchingCharacterData = characterData.Find(d => d.MatchesClient(c));
|
|
if (matchingCharacterData != null)
|
|
{
|
|
//hasn't spawned this round -> don't touch the data
|
|
if (!matchingCharacterData.HasSpawned) { continue; }
|
|
characterInfo ??= matchingCharacterData.CharacterInfo;
|
|
}
|
|
if (characterInfo == null || characterInfo.Discarded) { continue; }
|
|
//reduce skills if the character has died
|
|
bool diedToDisconnect = characterInfo.CauseOfDeath is { Type: CauseOfDeathType.Disconnected };
|
|
bool diedForReal = characterInfo.CauseOfDeath != null && !diedToDisconnect;
|
|
if (diedForReal)
|
|
{
|
|
characterInfo.ApplyDeathEffects();
|
|
}
|
|
c.CharacterInfo = characterInfo;
|
|
|
|
//Different scenarios for saving (or not saving) the character:
|
|
// 1. Client is controlling a character, character is alive
|
|
// -> save normally
|
|
// 2. Client is not controlling a character, previous character died
|
|
// -> save normally (applying skill reduction, getting rid of the items the character had, etc)
|
|
// 3. Client is not controlling a character, previous character died due to a disconnect (e.g. client left or went to the lobby, and later returned)
|
|
// -> do not save, the character should retain their skills and items
|
|
// 4. Client is not controlling a character, and they don't have any previous character data
|
|
// -> we shouldn't be getting to this point at all, we return earlier if no character info that has spawned is found
|
|
// 5. Client is not controlling a character, but they have previous character data which has not yet spawned (e.g. they've played on this server on some previous round)
|
|
// -> we shouldn't be getting to this point at all, we return earlier if no character info that has spawned is found
|
|
|
|
if (c.Character != null || diedForReal)
|
|
{
|
|
SetClientCharacterData(c);
|
|
}
|
|
}
|
|
|
|
//refresh the character data of clients who aren't in the server anymore
|
|
List<CharacterCampaignData> prevCharacterData = new List<CharacterCampaignData>(characterData);
|
|
foreach (CharacterCampaignData data in prevCharacterData)
|
|
{
|
|
if (data.HasSpawned && !GameMain.Server.ConnectedClients.Any(c => data.MatchesClient(c)))
|
|
{
|
|
var character = Character.CharacterList.Find(c => c.Info == data.CharacterInfo && !c.IsHusk);
|
|
if (character != null &&
|
|
(!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected))
|
|
{
|
|
//character still alive (or killed by Disconnect) -> save it as-is
|
|
characterData.RemoveAll(cd => cd.IsDuplicate(data));
|
|
data.Refresh(character, refreshHealthData: character.CauseOfDeath?.Type != CauseOfDeathType.Disconnected);
|
|
characterData.Add(data);
|
|
}
|
|
//check the cause of death in the CharacterInfo too (the character instance may have despawned, so we can't just rely on that)
|
|
else if (data.CharacterInfo.CauseOfDeath is not { Type: CauseOfDeathType.Disconnected })
|
|
{
|
|
//character dead or removed -> reduce skills, remove items, health data, etc
|
|
data.CharacterInfo.ApplyDeathEffects();
|
|
data.Reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
MoveDiscardedCharacterBalancesToBank();
|
|
|
|
characterData.ForEach(cd => cd.HasSpawned = false);
|
|
foreach (var cd in characterData)
|
|
{
|
|
//remove from crewmanager - we don't need to save the data there if it's been saved as CharacterCampaignData
|
|
//(e.g. if a client has taken over a bot, we need to do this to prevent it being saved twice)
|
|
CrewManager.RemoveCharacterInfo(cd.CharacterInfo);
|
|
}
|
|
|
|
SavePets();
|
|
|
|
//remove all items that are in someone's inventory
|
|
foreach (Character c in Character.CharacterList)
|
|
{
|
|
if (c.Inventory == null) { continue; }
|
|
if (Level.Loaded.Type == LevelData.LevelType.Outpost && c.Submarine != Level.Loaded.StartOutpost)
|
|
{
|
|
Map.CurrentLocation.RegisterTakenItems(c.Inventory.AllItems.Where(it => it.SpawnedInCurrentOutpost && it.OriginalModuleIndex > 0));
|
|
}
|
|
|
|
if (c.Info != null && c.IsBot)
|
|
{
|
|
if (c.IsDead && c.CauseOfDeath?.Type != CauseOfDeathType.Disconnected) { CrewManager.RemoveCharacterInfo(c.Info); }
|
|
c.Info.HealthData = new XElement("health");
|
|
c.CharacterHealth.Save(c.Info.HealthData);
|
|
c.Info.InventoryData = new XElement("inventory");
|
|
c.SaveInventory();
|
|
c.Info.SaveOrderData();
|
|
}
|
|
|
|
c.Inventory.DeleteAllItems();
|
|
}
|
|
|
|
SaveActiveOrders();
|
|
}
|
|
|
|
public void MoveDiscardedCharacterBalancesToBank()
|
|
{
|
|
foreach (var discardedCharacter in discardedCharacters)
|
|
{
|
|
if (discardedCharacter.WalletData != null)
|
|
{
|
|
var wallet =
|
|
Character.CharacterList.Find(c => c.Info == discardedCharacter.CharacterInfo)?.Wallet ??
|
|
new Wallet(Option<Character>.None(), discardedCharacter.WalletData);
|
|
Bank.Give(wallet.Balance);
|
|
}
|
|
}
|
|
discardedCharacters.Clear();
|
|
}
|
|
|
|
protected override IEnumerable<CoroutineStatus> DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror)
|
|
{
|
|
IncrementAllLastUpdateIds();
|
|
|
|
switch (transitionType)
|
|
{
|
|
case TransitionType.None:
|
|
throw new InvalidOperationException("Level transition failed (no transitions available).");
|
|
case TransitionType.ReturnToPreviousLocation:
|
|
//deselect destination on map
|
|
map.SelectLocation(-1);
|
|
break;
|
|
case TransitionType.ProgressToNextLocation:
|
|
Map.MoveToNextLocation();
|
|
TotalPassedLevels++;
|
|
break;
|
|
case TransitionType.End:
|
|
EndCampaign();
|
|
IsFirstRound = true;
|
|
break;
|
|
case TransitionType.ProgressToNextEmptyLocation:
|
|
Map.Visit(Map.CurrentLocation);
|
|
TotalPassedLevels++;
|
|
break;
|
|
}
|
|
|
|
Map.ProgressWorld(this, transitionType, GameMain.GameSession.RoundDuration);
|
|
|
|
bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
|
|
if (success)
|
|
{
|
|
foreach (Client c in GameMain.Server.ConnectedClients)
|
|
{
|
|
if (c.Character?.HasAbilityFlag(AbilityFlags.RetainExperienceForNewCharacter) ?? false)
|
|
{
|
|
(GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(c);
|
|
}
|
|
}
|
|
// Event history must be registered before ending the round or it will be cleared
|
|
GameMain.GameSession.EventManager.StoreEventDataAtRoundEnd();
|
|
}
|
|
|
|
//store the currently active missions at this point so we can communicate their states to clients, they're cleared in EndRound
|
|
List<Mission> missions = GameMain.GameSession.Missions.ToList();
|
|
|
|
GameMain.GameSession.EndRound("", transitionType);
|
|
|
|
//--------------------------------------
|
|
|
|
if (success)
|
|
{
|
|
SavePlayers();
|
|
yield return CoroutineStatus.Running;
|
|
LeaveUnconnectedSubs(leavingSub);
|
|
NextLevel = newLevel;
|
|
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
|
|
SaveUtil.SaveGame(GameMain.GameSession.DataPath);
|
|
}
|
|
else
|
|
{
|
|
PendingSubmarineSwitch = null;
|
|
GameMain.Server.EndGame(TransitionType.None, wasSaved: false);
|
|
LoadCampaign(GameMain.GameSession.DataPath, client: null);
|
|
LastSaveID++;
|
|
IncrementAllLastUpdateIds();
|
|
yield return CoroutineStatus.Success;
|
|
}
|
|
|
|
CrewManager?.ClearCurrentOrders();
|
|
|
|
//--------------------------------------
|
|
|
|
GameMain.Server.EndGame(transitionType, wasSaved: true, missions);
|
|
|
|
ForceMapUI = false;
|
|
|
|
NextLevel = newLevel;
|
|
MirrorLevel = mirror;
|
|
|
|
|
|
yield return new WaitForSeconds(EndTransitionDuration * 0.5f);
|
|
|
|
//don't start the next round automatically if we just finished the campaign
|
|
if (transitionType != TransitionType.End)
|
|
{
|
|
GameMain.Server.TryStartGame();
|
|
}
|
|
|
|
yield return CoroutineStatus.Success;
|
|
}
|
|
|
|
partial void InitProjSpecific()
|
|
{
|
|
Identifier eventId = nameof(MultiPlayerCampaign).ToIdentifier();
|
|
CargoManager.OnItemsInBuyCrateChanged.RegisterOverwriteExisting(eventId, _ => IncrementLastUpdateIdForFlag(NetFlags.ItemsInBuyCrate));
|
|
CargoManager.OnPurchasedItemsChanged.RegisterOverwriteExisting(eventId, _ => IncrementLastUpdateIdForFlag(NetFlags.PurchasedItems));
|
|
CargoManager.OnSoldItemsChanged.RegisterOverwriteExisting(eventId, _ => IncrementLastUpdateIdForFlag(NetFlags.SoldItems));
|
|
UpgradeManager.OnUpgradesChanged.RegisterOverwriteExisting(eventId, _ => IncrementLastUpdateIdForFlag(NetFlags.UpgradeManager));
|
|
|
|
Reputation.OnAnyReputationValueChanged.RegisterOverwriteExisting(eventId, _ => IncrementLastUpdateIdForFlag(NetFlags.Reputation));
|
|
|
|
Map.OnLocationSelected = (loc, connection) => IncrementLastUpdateIdForFlag(NetFlags.MapAndMissions);
|
|
Map.OnMissionsSelected = (loc, mission) => IncrementLastUpdateIdForFlag(NetFlags.MapAndMissions);
|
|
|
|
//increment save ID so clients know they're lacking the most up-to-date save file
|
|
LastSaveID++;
|
|
}
|
|
|
|
public bool CanPurchaseSub(SubmarineInfo info, Client client)
|
|
=> CanAfford(info.GetPrice(), client) && GetCampaignSubs().Contains(info);
|
|
|
|
private readonly List<CharacterCampaignData> discardedCharacters = new List<CharacterCampaignData>();
|
|
public void DiscardClientCharacterData(Client client)
|
|
{
|
|
foreach (var data in characterData.ToList())
|
|
{
|
|
if (data.MatchesClient(client))
|
|
{
|
|
if (!discardedCharacters.Any(d => d.MatchesClient(client)))
|
|
{
|
|
discardedCharacters.Add(data);
|
|
}
|
|
DebugConsole.Log($"Client \"{client}\" discarded the character ({data.Name})");
|
|
data.CharacterInfo.Discarded = true;
|
|
characterData.Remove(data);
|
|
IncrementLastUpdateIdForFlag(NetFlags.CharacterInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
public CharacterCampaignData GetClientCharacterData(Client client)
|
|
{
|
|
return characterData.Find(cd => cd.MatchesClient(client));
|
|
}
|
|
|
|
public CharacterCampaignData GetCharacterData(CharacterInfo characterInfo)
|
|
{
|
|
return characterData.Find(cd => cd.CharacterInfo == characterInfo);
|
|
}
|
|
|
|
public CharacterCampaignData SetClientCharacterData(Client client)
|
|
{
|
|
characterData.RemoveAll(cd => cd.MatchesClient(client));
|
|
var data = new CharacterCampaignData(client);
|
|
characterData.Add(data);
|
|
IncrementLastUpdateIdForFlag(NetFlags.CharacterInfo);
|
|
return data;
|
|
}
|
|
|
|
public void AssignClientCharacterInfos(IEnumerable<Client> connectedClients)
|
|
{
|
|
foreach (Client client in connectedClients)
|
|
{
|
|
if (client.SpectateOnly && GameMain.Server.ServerSettings.AllowSpectating) { continue; }
|
|
var matchingData = GetClientCharacterData(client);
|
|
if (matchingData != null) { client.CharacterInfo = matchingData.CharacterInfo; }
|
|
}
|
|
IncrementLastUpdateIdForFlag(NetFlags.CharacterInfo);
|
|
}
|
|
|
|
public Dictionary<Client, Job> GetAssignedJobs(IEnumerable<Client> connectedClients)
|
|
{
|
|
var assignedJobs = new Dictionary<Client, Job>();
|
|
foreach (Client client in connectedClients)
|
|
{
|
|
var matchingData = GetClientCharacterData(client);
|
|
if (matchingData != null) assignedJobs.Add(client, matchingData.CharacterInfo.Job);
|
|
}
|
|
return assignedJobs;
|
|
}
|
|
|
|
public override void Update(float deltaTime)
|
|
{
|
|
if (CoroutineManager.IsCoroutineRunning("LevelTransition")) { return; }
|
|
|
|
Map?.Radiation?.UpdateRadiation(deltaTime);
|
|
|
|
base.Update(deltaTime);
|
|
|
|
MedicalClinic?.Update(deltaTime);
|
|
|
|
if (Level.Loaded != null)
|
|
{
|
|
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
|
|
{
|
|
var transitionType = GetAvailableTransition(out _, out Submarine leavingSub);
|
|
if (transitionType == TransitionType.End ||
|
|
(Level.Loaded.IsEndBiome && transitionType == TransitionType.ProgressToNextLocation))
|
|
{
|
|
LoadNewLevel();
|
|
}
|
|
else if (GameMain.Server.ConnectedClients.Count == 0 || GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead))
|
|
{
|
|
if (transitionType == TransitionType.ProgressToNextLocation && Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub))
|
|
{
|
|
LoadNewLevel();
|
|
}
|
|
else if (transitionType == TransitionType.ReturnToPreviousLocation && Level.Loaded.StartOutpost != null && Level.Loaded.StartOutpost.DockedTo.Contains(leavingSub))
|
|
{
|
|
LoadNewLevel();
|
|
}
|
|
}
|
|
}
|
|
else if (Level.Loaded.IsEndBiome)
|
|
{
|
|
var transitionType = GetAvailableTransition(out _, out Submarine leavingSub);
|
|
if (transitionType == TransitionType.ProgressToNextLocation)
|
|
{
|
|
LoadNewLevel();
|
|
}
|
|
}
|
|
else if (Level.Loaded.Type == LevelData.LevelType.Outpost)
|
|
{
|
|
KeepCharactersCloseToOutpost(deltaTime);
|
|
}
|
|
}
|
|
|
|
UpdateClientsToCheck(deltaTime);
|
|
UpdateWallets();
|
|
}
|
|
|
|
private void UpdateClientsToCheck(float deltaTime)
|
|
{
|
|
if (clientCheckTimer < clientCheckInterval)
|
|
{
|
|
clientCheckTimer += deltaTime;
|
|
return;
|
|
}
|
|
|
|
clientCheckTimer = 0;
|
|
walletsToCheck.Clear();
|
|
walletsToCheck.Add(0, Bank);
|
|
|
|
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Player))
|
|
{
|
|
walletsToCheck.Add(character.ID, character.Wallet);
|
|
}
|
|
}
|
|
|
|
private void UpdateWallets()
|
|
{
|
|
foreach (var (id, wallet) in walletsToCheck)
|
|
{
|
|
if (wallet.HasTransactions())
|
|
{
|
|
NetWalletTransaction transaction = wallet.DequeueAndMergeTransactions(id);
|
|
if (!wallet.ShouldForceUpdate && transaction.ChangedData.BalanceChanged.IsNone() && transaction.ChangedData.RewardDistributionChanged.IsNone()) { continue; }
|
|
transactions.Add(transaction);
|
|
wallet.ShouldForceUpdate = false;
|
|
}
|
|
}
|
|
|
|
if (transactions.Count == 0) { return; }
|
|
|
|
NetWalletUpdate walletUpdate = new NetWalletUpdate
|
|
{
|
|
Transactions = transactions.ToArray()
|
|
};
|
|
|
|
transactions.Clear();
|
|
|
|
foreach (Client client in GameMain.Server.ConnectedClients)
|
|
{
|
|
IWriteMessage msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.MONEY);
|
|
((INetSerializableStruct)walletUpdate).Write(msg);
|
|
GameMain.Server?.ServerPeer?.Send(msg, client.Connection, DeliveryMethod.Reliable);
|
|
}
|
|
}
|
|
|
|
public override void End(TransitionType transitionType = TransitionType.None)
|
|
{
|
|
GameOver = !GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
|
|
base.End(transitionType);
|
|
}
|
|
|
|
private bool IsFlagRequired(Client c, NetFlags flag)
|
|
=> !c.LastRecvCampaignUpdate.TryGetValue(flag, out var id) || NetIdUtils.IdMoreRecent(GetLastUpdateIdForFlag(flag), id);
|
|
|
|
public void ServerWrite(IWriteMessage msg, Client c)
|
|
{
|
|
System.Diagnostics.Debug.Assert(map.Locations.Count < UInt16.MaxValue);
|
|
|
|
NetFlags requiredFlags = lastUpdateID.Keys.Where(k => IsFlagRequired(c, k)).Aggregate((NetFlags)0, (f1, f2) => f1 | f2);
|
|
|
|
msg.WriteUInt16((UInt16)requiredFlags);
|
|
|
|
msg.WriteBoolean(IsFirstRound);
|
|
msg.WriteByte(CampaignID);
|
|
msg.WriteByte(RoundID);
|
|
msg.WriteUInt16(lastSaveID);
|
|
msg.WriteString(map.Seed);
|
|
|
|
if (requiredFlags.HasFlag(NetFlags.Misc))
|
|
{
|
|
msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.Misc));
|
|
msg.WriteBoolean(PurchasedHullRepairs);
|
|
msg.WriteBoolean(PurchasedItemRepairs);
|
|
msg.WriteBoolean(PurchasedLostShuttles);
|
|
}
|
|
|
|
if (requiredFlags.HasFlag(NetFlags.MapAndMissions))
|
|
{
|
|
msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.MapAndMissions));
|
|
msg.WriteBoolean(ForceMapUI);
|
|
msg.WriteBoolean(map.AllowDebugTeleport);
|
|
msg.WriteUInt16(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex);
|
|
msg.WriteUInt16(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex);
|
|
|
|
if (map.CurrentLocation != null)
|
|
{
|
|
msg.WriteByte((byte)map.CurrentLocation.AvailableMissions.Count());
|
|
foreach (Mission mission in map.CurrentLocation.AvailableMissions)
|
|
{
|
|
msg.WriteIdentifier(mission.Prefab.Identifier);
|
|
if (mission.Locations[0] == mission.Locations[1])
|
|
{
|
|
msg.WriteByte((byte)255);
|
|
}
|
|
else
|
|
{
|
|
Location missionDestination = mission.Locations[0] == map.CurrentLocation ? mission.Locations[1] : mission.Locations[0];
|
|
LocationConnection connection = map.CurrentLocation.Connections.Find(c => c.OtherLocation(map.CurrentLocation) == missionDestination);
|
|
msg.WriteByte((byte)map.CurrentLocation.Connections.IndexOf(connection));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
msg.WriteByte((byte)0);
|
|
}
|
|
|
|
var selectedMissionIndices = map.GetSelectedMissionIndices();
|
|
msg.WriteByte((byte)selectedMissionIndices.Count());
|
|
foreach (int selectedMissionIndex in selectedMissionIndices)
|
|
{
|
|
msg.WriteByte((byte)selectedMissionIndex);
|
|
}
|
|
|
|
WriteStores(msg);
|
|
}
|
|
|
|
if (requiredFlags.HasFlag(NetFlags.SubList))
|
|
{
|
|
msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.SubList));
|
|
var subList = GameMain.NetLobbyScreen.GetSubList();
|
|
List<int> ownedSubmarineIndices = new List<int>();
|
|
for (int i = 0; i < subList.Count; i++)
|
|
{
|
|
if (GameMain.GameSession.OwnedSubmarines.Any(s => s.Name == subList[i].Name))
|
|
{
|
|
ownedSubmarineIndices.Add(i);
|
|
}
|
|
}
|
|
msg.WriteUInt16((ushort)ownedSubmarineIndices.Count);
|
|
foreach (int index in ownedSubmarineIndices)
|
|
{
|
|
msg.WriteUInt16((ushort)index);
|
|
}
|
|
}
|
|
if (requiredFlags.HasFlag(NetFlags.UpgradeManager))
|
|
{
|
|
msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.UpgradeManager));
|
|
msg.WriteUInt16((ushort)UpgradeManager.PendingUpgrades.Count);
|
|
foreach (var (prefab, category, level) in UpgradeManager.PendingUpgrades)
|
|
{
|
|
msg.WriteIdentifier(prefab.Identifier);
|
|
msg.WriteIdentifier(category.Identifier);
|
|
msg.WriteByte((byte)level);
|
|
}
|
|
msg.WriteUInt16((ushort)UpgradeManager.PurchasedItemSwaps.Count);
|
|
foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps)
|
|
{
|
|
msg.WriteUInt16(itemSwap.ItemToRemove.ID);
|
|
msg.WriteIdentifier(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty);
|
|
}
|
|
}
|
|
|
|
if (requiredFlags.HasFlag(NetFlags.ItemsInBuyCrate))
|
|
{
|
|
msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.ItemsInBuyCrate));
|
|
WriteItems(msg, CargoManager.ItemsInBuyCrate);
|
|
WriteStores(msg);
|
|
}
|
|
|
|
if (requiredFlags.HasFlag(NetFlags.ItemsInSellFromSubCrate))
|
|
{
|
|
msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.ItemsInSellFromSubCrate));
|
|
WriteItems(msg, CargoManager.ItemsInSellFromSubCrate);
|
|
WriteStores(msg);
|
|
}
|
|
|
|
if (requiredFlags.HasFlag(NetFlags.PurchasedItems))
|
|
{
|
|
msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.PurchasedItems));
|
|
WriteItems(msg, CargoManager.PurchasedItems);
|
|
WriteStores(msg);
|
|
|
|
}
|
|
if (requiredFlags.HasFlag(NetFlags.SoldItems))
|
|
{
|
|
msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.SoldItems));
|
|
WriteItems(msg, CargoManager.SoldItems);
|
|
WriteStores(msg);
|
|
}
|
|
if (requiredFlags.HasFlag(NetFlags.Reputation))
|
|
{
|
|
msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.Reputation));
|
|
// hopefully we'll never have more than 128 factions
|
|
msg.WriteByte((byte)Factions.Count);
|
|
foreach (Faction faction in Factions)
|
|
{
|
|
msg.WriteIdentifier(faction.Prefab.Identifier);
|
|
msg.WriteSingle(faction.Reputation.Value);
|
|
}
|
|
}
|
|
if (requiredFlags.HasFlag(NetFlags.CharacterInfo))
|
|
{
|
|
msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.CharacterInfo));
|
|
var characterData = GetClientCharacterData(c);
|
|
if (characterData?.CharacterInfo == null)
|
|
{
|
|
msg.WriteBoolean(false);
|
|
}
|
|
else
|
|
{
|
|
msg.WriteBoolean(true);
|
|
characterData.CharacterInfo.ServerWrite(msg);
|
|
}
|
|
}
|
|
|
|
void WriteStores(IWriteMessage msg)
|
|
{
|
|
if (map.CurrentLocation != null)
|
|
{
|
|
// Store balance
|
|
bool hasStores = map.CurrentLocation.Stores != null && map.CurrentLocation.Stores.Any();
|
|
msg.WriteBoolean(hasStores);
|
|
if (hasStores)
|
|
{
|
|
msg.WriteByte((byte)map.CurrentLocation.Stores.Count);
|
|
foreach (var store in map.CurrentLocation.Stores.Values)
|
|
{
|
|
msg.WriteIdentifier(store.Identifier);
|
|
msg.WriteUInt16((UInt16)store.Balance);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
msg.WriteByte((byte)0);
|
|
// Store balance
|
|
msg.WriteBoolean(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ServerRead(IReadMessage msg, Client sender)
|
|
{
|
|
UInt16 currentLocIndex = msg.ReadUInt16();
|
|
UInt16 selectedLocIndex = msg.ReadUInt16();
|
|
|
|
byte selectedMissionCount = msg.ReadByte();
|
|
List<int> selectedMissionIndices = new List<int>();
|
|
for (int i = 0; i < selectedMissionCount; i++)
|
|
{
|
|
selectedMissionIndices.Add(msg.ReadByte());
|
|
}
|
|
|
|
bool purchasedHullRepairs = msg.ReadBoolean();
|
|
bool purchasedItemRepairs = msg.ReadBoolean();
|
|
bool purchasedLostShuttles = msg.ReadBoolean();
|
|
|
|
var buyCrateItems = ReadPurchasedItems(msg, sender);
|
|
var subSellCrateItems = ReadPurchasedItems(msg, sender);
|
|
var purchasedItems = ReadPurchasedItems(msg, sender);
|
|
var soldItems = ReadSoldItems(msg);
|
|
|
|
ushort purchasedUpgradeCount = msg.ReadUInt16();
|
|
List<PurchasedUpgrade> purchasedUpgrades = new List<PurchasedUpgrade>();
|
|
for (int i = 0; i < purchasedUpgradeCount; i++)
|
|
{
|
|
Identifier upgradeIdentifier = msg.ReadIdentifier();
|
|
UpgradePrefab prefab = UpgradePrefab.Find(upgradeIdentifier);
|
|
|
|
Identifier categoryIdentifier = msg.ReadIdentifier();
|
|
UpgradeCategory category = UpgradeCategory.Find(categoryIdentifier);
|
|
|
|
int upgradeLevel = msg.ReadByte();
|
|
|
|
if (category == null || prefab == null) { continue; }
|
|
purchasedUpgrades.Add(new PurchasedUpgrade(prefab, category, upgradeLevel));
|
|
}
|
|
|
|
ushort purchasedItemSwapCount = msg.ReadUInt16();
|
|
List<PurchasedItemSwap> purchasedItemSwaps = new List<PurchasedItemSwap>();
|
|
for (int i = 0; i < purchasedItemSwapCount; i++)
|
|
{
|
|
UInt16 itemToRemoveID = msg.ReadUInt16();
|
|
Identifier itemToInstallIdentifier = msg.ReadIdentifier();
|
|
ItemPrefab itemToInstall = itemToInstallIdentifier.IsEmpty ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier);
|
|
if (Entity.FindEntityByID(itemToRemoveID) is not Item itemToRemove) { continue; }
|
|
purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall));
|
|
}
|
|
|
|
if (purchasedUpgradeCount > 0 || purchasedItemSwapCount > 0)
|
|
{
|
|
//if the client attempted to purchase something, increment flag regardless of whether the upgrades were actually purchased or not
|
|
//so we can sync the correct state in case the client incorrectly assumed they can buy something (e.g. lost permissions just as they were purchasing)
|
|
IncrementLastUpdateIdForFlag(NetFlags.UpgradeManager);
|
|
}
|
|
|
|
int hullRepairCost = GetHullRepairCost();
|
|
int itemRepairCost = GetItemRepairCost();
|
|
int shuttleRetrieveCost = CampaignMode.ShuttleReplaceCost;
|
|
Location location = Map.CurrentLocation;
|
|
if (location != null)
|
|
{
|
|
hullRepairCost = location.GetAdjustedMechanicalCost(hullRepairCost);
|
|
itemRepairCost = location.GetAdjustedMechanicalCost(itemRepairCost);
|
|
shuttleRetrieveCost = location.GetAdjustedMechanicalCost(shuttleRetrieveCost);
|
|
}
|
|
|
|
Wallet personalWallet = GetWallet(sender);
|
|
personalWallet?.ForceUpdate();
|
|
if (AllowedToManageWallets(sender))
|
|
{
|
|
Bank.ForceUpdate();
|
|
}
|
|
|
|
if (purchasedHullRepairs && !PurchasedHullRepairs)
|
|
{
|
|
if (GetBalance(sender) >= hullRepairCost)
|
|
{
|
|
TryPurchase(sender, hullRepairCost);
|
|
PurchasedHullRepairs = true;
|
|
GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs");
|
|
}
|
|
}
|
|
|
|
if (purchasedItemRepairs && !PurchasedItemRepairs)
|
|
{
|
|
if (GetBalance(sender) >= itemRepairCost)
|
|
{
|
|
TryPurchase(sender, itemRepairCost);
|
|
PurchasedItemRepairs = true;
|
|
GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs");
|
|
}
|
|
}
|
|
|
|
if (purchasedLostShuttles && !PurchasedLostShuttles)
|
|
{
|
|
if (GameMain.GameSession?.SubmarineInfo != null && GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied)
|
|
{
|
|
GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox);
|
|
}
|
|
else if (TryPurchase(sender, shuttleRetrieveCost))
|
|
{
|
|
PurchasedLostShuttles = true;
|
|
GameAnalyticsManager.AddMoneySpentEvent(shuttleRetrieveCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle");
|
|
}
|
|
}
|
|
|
|
if (currentLocIndex < Map.Locations.Count && Map.AllowDebugTeleport)
|
|
{
|
|
Map.SetLocation(currentLocIndex);
|
|
}
|
|
|
|
if (AllowedToManageCampaign(sender, ClientPermissions.ManageMap))
|
|
{
|
|
Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex);
|
|
if (Map.SelectedLocation == null) { Map.SelectRandomLocation(preferUndiscovered: true); }
|
|
if (Map.SelectedConnection != null) { Map.SelectMission(selectedMissionIndices); }
|
|
CheckTooManyMissions(Map.CurrentLocation, sender);
|
|
}
|
|
|
|
if (HasCampaignInteractionAvailable(sender, InteractionType.Store))
|
|
{
|
|
var prevBuyCrateItems = new Dictionary<Identifier, List<PurchasedItem>>();
|
|
foreach (var kvp in CargoManager.ItemsInBuyCrate)
|
|
{
|
|
prevBuyCrateItems.Add(kvp.Key, new List<PurchasedItem>(kvp.Value));
|
|
}
|
|
foreach (var store in prevBuyCrateItems)
|
|
{
|
|
foreach (var item in store.Value.ToList())
|
|
{
|
|
CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, -item.Quantity, sender);
|
|
}
|
|
}
|
|
foreach (var store in buyCrateItems)
|
|
{
|
|
foreach (var item in store.Value.ToList())
|
|
{
|
|
if (map?.CurrentLocation?.Stores == null || !map.CurrentLocation.Stores.ContainsKey(store.Key)) { continue; }
|
|
int availableQuantity = map.CurrentLocation.Stores[store.Key].Stock.Find(s => s.ItemPrefab == item.ItemPrefab)?.Quantity ?? 0;
|
|
int alreadyPurchasedQuantity =
|
|
CargoManager.GetBuyCrateItem(store.Key, item.ItemPrefab)?.Quantity ?? 0 +
|
|
CargoManager.GetPurchasedItemCount(store.Key, item.ItemPrefab);
|
|
item.Quantity = MathHelper.Clamp(item.Quantity, 0, availableQuantity - alreadyPurchasedQuantity);
|
|
CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, item.Quantity, sender);
|
|
}
|
|
}
|
|
|
|
var prevPurchasedItems = new Dictionary<Identifier, List<PurchasedItem>>();
|
|
foreach (var kvp in CargoManager.PurchasedItems)
|
|
{
|
|
prevPurchasedItems.Add(kvp.Key, new List<PurchasedItem>(kvp.Value));
|
|
}
|
|
|
|
foreach (var storeId in purchasedItems.Keys)
|
|
{
|
|
DebugConsole.Log($"Purchased items ({storeId}):\n");
|
|
if (prevPurchasedItems.TryGetValue(storeId, out var alreadyPurchased))
|
|
{
|
|
var delivered = alreadyPurchased.Where(it => it.Delivered);
|
|
var notDelivered = alreadyPurchased.Where(it => !it.Delivered);
|
|
if (delivered.Any())
|
|
{
|
|
DebugConsole.Log($" Already delivered:\n" + string.Concat(delivered.Select(it => $" - {it.ItemPrefab.Name} (x{it.Quantity})")));
|
|
}
|
|
if (notDelivered.Any())
|
|
{
|
|
DebugConsole.Log($" Already purchased:\n" + string.Concat(notDelivered.Where(it => !it.Delivered).Select(it => $" - {it.ItemPrefab.Name} (x{it.Quantity})")));
|
|
}
|
|
}
|
|
DebugConsole.Log($" New purchases:");
|
|
foreach (var purchasedItem in purchasedItems[storeId])
|
|
{
|
|
if (purchasedItem.Delivered) { continue; }
|
|
int quantity = purchasedItem.Quantity;
|
|
if (alreadyPurchased != null)
|
|
{
|
|
quantity -= alreadyPurchased.Where(it => it.DeliverImmediately == purchasedItem.DeliverImmediately && it.ItemPrefab == purchasedItem.ItemPrefab).Sum(it => it.Quantity);
|
|
}
|
|
if (quantity > 0)
|
|
{
|
|
DebugConsole.Log($" - {purchasedItem.ItemPrefab.Name} (x{quantity})");
|
|
}
|
|
}
|
|
}
|
|
foreach (var storeId in soldItems.Keys)
|
|
{
|
|
DebugConsole.Log($"Sold items:\n" + string.Concat(soldItems[storeId].Select(it => $" - {it.ItemPrefab.Name}")));
|
|
}
|
|
foreach (var kvp in purchasedItems)
|
|
{
|
|
var storeId = kvp.Key;
|
|
var purchasedItemList = kvp.Value;
|
|
foreach (var purchasedItem in purchasedItemList)
|
|
{
|
|
int desiredQuantity = purchasedItem.Quantity;
|
|
if (prevPurchasedItems.TryGetValue(storeId, out var alreadyPurchasedList) &&
|
|
alreadyPurchasedList.FirstOrDefault(p => p.ItemPrefab == purchasedItem.ItemPrefab && p.DeliverImmediately == purchasedItem.DeliverImmediately) is { } alreadyPurchased)
|
|
{
|
|
desiredQuantity -= alreadyPurchased.Quantity;
|
|
}
|
|
int availableQuantity = map.CurrentLocation.Stores[storeId].Stock.Find(s => s.ItemPrefab == purchasedItem.ItemPrefab)?.Quantity ?? 0;
|
|
purchasedItem.Quantity = Math.Min(desiredQuantity, availableQuantity);
|
|
}
|
|
CargoManager.PurchaseItems(storeId, purchasedItemList, removeFromCrate: false, client: sender);
|
|
}
|
|
|
|
foreach (var (storeIdentifier, items) in CargoManager.PurchasedItems)
|
|
{
|
|
if (!prevPurchasedItems.ContainsKey(storeIdentifier))
|
|
{
|
|
CargoManager.LogNewItemPurchases(storeIdentifier, items, sender);
|
|
continue;
|
|
}
|
|
|
|
List<PurchasedItem> newItems = new List<PurchasedItem>();
|
|
List<PurchasedItem> prevItems = prevPurchasedItems[storeIdentifier];
|
|
|
|
foreach (PurchasedItem item in items)
|
|
{
|
|
PurchasedItem matching = prevItems.FirstOrDefault(ppi => ppi.ItemPrefab == item.ItemPrefab);
|
|
if (matching is null)
|
|
{
|
|
newItems.Add(item);
|
|
continue;
|
|
}
|
|
if (matching.Quantity < item.Quantity)
|
|
{
|
|
newItems.Add(new PurchasedItem(item.ItemPrefab, item.Quantity - matching.Quantity, sender));
|
|
}
|
|
}
|
|
|
|
if (newItems.Any())
|
|
{
|
|
CargoManager.LogNewItemPurchases(storeIdentifier, newItems, sender);
|
|
}
|
|
}
|
|
|
|
bool allowedToSellSubItems = AllowedToManageCampaign(sender, ClientPermissions.SellSubItems);
|
|
if (allowedToSellSubItems)
|
|
{
|
|
var prevSubSellCrateItems = new Dictionary<Identifier, List<PurchasedItem>>(CargoManager.ItemsInSellFromSubCrate);
|
|
foreach (var store in prevSubSellCrateItems)
|
|
{
|
|
foreach (var item in store.Value.ToList())
|
|
{
|
|
CargoManager.ModifyItemQuantityInSubSellCrate(store.Key, item.ItemPrefab, -item.Quantity, sender);
|
|
}
|
|
}
|
|
foreach (var store in subSellCrateItems)
|
|
{
|
|
foreach (var item in store.Value.ToList())
|
|
{
|
|
CargoManager.ModifyItemQuantityInSubSellCrate(store.Key, item.ItemPrefab, item.Quantity, sender);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool allowedToSellInventoryItems = AllowedToManageCampaign(sender, ClientPermissions.SellInventoryItems);
|
|
if (allowedToSellInventoryItems && allowedToSellSubItems)
|
|
{
|
|
// for some reason CargoManager.SoldItem is never cleared by the server, I've added a check to SellItems that ignores all
|
|
// sold items that are removed so they should be discarded on the next message
|
|
var prevSoldItems = new Dictionary<Identifier, List<SoldItem>>(CargoManager.SoldItems);
|
|
foreach (var store in prevSoldItems)
|
|
{
|
|
CargoManager.BuyBackSoldItems(store.Key, store.Value.ToList(), sender);
|
|
}
|
|
foreach (var store in soldItems)
|
|
{
|
|
CargoManager.SellItems(store.Key, store.Value.ToList(), sender);
|
|
}
|
|
}
|
|
else if (allowedToSellInventoryItems || allowedToSellSubItems)
|
|
{
|
|
var prevSoldItems = new Dictionary<Identifier, List<SoldItem>>(CargoManager.SoldItems);
|
|
foreach (var store in prevSoldItems)
|
|
{
|
|
store.Value.RemoveAll(predicate);
|
|
CargoManager.BuyBackSoldItems(store.Key, store.Value.ToList(), sender);
|
|
}
|
|
foreach (var store in soldItems)
|
|
{
|
|
store.Value.RemoveAll(predicate);
|
|
}
|
|
foreach (var store in soldItems)
|
|
{
|
|
CargoManager.SellItems(store.Key, store.Value.ToList(), sender);
|
|
}
|
|
bool predicate(SoldItem i) => allowedToSellInventoryItems != (i.Origin == SoldItem.SellOrigin.Character);
|
|
}
|
|
}
|
|
|
|
if ((purchasedUpgrades.Any() || purchasedItemSwaps.Any()) &&
|
|
HasCampaignInteractionAvailable(sender, InteractionType.Upgrade))
|
|
{
|
|
var characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
|
foreach (var (prefab, category, _) in purchasedUpgrades)
|
|
{
|
|
UpgradeManager.TryPurchaseUpgrade(prefab, category, client: sender);
|
|
// unstable logging
|
|
int price = prefab.Price.GetBuyPrice(prefab, UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation, characterList);
|
|
int level = UpgradeManager.GetUpgradeLevel(prefab, category);
|
|
GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage);
|
|
}
|
|
foreach (var purchasedItemSwap in purchasedItemSwaps)
|
|
{
|
|
if (purchasedItemSwap.ItemToInstall == null)
|
|
{
|
|
UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove, client: sender);
|
|
}
|
|
else
|
|
{
|
|
UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, client: sender);
|
|
}
|
|
}
|
|
foreach (Item item in Item.ItemList)
|
|
{
|
|
if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item))
|
|
{
|
|
UpgradeManager.CancelItemSwap(item);
|
|
item.PendingItemSwap = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool HasCampaignInteractionAvailable(Client sender, InteractionType interactionType)
|
|
{
|
|
if (sender.Character == null || sender.Character.IsIncapacitated) { return false; }
|
|
if (GameMain.Server?.ServerSettings is { AllowRemoteCampaignInteractions: true }) { return true; }
|
|
foreach (var otherCharacter in Character.CharacterList)
|
|
{
|
|
if (otherCharacter.CampaignInteractionType != interactionType) { continue; }
|
|
//larger-than default maximum distance to give legit clients some leeway if their position is a bit off
|
|
if (sender.Character.CanInteractWith(otherCharacter, maxDist: 250.0f))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void ServerReadMoney(IReadMessage msg, Client sender)
|
|
{
|
|
NetWalletTransfer transfer = INetSerializableStruct.Read<NetWalletTransfer>(msg);
|
|
|
|
if (GameMain.Server is null) { return; }
|
|
|
|
if (transfer.Sender.TryUnwrap(out var id))
|
|
{
|
|
if (id != sender.CharacterID && !AllowedToManageWallets(sender)) { return; }
|
|
|
|
Wallet wallet = GetWalletByID(id);
|
|
if (wallet is InvalidWallet) { return; }
|
|
|
|
TransferMoney(wallet);
|
|
}
|
|
else
|
|
{
|
|
if (!AllowedToManageWallets(sender))
|
|
{
|
|
if (transfer.Receiver.TryUnwrap(out var receiverId) && receiverId == sender.CharacterID)
|
|
{
|
|
if (transfer.Amount > GameMain.Server.ServerSettings.MaximumMoneyTransferRequest) { return; }
|
|
GameMain.Server.Voting.StartTransferVote(sender, null, transfer.Amount, sender);
|
|
GameServer.Log($"{sender.Name} started a vote to transfer {transfer.Amount} mk from the bank.", ServerLog.MessageType.Money);
|
|
}
|
|
return;
|
|
}
|
|
|
|
TransferMoney(Bank);
|
|
}
|
|
|
|
void TransferMoney(Wallet from)
|
|
{
|
|
if (!from.TryDeduct(transfer.Amount)) { return; }
|
|
|
|
if (transfer.Receiver.TryUnwrap(out var id))
|
|
{
|
|
Wallet wallet = GetWalletByID(id);
|
|
if (wallet is InvalidWallet) { return; }
|
|
|
|
wallet.Give(transfer.Amount);
|
|
GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {wallet.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money);
|
|
}
|
|
else
|
|
{
|
|
Bank.Give(transfer.Amount);
|
|
GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {Bank.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money);
|
|
}
|
|
}
|
|
|
|
Wallet GetWalletByID(ushort id)
|
|
{
|
|
Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == id);
|
|
return targetCharacter is null ? Wallet.Invalid : targetCharacter.Wallet;
|
|
}
|
|
}
|
|
|
|
public void ServerReadRewardDistribution(IReadMessage msg, Client sender)
|
|
{
|
|
NetWalletSetSalaryUpdate update = INetSerializableStruct.Read<NetWalletSetSalaryUpdate>(msg);
|
|
|
|
if (!AllowedToManageWallets(sender)) { return; }
|
|
|
|
if (update.Target.TryUnwrap(out ushort id))
|
|
{
|
|
Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == id);
|
|
targetCharacter?.Wallet.SetRewardDistribution(update.NewRewardDistribution);
|
|
GameServer.Log($"{sender.Name} changed the salary of {targetCharacter?.Name} to {update.NewRewardDistribution}%.", ServerLog.MessageType.Money);
|
|
return;
|
|
}
|
|
|
|
Bank.SetRewardDistribution(update.NewRewardDistribution);
|
|
GameServer.Log($"{sender.Name} changed the default salary to {update.NewRewardDistribution}%.", ServerLog.MessageType.Money);
|
|
}
|
|
|
|
public void ResetSalaries(Client sender)
|
|
{
|
|
if (!AllowedToManageWallets(sender)) { return; }
|
|
|
|
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Player))
|
|
{
|
|
character.Wallet.SetRewardDistribution(Bank.RewardDistribution);
|
|
}
|
|
}
|
|
|
|
public void ServerReadCrew(IReadMessage msg, Client sender)
|
|
{
|
|
UInt16[] pendingHires = null;
|
|
bool[] pendingToReserveBench = null;
|
|
Dictionary<int, BotStatus> existingBotsClient = null;
|
|
|
|
bool updatePending = msg.ReadBoolean();
|
|
if (updatePending)
|
|
{
|
|
ushort pendingHireLength = msg.ReadUInt16();
|
|
pendingHires = new UInt16[pendingHireLength];
|
|
pendingToReserveBench = new bool[pendingHireLength];
|
|
for (int i = 0; i < pendingHireLength; i++)
|
|
{
|
|
pendingHires[i] = msg.ReadUInt16();
|
|
pendingToReserveBench[i] = msg.ReadBoolean();
|
|
}
|
|
}
|
|
|
|
bool validateHires = msg.ReadBoolean();
|
|
|
|
bool renameCharacter = msg.ReadBoolean();
|
|
UInt16 renamedIdentifier = 0;
|
|
string newName = null;
|
|
bool existingCrewMember = false;
|
|
if (renameCharacter)
|
|
{
|
|
renamedIdentifier = msg.ReadUInt16();
|
|
newName = Client.SanitizeName(msg.ReadString());
|
|
existingCrewMember = msg.ReadBoolean();
|
|
if (!GameMain.Server.IsNameValid(sender, newName, clientRenamingSelf: renamedIdentifier == sender.CharacterInfo?.ID))
|
|
{
|
|
renameCharacter = false;
|
|
}
|
|
}
|
|
|
|
bool fireCharacter = msg.ReadBoolean();
|
|
int firedIdentifier = -1;
|
|
if (fireCharacter) { firedIdentifier = msg.ReadUInt16(); }
|
|
|
|
Location location = map?.CurrentLocation;
|
|
CharacterInfo firedCharacter = null;
|
|
(ushort id, string newName) appliedRename = (Entity.NullEntityID, string.Empty);
|
|
|
|
if (location != null)
|
|
{
|
|
if (fireCharacter && AllowedToManageCampaign(sender, ClientPermissions.ManageHires) && HasCampaignInteractionAvailable(sender, InteractionType.Crew))
|
|
{
|
|
firedCharacter = CrewManager.GetCharacterInfos(includeReserveBench: true).FirstOrDefault(info => info.ID == firedIdentifier);
|
|
if (firedCharacter != null && (firedCharacter.Character?.IsBot ?? true))
|
|
{
|
|
CrewManager.FireCharacter(firedCharacter);
|
|
}
|
|
else
|
|
{
|
|
DebugConsole.ThrowError($"Tried to fire an invalid character ({firedIdentifier})");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
GameServer.Log($"{sender.Name} attempted to fire a character without having access to an appropriate NPC.", ServerLog.MessageType.Error);
|
|
}
|
|
|
|
if (renameCharacter)
|
|
{
|
|
CharacterInfo characterInfo = null;
|
|
if (AllowedToManageCampaign(sender, ClientPermissions.ManageHires))
|
|
{
|
|
if (existingCrewMember && CrewManager != null)
|
|
{
|
|
characterInfo = CrewManager.GetCharacterInfos(includeReserveBench: true).FirstOrDefault(info => info.ID == renamedIdentifier);
|
|
}
|
|
else if (!existingCrewMember && location.HireManager != null)
|
|
{
|
|
characterInfo = location.HireManager.AvailableCharacters.FirstOrDefault(info => info.ID == renamedIdentifier);
|
|
}
|
|
if (characterInfo != null && characterInfo != sender.CharacterInfo && !HasCampaignInteractionAvailable(sender, InteractionType.Crew))
|
|
{
|
|
GameServer.Log($"{sender.Name} attempted to rename a character without having access to an appropriate NPC.", ServerLog.MessageType.Error);
|
|
characterInfo = null;
|
|
}
|
|
}
|
|
else if (characterInfo == null && renamedIdentifier == sender.CharacterInfo?.ID)
|
|
{
|
|
characterInfo = sender.CharacterInfo;
|
|
}
|
|
if (characterInfo != null &&
|
|
(characterInfo.Character == null || characterInfo.Character is { IsBot: true } || (characterInfo.RenamingEnabled && characterInfo == sender.CharacterInfo)))
|
|
{
|
|
GameServer.Log($"{sender.Name} renamed the character \"{characterInfo.Name}\" as \"{newName}\".", ServerLog.MessageType.ServerMessage);
|
|
if (existingCrewMember)
|
|
{
|
|
CrewManager.RenameCharacter(characterInfo, newName);
|
|
if (characterInfo == sender.CharacterInfo)
|
|
{
|
|
//renaming is only allowed once
|
|
characterInfo.RenamingEnabled = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
location.HireManager.RenameCharacter(characterInfo, newName);
|
|
}
|
|
appliedRename = (characterInfo.ID, newName);
|
|
}
|
|
else
|
|
{
|
|
string errorMsg = $"Tried to rename an invalid character ({renamedIdentifier}, {characterInfo?.Name ?? "null"})";
|
|
DebugConsole.ThrowError(errorMsg);
|
|
GameMain.Server?.SendConsoleMessage(errorMsg, sender, Color.Red);
|
|
}
|
|
}
|
|
|
|
if (location.HireManager != null && HasCampaignInteractionAvailable(sender, InteractionType.Crew))
|
|
{
|
|
if (validateHires)
|
|
{
|
|
foreach (CharacterInfo hireInfo in location.HireManager.PendingHires)
|
|
{
|
|
TryHireCharacter(location, hireInfo, client: sender);
|
|
}
|
|
}
|
|
|
|
if (updatePending)
|
|
{
|
|
List<CharacterInfo> pendingHireInfos = new List<CharacterInfo>();
|
|
int i = 0;
|
|
foreach (UInt16 identifier in pendingHires)
|
|
{
|
|
CharacterInfo match = location.GetHireableCharacters().FirstOrDefault(info => info.ID == identifier);
|
|
if (match == null)
|
|
{
|
|
DebugConsole.ThrowError($"Tried to add a character that doesn't exist ({identifier}) to pending hires");
|
|
continue;
|
|
}
|
|
|
|
match.BotStatus = pendingToReserveBench[i++] ? BotStatus.PendingHireToReserveBench : BotStatus.PendingHireToActiveService;
|
|
if (match.BotStatus == BotStatus.PendingHireToActiveService)
|
|
{
|
|
//can't add more bots to active service is max has been reached
|
|
if (pendingHireInfos.Count(ci => ci.BotStatus == BotStatus.PendingHireToActiveService) + CrewManager.GetCharacterInfos().Count() >= CrewManager.MaxCrewSize) { continue; }
|
|
}
|
|
|
|
pendingHireInfos.Add(match);
|
|
}
|
|
location.HireManager.PendingHires = pendingHireInfos;
|
|
}
|
|
|
|
location.HireManager.AvailableCharacters.ForEachMod(info =>
|
|
{
|
|
if (!location.HireManager.PendingHires.Contains(info))
|
|
{
|
|
location.HireManager.RenameCharacter(info, info.OriginalName);
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
GameServer.Log($"{sender.Name} attempted to hire characters without having access to an appropriate NPC.", ServerLog.MessageType.Error);
|
|
}
|
|
}
|
|
|
|
// bounce back
|
|
if (renameCharacter && existingCrewMember)
|
|
{
|
|
SendCrewState(appliedRename, firedCharacter);
|
|
}
|
|
else
|
|
{
|
|
SendCrewState(firedCharacter: firedCharacter);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies the clients of the current bot situation like syncing pending and available hires
|
|
/// </summary>
|
|
/// <param name="hiredCharacters">Inform the clients that these characters have been hired.</param>
|
|
/// <param name="firedCharacter">Inform the clients that this character has been fired.</param>
|
|
/// <remarks>
|
|
/// It might be obsolete to sync available hires. I found that the available hires are always the same between
|
|
/// the client and the server when there's only one person on the server but when a second person joins both of
|
|
/// their available hires are different from the server.
|
|
/// </remarks>
|
|
public void SendCrewState((ushort id, string newName) renamedCrewMember = default, CharacterInfo firedCharacter = null, bool createNotification = true)
|
|
{
|
|
List<CharacterInfo> availableHires = new List<CharacterInfo>();
|
|
List<CharacterInfo> pendingHires = new List<CharacterInfo>();
|
|
|
|
if (map.CurrentLocation != null && map.CurrentLocation.Type.HasHireableCharacters)
|
|
{
|
|
availableHires = map.CurrentLocation.GetHireableCharacters().ToList();
|
|
pendingHires = map.CurrentLocation?.HireManager.PendingHires;
|
|
}
|
|
|
|
foreach (Client client in GameMain.Server.ConnectedClients)
|
|
{
|
|
IWriteMessage msg = new WriteOnlyMessage();
|
|
msg.WriteByte((byte)ServerPacketHeader.CREW);
|
|
|
|
msg.WriteBoolean(createNotification);
|
|
|
|
msg.WriteUInt16((ushort)availableHires.Count);
|
|
foreach (CharacterInfo hire in availableHires)
|
|
{
|
|
hire.ServerWrite(msg);
|
|
msg.WriteInt32(hire.Salary);
|
|
}
|
|
|
|
msg.WriteUInt16((ushort)pendingHires.Count);
|
|
foreach (CharacterInfo pendingHire in pendingHires)
|
|
{
|
|
msg.WriteUInt16(pendingHire.ID);
|
|
msg.WriteBoolean(pendingHire.BotStatus == BotStatus.PendingHireToReserveBench);
|
|
}
|
|
|
|
var crewManager = CrewManager.GetCharacterInfos();
|
|
msg.WriteUInt16((ushort)crewManager.Count());
|
|
foreach (CharacterInfo info in crewManager)
|
|
{
|
|
info.ServerWrite(msg);
|
|
}
|
|
|
|
var reserveBench = CrewManager.GetReserveBenchInfos();
|
|
msg.WriteUInt16((ushort)reserveBench.Count());
|
|
foreach (CharacterInfo info in reserveBench)
|
|
{
|
|
info.ServerWrite(msg);
|
|
}
|
|
|
|
bool validRenaming = renamedCrewMember.id > 0 && !string.IsNullOrEmpty(renamedCrewMember.newName);
|
|
msg.WriteBoolean(validRenaming);
|
|
if (validRenaming)
|
|
{
|
|
msg.WriteUInt16(renamedCrewMember.id);
|
|
msg.WriteString(renamedCrewMember.newName);
|
|
}
|
|
|
|
msg.WriteBoolean(firedCharacter != null);
|
|
if (firedCharacter != null) { msg.WriteUInt16(firedCharacter.ID); }
|
|
|
|
GameMain.Server.ServerPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
|
|
}
|
|
}
|
|
|
|
public override bool TryPurchase(Client client, int price)
|
|
{
|
|
//disconnected clients can never purchase anything
|
|
//(can happen e.g. if someone starts a vote to buy something and then disconnects)
|
|
if (client != null && !GameMain.Server.ConnectedClients.Contains(client)) { return false; }
|
|
|
|
if (price == 0) { return true; }
|
|
|
|
Wallet wallet = GetWallet(client);
|
|
if (!AllowedToManageWallets(client))
|
|
{
|
|
return wallet.TryDeduct(price);
|
|
}
|
|
|
|
int balance = wallet.Balance;
|
|
|
|
if (balance >= price)
|
|
{
|
|
return wallet.TryDeduct(price);
|
|
}
|
|
|
|
if (balance + Bank.Balance >= price)
|
|
{
|
|
int remainder = price - balance;
|
|
if (balance > 0) { wallet.Deduct(balance); }
|
|
Bank.Deduct(remainder);
|
|
return true ;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public override int GetBalance(Client client = null)
|
|
{
|
|
if (client is null) { return 0; }
|
|
|
|
Wallet wallet = GetWallet(client);
|
|
if (!AllowedToManageWallets(client))
|
|
{
|
|
return wallet.Balance;
|
|
}
|
|
|
|
return wallet.Balance + Bank.Balance;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serializes the campaign and character data to XML.
|
|
/// </summary>
|
|
/// <param name="element">Game session element to save the campaign data to.</param>
|
|
/// <param name="isSavingOnLoading">
|
|
/// Whether the save is being done during loading to ensure the campaign ID matches the one in the save file.
|
|
/// Used to work around some quirks with the backup save system.
|
|
/// See: <see cref="SaveUtil.SaveGame(CampaignDataPath,bool)"/>
|
|
/// </param>
|
|
public override void Save(XElement element, bool isSavingOnLoading)
|
|
{
|
|
element.Add(new XAttribute("campaignid", CampaignID));
|
|
XElement modeElement = new XElement("MultiPlayerCampaign",
|
|
new XAttribute("purchasedlostshuttles", PurchasedLostShuttlesInLatestSave),
|
|
new XAttribute("purchasedhullrepairs", PurchasedHullRepairsInLatestSave),
|
|
new XAttribute("purchaseditemrepairs", PurchasedItemRepairsInLatestSave),
|
|
new XAttribute("cheatsenabled", CheatsEnabled));
|
|
|
|
DebugConsole.NewMessage("Saved PurchasedHullRepairs: "+ PurchasedHullRepairs+" (in last save "+PurchasedHullRepairsInLatestSave+")", Color.Magenta);
|
|
|
|
modeElement.Add(Settings.Save());
|
|
modeElement.Add(SaveStats());
|
|
if (GameMain.Server?.TraitorManager is TraitorManager traitorManager)
|
|
{
|
|
modeElement.Add(traitorManager.Save());
|
|
}
|
|
modeElement.Add(Bank.Save());
|
|
|
|
if (GameMain.GameSession?.EventManager != null)
|
|
{
|
|
modeElement.Add(GameMain.GameSession?.EventManager.Save());
|
|
}
|
|
|
|
foreach ((CharacterTeamType team, Identifier unlockedRecipe) in GameMain.GameSession.UnlockedRecipes)
|
|
{
|
|
modeElement.Add(
|
|
new XElement("unlockedrecipe",
|
|
new XAttribute("identifier", unlockedRecipe),
|
|
new XAttribute("team", team)));
|
|
}
|
|
|
|
CampaignMetadata?.Save(modeElement);
|
|
Map.Save(modeElement);
|
|
CargoManager?.SavePurchasedItems(modeElement);
|
|
UpgradeManager?.Save(modeElement);
|
|
|
|
if (petsElement != null)
|
|
{
|
|
modeElement.Add(petsElement);
|
|
}
|
|
|
|
// save bots
|
|
var crewManagerElement = CrewManager.SaveMultiplayer(modeElement);
|
|
if (ActiveOrdersElement != null)
|
|
{
|
|
crewManagerElement.Add(ActiveOrdersElement);
|
|
}
|
|
|
|
XElement savedExperiencePointsElement = new XElement("SavedExperiencePoints");
|
|
foreach (var savedExperiencePoint in savedExperiencePoints)
|
|
{
|
|
savedExperiencePointsElement.Add(new XElement("Point",
|
|
new XAttribute("accountid", savedExperiencePoint.AccountId.TryUnwrap(out var accountId) ? accountId.StringRepresentation : ""),
|
|
new XAttribute("address", savedExperiencePoint.Address.StringRepresentation),
|
|
new XAttribute("points", savedExperiencePoint.ExperiencePoints)));
|
|
}
|
|
|
|
element.Add(modeElement);
|
|
|
|
// save character data to a separate file
|
|
|
|
// When loading a campaign in multiplayer, we save the campaign to ensure the campaign ID that gets assigned
|
|
// matches the one in the save file, this is a problem with the backup save system since this causes the
|
|
// character data to save too, and we don't want to overwrite the main save file's character data.
|
|
// So we instead save over the load path in this case, which in backup saves is the backup file
|
|
// which we don't mind getting overriden since the data should be the same
|
|
string characterDataPath = isSavingOnLoading
|
|
? GetCharacterDataPathForLoading()
|
|
: GetCharacterDataPathForSaving();
|
|
XDocument characterDataDoc = new XDocument(new XElement("CharacterData"));
|
|
foreach (CharacterCampaignData cd in characterData)
|
|
{
|
|
characterDataDoc.Root.Add(cd.Save());
|
|
}
|
|
try
|
|
{
|
|
SaveUtil.DeleteIfExists(characterDataPath);
|
|
characterDataDoc.SaveSafe(characterDataPath);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
DebugConsole.ThrowError("Saving multiplayer campaign characters to \"" + characterDataPath + "\" failed!", e);
|
|
}
|
|
|
|
lastSaveID++;
|
|
DebugConsole.Log("Campaign saved, save ID " + lastSaveID);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load the current character save file and add/replace a single character's data with a new version immediately.
|
|
/// </summary>
|
|
/// <param name="newData">New character to insert. If it matches one existing in the save, that will get replaced.</param>
|
|
/// <param name="skipBackup">By default, replaced characters will be temporarily backed up, but that might be unwanted
|
|
/// eg. when using this method to save a character itself restored from the backup.</param>
|
|
public void SaveSingleCharacter(CharacterCampaignData newData, bool skipBackup = false)
|
|
{
|
|
string characterDataPath = GetCharacterDataPathForSaving();
|
|
if (!File.Exists(characterDataPath))
|
|
{
|
|
DebugConsole.ThrowError($"Failed to load the character data for the campaign. Could not find the file \"{characterDataPath}\".");
|
|
}
|
|
else
|
|
{
|
|
var loadedCharacterData = XMLExtensions.TryLoadXml(characterDataPath);
|
|
if (loadedCharacterData?.Root == null) { return; }
|
|
var oldData = loadedCharacterData.Root.Elements()
|
|
.FirstOrDefault(subElement => new CharacterCampaignData(subElement).IsDuplicate(newData));
|
|
|
|
if (oldData != null)
|
|
{
|
|
if (!skipBackup)
|
|
{
|
|
replacedCharacterDataBackup.Add(new CharacterCampaignData(oldData));
|
|
}
|
|
oldData.Remove();
|
|
}
|
|
loadedCharacterData.Root.Add(newData.Save());
|
|
|
|
try
|
|
{
|
|
loadedCharacterData.SaveSafe(characterDataPath);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
DebugConsole.ThrowError("Saving multiplayer campaign characters to \"" + characterDataPath + "\" failed!", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
public CharacterCampaignData RestoreSingleCharacterFromBackup(Client client)
|
|
{
|
|
if (replacedCharacterDataBackup.Find(cd => cd.MatchesClient(client)) is CharacterCampaignData characterToRestore)
|
|
{
|
|
replacedCharacterDataBackup.Remove(characterToRestore);
|
|
return characterToRestore;
|
|
}
|
|
|
|
return default;
|
|
}
|
|
}
|
|
}
|