diff --git a/README.md b/README.md index f66b457..b07e9f6 100644 --- a/README.md +++ b/README.md @@ -42,20 +42,6 @@ It is recommended to subscribe to the NML item on the Steam Workshop to receive 3. enable experimental mode and start the game -### Automatic Installation - -The installer is available for both Linux and Windows. -Click the links below to download: -* [Linux Installer](https://github.com/gmBlahaj/nml-installer/releases/download/nml/nml-setup-linux) -* [Windows Installer](https://github.com/gmBlahaj/nml-installer/releases/download/nml/nml-setup-win.exe) - -
- -### Downgrade Tool - -If you're trying to play mods that aren't compatible with the latest version of Worldbox, you can use **Worldbox Rewind** to downgrade to an older version: -* [Download Worldbox Rewind](https://gmblahaj.xyz/pages/rewind) -

Others

diff --git a/api/ModDeclare.cs b/api/ModDeclare.cs index b4ecfab..89aecf7 100644 --- a/api/ModDeclare.cs +++ b/api/ModDeclare.cs @@ -127,6 +127,7 @@ public ModDeclare(string pFilePath) AICheckTranslation = modDeclare.AICheckTranslation; AICheckAudio = modDeclare.AICheckAudio; Tags = modDeclare.Tags ?? new List(); + SteamId = modDeclare.SteamId; Dependencies ??= Array.Empty(); OptionalDependencies ??= Array.Empty(); @@ -154,6 +155,10 @@ public ModDeclare(string pFilePath) Regex regex = new(@"^\d+$"); if (!regex.IsMatch(pathSegments[++currentSearchIndex])) return; IsWorkshopLoaded = true; + if (SteamId == 0 && ulong.TryParse(pathSegments[currentSearchIndex], out ulong parsedId)) + { + SteamId = parsedId; + } } /// @@ -345,6 +350,9 @@ public string GetAIAttributionDisplay() }; } + [JsonProperty("steam_id")] + public ulong SteamId { get; internal set; } + private List _tags = new(); [JsonProperty("tags")] @@ -375,11 +383,9 @@ public void Save() { try { + if (string.IsNullOrEmpty(FolderPath) || !Directory.Exists(FolderPath)) return; string jsonPath = Path.Combine(FolderPath, "mod.json"); - if (File.Exists(jsonPath)) - { - File.WriteAllText(jsonPath, JsonConvert.SerializeObject(this, Formatting.Indented)); - } + File.WriteAllText(jsonPath, JsonConvert.SerializeObject(this, Formatting.Indented)); } catch (Exception e) { diff --git a/resources/commit b/resources/commit index e38f0b0..630218e 100644 --- a/resources/commit +++ b/resources/commit @@ -1 +1 @@ -4bb2ea1fa3af3aa45c8ebb7aefcf096a5bc3b2f0 +98377654d461586e413f2e9bc28ed3fa24514e27 diff --git a/resources/locales/ch.json b/resources/locales/ch.json index 3278ef7..f542049 100644 --- a/resources/locales/ch.json +++ b/resources/locales/ch.json @@ -6,6 +6,23 @@ "NeoModLoader Update Title": "NeoModLoader 更新", "NeoModLoader Update Complete": "NeoModLoader 更新至{0},\n 重啟遊戲以應用更新。", "ModUpload Title": "上傳模組", + "ModUpdate Title": "更新模組", + "ModUploadModeWindow Title": "Steam 創意工坊", + "ModUploadMode_Upload": "上傳", + "ModUploadMode_Update": "更新", + "ModUploadMode_ViewWorkshop": "查看創意工坊", + "ModUploadMode_Back": "返回", + "ModUploadMode_NewUpload": "新上傳", + "ModUploadMode_NoSteamIdPrompt": "未找到此模組的 Steam 創意工坊 ID。\n\n請在上傳視窗中輸入創意工坊 ID 以進行更新。", + "ModUploadMode_EnterId": "輸入 ID", + "ModUploadMode_WantToChange": "你想要修改任何內容嗎?", + "ModUploadMode_Yes": "是", + "ModUploadMode_No": "否", + "ModUploadMode_AiWarning": "確定嗎?\n\n如果你使用了 AI,在更新前需要修改免責聲明。", + "ModUploadMode_ChangeDisclaimer": "修改免責聲明", + "ModUploadMode_Cancel": "取消", + "Clear Steam ID": "清除 Steam ID", + "Clear Steam ID Confirm": "確定要清除綁定的創意工坊 ID 嗎?", "ModUpload_Categories_Title": "分類標籤 (點擊切換):", "Information Title": "提示", "AIConfirmWindow Title": "AI 內容免責聲明", diff --git a/resources/locales/cz.json b/resources/locales/cz.json index 1b82704..f4fff90 100644 --- a/resources/locales/cz.json +++ b/resources/locales/cz.json @@ -6,6 +6,23 @@ "NeoModLoader Update Title": "NeoModLoader 更新", "NeoModLoader Update Complete": "NeoModLoader 更新至{0},\n 重启游戏以应用更新。", "ModUpload Title": "上传模组", + "ModUpdate Title": "更新模组", + "ModUploadModeWindow Title": "Steam 创意工坊", + "ModUploadMode_Upload": "上传", + "ModUploadMode_Update": "更新", + "ModUploadMode_ViewWorkshop": "查看创意工坊", + "ModUploadMode_Back": "返回", + "ModUploadMode_NewUpload": "新上传", + "ModUploadMode_NoSteamIdPrompt": "未找到此模组的 Steam 创意工坊 ID。\n\n请在上传窗口中输入创意工坊 ID 以进行更新。", + "ModUploadMode_EnterId": "输入 ID", + "ModUploadMode_WantToChange": "你想要修改任何内容吗?", + "ModUploadMode_Yes": "是", + "ModUploadMode_No": "否", + "ModUploadMode_AiWarning": "确定吗?\n\n如果你使用了 AI,在更新前需要修改免责声明。", + "ModUploadMode_ChangeDisclaimer": "修改免责声明", + "ModUploadMode_Cancel": "取消", + "Clear Steam ID": "清除 Steam ID", + "Clear Steam ID Confirm": "确定要清除绑定的创意工坊 ID 吗?", "ModUpload_Categories_Title": "分类标签 (点击切换):", "Information Title": "信息", "AIConfirmWindow Title": "AI 内容免责声明", diff --git a/resources/locales/en.json b/resources/locales/en.json index f4a866b..db8a821 100644 --- a/resources/locales/en.json +++ b/resources/locales/en.json @@ -6,6 +6,23 @@ "NeoModLoader Update Title": "NeoModLoader Update", "NeoModLoader Update Complete": "NeoModLoader has been updated to version {0}.\nPlease restart the game to apply the update.", "ModUpload Title": "Upload Mod", + "ModUpdate Title": "Update Mod", + "ModUploadModeWindow Title": "Steam Workshop", + "ModUploadMode_Upload": "Upload", + "ModUploadMode_Update": "Update", + "ModUploadMode_ViewWorkshop": "View Workshop", + "ModUploadMode_Back": "Back", + "ModUploadMode_NewUpload": "New Upload", + "ModUploadMode_NoSteamIdPrompt": "No Steam Workshop ID found for this mod.\n\nPlease enter the Workshop ID in the upload window to update it.", + "ModUploadMode_EnterId": "Enter ID", + "ModUploadMode_WantToChange": "Do you want to change anything?", + "ModUploadMode_Yes": "Yes", + "ModUploadMode_No": "No", + "ModUploadMode_AiWarning": "Are you sure?\n\nIf you used AI, you need to change the disclaimer before updating.", + "ModUploadMode_ChangeDisclaimer": "Change Disclaimer", + "ModUploadMode_Cancel": "Cancel", + "Clear Steam ID": "Clear Steam ID", + "Clear Steam ID Confirm": "Are you sure you want to clear the associated Steam Workshop ID?", "ModUpload_Categories_Title": "Category Tags (Click to toggle):", "Information Title": "Information", "AIConfirmWindow Title": "AI Content Disclaimer", diff --git a/services/IPlatformSpecificModWorkshopService.cs b/services/IPlatformSpecificModWorkshopService.cs index b3b05ed..54186f0 100644 --- a/services/IPlatformSpecificModWorkshopService.cs +++ b/services/IPlatformSpecificModWorkshopService.cs @@ -10,8 +10,7 @@ internal interface IPlatformSpecificModWorkshopService Promise UploadMod(string name, string description, string previewImagePath, string workshopPath, string changelog, bool verified, string aiTag = null, List extraTags = null); - Promise EditMod(ulong fileID, string previewImagePath, string workshopPath, string changelog, - string aiTag = null, List extraTags = null); + Promise EditMod(ulong fileID, string previewImagePath, string workshopPath, string changelog, ModDeclare mod_decl); void FindSubscribedMods(); ModDeclare GetNextModFromWorkshopItem(); } \ No newline at end of file diff --git a/services/ModWorkshopService.cs b/services/ModWorkshopService.cs index 04df0be..a0b4998 100644 --- a/services/ModWorkshopService.cs +++ b/services/ModWorkshopService.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; using NeoModLoader.api; using NeoModLoader.api.attributes; using NeoModLoader.constants; @@ -38,10 +39,10 @@ private static void UploadModLoader(string changelog) /// /// Try to Upload a mod to Steam Workshop /// - public static Promise UploadMod(IMod mod, string changelog, bool verified = false) + public static Promise UploadMod(IMod mod, string changelog, bool verified = false, string uploadName = null) { ModDeclare mod_decl = mod.GetDeclaration(); - string name = mod_decl.Name; + string name = string.IsNullOrEmpty(uploadName) ? mod_decl.Name : uploadName; string description = BuildModDescription(mod_decl); string workshopPath = Path.Combine(SaveManager.generateMainPath("workshop_upload_mod") + mod_decl.UID); @@ -50,7 +51,6 @@ public static Promise UploadMod(IMod mod, string changelog, bool verified = fals string previewImagePath = PreparePreviewImage(mod_decl, workshopPath); - // This works for BepInEx mods File.WriteAllText(Path.Combine(workshopPath, "mod.json"), JsonConvert.SerializeObject(mod_decl, Formatting.Indented)); return workshopServiceBackend.UploadMod(name, description, previewImagePath, workshopPath, changelog, verified, mod_decl.GetAIAttributionDisplay(), mod_decl.Tags); @@ -68,10 +68,42 @@ public static Promise TryEditMod(ulong fileID, IMod mod, string changelog) File.WriteAllText(Path.Combine(workshopPath, "mod.json"), JsonConvert.SerializeObject(mod_decl, Formatting.Indented)); - return workshopServiceBackend.EditMod(fileID, previewImagePath, workshopPath, changelog, mod_decl.GetAIAttributionDisplay(), mod_decl.Tags); + return workshopServiceBackend.EditMod(fileID, previewImagePath, workshopPath, changelog, mod_decl); + } + + public static string UpdateDescriptionDisclaimer(string steamDescription, ModDeclare mod_decl) + { + string newDisclaimer = BuildAIDisclaimer(mod_decl); + if (string.IsNullOrEmpty(steamDescription)) + { + return BuildModDescription(mod_decl); + } + + string pattern = @"\[hr\]\[\/hr\](?:(?!\[hr\]\[\/hr\])[\s\S])*?(?:AI Content Disclosure|Authorship:|generative AI|AI Content)[\s\S]*?\[hr\]\[\/hr\]"; + if (Regex.IsMatch(steamDescription, pattern, RegexOptions.IgnoreCase)) + { + return Regex.Replace(steamDescription, pattern, newDisclaimer, RegexOptions.IgnoreCase); + } + + string partialPattern = @"(?:\[hr\]\[\/hr\]\s*)?(?:\[b\])?(?:AI Content Disclosure|Authorship:)[\s\S]*?(?:\[hr\]\[\/hr\]|(?=(?:ModLoader:|模组加载器:|\z)))"; + if (Regex.IsMatch(steamDescription, partialPattern, RegexOptions.IgnoreCase)) + { + return Regex.Replace(steamDescription, partialPattern, newDisclaimer, RegexOptions.IgnoreCase); + } + + string footerPattern = @"(?:\r?\n)+(?:ModLoader:\s*https?:\S+|模组加载器:\s*https?:\S+)[\s\S]*$"; + Match footerMatch = Regex.Match(steamDescription, footerPattern, RegexOptions.IgnoreCase); + if (footerMatch.Success) + { + string beforeFooter = steamDescription.Substring(0, footerMatch.Index).TrimEnd(); + string footer = steamDescription.Substring(footerMatch.Index).TrimStart(); + return beforeFooter + "\n\n" + newDisclaimer + "\n\n" + footer; + } + + return steamDescription.TrimEnd() + "\n\n" + newDisclaimer; } - private static string BuildModDescription(ModDeclare mod_decl) + public static string BuildModDescription(ModDeclare mod_decl) { string name = mod_decl.Name; string aiDisclaimer = BuildAIDisclaimer(mod_decl); @@ -84,7 +116,7 @@ private static string BuildModDescription(ModDeclare mod_decl) $"模组加载器: {CoreConstants.RepoURL}"; } - private static string BuildAIDisclaimer(ModDeclare mod_decl) + public static string BuildAIDisclaimer(ModDeclare mod_decl) { if (mod_decl.AIAttribution == "ai_made" || mod_decl.AIAttribution == "ai_assisted") { @@ -144,7 +176,8 @@ private static void CopyModFiles(string sourceFolder, string targetFolder) private static string PreparePreviewImage(ModDeclare mod_decl, string workshopPath) { string previewImagePath; - if (string.IsNullOrEmpty(mod_decl.IconPath)) + if (string.IsNullOrEmpty(mod_decl.IconPath) || + !File.Exists(Path.Combine(mod_decl.FolderPath, mod_decl.IconPath))) { using Stream icon_stream = Assembly.GetExecutingAssembly() .GetManifestResourceStream("NeoModLoader.resources.logo.png"); diff --git a/services/ModWorkshopServiceUnix.cs b/services/ModWorkshopServiceUnix.cs index 01e3f16..fe26a67 100644 --- a/services/ModWorkshopServiceUnix.cs +++ b/services/ModWorkshopServiceUnix.cs @@ -1,5 +1,6 @@ extern alias unixsteamwork; using System.Reflection; +using System.Threading.Tasks; using NeoModLoader.api; using NeoModLoader.constants; using NeoModLoader.ui; @@ -128,23 +129,62 @@ public Promise UploadMod(string name, string description, string previewImagePat return promise; } - public Promise EditMod(ulong fileID, string previewImagePath, string workshopPath, string changelog, - string aiTag = null, List extraTags = null) + public Promise EditMod(ulong fileID, string previewImagePath, string workshopPath, string changelog, ModDeclare mod_decl) { Promise promise = new(); - // Create Upload Files Descriptor + var progress = ModUploadingProgressWindow.ShowWindow(); + ExecuteEditMod(fileID, previewImagePath, workshopPath, changelog, mod_decl, promise, progress); + return promise; + } + + private async Task FetchUpdatedDescriptionAsync(ulong fileID, ModDeclare mod_decl) + { + try + { + Query q = Query.Items.WithFileId(fileID).WithLongDescription(true); + ResultPage? resultPage = await q.GetPageAsync(1); + if (resultPage.HasValue) + { + foreach (var entry in resultPage.Value.Entries) + { + if (!string.IsNullOrEmpty(entry.Description)) + { + return ModWorkshopService.UpdateDescriptionDisclaimer(entry.Description, mod_decl); + } + } + } + } + catch (Exception e) + { + LogService.LogWarning($"Failed to get existing description: {e.Message}"); + } + + return ModWorkshopService.BuildModDescription(mod_decl); + } + + private Editor ConfigureEditor(ulong fileID, string previewImagePath, string workshopPath, string changelog, + string desc, ModDeclare mod_decl) + { Editor editor = new Editor(fileID) .WithPreviewFile(previewImagePath) - .WithContent(workshopPath).WithChangeLog(changelog) + .WithContent(workshopPath) + .WithChangeLog(changelog) .WithTag("Mod"); + if (!string.IsNullOrEmpty(desc)) + { + editor = editor.WithDescription(desc); + } + + string aiTag = mod_decl.GetAIAttributionDisplay(); if (!string.IsNullOrEmpty(aiTag)) { editor = editor.WithTag(aiTag); } - if (extraTags != null) + + if (mod_decl.Tags != null) { - foreach (string tag in extraTags) + foreach (string tag in mod_decl.Tags) { if (!string.IsNullOrEmpty(tag)) { @@ -153,34 +193,36 @@ public Promise EditMod(ulong fileID, string previewImagePath, string workshopPat } } - editor.SubmitAsync(ModUploadingProgressWindow.ShowWindow()).ContinueWith( - delegate(Task taskResult) - { - if (taskResult.Status != TaskStatus.RanToCompletion) - { - promise.Reject(taskResult.Exception.GetBaseException()); - return; - } + return editor; + } - PublishResult result = taskResult.Result; - // Result process refer to: https://partner.steamgames.com/doc/api/steam_api#EResult - if (result.NeedsWorkshopAgreement) - { - LogService.LogWarning("Needs Workshop Agreement"); - // TODO: Open Workshop Agreement - Application.OpenURL("steam://url/CommunityFilePage/" + result.FileId); - } + private async void ExecuteEditMod(ulong fileID, string previewImagePath, string workshopPath, string changelog, + ModDeclare mod_decl, Promise promise, ModUploadingProgressWindow.UploadProgress progress) + { + try + { + string desc = await FetchUpdatedDescriptionAsync(fileID, mod_decl); + Editor editor = ConfigureEditor(fileID, previewImagePath, workshopPath, changelog, desc, mod_decl); - if (result.Result != Result.OK) - { - promise.Reject(new Exception(result.Result.ToString())); - return; - } + PublishResult result = await editor.SubmitAsync(progress); + if (result.NeedsWorkshopAgreement) + { + Application.OpenURL("steam://url/CommunityFilePage/" + result.FileId); + } - // result.FileId; - promise.Resolve(); - }, TaskScheduler.FromCurrentSynchronizationContext()); - return promise; + if (result.Result != Result.OK) + { + promise.Reject(new Exception(result.Result.ToString())); + return; + } + + ModUploadingProgressWindow.Instance.fileId = fileID; + promise.Resolve(); + } + catch (Exception ex) + { + promise.Reject(ex); + } } public async void FindSubscribedMods() diff --git a/services/ModWorkshopServiceWindows.cs b/services/ModWorkshopServiceWindows.cs index bb8e119..bd1070c 100644 --- a/services/ModWorkshopServiceWindows.cs +++ b/services/ModWorkshopServiceWindows.cs @@ -1,5 +1,6 @@ extern alias winsteamwork; using System.Reflection; +using System.Threading.Tasks; using NeoModLoader.api; using NeoModLoader.constants; using NeoModLoader.ui; @@ -128,23 +129,62 @@ public Promise UploadMod(string name, string description, string previewImagePat return promise; } - public Promise EditMod(ulong fileID, string previewImagePath, string workshopPath, string changelog, - string aiTag = null, List extraTags = null) + public Promise EditMod(ulong fileID, string previewImagePath, string workshopPath, string changelog, ModDeclare mod_decl) { Promise promise = new(); - // Create Upload Files Descriptor + var progress = ModUploadingProgressWindow.ShowWindow(); + ExecuteEditMod(fileID, previewImagePath, workshopPath, changelog, mod_decl, promise, progress); + return promise; + } + + private async Task FetchUpdatedDescriptionAsync(ulong fileID, ModDeclare mod_decl) + { + try + { + Query q = Query.Items.WithFileId(fileID).WithLongDescription(true); + ResultPage? resultPage = await q.GetPageAsync(1); + if (resultPage.HasValue) + { + foreach (var entry in resultPage.Value.Entries) + { + if (!string.IsNullOrEmpty(entry.Description)) + { + return ModWorkshopService.UpdateDescriptionDisclaimer(entry.Description, mod_decl); + } + } + } + } + catch (Exception e) + { + LogService.LogWarning($"Failed to get existing description: {e.Message}"); + } + + return ModWorkshopService.BuildModDescription(mod_decl); + } + + private Editor ConfigureEditor(ulong fileID, string previewImagePath, string workshopPath, string changelog, + string desc, ModDeclare mod_decl) + { Editor editor = new Editor(fileID) .WithPreviewFile(previewImagePath) - .WithContent(workshopPath).WithChangeLog(changelog) + .WithContent(workshopPath) + .WithChangeLog(changelog) .WithTag("Mod"); + if (!string.IsNullOrEmpty(desc)) + { + editor = editor.WithDescription(desc); + } + + string aiTag = mod_decl.GetAIAttributionDisplay(); if (!string.IsNullOrEmpty(aiTag)) { editor = editor.WithTag(aiTag); } - if (extraTags != null) + + if (mod_decl.Tags != null) { - foreach (string tag in extraTags) + foreach (string tag in mod_decl.Tags) { if (!string.IsNullOrEmpty(tag)) { @@ -153,34 +193,36 @@ public Promise EditMod(ulong fileID, string previewImagePath, string workshopPat } } - editor.SubmitAsync(ModUploadingProgressWindow.ShowWindow()).ContinueWith( - delegate(Task taskResult) - { - if (taskResult.Status != TaskStatus.RanToCompletion) - { - promise.Reject(taskResult.Exception.GetBaseException()); - return; - } + return editor; + } - PublishResult result = taskResult.Result; - // Result process refer to: https://partner.steamgames.com/doc/api/steam_api#EResult - if (result.NeedsWorkshopAgreement) - { - LogService.LogWarning("Needs Workshop Agreement"); - // TODO: Open Workshop Agreement - Application.OpenURL("steam://url/CommunityFilePage/" + result.FileId); - } + private async void ExecuteEditMod(ulong fileID, string previewImagePath, string workshopPath, string changelog, + ModDeclare mod_decl, Promise promise, ModUploadingProgressWindow.UploadProgress progress) + { + try + { + string desc = await FetchUpdatedDescriptionAsync(fileID, mod_decl); + Editor editor = ConfigureEditor(fileID, previewImagePath, workshopPath, changelog, desc, mod_decl); - if (result.Result != Result.OK) - { - promise.Reject(new Exception(result.Result.ToString())); - return; - } + PublishResult result = await editor.SubmitAsync(progress); + if (result.NeedsWorkshopAgreement) + { + Application.OpenURL("steam://url/CommunityFilePage/" + result.FileId); + } - // result.FileId; - promise.Resolve(); - }, TaskScheduler.FromCurrentSynchronizationContext()); - return promise; + if (result.Result != Result.OK) + { + promise.Reject(new Exception(result.Result.ToString())); + return; + } + + ModUploadingProgressWindow.Instance.fileId = fileID; + promise.Resolve(); + } + catch (Exception ex) + { + promise.Reject(ex); + } } public ModDeclare GetNextModFromWorkshopItem() diff --git a/ui/AIConfirmWindow.cs b/ui/AIConfirmWindow.cs index 8405d18..6971d91 100644 --- a/ui/AIConfirmWindow.cs +++ b/ui/AIConfirmWindow.cs @@ -9,7 +9,10 @@ namespace NeoModLoader.ui; internal class AIConfirmWindow : AbstractWindow { private Action onConfirmAction; + private Action onBackAction; private Text messageText; + private Text backText; + private Text continueText; protected override void Init() { @@ -57,7 +60,7 @@ protected override void Init() backBg.sprite = SpriteTextureLoader.getSprite("ui/special/special_buttonred"); backBg.type = Image.Type.Sliced; - Text backText = new GameObject("Text", typeof(Text)).GetComponent(); + backText = new GameObject("Text", typeof(Text)).GetComponent(); backText.transform.SetParent(backButtonObj.transform); backText.transform.localScale = Vector3.one; backText.GetComponent().sizeDelta = new Vector2(85, 25); @@ -67,7 +70,15 @@ protected override void Init() backButtonObj.GetComponent