Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/Steam/Workshop.cs
1 #nullable enable
2 using Barotrauma.IO;
3 using Microsoft.Xna.Framework.Graphics;
4 using RestSharp;
5 using System;
6 using System.Collections.Generic;
7 using System.Collections.Immutable;
8 using System.Linq;
9 using System.Threading;
10 using System.Threading.Tasks;
12 
13 namespace Barotrauma.Steam
14 {
15  static partial class SteamManager
16  {
17  public static partial class Workshop
18  {
19  public const int MaxThumbnailSize = 1024 * 1024;
20 
21  public static readonly ImmutableArray<Identifier> Tags = new []
22  {
23  "submarine",
24  "item",
25  "monster",
26  "art",
27  "mission",
28  "event set",
29  "total conversion",
30  "environment",
31  "item assembly",
32  "language",
33  }.ToIdentifiers().ToImmutableArray();
34 
35  public class ItemThumbnail : IDisposable
36  {
37  private struct RefCounter
38  {
39  internal bool Loading;
40  internal Texture2D? Texture;
41  internal int Count;
42  }
43  private readonly static Dictionary<UInt64, RefCounter> TextureRefs
44  = new Dictionary<ulong, RefCounter>();
45 
46  public UInt64 ItemId { get; private set; }
47  public Texture2D? Texture
48  {
49  get
50  {
51  lock (TextureRefs)
52  {
53  if (TextureRefs.TryGetValue(ItemId, out var refCounter))
54  {
55  return refCounter.Texture;
56  }
57  }
58  return null;
59  }
60  }
61 
62  public bool Loading
63  {
64  get
65  {
66  lock (TextureRefs)
67  {
68  if (TextureRefs.TryGetValue(ItemId, out var refCounter))
69  {
70  return refCounter.Loading;
71  }
72  }
73  return false;
74  }
75  }
76 
77  public ItemThumbnail(in Steamworks.Ugc.Item item, CancellationToken cancellationToken)
78  {
79  ItemId = item.Id;
80  lock (TextureRefs)
81  {
82  if (TextureRefs.TryGetValue(ItemId, out var refCounter))
83  {
84  TextureRefs[ItemId] = new RefCounter { Texture = refCounter.Texture, Count = refCounter.Count + 1, Loading = refCounter.Loading };
85  }
86  else
87  {
88  TextureRefs[ItemId] = new RefCounter { Texture = null, Count = 1, Loading = true };
89  TaskPool.Add($"Workshop thumbnail {item.Title}", GetTexture(item, cancellationToken), SaveTextureToRefCounter(item.Id));
90  }
91  }
92  }
93 
94  public void Dispose()
95  {
96  if (ItemId == 0) { return; }
97  lock (TextureRefs)
98  {
99  var refCounter = TextureRefs[ItemId];
100  TextureRefs[ItemId] = new RefCounter { Texture = refCounter.Texture, Count = refCounter.Count - 1 };
101  if (TextureRefs[ItemId].Count <= 0)
102  {
103  TextureRefs[ItemId].Texture?.Dispose();
104  TextureRefs.Remove(ItemId);
105  }
106  ItemId = 0;
107  }
108  }
109 
110  private static async Task<Texture2D?> GetTexture(Steamworks.Ugc.Item item, CancellationToken cancellationToken)
111  {
112  await Task.Yield();
113 
114  string? thumbnailUrl = item.PreviewImageUrl;
115  if (thumbnailUrl.IsNullOrWhiteSpace()) { return null; }
116  var client = new RestClient(thumbnailUrl);
117  var request = new RestRequest(".", Method.GET);
118  IRestResponse response = await client.ExecuteAsync(request, cancellationToken);
119  if (response is { StatusCode: System.Net.HttpStatusCode.OK, ResponseStatus: ResponseStatus.Completed })
120  {
121  using var dataStream = new System.IO.MemoryStream();
122  await dataStream.WriteAsync(response.RawBytes, cancellationToken);
123  dataStream.Seek(0, System.IO.SeekOrigin.Begin);
124  return TextureLoader.FromStream(dataStream, compress: false);
125  }
126  return null;
127  }
128 
129  private static Action<Task> SaveTextureToRefCounter(UInt64 itemId)
130  => (t) =>
131  {
132  if (t.IsCanceled) { return; }
133  Texture2D? texture = ((Task<Texture2D?>)t).Result;
134  lock (TextureRefs)
135  {
136  if (TextureRefs.TryGetValue(itemId, out var refCounter))
137  {
138  TextureRefs[itemId] = new RefCounter { Texture = texture, Count = refCounter.Count, Loading = false };
139  }
140  else if (texture != null)
141  {
142  texture.Dispose();
143  }
144  }
145  };
146 
147  public override int GetHashCode() => (int)ItemId;
148 
149  public override bool Equals(object? obj)
150  => obj is ItemThumbnail { ItemId: UInt64 otherId }
151  && otherId == ItemId;
152  }
153 
154  public const string PublishStagingDir = "WorkshopStaging";
155 
156  public static void DeletePublishStagingCopy()
157  {
158  if (Directory.Exists(PublishStagingDir)) { Directory.Delete(PublishStagingDir, recursive: true); }
159  }
160 
161  private static void RefreshLocalMods()
162  {
163  CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.LocalPackages.Refresh());
164  }
165 
166  public static async Task CreatePublishStagingCopy(string title, string modVersion, ContentPackage contentPackage)
167  {
168  await Task.Yield();
169 
170  if (!ContentPackageManager.LocalPackages.Contains(contentPackage))
171  {
172  throw new Exception("Expected local package");
173  }
174 
175  DeletePublishStagingCopy();
176  Directory.CreateDirectory(PublishStagingDir);
177  await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir, ShouldCorrectPaths.No);
178 
179  var stagingFileListPath = Path.Combine(PublishStagingDir, ContentPackage.FileListFileName);
180 
181  var result = ContentPackage.TryLoad(stagingFileListPath);
182  if (!result.TryUnwrapSuccess(out var tempPkg))
183  {
184  throw new Exception("Staging copy could not be loaded",
185  result.TryUnwrapFailure(out var exception) ? exception : null);
186  }
187 
188  //Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be
189  ModProject modProject = new ModProject(tempPkg)
190  {
191  ModVersion = modVersion,
192  Name = title,
193  ExpectedHash = tempPkg.CalculateHash(name: title, modVersion: modVersion)
194  };
195  modProject.Save(stagingFileListPath);
196  }
197 
198  public static async Task<Option<ContentPackage>> CreateLocalCopy(ContentPackage contentPackage)
199  {
200  await Task.Yield();
201 
202  if (!ContentPackageManager.WorkshopPackages.Contains(contentPackage))
203  {
204  throw new Exception("Expected Workshop package");
205  }
206 
207  if (!contentPackage.UgcId.TryUnwrap(out var ugcId) || !(ugcId is SteamWorkshopId workshopId))
208  {
209  throw new Exception($"Steam Workshop ID not set for {contentPackage.Name}");
210  }
211 
212  string sanitizedName = ToolBox.RemoveInvalidFileNameChars(contentPackage.Name).Trim();
213  if (sanitizedName.IsNullOrWhiteSpace())
214  {
215  throw new Exception($"Sanitized name for {contentPackage.Name} is empty");
216  }
217 
218  string newPath = $"{ContentPackage.LocalModsDir}/{sanitizedName}";
219  if (File.Exists(newPath) || Directory.Exists(newPath))
220  {
221  newPath += $"_{workshopId.Value}";
222  }
223 
224  if (File.Exists(newPath) || Directory.Exists(newPath))
225  {
226  throw new Exception($"{newPath} already exists");
227  }
228 
229  await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, newPath, ShouldCorrectPaths.Yes);
230 
231  ModProject modProject = new ModProject(contentPackage);
232  modProject.DiscardHashAndInstallTime();
233  modProject.Save(Path.Combine(newPath, ContentPackage.FileListFileName));
234 
235  RefreshLocalMods();
236 
237  return ContentPackageManager.LocalPackages.FirstOrNone(p => p.UgcId == contentPackage.UgcId);
238  }
239 
240  private struct InstallWaiter
241  {
242  private static readonly HashSet<ulong> waitingIds = new HashSet<ulong>();
243  public ulong Id { get; private set; }
244 
245  public InstallWaiter(ulong id)
246  {
247  Id = id;
248  lock (waitingIds) { waitingIds.Add(Id); }
249  }
250 
251  public bool Waiting
252  {
253  get
254  {
255  if (Id == 0) { return false; }
256 
257  lock (waitingIds)
258  {
259  return waitingIds.Contains(Id);
260  }
261  }
262  }
263 
264  public static void StopWaiting(ulong id)
265  {
266  lock (waitingIds)
267  {
268  waitingIds.Remove(id);
269  }
270  }
271  }
272 
273  public static async Task Reinstall(Steamworks.Ugc.Item workshopItem)
274  {
275  NukeDownload(workshopItem);
276  var toUninstall
277  = ContentPackageManager.WorkshopPackages.Where(p =>
278  p.UgcId.TryUnwrap(out var ugcId)
279  && ugcId is SteamWorkshopId workshopId
280  && workshopId.Value == workshopItem.Id)
281  .ToHashSet();
282  toUninstall.Select(p => p.Dir).ForEach(d => Directory.Delete(d));
283  CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.WorkshopPackages.Refresh());
284  var installWaiter = WaitForInstall(workshopItem);
285  DownloadModThenEnqueueInstall(workshopItem);
286  await installWaiter;
287  }
288 
289  public static async Task WaitForInstall(Steamworks.Ugc.Item item)
290  => await WaitForInstall(item.Id);
291 
292  public static async Task WaitForInstall(ulong item)
293  {
294  var installWaiter = new InstallWaiter(item);
295  while (installWaiter.Waiting) { await Task.Delay(500); }
296  await Task.Delay(500);
297  }
298 
299  public static void OnItemDownloadComplete(ulong id, bool forceInstall = false)
300  {
301  if (Screen.Selected is not MainMenuScreen && !forceInstall)
302  {
303  if (!MainMenuScreen.WorkshopItemsToUpdate.Contains(id))
304  {
305  MainMenuScreen.WorkshopItemsToUpdate.Enqueue(id);
306  }
307  return;
308  }
309  else if (!CanBeInstalled(id))
310  {
311  DebugConsole.Log($"Cannot install {id}");
312  InstallWaiter.StopWaiting(id);
313  }
314  else if (ContentPackageManager.WorkshopPackages.Any(p =>
315  p.UgcId.TryUnwrap(out var ugcId)
316  && ugcId is SteamWorkshopId workshopId
317  && workshopId.Value == id))
318  {
319  DebugConsole.Log($"Already installed {id}.");
320  InstallWaiter.StopWaiting(id);
321  }
322  else if (InstallTaskCounter.IsInstalling(id))
323  {
324  DebugConsole.Log($"Already installing {id}.");
325  }
326  else
327  {
328  DebugConsole.Log($"Finished downloading {id}, installing...");
329  TaskPool.Add($"InstallItem{id}", InstallMod(id), t => InstallWaiter.StopWaiting(id));
330  }
331  }
332  }
333  }
334 }
ItemThumbnail(in Steamworks.Ugc.Item item, CancellationToken cancellationToken)