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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using WheelWizard.Services;

namespace WheelWizard.GameBanana.Domain;

Expand All @@ -16,7 +17,8 @@ public class GameBananaModPreview
public required string Version { get; set; }

[JsonPropertyName("_aTags")]
public required List<string> Tags { get; set; }
[JsonConverter(typeof(GameBananaTagListJsonConverter))]
public required List<GameBananaTag> Tags { get; set; }

[JsonPropertyName("_sProfileUrl")]
public required string ProfileUrl { get; set; }
Expand Down Expand Up @@ -52,4 +54,7 @@ public class GameBananaModPreview
/// </summary>
[JsonPropertyName("_sModelName")]
public required string ModelName { get; set; }

[JsonIgnore]
public bool UsesPatches => ModStorageSystemHelper.UsesPatches(Tags);
}
72 changes: 72 additions & 0 deletions WheelWizard/Features/GameBanana/Domain/GameBananaTag.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace WheelWizard.GameBanana.Domain;

public class GameBananaTag
{
public string Title { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
}

public sealed class GameBananaTagListJsonConverter : JsonConverter<List<GameBananaTag>>
{
public override List<GameBananaTag> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return [];

if (reader.TokenType != JsonTokenType.StartArray)
throw new JsonException($"Expected StartArray token, but got {reader.TokenType}.");

var tags = new List<GameBananaTag>();

while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
return tags;

switch (reader.TokenType)
{
case JsonTokenType.String:
var tagText = reader.GetString() ?? string.Empty;
tags.Add(new() { Title = tagText, Value = tagText });
break;
case JsonTokenType.StartObject:
using (var tagDocument = JsonDocument.ParseValue(ref reader))
{
var root = tagDocument.RootElement;
tags.Add(
new()
{
Title = root.TryGetProperty("_sTitle", out var title) ? title.GetString() ?? string.Empty : string.Empty,
Value = root.TryGetProperty("_sValue", out var value) ? value.GetString() ?? string.Empty : string.Empty,
}
);
}
break;

default:
using (JsonDocument.ParseValue(ref reader)) { }
break;
}
}

throw new JsonException("Unexpected end of GameBanana tag payload.");
}

public override void Write(Utf8JsonWriter writer, List<GameBananaTag> value, JsonSerializerOptions options)
{
writer.WriteStartArray();

foreach (var tag in value)
{
writer.WriteStartObject();
writer.WriteString("_sTitle", tag.Title);
writer.WriteString("_sValue", tag.Value);
writer.WriteEndObject();
}

writer.WriteEndArray();
}
}
1 change: 1 addition & 0 deletions WheelWizard/Features/Settings/ISettingsServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public interface ISettingsProperties
Setting LAUNCH_WITH_DOLPHIN { get; }
Setting LAUNCH_RR_ON_STARTUP { get; }
Setting PREFERS_MODS_ROW_VIEW { get; }
Setting USE_PATCHES_SYSTEM { get; }
Setting FOCUSED_USER { get; }
Setting ENABLE_ANIMATIONS { get; }
Setting TESTING_MODE_ENABLED { get; }
Expand Down
2 changes: 2 additions & 0 deletions WheelWizard/Features/Settings/SettingsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ IFileSystem fileSystem
LAUNCH_WITH_DOLPHIN = RegisterWhWz("LaunchWithDolphin", false);
LAUNCH_RR_ON_STARTUP = RegisterWhWz("LaunchRrOnStartup", false);
PREFERS_MODS_ROW_VIEW = RegisterWhWz("PrefersModsRowView", true);
USE_PATCHES_SYSTEM = RegisterWhWz("UsePatchesSystem", false);
FOCUSED_USER = RegisterWhWz("FavoriteUser", 0, value => (int)(value ?? -1) >= 0 && (int)(value ?? -1) < 4);

ENABLE_ANIMATIONS = RegisterWhWz("EnableAnimations", true);
Expand Down Expand Up @@ -198,6 +199,7 @@ IFileSystem fileSystem
public Setting LAUNCH_WITH_DOLPHIN { get; }
public Setting LAUNCH_RR_ON_STARTUP { get; }
public Setting PREFERS_MODS_ROW_VIEW { get; }
public Setting USE_PATCHES_SYSTEM { get; }
public Setting FOCUSED_USER { get; }
public Setting ENABLE_ANIMATIONS { get; }
public Setting TESTING_MODE_ENABLED { get; }
Expand Down
133 changes: 100 additions & 33 deletions WheelWizard/Services/Launcher/Helpers/ModsLaunchHelper.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,87 @@
using Avalonia.Threading;
using WheelWizard.Helpers;
using WheelWizard.Models.Mods;
using WheelWizard.Resources.Languages;
using WheelWizard.Views.Popups.Generic;

namespace WheelWizard.Services.Launcher.Helpers;

public static class ModsLaunchHelper
{
public static readonly string MyStuffFolderPath = PathManager.MyStuffFolderPath;
public static readonly string ModsFolderPath = PathManager.ModsFolderPath;
public static readonly string[] AcceptedModExtensions = ["*.szs", "*.arc", "*.brstm", "*.brsar", "*.thp"];

public static async Task PrepareModsForLaunch(string? myStuffFolderPath = null)
//todo: move this to like an actual static place somewhere that makes more sense.
public static readonly string[] AcceptedMyStuffExtensions =
[
".bcp",
".szs",
".bdof",
".bfg",
".blight",
".bmg",
".bmm",
".brctr",
".breff",
".breft",
".brfnt",
".brlan",
".brlyt",
".brres",
".brsar",
".brstm",
".bsp",
".bti",
".chr0",
".clr0",
".krm",
".mdl0",
".pat0",
".rkc",
".rkg",
".scn0",
".shp0",
".srt0",
".tex0",
".thp",
".tpl",
".u8",
".yaz0",
".ast",
".bdl",
".bmd",
".bco",
".bol",
".dat",
".rarc",
".ct-def",
".le-def",
".lex",
".lfl",
".lpar",
".lta",
".tplx",
".wbz",
".wlz",
".wu8",
".ybz",
".ylz",
];

public static async Task PrepareModsForLaunch(string targetFolderPath, string inactiveFolderPath, ModStorageSystem storageSystem)
{
var resolvedMyStuffFolderPath = myStuffFolderPath ?? MyStuffFolderPath;
ClearInactiveFolder(inactiveFolderPath);

var mods = ModManager.Instance.Mods.Where(mod => mod.IsEnabled).ToArray();
if (mods.Length == 0)
{
if (Directory.Exists(resolvedMyStuffFolderPath) && Directory.EnumerateFiles(resolvedMyStuffFolderPath).Any())
if (Directory.Exists(targetFolderPath) && Directory.EnumerateFiles(targetFolderPath).Any())
{
var modsFoundQuestion = new YesNoWindow()
.SetButtonText(Common.Action_Delete, Common.Action_Keep)
.SetMainText(Phrases.Question_LaunchClearModsFound_Title)
.SetExtraText(Phrases.Question_LaunchClearModsFound_Extra);
.SetExtraText(ModStorageSystemHelper.GetClearFolderPrompt(storageSystem));
if (await modsFoundQuestion.AwaitAnswer())
Directory.Delete(resolvedMyStuffFolderPath, true);
Directory.Delete(targetFolderPath, true);
Comment thread
patchzyy marked this conversation as resolved.

return;
}
Expand All @@ -38,34 +95,23 @@ public static async Task PrepareModsForLaunch(string? myStuffFolderPath = null)
{
if (!mod.IsEnabled)
continue;

var modFolder = Path.Combine(ModsFolderPath, mod.Title);
foreach (var extension in AcceptedModExtensions)
{
var files = Directory.GetFiles(modFolder, extension, SearchOption.AllDirectories);
foreach (var file in files)
{
var relativePath = Path.GetFileName(file);
// Since higher priority mods overwrite lower ones, we can overwrite entries in the dictionary
finalFiles[relativePath] = file;
}
}
}
if (!Directory.Exists(modFolder))
continue;

// Get existing files in MyStuff
var existingFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (Directory.Exists(resolvedMyStuffFolderPath))
{
var files = Directory.GetFiles(resolvedMyStuffFolderPath, "*.*", SearchOption.TopDirectoryOnly);
foreach (var file in files)
foreach (var file in Directory.GetFiles(modFolder, "*", SearchOption.AllDirectories))
{
if (!ShouldCopyFile(mod, file, storageSystem))
continue;

var relativePath = Path.GetFileName(file);
existingFiles.Add(relativePath);
// Since higher priority mods overwrite lower ones, we can overwrite entries in the dictionary
finalFiles[relativePath] = file;
}
}
else
{
Directory.CreateDirectory(resolvedMyStuffFolderPath);
}

Directory.CreateDirectory(targetFolderPath);

var totalFiles = finalFiles.Count;
var progressWindow = new ProgressWindow(Phrases.Progress_InstallingMods).SetGoal(
Expand All @@ -76,10 +122,10 @@ public static async Task PrepareModsForLaunch(string? myStuffFolderPath = null)
await Task.Run(() =>
{
var processedFiles = 0;
// Delete files in MyStuff that are not in finalFiles
if (Directory.Exists(resolvedMyStuffFolderPath))
// Delete files in the active loose-mod folder that are not in finalFiles
if (Directory.Exists(targetFolderPath))
{
var files = Directory.GetFiles(resolvedMyStuffFolderPath, "*.*", SearchOption.TopDirectoryOnly);
var files = Directory.GetFiles(targetFolderPath, "*.*", SearchOption.TopDirectoryOnly);
foreach (var file in files)
{
var relativePath = Path.GetFileName(file);
Expand All @@ -94,7 +140,7 @@ await Task.Run(() =>
{
var relativePath = kvp.Key;
var sourceFile = kvp.Value;
var destinationFile = Path.Combine(resolvedMyStuffFolderPath, relativePath);
var destinationFile = Path.Combine(targetFolderPath, relativePath);

processedFiles++;
var progress = (int)((processedFiles) / (double)totalFiles * 100);
Expand Down Expand Up @@ -131,4 +177,25 @@ await Task.Run(() =>

progressWindow.Close();
}

private static void ClearInactiveFolder(string inactiveFolderPath)
{
if (!Directory.Exists(inactiveFolderPath))
return;

Directory.Delete(inactiveFolderPath, true);
}

private static bool ShouldCopyFile(Mod mod, string filePath, ModStorageSystem storageSystem)
{
var modMetadataFile = Path.Combine(ModsFolderPath, mod.Title, $"{mod.Title}.ini");
if (Path.GetFullPath(filePath).Equals(Path.GetFullPath(modMetadataFile), StringComparison.OrdinalIgnoreCase))
return false;

if (storageSystem == ModStorageSystem.Patches)
return true;

var extension = Path.GetExtension(filePath);
return AcceptedMyStuffExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
}
}
7 changes: 6 additions & 1 deletion WheelWizard/Services/Launcher/RrBetaLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ public async Task Launch()
DolphinLaunchHelper.KillDolphin();
if (WiiMoteSettings.IsForceSettingsEnabled())
WiiMoteSettings.DisableVirtualWiiMote();
await ModsLaunchHelper.PrepareModsForLaunch(PathManager.RrBetaMyStuffFolderPath);
var storageSystem = ModStorageSystemHelper.GetCurrent(_settingsManager);
await ModsLaunchHelper.PrepareModsForLaunch(
ModStorageSystemHelper.GetTargetFolderPath(storageSystem, isBeta: true),
ModStorageSystemHelper.GetInactiveFolderPath(storageSystem, isBeta: true),
storageSystem
);
if (!File.Exists(PathManager.GameFilePath))
{
Dispatcher.UIThread.Post(() =>
Expand Down
7 changes: 6 additions & 1 deletion WheelWizard/Services/Launcher/RrLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ public async Task Launch()
DolphinLaunchHelper.KillDolphin();
if (WiiMoteSettings.IsForceSettingsEnabled())
WiiMoteSettings.DisableVirtualWiiMote();
await ModsLaunchHelper.PrepareModsForLaunch();
var storageSystem = ModStorageSystemHelper.GetCurrent(_settingsManager);
await ModsLaunchHelper.PrepareModsForLaunch(
ModStorageSystemHelper.GetTargetFolderPath(storageSystem),
ModStorageSystemHelper.GetInactiveFolderPath(storageSystem),
storageSystem
);
if (!File.Exists(PathManager.GameFilePath))
{
Dispatcher.UIThread.Post(() =>
Expand Down
59 changes: 59 additions & 0 deletions WheelWizard/Services/ModStorageSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using WheelWizard.GameBanana.Domain;
using WheelWizard.Resources.Languages;
using WheelWizard.Settings;

namespace WheelWizard.Services;

public enum ModStorageSystem
{
MyStuff,
Patches,
}

public static class ModStorageSystemHelper
{
public static ModStorageSystem GetCurrent(ISettingsManager settingsManager)
{
return settingsManager.Get<bool>(settingsManager.USE_PATCHES_SYSTEM) ? ModStorageSystem.Patches : ModStorageSystem.MyStuff;
}

public static string GetDisplayName(ModStorageSystem storageSystem) =>
storageSystem == ModStorageSystem.Patches ? "Patches" : Common.PageTitle_Mods;

public static string GetClearFolderPrompt(ModStorageSystem storageSystem)
{
if (storageSystem == ModStorageSystem.MyStuff)
return Phrases.Question_LaunchClearModsFound_Extra;

//todo: translation lol
return "You are about to launch the game without mods. Do you want to clear your Patches folder?";
}
Comment on lines +20 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Address missing localization for Patches display strings.

Lines 21 and 29 contain hardcoded English strings ("Patches" and the prompt text) while MyStuff uses localized resources. The TODO on line 28 acknowledges this. Consider adding these strings to your language resources for consistency.

Would you like me to help identify all the required localization keys and open an issue to track this?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@WheelWizard/Services/ModStorageSystem.cs` around lines 20 - 30,
GetDisplayName and GetClearFolderPrompt are returning hardcoded English strings
for ModStorageSystem.Patches ("Patches" and the clear-folder prompt) instead of
using localized resources like Common.PageTitle_Mods and Phrases.*; update these
to use new localization keys (e.g., Common.PageTitle_Patches and
Phrases.Question_LaunchClearPatches_Extra) added to your resource files, replace
the literal "Patches" in GetDisplayName and the literal prompt in
GetClearFolderPrompt with those resource lookups, and add corresponding entries
in the language resource files so all branches of the ModStorageSystem enum are
localized consistently.


public static string GetTargetFolderPath(ModStorageSystem storageSystem, bool isBeta = false)
{
if (storageSystem == ModStorageSystem.Patches)
return isBeta ? PathManager.RrBetaPatchesFolderPath : PathManager.PatchesFolderPath;

return isBeta ? PathManager.RrBetaMyStuffFolderPath : PathManager.MyStuffFolderPath;
}

public static string GetInactiveFolderPath(ModStorageSystem storageSystem, bool isBeta = false)
{
var inactiveStorageSystem = storageSystem == ModStorageSystem.Patches ? ModStorageSystem.MyStuff : ModStorageSystem.Patches;
return GetTargetFolderPath(inactiveStorageSystem, isBeta);
}

public static bool UsesPatches(IEnumerable<GameBananaTag>? tags) => tags?.Any(tag => IsPatchesTag(tag.Title)) == true;

public static bool IsPatchesTag(string? tagTitle)
{
var normalizedTitle = tagTitle?.Trim();
if (string.IsNullOrWhiteSpace(normalizedTitle))
return false;

var titleOnly = normalizedTitle.Split(':', 2)[0].Trim();
return (
titleOnly.Equals("patch", StringComparison.OrdinalIgnoreCase) || titleOnly.Equals("patches", StringComparison.OrdinalIgnoreCase)
);
}
Comment thread
patchzyy marked this conversation as resolved.
}
Loading
Loading