Client LuaCsForBarotrauma
ModDownloadScreen.cs
1 #nullable enable
2 using System;
3 using System.Collections.Generic;
4 using System.Diagnostics.CodeAnalysis;
5 using System.Linq;
7 using Barotrauma.IO;
9 using Barotrauma.Steam;
10 using Microsoft.Xna.Framework;
11 using Microsoft.Xna.Framework.Graphics;
12 using Color = Microsoft.Xna.Framework.Color;
14 
15 namespace Barotrauma
16 {
18  {
19  private readonly Queue<ServerContentPackage> pendingDownloads =
20  new Queue<ServerContentPackage>();
21  private ServerContentPackage? currentDownload;
22 
23  private readonly List<ContentPackage> downloadedPackages = new List<ContentPackage>();
24  public IEnumerable<ContentPackage> DownloadedPackages => downloadedPackages;
25 
26  private bool confirmDownload;
27 
28  public void Reset()
29  {
30  pendingDownloads.Clear();
31  downloadedPackages.Clear();
32  currentDownload = null;
33  confirmDownload = false;
34  }
35 
36  private void DeletePrevDownloads()
37  {
38  if (Directory.Exists(ModReceiver.DownloadFolder))
39  {
40  Directory.Delete(ModReceiver.DownloadFolder, recursive: true);
41  }
42  }
43 
44  [DoesNotReturn]
45  private static void LogAndThrowException(string errorMsg, string analyticsId)
46  {
47  GameAnalyticsManager.AddErrorEventOnce(analyticsId, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
48  throw new InvalidOperationException(errorMsg);
49  }
50 
51  public override void Select()
52  {
53  base.Select();
54  DeletePrevDownloads();
55  Reset();
56 
57  bool allowDownloads = GameMain.Client.ClientPeer is { AllowModDownloads: true };
58 
60 
61  var mainVisibleFrame = new GUIFrame(new RectTransform((0.6f, 0.8f), Frame.RectTransform, Anchor.Center));
62  GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.93f, mainVisibleFrame.RectTransform, Anchor.Center));
63 
64  void mainLayoutSpacing()
65  => new GUIFrame(new RectTransform((1.0f, 0.02f), mainLayout.RectTransform), style: null);
66 
67  var serverName = new GUITextBlock(new RectTransform((1.0f, 0.08f), mainLayout.RectTransform),
68  "", font: GUIStyle.LargeFont,
69  textAlignment: Alignment.CenterLeft)
70  {
71  TextGetter = () => GameMain.Client.ServerName
72  };
73  mainLayoutSpacing();
74  var downloadList = new GUIListBox(new RectTransform((1.0f, 0.76f), mainLayout.RectTransform));
75  mainLayoutSpacing();
76  var disconnectButton = new GUIButton(new RectTransform((0.3f, 0.1f), mainLayout.RectTransform),
77  TextManager.Get("Disconnect"))
78  {
79  OnClicked = (guiButton, o) =>
80  {
81  GameMain.Client?.Quit();
83  return false;
84  }
85  };
86 
87  if (!GameMain.Client.IsServerOwner && GameMain.Client.ClientPeer.ServerContentPackages.Length == 0)
88  {
89  LogAndThrowException("Error in ModDownloadScreen: the list of mods the server has enabled was empty. "
90  +$"Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}",
91  analyticsId: "ModDownloadScreen.Select:NoContentPackages");
92  }
93 
94  var missingPackages = GameMain.Client.ClientPeer.ServerContentPackages
95  .Where(sp => sp.ContentPackage is null).ToArray();
96  if (!missingPackages.Any(p => p.IsMandatory))
97  {
99  {
100  var corePackage = GameMain.Client.ClientPeer.ServerContentPackages
101  .Select(p => p.CorePackage)
102  .OfType<CorePackage>().FirstOrDefault();
103  if (corePackage is null)
104  {
105  LogAndThrowException($"Error in ModDownloadScreen: no core packages in the list of mods the server has enabled. " +
106  $"Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}",
107  analyticsId: "ModDownloadScreen.Select:NoCorePackage");
108  }
109 
110  ContentPackageManager.EnabledPackages.BackUp();
111  ContentPackageManager.EnabledPackages.SetCore(corePackage);
112  List<RegularPackage> regularPackages =
113  GameMain.Client.ClientPeer.ServerContentPackages
114  .Select(p => p.RegularPackage)
115  .OfType<RegularPackage>().ToList();
116  //keep enabled client-side-only mods enabled
117  regularPackages.AddRange(ContentPackageManager.EnabledPackages.Regular.Where(p => !p.HasMultiplayerSyncedContent && !regularPackages.Contains(p)));
118  ContentPackageManager.EnabledPackages.SetRegular(regularPackages);
119  }
122  return;
123  }
124 
125  if (missingPackages.FirstOrDefault(p => p.IsVanilla) is { } mismatchedVanilla)
126  {
127  LogAndThrowException("Error in ModDownloadScreen: mismatched Vanilla package: "
128  +$"local hash is {ContentPackageManager.VanillaCorePackage?.Hash.StringRepresentation ?? "[NULL]"}, "
129  +$"remote hash is {mismatchedVanilla.Hash.StringRepresentation}. "
130  +$"Content package list received: {GameMain.Client.ClientPeer.ContentPackageOrderReceived}",
131  analyticsId: "ModDownloadScreen.Select:MismatchedVanilla");
132  }
133 
134  GUIMessageBox msgBox = new GUIMessageBox(
135  TextManager.Get("ModDownloadTitle"),
136  "",
137  Array.Empty<LocalizedString>(),
138  relativeSize: (0.5f, 0.75f));
139 
140  GUILayoutGroup innerLayout = msgBox.Content;
141  innerLayout.Stretch = true;
142 
143  void innerLayoutSpacing(float height)
144  => new GUIFrame(new RectTransform((1.0f, height), innerLayout.RectTransform), style: null);
145 
146  GUITextBlock textBlock(LocalizedString str, GUIFont font, Alignment alignment = Alignment.CenterLeft)
147  {
148  var tb = new GUITextBlock(new RectTransform(Point.Zero, innerLayout.RectTransform), str,
149  wrap: true, textAlignment: alignment, font: font);
150  new GUICustomComponent(new RectTransform(Vector2.Zero, tb.RectTransform), onUpdate:
151  (deltaTime, component) =>
152  {
153  if (tb.RectTransform.NonScaledSize.X != innerLayout.Rect.Width)
154  {
155  tb.RectTransform.NonScaledSize = (innerLayout.Rect.Width, 0);
156  tb.RectTransform.NonScaledSize = (innerLayout.Rect.Width,
157  (int)tb.Font.MeasureString(tb.WrappedText).Y);
158  }
159  });
160  return tb;
161  }
162 
163  var header = textBlock(TextManager.Get("ModDownloadHeader"), GUIStyle.Font);
164  innerLayoutSpacing(0.05f);
165 
166  var msgBoxModList = new GUIListBox(new RectTransform(Vector2.One, innerLayout.RectTransform));
167 
168  innerLayoutSpacing(0.05f);
169  var footer = textBlock(TextManager.Get(allowDownloads ? "ModDownloadFooter" : "ModDownloadFooterFail"), GUIStyle.Font, Alignment.Center);
170 
171  innerLayoutSpacing(0.05f);
172  GUILayoutGroup buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), innerLayout.RectTransform), isHorizontal: true);
173 
174  void buttonContainerSpacing(float width)
175  => new GUIFrame(new RectTransform((width, 1.0f), buttonContainer.RectTransform), style: null);
176 
177  void button(LocalizedString text, Action action, float width = 0.3f)
178  => new GUIButton(new RectTransform((width, 1.0f), buttonContainer.RectTransform), text)
179  {
180  OnClicked = (_, __) =>
181  {
182  action();
183  msgBox.Close();
184  return false;
185  }
186  };
187 
188  if (allowDownloads)
189  {
190  buttonContainerSpacing(0.1f);
191  button(TextManager.Get("Yes"), () => confirmDownload = true);
192  buttonContainerSpacing(0.2f);
193  button(TextManager.Get("No"), () =>
194  {
195  GameMain.Client?.Quit();
196  GameMain.MainMenuScreen.Select();
197  });
198  buttonContainerSpacing(0.1f);
199  }
200  else
201  {
202  buttonContainerSpacing(0.15f);
203  button(TextManager.Get("Cancel"), () =>
204  {
205  GameMain.Client?.Quit();
206  GameMain.MainMenuScreen.Select();
207  }, width: 0.7f);
208  buttonContainerSpacing(0.15f);
209  }
210 
211  var missingIds = missingPackages
212  .Where(p => p.IsMandatory)
213  .Select(mp => ContentPackageId.Parse(mp.UgcId))
214  .NotNone()
215  .Where(id => ContentPackageManager.WorkshopPackages.All(wp => !wp.UgcId.Equals(id)))
216  .ToArray();
217  if (missingIds.Any() && SteamManager.IsInitialized)
218  {
219  buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), innerLayout.RectTransform), isHorizontal: true);
220  buttonContainerSpacing(0.15f);
221  button(TextManager.Get("SubscribeToAllOnWorkshop"), () =>
222  {
223  if (GameMain.Client != null)
224  {
225  BulkDownloader.SubscribeToServerMods(missingIds.OfType<SteamWorkshopId>().Select(id => id.Value),
226  new ConnectCommand(
227  serverName: GameMain.Client.ServerName,
228  endpoint: GameMain.Client.ClientPeer.ServerEndpoint));
229  GameMain.Client.Quit();
230  }
231  GameMain.MainMenuScreen.Select();
232  }, width: 0.7f);
233  buttonContainerSpacing(0.15f);
234  }
235 
236  foreach (var p in missingPackages.Where(p => p.IsMandatory))
237  {
238  pendingDownloads.Enqueue(p);
239 
240  //Message box frame
241  new GUITextBlock(new RectTransform((1.0f, 0.1f), msgBoxModList.Content.RectTransform), p.Name)
242  {
243  CanBeFocused = false
244  };
245 
246  //Download progress frame
247  var downloadFrame = new GUIFrame(new RectTransform((1.0f, 0.06f), downloadList.Content.RectTransform),
248  style: "ListBoxElement")
249  {
250  UserData = p,
251  CanBeFocused = false
252  };
253  new GUITextBlock(new RectTransform((0.5f, 1.0f), downloadFrame.RectTransform), p.Name)
254  {
255  CanBeFocused = false
256  };
257  var downloadProgress = new GUIProgressBar(
258  new RectTransform((0.5f, 0.75f), downloadFrame.RectTransform, Anchor.CenterRight),
259  0.0f, color: GUIStyle.Green);
260  downloadProgress.ProgressGetter = () =>
261  {
262  if (currentDownload == p)
263  {
264  FileReceiver.FileTransferIn? getTransfer() => GameMain.Client?.FileReceiver.ActiveTransfers.FirstOrDefault(t => t.FileType == FileTransferType.Mod);
265 
266  if (downloadProgress.GetAnyChild<GUITextBlock>() is null)
267  {
268  GUILayoutGroup progressBarLayout
269  = new GUILayoutGroup(new RectTransform(Vector2.One, downloadProgress.RectTransform), isHorizontal: true);
270 
271  void progressBarText(float width, Alignment textAlignment, Func<string> getter)
272  {
273  var textContainer = new GUIFrame(new RectTransform((width, 1.0f), progressBarLayout.RectTransform),
274  style: null);
275  var textShadow = new GUITextBlock(new RectTransform(Vector2.One, textContainer.RectTransform) { AbsoluteOffset = new Point(GUI.IntScale(3)) }, "",
276  textColor: Color.Black, textAlignment: textAlignment);
277  var text = new GUITextBlock(new RectTransform(Vector2.One, textContainer.RectTransform), "",
278  textAlignment: textAlignment);
279  new GUICustomComponent(new RectTransform(Vector2.Zero, textContainer.RectTransform), onUpdate:
280  (f, component) =>
281  {
282  string str = getter();
283  if (text.Text?.SanitizedValue != str)
284  {
285  text.Text = str;
286  textShadow.Text = str;
287  }
288  });
289  }
290  progressBarText(0.475f, Alignment.CenterRight, () => MathUtils.GetBytesReadable(getTransfer()?.Received ?? 0));
291  progressBarText(0.05f, Alignment.Center, () => "/");
292  progressBarText(0.475f, Alignment.CenterLeft, () => MathUtils.GetBytesReadable(getTransfer()?.FileSize ?? 0));
293  }
294 
295  return getTransfer()?.Progress ?? 0.0f;
296  }
297 
298  if (!pendingDownloads.Contains(p))
299  {
300  downloadProgress.GetAllChildren<GUITextBlock>().ToArray().ForEach(c => downloadProgress.RemoveChild(c));
301  return 1.0f;
302  }
303 
304  return 0.0f;
305  };
306  }
307  }
308 
309  public override void Update(double deltaTime)
310  {
311  base.Update(deltaTime);
312  if (GameMain.Client is null) { return; }
313  if (!confirmDownload) { return; }
314  if (currentDownload is null)
315  {
316  if (pendingDownloads.TryDequeue(out currentDownload))
317  {
318  GameMain.Client.RequestFile(FileTransferType.Mod, currentDownload.Name, currentDownload.Hash.StringRepresentation);
319  }
320  else
321  {
322  var serverPackages = GameMain.Client.ClientPeer.ServerContentPackages;
323  CorePackage corePackage
324  = downloadedPackages.FirstOrDefault(p => p is CorePackage) as CorePackage
325  ?? serverPackages.FirstOrDefault(p => p.CorePackage != null)?.CorePackage
326  ?? throw new Exception($"Failed to find core package to enable");
327 
328  List<RegularPackage> regularPackages = new List<RegularPackage>();
329  foreach (var p in serverPackages)
330  {
331  if (p.CorePackage != null)
332  {
333  // This package is one of our installed core packages
334  continue;
335  }
336 
337  if (corePackage.Hash.Equals(p.Hash))
338  {
339  // This package is the core package we downloaded from the server
340  continue;
341  }
342  RegularPackage? matchingPackage =
343  p.RegularPackage ?? downloadedPackages.FirstOrDefault(d => d is RegularPackage && d.Hash.Equals(p.Hash)) as RegularPackage;
344  if (matchingPackage is null)
345  {
346  if (!p.IsMandatory)
347  {
348  //we don't need to care about missing non-mandatory (= submarine) mods
349  continue;
350  }
351  else
352  {
353  throw new Exception($"Could not find regular package \"{p.Name}\"");
354  }
355  }
356  regularPackages.Add(matchingPackage);
357  }
358  foreach (var regularPackage in regularPackages)
359  {
360  DebugConsole.NewMessage($"Enabling \"{regularPackage.Name}\" ({regularPackage.Dir})", Color.Lime);
361  }
362 
363  //keep enabled client-side-only mods enabled
364  regularPackages.AddRange(ContentPackageManager.EnabledPackages.Regular.Where(p => !p.HasMultiplayerSyncedContent && !regularPackages.Contains(p)));
365 
366  ContentPackageManager.EnabledPackages.BackUp();
367  ContentPackageManager.EnabledPackages.SetCore(corePackage);
368  ContentPackageManager.EnabledPackages.SetRegular(regularPackages);
369 
370  //see if any of the packages we enabled contain subs that we were missing previously, and update their paths
371  foreach (var serverSub in GameMain.Client.ServerSubmarines)
372  {
373  if (File.Exists(serverSub.FilePath)) { continue; }
374  var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSub.Name && s.MD5Hash == serverSub.MD5Hash);
375  if (matchingSub != null)
376  {
377  serverSub.FilePath = matchingSub.FilePath;
378  }
379  }
383  }
384  }
385  }
386 
388  {
389  if (currentDownload is null) { throw new Exception("Current download is null"); }
390 
391  string path = transfer.FilePath;
392  if (!path.EndsWith(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase))
393  {
394  return;
395  }
396  string dir = path.RemoveFromEnd(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase);
397 
398  SaveUtil.DecompressToDirectory(path, dir);
399  var result = ContentPackage.TryLoad(Path.Combine(dir, ContentPackage.FileListFileName));
400 
401  if (!result.TryUnwrapSuccess(out var newPackage))
402  {
403  throw new Exception($"Failed to load downloaded mod \"{currentDownload.Name}\"",
404  result.TryUnwrapFailure(out var exception) ? exception : null);
405  }
406  if (!currentDownload.Hash.Equals(newPackage.Hash))
407  {
408  throw new Exception($"Hash mismatch for downloaded mod \"{currentDownload.Name}\" (expected {currentDownload.Hash}, got {newPackage.Hash})");
409  }
410  downloadedPackages.Add(newPackage);
411 
412  currentDownload = null;
413 
414  }
415 
416  public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
417  {
418  spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable);
419  GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch);
420  GUI.Draw(Cam, spriteBatch);
421  spriteBatch.End();
422  }
423  }
424 }
static Result< ContentPackage, Exception > TryLoad(string path)
CorePackage(XDocument doc, string path)
Definition: CorePackage.cs:33
virtual void ClearChildren()
RectTransform RectTransform
GUIComponent that can be used to render custom content on the UI
bool Stretch
Note that stretching cannot be undone, because the previous child sizes are not stored.
GUILayoutGroup Content
static NetLobbyScreen NetLobbyScreen
Definition: GameMain.cs:55
static RasterizerState ScissorTestEnable
Definition: GameMain.cs:195
static MainMenuScreen MainMenuScreen
Definition: GameMain.cs:53
static GameClient Client
Definition: GameMain.cs:188
static LuaCsSetup LuaCs
Definition: GameMain.cs:26
void DrawBackground(GraphicsDevice graphics, SpriteBatch spriteBatch)
override bool Equals(object? obj)
Definition: Md5Hash.cs:147
IEnumerable< ContentPackage > DownloadedPackages
void CurrentDownloadFinished(FileReceiver.FileTransferIn transfer)
override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
override void Update(double deltaTime)
void UpdateSubList(GUIComponent subList, IEnumerable< SubmarineInfo > submarines)
void RequestFile(FileTransferType fileType, string file, string fileHash)
Definition: GameClient.cs:2415
readonly List< SubmarineInfo > ServerSubmarines
Definition: GameClient.cs:94
RegularPackage(XDocument doc, string path)
static IEnumerable< SubmarineInfo > SavedSubmarines