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
28 changes: 24 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest, macos-latest, windows-latest]

steps:
- name: Checkout code
Expand All @@ -26,8 +26,20 @@ jobs:
run: dotnet tool install -g vpk

- name: Add .NET tools to PATH
if: runner.os != 'Windows'
run: echo "${HOME}/.dotnet/tools" >> $GITHUB_PATH

- name: Add .NET tools to PATH (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: echo "$env:USERPROFILE\.dotnet\tools" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append

- name: Install NativeAOT prerequisites (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y clang zlib1g-dev

- name: Restore dependencies
run: dotnet restore

Expand All @@ -36,9 +48,11 @@ jobs:

- name: Extract clean version
id: version
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
shell: bash
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"

- name: Make platform script executable (Linux/macOS only)
- name: Make platform scripts executable (Linux/macOS)
if: runner.os != 'Windows'
run: |
chmod +x MapWizard.Desktop/build-linux.sh
chmod +x MapWizard.Desktop/build-osx.sh
Expand All @@ -49,14 +63,20 @@ jobs:
run: |
cd MapWizard.Desktop
./build-linux.sh ${{ env.VERSION }}
./build-win.sh ${{ env.VERSION }}

- name: Build with Velopack - macOS
if: runner.os == 'macOS'
run: |
cd MapWizard.Desktop
./build-osx.sh ${{ env.VERSION }}

- name: Build with Velopack - Windows
if: runner.os == 'Windows'
shell: cmd
run: |
cd MapWizard.Desktop
build-win.bat %VERSION%

- name: Upload Artifacts
uses: actions/upload-artifact@v6
with:
Expand Down
27 changes: 23 additions & 4 deletions .github/workflows/prerelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest, macos-latest, windows-latest]

steps:
- name: Checkout code
Expand All @@ -202,33 +202,52 @@ jobs:
run: dotnet tool install -g vpk

- name: Add .NET tools to PATH
if: runner.os != 'Windows'
run: echo "${HOME}/.dotnet/tools" >> $GITHUB_PATH

- name: Add .NET tools to PATH (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: echo "$env:USERPROFILE\.dotnet\tools" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append

- name: Install NativeAOT prerequisites (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y clang zlib1g-dev

- name: Restore dependencies
run: dotnet restore

- name: Run tests
run: dotnet test MapWizard.Tests/MapWizard.Tests.csproj --no-restore --verbosity normal

- name: Make platform script executable
- name: Make platform script executable (Linux/macOS)
if: runner.os != 'Windows'
run: |
chmod +x MapWizard.Desktop/build-linux.sh
chmod +x MapWizard.Desktop/build-osx.sh
chmod +x MapWizard.Desktop/build-win.sh

- name: Build with Velopack - Linux and Windows
- name: Build with Velopack - Linux
if: runner.os == 'Linux'
run: |
cd MapWizard.Desktop
./build-linux.sh ${{ needs.prepare.outputs.next_version }}
./build-win.sh ${{ needs.prepare.outputs.next_version }}

- name: Build with Velopack - macOS
if: runner.os == 'macOS'
run: |
cd MapWizard.Desktop
./build-osx.sh ${{ needs.prepare.outputs.next_version }}

- name: Build with Velopack - Windows
if: runner.os == 'Windows'
shell: cmd
run: |
cd MapWizard.Desktop
build-win.bat ${{ needs.prepare.outputs.next_version }}

- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
Expand Down
39 changes: 39 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,42 @@ jobs:

- name: Run tests
run: dotnet test MapWizard.Tests/MapWizard.Tests.csproj --no-restore --verbosity normal

aot:
name: NativeAOT Publish (${{ matrix.name }})
# Publish checks only run on merge to main, not on PRs.
# PRs only run the Linux test job above.
if: github.event_name == 'push'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
name: Linux
rid: linux-x64
- os: windows-latest
name: Windows
rid: win-x64
steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'

- name: Install NativeAOT prerequisites (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y clang zlib1g-dev

- name: Restore dependencies
run: dotnet restore

# MapWizard.Desktop.csproj sets PublishAot=true, so this catches AOT/trim
# regressions before they reach a release build.
- name: Publish with NativeAOT
run: dotnet publish MapWizard.Desktop/MapWizard.Desktop.csproj -c Release -r ${{ matrix.rid }} -o "${{ runner.temp }}/mapwizard-aot"
1 change: 1 addition & 0 deletions MapWizard.CLI/MapWizard.CLI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAOT>true</PublishAOT>
<IsAotCompatible>true</IsAotCompatible>
<LangVersion>latestmajor</LangVersion>
</PropertyGroup>

Expand Down
6 changes: 5 additions & 1 deletion MapWizard.Desktop/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ public override void OnFrameworkInitializationCompleted()

var services = _services ?? throw new InvalidOperationException("Application services were not initialized.");
var mainWindow = services.GetRequiredService<MainWindow>();
mainWindow.Opened += (_, _) => mainWindow.GetViewModel().RequestStartupUpdateCheck();
mainWindow.Opened += (_, _) =>
{
mainWindow.GetViewModel().RequestStartupUpdateCheck();
mainWindow.GetViewModel().PreloadPages(mainWindow.GetPreloadWarmupHost());
};

switch (ApplicationLifetime)
{
Expand Down
3 changes: 2 additions & 1 deletion MapWizard.Desktop/Controls/SmoothScrollViewer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Rendering.Composition;
using Avalonia.Reactive;
using Avalonia.Threading;
using Avalonia.VisualTree;
using MapWizard.Theme.Motion;
Expand Down Expand Up @@ -144,7 +145,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_presenterBoundsSubscription = _presenter
.GetObservable(BoundsProperty)
.Subscribe(_ => UpdateFade());
.Subscribe(new AnonymousObserver<Rect>(_ => UpdateFade()));
}

ResolveContentVisual();
Expand Down
16 changes: 7 additions & 9 deletions MapWizard.Desktop/Converters/MapPathSummaryConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@

namespace MapWizard.Desktop.Converters;

public sealed class MapPathSummaryConverter : IValueConverter
public sealed partial class MapPathSummaryConverter : IValueConverter
{
private static readonly Regex FullPattern =
new(@"^(?<artist>.+?) - (?<title>.+?) \((?<mapper>.+?)\) \[(?<diff>.+?)\]$",
RegexOptions.Compiled);
[GeneratedRegex(@"^(?<artist>.+?) - (?<title>.+?) \((?<mapper>.+?)\) \[(?<diff>.+?)\]$")]
private static partial Regex FullPattern();

private static readonly Regex ShortPattern =
new(@"^(?<artist>.+?) - (?<title>.+?) \[(?<diff>.+?)\]$",
RegexOptions.Compiled);
[GeneratedRegex(@"^(?<artist>.+?) - (?<title>.+?) \[(?<diff>.+?)\]$")]
private static partial Regex ShortPattern();

public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
Expand Down Expand Up @@ -46,7 +44,7 @@ public object Convert(object? value, Type targetType, object? parameter, Culture
return "No map selected";
}

var fullMatch = FullPattern.Match(fileName);
var fullMatch = FullPattern().Match(fileName);
if (fullMatch.Success)
{
var artist = fullMatch.Groups["artist"].Value.Trim();
Expand All @@ -55,7 +53,7 @@ public object Convert(object? value, Type targetType, object? parameter, Culture
return $"{artist} - {title} [{diff}]";
}

var shortMatch = ShortPattern.Match(fileName);
var shortMatch = ShortPattern().Match(fileName);
if (shortMatch.Success)
{
var artist = shortMatch.Groups["artist"].Value.Trim();
Expand Down
17 changes: 16 additions & 1 deletion MapWizard.Desktop/MapWizard.Desktop.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
<SelfContained>true</SelfContained>
<LangVersion>latestmajor</LangVersion>
<Version>1.0.0</Version>
<PublishAot>true</PublishAot>
<IsAotCompatible>true</IsAotCompatible>
<StripSymbols>true</StripSymbols>
</PropertyGroup>

<ItemGroup>
Expand All @@ -26,7 +29,6 @@
<PackageReference Include="OsuMemoryDataProvider" Version="0.12.2" />
<PackageReference Include="OsuWineMemReader" Version="1.0.1" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="4.152.1" />
<PackageReference Include="System.Reactive" Version="7.0.0" />
<PackageReference Include="Velopack" Version="1.2.0" />
<PackageReference Include="Xaml.Behaviors" Version="12.0.7" />
</ItemGroup>
Expand All @@ -49,6 +51,19 @@
<Folder Include="Assets\Skins\Legacy\" />
</ItemGroup>

<!--
These libraries resolve types and members via reflection, so NativeAOT trimming
must keep their metadata intact.
- BeatmapParser: reads/writes .osu sections by enumerating properties.
- OsuMemoryDataProvider/ProcessMemoryDataFinder: structured memory reader maps
model properties via MemoryAddressAttribute.
-->
<ItemGroup>
<TrimmerRootAssembly Include="BeatmapParser" />
<TrimmerRootAssembly Include="OsuMemoryDataProvider" Condition="'$(RuntimeIdentifier)' == 'win-x64'" />
<TrimmerRootAssembly Include="ProcessMemoryDataFinder" Condition="'$(RuntimeIdentifier)' == 'win-x64'" />
</ItemGroup>

<ItemGroup>
<None Include="Native\Bass\linux-x64\libbass.so" Link="libbass.so" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<None Include="Native\Bass\win-x64\bass.dll" Link="bass.dll" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
Expand Down
9 changes: 9 additions & 0 deletions MapWizard.Desktop/Models/DestinationMapsetCard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,13 @@ public partial class DestinationMapsetCard(
public SelectedMap ReferenceBeatmap { get; } = referenceBeatmap;
public ObservableCollection<MapsetDifficultyCard> Difficulties { get; } = new(difficulties);
public bool HasUnselectedDifficulties => Difficulties.Any(difficulty => !difficulty.IsSelected);

/// <summary>
/// Re-raises derived state after <see cref="Difficulties"/> entries were
/// updated in place, so bindings like the select-all button stay current.
/// </summary>
public void RefreshDifficultySelectionState()
{
OnPropertyChanged(nameof(HasUnselectedDifficulties));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,17 @@
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using MapWizard.Tools.ComboColourStudio;

namespace MapWizard.Desktop.Services.ComboColourService;

public class ComboColourProjectStore : IComboColourProjectStore
public partial class ComboColourProjectStore : IComboColourProjectStore
{
private const string AppDirectoryName = "MapWizard";
private const string ProjectDirectoryName = "ComboColourStudio";
private const string StorageFileName = "projects.json";

private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true
};

private readonly object _syncRoot = new();
private readonly string _storageFilePath;

Expand Down Expand Up @@ -98,13 +94,13 @@ private List<PersistedProjectRecord> ReadRecords()
return [];
}

return JsonSerializer.Deserialize<List<PersistedProjectRecord>>(json, JsonOptions) ?? [];
return JsonSerializer.Deserialize(json, ProjectJsonContext.Default.ListPersistedProjectRecord) ?? [];
}

private void WriteRecords(List<PersistedProjectRecord> records)
{
var tempPath = _storageFilePath + ".tmp";
var json = JsonSerializer.Serialize(records, JsonOptions);
var json = JsonSerializer.Serialize(records, ProjectJsonContext.Default.ListPersistedProjectRecord);

File.WriteAllText(tempPath, json);
File.Move(tempPath, _storageFilePath, overwrite: true);
Expand Down Expand Up @@ -241,4 +237,8 @@ private sealed class PersistedColourPoint
public ColourPointMode Mode { get; set; } = ColourPointMode.Normal;
public List<int> ColourSequence { get; set; } = [];
}

[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(List<PersistedProjectRecord>))]
private sealed partial class ProjectJsonContext : JsonSerializerContext;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public sealed class ManagedBassPlaybackService : IAudioPlaybackService, IDisposa
new(StringComparer.OrdinalIgnoreCase);

// Prevent the delegate from being garbage-collected while BASS holds a reference to it.
// This static root is the delegate handed to native BASS_ChannelSetSync, which matters
// because ManagedBass only roots sync callbacks when dynamic code is supported
// (see ChannelReferences.Add) - under NativeAOT that is false.
private static readonly SyncProcedure HitsoundStreamEndSync = OnHitsoundStreamEnd;

private static void OnHitsoundStreamEnd(int handle, int channel, int data, IntPtr user)
Expand Down Expand Up @@ -298,7 +301,16 @@ public bool PlayHitsound(string filePath, float volumeMultiplier = 1f, string pl

// Register a sync to automatically free the stream channel when playback ends,
// preventing handle leaks since stream channels are not auto-freed.
Bass.ChannelSetSync(channelHandle, SyncFlags.End | SyncFlags.Onetime, 0,
//
// NOTE: SyncFlags.Onetime must NOT be used here. ManagedBass.ChannelSetSync wraps the
// procedure in a fresh delegate when Onetime is set, and roots that wrapper only when
// RuntimeFeature.IsDynamicCodeSupported is true - which is false under NativeAOT.
// The wrapper then has no managed root, so the GC can collect it while BASS still
// holds its native function pointer, and the audio thread segfaults when the sync
// fires (reproduced with NativeAOT builds). Without Onetime, ManagedBass passes the
// statically-rooted delegate straight through. End syncs fire once per channel
// lifetime anyway, and freeing the channel in the callback tears down its syncs.
Bass.ChannelSetSync(channelHandle, SyncFlags.End, 0,
HitsoundStreamEndSync);

if (!Bass.ChannelPlay(channelHandle, true))
Expand Down
Loading
Loading