Guidance for coding agents working in the osuplayer repository.
Complements README.md with implementation-focused context.
Desktop music player for osu! songs, written in C# / .NET 10.
- UI framework: Avalonia 12.0.1 + ReactiveUI + FluentAvaloniaUI 3.0.0-preview1
- Solution:
OsuPlayer.sln(multi-project)
| Project | Purpose |
|---|---|
OsuPlayer/ |
Main UI app and composition root |
OsuPlayer.Data/ |
Enums, models, shared data contracts |
OsuPlayer.Interfaces/ |
Service interfaces |
OsuPlayer.Services/ |
Service implementations (audio, shuffle, sort, history, etc.) |
OsuPlayer.Extensions/ |
Helpers, value converters, enum extensions |
OsuPlayer.IO/ |
Storage, import, DB reader logic |
OsuPlayer.Network/ |
API and network clients |
OsuPlayer.Tests/ |
NUnit test project |
OsuPlayer.CrashHandler/ |
Separate crash-handler executable |
OsuPlayer.Updater/ |
Separate auto-updater executable |
Run all commands from the repo root.
dotnet restore
dotnet build OsuPlayer.sln -c Debug
dotnet run --project OsuPlayer
dotnet test OsuPlayer.Tests/OsuPlayer.Tests.csproj -c DebugCI (.github/workflows/dotnet.yml) builds and then runs tests from the built test DLLs.
- Composition root:
OsuPlayer/Program.cs—Register(...)method. - Services are registered with Splat (
RegisterLazySingleton<IFoo>(() => new Foo())). - Resolved in ViewModels and windows via
Locator.Current.GetRequiredService<IFoo>(). - Interfaces live in
OsuPlayer.Interfaces/; implementations inOsuPlayer.Services/orOsuPlayer/Modules/.
Every view follows this three-file structure:
Foo.axaml — layout/template (Avalonia XAML)
Foo.axaml.cs — code-behind (event handlers, wiring)
FooViewModel.cs — ViewModel (state + commands, extends ViewModelBase)
Foo.axaml.cs is listed as DependentUpon the .axaml in .csproj.
The codebase uses Founntain.Nein's Bindable<T> instead of plain properties for reactive shared state:
public Bindable<RepeatMode> RepeatMode { get; } = new(Enums.RepeatMode.NoRepeat);React to changes with BindValueChanged:
someBindable.BindValueChanged(e => DoSomething(e.NewValue), true);The true argument fires immediately with the current value.
OsuPlayer.Extensions adds a Next() extension on enums that cycles through all declared values and wraps around:
RepeatMode.Value = RepeatMode.Value.Next();Order of cycling matches the declaration order in the enum.
Value converters in OsuPlayer.Extensions/ValueConverters/ implement IValueConverter and map model values to UI values (icons, strings, colors). Many take a Bindable<T> directly.
Prefer declarative AXAML techniques over programmatic code-behind for responsive layout. Use the following priority order:
-
Container queries — the preferred approach for anything size-driven. Mark an ancestor as a container and use
ContainerQuerystyle blocks to change any property (font size, visibility, spacing, orientation, etc.) at breakpoints. This is the Avalonia equivalent of CSS media/container queries and keeps all layout logic in AXAML.<ScrollViewer Container.Name="myView" Container.Sizing="Width"> <ScrollViewer.Styles> <Style Selector="TextBlock#Title"> <Setter Property="FontSize" Value="48" /> </Style> <ContainerQuery Name="myView" Query="max-width:800"> <Style Selector="TextBlock#Title"> <Setter Property="FontSize" Value="32" /> </Style> </ContainerQuery> </ScrollViewer.Styles> ... </ScrollViewer>
-
OnFormFactor— for structural differences between desktop and mobile that don't need to react to live resizing (resolved once at startup). -
Reflowing panels —
WrapPanelorUniformGridLayoutfor collections of items that should naturally reflow without explicit breakpoints. -
Breakpoint view models / code-behind — last resort only, when the transition involves multiple coordinated changes, non-size triggers, or conditions that cannot be expressed in AXAML (e.g. dynamically changing
ColumnDefinitionsstrings). ExistingSizeChangedhandlers in views likeArtistVieware an example of this pattern where it was unavoidable.
| File | Role |
|---|---|
OsuPlayer/Modules/Audio/Player.cs |
Core player: next/prev, repeat, shuffle, song lifecycle |
OsuPlayer/Modules/Audio/BassEngine.cs |
ManagedBass wrapper (low-level playback) |
OsuPlayer/Modules/Audio/LinuxMprisService.cs |
MPRIS2 D-Bus service — routes GNOME/KDE media keys to the player on Linux |
OsuPlayer/Modules/Audio/Interfaces/ |
IPlayer, IHasPlaylists, IPlayModes, etc. |
OsuPlayer.Services/OsuSongSourceService.cs |
Provides the full song library list |
OsuPlayer.Services/ShuffleService.cs |
Shuffle state and algorithm selection |
OsuPlayer.Services/ShuffleImpl/ |
Concrete shuffle algorithms (RngShuffler, BalancedShuffler, etc.) |
OsuPlayer.Services/SortService.cs |
Song list sort logic |
| View | Purpose |
|---|---|
PlayerControlView |
Bottom player bar (transport controls, volume, repeat, shuffle) |
HomeView |
Main song library browser |
SearchView |
Song search |
PlaylistView |
Playlist browser and playlist song list |
PlaylistEditorView |
Create/edit playlists |
SettingsView |
App settings |
EqualizerView |
EQ configuration |
BeatmapsView |
Beatmap detail view |
BlacklistEditorView |
Manage blacklisted songs |
PlayHistoryView |
Recently played list |
StatisticsView |
Playback statistics |
TopBarView |
Window top bar / navigation |
UserView / EditUserView |
User profile |
| Window | Purpose |
|---|---|
FluentAppWindow |
Main application window |
Miniplayer |
Compact always-on-top player window |
FullscreenWindow |
Fullscreen mode |
LoginWindow / CreateProfileWindow |
Auth flow |
ExportSongsProcessWindow |
Export progress |
| File | Purpose |
|---|---|
OsuPlayer.Data/OsuPlayer/Enums/ |
All enums (RepeatMode, ShuffleMode, PlayDirection, etc.) |
OsuPlayer.Data/DataModels/ |
Song/playlist/user model types |
OsuPlayer.IO/Storage/ |
Config read/write (ConfigContainer, JsonService) |
data/config.json |
Runtime config (user-local, not committed) |
data/playlists.json |
Saved playlists (user-local) |
| Service | Purpose |
|---|---|
HistoryService |
Track play history |
LastFmService |
Last.fm scrobbling |
DiscordService |
Discord rich presence |
ProfileManagerService |
User profile management |
ApiStatisticsService |
Music Player for osu! API stats |
LoggingService |
App-level logging |
JsonService |
Generic JSON persistence |
DbReaderFactory |
Creates the osu! DB reader for the current install type |
- Interfaces first: add to
OsuPlayer.Interfaces/, implement inOsuPlayer.Services/or the appropriate module. - No magic strings for config keys: use the existing
ConfigContainerproperties. - Icon system: use
MaterialIconKindfromMaterial.Icons.Avalonia. Converters map model values to icon kinds. PlayerControlViewandMiniplayermust stay behaviorally aligned. If you change player behavior, update both.- Value converters: when a UI element needs a derived value from a model property, write a converter in
OsuPlayer.Extensions/ValueConverters/rather than putting logic in code-behind. - Tests: unit tests live in
OsuPlayer.Tests/. Add or update tests when changing public behavior. Tests use NUnit 4.
LinuxMprisService implements the MPRIS2 D-Bus interface so GNOME Shell, KDE, and other desktop environments route hardware media keys to Music Player for osu!. It is instantiated by Player.cs on Linux only.
- Do not add an explicit
Tmds.DBus.Protocolpackage reference — it is resolved transitively fromAvalonia.FreeDesktopand must stay in sync with Avalonia. PlayerControlViewandMiniplayermust remain aligned with any transport changes that also affect MPRIS.
The project targets Avalonia 12.0.1 / FluentAvaloniaUI 3.0.0-preview1 / SkiaSharp 3.119.3-preview / ReactiveUI.Avalonia 14.7.1. When writing AXAML:
ItemsRepeater,StackLayout,WrapLayoutdo not exist in Avalonia 12. Do not add the oldAvalonia.Controls.ItemsRepeaterNuGet package. UseItemsControl+ItemsPanelTemplatewithStackPanelorWrapPanelinstead.HyperlinkButtonis in Avalonia core — use it without a namespace prefix, notui:HyperlinkButton.Avalonia.ReactiveUIis replaced byReactiveUI.Avaloniain Avalonia 12 — do not reference the old package name.Avalonia.Diagnostics(the old embedded DevTools package) is not used in Avalonia 12. The project usesAvaloniaUI.DiagnosticsSupportinstead, withthis.AttachDeveloperTools()called inApp.Initialize(). The DevTools UI is a separate global tool (dotnet tool install --global AvaloniaUI.DeveloperTools); press F12 at runtime to connect.
Avalonia uses a single-threaded UI model. All control reads/writes must happen on the UI thread.
- Never block the UI thread — use
async/awaitinstead of.Resultor.Wait(), which risk deadlocks. - Marshal background work to UI — use
Dispatcher.UIThread.Post(() => ...)(fire-and-forget) orawait Dispatcher.UIThread.InvokeAsync(() => ...)(awaitable) to update controls from background threads. - Event handlers already run on the UI thread — don't wrap them in unnecessary
Dispatchercalls. - Use
Dispatcher.UIThread.CheckAccess()to verify thread before updating UI in shared code paths. - Yield during heavy loops — when processing many items on the UI thread, call
await Dispatcher.Yield(DispatcherPriority.Background)between batches to keep the UI responsive.
ListBoxvirtualizes by default — only visible items are rendered. Do not place aListBoxinside aStackPanel— it gives infinite height and disables virtualization. UseGridwithRowDefinitions="*"orDockPanelinstead.ItemsControldoes not virtualize. For large collections, preferListBox(if selection is needed) or wrap items in aScrollViewerfor automatic virtualization.- Using
WrapPanelorStackPanelas anItemsPaneldisables virtualization. OnlyVirtualizingStackPanelvirtualizes.
- Prefer simpler panels (
StackPanel,Panel) overGridwhen possible — they are lighter. - Avoid deeply nesting panels beyond 3 levels. A single
Gridwith proper row/column definitions is often better. - For large scrollable lists, always use
ListBoxwith virtualization, not hundreds of controls in aStackPanelinside aScrollViewer.
- Hide unused controls with
IsVisible="False"instead ofOpacity="0"— invisible controls skip layout and rendering entirely. - Minimize blur effects (
BlurEffect,DropShadowEffectwith blur) — they significantly impact frame rates, especially on lower-end hardware. - Ensure
UseLayoutRounding="True"for crisp text and icon rendering.
- Resolve binding errors — they appear in the Output window and cause repeated failed lookups each frame.
- Use
ObservableCollection<T>for bound lists, notList<T>— plain lists don't notify the UI of changes.
Avalonia uses a CSS-inspired styling system. Follow these guidelines:
- Prefer style classes over inline properties — define shared visual attributes in
<Style>blocks and apply viaClasses="my-class"rather than repeatingBackground,Padding, etc. on every control. - Order selectors general to specific — later declarations win when specificity is equal. Place base styles first, then class-specific, then pseudo-class overrides.
- Local values beat styles — a property set directly on a control (e.g.
FontSize="24"in AXAML) overrides any style setter. Remove the inline value if you want a style to control it. - Use
DynamicResourcefor theme-aware values — prefer{DynamicResource SomeKey}over hardcoded colors so the UI responds to theme changes. - Scope styles appropriately — app-wide styles go in
App.axamlor shared style files; page-specific styles go in<UserControl.Styles>; component-specific styles go in<Control.Styles>. - Use DevTools (F12) at runtime to inspect the visual tree, active styles, and which value source is winning for a given property.
- Pseudo-classes in control themes — overriding a property like
Backgroundon aButtonstyle won't affect the:pointeroverstate if the button's control theme setsBackgroundon an inner template element. Use/template/selectors or override the template to target inner parts.
data/is user-local runtime state. Never commit its contents.- Do not hardcode secrets or credentials anywhere in source.
- Do not edit generated or build outputs (
bin/,obj/). - Keep changes minimal and scoped. Do not refactor unrelated files in the same patch.
Before finishing any change:
dotnet build OsuPlayer.sln -c Debug— must succeed with 0 errors.dotnet test OsuPlayer.Tests/OsuPlayer.Tests.csproj -c Debug— must pass (explain any pre-existing failures).- If you changed a converter, check all views that bind to it.
- If you changed playback behavior, verify both
PlayerControlViewandMiniplayer. - If you changed a service interface, update the registration in
Program.csif needed.
If instructions conflict: explicit user request > nearest AGENTS.md > this root file.