using Barotrauma.IO; using Barotrauma.Steam; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Barotrauma { class SteamWorkshopScreen : Screen { private GUIFrame menu; private GUIListBox subscribedItemList, topItemList; private GUITextBox subscribedItemFilter, topItemFilter; private GUIListBox publishedItemList, myItemList; //shows information of a selected workshop item private GUIFrame modsPreviewFrame, browsePreviewFrame; //menu for creating new items private GUIFrame createItemFrame; //listbox that shows the files included in the item being created private GUIListBox createItemFileList; private GUIImage previewIcon; private System.IO.FileSystemWatcher createItemWatcher; private readonly List tabButtons = new List(); private class PendingPreviewImageDownload { /// /// Was the image downloaded /// public bool Downloaded = false; /// /// How many tasks are looking to create a preview image based on this download /// public int PendingLoads = 1; } private readonly Dictionary pendingPreviewImageDownloads = new Dictionary(); private readonly Dictionary itemPreviewSprites = new Dictionary(); private enum Tab { Mods, Browse, Publish } private GUIComponent[] tabs; private ContentPackage itemContentPackage; private Steamworks.Ugc.Editor? itemEditor; private enum VisibilityType { Public, FriendsOnly, Private } public SteamWorkshopScreen() { GameMain.Instance.ResolutionChanged += CreateUI; CreateUI(); Steamworks.SteamUGC.GlobalOnItemInstalled += OnItemInstalled; } private void CreateUI() { tabs = new GUIComponent[Enum.GetValues(typeof(Tab)).Length]; menu = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), GUI.Canvas, Anchor.Center) { MinSize = new Point(GameMain.GraphicsHeight, 0) }); var container = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), menu.RectTransform, Anchor.Center)) { Stretch = true }; var topButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.03f), container.RectTransform), isHorizontal: true); foreach (Tab tab in Enum.GetValues(typeof(Tab))) { GUIButton tabButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), topButtonContainer.RectTransform), TextManager.Get(tab.ToString() + "Tab"), style: "GUITabButton") { UserData = tab, OnClicked = (btn, userData) => { SelectTab((Tab)userData); return true; } }; tabButtons.Add(tabButton); } topButtonContainer.RectTransform.MinSize = new Point(0, topButtonContainer.RectTransform.Children.Max(c => c.MinSize.Y)); topButtonContainer.RectTransform.MaxSize = new Point(int.MaxValue, topButtonContainer.RectTransform.MinSize.Y); var tabContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.7f), container.RectTransform), style: "InnerFrame"); var bottomButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), container.RectTransform), isHorizontal: true); GUIButton backButton = new GUIButton(new RectTransform(new Vector2(0.1f, 0.9f), bottomButtonContainer.RectTransform) { MinSize = new Point(150, 0) }, TextManager.Get("Back")) { OnClicked = GameMain.MainMenuScreen.ReturnToMainMenu }; backButton.SelectedColor = backButton.Color; topButtonContainer.RectTransform.MinSize = new Point(0, backButton.RectTransform.MinSize.Y); topButtonContainer.RectTransform.MaxSize = new Point(int.MaxValue, backButton.RectTransform.MinSize.Y); //------------------------------------------------------------------------------- //Subscribed Mods tab //------------------------------------------------------------------------------- tabs[(int)Tab.Mods] = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), tabContainer.RectTransform, Anchor.Center), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.02f }; var modsContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), tabs[(int)Tab.Mods].RectTransform)) { Stretch = true, RelativeSpacing = 0.02f }; subscribedItemList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), modsContainer.RectTransform)) { ScrollBarVisible = true, OnSelected = (GUIComponent component, object userdata) => { if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } ShowItemPreview(userdata as Steamworks.Ugc.Item?, modsPreviewFrame); return true; } }; subscribedItemFilter = CreateFilterBox(modsContainer, subscribedItemList); modsPreviewFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 1.0f), tabs[(int)Tab.Mods].RectTransform, Anchor.TopRight), style: null); //------------------------------------------------------------------------------- //Popular Mods tab //------------------------------------------------------------------------------- tabs[(int)Tab.Browse] = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), tabContainer.RectTransform, Anchor.Center), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.02f }; var listContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), tabs[(int)Tab.Browse].RectTransform), childAnchor: Anchor.TopCenter) { Stretch = true, RelativeSpacing = 0.02f }; topItemList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.9f), listContainer.RectTransform)) { ScrollBarVisible = true, OnSelected = (GUIComponent component, object userdata) => { ShowItemPreview(userdata as Steamworks.Ugc.Item?, browsePreviewFrame); return true; } }; topItemFilter = CreateFilterBox(listContainer, topItemList); new GUIButton(new RectTransform(new Vector2(1.0f, 0.02f), listContainer.RectTransform), TextManager.Get("FindModsButton"), style: "GUIButtonSmall") { OnClicked = (btn, userdata) => { SteamManager.OverlayCustomURL("steam://url/SteamWorkshopPage/" + SteamManager.AppID); return true; } }; browsePreviewFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 1.0f), tabs[(int)Tab.Browse].RectTransform, Anchor.TopRight), style: null); //------------------------------------------------------------------------------- //Publish tab //------------------------------------------------------------------------------- tabs[(int)Tab.Publish] = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), tabContainer.RectTransform, Anchor.Center), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.02f }; var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), tabs[(int)Tab.Publish].RectTransform)) { Stretch = true, RelativeSpacing = 0.02f }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), TextManager.Get("PublishedWorkshopItems"), font: GUI.SubHeadingFont); publishedItemList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), leftColumn.RectTransform)) { OnSelected = (component, userdata) => { if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } if (GUI.MouseOn is GUITickBox || GUI.MouseOn?.Parent is GUITickBox) { return false; } myItemList.Deselect(); if (userdata is Steamworks.Ugc.Item?) { var item = userdata as Steamworks.Ugc.Item?; if (!(item?.IsInstalled ?? false)) { return false; } if (CreateWorkshopItem(item)) { ShowCreateItemFrame(); } } return true; } }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), TextManager.Get("YourWorkshopItems"), font: GUI.SubHeadingFont); myItemList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), leftColumn.RectTransform)) { OnSelected = (component, userdata) => { if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } publishedItemList.Deselect(); if (userdata is SubmarineInfo sub) { CreateWorkshopItem(sub); } else if (userdata is ContentPackage contentPackage) { CreateWorkshopItem(contentPackage); } ShowCreateItemFrame(); return true; } }; createItemFrame = new GUIFrame(new RectTransform(new Vector2(0.58f, 1.0f), tabs[(int)Tab.Publish].RectTransform, Anchor.TopRight), style: null); SelectTab(Tab.Mods); CoroutineManager.StartCoroutine(PollSubscribedItems()); } private GUITextBox CreateFilterBox(GUIComponent parent, GUIListBox listbox) { var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), isHorizontal: true) { Stretch = true }; filterContainer.RectTransform.SetAsFirstChild(); var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; searchBox.OnTextChanged += (textBox, text) => { foreach (GUIComponent child in listbox.Content.Children) { if (!(child.UserData is Steamworks.Ugc.Item item)) { continue; } child.Visible = string.IsNullOrEmpty(text) ? true : (item.Title?.ToLower().Contains(text.ToLower()) ?? false); } return true; }; return searchBox; } public override void Select() { base.Select(); modsPreviewFrame.ClearChildren(); browsePreviewFrame.ClearChildren(); createItemFrame.ClearChildren(); itemContentPackage = null; itemEditor = null; SelectTab(Tab.Mods); } public override void OnFileDropped(string filePath, string extension) { switch (extension) { case ".png": // workshop preview case ".jpg": case ".jpeg": if (previewIcon == null || itemContentPackage == null) { break; } OnPreviewImageSelected(previewIcon, filePath); break; default: DebugConsole.ThrowError($"Could not drag and drop the file. \"{extension}\" is not a valid file extension! (expected .png, .jpg or .jpeg)"); break; } } private void OnItemInstalled(ulong itemId) { RefreshSubscribedItems(); } float subscribePollAdditionalWait = 0.0f; private IEnumerable PollSubscribedItems() { if (!SteamManager.IsInitialized) { yield return CoroutineStatus.Success; } uint numSubscribed = 0; while (true) { while (CoroutineManager.IsCoroutineRunning("Load")) { yield return new WaitForSeconds(1.0f); } while (subscribePollAdditionalWait > 0.01f) { subscribePollAdditionalWait = Math.Min(subscribePollAdditionalWait, 3.0f); float wait = subscribePollAdditionalWait; yield return new WaitForSeconds(wait); subscribePollAdditionalWait -= wait; } uint newNumSubscribed = Steamworks.SteamUGC.NumSubscribedItems; if (newNumSubscribed != numSubscribed) { RefreshSubscribedItems(); numSubscribed = newNumSubscribed; } yield return new WaitForSeconds(1.0f); } } private void SelectTab(Tab tab) { for (int i = 0; i < tabs.Length; i++) { tabButtons[i].Selected = tabs[i].Visible = i == (int)tab; } if (createItemFrame.CountChildren == 0) { new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.9f), createItemFrame.RectTransform, Anchor.Center), TextManager.Get("WorkshopItemCreateHelpText"), wrap: true) { CanBeFocused = false }; } createItemWatcher?.Dispose(); createItemWatcher = null; if (Screen.Selected == this) { switch (tab) { case Tab.Mods: RefreshSubscribedItems(); break; case Tab.Browse: RefreshPopularItems(); break; case Tab.Publish: RefreshPublishedItems(); break; } } } public IEnumerable RefreshDownloadState() { bool isDownloading = true; while (true) { SteamManager.GetSubscribedWorkshopItems((items) => { isDownloading = items.Any(it => it.IsDownloading || it.IsDownloadPending); GameMain.MainMenuScreen.SetDownloadingModsNotification(isDownloading); }); if (!isDownloading) { break; } yield return new WaitForSeconds(0.5f); } yield return CoroutineStatus.Success; } private void RefreshSubscribedItems() { SteamManager.GetSubscribedWorkshopItems((items) => { //filter out the items published by the player (they're shown in the publish tab) var mySteamID = SteamManager.GetSteamID(); OnItemsReceived(GetVisibleItems(items.Where(it => it.Owner.Id != mySteamID)), subscribedItemList); GameMain.MainMenuScreen.SetDownloadingModsNotification(items.Any(it => it.IsDownloading || it.IsDownloadPending)); }); } private void RefreshPopularItems() { SteamManager.GetPopularWorkshopItems((items) => { OnItemsReceived(GetVisibleItems(items), topItemList); }, 20); } private void RefreshPublishedItems() { SteamManager.GetPublishedWorkshopItems((items) => { OnItemsReceived(items, publishedItemList); }); RefreshMyItemList(); } private IEnumerable GetVisibleItems(IEnumerable items) { #if UNSTABLE //show everything in Unstable return items; #else //hide Unstable items in normal version return items.Where(it => !it.HasTag("unstable")); #endif } private void RefreshMyItemList() { myItemList.ClearChildren(); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), myItemList.Content.RectTransform), TextManager.Get("WorkshopLabelSubmarines"), textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) { CanBeFocused = false }; foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { if (sub.HasTag(SubmarineTag.HideInMenus)) { continue; } string subPath = Path.GetFullPath(sub.FilePath); //ignore files that are part of the vanilla content package if (GameMain.VanillaContent != null && GameMain.VanillaContent.Files.Any(s => Path.GetFullPath(s.Path) == subPath)) { continue; } //ignore subs that are part of a workshop content package if (ContentPackage.AllPackages.Any(cp => cp.SteamWorkshopId != 0 && cp.Files.Any(f => f.Type == ContentType.Submarine && Path.GetFullPath(f.Path) == subPath))) { continue; } //ignore subs that are defined in a content package with more files than just the sub //(these will be listed in the "content packages" section) if (ContentPackage.AllPackages.Any(cp => cp.Files.Count > 1 && cp.Files.Any(f => f.Type == ContentType.Submarine && Path.GetFullPath(f.Path) == subPath))) { continue; } CreateMyItemFrame(sub, myItemList); } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), myItemList.Content.RectTransform), TextManager.Get("WorkshopLabelContentPackages"), textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) { CanBeFocused = false }; foreach (ContentPackage contentPackage in ContentPackage.AllPackages) { if (contentPackage.SteamWorkshopId != 0 || contentPackage.HideInWorkshopMenu) { continue; } if (contentPackage == GameMain.VanillaContent) { continue; } //don't list content packages that only define one sub (they're visible in the "Submarines" section) if (contentPackage.Files.Count == 1 && contentPackage.Files[0].Type == ContentType.Submarine) { continue; } CreateMyItemFrame(contentPackage, myItemList); } } private void OnItemsReceived(IEnumerable itemDetails, GUIListBox listBox) { CrossThread.RequestExecutionOnMainThread(() => { listBox.ClearChildren(); foreach (var item in itemDetails) { CreateWorkshopItemFrame(item, listBox); } if (itemDetails.Count() == 0 && listBox == subscribedItemList) { new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.9f), listBox.Content.RectTransform, Anchor.Center), TextManager.Get("NoSubscribedMods"), wrap: true) { CanBeFocused = false }; } }); } private void CreateWorkshopItemFrame(Steamworks.Ugc.Item? item, GUIListBox listBox) { if (string.IsNullOrEmpty(item?.Title)) { return; } string text = string.Empty; if (listBox == subscribedItemList) { text = subscribedItemFilter.Text; } else if (listBox == topItemList) { text = topItemFilter.Text; } bool visible = string.IsNullOrEmpty(text) || (item?.Title?.ToLower().Contains(text.ToLower()) ?? false); int prevIndex = -1; var existingFrame = listBox.Content.FindChild((component) => { return (component.UserData is Steamworks.Ugc.Item?) && (component.UserData as Steamworks.Ugc.Item?)?.Id == item?.Id; }); if (existingFrame != null) { prevIndex = listBox.Content.GetChildIndex(existingFrame); listBox.Content.RemoveChild(existingFrame); } var itemFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform, minSize: new Point(0, 80)), style: "ListBoxElement") { UserData = item, Visible = visible }; if (prevIndex > -1) { itemFrame.RectTransform.RepositionChildInHierarchy(prevIndex); } var innerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), itemFrame.RectTransform, Anchor.Center), isHorizontal: true) { CanBeFocused = false, Stretch = true }; int iconSize = innerFrame.Rect.Height; if (itemPreviewSprites.ContainsKey(item?.PreviewImageUrl)) { new GUIImage(new RectTransform(new Point(iconSize), innerFrame.RectTransform), itemPreviewSprites[item?.PreviewImageUrl], scaleToFit: true) { UserData = "previewimage", CanBeFocused = false }; } else if (Screen.Selected == this) { new GUIImage(new RectTransform(new Point(iconSize), innerFrame.RectTransform), SteamManager.DefaultPreviewImage, scaleToFit: true) { UserData = "previewimage", CanBeFocused = false }; try { if (!string.IsNullOrEmpty(item?.PreviewImageUrl)) { string imagePreviewPath = Path.Combine(SteamManager.WorkshopItemPreviewImageFolder, item?.Id + ".png"); bool isNewImage; lock (pendingPreviewImageDownloads) { isNewImage = !pendingPreviewImageDownloads.ContainsKey(item.Value.Id); if (isNewImage) { if (File.Exists(imagePreviewPath)) { File.Delete(imagePreviewPath); } pendingPreviewImageDownloads.Add(item.Value.Id, new PendingPreviewImageDownload()); } } if (isNewImage) { Directory.CreateDirectory(SteamManager.WorkshopItemPreviewImageFolder); Uri baseAddress = new Uri(item?.PreviewImageUrl); Uri directory = new Uri(baseAddress, "."); // "." == current dir, like MS-DOS string fileName = Path.GetFileName(baseAddress.LocalPath); IRestClient client = new RestClient(directory); var request = new RestRequest(fileName, Method.GET); client.ExecuteAsync(request, response => { OnPreviewImageDownloaded(response, imagePreviewPath, () => { lock (pendingPreviewImageDownloads) { pendingPreviewImageDownloads[item.Value.Id].Downloaded = true; } CoroutineManager.StartCoroutine(WaitForItemPreviewDownloaded(item, listBox, imagePreviewPath)); }); }); } else { lock (pendingPreviewImageDownloads) { pendingPreviewImageDownloads[item.Value.Id].PendingLoads++; } CoroutineManager.StartCoroutine(WaitForItemPreviewDownloaded(item, listBox, imagePreviewPath)); } } } catch (Exception e) { lock (pendingPreviewImageDownloads) { pendingPreviewImageDownloads.Remove(item.Value.Id); } DebugConsole.ThrowError("Downloading the preview image of the Workshop item \"" + item?.Title + "\" failed.", e); } } var rightColumn = new GUILayoutGroup(new RectTransform(new Point(innerFrame.Rect.Width - iconSize, innerFrame.Rect.Height), innerFrame.RectTransform), childAnchor: Anchor.CenterLeft) { IsHorizontal = true, Stretch = true, RelativeSpacing = 0.05f, CanBeFocused = false }; var titleText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), rightColumn.RectTransform), item?.Title, textAlignment: Alignment.CenterLeft, wrap: true) { UserData = "titletext", CanBeFocused = false }; if ((item?.IsSubscribed ?? false) && (item?.IsInstalled ?? false) && Directory.Exists(item?.Directory)) { bool installed = SteamManager.CheckWorkshopItemInstalled(item); if (!installed) { bool? compatible = SteamManager.CheckWorkshopItemCompatibility(item); if (compatible.HasValue && !compatible.Value) { new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.3f), rightColumn.RectTransform), TextManager.Get("WorkshopItemIncompatible"), textColor: GUI.Style.Red) { ToolTip = TextManager.Get("WorkshopItemIncompatibleTooltip") }; } else { installed = SteamManager.InstallWorkshopItem(item, out string errorMsg, Selected == this); if (!installed) { DebugConsole.NewMessage(errorMsg, Color.Red); titleText.TextColor = Color.Red; titleText.ToolTip = itemFrame.ToolTip = TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item?.Title, errorMsg }); } } } if (installed) { bool upToDate = SteamManager.CheckWorkshopItemUpToDate(item); if (!upToDate) { if (!SteamManager.UpdateWorkshopItem(item, out string errorMsg)) { DebugConsole.NewMessage(errorMsg, Color.Red); titleText.TextColor = Color.Red; titleText.ToolTip = itemFrame.ToolTip = TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item?.Title, errorMsg }); } } } } else if (item?.IsDownloading ?? false) { new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), rightColumn.RectTransform), TextManager.Get("WorkshopItemDownloading")); } else if (item?.IsDownloadPending ?? false) { new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), rightColumn.RectTransform), TextManager.Get("WorkshopItemDownloadPending")); } else if (!(item?.IsSubscribed ?? false) && (listBox != subscribedItemList)) { var downloadBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIPlusButton") { ToolTip = TextManager.Get("DownloadButton"), ForceUpperCase = true, UserData = item }; downloadBtn.OnClicked = (btn, userdata) => { DownloadItem(itemFrame, downloadBtn, item); return true; }; } if ((item?.IsSubscribed ?? false) && listBox == subscribedItemList) { var reinstallBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIReloadButton") { ToolTip = TextManager.Get("WorkshopItemReinstall"), ForceUpperCase = true, UserData = "reinstall" }; reinstallBtn.OnClicked = (btn, userdata) => { var elem = subscribedItemList.Content.GetChildByUserData(item); try { bool reselect = GameMain.Config.AllEnabledPackages.Any(cp => cp.SteamWorkshopId != 0 && cp.SteamWorkshopId == item?.Id); if (!SteamManager.UninstallWorkshopItem(item, false, out string errorMsg)) { DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\": {errorMsg}", null, true); elem.Flash(GUI.Style.Red); return true; } SteamManager.ForceRedownload(item?.Id ?? 0, () => { if (!SteamManager.InstallWorkshopItem(item, out string errorMsg, reselect, true)) { DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\": {errorMsg}", null, true); elem.Flash(GUI.Style.Red); } RefreshSubscribedItems(); }); RefreshSubscribedItems(); } catch (Exception e) { DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\"", e, true); elem.Flash(GUI.Style.Red); } return true; }; reinstallBtn.Enabled = !item.Value.IsDownloading && !item.Value.IsDownloadPending; var unsubBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIMinusButton") { ToolTip = TextManager.Get("WorkshopItemUnsubscribe"), ForceUpperCase = true, UserData = "unsubscribe" }; unsubBtn.OnClicked = (btn, userdata) => { subscribePollAdditionalWait += 1.0f; item?.Unsubscribe(); SteamManager.UninstallWorkshopItem(item, true, out _); subscribedItemList.RemoveChild(subscribedItemList.Content.GetChildByUserData(item)); return true; }; } innerFrame.Recalculate(); listBox.RecalculateChildren(); } public void SetReinstallButtonStatus(Steamworks.Ugc.Item? item, bool enabled, Color? flashColor) { var child = subscribedItemList.Content.FindChild((component) => { return (component.UserData is Steamworks.Ugc.Item?) && (component.UserData as Steamworks.Ugc.Item?)?.Id == item?.Id; }); if (child != null) { var reinstallBtn = child.FindChild("reinstall", true); if (reinstallBtn != null) { reinstallBtn.Enabled = enabled; } var unsubBtn = child.FindChild("unsubscribe", true); if (unsubBtn != null) { unsubBtn.Enabled = enabled; } if (flashColor.HasValue) { child.Flash(flashColor); } } } private void RemoveItemFromLists(ulong itemID) { RemoveItemFromList(publishedItemList); RemoveItemFromList(subscribedItemList); RemoveItemFromList(topItemList); void RemoveItemFromList(GUIListBox listBox) { listBox.Content.RemoveChild( listBox.Content.Children.FirstOrDefault(c => c.UserData is Steamworks.Ugc.Item? && (c.UserData as Steamworks.Ugc.Item?)?.Id == itemID)); } } private void CreateMyItemFrame(SubmarineInfo submarine, GUIListBox listBox) { var itemFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform, minSize: new Point(0, 80)), style: "ListBoxElement") { UserData = submarine }; var innerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), itemFrame.RectTransform, Anchor.Center), isHorizontal: true) { RelativeSpacing = 0.1f, Stretch = true }; if (submarine.PreviewImage != null) { new GUIImage(new RectTransform(new Point(innerFrame.Rect.Height), innerFrame.RectTransform), submarine.PreviewImage, scaleToFit: true); } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), innerFrame.RectTransform), submarine.Name, textAlignment: Alignment.CenterLeft); } private void CreateMyItemFrame(ContentPackage contentPackage, GUIListBox listBox) { var itemFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform, minSize: new Point(0, 80)), style: "ListBoxElement") { UserData = contentPackage }; var innerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), itemFrame.RectTransform, Anchor.Center), isHorizontal: true) { RelativeSpacing = 0.1f, Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), innerFrame.RectTransform), contentPackage.Name, textAlignment: Alignment.CenterLeft); } private void OnPreviewImageDownloaded(IRestResponse response, string previewImagePath, Action action) { if (response.ResponseStatus == ResponseStatus.Completed) { TaskPool.Add("WritePreviewImageAsync", WritePreviewImageAsync(response, previewImagePath), (task) => { action?.Invoke(); }); } } private async Task WritePreviewImageAsync(IRestResponse response, string previewImagePath) { await Task.Yield(); try { File.WriteAllBytes(previewImagePath, response.RawBytes); } catch (Exception e) { string errorMsg = "Failed to save workshop item preview image to \"" + previewImagePath + "\"."; GameAnalyticsManager.AddErrorEventOnce("SteamWorkshopScreen.OnItemPreviewDownloaded:WriteAllBytesFailed" + previewImagePath, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + e.Message); return; } } private IEnumerable WaitForItemPreviewDownloaded(Steamworks.Ugc.Item? item, GUIListBox listBox, string previewImagePath) { while (true) { lock (pendingPreviewImageDownloads) { if (pendingPreviewImageDownloads[item.Value.Id].Downloaded){ break; } } yield return new WaitForSeconds(0.2f); } if (File.Exists(previewImagePath)) { TaskPool.Add("LoadPreviewImageAsync", LoadPreviewImageAsync(item?.PreviewImageUrl, previewImagePath), new Tuple(item, listBox), (task, tuple) => { //must be done in the main thread because creating/removing GUI elements is not thread-safe CrossThread.RequestExecutionOnMainThread(() => { (var it, var lb) = tuple; if (lb.Content.FindChild(item)?.GetChildByUserData("previewimage") is GUIImage previewImage) { previewImage.Sprite = ((Task)task).Result; } else { CreateWorkshopItemFrame(it, lb); } if (modsPreviewFrame.FindChild(it) != null) { ShowItemPreview(it, modsPreviewFrame); } if (browsePreviewFrame.FindChild(item) != null) { ShowItemPreview(it, browsePreviewFrame); } lock (pendingPreviewImageDownloads) { pendingPreviewImageDownloads[it.Value.Id].PendingLoads--; if (pendingPreviewImageDownloads[it.Value.Id].PendingLoads <= 0) { pendingPreviewImageDownloads.Remove(it.Value.Id); } } }); }); } yield return CoroutineStatus.Success; } private async Task LoadPreviewImageAsync(string previewImageUrl, string previewImagePath) { await Task.Yield(); lock (itemPreviewSprites) { if (itemPreviewSprites.ContainsKey(previewImageUrl)) { return itemPreviewSprites[previewImageUrl]; } else { Sprite newSprite = new Sprite(previewImagePath, sourceRectangle: null); itemPreviewSprites.Add(previewImageUrl, newSprite); return newSprite; } } } private bool DownloadItem(GUIComponent frame, GUIButton downloadButton, Steamworks.Ugc.Item? item) { if (item == null) { return false; } var parentElement = downloadButton.Parent; parentElement.RemoveChild(downloadButton); var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), parentElement.RectTransform), TextManager.Get("WorkshopItemDownloading")); SteamManager.SubscribeToWorkshopItem(item.Value.Id, () => { if (SteamManager.InstallWorkshopItem(item, out _)) { textBlock.Text = TextManager.Get("workshopiteminstalled"); frame.Flash(GUI.Style.Green); } else { frame.Flash(GUI.Style.Red); } RefreshSubscribedItems(); }); return true; } private void ShowItemPreview(Steamworks.Ugc.Item? item, GUIFrame itemPreviewFrame) { itemPreviewFrame.ClearChildren(); if (item == null) { return; } var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), itemPreviewFrame.RectTransform, Anchor.Center)) { Stretch = true, UserData = item }; var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), content.RectTransform)) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerArea.RectTransform), item?.Title, textAlignment: Alignment.CenterLeft, font: GUI.LargeFont, wrap: true); new GUITextBlock(new RectTransform(new Vector2(0.3f, 0.0f), headerArea.RectTransform), item?.Owner.Name, textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont); var btn = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), headerArea.RectTransform, Anchor.CenterRight), TextManager.Get("WorkshopShowItemInSteam"), style: "GUIButtonSmall") { IgnoreLayoutGroups = true, OnClicked = (btn, userdata) => { SteamManager.OverlayCustomURL("steam://url/CommunityFilePage/" + item?.Id); return true; } }; //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), style: null); new GUIFrame(new RectTransform(new Vector2(1.0f, 0.005f), content.RectTransform), style: "HorizontalLine"); //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), style: null); //--------------- var centerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.01f }; if (itemPreviewSprites.ContainsKey(item?.PreviewImageUrl)) { new GUIImage(new RectTransform(new Vector2(0.5f, 1.0f), centerArea.RectTransform), itemPreviewSprites[item?.PreviewImageUrl], scaleToFit: true); } else { new GUIImage(new RectTransform(new Vector2(0.5f, 0.0f), centerArea.RectTransform), SteamManager.DefaultPreviewImage, scaleToFit: true); } var statsFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), centerArea.RectTransform), style: "GUIFrameListBox"); var statsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), statsFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.01f }; //score ------------------------------------- var scoreContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), statsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true, RelativeSpacing = 0.02f }; new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.0f), scoreContainer.RectTransform), TextManager.Get("WorkshopItemScore"), font: GUI.SubHeadingFont); int starCount = (int)Math.Round((item?.Score ?? 0.0f) * 5); for (int i = 0; i < 5; i++) { new GUIImage(new RectTransform(new Point(scoreContainer.Rect.Height), scoreContainer.RectTransform), i < starCount ? "GUIStarIconBright" : "GUIStarIconDark"); } new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.0f), scoreContainer.RectTransform), TextManager.GetWithVariable("WorkshopItemVotes", "[votecount]", (item.Value.VotesUp + item.Value.VotesDown).ToString()), textAlignment: Alignment.CenterRight); //tags ------------------------------------ List tags = new List(); for (int i = 0; i < item?.Tags.Length && i < 5; i++) { if (string.IsNullOrEmpty(item?.Tags[i])) { continue; } string tag = TextManager.Get("Workshop.ContentTag." + item?.Tags[i].Replace(" ", ""), true); if (string.IsNullOrEmpty(tag)) { tag = item?.Tags[i].CapitaliseFirstInvariant(); } tags.Add(tag); } if (tags.Count > 0) { var tagContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), statsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true, RelativeSpacing = 0.05f, CanBeFocused = true }; new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), tagContainer.RectTransform), TextManager.Get("WorkshopItemTags"), font: GUI.SubHeadingFont); var t = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), tagContainer.RectTransform, Anchor.TopRight), string.Join(", ", tags), textAlignment: Alignment.CenterRight); t.RectTransform.SizeChanged += () => { t.TextScale = 1.0f; t.AutoScaleHorizontal = true; }; } var fileSize = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), statsContent.RectTransform), TextManager.Get("WorkshopItemFileSize"), font: GUI.SubHeadingFont); new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), fileSize.RectTransform, Anchor.TopRight), MathUtils.GetBytesReadable(item?.IsInstalled ?? false ? (long)item.Value.SizeBytes : item.Value.DownloadBytesDownloaded), textAlignment: Alignment.CenterRight); //var dateContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), isHorizontal: true); var creationDate = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), statsContent.RectTransform), TextManager.Get("WorkshopItemCreationDate"), font: GUI.SubHeadingFont); new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), creationDate.RectTransform, Anchor.CenterRight), item?.Created.ToString("dd.MM.yyyy"), textAlignment: Alignment.CenterRight); var modificationDate = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), statsContent.RectTransform), TextManager.Get("WorkshopItemModificationDate"), font: GUI.SubHeadingFont); new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), modificationDate.RectTransform, Anchor.CenterRight), item?.Updated.ToString("dd.MM.yyyy"), textAlignment: Alignment.CenterRight); if (item?.IsSubscribed ?? false) { var buttonContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), statsContent.RectTransform), style: null); var unsubscribeButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.95f), buttonContainer.RectTransform, Anchor.Center), TextManager.Get("WorkshopItemUnsubscribe"), style: "GUIButtonSmall") { UserData = item, OnClicked = (btn, userdata) => { subscribePollAdditionalWait += 1.0f; item?.Unsubscribe(); SteamManager.UninstallWorkshopItem(item, true, out _); subscribedItemList.RemoveChild(subscribedItemList.Content.GetChildByUserData(item)); itemPreviewFrame.ClearChildren(); return true; } }; buttonContainer.RectTransform.MinSize = unsubscribeButton.RectTransform.MinSize; statsContent.Recalculate(); } //------------------ //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), style: null); var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform)) { ScrollBarVisible = true }; //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), descriptionContainer.Content.RectTransform) { MinSize = new Point(0, 5) }, style: null); string description = item?.Description; description = ToolBox.RemoveBBCodeTags(description); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), descriptionContainer.Content.RectTransform), description, wrap: true) { CanBeFocused = false }; } private void CreateWorkshopItem(SubmarineInfo sub) { string destinationFolder = Path.Combine("Mods", sub.Name.Trim()); itemContentPackage = ContentPackage.CreatePackage(sub.Name, Path.Combine(destinationFolder, SteamManager.MetadataFileName), corePackage: false); SteamManager.CreateWorkshopItemStaging(itemContentPackage, out itemEditor); bool fileMoved = false; string submarineDir = Path.GetDirectoryName(sub.FilePath); if (submarineDir != Path.GetDirectoryName(destinationFolder)) { string destinationPath = Path.Combine(destinationFolder, Path.GetFileName(sub.FilePath)); if (!File.Exists(destinationPath)) { File.Move(sub.FilePath, destinationPath); } fileMoved = true; sub.FilePath = destinationPath; } itemContentPackage.AddFile(sub.FilePath, ContentType.Submarine); itemContentPackage.Name = sub.Name; itemContentPackage.Save(itemContentPackage.Path); if (fileMoved) { GameMain.Config.EnableRegularPackage(itemContentPackage); } itemEditor = itemEditor?.WithTitle(sub.Name).WithTag("Submarine").WithDescription(sub.Description); if (sub.PreviewImage != null) { string previewImagePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(itemContentPackage.Path), SteamManager.PreviewImageName)); try { using (System.IO.Stream s = File.Create(previewImagePath)) { sub.PreviewImage.Texture.SaveAsPng(s, (int)sub.PreviewImage.size.X, (int)sub.PreviewImage.size.Y); itemEditor = itemEditor?.WithPreviewFile(previewImagePath); } if (new FileInfo(previewImagePath).Length > 1024 * 1024) { new GUIMessageBox(TextManager.Get("Error"), TextManager.Get("WorkshopItemPreviewImageTooLarge")); itemEditor = itemEditor?.WithPreviewFile(SteamManager.DefaultPreviewImagePath); } } catch (Exception e) { DebugConsole.ThrowError("Saving submarine preview image failed.", e); itemEditor = itemEditor?.WithPreviewFile(null); } } } private void CreateWorkshopItem(ContentPackage contentPackage) { //SteamManager.CreateWorkshopItemStaging(new List(), out itemEditor, out itemContentPackage); itemContentPackage = contentPackage; SteamManager.CreateWorkshopItemStaging(itemContentPackage, out itemEditor); itemEditor = itemEditor?.WithTitle(contentPackage.Name); /*string modDirectory = ""; foreach (ContentFile file in contentPackage.Files) { itemContentPackage.AddFile(file.Path, file.Type); //if some of the content files are in a subdirectory of the Mods folder, //assume that directory contains mod files for this package and copy them to the staging folder if (modDirectory == "" && ContentPackage.IsModFilePathAllowed(file.Path)) { string directoryName = Path.GetDirectoryName(file.Path); string[] splitPath = directoryName.Split(Path.DirectorySeparatorChar); if (splitPath.Length >= 2 && splitPath[0] == "Mods") { modDirectory = splitPath[1]; } } } if (!string.IsNullOrEmpty(modDirectory)) { SaveUtil.CopyFolder(Path.Combine("Mods", modDirectory), Path.Combine(SteamManager.WorkshopItemStagingFolder, "Mods", modDirectory), copySubDirs: true); }*/ } private bool CreateWorkshopItem(Steamworks.Ugc.Item? item) { if (!(item?.IsInstalled ?? false)) { new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariable("WorkshopErrorInstallRequiredToEdit", "[itemname]", (item?.Title ?? "[NULL]"))); return false; } if (!SteamManager.CreateWorkshopItemStaging(item, out itemEditor, out itemContentPackage)) { return false; } var tickBox = publishedItemList.Content.GetChildByUserData(item)?.GetAnyChild(); if (tickBox != null) { tickBox.Selected = true; } return true; } private void ShowCreateItemFrame() { createItemFrame.ClearChildren(); if (itemEditor == null) { return; } if (itemContentPackage == null) { string errorMsg = "Failed to edit workshop item (content package null)\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("SteamWorkshopScreen.ShowCreateItemFrame:ContentPackageNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } var createItemContent = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.98f), createItemFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; var topPanel = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), createItemContent.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.01f }; var topLeftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), topPanel.RectTransform)) { Stretch = true, RelativeSpacing = 0.02f }; var topRightColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), topPanel.RectTransform)) { Stretch = true, RelativeSpacing = 0.02f }; // top right column -------------------------------------------------------------------------------------- new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), topRightColumn.RectTransform), TextManager.Get("WorkshopItemTitle"), font: GUI.SubHeadingFont); var titleBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.15f), topRightColumn.RectTransform), itemEditor?.Title); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), topRightColumn.RectTransform), TextManager.Get("WorkshopItemDescription"), font: GUI.SubHeadingFont); var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), topRightColumn.RectTransform)); var descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform), itemEditor?.Description, textAlignment: Alignment.TopLeft, style: "GUITextBoxNoBorder", font: GUI.SmallFont, wrap: true); descriptionBox.OnTextChanged += (textBox, text) => { Vector2 textSize = textBox.Font.MeasureString(descriptionBox.WrappedText); textBox.RectTransform.NonScaledSize = new Point(textBox.RectTransform.NonScaledSize.X, Math.Max(descriptionContainer.Content.Rect.Height, (int)textSize.Y + 10)); descriptionContainer.UpdateScrollBarSize(); descriptionContainer.BarScroll = 1.0f; itemEditor = itemEditor?.WithDescription(text); return true; }; descriptionContainer.RectTransform.SizeChanged += () => { descriptionBox.Text = descriptionBox.Text; }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), topRightColumn.RectTransform), TextManager.Get("WorkshopItemTags"), font: GUI.SubHeadingFont); var tagHolder = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.17f), topRightColumn.RectTransform) { MinSize = new Point(0, 50) }, isHorizontal: true) { Spacing = 5 }; HashSet availableTags = new HashSet(); foreach (string tag in itemEditor?.Tags ?? Enumerable.Empty()) { if (!string.IsNullOrEmpty(tag)) { availableTags.Add(tag.ToLowerInvariant()); } } foreach (string tag in SteamManager.PopularTags) { if (!string.IsNullOrEmpty(tag)) { availableTags.Add(tag.ToLowerInvariant()); } if (availableTags.Count > 10) { break; } } foreach (string tag in availableTags) { var tagBtn = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), tagHolder.Content.RectTransform, anchor: Anchor.CenterLeft), tag.CapitaliseFirstInvariant(), style: "GUIButtonRound"); tagBtn.TextBlock.AutoScaleHorizontal = true; tagBtn.Selected = itemEditor?.Tags?.Any(t => t.Equals(tag, StringComparison.OrdinalIgnoreCase)) ?? false; tagBtn.OnClicked = (btn, userdata) => { if (!tagBtn.Selected) { if (!(itemEditor?.Tags?.Any(t => t.ToLowerInvariant() == tag) ?? false)) { itemEditor = itemEditor?.WithTag(tagBtn.Text); } tagBtn.Selected = true; } else { itemEditor?.Tags?.RemoveAll(t => t.Equals(tagBtn.Text, StringComparison.OrdinalIgnoreCase)); tagBtn.Selected = false; } return true; }; } tagHolder.UpdateScrollBarSize(); // top left column -------------------------------------------------------------------------------------- new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), topLeftColumn.RectTransform), TextManager.Get("WorkshopItemPreviewImage"), font: GUI.SubHeadingFont); previewIcon = new GUIImage(new RectTransform(new Vector2(1.0f, 0.7f), topLeftColumn.RectTransform), SteamManager.DefaultPreviewImage, scaleToFit: true); new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), topLeftColumn.RectTransform), TextManager.Get("WorkshopItemBrowse"), style: "GUIButtonSmall") { OnClicked = (btn, userdata) => { FileSelection.OnFileSelected = (file) => { OnPreviewImageSelected(previewIcon, file); }; FileSelection.ClearFileTypeFilters(); FileSelection.AddFileTypeFilter("PNG", "*.png"); FileSelection.AddFileTypeFilter("JPEG", "*.jpg, *.jpeg"); FileSelection.AddFileTypeFilter("All files", "*.*"); FileSelection.SelectFileTypeFilter("*.png"); FileSelection.Open = true; return true; } }; //if preview image has not been set, but there's a PreviewImage file inside the mod folder, use that by default if (string.IsNullOrEmpty(itemEditor?.PreviewFile)) { string previewImagePath = Path.Combine(Path.GetDirectoryName(itemContentPackage.Path), SteamManager.PreviewImageName); if (File.Exists(previewImagePath)) { itemEditor = itemEditor?.WithPreviewFile(Path.GetFullPath(previewImagePath)); } } if (!string.IsNullOrEmpty(itemEditor?.PreviewFile)) { itemEditor = itemEditor?.WithPreviewFile(Path.GetFullPath(itemEditor?.PreviewFile)); if (itemPreviewSprites.ContainsKey(itemEditor?.PreviewFile)) { itemPreviewSprites[itemEditor?.PreviewFile].Remove(); } var newPreviewImage = new Sprite(itemEditor?.PreviewFile, sourceRectangle: null); previewIcon.Sprite = newPreviewImage; itemPreviewSprites[itemEditor?.PreviewFile] = newPreviewImage; } new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), topLeftColumn.RectTransform), TextManager.Get("WorkshopItemCorePackage")) { ToolTip = TextManager.Get("WorkshopItemCorePackageTooltip"), Selected = itemContentPackage.IsCorePackage, OnSelected = (tickbox) => { if (tickbox.Selected) { if (!itemContentPackage.ContainsRequiredCorePackageFiles(out List missingContentTypes)) { new GUIMessageBox( TextManager.Get("Error"), TextManager.GetWithVariables("ContentPackageCantMakeCorePackage", new string[2] { "[packagename]", "[missingfiletypes]" }, new string[2] { itemContentPackage.Name, string.Join(", ", missingContentTypes) }, new bool[2] { false, true })); tickbox.Selected = false; } else { itemContentPackage.IsCorePackage = tickbox.Selected; } } else { itemContentPackage.IsCorePackage = false; } return true; } }; // file list -------------------------------------------------------------------------------------- //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), createItemContent.RectTransform), style: null); var fileListTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), createItemContent.RectTransform), TextManager.Get("WorkshopItemFiles"), font: GUI.SubHeadingFont); new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), fileListTitle.RectTransform, Anchor.CenterRight), TextManager.Get("WorkshopItemShowFolder"), style: "GUIButtonSmall") { IgnoreLayoutGroups = true, OnClicked = (btn, userdata) => { ToolBox.OpenFileWithShell(Path.GetFullPath(Path.GetDirectoryName(itemContentPackage.Path))); return true; } }; createItemFileList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.35f), createItemContent.RectTransform)); createItemWatcher?.Dispose(); createItemWatcher = new System.IO.FileSystemWatcher(Path.GetDirectoryName(itemContentPackage.Path)) { Filter = "*", NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName }; createItemWatcher.Created += OnFileSystemChanges; createItemWatcher.Deleted += OnFileSystemChanges; createItemWatcher.Renamed += OnFileSystemChanges; createItemWatcher.EnableRaisingEvents = true; RefreshCreateItemFileList(); var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), createItemContent.RectTransform), isHorizontal: true) { RelativeSpacing = 0.02f }; new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonContainer.RectTransform, Anchor.TopRight), TextManager.Get("WorkshopItemRefreshFileList"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("WorkshopItemRefreshFileListTooltip"), OnClicked = (btn, userdata) => { itemContentPackage = new ContentPackage(itemContentPackage.Path); RefreshCreateItemFileList(); return true; } }; new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonContainer.RectTransform, Anchor.TopRight), TextManager.Get("WorkshopItemAddFiles"), style: "GUIButtonSmall") { OnClicked = (btn, userdata) => { FileSelection.OnFileSelected = (file) => { OnAddFilesSelected(new string[] { file }); }; FileSelection.ClearFileTypeFilters(); FileSelection.AddFileTypeFilter("PNG", "*.png"); FileSelection.AddFileTypeFilter("JPEG", "*.jpg, *.jpeg"); FileSelection.AddFileTypeFilter("OGG", "*.ogg"); FileSelection.AddFileTypeFilter("XML", "*.xml"); FileSelection.AddFileTypeFilter("TXT", "*.txt"); FileSelection.AddFileTypeFilter("All files", "*.*"); FileSelection.SelectFileTypeFilter("*.*"); FileSelection.Open = true; return true; } }; //the item has been already published if it has a non-zero ID -> allow adding a changenote if ((itemEditor?.FileId ?? 0) > 0) { var bottomRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), createItemContent.RectTransform), isHorizontal: true); var changeNoteLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), bottomRow.RectTransform)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), changeNoteLayout.RectTransform), TextManager.Get("WorkshopItemChangenote"), font: GUI.SubHeadingFont) { ToolTip = TextManager.Get("WorkshopItemChangenoteTooltip") }; var changenoteContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.75f), changeNoteLayout.RectTransform)); var changenoteBox = new GUITextBox(new RectTransform(Vector2.One, changenoteContainer.Content.RectTransform), "", textAlignment: Alignment.TopLeft, style: "GUITextBoxNoBorder", wrap: true) { ToolTip = TextManager.Get("WorkshopItemChangenoteTooltip") }; changenoteBox.OnTextChanged += (textBox, text) => { Vector2 textSize = textBox.Font.MeasureString(changenoteBox.WrappedText); textBox.RectTransform.NonScaledSize = new Point(textBox.RectTransform.NonScaledSize.X, Math.Max(changenoteContainer.Content.Rect.Height, (int)textSize.Y + 10)); changenoteContainer.UpdateScrollBarSize(); changenoteContainer.BarScroll = 1.0f; itemEditor = itemEditor?.WithChangeLog(text); return true; }; } var bottomButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), createItemContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.03f }; var visibilityLabel = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), bottomButtonContainer.RectTransform), TextManager.Get("WorkshopItemVisibility"), textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) { ToolTip = TextManager.Get("WorkshopItemVisibilityTooltip") }; visibilityLabel.RectTransform.MaxSize = new Point((int)(visibilityLabel.TextSize.X * 1.1f), 0); var visibilityDropDown = new GUIDropDown(new RectTransform(new Vector2(0.2f, 1.0f), bottomButtonContainer.RectTransform)); foreach (VisibilityType visibilityType in Enum.GetValues(typeof(VisibilityType))) { visibilityDropDown.AddItem(TextManager.Get("WorkshopItemVisibility." + visibilityType), visibilityType); } visibilityDropDown.SelectItem(itemEditor.Value.IsPublic ? VisibilityType.Public : itemEditor.Value.IsFriendsOnly ? VisibilityType.FriendsOnly : VisibilityType.Private); visibilityDropDown.OnSelected = (c, ud) => { if (!(ud is VisibilityType visibilityType)) { return false; } switch (visibilityType) { case VisibilityType.Public: itemEditor = itemEditor?.WithPublicVisibility(); break; case VisibilityType.FriendsOnly: itemEditor = itemEditor?.WithFriendsOnlyVisibility(); break; case VisibilityType.Private: itemEditor = itemEditor?.WithPrivateVisibility(); break; } return true; }; if ((itemEditor?.FileId ?? 0) > 0) { new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), bottomButtonContainer.RectTransform), TextManager.Get("WorkshopItemDelete"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("WorkshopItemDeleteTooltip"), TextColor = GUI.Style.Red, OnClicked = (btn, userData) => { if (itemEditor == null) { return false; } var deleteVerification = new GUIMessageBox("", TextManager.GetWithVariable("WorkshopItemDeleteVerification", "[itemname]", itemEditor?.Title), new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); deleteVerification.Buttons[0].OnClicked = (yesBtn, userdata) => { if (itemEditor == null) { return false; } RemoveItemFromLists(itemEditor.Value.FileId); TaskPool.Add("DeleteFileAsync", Steamworks.SteamUGC.DeleteFileAsync(itemEditor.Value.FileId), (t) => { if (t.Status == TaskStatus.Faulted) { TaskPool.PrintTaskExceptions(t, "Failed to delete Workshop item " + (itemEditor?.Title ?? "[NULL]")); return; } }); itemEditor = null; SelectTab(Tab.Browse); deleteVerification.Close(); createItemFrame.ClearChildren(); itemContentPackage.SteamWorkshopId = 0; itemContentPackage.Save(itemContentPackage.Path); return true; }; deleteVerification.Buttons[1].OnClicked = deleteVerification.Close; return true; } }; } var publishBtn = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), bottomButtonContainer.RectTransform, Anchor.CenterRight), TextManager.Get((itemEditor?.FileId ?? 0) > 0 ? "WorkshopItemUpdate" : "WorkshopItemPublish")) { IgnoreLayoutGroups = true, ToolTip = TextManager.Get("WorkshopItemPublishTooltip"), OnClicked = (btn, userData) => { itemEditor = itemEditor?.WithTitle(titleBox.Text); itemEditor = itemEditor?.WithDescription(descriptionBox.Text); if (string.IsNullOrWhiteSpace(itemEditor?.Title)) { titleBox.Flash(GUI.Style.Red); return false; } if (string.IsNullOrWhiteSpace(itemEditor?.Description)) { descriptionBox.Flash(GUI.Style.Red); return false; } if (createItemFileList.Content.CountChildren == 0) { createItemFileList.Flash(GUI.Style.Red); } if (!itemContentPackage.CheckErrors(out List errorMessages)) { new GUIMessageBox( TextManager.GetWithVariable("workshopitempublishfailed", "[itemname]", itemEditor?.Title), string.Join("\n", errorMessages)); return false; } PublishWorkshopItem(); return true; } }; publishBtn.TextBlock.AutoScaleHorizontal = true; } private void OnPreviewImageSelected(GUIImage previewImageElement, string filePath) { string previewImagePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(itemContentPackage.Path), SteamManager.PreviewImageName)); if (new FileInfo(filePath).Length > 1024 * 1024) { new GUIMessageBox(TextManager.Get("Error"), TextManager.Get("WorkshopItemPreviewImageTooLarge")); return; } if (Path.GetFullPath(filePath) != previewImagePath) { try { File.Copy(filePath, previewImagePath, overwrite: true); } catch (System.IO.IOException e) { DebugConsole.ThrowError("Failed to copy the preview image \"{previewImagePath}\" to the mod folder.", e); return; } } if (itemPreviewSprites.ContainsKey(previewImagePath)) { itemPreviewSprites[previewImagePath].Remove(); } var newPreviewImage = new Sprite(previewImagePath, sourceRectangle: null); previewImageElement.Sprite = newPreviewImage; itemPreviewSprites[previewImagePath] = newPreviewImage; itemEditor?.WithPreviewFile(previewImagePath); } private void OnAddFilesSelected(string[] fileNames) { if (fileNames == null) { return; } for (int i = 0; i < fileNames.Length; i++) { string file = fileNames[i]?.Trim(); if (string.IsNullOrEmpty(file) || !File.Exists(file)) { continue; } string modFolder = Path.GetDirectoryName(itemContentPackage.Path); string filePathRelativeToModFolder = Path.GetRelativePath(Path.Combine(Environment.CurrentDirectory, modFolder), file); //file is not inside the mod folder, we need to move it if (filePathRelativeToModFolder.StartsWith("..") || Path.GetPathRoot(Environment.CurrentDirectory) != Path.GetPathRoot(file)) { string destinationPath = Path.Combine(modFolder, Path.GetFileName(file)); //add a number to the filename if a file with the same name already exists i = 2; while (File.Exists(destinationPath)) { destinationPath = Path.Combine(modFolder, $"{Path.GetFileNameWithoutExtension(file)} ({i}){Path.GetExtension(file)}"); i++; } try { File.Copy(file, destinationPath); } catch (Exception e) { DebugConsole.ThrowError("Copying the file \"" + file + "\" to the mod folder failed.", e); return; } } } RefreshCreateItemFileList(); } volatile bool refreshFileList = false; private void OnFileSystemChanges(object sender, System.IO.FileSystemEventArgs e) { refreshFileList = true; } private void RefreshCreateItemFileList() { createItemFileList.ClearChildren(); if (itemContentPackage == null) return; var contentTypes = Enum.GetValues(typeof(ContentType)); List files = itemContentPackage.FilesUnsaved.ToList(); for (int i = files.Count - 1; i >= 0; i--) { ContentFile contentFile = files[i]; bool fileExists = File.Exists(contentFile.Path); if (contentFile.Type == ContentType.ServerExecutable) { fileExists |= File.Exists(Path.GetFileNameWithoutExtension(contentFile.Path) + ".dll"); } if (!fileExists) { itemContentPackage.RemoveFile(contentFile); files.RemoveAt(i); } } List allFiles = Directory.GetFiles(Path.GetDirectoryName(itemContentPackage.Path), "*", System.IO.SearchOption.AllDirectories) .Select(f => new ContentFile(f, ContentType.None)) .Where(file => Path.GetFileName(file.Path) != SteamManager.MetadataFileName && Path.GetFileName(file.Path) != SteamManager.PreviewImageName) .ToList(); for (int i=0;i string.Equals(Path.GetFullPath(f.Path).CleanUpPath(), Path.GetFullPath(file.Path).CleanUpPath(), StringComparison.InvariantCultureIgnoreCase)); if (otherFile != null) { //replace the generated ContentFile object with the one that's present in the //content package to determine which tickboxes should already be checked allFiles[i] = otherFile; files.Remove(otherFile); } } allFiles.AddRange(files); foreach (ContentFile contentFile in allFiles) { bool illegalPath = !ContentPackage.IsModFilePathAllowed(contentFile); bool fileExists = File.Exists(contentFile.Path); if (contentFile.Type == ContentType.ServerExecutable) { fileExists |= File.Exists(Path.GetFileNameWithoutExtension(contentFile.Path) + ".dll"); } var fileFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.12f), createItemFileList.Content.RectTransform) { MinSize = new Point(0, 20) }, style: "ListBoxElement") { CanBeFocused = false, UserData = contentFile }; var content = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 1.0f), fileFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true, RelativeSpacing = 0.05f }; var tickBox = new GUITickBox(new RectTransform(Vector2.One, content.RectTransform, scaleBasis: ScaleBasis.BothHeight), "") { Selected = itemContentPackage.FilesUnsaved.Contains(contentFile), UserData = contentFile }; tickBox.OnSelected = (tb) => { ContentFile f = tb.UserData as ContentFile; if (tb.Selected) { if (!itemContentPackage.FilesUnsaved.Contains(f)) { itemContentPackage.AddFile(f); } } else { if (itemContentPackage.FilesUnsaved.Contains(f)) { itemContentPackage.RemoveFile(f); } } return true; }; var nameText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), content.RectTransform, Anchor.CenterLeft), contentFile.Path, font: GUI.SmallFont) { ToolTip = contentFile.Path }; if (!fileExists) { nameText.TextColor = GUI.Style.Red; tickBox.ToolTip = TextManager.Get("WorkshopItemFileNotFound"); } else if (illegalPath && !ContentPackage.AllPackages.Any(cp => cp.FilesUnsaved.Any(f => Path.GetFullPath(f.Path) == Path.GetFullPath(contentFile.Path)))) { nameText.TextColor = GUI.Style.Red; tickBox.ToolTip = TextManager.Get("WorkshopItemIllegalPath"); } var contentTypeSelection = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f), content.RectTransform, Anchor.CenterRight), elementCount: contentTypes.Length) { UserData = contentFile, }; foreach (ContentType contentType in contentTypes) { contentTypeSelection.AddItem(contentType.ToString(), contentType); } contentTypeSelection.SelectItem(contentFile.Type); contentTypeSelection.OnSelected = (GUIComponent selected, object userdata) => { ((ContentFile)contentTypeSelection.UserData).Type = (ContentType)userdata; itemContentPackage.Save(itemContentPackage.Path); return true; }; if (!files.Contains(contentFile)) //this prevents deletion of files not contained in the mod's path (i.e. vanilla content) { new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), content.RectTransform), TextManager.Get("Delete"), style: "GUIButtonSmall") { OnClicked = (btn, userdata) => { var msgBox = new GUIMessageBox(TextManager.Get("ConfirmFileDeletionHeader"), TextManager.GetWithVariable("ConfirmFileDeletion", "[file]", contentFile.Path), new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) { UserData = "verificationprompt" }; msgBox.Buttons[0].OnClicked = (applyButton, obj) => { try { File.Delete(contentFile.Path); if (contentFile.Type == ContentType.Submarine) { SubmarineInfo.RefreshSavedSub(contentFile.Path); } } catch (Exception e) { DebugConsole.ThrowError($"Failed to delete \"${contentFile.Path}\".", e); } //RefreshCreateItemFileList(); RefreshMyItemList(); return true; }; msgBox.Buttons[0].OnClicked += msgBox.Close; msgBox.Buttons[1].OnClicked = msgBox.Close; return true; } }; } content.Recalculate(); fileFrame.RectTransform.MinSize = new Point(0, (int)(content.RectTransform.Children.Max(c => c.MinSize.Y) / content.RectTransform.RelativeSize.Y)); nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, maxWidth: nameText.Rect.Width); } itemContentPackage.Save(itemContentPackage.Path); } private void PublishWorkshopItem() { if (itemContentPackage == null || itemEditor == null) { return; } #if UNSTABLE var msgBox = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("unstableworkshopitempublishwarning"), new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked = (btn, userdata) => { var workshopPublishStatus = SteamManager.StartPublishItem(itemContentPackage, itemEditor); if (workshopPublishStatus != null) { if (!(itemEditor?.HasTag("unstable") ?? false)) { itemEditor = itemEditor?.WithTag("unstable"); } CoroutineManager.StartCoroutine(WaitForPublish(workshopPublishStatus), "WaitForPublish"); } msgBox.Close(); return true; }; msgBox.Buttons[1].OnClicked += msgBox.Close; #else itemEditor = itemEditor?.WithoutTag("unstable"); var workshopPublishStatus = SteamManager.StartPublishItem(itemContentPackage, itemEditor); if (workshopPublishStatus == null) { return; } CoroutineManager.StartCoroutine(WaitForPublish(workshopPublishStatus), "WaitForPublish"); #endif } private IEnumerable WaitForPublish(SteamManager.WorkshopPublishStatus workshopPublishStatus) { var item = workshopPublishStatus.Item; var coroutine = workshopPublishStatus.Coroutine; string pleaseWaitText = TextManager.Get("WorkshopPublishPleaseWait"); var msgBox = new GUIMessageBox( pleaseWaitText, TextManager.GetWithVariable("WorkshopPublishInProgress", "[itemname]", item?.Title), new string[] { TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked = (btn, userdata) => { CoroutineManager.StopCoroutines("WaitForPublish"); createItemFrame.ClearChildren(); SelectTab(Tab.Browse); msgBox.Close(); return true; }; yield return CoroutineStatus.Running; while (CoroutineManager.IsCoroutineRunning(coroutine)) { msgBox.Header.Text = pleaseWaitText + new string('.', ((int)Timing.TotalTime % 3 + 1)); yield return CoroutineStatus.Running; } msgBox.Close(); if (workshopPublishStatus.Success ?? false) { new GUIMessageBox("", TextManager.GetWithVariable("WorkshopItemPublished", "[itemname]", item?.Title)); } else { string errorMsg = workshopPublishStatus.Result.HasValue ? TextManager.GetWithVariable("WorkshopPublishError." + workshopPublishStatus.Result?.Result.ToString(), "[savepath]", SaveUtil.SaveFolder, returnNull: true) : null; if (errorMsg == null) { new GUIMessageBox( TextManager.Get("Error"), TextManager.GetWithVariable("WorkshopItemPublishFailed", "[itemname]", item?.Title) + (workshopPublishStatus?.TaskStatus != null ? " Task ended with status " +workshopPublishStatus?.TaskStatus?.ToString() : " Publish failed with result "+ workshopPublishStatus.Result?.Result.ToString())); } else { new GUIMessageBox(TextManager.Get("Error"), errorMsg); } } createItemFrame.ClearChildren(); SelectTab(Tab.Browse); } #region UI management public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { graphics.Clear(Color.CornflowerBlue); GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); GUI.Draw(Cam, spriteBatch); spriteBatch.End(); } public override void AddToGUIUpdateList() { menu.AddToGUIUpdateList(); } public override void Update(double deltaTime) { if (refreshFileList) { RefreshCreateItemFileList(); refreshFileList = false; } } #endregion } }