3 using System.Collections.Generic;
4 using System.Globalization;
7 using System.Threading.Tasks;
10 using Microsoft.Xna.Framework;
11 using Microsoft.Xna.Framework.Graphics;
19 sealed
partial class MutableWorkshopMenu : WorkshopMenu
21 private class LocalThumbnail : IDisposable
23 public Texture2D? Texture {
get;
private set; } =
null;
24 public bool Loading =
true;
26 public LocalThumbnail(
string path)
28 TaskPool.Add($
"LocalThumbnail {path}",
32 return TextureLoader.FromFile(path, compress:
false, mipmap:
false);
37 Task<Texture2D?> texTask = (t as Task<Texture2D?>)!;
40 texTask.Result?.Dispose();
44 Texture = texTask.Result;
49 private bool disposed =
false;
52 if (disposed) {
return; }
59 private LocalThumbnail? localThumbnail =
null;
61 private void CreateLocalThumbnail(
string path, GUIFrame thumbnailContainer)
63 thumbnailContainer.ClearChildren();
64 localThumbnail?.Dispose();
65 localThumbnail =
new LocalThumbnail(path);
66 CreateAsyncThumbnailComponent(thumbnailContainer, () => localThumbnail?.Texture, () => localThumbnail is { Loading:
true });
69 private static async Task<(
int FileCount,
int ByteCount)> GetModDirInfo(
string dir, GUITextBlock label)
74 var files = Directory.GetFiles(dir, pattern:
"*", option: System.IO.SearchOption.AllDirectories);
75 foreach (var file
in files)
80 label.Text = TextManager.GetWithVariables(
82 (
"[filecount]", fileCount.ToString(CultureInfo.InvariantCulture)),
83 (
"[size]", MathUtils.GetBytesReadable(byteCount)));
86 return (fileCount, byteCount);
89 private void DeselectPublishedItem()
91 if (selfModsListOption.TryUnwrap(out var selfModsList))
93 var deselectCarrier = selfModsList.Parent.FindChild(c => c.UserData is ActionCarrier { Id: var id } &&
id ==
"deselect");
94 Action? deselectAction = deselectCarrier.UserData is ActionCarrier { Action: var action }
97 deselectAction?.Invoke();
103 private static bool PackageMatchesItem(ContentPackage p, Steamworks.Ugc.Item workshopItem)
104 => p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == workshopItem.Id;
106 private void PopulatePublishTab(ItemOrPackage itemOrPackage, GUIFrame parentFrame)
108 ContentPackageManager.LocalPackages.Refresh();
109 ContentPackageManager.WorkshopPackages.Refresh();
111 parentFrame.ClearChildren();
112 GUILayoutGroup mainLayout =
new GUILayoutGroup(
new RectTransform(Vector2.One, parentFrame.RectTransform),
113 childAnchor:
Anchor.TopCenter);
115 Steamworks.Ugc.Item workshopItem = itemOrPackage.TryGet(out Steamworks.Ugc.Item item) ? item :
default;
117 ContentPackage? localPackage = itemOrPackage.TryGet(out ContentPackage package)
119 : ContentPackageManager.LocalPackages.FirstOrDefault(p => PackageMatchesItem(p, workshopItem));
120 ContentPackage? workshopPackage
121 = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => PackageMatchesItem(p, workshopItem));
122 if (localPackage is
null)
124 new GUIFrame(
new RectTransform((1.0f, 0.15f), mainLayout.RectTransform), style:
null);
127 bool workshopCopyExists =
128 ContentPackageManager.WorkshopPackages.Any(p => PackageMatchesItem(p, workshopItem));
130 new GUITextBlock(
new RectTransform((0.7f, 0.4f), mainLayout.RectTransform),
131 TextManager.Get(workshopCopyExists ?
"LocalCopyRequired" :
"ItemInstallRequired"),
134 var buttonLayout =
new GUILayoutGroup(
new RectTransform((0.6f, 0.1f), mainLayout.RectTransform),
136 var yesButton =
new GUIButton(
new RectTransform((0.5f, 1.0f), buttonLayout.RectTransform),
137 text: TextManager.Get(
"Yes"))
139 OnClicked = (button, o) =>
141 CoroutineManager.StartCoroutine(MessageBoxCoroutine((currentStepText, messageBox)
142 => CreateLocalCopy(currentStepText, workshopItem, parentFrame)),
143 $
"CreateLocalCopy {workshopItem.Id}");
147 var noButton =
new GUIButton(
new RectTransform((0.5f, 1.0f), buttonLayout.RectTransform),
148 text: TextManager.Get(
"No"))
150 OnClicked = (button, o) =>
152 DeselectPublishedItem();
159 if (!ContentPackageManager.LocalPackages.Contains(localPackage))
161 throw new Exception($
"Content package \"{localPackage.Name}\" is not a local package!");
165 new GUITextBlock(
new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), localPackage.Name,
166 font: GUIStyle.LargeFont);
167 if (workshopItem.Id != 0)
169 var showInSteamButton = CreateShowInSteamButton(workshopItem,
new RectTransform((0.2f, 1.0f), selectedTitle.RectTransform,
Anchor.CenterRight));
172 Spacer(mainLayout, height: 0.03f);
174 var (leftTop, _, rightTop)
175 = CreateSidebars(mainLayout, leftWidth: 0.2f, centerWidth: 0.01f, rightWidth: 0.79f,
177 leftTop.Stretch =
true;
178 rightTop.Stretch =
true;
180 Label(leftTop, TextManager.Get(
"WorkshopItemPreviewImage"), GUIStyle.SubHeadingFont);
181 string? thumbnailPath =
null;
182 var thumbnailContainer = CreateThumbnailContainer(leftTop, Vector2.One,
ScaleBasis.BothWidth);
183 if (workshopItem.Id != 0)
185 CreateItemThumbnail(workshopItem, taskCancelSrc.Token, thumbnailContainer);
188 var browseThumbnail =
190 TextManager.Get(
"WorkshopItemBrowse"), style:
"GUIButtonSmall")
192 OnClicked = (button, o) =>
194 FileSelection.ClearFileTypeFilters();
195 FileSelection.AddFileTypeFilter(
"PNG",
"*.png");
196 FileSelection.AddFileTypeFilter(
"JPEG",
"*.jpg, *.jpeg");
197 FileSelection.AddFileTypeFilter(
"All files",
"*.*");
198 FileSelection.SelectFileTypeFilter(
"*.png");
199 FileSelection.CurrentDirectory
200 = Path.GetFullPath(Path.GetDirectoryName(localPackage.Path)!);
202 FileSelection.OnFileSelected = (fn) =>
204 if (
new FileInfo(fn).
Length > SteamManager.Workshop.MaxThumbnailSize)
206 new GUIMessageBox(TextManager.Get(
"Error"), TextManager.Get(
"WorkshopItemPreviewImageTooLarge"));
210 CreateLocalThumbnail(thumbnailPath, thumbnailContainer);
213 FileSelection.Open =
true;
219 Label(rightTop, TextManager.Get(
"WorkshopItemTitle"), GUIStyle.SubHeadingFont);
220 var titleTextBox =
new GUITextBox(
NewItemRectT(rightTop), localPackage.Name);
222 Label(rightTop, TextManager.Get(
"WorkshopItemDescription"), GUIStyle.SubHeadingFont);
223 var descriptionTextBox
226 if (workshopItem.Id != 0)
229 $
"GetFullDescription{workshopItem.Id}",
230 SteamManager.Workshop.GetItemAsap(workshopItem.Id.Value, withLongDescription:
true),
233 if (!t.TryGetResult(out Option<Steamworks.Ugc.Item> itemWithDescriptionOption)) { return; }
235 descriptionTextBox.Text =
236 itemWithDescriptionOption.TryUnwrap(out var itemWithDescription)
237 ? itemWithDescription.Description ?? descriptionTextBox.Text
238 : descriptionTextBox.Text;
239 descriptionTextBox.Deselect();
243 var (leftBottom, _, rightBottom)
244 = CreateSidebars(mainLayout, leftWidth: 0.49f, centerWidth: 0.01f, rightWidth: 0.5f, height: 0.5f);
245 leftBottom.Stretch =
true;
246 rightBottom.Stretch =
true;
248 Label(leftBottom, TextManager.Get(
"WorkshopItemVersion"), GUIStyle.SubHeadingFont);
249 var modVersion = localPackage.ModVersion;
250 if (workshopPackage is { ModVersion: { } workshopVersion } &&
251 modVersion.Equals(workshopVersion, StringComparison.OrdinalIgnoreCase))
253 modVersion = ModProject.IncrementModVersion(modVersion);
256 char[] forbiddenVersionCharacters = {
';',
'=' };
257 var versionTextBox =
new GUITextBox(
NewItemRectT(leftBottom), modVersion);
258 versionTextBox.OnTextChanged += (box, text) =>
260 if (text.Any(c => forbiddenVersionCharacters.Contains(c)))
262 foreach (var c
in forbiddenVersionCharacters)
264 text = text.Replace($
"{c}",
"");
268 box.Flash(GUIStyle.Red);
274 Label(leftBottom, TextManager.Get(
"WorkshopItemChangeNote"), GUIStyle.SubHeadingFont);
277 Label(rightBottom, TextManager.Get(
"WorkshopItemTags"), GUIStyle.SubHeadingFont);
278 var tagsList = CreateTagsList(SteamManager.Workshop.Tags,
NewItemRectT(rightBottom, heightScale: 4.0f),
280 Dictionary<Identifier, GUIButton> tagButtons = tagsList.Content.Children.Cast<GUIButton>()
281 .
Select(b => ((Identifier)b.UserData, b)).ToDictionary();
282 if (workshopItem.Tags !=
null)
284 foreach (Identifier tag
in workshopItem.Tags.ToIdentifiers())
286 if (tagButtons.TryGetValue(tag, out var button)) { button.Selected =
true; }
290 GUILayoutGroup visibilityLayout =
new GUILayoutGroup(
NewItemRectT(rightBottom), isHorizontal:
true);
292 var visibilityLabel =
Label(visibilityLayout, TextManager.Get(
"WorkshopItemVisibility"), GUIStyle.SubHeadingFont);
293 visibilityLabel.RectTransform.RelativeSize = (0.6f, 1.0f);
294 visibilityLabel.TextAlignment = Alignment.CenterRight;
296 Steamworks.Ugc.Visibility visibility = workshopItem.Visibility;
297 var visibilityDropdown = DropdownEnum(
299 (v) => TextManager.Get($
"WorkshopItemVisibility.{v}"),
301 (v) => visibility = v);
302 visibilityDropdown.RectTransform.RelativeSize = (0.4f, 1.0f);
304 var fileInfoLabel =
Label(rightBottom,
"", GUIStyle.Font, heightScale: 1.0f);
305 fileInfoLabel.TextAlignment = Alignment.CenterRight;
306 TaskPool.AddWithResult($
"FileInfoLabel{workshopItem.Id}", GetModDirInfo(localPackage.Dir, fileInfoLabel), t => { });
308 GUILayoutGroup buttonLayout =
new GUILayoutGroup(
NewItemRectT(rightBottom), isHorizontal:
true, childAnchor:
Anchor.CenterRight);
310 RectTransform newButtonRectT()
311 =>
new RectTransform((0.4f, 1.0f), buttonLayout.RectTransform);
313 var publishItemButton =
new GUIButton(newButtonRectT(), TextManager.Get(workshopItem.Id != 0 ?
"WorkshopItemUpdate" :
"WorkshopItemPublish"))
315 OnClicked = (button, o) =>
318 string packageName = localPackage.Name;
319 var result = ContentPackageManager.ReloadContentPackage(localPackage);
320 if (!result.TryUnwrapSuccess(out localPackage))
322 throw new Exception($
"\"{packageName}\" was removed upon reload",
323 result.TryUnwrapFailure(out var exception) ? exception :
null);
327 Steamworks.Ugc.Editor ugcEditor =
329 ? Steamworks.Ugc.Editor.NewCommunityFile
330 :
new Steamworks.Ugc.Editor(workshopItem.Id);
331 ugcEditor = ugcEditor
332 .InLanguage(SteamUtils.SteamUILanguage ??
string.Empty)
333 .WithTitle(titleTextBox.Text)
334 .WithDescription(descriptionTextBox.Text)
335 .WithTags(tagButtons.Where(kvp => kvp.Value.Selected).Select(kvp => kvp.Key.Value))
336 .WithChangeLog(changeNoteTextBox.Text)
337 .WithMetaData($
"gameversion={localPackage.GameVersion};modversion={versionTextBox.Text}")
338 .WithVisibility(visibility)
339 .WithPreviewFile(thumbnailPath);
341 CoroutineManager.StartCoroutine(
342 MessageBoxCoroutine((currentStepText, messageBox)
343 => PublishItem(currentStepText, messageBox, versionTextBox.Text, ugcEditor, localPackage)));
349 if (workshopItem.Id != 0)
351 var deleteItemButton =
new GUIButton(newButtonRectT(), TextManager.Get(
"WorkshopItemDelete"), color: GUIStyle.Red)
353 OnClicked = (button, o) =>
355 var confirmDeletion =
new GUIMessageBox(
356 headerText: TextManager.Get(
"WorkshopItemDelete"),
357 text: TextManager.GetWithVariable(
"WorkshopItemDeleteVerification",
"[itemname]", workshopItem.Title!),
358 buttons:
new[] { TextManager.Get(
"Yes"), TextManager.Get(
"No") });
359 confirmDeletion.Buttons[0].OnClicked = (yesBuffer, o1) =>
361 TaskPool.AddWithResult($
"Delete{workshopItem.Id}", Steamworks.SteamUGC.DeleteFileAsync(workshopItem.Id),
364 SteamManager.Workshop.Uninstall(workshopItem);
365 confirmDeletion.Close();
366 DeselectPublishedItem();
370 confirmDeletion.Buttons[1].OnClicked = (noButton, o1) =>
372 confirmDeletion.Close();
378 HoverColor = Color.Lerp(GUIStyle.Red, Color.White, 0.3f),
379 PressedColor = Color.Lerp(GUIStyle.Red, Color.Black, 0.3f),
381 deleteItemButton.TextBlock.TextColor = Color.Black;
382 deleteItemButton.TextBlock.HoverTextColor = Color.Black;
387 private IEnumerable<CoroutineStatus> MessageBoxCoroutine(Func<GUITextBlock, GUIMessageBox, IEnumerable<CoroutineStatus>> subcoroutine)
389 var messageBox =
new GUIMessageBox(
"", TextManager.Get(
"ellipsis").Fallback(
"..."), buttons:
new [] { TextManager.Get(
"Cancel") });
390 messageBox.Buttons[0].OnClicked = (button, o) =>
396 var coroutineEval = subcoroutine(messageBox.Text, messageBox).GetEnumerator();
399 var status = coroutineEval.Current;
400 if (messageBox.Closed)
402 yield
return CoroutineStatus.Success;
405 else if (status == CoroutineStatus.Failure || status == CoroutineStatus.Success)
415 bool moveNext =
true;
418 moveNext = coroutineEval.MoveNext();
422 DebugConsole.ThrowError($
"{e.Message} {e.StackTrace.CleanupStackTrace()}");
432 private IEnumerable<CoroutineStatus> CreateLocalCopy(GUITextBlock currentStepText, Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame)
434 ContentPackage? workshopCopy =
435 ContentPackageManager.WorkshopPackages.FirstOrDefault(p => PackageMatchesItem(p, workshopItem));
436 if (workshopCopy is
null)
438 if (!SteamManager.Workshop.CanBeInstalled(workshopItem))
440 SteamManager.Workshop.NukeDownload(workshopItem);
442 SteamManager.Workshop.DownloadModThenEnqueueInstall(workshopItem);
443 TaskPool.Add($
"Install {workshopItem.Title}",
444 SteamManager.Workshop.WaitForInstall(workshopItem),
447 ContentPackageManager.WorkshopPackages.Refresh();
449 while (!ContentPackageManager.WorkshopPackages.Any(p => PackageMatchesItem(p, workshopItem)))
451 currentStepText.Text = SteamManager.Workshop.CanBeInstalled(workshopItem)
452 ? TextManager.Get(
"PublishPopupInstall")
453 : TextManager.GetWithVariable(
"PublishPopupDownload",
"[percentage]", Percentage(workshopItem.DownloadAmount));
454 yield
return new WaitForSeconds(0.5f);
458 ContentPackageManager.WorkshopPackages.First(p => PackageMatchesItem(p, workshopItem));
461 bool localCopyMade =
false;
462 TaskPool.AddWithResult($
"Create local copy {workshopItem.Title}",
463 SteamManager.Workshop.CreateLocalCopy(workshopCopy),
466 ContentPackageManager.LocalPackages.Refresh();
467 localCopyMade = true;
469 while (!localCopyMade)
471 currentStepText.Text = TextManager.Get(
"PublishPopupCreateLocal");
472 yield
return new WaitForSeconds(0.5f);
474 PopulatePublishTab(workshopItem, parentFrame);
476 yield
return CoroutineStatus.Success;
479 private IEnumerable<CoroutineStatus> PublishItem(
480 GUITextBlock currentStepText, GUIMessageBox messageBox,
481 string modVersion, Steamworks.Ugc.Editor editor, ContentPackage localPackage)
483 if (!SteamManager.IsInitialized)
485 yield
return CoroutineStatus.Failure;
488 bool stagingReady =
false;
489 Exception? stagingException =
null;
490 TaskPool.Add(
"CreatePublishStagingCopy",
491 SteamManager.Workshop.CreatePublishStagingCopy(editor.Title ?? localPackage.Name, modVersion, localPackage),
495 stagingException = t.Exception?.GetInnermost();
497 TrySetText(
"PublishPopupStaging");
499 while (!stagingReady) { yield
return new WaitForSeconds(0.5f); }
501 if (stagingException !=
null)
503 throw new Exception($
"Failed to create staging copy: {stagingException.Message} {stagingException.StackTrace.CleanupStackTrace()}");
507 .WithContent(SteamManager.Workshop.PublishStagingDir)
508 .ForAppId(SteamManager.AppID);
510 messageBox.Buttons[0].Enabled =
false;
511 Steamworks.Ugc.PublishResult? result =
null;
512 Exception? resultException =
null;
513 TaskPool.Add($
"Publishing {localPackage.Name} ({localPackage.UgcId})",
514 editor.SubmitAsync(),
517 if (t.TryGetResult(out Steamworks.Ugc.PublishResult publishResult))
519 result = publishResult;
521 resultException = t.Exception?.GetInnermost();
523 TrySetText(
"PublishPopupSubmit");
524 while (!result.HasValue && resultException is
null) { yield
return new WaitForSeconds(0.5f); }
526 if (result is { Success:
true })
528 var resultId = result.Value.FileId;
529 bool packageMatchesResult(ContentPackage p)
530 => p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == resultId;
531 Steamworks.Ugc.Item resultItem =
new Steamworks.Ugc.Item(resultId);
532 Task downloadTask = SteamManager.Workshop.ForceRedownload(resultItem);
533 while (!resultItem.IsInstalled && !downloadTask.IsCompleted)
535 currentStepText.Text = TextManager.GetWithVariable(
"PublishPopupDownload",
"[percentage]", Percentage(resultItem.DownloadAmount));
536 yield
return new WaitForSeconds(0.5f);
540 DateTime waitInstallUntil = DateTime.Now +
new TimeSpan(0, 0, seconds: 30);
541 while (!resultItem.IsInstalled || resultItem.IsDownloading)
543 if (DateTime.Now > waitInstallUntil)
545 throw new Exception($
"Failed to install item: download task ended with status {downloadTask.Status}," +
546 $
" item installed: {resultItem.IsInstalled}, " +
547 $
" item downloading: {resultItem.IsDownloading}, " +
548 $
"exception was {downloadTask.Exception?.GetInnermost()?.ToString().CleanupStackTrace() ?? "[NULL]
"}");
550 yield
return new WaitForSeconds(0.5f);
553 ContentPackage? pkgToNuke
554 = ContentPackageManager.WorkshopPackages.FirstOrDefault(packageMatchesResult);
555 if (pkgToNuke !=
null)
557 Directory.Delete(pkgToNuke.Dir, recursive:
true);
558 ContentPackageManager.WorkshopPackages.Refresh();
561 bool installed =
false;
563 "InstallNewlyPublished",
564 SteamManager.Workshop.WaitForInstall(resultItem),
571 TrySetText(
"PublishPopupInstall");
572 yield
return new WaitForSeconds(0.5f);
575 ContentPackageManager.WorkshopPackages.Refresh();
576 ContentPackageManager.EnabledPackages.RefreshUpdatedMods();
578 var localModProject =
new ModProject(localPackage)
580 UgcId = Option<ContentPackageId>.Some(
new SteamWorkshopId(resultId)),
581 ModVersion = modVersion
583 localModProject.DiscardHashAndInstallTime();
584 localModProject.Save(localPackage.Path);
585 ContentPackageManager.ReloadContentPackage(localPackage);
586 DeselectPublishedItem();
588 if (result.Value.NeedsWorkshopAgreement)
590 SteamManager.OverlayCustomUrl(resultItem.Url);
592 new GUIMessageBox(
string.Empty, TextManager.GetWithVariable(
"workshopitempublished",
"[itemname]", localPackage.Name));
594 else if (resultException !=
null)
596 throw new Exception($
"Failed to publish item: {resultException.Message} {resultException.StackTrace.CleanupStackTrace()}");
600 new GUIMessageBox(TextManager.Get(
"error"), TextManager.GetWithVariable(
"workshopitempublishfailed",
"[itemname]", localPackage.Name));
603 SteamManager.Workshop.DeletePublishStagingCopy();
606 void TrySetText(
string textTag)
608 if (currentStepText?.Text !=
null)
610 currentStepText.Text = TextManager.Get(textTag);