Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

</br>

### 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)


<h2 align="center">Others</h2>
<h3 align="center">
Expand Down
14 changes: 10 additions & 4 deletions api/ModDeclare.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ public ModDeclare(string pFilePath)
AICheckTranslation = modDeclare.AICheckTranslation;
AICheckAudio = modDeclare.AICheckAudio;
Tags = modDeclare.Tags ?? new List<string>();
SteamId = modDeclare.SteamId;

Dependencies ??= Array.Empty<string>();
OptionalDependencies ??= Array.Empty<string>();
Expand Down Expand Up @@ -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;
}
}

/// <summary>
Expand Down Expand Up @@ -345,6 +350,9 @@ public string GetAIAttributionDisplay()
};
}

[JsonProperty("steam_id")]
public ulong SteamId { get; internal set; }

private List<string> _tags = new();

[JsonProperty("tags")]
Expand Down Expand Up @@ -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)
{
Expand Down
2 changes: 1 addition & 1 deletion resources/commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4bb2ea1fa3af3aa45c8ebb7aefcf096a5bc3b2f0
98377654d461586e413f2e9bc28ed3fa24514e27
17 changes: 17 additions & 0 deletions resources/locales/ch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 內容免責聲明",
Expand Down
17 changes: 17 additions & 0 deletions resources/locales/cz.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 内容免责声明",
Expand Down
17 changes: 17 additions & 0 deletions resources/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 1 addition & 2 deletions services/IPlatformSpecificModWorkshopService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> extraTags = null);

Promise EditMod(ulong fileID, string previewImagePath, string workshopPath, string changelog,
string aiTag = null, List<string> extraTags = null);
Promise EditMod(ulong fileID, string previewImagePath, string workshopPath, string changelog, ModDeclare mod_decl);
void FindSubscribedMods();
ModDeclare GetNextModFromWorkshopItem();
}
47 changes: 40 additions & 7 deletions services/ModWorkshopService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using NeoModLoader.api;
using NeoModLoader.api.attributes;
using NeoModLoader.constants;
Expand Down Expand Up @@ -38,10 +39,10 @@ private static void UploadModLoader(string changelog)
/// <summary>
/// Try to Upload a mod to Steam Workshop
/// </summary>
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);

Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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")
{
Expand Down Expand Up @@ -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");
Expand Down
104 changes: 73 additions & 31 deletions services/ModWorkshopServiceUnix.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
extern alias unixsteamwork;
using System.Reflection;
using System.Threading.Tasks;
using NeoModLoader.api;
using NeoModLoader.constants;
using NeoModLoader.ui;
Expand Down Expand Up @@ -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<string> 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<string> 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))
{
Expand All @@ -153,34 +193,36 @@ public Promise EditMod(ulong fileID, string previewImagePath, string workshopPat
}
}

editor.SubmitAsync(ModUploadingProgressWindow.ShowWindow()).ContinueWith(
delegate(Task<PublishResult> 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()
Expand Down
Loading
Loading