Client LuaCsForBarotrauma
LegacySteamUgcTransition.cs
1 #nullable enable
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
5 using System.Text;
6 using System.Threading.Tasks;
7 using System.Xml.Linq;
9 using Barotrauma.Steam;
10 using Microsoft.Xna.Framework;
11 using Barotrauma.IO;
12 
14 {
19  public static class LegacySteamUgcTransition
20  {
21  private const string readmeName = "LOCALMODS_README.txt";
22 
23  private enum ModsListChildType
24  {
25  Header,
26  Entry
27  }
28 
29  public static void Prepare()
30  {
31  TaskPool.Add("UgcTransition.Prepare", DetermineItemsToTransition(), t =>
32  {
33  if (!t.TryGetResult(out (OldSubs, OldItemAssemblies, OldMods) result)) { return; }
34  var (subs, itemAssemblies, mods) = result;
35  if (!subs.FilePaths.Any() && !itemAssemblies.FilePaths.Any() && !mods.Mods.Any()) { return; }
36 
37  var msgBox = new GUIMessageBox(TextManager.Get("Ugc.TransferTitle"), "", relativeSize: (0.5f, 0.8f),
38  buttons: new LocalizedString[] { TextManager.Get("Ugc.TransferButton") });
39 
40  var closeBtn = new GUIButton(
41  new RectTransform(Vector2.One * 1.5f, msgBox.Header.RectTransform, anchor: Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight),
42  style: "GUICancelButton")
43  {
44  OnClicked = (button, o) =>
45  {
46  msgBox.Close();
47  return false;
48  }
49  };
50 
51  var desc = new GUITextBlock(new RectTransform((1.0f, 0.24f), msgBox.Content.RectTransform),
52  text: TextManager.Get("Ugc.TransferDesc"), wrap: true, textAlignment: Alignment.CenterLeft);
53 
54  var modsList = new GUIListBox(new RectTransform((1.0f, 0.6f), msgBox.Content.RectTransform))
55  {
56  HoverCursor = CursorState.Default
57  };
58  Dictionary<string, GUITickBox> pathTickboxMap = new Dictionary<string, GUITickBox>();
59 
60  void addHeader(LocalizedString str)
61  {
62  var itemFrame = new GUIFrame(new RectTransform((1.0f, 0.08f), modsList.Content.RectTransform),
63  style: null)
64  {
65  CanBeFocused = false,
66  UserData = ModsListChildType.Header
67  };
68  if (str is RawLString { Value: "" }) { return; }
69 
70  bool clicked = true;
71  var tickBox = new GUITickBox(new RectTransform(Vector2.One, itemFrame.RectTransform),
72  label: str, font: GUIStyle.SubHeadingFont)
73  {
74  Selected = false,
75  OnSelected = box =>
76  {
77  if (!clicked) { return true; }
78  bool toggleTickbox = false;
79  foreach (var child in modsList.Content.Children)
80  {
81  if (child == itemFrame) { toggleTickbox = true; }
82  else if (child.UserData is ModsListChildType.Header) { toggleTickbox = false; }
83  else if (toggleTickbox)
84  {
85  var tb = child.GetAnyChild<GUITickBox>();
86  if (tb is null) { continue; }
87 
88  tb.Selected = box.Selected;
89  }
90  }
91  return true;
92  }
93  };
94  new GUICustomComponent(new RectTransform(Vector2.Zero, itemFrame.RectTransform),
95  onUpdate: (f, component) =>
96  {
97  clicked = false;
98  bool shouldBeSelected = true;
99  bool toggleTickbox = false;
100  foreach (var child in modsList.Content.Children)
101  {
102  if (child == itemFrame) { toggleTickbox = true; }
103  else if (child.UserData is ModsListChildType.Header) { toggleTickbox = false; }
104  else if (toggleTickbox)
105  {
106  var tb = child.GetAnyChild<GUITickBox>();
107  if (tb is null) { continue; }
108 
109  if (!tb.Selected)
110  {
111  shouldBeSelected = false;
112  break;
113  }
114  }
115  }
116  tickBox.Selected = shouldBeSelected;
117  clicked = true;
118  });
119  }
120  void addTickbox(string dir, string name, bool ticked)
121  {
122  var itemFrame = new GUIFrame(new RectTransform((1.0f, 0.07f), modsList.Content.RectTransform),
123  style: null)
124  {
125  CanBeFocused = false,
126  UserData = ModsListChildType.Entry
127  };
128  var tickbox = new GUITickBox(new RectTransform((0.97f, 1.0f), itemFrame.RectTransform, Anchor.CenterRight), name)
129  {
130  Selected = ticked
131  };
132  pathTickboxMap.Add(dir, tickbox);
133  }
134 
135  bool firstHeader = true;
136 
137  void addSpacer()
138  {
139  if (firstHeader) { firstHeader = false; return; }
140  addHeader("");
141  }
142 
143  if (subs.FilePaths.Any())
144  {
145  addSpacer();
146  addHeader(TextManager.Get("WorkshopLabelSubmarines"));
147  foreach (var sub in subs.FilePaths)
148  {
149  var subName = Path.GetFileNameWithoutExtension(sub);
150  addTickbox(sub, subName, ticked: !ContentPackageManager.LocalPackages.Any(p => p.NameMatches(subName)));
151  }
152  }
153 
154  if (itemAssemblies.FilePaths.Any())
155  {
156  addSpacer();
157  addHeader(TextManager.Get("ItemAssemblies"));
158  foreach (var itemAssembly in itemAssemblies.FilePaths)
159  {
160  var assemblyName = Path.GetFileNameWithoutExtension(itemAssembly);
161  addTickbox(itemAssembly, assemblyName, ticked: !ContentPackageManager.LocalPackages.Any(p => p.NameMatches(assemblyName)));
162  }
163  }
164 
165  if (mods.Mods.Any())
166  {
167  addSpacer();
168  addHeader(TextManager.Get("SubscribedMods"));
169  foreach (var mod in mods.Mods)
170  {
171  addTickbox(mod.Dir, mod.Name,
172  ticked: !(mod.Item is { } item && ContentPackageManager.LocalPackages.Any(p =>
173  p.UgcId.TryUnwrap(out var ugcId)
174  && ugcId is SteamWorkshopId workshopId
175  && workshopId.Value == item.Id)));
176  }
177  }
178 
179  GUIMessageBox? subMsgBox = null;
180 
181  void createSubMsgBox(LocalizedString str, bool closable)
182  {
183  subMsgBox?.Close();
184  subMsgBox = new GUIMessageBox(headerText: "", text: str,
185  buttons: closable ? new[] { TextManager.Get("Close") } : Array.Empty<LocalizedString>());
186  if (closable)
187  {
188  subMsgBox.Buttons[0].OnClicked = subMsgBox.Close;
189  }
190  }
191 
192  msgBox.Buttons[0].OnClicked = (b, o) =>
193  {
194  TaskPool.Add("TransferMods", TransferMods(pathTickboxMap), t2 =>
195  {
196  if (t2.Exception != null)
197  {
198  DebugConsole.ThrowError("There was an error transferring mods", t2.Exception.GetInnermost());
199  }
200  ContentPackageManager.LocalPackages.Refresh();
201  if (t2.TryGetResult(out string[]? modsToEnable))
202  {
203  var newRegular = ContentPackageManager.EnabledPackages.Regular.ToList();
204  newRegular.AddRange(ContentPackageManager.LocalPackages.Regular
205  .Where(r => modsToEnable.Contains(r.Dir.CleanUpPathCrossPlatform(correctFilenameCase: false))));
206  newRegular = newRegular.Distinct().ToList();
207  ContentPackageManager.EnabledPackages.SetRegular(newRegular);
208  }
209  createSubMsgBox(TextManager.Get("Ugc.TransferComplete"), closable: true);
210  });
211  msgBox.Close();
212  createSubMsgBox(TextManager.Get("Ugc.Transferring"), closable: false);
213  return false;
214  };
215  });
216  }
217 
218  private struct OldSubs
219  {
220  public readonly IReadOnlyList<string> FilePaths;
221 
222  public OldSubs(IReadOnlyList<string> filePaths)
223  {
224  FilePaths = filePaths;
225  }
226  }
227 
228  private struct OldItemAssemblies
229  {
230  public readonly IReadOnlyList<string> FilePaths;
231 
232  public OldItemAssemblies(IReadOnlyList<string> filePaths)
233  {
234  FilePaths = filePaths;
235  }
236  }
237 
238  private struct OldMods
239  {
240  public readonly IReadOnlyList<(string Dir, string Name, Steamworks.Ugc.Item? Item, DateTime InstallTime)> Mods;
241 
242  public OldMods(IReadOnlyList<(string Dir, string Name, Steamworks.Ugc.Item? Item, DateTime InstallTime)> mods)
243  {
244  Mods = mods;
245  }
246  }
247 
248  private const string oldSubsPath = "Submarines";
249  private const string oldModsPath = "Mods";
250  private const string oldItemAssembliesPath = "ItemAssemblies";
251 
252  private static async Task<(OldSubs Subs, OldItemAssemblies ItemAssemblies, OldMods Mods)> DetermineItemsToTransition()
253  {
254  string[] subs = Array.Empty<string>();
255  string[] itemAssemblies = Array.Empty<string>();
256  List<(string Dir, string Name, Steamworks.Ugc.Item? Item, DateTime InstallTime)> mods
257  = new List<(string Dir, string Name, Steamworks.Ugc.Item? Item, DateTime InstallTime)>();
258  if (FolderShouldBeTransitioned(oldModsPath))
259  {
260  string[] getFiles(string path, string pattern)
261  => Directory.Exists(path)
262  ? Directory.GetFiles(path, pattern, System.IO.SearchOption.TopDirectoryOnly)
263  : Array.Empty<string>();
264 
265  subs = getFiles(oldSubsPath, "*.sub");
266  itemAssemblies = getFiles(oldItemAssembliesPath, "*.xml");
267 
268  string[] allOldMods = Directory.GetDirectories(oldModsPath, "*", System.IO.SearchOption.TopDirectoryOnly);
269 
270  var publishedItems = await SteamManager.Workshop.GetPublishedItems();
271  foreach (var modDir in allOldMods)
272  {
273  var fileList = XMLExtensions.TryLoadXml(Path.Combine(modDir, ContentPackage.FileListFileName), out _);
274  if (fileList?.Root is null) { continue; }
275 
276  var oldId = fileList.Root.GetAttributeUInt64("steamworkshopid", 0);
277  var updateTime = File.GetLastWriteTime(modDir).ToUniversalTime();
278  var oldName = fileList.Root.GetAttributeString("name", "");
279 
280  var item = oldId != 0 ? publishedItems.FirstOrNull(it => it.Id == oldId) : null;
281  if (oldId == 0 || item.HasValue)
282  {
283  mods.Add((modDir, oldName, item, updateTime));
284  }
285  }
286  }
287 
288  while (!(Screen.Selected is MainMenuScreen)) { await Task.Delay(50); }
289 
290  return (new OldSubs(subs), new OldItemAssemblies(itemAssemblies), new OldMods(mods));
291  }
292 
293  private static bool FolderShouldBeTransitioned(string folderName)
294  {
295  return Directory.Exists(folderName)
296  && !File.Exists(Path.Combine(folderName, readmeName));
297  }
298 
299  private static async Task<string[]> TransferMods(Dictionary<string, GUITickBox> pathTickboxMap)
300  {
301  //WriteReadme(oldSubsPath); //can't do this because the old submarine discovery code is borked
302  WriteReadme(oldModsPath);
303  var modsToEnable = (await Task.WhenAll(pathTickboxMap.Select(TransferMod))).OfType<string>().ToArray();
304  return modsToEnable;
305  }
306 
307  private static Task<string?> TransferMod(KeyValuePair<string, GUITickBox> kvp)
308  => TransferMod(kvp.Key, kvp.Value);
309 
310  private static async Task<string?> TransferMod(string path, GUITickBox tickbox)
311  {
312  if (!tickbox.Selected) { return null; }
313  string dirName = Path.GetFileNameWithoutExtension(path);
314  string destPath = Path.Combine(ContentPackage.LocalModsDir, dirName);
315 
316  //find unique path to save in
317  for (int i = 0;;i++)
318  {
319  if (!Directory.Exists(destPath)) { break; }
320  destPath = Path.Combine(ContentPackage.LocalModsDir, $"{dirName}.{i}");
321  }
322 
323  bool isSub = path.StartsWith(oldSubsPath, StringComparison.OrdinalIgnoreCase);
324  bool isItemAssembly = path.StartsWith(oldItemAssembliesPath, StringComparison.OrdinalIgnoreCase);
325  if (isSub || isItemAssembly)
326  {
327  //copying a sub or item assembly: manually create filelist.xml
328  ModProject modProject = new ModProject
329  {
330  Name = dirName,
331  ModVersion = ContentPackage.DefaultModVersion
332  };
333 
334  Type fileType;
335  if (isSub)
336  {
337  fileType = typeof(SubmarineFile);
338  XDocument? doc = SubmarineInfo.OpenFile(path, out _);
339  if (doc?.Root != null)
340  {
341  SubmarineType subType = doc.Root.GetAttributeEnum("type", SubmarineType.Player);
342  fileType = SubEditorScreen.DetermineSubFileType(subType);
343  }
344  }
345  else
346  {
347  fileType = typeof(ItemAssemblyFile);
348  }
349 
350  modProject.AddFile(ModProject.File.FromPath(
351  Path.Combine(ContentPath.ModDirStr, $"{dirName}.{(isSub ? "sub" : "xml")}"),
352  fileType));
353 
354  Directory.CreateDirectory(destPath);
355  File.Copy(path, Path.Combine(destPath, $"{dirName}.{(isSub ? "sub" : "xml")}"));
356  modProject.Save(Path.Combine(destPath, ContentPackage.FileListFileName));
357 
358  return destPath.CleanUpPathCrossPlatform(correctFilenameCase: false);
359  }
360  else
361  {
362  //copying a mod: we have a neat method for that!
363  await SteamManager.Workshop.CopyDirectory(path, Path.GetFileName(path), path, destPath, SteamManager.Workshop.ShouldCorrectPaths.Yes);
364 
365  return null;
366  }
367  }
368 
369  private static void WriteReadme(string folderName)
370  {
371  if (!Directory.Exists(folderName)) { return; }
372  File.WriteAllText(path: Path.Combine(folderName, readmeName),
373  contents: "This folder is no longer used by Barotrauma;\n" +
374  "your mods and submarines should have been transferred\n" +
375  "to LocalMods. If they are not being found, delete this\n" +
376  "readme and relaunch the game.", encoding: Encoding.UTF8);
377  }
378  }
379 }
const string DefaultModVersion
const string ModDirStr
Definition: ContentPath.cs:14
GUIComponent that can be used to render custom content on the UI
List< GUIButton > Buttons
override bool Selected
Definition: GUITickBox.cs:18
static File FromPath(string path, Type type)
Prefer FromPath<T> when possible, this just exists for cases where the type can only be decided at ru...
void AddFile(File file)
Definition: ModProject.cs:102
void Save(string path)
Definition: ModProject.cs:172
static Type DetermineSubFileType(SubmarineType type)
CursorState
Definition: GUI.cs:40