Skip to content
Merged

Dev #255

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
1 change: 1 addition & 0 deletions Flatpak/io.github.TeamWheelWizard.WheelWizard.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
</provides>

<releases>
<release version="2.4.4" date="2026-04-15"/>
<release version="2.4.3" date="2026-04-01"/>
<release version="2.4.2" date="2026-03-25"/>
<release version="2.4.1" date="2026-02-25"/>
Expand Down
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
4 changes: 3 additions & 1 deletion WheelWizard/Services/Launcher/Helpers/DolphinLaunchHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ void AddFilesystemPerm(string newFilesystemPerm, string mode = "")

if (!TryFixFlatpakPortalAccess(PathManager.GameFilePath, "-r"))
AddFilesystemPerm(PathManager.GameFilePath, ":ro");
AddFilesystemPerm(PathManager.RrLaunchJsonFilePath, ":ro");
// We need to provide the directory where the `RR.json` is located in for portal access!
if (!TryFixFlatpakPortalAccess(Path.GetDirectoryName(PathManager.RrLaunchJsonFilePath) ?? "", "-r"))
AddFilesystemPerm(PathManager.RrLaunchJsonFilePath, ":ro");

return fixedFlatpakDolphinLocation;
}
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);

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
Loading
Loading