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
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.8" date="2026-06-01"/>
<release version="2.4.7" date="2026-05-27"/>
<release version="2.4.6" date="2026-05-27"/>
<release version="2.4.5" date="2026-05-17"/>
Expand Down
23 changes: 23 additions & 0 deletions WheelWizard.Test/Features/MiiSerializerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,27 @@ public void Deserialize_InvalidLengthData_ShouldFail()
Assert.True(result.IsFailure);
Assert.Equal("Invalid Mii data length.", result.Error.Message);
}

[Fact]
public void Deserialize_InvalidCalendarDate_ShouldFailInsteadOfThrowing()
{
var data = Convert.FromBase64String(dataList[0]);
ushort header = (ushort)((data[0] << 8) | data[1]);
header = (ushort)((header & ~(0x0F << 10)) | (2 << 10));
header = (ushort)((header & ~(0x1F << 5)) | (31 << 5));
data[0] = (byte)(header >> 8);
data[1] = (byte)header;

var result = MiiSerializer.Deserialize(data);

Assert.True(result.IsFailure);
}

[Fact]
public void Deserialize_InvalidBase64_ShouldFailInsteadOfThrowing()
{
var result = MiiSerializer.Deserialize("not valid base64");

Assert.True(result.IsFailure);
}
}
26 changes: 26 additions & 0 deletions WheelWizard.Test/Features/Settings/SettingsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,32 @@ public void Set_ReturnsFalse_WhenValidationFails()
Assert.Equal(0, manager.Get<int>(manager.FOCUSED_USER));
}

[Theory]
[InlineData(0.49)]
[InlineData(2.01)]
public void SavedWindowScale_RejectsValuesOutsideBounds(double scale)
{
var manager = CreateManager(new MockFileSystem(), out _, out _, out _);

var result = manager.Set(manager.SAVED_WINDOW_SCALE, scale, skipSave: true);

Assert.False(result);
Assert.Equal(1.0, manager.Get<double>(manager.SAVED_WINDOW_SCALE));
}

[Theory]
[InlineData(0.49)]
[InlineData(2.01)]
public void WindowScalePreview_RejectsValuesOutsideBounds(double scale)
{
var manager = CreateManager(new MockFileSystem(), out _, out _, out _);

var result = manager.Set(manager.WINDOW_SCALE, scale, skipSave: true);

Assert.False(result);
Assert.Equal(1.0, manager.Get<double>(manager.WINDOW_SCALE));
}

[Fact]
public void ValidateCorePathSettings_ReturnsAllExpectedIssues_WhenDefaultsAreInvalid()
{
Expand Down
3 changes: 2 additions & 1 deletion WheelWizard/Features/CustomDistributions/RetroRewindBeta.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ out bool badPassword
badPassword = false;
try
{
using var archive = ArchiveFactory.OpenArchive(zipPath, new ReaderOptions { Password = password });
using var archiveStream = _fileSystem.File.OpenRead(zipPath);
using var archive = ArchiveFactory.OpenArchive(archiveStream, new ReaderOptions { Password = password });
var entries = archive.Entries.Where(entry => !entry.IsDirectory).ToList();
if (entries.Count == 0)
return Ok();
Expand Down
8 changes: 5 additions & 3 deletions WheelWizard/Features/Settings/SettingsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ IFileSystem fileSystem

ENABLE_ANIMATIONS = RegisterWhWz("EnableAnimations", true);
TESTING_MODE_ENABLED = RegisterWhWz("TestingModeEnabled", false);
SAVED_WINDOW_SCALE = RegisterWhWz("WindowScale", 1.0, value => (double)(value ?? -1) >= 0.5 && (double)(value ?? -1) <= 2.0);
SAVED_WINDOW_SCALE = RegisterWhWz("WindowScale", 1.0, SettingValues.IsValidWindowScale);
REMOVE_BLUR = RegisterWhWz("REMOVE_BLUR", true);
RR_REGION = RegisterWhWz("RR_Region", MarioKartWiiEnums.Regions.None);
WW_LANGUAGE = RegisterWhWz("WW_Language", "en", value => SettingValues.WhWzLanguages.ContainsKey((string)value!));
Expand Down Expand Up @@ -155,11 +155,13 @@ IFileSystem fileSystem
#endregion

#region Virtual settings
WINDOW_SCALE = new VirtualSetting(
var windowScale = new VirtualSetting(
typeof(double),
value => _internalScale = (double)value!,
() => _internalScale == -1.0 ? SAVED_WINDOW_SCALE.Get() : _internalScale
).SetDependencies(SAVED_WINDOW_SCALE);
);
windowScale.SetValidation(SettingValues.IsValidWindowScale);
WINDOW_SCALE = windowScale.SetDependencies(SAVED_WINDOW_SCALE);

RECOMMENDED_SETTINGS = new VirtualSetting(
typeof(bool),
Expand Down
7 changes: 7 additions & 0 deletions WheelWizard/Features/Settings/Types/SettingConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@ public static class SettingValues
// you check for this value and replace it with its corresponding value in the language file
public const string NoName = "no name";
public const string NoLicense = "no license";
public const double MinWindowScale = 0.5;
public const double MaxWindowScale = 2.0;

public static readonly double[] WindowScales = [0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.8, 2];

public static bool IsValidWindowScale(object? value)
{
return value is double scale && scale >= MinWindowScale && scale <= MaxWindowScale;
}

public static readonly Dictionary<string, string> GFXRenderers = new() //Display name, value
{
#if WINDOWS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,31 @@ public static OperationResult<byte[]> Serialize(Mii? mii)
return data;
}

public static OperationResult<Mii> Deserialize(string data) => Deserialize(Convert.FromBase64String(data));
public static OperationResult<Mii> Deserialize(string data)
{
try
{
return Deserialize(Convert.FromBase64String(data));
}
catch (Exception ex)
{
return InvalidDataExc(ex);
}
}

public static OperationResult<Mii> Deserialize(byte[]? data)
{
try
{
return DeserializeCore(data);
}
catch (Exception ex)
{
return InvalidDataExc(ex);
}
}

private static OperationResult<Mii> DeserializeCore(byte[]? data)
{
Comment on lines 158 to 171

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 | 💤 Low value

Robustness refactor looks correct; consider logging the swallowed exception.

Routing DeserializeCore exceptions (including the impossible-date case from line 187) into a failure OperationResult is the right call for parsing untrusted Mii data. One caveat: catch (Exception ex) also absorbs genuine programming errors (e.g. IndexOutOfRangeException) and reports them as generic "invalid data". The message is preserved in the result, but a debug-level log here would aid diagnosis without changing behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@WheelWizard/Features/WiiManagement/MiiManagement/MiiSerializer.cs` around
lines 158 - 171, The catch in Deserialize currently swallows all exceptions from
DeserializeCore and returns InvalidDataExc(ex); add a debug-level log of the
caught exception before returning so programming errors are visible in
diagnostics without changing the returned OperationResult. Update the catch
(Exception ex) block in Deserialize to call the project's logging facility
(e.g., Trace/ILogger/Serilog) to log a short contextual message like
"Deserialize failed" along with ex (stack and message) and then return
InvalidDataExc(ex); reference Deserialize, DeserializeCore, and InvalidDataExc
when making the change.

if (data == null || data.Length != 74)
return Fail("Invalid Mii data length.", MessageTranslation.Error_MiiSerializer_MiiDataLength);
Expand Down Expand Up @@ -342,4 +364,9 @@ private static OperationResult<Mii> InvalidDataExc(string data)
{
return Fail(new InvalidDataException($"Invalid {data}"), MessageTranslation.Error_MiiSerializer_InvalidMiiData, null, [data]);
}

private static OperationResult<Mii> InvalidDataExc(Exception exception)
{
return Fail(exception, MessageTranslation.Error_MiiSerializer_InvalidMiiData, null, [exception.Message]);
}
}
14 changes: 13 additions & 1 deletion WheelWizard/Views/Layout.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public Layout()
InitializeComponent();
AddLayer();

ClampSavedWindowScaleToCurrentScreen();
OnSettingChanged(SettingsService.SAVED_WINDOW_SCALE);
_settingsSignalSubscription = SettingsSignalBus.Subscribe(OnSettingSignal);
UpdateTestingButtonVisibility();
Expand Down Expand Up @@ -148,7 +149,7 @@ private void OnSettingChanged(Setting setting)
// Note that this method will also be called whenever the setting changes
if (setting == SettingsService.WINDOW_SCALE || setting == SettingsService.SAVED_WINDOW_SCALE)
{
var scaleFactor = (double)setting.Get();
var scaleFactor = GetUsableWindowScale((double)setting.Get());
Height = WindowHeight * scaleFactor;
Width = WindowWidth * scaleFactor;
CompleteGrid.RenderTransform = new ScaleTransform(scaleFactor, scaleFactor);
Expand All @@ -163,6 +164,17 @@ private void OnSettingChanged(Setting setting)
UpdateTestingButtonVisibility();
}

private void ClampSavedWindowScaleToCurrentScreen()
{
var savedScale = SettingsService.Get<double>(SettingsService.SAVED_WINDOW_SCALE);
var usableScale = GetUsableWindowScale(savedScale);
if (!savedScale.Equals(usableScale))
SettingsService.Set(SettingsService.SAVED_WINDOW_SCALE, usableScale);
}

private double GetUsableWindowScale(double requestedScale) =>
ViewUtils.GetUsableWindowScale(requestedScale, new Size(WindowWidth, WindowHeight), this);
Comment on lines +167 to +176

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Avalonia 11.2.5 Window.Screens ScreenFromWindow availability before window shown

💡 Result:

In Avalonia 11.2.5, the Window.Screens.ScreenFromWindow(window) method is not reliable or may not return valid screen information before a window has been shown (i.e., before it has a valid platform implementation) [1][2]. The ScreenFromWindow method requires a TopLevel (or WindowBase) instance that has been initialized with a platform-specific implementation [2][3]. Before the window is shown, the PlatformImpl property is often null or uninitialized, which can lead to unexpected results or exceptions depending on the specific platform and state [2]. If you need to access screen information before a window is shown, consider these alternatives: 1. Use the static Screens API: You can access the general Screens collection via TopLevel.Screens or by using the global screen management services available in Avalonia to query primary or available displays without needing a specific window instance [4]. 2. Defer logic: If you need to position or size a window based on a specific screen, perform this logic in the Window's Opened event or an equivalent lifecycle hook where the PlatformImpl is guaranteed to be available [1][4]. 3. Platform-specific APIs: If you strictly require display information before any Avalonia UI elements are initialized, you may need to use platform-native APIs (such as Win32 APIs on Windows) or cross-platform libraries that operate independently of the Avalonia framework [5].

Citations:


🏁 Script executed:

rg -n "GetUsableWindowScale" -S .

Repository: TeamWheelWizard/WheelWizard

Length of output: 1055


🏁 Script executed:

rg -n "GetUsableWindowScale" -S .

Repository: TeamWheelWizard/WheelWizard

Length of output: 1055


🏁 Script executed:

rg -n "GetUsableWindowScale" -S .

Repository: TeamWheelWizard/WheelWizard

Length of output: 1055


🏁 Script executed:

sed -n '1,120p' WheelWizard/Views/ViewUtils.cs

Repository: TeamWheelWizard/WheelWizard

Length of output: 4997


🏁 Script executed:

sed -n '120,220p' WheelWizard/Views/Layout.axaml.cs

Repository: TeamWheelWizard/WheelWizard

Length of output: 4105


🏁 Script executed:

rg -n "ClampSavedWindowScaleToCurrentScreen\\(" WheelWizard/Views/Layout.axaml.cs

Repository: TeamWheelWizard/WheelWizard

Length of output: 182


🏁 Script executed:

rg -n "ClampSavedWindowScaleToCurrentScreen\\(" WheelWizard/Views/Layout.axaml.cs

Repository: TeamWheelWizard/WheelWizard

Length of output: 182


🏁 Script executed:

sed -n '50,110p' WheelWizard/Views/Layout.axaml.cs

Repository: TeamWheelWizard/WheelWizard

Length of output: 2197


🏁 Script executed:

sed -n '50,110p' WheelWizard/Views/Layout.axaml.cs

Repository: TeamWheelWizard/WheelWizard

Length of output: 2197


🏁 Script executed:

head -n 40 WheelWizard/Views/Layout.axaml.cs

Repository: TeamWheelWizard/WheelWizard

Length of output: 1461


🏁 Script executed:

head -n 40 WheelWizard/Views/Layout.axaml.cs

Repository: TeamWheelWizard/WheelWizard

Length of output: 1461


🏁 Script executed:

rg -n "MinWindowScale|MaxWindowScale" WheelWizard

Repository: TeamWheelWizard/WheelWizard

Length of output: 739


🏁 Script executed:

sed -n '1,80p' WheelWizard/Features/Settings/Types/SettingConstants.cs

Repository: TeamWheelWizard/WheelWizard

Length of output: 2367


🏁 Script executed:

rg -n "class BaseWindow" -S WheelWizard

Repository: TeamWheelWizard/WheelWizard

Length of output: 147


🏁 Script executed:

sed -n '1,80p' WheelWizard/Features/Settings/Types/SettingConstants.cs

Repository: TeamWheelWizard/WheelWizard

Length of output: 2367


🏁 Script executed:

rg -n "Avalonia\\.?|PackageReference.*Avalonia" -S . --glob '*.csproj' --glob '*.props' --glob '*.targets'

Repository: TeamWheelWizard/WheelWizard

Length of output: 1199


🏁 Script executed:

rg -n "Avalonia\\.?|PackageReference.*Avalonia" -S . --glob '*.csproj' --glob '*.props' --glob '*.targets'

Repository: TeamWheelWizard/WheelWizard

Length of output: 1199


Defer screen-based clamping until the window is opened

  • WheelWizard/Views/Layout.axaml.cs calls ClampSavedWindowScaleToCurrentScreen() inside public Layout() (constructor), before the window is shown.
  • ViewUtils.GetUsableWindowScale(...) uses window.Screens.ScreenFromWindow(window) ?? window.Screens.Primary; in Avalonia 11.2.5 ScreenFromWindow may not provide reliable screen geometry until after show/open, so multi-monitor setups can clamp against the wrong screen.
  • Move the clamp to Opened/OnOpened (or Loaded) so the correct WorkingArea/Scaling are available before updating SAVED_WINDOW_SCALE.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@WheelWizard/Views/Layout.axaml.cs` around lines 167 - 176, The clamp
currently runs in the Layout() constructor and may use incorrect screen info;
remove the call to ClampSavedWindowScaleToCurrentScreen() from the Layout()
constructor and instead invoke it once the window is shown by overriding
OnOpened or handling the Opened/Loaded event (e.g., override OnOpened or
subscribe to Opened and call ClampSavedWindowScaleToCurrentScreen there). Keep
the existing ClampSavedWindowScaleToCurrentScreen() and GetUsableWindowScale()
(which calls ViewUtils.GetUsableWindowScale) and ensure it updates
SettingsService.SAVED_WINDOW_SCALE only after the window is opened so
ScreenFromWindow/Primary use correct geometry.


private void UpdateModsButtonText()
{
ModsButton.Text = Common.PageTitle_Patches;
Expand Down
12 changes: 11 additions & 1 deletion WheelWizard/Views/Pages/Settings/WhWzSettings.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -647,8 +647,18 @@ private async void WindowScaleDropdown_OnSelectionChanged(object sender, Selecti
_editingScale = true;
var selectedScale = WindowScaleDropdown.SelectedItem?.ToString() ?? "1";
var scale = double.Parse(selectedScale.Split(" ").Last().Replace("%", "")) / 100;
scale = ViewUtils.GetUsableWindowScale(scale, new Avalonia.Size(Layout.WindowWidth, Layout.WindowHeight), ViewUtils.GetLayout());
var selectedItemText = ScaleToString(scale);
if (!WindowScaleDropdown.Items.Contains(selectedItemText))
WindowScaleDropdown.Items.Add(selectedItemText);
WindowScaleDropdown.SelectedItem = selectedItemText;

SettingsService.WINDOW_SCALE.Set(scale);
if (!SettingsService.WINDOW_SCALE.Set(scale))
{
WindowScaleDropdown.SelectedItem = ScaleToString((double)SettingsService.WINDOW_SCALE.Get());
_editingScale = false;
return;
}
var seconds = 10;

string ExtraScaleText() =>
Expand Down
3 changes: 2 additions & 1 deletion WheelWizard/Views/Popups/Base/PopupWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Avalonia.Media;
using WheelWizard.Settings;
using WheelWizard.Shared.DependencyInjection;
using WheelWizard.Views;

namespace WheelWizard.Views.Popups.Base;

Expand Down Expand Up @@ -147,7 +148,7 @@ protected override void OnResized(WindowResizedEventArgs e)

public void SetWindowSize(Size size)
{
var scaleFactor = SettingsService.Get<double>(SettingsService.WINDOW_SCALE);
var scaleFactor = ViewUtils.GetUsableWindowScale(SettingsService.Get<double>(SettingsService.WINDOW_SCALE), size, this);
Width = size.Width * scaleFactor;
Height = size.Height * scaleFactor;
CompleteGrid.RenderTransform = new ScaleTransform(scaleFactor, scaleFactor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ private void SetEyebrowColor(int index)
return;

var current = Editor.Mii.MiiEyebrows;
if (index == current.Type)
if (index == (int)current.Color)
return;

var result = MiiEyebrow.Create(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ private void SetEyeType(int index)
private void SetEyeColor(int index)
{
var current = Editor.Mii.MiiEyes;
if (index == current.Type)
if (index == (int)current.Color)
return;

var result = MiiEye.Create(current.Type, current.Rotation, current.Vertical, (MiiEyeColor)index, current.Size, current.Spacing);
Expand Down
17 changes: 17 additions & 0 deletions WheelWizard/Views/ViewUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Avalonia.Controls;
using Avalonia.Media;
using WheelWizard.Services.LiveData;
using WheelWizard.Settings.Types;
using WheelWizard.Utilities.RepeatedTasks;

namespace WheelWizard.Views;
Expand All @@ -26,6 +27,22 @@ public static void OpenLink(string link)

public static Layout GetLayout() => Layout.Instance;

public static double GetUsableWindowScale(double requestedScale, Size unscaledSize, Window window)
{
var maxScale = SettingValues.MaxWindowScale;
var screen = window.Screens.ScreenFromWindow(window) ?? window.Screens.Primary;
if (screen != null)
{
var screenScale = screen.Scaling <= 0 ? 1 : screen.Scaling;
var availableWidth = screen.WorkingArea.Width / screenScale;
var availableHeight = screen.WorkingArea.Height / screenScale;
maxScale = Math.Min(maxScale, Math.Min(availableWidth / unscaledSize.Width, availableHeight / unscaledSize.Height));
}

maxScale = Math.Max(SettingValues.MinWindowScale, maxScale);
return Math.Clamp(requestedScale, SettingValues.MinWindowScale, maxScale);
}

public static void RefreshWindow()
{
// Refresh window opens in the start page again, that is nessesairy
Expand Down
2 changes: 1 addition & 1 deletion WheelWizard/WheelWizard.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<NoWarn>$(NoWarn);AVLN3001</NoWarn>

<!-- Program details -->
<Version>2.4.7</Version>
<Version>2.4.8</Version>
<Description>This program will manage RetroRewind and mods :)</Description>
<Copyright>GNU v3.0</Copyright>
<RepositoryUrl>https://github.com/patchzyy/WheelWizard</RepositoryUrl>
Expand Down
Loading