Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using Barotrauma.IO;
7 using System.Linq;
8 using System.Xml.Linq;
9 
10 namespace Barotrauma
11 {
12  partial class MultiPlayerCampaign : CampaignMode
13  {
14  public bool SuppressStateSending = false;
15 
16  public override bool Paused
17  {
18  get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition"); }
19  }
20 
21  private UInt16 pendingSaveID = 1;
22  public UInt16 PendingSaveID
23  {
24  get
25  {
26  return pendingSaveID;
27  }
28  set
29  {
30  pendingSaveID = value;
31  //pending save ID 0 means "no save received yet"
32  //save IDs are always above 0, so we should never be waiting for 0
33  if (pendingSaveID == 0) { pendingSaveID++; }
34  }
35  }
36 
38  public override Wallet Wallet => GetWallet();
39 
40  public override int GetBalance(Client client = null)
41  {
43  {
44  return PersonalWallet.Balance;
45  }
46 
47  return PersonalWallet.Balance + Bank.Balance;
48  }
49 
50  public override Wallet GetWallet(Client client = null)
51  {
52  return PersonalWallet;
53  }
54 
55  public static void StartCampaignSetup(List<SaveInfo> saveFiles)
56  {
58  parent.ClearChildren();
59  parent.Visible = true;
62 
63  var layout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center))
64  {
65  Stretch = true
66  };
67 
68  var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), layout.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.1f) }, isHorizontal: true)
69  {
70  Stretch = true,
71  RelativeSpacing = 0.02f
72  };
73 
74  var campaignContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), layout.RectTransform, Anchor.BottomLeft), style: "GUIFrameListBox")
75  {
76  CanBeFocused = false
77  };
78 
79  var newCampaignContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.95f), campaignContainer.RectTransform, Anchor.Center), style: null);
80  var loadCampaignContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.95f), campaignContainer.RectTransform, Anchor.Center), style: null);
81 
82  GameMain.NetLobbyScreen.CampaignSetupUI = new MultiPlayerCampaignSetupUI(newCampaignContainer, loadCampaignContainer, saveFiles);
83 
84  var newCampaignButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform),
85  TextManager.Get("NewCampaign"), style: "GUITabButton")
86  {
87  Selected = true
88  };
89 
90  var loadCampaignButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.00f), buttonContainer.RectTransform),
91  TextManager.Get("LoadCampaign"), style: "GUITabButton");
92 
93  newCampaignButton.OnClicked = (btn, obj) =>
94  {
95  newCampaignButton.Selected = true;
96  loadCampaignButton.Selected = false;
97  newCampaignContainer.Visible = true;
98  loadCampaignContainer.Visible = false;
100  return true;
101  };
102  loadCampaignButton.OnClicked = (btn, obj) =>
103  {
104  newCampaignButton.Selected = false;
105  loadCampaignButton.Selected = true;
106  newCampaignContainer.Visible = false;
107  loadCampaignContainer.Visible = true;
109  return true;
110  };
111  loadCampaignContainer.Visible = false;
112 
113  GUITextBlock.AutoScaleAndNormalize(newCampaignButton.TextBlock, loadCampaignButton.TextBlock);
114 
117  }
118 
119  partial void InitProjSpecific()
120  {
121  CreateButtons();
122  }
123 
124  public override void HUDScaleChanged()
125  {
126  CreateButtons();
127  }
128 
129  private void CreateButtons()
130  {
132  endRoundButton.OnClicked = (btn, userdata) =>
133  {
135  onConfirm: () => GameMain.Client.RequestStartRound(),
136  onReturnToMapScreen: () =>
137  {
138  ShowCampaignUI = true;
139  if (CampaignUI == null) { InitCampaignUI(); }
141  });
142  return true;
143  };
144 
145  int readyButtonWidth = (int)(GUI.Scale * 50 * (GUI.IsUltrawide ? 3.0f : 1.0f));
146  int readyButtonHeight = (int)(GUI.Scale * 40);
147  int readyButtonCenter = readyButtonHeight / 2,
148  screenMiddle = GameMain.GraphicsWidth / 2;
149  ReadyCheckButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle + (endRoundButton.Rect.Width / 2) + GUI.IntScale(16), HUDLayoutSettings.ButtonAreaTop.Center.Y - readyButtonCenter, readyButtonWidth, readyButtonHeight), GUI.Canvas),
150  style: "RepairBuyButton")
151  {
152  ToolTip = TextManager.Get("ReadyCheck.Tooltip"),
153  OnClicked = delegate
154  {
155  if (CrewManager != null && CrewManager.ActiveReadyCheck == null)
156  {
157  ReadyCheck.CreateReadyCheck();
158  }
159  return true;
160  },
161  UserData = "ReadyCheckButton"
162  };
163  }
164 
165  public void InitCampaignUI()
166  {
167  campaignUIContainer = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black);
168  CampaignUI = new CampaignUI(this, campaignUIContainer)
169  {
170  StartRound = () =>
171  {
173  }
174  };
175  }
176 
177  public override void Start()
178  {
179  base.Start();
180  CoroutineManager.StartCoroutine(DoInitialCameraTransition(), "MultiplayerCampaign.DoInitialCameraTransition");
181  }
182 
183  protected override void LoadInitialLevel()
184  {
185  //clients should never call this
186  throw new InvalidOperationException("");
187  }
188 
189 
190  private IEnumerable<CoroutineStatus> DoInitialCameraTransition()
191  {
193  {
194  yield return CoroutineStatus.Running;
195  }
196 
197  if (GameMain.Client == null)
198  {
199  yield return CoroutineStatus.Success;
200  }
201 
202  if (GameMain.Client.LateCampaignJoin)
203  {
204  GameMain.Client.LateCampaignJoin = false;
205  yield return CoroutineStatus.Success;
206  }
207 
208  Character prevControlled = Character.Controlled;
209  GUI.DisableHUD = true;
210  if (IsFirstRound)
211  {
212  if (SlideshowPrefab.Prefabs.TryGet("campaignstart".ToIdentifier(), out var slideshow))
213  {
214  SlideshowPlayer = new SlideshowPlayer(GUICanvas.Instance, slideshow);
215  }
216 
217  Character.Controlled = null;
218  prevControlled?.ClearInputs();
219 
220  overlayColor = Color.LightGray;
221  overlaySprite = Map.CurrentLocation.Type.GetPortrait(Map.CurrentLocation.PortraitId);
222 
223  var outpost = GameMain.GameSession.Level.StartOutpost;
224  var borders = outpost.GetDockedBorders();
225  borders.Location += outpost.WorldPosition.ToPoint();
226  GameMain.GameScreen.Cam.Position = new Vector2(borders.X + borders.Width / 2, borders.Y - borders.Height / 2);
227  float startZoom = 0.8f /
228  ((float)Math.Max(borders.Width, borders.Height) / (float)GameMain.GameScreen.Cam.Resolution.X);
229  GameMain.GameScreen.Cam.Zoom = GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom);
230  while (SlideshowPlayer != null && !SlideshowPlayer.LastTextShown)
231  {
232  GUI.PreventPauseMenuToggle = true;
233  yield return CoroutineStatus.Running;
234  }
235  GUI.PreventPauseMenuToggle = false;
236  prevControlled ??= Character.Controlled;
237  GameMain.LightManager.LosAlpha = 0.0f;
238  var transition = new CameraTransition(prevControlled, GameMain.GameScreen.Cam,
239  null, null,
240  fadeOut: false,
241  losFadeIn: true,
242  waitDuration: 1,
243  panDuration: 5,
244  startZoom: startZoom, endZoom: 1.0f)
245  {
246  AllowInterrupt = true,
247  RemoveControlFromCharacter = false
248  };
249  overlayColor = Color.Transparent;
250  while (transition.Running)
251  {
252  yield return CoroutineStatus.Running;
253  }
254 
255  if (prevControlled != null)
256  {
257  Character.Controlled = prevControlled;
258  }
259  }
260  else
261  {
262  var transition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam,
263  null, null,
264  fadeOut: false,
265  losFadeIn: true,
266  panDuration: 5,
267  startZoom: 0.5f, endZoom: 1.0f)
268  {
269  AllowInterrupt = true,
270  RemoveControlFromCharacter = true
271  };
272  while (transition.Running)
273  {
274  yield return CoroutineStatus.Running;
275  }
276  }
277 
278  if (prevControlled != null)
279  {
280  prevControlled.SelectedItem = prevControlled.SelectedSecondaryItem = null;
281  if (prevControlled.AIController != null)
282  {
283  prevControlled.AIController.Enabled = true;
284  }
285  }
286  GUI.DisableHUD = false;
287  yield return CoroutineStatus.Success;
288  }
289 
290  protected override IEnumerable<CoroutineStatus> DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror)
291  {
292  yield return CoroutineStatus.Success;
293  }
294 
295  private IEnumerable<CoroutineStatus> DoLevelTransition()
296  {
297  SoundPlayer.OverrideMusicType = (CrewManager.GetCharacters().Any(c => !c.IsDead) ? "endround" : "crewdead").ToIdentifier();
298  SoundPlayer.OverrideMusicDuration = 18.0f;
299 
300  Level prevLevel = Level.Loaded;
301 
302  bool success = CrewManager.GetCharacters().Any(c => !c.IsDead);
303  CrewDead = false;
304 
305  var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton;
306  if (continueButton != null)
307  {
308  continueButton.Visible = false;
309  }
310 
311  Character.Controlled = null;
312 
313  yield return new WaitForSeconds(0.1f);
314 
315  GameMain.Client.EndCinematic?.Stop();
316  var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null,
317  Alignment.Center,
318  fadeOut: false,
319  panDuration: EndTransitionDuration);
320  GameMain.Client.EndCinematic = endTransition;
321 
322  Location portraitLocation = Map?.SelectedLocation ?? Map?.CurrentLocation ?? Level.Loaded?.StartLocation;
323  if (portraitLocation != null)
324  {
325  overlaySprite = portraitLocation.Type.GetPortrait(portraitLocation.PortraitId);
326  }
327  float fadeOutDuration = endTransition.PanDuration;
328  float t = 0.0f;
329  while (t < fadeOutDuration || endTransition.Running)
330  {
331  t += CoroutineManager.DeltaTime;
332  overlayColor = Color.Lerp(Color.Transparent, Color.White, t / fadeOutDuration);
333  yield return CoroutineStatus.Running;
334  }
335  overlayColor = Color.White;
336  yield return CoroutineStatus.Running;
337 
338  //--------------------------------------
339 
340  //wait for the new level to be loaded
341  DateTime timeOut = DateTime.Now + GameClient.LevelTransitionTimeOut;
342  while (Level.Loaded == prevLevel || Level.Loaded == null)
343  {
344  if (DateTime.Now > timeOut || Screen.Selected != GameMain.GameScreen) { break; }
345  yield return CoroutineStatus.Running;
346  }
347 
348  endTransition.Stop();
349  overlayColor = Color.Transparent;
350 
351  if (DateTime.Now > timeOut)
352  {
353  DebugConsole.ThrowError("Failed to start the round. Timed out while waiting for the level transition to finish.");
354  GameMain.NetLobbyScreen.Select();
355  }
356  if (Screen.Selected is not RoundSummaryScreen)
357  {
358  if (continueButton != null)
359  {
360  continueButton.Visible = true;
361  }
362  }
363 
364  GUI.SetSavingIndicatorState(false);
365  yield return CoroutineStatus.Success;
366  }
367 
368  public override void Update(float deltaTime)
369  {
370  if (CoroutineManager.IsCoroutineRunning("LevelTransition") || Level.Loaded == null) { return; }
371 
372  if (ShowCampaignUI || ForceMapUI)
373  {
374  if (CampaignUI == null) { InitCampaignUI(); }
375  Character.DisableControls = true;
376  }
377 
378  base.Update(deltaTime);
379 
380  SlideshowPlayer?.UpdateManually(deltaTime);
381 
383  PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape))
384  {
385  ShowCampaignUI = false;
386  if (GUIMessageBox.VisibleBox?.UserData is RoundSummary roundSummary &&
387  roundSummary.ContinueButton != null &&
388  roundSummary.ContinueButton.Visible)
389  {
391  }
392  }
393 
394  if (!GUI.DisableHUD && !GUI.DisableUpperHUD)
395  {
396  endRoundButton.UpdateManually(deltaTime);
397  ReadyCheckButton?.UpdateManually(deltaTime);
398  if (CoroutineManager.IsCoroutineRunning("LevelTransition") || ForceMapUI) { return; }
399  }
400 
401  if (Level.Loaded.Type == LevelData.LevelType.Outpost)
402  {
403  if (wasDocked)
404  {
405  var connectedSubs = Submarine.MainSub.GetConnectedSubs();
406  bool isDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost);
407  if (!isDocked)
408  {
409  //undocked from outpost, need to choose a destination
410  ForceMapUI = true;
411  if (CampaignUI == null) { InitCampaignUI(); }
413  }
414  }
415  //end biome is handled by the server (automatic transition without a map screen when the end of the level is reached)
416  else if (!Level.Loaded.IsEndBiome)
417  {
418  //wasn't initially docked (sub doesn't have a docking port?)
419  // -> choose a destination when the sub is far enough from the start outpost
421  {
422  ForceMapUI = true;
423  if (CampaignUI == null) { InitCampaignUI(); }
425  }
426  }
427 
428  if (CampaignUI == null) { InitCampaignUI(); }
429  }
430  else
431  {
432  var transitionType = GetAvailableTransition(out _, out _);
433  if (transitionType == TransitionType.None && CampaignUI?.SelectedTab == InteractionType.Map)
434  {
435  ShowCampaignUI = false;
436  }
437  HintManager.OnAvailableTransition(transitionType);
438  }
439  }
440 
441  public override void UpdateWhilePaused(float deltaTime)
442  {
443  SlideshowPlayer?.UpdateManually(deltaTime);
444  }
445 
446  public override void End(TransitionType transitionType = TransitionType.None)
447  {
448  base.End(transitionType);
449  ForceMapUI = ShowCampaignUI = false;
451 
452  // remove all event dialogue boxes
453  GUIMessageBox.MessageBoxes.ForEachMod(mb =>
454  {
455  if (mb is GUIMessageBox msgBox)
456  {
457  if (ReadyCheck.IsReadyCheck(mb) || mb.UserData is Pair<string, ushort> pair && pair.First.Equals("conversationaction", StringComparison.OrdinalIgnoreCase))
458  {
459  msgBox.Close();
460  }
461  }
462  });
463 
464  if (transitionType == TransitionType.End)
465  {
466  EndCampaign();
467  }
468  else
469  {
470  IsFirstRound = false;
471  CoroutineManager.StartCoroutine(DoLevelTransition(), "LevelTransition");
472  }
473  }
474 
475  protected override void EndCampaignProjSpecific()
476  {
477  if (GUIMessageBox.VisibleBox?.UserData is RoundSummary roundSummary)
478  {
480  }
482  GUI.DisableHUD = false;
484  {
487  };
488  }
489 
490  public void ClientWrite(IWriteMessage msg)
491  {
492  System.Diagnostics.Debug.Assert(map.Locations.Count < UInt16.MaxValue);
493 
494  msg.WriteUInt16(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex);
495  msg.WriteUInt16(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex);
496 
497  var selectedMissionIndices = map.GetSelectedMissionIndices();
498  msg.WriteByte((byte)selectedMissionIndices.Count());
499  foreach (int selectedMissionIndex in selectedMissionIndices)
500  {
501  msg.WriteByte((byte)selectedMissionIndex);
502  }
503  msg.WriteBoolean(PurchasedHullRepairs);
504  msg.WriteBoolean(PurchasedItemRepairs);
505  msg.WriteBoolean(PurchasedLostShuttles);
506 
507  WriteItems(msg, CargoManager.ItemsInBuyCrate);
508  WriteItems(msg, CargoManager.ItemsInSellFromSubCrate);
509  WriteItems(msg, CargoManager.PurchasedItems);
510  WriteItems(msg, CargoManager.SoldItems);
511 
512  msg.WriteUInt16((ushort)UpgradeManager.PurchasedUpgrades.Count);
513  foreach (var (prefab, category, level) in UpgradeManager.PurchasedUpgrades)
514  {
515  msg.WriteIdentifier(prefab.Identifier);
516  msg.WriteIdentifier(category.Identifier);
517  msg.WriteByte((byte)level);
518  }
519 
520  msg.WriteUInt16((ushort)UpgradeManager.PurchasedItemSwaps.Count);
521  foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps)
522  {
523  msg.WriteUInt16(itemSwap.ItemToRemove.ID);
524  msg.WriteIdentifier(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty);
525  }
526  }
527 
528  //static because we may need to instantiate the campaign if it hasn't been done yet
529  public static void ClientRead(IReadMessage msg)
530  {
531  NetFlags requiredFlags = (NetFlags)msg.ReadUInt16();
532 
533  bool isFirstRound = msg.ReadBoolean();
534  byte campaignID = msg.ReadByte();
535  UInt16 saveID = msg.ReadUInt16();
536  string mapSeed = msg.ReadString();
537 
538  bool refreshCampaignUI = false;
539 
540  if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign || campaignID != campaign.CampaignID)
541  {
542  string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer);
543 
544  GameMain.GameSession = new GameSession(null, savePath, GameModePreset.MultiPlayerCampaign, CampaignSettings.Empty, mapSeed);
546  campaign.CampaignID = campaignID;
548  }
549 
550  //server has a newer save file
551  if (NetIdUtils.IdMoreRecent(saveID, campaign.PendingSaveID)) { campaign.PendingSaveID = saveID; }
552  campaign.IsFirstRound = isFirstRound;
553 
554  if (requiredFlags.HasFlag(NetFlags.Misc))
555  {
556  DebugConsole.Log("Received campaign update (Misc)");
557  UInt16 id = msg.ReadUInt16();
558  bool purchasedHullRepairs = msg.ReadBoolean();
559  bool purchasedItemRepairs = msg.ReadBoolean();
560  bool purchasedLostShuttles = msg.ReadBoolean();
561  if (ShouldApply(NetFlags.Misc, id, requireUpToDateSave: false))
562  {
563  refreshCampaignUI = campaign.PurchasedHullRepairs != purchasedHullRepairs ||
564  campaign.PurchasedItemRepairs != purchasedItemRepairs ||
565  campaign.PurchasedLostShuttles != purchasedLostShuttles;
566  campaign.PurchasedHullRepairs = purchasedHullRepairs;
567  campaign.PurchasedItemRepairs = purchasedItemRepairs;
568  campaign.PurchasedLostShuttles = purchasedLostShuttles;
569  }
570  }
571 
572  if (requiredFlags.HasFlag(NetFlags.MapAndMissions))
573  {
574  DebugConsole.Log("Received campaign update (MapAndMissions)");
575  UInt16 id = msg.ReadUInt16();
576  bool forceMapUI = msg.ReadBoolean();
577  bool allowDebugTeleport = msg.ReadBoolean();
578  UInt16 currentLocIndex = msg.ReadUInt16();
579  UInt16 selectedLocIndex = msg.ReadUInt16();
580 
581  byte missionCount = msg.ReadByte();
582  var availableMissions = new List<(Identifier Identifier, byte ConnectionIndex)>();
583  for (int i = 0; i < missionCount; i++)
584  {
585  Identifier missionIdentifier = msg.ReadIdentifier();
586  byte connectionIndex = msg.ReadByte();
587  availableMissions.Add((missionIdentifier, connectionIndex));
588  }
589 
590  byte selectedMissionCount = msg.ReadByte();
591  List<int> selectedMissionIndices = new List<int>();
592  for (int i = 0; i < selectedMissionCount; i++)
593  {
594  selectedMissionIndices.Add(msg.ReadByte());
595  }
596 
597  if (ShouldApply(NetFlags.MapAndMissions, id, requireUpToDateSave: true))
598  {
599  campaign.ForceMapUI = forceMapUI;
600  campaign.Map.AllowDebugTeleport = allowDebugTeleport;
601  campaign.Map.SetLocation(currentLocIndex == UInt16.MaxValue ? -1 : currentLocIndex);
602  campaign.Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex);
603  foreach (var availableMission in availableMissions)
604  {
605  MissionPrefab missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == availableMission.Identifier);
606  if (missionPrefab == null)
607  {
608  DebugConsole.ThrowError($"Error when receiving campaign data from the server: mission prefab \"{availableMission.Identifier}\" not found.");
609  continue;
610  }
611  if (availableMission.ConnectionIndex == 255)
612  {
613  campaign.Map.CurrentLocation.UnlockMission(missionPrefab);
614  }
615  else
616  {
617  if (availableMission.ConnectionIndex < 0 || availableMission.ConnectionIndex >= campaign.Map.CurrentLocation.Connections.Count)
618  {
619  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}).");
620  continue;
621  }
622  LocationConnection connection = campaign.Map.CurrentLocation.Connections[availableMission.ConnectionIndex];
623  campaign.Map.CurrentLocation.UnlockMission(missionPrefab, connection);
624  }
625  }
626  campaign.Map.SelectMission(selectedMissionIndices);
627  ReadStores(msg, apply: true);
628  }
629  else
630  {
631  ReadStores(msg, apply: false);
632  }
633  }
634 
635  if (requiredFlags.HasFlag(NetFlags.SubList))
636  {
637  DebugConsole.Log("Received campaign update (SubList)");
638  UInt16 id = msg.ReadUInt16();
639  ushort ownedSubCount = msg.ReadUInt16();
640  List<ushort> ownedSubIndices = new List<ushort>();
641  for (int i = 0; i < ownedSubCount; i++)
642  {
643  ownedSubIndices.Add(msg.ReadUInt16());
644  }
645 
646  if (ShouldApply(NetFlags.SubList, id, requireUpToDateSave: false))
647  {
648  foreach (ushort ownedSubIndex in ownedSubIndices)
649  {
650  if (ownedSubIndex >= GameMain.Client.ServerSubmarines.Count)
651  {
652  string errorMsg;
653  if (GameMain.Client.ServerSubmarines.None())
654  {
655  errorMsg = $"Error in {nameof(MultiPlayerCampaign.ClientRead)}. Owned submarine index was out of bounds (list of server submarines is empty).";
656  }
657  else
658  {
659  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))}";
660  }
661  DebugConsole.ThrowError(errorMsg);
662  GameAnalyticsManager.AddErrorEventOnce(
663  "MultiPlayerCampaign.ClientRead.OwnerSubIndexOutOfBounds" + ownedSubIndex,
664  GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
665  continue;
666  }
667 
668  SubmarineInfo sub = GameMain.Client.ServerSubmarines[ownedSubIndex];
670  {
671  if (GameMain.GameSession.OwnedSubmarines.None(s => s.Name == sub.Name))
672  {
674  }
675  }
676  }
677  }
678  }
679 
680  if (requiredFlags.HasFlag(NetFlags.UpgradeManager))
681  {
682  DebugConsole.Log("Received campaign update (UpgradeManager)");
683  UInt16 id = msg.ReadUInt16();
684 
685  ushort pendingUpgradeCount = msg.ReadUInt16();
686  List<PurchasedUpgrade> pendingUpgrades = new List<PurchasedUpgrade>();
687  for (int i = 0; i < pendingUpgradeCount; i++)
688  {
689  Identifier upgradeIdentifier = msg.ReadIdentifier();
690  UpgradePrefab prefab = UpgradePrefab.Find(upgradeIdentifier);
691  Identifier categoryIdentifier = msg.ReadIdentifier();
692  UpgradeCategory category = UpgradeCategory.Find(categoryIdentifier);
693  int upgradeLevel = msg.ReadByte();
694  if (prefab == null || category == null) { continue; }
695  pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, upgradeLevel));
696  }
697 
698  ushort purchasedItemSwapCount = msg.ReadUInt16();
699  List<PurchasedItemSwap> purchasedItemSwaps = new List<PurchasedItemSwap>();
700  for (int i = 0; i < purchasedItemSwapCount; i++)
701  {
702  UInt16 itemToRemoveID = msg.ReadUInt16();
703  Identifier itemToInstallIdentifier = msg.ReadIdentifier();
704  ItemPrefab itemToInstall = itemToInstallIdentifier.IsEmpty ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier);
705  if (!(Entity.FindEntityByID(itemToRemoveID) is Item itemToRemove)) { continue; }
706  purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall));
707  }
708 
709  if (!Submarine.Unloading && !(Submarine.MainSub is { Loading: true }) &&
710  ShouldApply(NetFlags.UpgradeManager, id, requireUpToDateSave: true))
711  {
712  UpgradeStore.WaitForServerUpdate = false;
713  campaign.UpgradeManager.SetPendingUpgrades(pendingUpgrades);
714  campaign.UpgradeManager.PurchasedUpgrades.Clear();
715  foreach (var purchasedItemSwap in purchasedItemSwaps)
716  {
717  if (purchasedItemSwap.ItemToInstall == null)
718  {
719  campaign.UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove, force: true);
720  }
721  else
722  {
723  campaign.UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, isNetworkMessage: true);
724  }
725  }
726  foreach (Item item in Item.ItemList.ToList())
727  {
728  if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item))
729  {
730  item.PendingItemSwap = null;
731  }
732  }
733  campaign.CampaignUI?.UpgradeStore?.RequestRefresh();
734  }
735  }
736 
737 
738  if (requiredFlags.HasFlag(NetFlags.ItemsInBuyCrate))
739  {
740  DebugConsole.Log("Received campaign update (ItemsInBuyCrate)");
741  UInt16 id = msg.ReadUInt16();
742  var buyCrateItems = ReadPurchasedItems(msg, sender: null);
743  if (ShouldApply(NetFlags.ItemsInBuyCrate, id, requireUpToDateSave: true))
744  {
745  campaign.CargoManager.SetItemsInBuyCrate(buyCrateItems);
746  campaign.SetLastUpdateIdForFlag(NetFlags.ItemsInBuyCrate, id);
747  ReadStores(msg, apply: true);
748  }
749  else
750  {
751  ReadStores(msg, apply: false);
752  }
753  }
754  if (requiredFlags.HasFlag(NetFlags.ItemsInSellFromSubCrate))
755  {
756  DebugConsole.Log("Received campaign update (ItemsInSellFromSubCrate)");
757  UInt16 id = msg.ReadUInt16();
758  var subSellCrateItems = ReadPurchasedItems(msg, sender: null);
759  if (ShouldApply(NetFlags.ItemsInSellFromSubCrate, id, requireUpToDateSave: true))
760  {
761  campaign.CargoManager.SetItemsInSubSellCrate(subSellCrateItems);
762  campaign.SetLastUpdateIdForFlag(NetFlags.ItemsInSellFromSubCrate, id);
763  ReadStores(msg, apply: true);
764  }
765  else
766  {
767  ReadStores(msg, apply: false);
768  }
769  }
770  if (requiredFlags.HasFlag(NetFlags.PurchasedItems))
771  {
772  DebugConsole.Log("Received campaign update (PuchasedItems)");
773  UInt16 id = msg.ReadUInt16();
774  var purchasedItems = ReadPurchasedItems(msg, sender: null);
775  if (ShouldApply(NetFlags.PurchasedItems, id, requireUpToDateSave: true))
776  {
777  campaign.CargoManager.SetPurchasedItems(purchasedItems);
778  campaign.SetLastUpdateIdForFlag(NetFlags.PurchasedItems, id);
779  ReadStores(msg, apply: true);
780  }
781  else
782  {
783  ReadStores(msg, apply: false);
784  }
785  }
786  if (requiredFlags.HasFlag(NetFlags.SoldItems))
787  {
788  DebugConsole.Log("Received campaign update (SoldItems)");
789  UInt16 id = msg.ReadUInt16();
790  var soldItems = ReadSoldItems(msg);
791  if (ShouldApply(NetFlags.SoldItems, id, requireUpToDateSave: true))
792  {
793  campaign.CargoManager.SetSoldItems(soldItems);
794  campaign.SetLastUpdateIdForFlag(NetFlags.SoldItems, id);
795  ReadStores(msg, apply: true);
796  }
797  else
798  {
799  ReadStores(msg, apply: false);
800  }
801  }
802  if (requiredFlags.HasFlag(NetFlags.Reputation))
803  {
804  DebugConsole.Log("Received campaign update (Reputation)");
805  UInt16 id = msg.ReadUInt16();
806  Dictionary<Identifier, float> factionReps = new Dictionary<Identifier, float>();
807  byte factionsCount = msg.ReadByte();
808  for (int i = 0; i < factionsCount; i++)
809  {
810  factionReps.Add(msg.ReadIdentifier(), msg.ReadSingle());
811  }
812  if (ShouldApply(NetFlags.Reputation, id, requireUpToDateSave: true))
813  {
814  foreach (var (identifier, rep) in factionReps)
815  {
816  Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == identifier);
817  if (faction?.Reputation != null)
818  {
819  faction.Reputation.SetReputation(rep);
820  }
821  else
822  {
823  DebugConsole.ThrowError($"Received an update for a faction that doesn't exist \"{identifier}\".");
824  }
825  }
826  campaign?.CampaignUI?.UpgradeStore?.RequestRefresh();
827  }
828  }
829  if (requiredFlags.HasFlag(NetFlags.CharacterInfo))
830  {
831  DebugConsole.Log("Received campaign update (CharacterInfo)");
832  UInt16 id = msg.ReadUInt16();
833  bool hasCharacterData = msg.ReadBoolean();
834  CharacterInfo myCharacterInfo = null;
835  bool waitForModsDownloaded = Screen.Selected is ModDownloadScreen;
836  if (hasCharacterData)
837  {
838  myCharacterInfo = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg, requireJobPrefabFound: !waitForModsDownloaded);
839  }
840  if (!waitForModsDownloaded && ShouldApply(NetFlags.CharacterInfo, id, requireUpToDateSave: true))
841  {
842  if (myCharacterInfo != null)
843  {
844  GameMain.Client.CharacterInfo = myCharacterInfo;
846  }
847  else
848  {
850  }
851  }
852  }
853 
854  campaign.SuppressStateSending = true;
855  //we need to have the latest save file to display location/mission/store
856  if (campaign.LastSaveID == saveID)
857  {
859  }
860  if (refreshCampaignUI)
861  {
862  campaign?.CampaignUI?.UpgradeStore?.RequestRefresh();
863  }
864  campaign.SuppressStateSending = false;
865 
866  bool ShouldApply(NetFlags flag, UInt16 id, bool requireUpToDateSave)
867  {
868  if (NetIdUtils.IdMoreRecent(id, campaign.GetLastUpdateIdForFlag(flag)) &&
869  (!requireUpToDateSave || saveID == campaign.LastSaveID))
870  {
871  campaign.SetLastUpdateIdForFlag(flag, id);
872  return true;
873  }
874  else
875  {
876  return false;
877  }
878  }
879 
880  void ReadStores(IReadMessage msg, bool apply)
881  {
882  var storeBalances = new Dictionary<Identifier, UInt16>();
883  if (msg.ReadBoolean())
884  {
885  byte storeCount = msg.ReadByte();
886  for (int i = 0; i < storeCount; i++)
887  {
888  Identifier identifier = msg.ReadIdentifier();
889  UInt16 storeBalance = msg.ReadUInt16();
890  storeBalances.Add(identifier, storeBalance);
891  }
892  }
893  if (apply)
894  {
895  foreach (var balance in storeBalances)
896  {
897  if (campaign.Map?.CurrentLocation?.GetStore(balance.Key) is { } store)
898  {
899  store.Balance = balance.Value;
900  }
901  }
902  }
903  }
904 
905  }
906 
907  public void ClientReadCrew(IReadMessage msg)
908  {
909  bool createNotification = msg.ReadBoolean();
910 
911  ushort availableHireLength = msg.ReadUInt16();
912  List<CharacterInfo> availableHires = new List<CharacterInfo>();
913  for (int i = 0; i < availableHireLength; i++)
914  {
916  hire.Salary = msg.ReadInt32();
917  availableHires.Add(hire);
918  }
919 
920  ushort pendingHireLength = msg.ReadUInt16();
921  List<UInt16> pendingHires = new List<UInt16>();
922  for (int i = 0; i < pendingHireLength; i++)
923  {
924  pendingHires.Add(msg.ReadUInt16());
925  }
926 
927  ushort hiredLength = msg.ReadUInt16();
928  List<CharacterInfo> hiredCharacters = new List<CharacterInfo>();
929  for (int i = 0; i < hiredLength; i++)
930  {
932  hired.Salary = msg.ReadInt32();
933  hiredCharacters.Add(hired);
934  }
935 
936  bool renameCrewMember = msg.ReadBoolean();
937  if (renameCrewMember)
938  {
939  UInt16 renamedIdentifier = msg.ReadUInt16();
940  string newName = msg.ReadString();
941  CharacterInfo renamedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == renamedIdentifier);
942  if (renamedCharacter != null)
943  {
944  CrewManager.RenameCharacter(renamedCharacter, newName);
945  // Since renaming can only be done once in permadeath, we can safely set this to false to disable the renaming in the UI.
946  renamedCharacter.RenamingEnabled = false;
947  }
948  else
949  {
950  DebugConsole.ThrowError($"Could not find a character to rename with the ID {renamedIdentifier}.");
951  }
952  }
953 
954  bool fireCharacter = msg.ReadBoolean();
955  if (fireCharacter)
956  {
957  UInt16 firedIdentifier = msg.ReadUInt16();
958  CharacterInfo firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == firedIdentifier);
959  // this one might and is allowed to be null since the character is already fired on the original sender's game
960  if (firedCharacter != null) { CrewManager.FireCharacter(firedCharacter); }
961  }
962 
963  if (map?.CurrentLocation?.HireManager != null && CampaignUI?.HRManagerUI != null)
964  {
965  //can't apply until we have the latest save file
966  if (!NetIdUtils.IdMoreRecent(pendingSaveID, LastSaveID))
967  {
968  CampaignUI.HRManagerUI.SetHireables(map.CurrentLocation, availableHires);
969  if (hiredCharacters.Any()) { CampaignUI.HRManagerUI.ValidateHires(hiredCharacters, takeMoney: false, createNotification: createNotification); }
970  CampaignUI.HRManagerUI.SetPendingHires(pendingHires, map.CurrentLocation);
971  if (renameCrewMember || fireCharacter) { CampaignUI.HRManagerUI.UpdateCrew(); }
972  }
973  }
974  else
975  {
976  //This is pretty nasty: setting hireables is handled through CrewManagement,
977  //which is part of the Campaign UI that might not exist when the client is still initializing the round.
978  //If that's the case, let's force the available hires here so they're available when the UI is created
979  CurrentLocation?.ForceHireableCharacters(availableHires);
980  }
981 
982  }
983 
984  public void ClientReadMoney(IReadMessage inc)
985  {
986  NetWalletUpdate update = INetSerializableStruct.Read<NetWalletUpdate>(inc);
987  foreach (NetWalletTransaction transaction in update.Transactions)
988  {
989  WalletInfo info = transaction.Info;
990  if (transaction.CharacterID.TryUnwrap(out var charID))
991  {
992  Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID);
993  if (targetCharacter is null) { break; }
994  Wallet wallet = targetCharacter.Wallet;
995 
996  wallet.Balance = info.Balance;
997  wallet.RewardDistribution = info.RewardDistribution;
998  TryInvokeEvent(wallet, transaction.ChangedData, info);
999  }
1000  else
1001  {
1002  Bank.Balance = info.Balance;
1003  Bank.RewardDistribution = info.RewardDistribution;
1004  TryInvokeEvent(Bank, transaction.ChangedData, info);
1005  }
1006  }
1007 
1008  void TryInvokeEvent(Wallet wallet, WalletChangedData data, WalletInfo info)
1009  {
1010  if (data.BalanceChanged.IsSome() || data.RewardDistributionChanged.IsSome())
1011  {
1012  OnMoneyChanged.Invoke(new WalletChangedEvent(wallet, data, info));
1013  }
1014  }
1015  }
1016 
1017  public override bool TryPurchase(Client client, int price)
1018  {
1019  if (price == 0)
1020  {
1021  return true;
1022  }
1023 
1024  if (!AllowedToManageCampaign(ClientPermissions.ManageMoney))
1025  {
1026  return PersonalWallet.TryDeduct(price);
1027  }
1028 
1029  int balance = PersonalWallet.Balance;
1030 
1031  if (balance >= price)
1032  {
1033  return PersonalWallet.TryDeduct(price);
1034  }
1035 
1036  if (balance + Bank.Balance >= price)
1037  {
1038  int remainder = price - balance;
1039  if (balance > 0) { PersonalWallet.Deduct(balance); }
1040  Bank.Deduct(remainder);
1041  return true ;
1042  }
1043 
1044  return false;
1045  }
1046 
1047  public override void Save(XElement element)
1048  {
1049  //do nothing, the clients get the save files from the server
1050  }
1051 
1052  public void LoadState(string filePath)
1053  {
1054  DebugConsole.Log($"Loading save file for an existing game session ({filePath})");
1055  SaveUtil.DecompressToDirectory(filePath, SaveUtil.TempPath);
1056 
1057  string gamesessionDocPath = Path.Combine(SaveUtil.TempPath, SaveUtil.GameSessionFileName);
1058  XDocument doc = XMLExtensions.TryLoadXml(gamesessionDocPath);
1059  if (doc == null)
1060  {
1061  DebugConsole.ThrowError($"Failed to load the state of a multiplayer campaign. Could not open the file \"{gamesessionDocPath}\".");
1062  return;
1063  }
1064  Load(doc.Root.Element("MultiPlayerCampaign"));
1065  GameMain.GameSession.OwnedSubmarines = SaveUtil.LoadOwnedSubmarines(doc, out SubmarineInfo selectedSub);
1066  GameMain.GameSession.SubmarineInfo = selectedSub;
1067  }
1068  }
1069 }
void TryEndRoundWithFuelCheck(Action onConfirm, Action onReturnToMapScreen)
Action< SubmarineInfo, string, string, CampaignSettings > StartNewGame
CampaignMode.InteractionType SelectedTab
Definition: CampaignUI.cs:18
void SelectTab(CampaignMode.InteractionType tab, Character npc=null)
Definition: CampaignUI.cs:577
HRManagerUI HRManagerUI
Definition: CampaignUI.cs:42
Dictionary< Identifier, List< PurchasedItem > > PurchasedItems
Dictionary< Identifier, List< PurchasedItem > > ItemsInSellFromSubCrate
Dictionary< Identifier, List< PurchasedItem > > ItemsInBuyCrate
Dictionary< Identifier, List< SoldItem > > SoldItems
Stores information about the Character that is needed between rounds in the menu etc....
static CharacterInfo ClientRead(Identifier speciesName, IReadMessage inc, bool requireJobPrefabFound=true)
static readonly Identifier HumanSpeciesName
static CoroutineStatus Running
static CoroutineStatus Success
Responsible for keeping track of the characters in the player crew, saving and loading their orders,...
IEnumerable< CharacterInfo > GetCharacterInfos()
Note: this only returns AI characters' infos in multiplayer. The infos are used to manage hiring/firi...
void RenameCharacter(CharacterInfo characterInfo, string newName)
static Entity FindEntityByID(ushort ID)
Find an entity based on the ID
Definition: Entity.cs:204
Reputation Reputation
Definition: Factions.cs:17
OnClickedHandler OnClicked
Definition: GUIButton.cs:16
override bool Enabled
Definition: GUIButton.cs:27
GUIComponent GetChildByUserData(object obj)
Definition: GUIComponent.cs:66
virtual void ClearChildren()
int GetChildIndex(GUIComponent child)
Definition: GUIComponent.cs:60
void UpdateManually(float deltaTime, bool alsoChildren=false, bool recursive=true)
By default, all the gui elements are updated automatically in the same order they appear on the updat...
virtual Rectangle Rect
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
Definition: GUIListBox.cs:33
static readonly List< GUIComponent > MessageBoxes
static GUIComponent VisibleBox
static void AutoScaleAndNormalize(params GUITextBlock[] textBlocks)
Set the text scale of the GUITextBlocks so that they all use the same scale and can fit the text with...
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetLobbyScreen NetLobbyScreen
Definition: GameMain.cs:55
static CampaignEndScreen CampaignEndScreen
Definition: GameMain.cs:76
static GameClient Client
Definition: GameMain.cs:188
static GameMain Instance
Definition: GameMain.cs:144
static GameModePreset MultiPlayerCampaign
The "HR manager" UI, which is used to hire/fire characters and rename crewmates.
Definition: HRManagerUI.cs:15
void SetPendingHires(List< UInt16 > characterInfos, Location location)
bool ValidateHires(List< CharacterInfo > hires, bool takeMoney=true, bool createNetworkEvent=false, bool createNotification=true)
Definition: HRManagerUI.cs:788
void SetHireables(Location location, List< CharacterInfo > availableHires)
Definition: HRManagerUI.cs:316
static readonly List< Item > ItemList
static ItemPrefab Find(string name, Identifier identifier)
static readonly PrefabCollection< MissionPrefab > Prefabs
override IEnumerable< CoroutineStatus > DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror)
override void End(TransitionType transitionType=TransitionType.None)
override void LoadInitialLevel()
Load the first level and start the round after loading a save file
void SetCampaignCharacterInfo(CharacterInfo newCampaignCharacterInfo)
bool CheckIfCampaignSubMatches(SubmarineInfo serverSubmarine, SubmarineDeliveryData deliveryData)
static readonly TimeSpan LevelTransitionTimeOut
Definition: GameClient.cs:20
readonly List< SubmarineInfo > ServerSubmarines
Definition: GameClient.cs:94
void SetupNewCampaign(SubmarineInfo sub, string saveName, string mapSeed, CampaignSettings settings)
Definition: GameClient.cs:2899
void SetupLoadCampaign(string saveName)
Definition: GameClient.cs:2919
void RequestStartRound(bool continueCampaign=false)
Tell the server to start the round (permission required)
Definition: GameClient.cs:2854
void SetReputation(float newReputation)
Definition: Reputation.cs:68
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
This class handles all upgrade logic. Storing, applying, checking and validation of upgrades.
readonly List< PurchasedUpgrade > PurchasedUpgrades
This is used by the client to notify the server which upgrades are yet to be paid for.