3 using Microsoft.Xna.Framework.Graphics;
6 using System.Collections.Generic;
7 using System.Collections.Immutable;
9 using System.Threading;
10 using System.Threading.Tasks;
15 static partial class SteamManager
17 public static partial class Workshop
19 public const int MaxThumbnailSize = 1024 * 1024;
21 public static readonly ImmutableArray<Identifier> Tags =
new []
33 }.ToIdentifiers().ToImmutableArray();
37 private struct RefCounter
43 private readonly
static Dictionary<UInt64, RefCounter> TextureRefs
44 =
new Dictionary<ulong, RefCounter>();
46 public UInt64
ItemId {
get;
private set; }
53 if (TextureRefs.TryGetValue(
ItemId, out var refCounter))
55 return refCounter.Texture;
68 if (TextureRefs.TryGetValue(
ItemId, out var refCounter))
70 return refCounter.Loading;
77 public ItemThumbnail(in Steamworks.Ugc.Item item, CancellationToken cancellationToken)
82 if (TextureRefs.TryGetValue(
ItemId, out var refCounter))
84 TextureRefs[
ItemId] =
new RefCounter {
Texture = refCounter.Texture, Count = refCounter.Count + 1,
Loading = refCounter.Loading };
89 TaskPool.Add($
"Workshop thumbnail {item.Title}", GetTexture(item, cancellationToken), SaveTextureToRefCounter(item.Id));
96 if (
ItemId == 0) {
return; }
99 var refCounter = TextureRefs[
ItemId];
100 TextureRefs[
ItemId] =
new RefCounter {
Texture = refCounter.Texture, Count = refCounter.Count - 1 };
101 if (TextureRefs[
ItemId].Count <= 0)
103 TextureRefs[
ItemId].Texture?.Dispose();
104 TextureRefs.Remove(
ItemId);
110 private static async Task<Texture2D?> GetTexture(Steamworks.Ugc.Item item, CancellationToken cancellationToken)
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 })
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);
129 private static Action<Task> SaveTextureToRefCounter(UInt64 itemId)
132 if (t.IsCanceled) {
return; }
133 Texture2D? texture = ((Task<Texture2D?>)t).Result;
136 if (TextureRefs.TryGetValue(itemId, out var refCounter))
138 TextureRefs[itemId] =
new RefCounter {
Texture = texture, Count = refCounter.Count,
Loading =
false };
140 else if (texture !=
null)
154 public const string PublishStagingDir =
"WorkshopStaging";
156 public static void DeletePublishStagingCopy()
158 if (Directory.Exists(PublishStagingDir)) { Directory.Delete(PublishStagingDir, recursive:
true); }
161 private static void RefreshLocalMods()
163 CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.LocalPackages.Refresh());
166 public static async Task CreatePublishStagingCopy(
string title,
string modVersion, ContentPackage contentPackage)
170 if (!ContentPackageManager.LocalPackages.Contains(contentPackage))
172 throw new Exception(
"Expected local package");
175 DeletePublishStagingCopy();
176 Directory.CreateDirectory(PublishStagingDir);
177 await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir, ShouldCorrectPaths.No);
179 var stagingFileListPath = Path.Combine(PublishStagingDir, ContentPackage.FileListFileName);
181 var result = ContentPackage.TryLoad(stagingFileListPath);
182 if (!result.TryUnwrapSuccess(out var tempPkg))
184 throw new Exception(
"Staging copy could not be loaded",
185 result.TryUnwrapFailure(out var exception) ? exception :
null);
189 ModProject modProject =
new ModProject(tempPkg)
191 ModVersion = modVersion,
193 ExpectedHash = tempPkg.CalculateHash(name: title, modVersion: modVersion)
195 modProject.Save(stagingFileListPath);
198 public static async Task<Option<ContentPackage>> CreateLocalCopy(ContentPackage contentPackage)
202 if (!ContentPackageManager.WorkshopPackages.Contains(contentPackage))
204 throw new Exception(
"Expected Workshop package");
207 if (!contentPackage.UgcId.TryUnwrap(out var ugcId) || !(ugcId is SteamWorkshopId workshopId))
209 throw new Exception($
"Steam Workshop ID not set for {contentPackage.Name}");
212 string sanitizedName = ToolBox.RemoveInvalidFileNameChars(contentPackage.Name).Trim();
213 if (sanitizedName.IsNullOrWhiteSpace())
215 throw new Exception($
"Sanitized name for {contentPackage.Name} is empty");
218 string newPath = $
"{ContentPackage.LocalModsDir}/{sanitizedName}";
219 if (File.Exists(newPath) || Directory.Exists(newPath))
221 newPath += $
"_{workshopId.Value}";
224 if (File.Exists(newPath) || Directory.Exists(newPath))
226 throw new Exception($
"{newPath} already exists");
229 await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, newPath, ShouldCorrectPaths.Yes);
231 ModProject modProject =
new ModProject(contentPackage);
232 modProject.DiscardHashAndInstallTime();
233 modProject.Save(Path.Combine(newPath, ContentPackage.FileListFileName));
237 return ContentPackageManager.LocalPackages.FirstOrNone(p => p.UgcId == contentPackage.UgcId);
240 private struct InstallWaiter
242 private static readonly HashSet<ulong> waitingIds =
new HashSet<ulong>();
243 public ulong Id {
get;
private set; }
245 public InstallWaiter(ulong
id)
248 lock (waitingIds) { waitingIds.Add(Id); }
255 if (Id == 0) {
return false; }
259 return waitingIds.Contains(Id);
264 public static void StopWaiting(ulong
id)
268 waitingIds.Remove(
id);
273 public static async Task Reinstall(Steamworks.Ugc.Item workshopItem)
275 NukeDownload(workshopItem);
277 = ContentPackageManager.WorkshopPackages.Where(p =>
278 p.UgcId.TryUnwrap(out var ugcId)
279 && ugcId is SteamWorkshopId workshopId
280 && workshopId.Value == workshopItem.Id)
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);
289 public static async Task WaitForInstall(Steamworks.Ugc.Item item)
290 => await WaitForInstall(item.Id);
292 public static async Task WaitForInstall(ulong item)
294 var installWaiter =
new InstallWaiter(item);
295 while (installWaiter.Waiting) { await Task.Delay(500); }
296 await Task.Delay(500);
299 public static void OnItemDownloadComplete(ulong
id,
bool forceInstall =
false)
301 if (Screen.Selected is not MainMenuScreen && !forceInstall)
303 if (!MainMenuScreen.WorkshopItemsToUpdate.Contains(
id))
305 MainMenuScreen.WorkshopItemsToUpdate.Enqueue(
id);
309 else if (!CanBeInstalled(
id))
311 DebugConsole.Log($
"Cannot install {id}");
312 InstallWaiter.StopWaiting(
id);
314 else if (ContentPackageManager.WorkshopPackages.Any(p =>
315 p.UgcId.TryUnwrap(out var ugcId)
316 && ugcId is SteamWorkshopId workshopId
317 && workshopId.Value ==
id))
319 DebugConsole.Log($
"Already installed {id}.");
320 InstallWaiter.StopWaiting(
id);
322 else if (InstallTaskCounter.IsInstalling(
id))
324 DebugConsole.Log($
"Already installing {id}.");
328 DebugConsole.Log($
"Finished downloading {id}, installing...");
329 TaskPool.Add($
"InstallItem{id}", InstallMod(
id), t => InstallWaiter.StopWaiting(
id));
override int GetHashCode()
override bool Equals(object? obj)
ItemThumbnail(in Steamworks.Ugc.Item item, CancellationToken cancellationToken)