using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; namespace Barotrauma { partial class MultiPlayerCampaign : CampaignMode { public bool SuppressStateSending = false; public override bool Paused { get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition"); } } private UInt16 pendingSaveID = 1; public UInt16 PendingSaveID { get { return pendingSaveID; } set { pendingSaveID = value; //pending save ID 0 means "no save received yet" //save IDs are always above 0, so we should never be waiting for 0 if (pendingSaveID == 0) { pendingSaveID++; } } } public Wallet PersonalWallet => Character.Controlled?.Wallet ?? Wallet.Invalid; public override Wallet Wallet => GetWallet(); public override int GetBalance(Client client = null) { if (!AllowedToManageWallets()) { return PersonalWallet.Balance; } return PersonalWallet.Balance + Bank.Balance; } public override Wallet GetWallet(Client client = null) { return PersonalWallet; } public static void StartCampaignSetup(List saveFiles) { var parent = GameMain.NetLobbyScreen.CampaignSetupFrame; parent.ClearChildren(); parent.Visible = true; GameMain.NetLobbyScreen.HighlightMode( GameMain.NetLobbyScreen.ModeList.Content.GetChildIndex(GameMain.NetLobbyScreen.ModeList.Content.GetChildByUserData(GameModePreset.MultiPlayerCampaign))); var layout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center)) { Stretch = true }; var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), layout.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.1f) }, isHorizontal: true) { Stretch = true, RelativeSpacing = 0.02f }; var campaignContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), layout.RectTransform, Anchor.BottomLeft), style: "GUIFrameListBox") { CanBeFocused = false }; var newCampaignContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.95f), campaignContainer.RectTransform, Anchor.Center), style: null); var loadCampaignContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.95f), campaignContainer.RectTransform, Anchor.Center), style: null); GameMain.NetLobbyScreen.CampaignSetupUI = new MultiPlayerCampaignSetupUI(newCampaignContainer, loadCampaignContainer, saveFiles); var newCampaignButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform), TextManager.Get("NewCampaign"), style: "GUITabButton") { Selected = true }; var loadCampaignButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.00f), buttonContainer.RectTransform), TextManager.Get("LoadCampaign"), style: "GUITabButton"); newCampaignButton.OnClicked = (btn, obj) => { newCampaignButton.Selected = true; loadCampaignButton.Selected = false; newCampaignContainer.Visible = true; loadCampaignContainer.Visible = false; GameMain.NetLobbyScreen?.RefreshStartButtonVisibility(); return true; }; loadCampaignButton.OnClicked = (btn, obj) => { newCampaignButton.Selected = false; loadCampaignButton.Selected = true; newCampaignContainer.Visible = false; loadCampaignContainer.Visible = true; GameMain.NetLobbyScreen?.RefreshStartButtonVisibility(); return true; }; loadCampaignContainer.Visible = false; GUITextBlock.AutoScaleAndNormalize(newCampaignButton.TextBlock, loadCampaignButton.TextBlock); GameMain.NetLobbyScreen.CampaignSetupUI.StartNewGame = GameMain.Client.SetupNewCampaign; GameMain.NetLobbyScreen.CampaignSetupUI.LoadGame = GameMain.Client.SetupLoadCampaign; } partial void InitProjSpecific() { CreateButtons(); } public override void HUDScaleChanged() { CreateButtons(); } private void CreateButtons() { endRoundButton = CreateEndRoundButton(); endRoundButton.OnClicked = (btn, userdata) => { TryEndRoundWithFuelCheck( onConfirm: () => GameMain.Client.RequestStartRound(), onReturnToMapScreen: () => { ShowCampaignUI = true; if (CampaignUI == null) { InitCampaignUI(); } CampaignUI.SelectTab(InteractionType.Map); }); return true; }; int readyButtonWidth = (int)(GUI.Scale * 50 * (GUI.IsUltrawide ? 3.0f : 1.0f)); int readyButtonHeight = (int)(GUI.Scale * 40); int readyButtonCenter = readyButtonHeight / 2, screenMiddle = GameMain.GraphicsWidth / 2; ReadyCheckButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle + (endRoundButton.Rect.Width / 2) + GUI.IntScale(16), HUDLayoutSettings.ButtonAreaTop.Center.Y - readyButtonCenter, readyButtonWidth, readyButtonHeight), GUI.Canvas), style: "RepairBuyButton") { ToolTip = TextManager.Get("ReadyCheck.Tooltip"), OnClicked = delegate { if (CrewManager != null && CrewManager.ActiveReadyCheck == null) { ReadyCheck.CreateReadyCheck(); } return true; }, UserData = "ReadyCheckButton" }; } public void InitCampaignUI() { campaignUIContainer = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black); CampaignUI = new CampaignUI(this, campaignUIContainer) { StartRound = () => { GameMain.Client.RequestStartRound(); } }; } public override void Start() { base.Start(); CoroutineManager.StartCoroutine(DoInitialCameraTransition(), "MultiplayerCampaign.DoInitialCameraTransition"); } protected override void LoadInitialLevel() { //clients should never call this throw new InvalidOperationException(""); } private IEnumerable DoInitialCameraTransition() { while (GameMain.Instance.LoadingScreenOpen) { yield return CoroutineStatus.Running; } if (GameMain.Client == null) { yield return CoroutineStatus.Success; } if (GameMain.Client.LateCampaignJoin) { GameMain.Client.LateCampaignJoin = false; yield return CoroutineStatus.Success; } Character prevControlled = Character.Controlled; GUI.DisableHUD = true; if (IsFirstRound) { if (SlideshowPrefab.Prefabs.TryGet("campaignstart".ToIdentifier(), out var slideshow)) { SlideshowPlayer = new SlideshowPlayer(GUICanvas.Instance, slideshow); } Character.Controlled = null; prevControlled?.ClearInputs(); overlayColor = Color.LightGray; overlaySprite = Map.CurrentLocation.Type.GetPortrait(Map.CurrentLocation.PortraitId); var outpost = GameMain.GameSession.Level.StartOutpost; var borders = outpost.GetDockedBorders(); borders.Location += outpost.WorldPosition.ToPoint(); GameMain.GameScreen.Cam.Position = new Vector2(borders.X + borders.Width / 2, borders.Y - borders.Height / 2); float startZoom = 0.8f / ((float)Math.Max(borders.Width, borders.Height) / (float)GameMain.GameScreen.Cam.Resolution.X); GameMain.GameScreen.Cam.Zoom = GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); while (SlideshowPlayer != null && !SlideshowPlayer.LastTextShown) { GUI.PreventPauseMenuToggle = true; yield return CoroutineStatus.Running; } GUI.PreventPauseMenuToggle = false; prevControlled ??= Character.Controlled; GameMain.LightManager.LosAlpha = 0.0f; var transition = new CameraTransition(prevControlled, GameMain.GameScreen.Cam, null, null, fadeOut: false, losFadeIn: true, waitDuration: 1, panDuration: 5, startZoom: startZoom, endZoom: 1.0f) { AllowInterrupt = true, RemoveControlFromCharacter = false }; overlayColor = Color.Transparent; while (transition.Running) { yield return CoroutineStatus.Running; } if (prevControlled != null) { Character.Controlled = prevControlled; } } else { var transition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, null, fadeOut: false, losFadeIn: true, panDuration: 5, startZoom: 0.5f, endZoom: 1.0f) { AllowInterrupt = true, RemoveControlFromCharacter = true }; while (transition.Running) { yield return CoroutineStatus.Running; } } if (prevControlled != null) { prevControlled.SelectedItem = prevControlled.SelectedSecondaryItem = null; if (prevControlled.AIController != null) { prevControlled.AIController.Enabled = true; } } GUI.DisableHUD = false; yield return CoroutineStatus.Success; } protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror) { yield return CoroutineStatus.Success; } private IEnumerable DoLevelTransition() { SoundPlayer.OverrideMusicType = (CrewManager.GetCharacters().Any(c => !c.IsDead) ? "endround" : "crewdead").ToIdentifier(); SoundPlayer.OverrideMusicDuration = 18.0f; Level prevLevel = Level.Loaded; bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); CrewDead = false; var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton; if (continueButton != null) { continueButton.Visible = false; } Character.Controlled = null; yield return new WaitForSeconds(0.1f); GameMain.Client.EndCinematic?.Stop(); var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, Alignment.Center, fadeOut: false, panDuration: EndTransitionDuration); GameMain.Client.EndCinematic = endTransition; Location portraitLocation = Map?.SelectedLocation ?? Map?.CurrentLocation ?? Level.Loaded?.StartLocation; if (portraitLocation != null) { overlaySprite = portraitLocation.Type.GetPortrait(portraitLocation.PortraitId); } float fadeOutDuration = endTransition.PanDuration; float t = 0.0f; while (t < fadeOutDuration || endTransition.Running) { t += CoroutineManager.DeltaTime; overlayColor = Color.Lerp(Color.Transparent, Color.White, t / fadeOutDuration); yield return CoroutineStatus.Running; } overlayColor = Color.White; yield return CoroutineStatus.Running; //-------------------------------------- //wait for the new level to be loaded DateTime timeOut = DateTime.Now + GameClient.LevelTransitionTimeOut; while (Level.Loaded == prevLevel || Level.Loaded == null) { if (DateTime.Now > timeOut || Screen.Selected != GameMain.GameScreen) { break; } yield return CoroutineStatus.Running; } endTransition.Stop(); overlayColor = Color.Transparent; if (DateTime.Now > timeOut) { DebugConsole.ThrowError("Failed to start the round. Timed out while waiting for the level transition to finish."); GameMain.NetLobbyScreen.Select(); } if (Screen.Selected is not RoundSummaryScreen) { if (continueButton != null) { continueButton.Visible = true; } } GUI.SetSavingIndicatorState(false); yield return CoroutineStatus.Success; } public override void Update(float deltaTime) { if (CoroutineManager.IsCoroutineRunning("LevelTransition") || Level.Loaded == null) { return; } if (ShowCampaignUI || ForceMapUI) { if (CampaignUI == null) { InitCampaignUI(); } Character.DisableControls = true; } base.Update(deltaTime); SlideshowPlayer?.UpdateManually(deltaTime); if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) { ShowCampaignUI = false; if (GUIMessageBox.VisibleBox?.UserData is RoundSummary roundSummary && roundSummary.ContinueButton != null && roundSummary.ContinueButton.Visible) { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); } } if (!GUI.DisableHUD && !GUI.DisableUpperHUD) { endRoundButton.UpdateManually(deltaTime); ReadyCheckButton?.UpdateManually(deltaTime); if (CoroutineManager.IsCoroutineRunning("LevelTransition") || ForceMapUI) { return; } } if (Level.Loaded.Type == LevelData.LevelType.Outpost) { if (wasDocked) { var connectedSubs = Submarine.MainSub.GetConnectedSubs(); bool isDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); if (!isDocked) { //undocked from outpost, need to choose a destination ForceMapUI = true; if (CampaignUI == null) { InitCampaignUI(); } CampaignUI.SelectTab(InteractionType.Map); } } //end biome is handled by the server (automatic transition without a map screen when the end of the level is reached) else if (!Level.Loaded.IsEndBiome) { //wasn't initially docked (sub doesn't have a docking port?) // -> choose a destination when the sub is far enough from the start outpost if (!Submarine.MainSub.AtStartExit) { ForceMapUI = true; if (CampaignUI == null) { InitCampaignUI(); } CampaignUI.SelectTab(InteractionType.Map); } } if (CampaignUI == null) { InitCampaignUI(); } } else { var transitionType = GetAvailableTransition(out _, out _); if (transitionType == TransitionType.None && CampaignUI?.SelectedTab == InteractionType.Map) { ShowCampaignUI = false; } HintManager.OnAvailableTransition(transitionType); } } public override void UpdateWhilePaused(float deltaTime) { SlideshowPlayer?.UpdateManually(deltaTime); } public override void End(TransitionType transitionType = TransitionType.None) { base.End(transitionType); ForceMapUI = ShowCampaignUI = false; SlideshowPlayer?.Finish(); // remove all event dialogue boxes GUIMessageBox.MessageBoxes.ForEachMod(mb => { if (mb is GUIMessageBox msgBox) { if (ReadyCheck.IsReadyCheck(mb) || mb.UserData is Pair pair && pair.First.Equals("conversationaction", StringComparison.OrdinalIgnoreCase)) { msgBox.Close(); } } }); if (transitionType == TransitionType.End) { EndCampaign(); } else { IsFirstRound = false; CoroutineManager.StartCoroutine(DoLevelTransition(), "LevelTransition"); } } protected override void EndCampaignProjSpecific() { if (GUIMessageBox.VisibleBox?.UserData is RoundSummary roundSummary) { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); } GameMain.CampaignEndScreen.Select(); GUI.DisableHUD = false; GameMain.CampaignEndScreen.OnFinished = () => { GameMain.NetLobbyScreen.Select(); if (GameMain.NetLobbyScreen.QuitCampaignButton != null) { GameMain.NetLobbyScreen.QuitCampaignButton.Enabled = false; } }; } public void ClientWrite(IWriteMessage msg) { System.Diagnostics.Debug.Assert(map.Locations.Count < UInt16.MaxValue); msg.WriteUInt16(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex); msg.WriteUInt16(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex); var selectedMissionIndices = map.GetSelectedMissionIndices(); msg.WriteByte((byte)selectedMissionIndices.Count()); foreach (int selectedMissionIndex in selectedMissionIndices) { msg.WriteByte((byte)selectedMissionIndex); } msg.WriteBoolean(PurchasedHullRepairs); msg.WriteBoolean(PurchasedItemRepairs); msg.WriteBoolean(PurchasedLostShuttles); WriteItems(msg, CargoManager.ItemsInBuyCrate); WriteItems(msg, CargoManager.ItemsInSellFromSubCrate); WriteItems(msg, CargoManager.PurchasedItems); WriteItems(msg, CargoManager.SoldItems); msg.WriteUInt16((ushort)UpgradeManager.PurchasedUpgrades.Count); foreach (var (prefab, category, level) in UpgradeManager.PurchasedUpgrades) { 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); } } //static because we may need to instantiate the campaign if it hasn't been done yet public static void ClientRead(IReadMessage msg) { NetFlags requiredFlags = (NetFlags)msg.ReadUInt16(); bool isFirstRound = msg.ReadBoolean(); byte campaignID = msg.ReadByte(); byte roundId = msg.ReadByte(); UInt16 saveID = msg.ReadUInt16(); string mapSeed = msg.ReadString(); bool refreshCampaignUI = false; if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaignID != campaign.CampaignID) { string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer); GameMain.GameSession = new GameSession(null, Option.None, CampaignDataPath.CreateRegular(savePath), GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty, mapSeed); campaign = (MultiPlayerCampaign)GameMain.GameSession.GameMode; campaign.CampaignID = campaignID; GameMain.NetLobbyScreen.ToggleCampaignMode(true); } //server has a newer save file if (NetIdUtils.IdMoreRecent(saveID, campaign.PendingSaveID)) { campaign.PendingSaveID = saveID; } campaign.IsFirstRound = isFirstRound; if (requiredFlags.HasFlag(NetFlags.Misc)) { DebugConsole.Log("Received campaign update (Misc), round id: " + roundId); UInt16 id = msg.ReadUInt16(); bool purchasedHullRepairs = msg.ReadBoolean(); bool purchasedItemRepairs = msg.ReadBoolean(); bool purchasedLostShuttles = msg.ReadBoolean(); if (ShouldApply(NetFlags.Misc, id, requireUpToDateSave: false)) { refreshCampaignUI = campaign.PurchasedHullRepairs != purchasedHullRepairs || campaign.PurchasedItemRepairs != purchasedItemRepairs || campaign.PurchasedLostShuttles != purchasedLostShuttles; campaign.PurchasedHullRepairs = purchasedHullRepairs; campaign.PurchasedItemRepairs = purchasedItemRepairs; campaign.PurchasedLostShuttles = purchasedLostShuttles; } } if (requiredFlags.HasFlag(NetFlags.MapAndMissions)) { DebugConsole.Log("Received campaign update (MapAndMissions), round id: " + roundId); UInt16 id = msg.ReadUInt16(); bool forceMapUI = msg.ReadBoolean(); bool allowDebugTeleport = msg.ReadBoolean(); UInt16 currentLocIndex = msg.ReadUInt16(); UInt16 selectedLocIndex = msg.ReadUInt16(); byte missionCount = msg.ReadByte(); var availableMissions = new List<(Identifier Identifier, byte ConnectionIndex)>(); for (int i = 0; i < missionCount; i++) { Identifier missionIdentifier = msg.ReadIdentifier(); byte connectionIndex = msg.ReadByte(); availableMissions.Add((missionIdentifier, connectionIndex)); } byte selectedMissionCount = msg.ReadByte(); List selectedMissionIndices = new List(); for (int i = 0; i < selectedMissionCount; i++) { selectedMissionIndices.Add(msg.ReadByte()); } if (ShouldApply(NetFlags.MapAndMissions, id, requireUpToDateSave: true)) { campaign.ForceMapUI = forceMapUI; campaign.Map.AllowDebugTeleport = allowDebugTeleport; campaign.Map.SetLocation(currentLocIndex == UInt16.MaxValue ? -1 : currentLocIndex); campaign.Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); foreach (var availableMission in availableMissions) { MissionPrefab missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == availableMission.Identifier); if (missionPrefab == null) { DebugConsole.ThrowError($"Error when receiving campaign data from the server: mission prefab \"{availableMission.Identifier}\" not found."); continue; } if (availableMission.ConnectionIndex == 255) { campaign.Map.CurrentLocation.UnlockMission(missionPrefab); } else { if (availableMission.ConnectionIndex < 0 || availableMission.ConnectionIndex >= campaign.Map.CurrentLocation.Connections.Count) { DebugConsole.ThrowError($"Error when receiving campaign data from the server: connection index for mission \"{availableMission.Identifier}\" out of range (index: {availableMission.ConnectionIndex}, current location: {campaign.Map.CurrentLocation.DisplayName}, connections: {campaign.Map.CurrentLocation.Connections.Count})."); continue; } LocationConnection connection = campaign.Map.CurrentLocation.Connections[availableMission.ConnectionIndex]; campaign.Map.CurrentLocation.UnlockMission(missionPrefab, connection); } } campaign.Map.SelectMission(selectedMissionIndices); ReadStores(msg, apply: true); } else { ReadStores(msg, apply: false); } } if (requiredFlags.HasFlag(NetFlags.SubList)) { DebugConsole.Log("Received campaign update (SubList), round id: " + roundId); UInt16 id = msg.ReadUInt16(); ushort ownedSubCount = msg.ReadUInt16(); List ownedSubIndices = new List(); for (int i = 0; i < ownedSubCount; i++) { ownedSubIndices.Add(msg.ReadUInt16()); } if (ShouldApply(NetFlags.SubList, id, requireUpToDateSave: false)) { foreach (ushort ownedSubIndex in ownedSubIndices) { if (ownedSubIndex >= GameMain.Client.ServerSubmarines.Count) { string errorMsg; if (GameMain.Client.ServerSubmarines.None()) { errorMsg = $"Error in {nameof(MultiPlayerCampaign.ClientRead)}. Owned submarine index was out of bounds (list of server submarines is empty)."; } else { errorMsg = $"Error in {nameof(MultiPlayerCampaign.ClientRead)}. Owned submarine index was out of bounds. Index: {ownedSubIndex}, submarines: {string.Join(", ", GameMain.Client.ServerSubmarines.Select(s => s.Name))}"; } DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "MultiPlayerCampaign.ClientRead.OwnerSubIndexOutOfBounds" + ownedSubIndex, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); continue; } SubmarineInfo sub = GameMain.Client.ServerSubmarines[ownedSubIndex]; if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Owned)) { if (GameMain.GameSession.OwnedSubmarines.None(s => s.Name == sub.Name)) { GameMain.GameSession.OwnedSubmarines.Add(sub); } } } } } if (requiredFlags.HasFlag(NetFlags.UpgradeManager)) { DebugConsole.Log("Received campaign update (UpgradeManager), round id: " + roundId); UInt16 id = msg.ReadUInt16(); ushort pendingUpgradeCount = msg.ReadUInt16(); List pendingUpgrades = new List(); for (int i = 0; i < pendingUpgradeCount; i++) { Identifier upgradeIdentifier = msg.ReadIdentifier(); UpgradePrefab prefab = UpgradePrefab.Find(upgradeIdentifier); Identifier categoryIdentifier = msg.ReadIdentifier(); UpgradeCategory category = UpgradeCategory.Find(categoryIdentifier); int upgradeLevel = msg.ReadByte(); if (prefab == null || category == null) { continue; } pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, upgradeLevel)); } ushort purchasedItemSwapCount = msg.ReadUInt16(); List purchasedItemSwaps = new List(); 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 Item itemToRemove)) { continue; } purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); } if (!Submarine.Unloading && !(Submarine.MainSub is { Loading: true }) && ShouldApply(NetFlags.UpgradeManager, id, requireUpToDateSave: true)) { UpgradeStore.WaitForServerUpdate = false; campaign.UpgradeManager.SetPendingUpgrades(pendingUpgrades); campaign.UpgradeManager.PurchasedUpgrades.Clear(); foreach (var purchasedItemSwap in purchasedItemSwaps) { if (purchasedItemSwap.ItemToInstall == null) { campaign.UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove, force: true); } else { campaign.UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, isNetworkMessage: true); } } foreach (Item item in Item.ItemList.ToList()) { if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) { item.PendingItemSwap = null; } } campaign.CampaignUI?.UpgradeStore?.RequestRefresh(); } } if (requiredFlags.HasFlag(NetFlags.ItemsInBuyCrate)) { DebugConsole.Log("Received campaign update (ItemsInBuyCrate), round id: " + roundId); UInt16 id = msg.ReadUInt16(); var buyCrateItems = ReadPurchasedItems(msg, sender: null); if (ShouldApply(NetFlags.ItemsInBuyCrate, id, requireUpToDateSave: true)) { campaign.CargoManager.SetItemsInBuyCrate(buyCrateItems); campaign.SetLastUpdateIdForFlag(NetFlags.ItemsInBuyCrate, id); ReadStores(msg, apply: true); } else { ReadStores(msg, apply: false); } } if (requiredFlags.HasFlag(NetFlags.ItemsInSellFromSubCrate)) { DebugConsole.Log("Received campaign update (ItemsInSellFromSubCrate), round id: " + roundId); UInt16 id = msg.ReadUInt16(); var subSellCrateItems = ReadPurchasedItems(msg, sender: null); if (ShouldApply(NetFlags.ItemsInSellFromSubCrate, id, requireUpToDateSave: true)) { campaign.CargoManager.SetItemsInSubSellCrate(subSellCrateItems); campaign.SetLastUpdateIdForFlag(NetFlags.ItemsInSellFromSubCrate, id); ReadStores(msg, apply: true); } else { ReadStores(msg, apply: false); } } if (requiredFlags.HasFlag(NetFlags.PurchasedItems)) { DebugConsole.Log("Received campaign update (PuchasedItems), round id: " + roundId); UInt16 id = msg.ReadUInt16(); var purchasedItems = ReadPurchasedItems(msg, sender: null); if (ShouldApply(NetFlags.PurchasedItems, id, requireUpToDateSave: true)) { campaign.CargoManager.SetPurchasedItems(purchasedItems); campaign.SetLastUpdateIdForFlag(NetFlags.PurchasedItems, id); ReadStores(msg, apply: true); } else { ReadStores(msg, apply: false); } } if (requiredFlags.HasFlag(NetFlags.SoldItems)) { DebugConsole.Log("Received campaign update (SoldItems), round id: " + roundId); UInt16 id = msg.ReadUInt16(); var soldItems = ReadSoldItems(msg); if (ShouldApply(NetFlags.SoldItems, id, requireUpToDateSave: true)) { campaign.CargoManager.SetSoldItems(soldItems); campaign.SetLastUpdateIdForFlag(NetFlags.SoldItems, id); ReadStores(msg, apply: true); } else { ReadStores(msg, apply: false); } } if (requiredFlags.HasFlag(NetFlags.Reputation)) { DebugConsole.Log("Received campaign update (Reputation), round id: " + roundId); UInt16 id = msg.ReadUInt16(); Dictionary factionReps = new Dictionary(); byte factionsCount = msg.ReadByte(); for (int i = 0; i < factionsCount; i++) { factionReps.Add(msg.ReadIdentifier(), msg.ReadSingle()); } if (ShouldApply(NetFlags.Reputation, id, requireUpToDateSave: true)) { foreach (var (identifier, rep) in factionReps) { Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == identifier); if (faction?.Reputation != null) { faction.Reputation.SetReputation(rep); } else { DebugConsole.ThrowError($"Received an update for a faction that doesn't exist \"{identifier}\"."); } } campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); } } if (requiredFlags.HasFlag(NetFlags.CharacterInfo)) { DebugConsole.Log("Received campaign update (CharacterInfo), round id: " + roundId); UInt16 id = msg.ReadUInt16(); bool hasCharacterData = msg.ReadBoolean(); CharacterInfo myCharacterInfo = null; bool waitForModsDownloaded = Screen.Selected is ModDownloadScreen; if (hasCharacterData) { myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg, requireJobPrefabFound: !waitForModsDownloaded); } //don't require the correct round ID for the character info if we're in the lobby // = allow updating the character to the latest one in the lobby, even though we've not loaded to the same round as the server if (!waitForModsDownloaded && ShouldApply(NetFlags.CharacterInfo, id, requireUpToDateSave: true, requireCorrectRoundId: Screen.Selected != GameMain.NetLobbyScreen)) { if (myCharacterInfo != null) { GameMain.Client.CharacterInfo = myCharacterInfo; GameMain.NetLobbyScreen.SetCampaignCharacterInfo(myCharacterInfo); GameMain.GameSession.RefreshAnyOpenPlayerInfo(); } else { //don't reset the character info nor the open UI here here, //the client needs it to be able to customize the character they want to next spawn as GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null); //if we've already discarded our current character and the server is just "verifying" that, //no need to refresh the UI (no changes, refreshing would just throw the client out of the character settings panel) if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded) { GameMain.GameSession.RefreshAnyOpenPlayerInfo(); } } } } campaign.SuppressStateSending = true; //we need to have the latest save file to display location/mission/store if (campaign.LastSaveID == saveID) { GameMain.NetLobbyScreen.ToggleCampaignMode(true); } if (refreshCampaignUI) { campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); } campaign.SuppressStateSending = false; bool ShouldApply(NetFlags flag, UInt16 id, bool requireUpToDateSave, bool requireCorrectRoundId = true) { if (requireCorrectRoundId && roundId != campaign.RoundID) { DebugConsole.Log($"Received campaing update for a different round (client: {campaign.RoundID}, server: {roundId}), ignoring..."); return false; } if (NetIdUtils.IdMoreRecent(id, campaign.GetLastUpdateIdForFlag(flag)) && (!requireUpToDateSave || saveID == campaign.LastSaveID)) { campaign.SetLastUpdateIdForFlag(flag, id); return true; } else { return false; } } void ReadStores(IReadMessage msg, bool apply) { var storeBalances = new Dictionary(); if (msg.ReadBoolean()) { byte storeCount = msg.ReadByte(); for (int i = 0; i < storeCount; i++) { Identifier identifier = msg.ReadIdentifier(); UInt16 storeBalance = msg.ReadUInt16(); storeBalances.Add(identifier, storeBalance); } } if (apply) { foreach (var balance in storeBalances) { if (campaign.Map?.CurrentLocation?.GetStore(balance.Key) is { } store) { store.Balance = balance.Value; } } } } } public void ClientReadCrew(IReadMessage msg) { bool createNotification = msg.ReadBoolean(); ushort availableHireLength = msg.ReadUInt16(); List availableHires = new List(); for (int i = 0; i < availableHireLength; i++) { CharacterInfo hire = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); hire.Salary = msg.ReadInt32(); availableHires.Add(hire); } ushort pendingHireLength = msg.ReadUInt16(); List pendingHires = new List(); bool[] pendingHiresToReserveBench = new bool[pendingHireLength]; for (int i = 0; i < pendingHireLength; i++) { pendingHires.Add(msg.ReadUInt16()); pendingHiresToReserveBench[i] = msg.ReadBoolean(); } List hiredCharacters = new List(); List updatedCrewManager = new List(); ushort crewLength = msg.ReadUInt16(); for (int i = 0; i < crewLength; i++) { CharacterInfo crewMember = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); if (crewMember.IsNewHire) { hiredCharacters.Add(crewMember); } updatedCrewManager.Add(crewMember); } bool crewManagerUpdated = GameMain.GameSession.CrewManager?.UpdateCrewManagerIfNecessary(updatedCrewManager) ?? false; ushort reserveBenchLength = msg.ReadUInt16(); List updatedReserveBench = new List(); for (int i = 0; i < reserveBenchLength; i++) { CharacterInfo info = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); updatedReserveBench.Add(info); } bool reserveBenchUpdated = GameMain.GameSession.CrewManager?.UpdateReserveBenchIfNeeded(updatedReserveBench) ?? false; bool renameCrewMember = msg.ReadBoolean(); if (renameCrewMember) { UInt16 renamedIdentifier = msg.ReadUInt16(); string newName = msg.ReadString(); CharacterInfo renamedCharacter = CrewManager.GetCharacterInfos(includeReserveBench: true).FirstOrDefault(info => info.ID == renamedIdentifier); if (renamedCharacter != null) { CrewManager.RenameCharacter(renamedCharacter, newName); // Since renaming can only be done once in permadeath, we can safely set this to false to disable the renaming in the UI. renamedCharacter.RenamingEnabled = false; } else { DebugConsole.ThrowError($"Could not find a character to rename with the ID {renamedIdentifier}."); } } bool fireCharacter = msg.ReadBoolean(); if (fireCharacter) { UInt16 firedIdentifier = msg.ReadUInt16(); CharacterInfo firedCharacter = CrewManager.GetCharacterInfos(includeReserveBench: true).FirstOrDefault(info => info.ID == firedIdentifier); // this one might and is allowed to be null since the character is already fired on the original sender's game if (firedCharacter != null) { CrewManager.FireCharacter(firedCharacter); } } if (map?.CurrentLocation?.HireManager != null && CampaignUI?.HRManagerUI != null) { //can't apply until we have the latest save file if (!NetIdUtils.IdMoreRecent(pendingSaveID, LastSaveID)) { CampaignUI.HRManagerUI.SetHireables(map.CurrentLocation, availableHires); if (hiredCharacters.Any()) { CampaignUI.HRManagerUI.ValidateHires(hiredCharacters, takeMoney: false, createNotification: createNotification); } //don't check the crew size limit: if the server says someone's hired, then it's so CampaignUI.HRManagerUI.SetPendingHires(pendingHires, pendingHiresToReserveBench, map.CurrentLocation, checkCrewSizeLimit: false); } } else { //This is pretty nasty: setting hireables is handled through CrewManagement, //which is part of the Campaign UI that might not exist when the client is still initializing the round. //If that's the case, let's force the available hires here so they're available when the UI is created CurrentLocation?.ForceHireableCharacters(availableHires); } if (fireCharacter || renameCrewMember || crewManagerUpdated || reserveBenchUpdated) { CampaignUI?.HRManagerUI?.RefreshHRView(); GameMain.GameSession?.DeathPrompt?.UpdateBotList(); } } public void ClientReadMoney(IReadMessage inc) { NetWalletUpdate update = INetSerializableStruct.Read(inc); foreach (NetWalletTransaction transaction in update.Transactions) { WalletInfo info = transaction.Info; if (transaction.CharacterID.TryUnwrap(out var charID)) { Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID); if (targetCharacter is null) { break; } Wallet wallet = targetCharacter.Wallet; wallet.Balance = info.Balance; wallet.RewardDistribution = info.RewardDistribution; TryInvokeEvent(wallet, transaction.ChangedData, info); } else { Bank.Balance = info.Balance; Bank.RewardDistribution = info.RewardDistribution; TryInvokeEvent(Bank, transaction.ChangedData, info); } } void TryInvokeEvent(Wallet wallet, WalletChangedData data, WalletInfo info) { if (data.BalanceChanged.IsSome() || data.RewardDistributionChanged.IsSome()) { OnMoneyChanged.Invoke(new WalletChangedEvent(wallet, data, info)); } } } public override bool TryPurchase(Client client, int price) { if (price == 0) { return true; } if (!AllowedToManageCampaign(ClientPermissions.ManageMoney)) { return PersonalWallet.TryDeduct(price); } int balance = PersonalWallet.Balance; if (balance >= price) { return PersonalWallet.TryDeduct(price); } if (balance + Bank.Balance >= price) { int remainder = price - balance; if (balance > 0) { PersonalWallet.Deduct(balance); } Bank.Deduct(remainder); return true ; } return false; } public override void Save(XElement element, bool isSavingOnLoading) { //do nothing, the clients get the save files from the server } public void LoadState(string filePath) { DebugConsole.Log($"Loading save file for an existing game session ({filePath})"); SaveUtil.DecompressToDirectory(filePath, SaveUtil.TempPath); string gamesessionDocPath = Path.Combine(SaveUtil.TempPath, SaveUtil.GameSessionFileName); XDocument doc = XMLExtensions.TryLoadXml(gamesessionDocPath); if (doc == null) { DebugConsole.ThrowError($"Failed to load the state of a multiplayer campaign. Could not open the file \"{gamesessionDocPath}\"."); return; } Load(doc.Root.Element("MultiPlayerCampaign")); GameMain.GameSession.OwnedSubmarines = SaveUtil.LoadOwnedSubmarines(doc, out SubmarineInfo selectedSub); GameMain.GameSession.SubmarineInfo = selectedSub; } } }