This document provides coding agents with essential information about building, testing, and maintaining this codebase.
StreamDeckSimHub is a Stream Deck plugin for SimHub racing simulation hub, built with .NET 8.0 (C#), WPF, and a React-based Property Inspector UI. The plugin enables Stream Deck buttons to dynamically update based on SimHub properties.
- Language: C# with nullable reference types enabled
- Framework: .NET 8.0-windows (WPF + Windows Forms)
- Architecture: Plugin-based event-driven with MVVM pattern
- Testing: NUnit + Moq
- License: LGPL-3.0-or-later
- .NET 8.0 SDK
- Windows (required for WPF)
- PowerShell (for bundling)
dotnet restore StreamDeckSimHub.Plugin/StreamDeckSimHub.Plugin.csproj
dotnet restore StreamDeckSimHub.PluginTests/StreamDeckSimHub.PluginTests.csproj# Build main plugin
dotnet build StreamDeckSimHub.Plugin/StreamDeckSimHub.Plugin.csproj
# Build tests
dotnet build StreamDeckSimHub.PluginTests/StreamDeckSimHub.PluginTests.csproj
# Build with no restore
dotnet build --no-restore StreamDeckSimHub.Plugin/StreamDeckSimHub.Plugin.csproj# Publish plugin (triggers versioning and bundling via PowerShell scripts)
dotnet publish StreamDeckSimHub.Plugin/StreamDeckSimHub.Plugin.csprojStreamDeckSimHub.sln contains all three projects: the plugin, the plugin tests, and StreamDeckSimHub.Installer (a .NET Framework 4.8 WPF installer, but still an SDK-style project, see StreamDeckSimHub.Installer/AGENTS.md). Building/testing the solution as a whole (e.g. dotnet build StreamDeckSimHub.sln, dotnet test) builds all three projects.
# Run all tests
dotnet test StreamDeckSimHub.PluginTests/StreamDeckSimHub.PluginTests.csproj
# Run tests with no build
dotnet test --no-build --verbosity normal StreamDeckSimHub.PluginTests/StreamDeckSimHub.PluginTests.csproj
# Run single test by full name
dotnet test StreamDeckSimHub.PluginTests/StreamDeckSimHub.PluginTests.csproj --filter "FullyQualifiedName=StreamDeckSimHub.PluginTests.Actions.HotkeyActionTests.TestOldComparisonInteger"
# Run all tests in a class
dotnet test StreamDeckSimHub.PluginTests/StreamDeckSimHub.PluginTests.csproj --filter "FullyQualifiedName~StreamDeckSimHub.PluginTests.Actions.HotkeyActionTests"
# Run tests matching name pattern
dotnet test StreamDeckSimHub.PluginTests/StreamDeckSimHub.PluginTests.csproj --filter "Name~OldComparison"StreamDeckSimHub.Plugin/
├── Actions/ # Stream Deck action implementations
│ ├── GenericButton/ # Complex configurable button (main feature)
│ │ ├── Model/ # Business logic, MVVM models
│ │ ├── JsonSettings/# DTO classes for serialization
│ │ └── Renderer/ # Button rendering (ImageSharp)
│ ├── HotkeyAction.cs # 2-state hotkey
│ ├── Hotkey4Action.cs # 4-state hotkey
│ ├── DialAction.cs # Encoder/dial
│ ├── FlagsAction.cs # Racing flags
│ └── InputAction.cs # Input trigger
├── ActionEditor/ # WPF visual editor
│ ├── Views/ # XAML views
│ ├── ViewModels/ # CommunityToolkit.Mvvm view models
│ └── Behaviors/ # WPF behaviors
├── SimHub/ # SimHub TCP connection & integration
├── PropertyLogic/ # NCalc expression evaluation and other property handling
├── Tools/ # Utilities (ImageManager, StateManager, etc.)
└── pi/ # Property Inspector (HTML/JS/React)
- Always write the simplest, cleanest code possible.
- Code must always be consistent with the rest of the application.
- Never write unnecessary code.
- Remove unused imports.
-
All C# files must include:
// Copyright (C) YEAR Martin Renner // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER)
whereas "YEAR" should be replaced with the current year.
-
When modifying an existing file, update the year in the header to the current year.
- Implicit usings are DISABLED for main plugin project
- Use
GlobalUsings.csfor global imports (seeStreamDeckSimHub.Plugin/GlobalUsings.cs) - Default global usings:
System,System.Collections.Generic,System.Linq,System.Threading,System.Threading.Tasks - Always explicitly import other namespaces at file top
- Order: System namespaces, then third-party, then project namespaces
- Classes/Interfaces: PascalCase (e.g.,
GenericButtonAction,ISimHubConnection) - Methods/Properties: PascalCase (e.g.,
PropertyChanged,DisplayItems) - Private fields:
_camelCasewith underscore prefix (e.g.,_settingsConverter,_imageManager) - Parameters/Locals: camelCase (e.g.,
propertyName,propValueTrue) - Constants: PascalCase (e.g.,
NewActionKeySize)
- Nullable reference types enabled - use
?for nullable types - Use
requiredkeyword for required properties:public required Size KeySize { get; set; } - Prefer explicit types over
varwhen type is not obvious - Use
varfor obvious types:var logger = Mock.Of<ILogger<PropertyComparer>>();
- Dependency Injection: Register services in
Program.cs::ConfigureServices()- Use constructor injection
- Prefer
Singletonfor managers and connections
- MVVM: Use
CommunityToolkit.Mvvmbase classesObservableObjectfor view models[ObservableProperty]attribute for bindable propertiesRelayCommandfor commands
- Action Classes: Inherit from
StreamDeckAction<TSettings>- Decorate with
[StreamDeckAction("action.id")] - Implement lifecycle methods:
OnWillAppear,OnKeyDown, etc.
- Decorate with
- Error Handling:
- Use try-catch for async operations
- Log errors with NLog via injected
ILogger<T> - Propagate exceptions appropriately
- Use
async/awaitfor I/O operations - Async methods should have
Asyncsuffix (except event handlers) - Use
ConfigureAwait(false)where appropriate - Return
Taskfor async void methods (except event handlers)
- Use
HashSet<T>for unique collections with case-insensitive keys:new(StringComparer.OrdinalIgnoreCase) - Use
ObservableCollection<T>for bindable collections in MVVM - Use
SortedDictionary<K, V>for ordered key-value pairs - Prefer collection initializers:
new List<string> { "item1", "item2" }
- XML doc comments for public APIs:
/// <summary>,/// <param>,/// <returns> - Inline comments for complex logic
- Use
<c>code</c>for inline code in XML comments - Use
<remarks>for additional context
- Framework: NUnit
- Mocking: Moq
- Use
[SetUp]for test initialization - Test method naming:
Test<Scenario>(e.g.,TestOldComparisonInteger) - Use
Assert.Thatwith constraint model:Assert.That(actual, Is.EqualTo(expected)) - Mock dependencies:
var mock = Mock.Of<IInterface>();
- DTO classes: End with
Dtosuffix (e.g.,SettingsDto,DisplayItemDto) - Model classes: No suffix (e.g.,
Settings,DisplayItem) - Conversion: Use
SettingsConverterto convert between DTOs and models - Serialization: Use
Newtonsoft.Jsonwith[JsonProperty]attributes
- Subscribe to SimHub properties via
ISimHubConnection - Implement
IPropertyChangedReceiverinterface - Handle property changes in
PropertyChangedmethod - Return fast - use
Taskfor long operations
- Use
ImageManagerfor image caching and retrieval - SVG support via
Svg.Skia - Bitmap support via
SixLabors.ImageSharp - Images from: file system,
@flags,@coreresources
- Use
NCalcHandlerfor property expressions - Expressions support SimHub properties:
[PropertyName] - Use
PropertyComparerfor condition evaluation
- ImplicitUsings: Disabled in main project - always explicitly import
- Windows Forms conflict:
ColorDialogrequires Windows Forms, causes type conflicts with ImageSharp - Nullable warnings: All projects have nullable enabled - handle nulls properly
- StreamDeck SDK: Custom
SharpDeck.dllin/lib/- not a NuGet package - React build: React components are pre-built production files in
pi/react/
Program.cs- DI container configurationGlobalUsings.cs- Global using directivesappsettings.json- NLog and SimHub connection settingsDirectory.Build.props- MSBuild properties (Nerdbank.GitVersioning)version.json- Version configurationmanifest-streamdeck.json- Stream Deck plugin manifest
- This project uses the Gitflow workflow (
develop/release/vX.Y/main) together with Nerdbank.GitVersioning (nbgv), same as the sibling projectSimHubPropertyServer. - GitHub Actions workflows (
.github/workflows/):test.yml- builds and tests the solution on every push.prepare-release.yaml-workflow_dispatchto cut arelease/vX.Ybranch fromdevelop.create-release.yaml-workflow_dispatchto merge a release branch intomain, tag it vianbgv tag, and merge back intodevelop.publish-release.yaml- triggers on pushedvX.Y.Ztags, builds the plugin and the installer, and creates a draft GitHub release with the Stream Deck plugin, the Stream Dock plugin, and the installer attached.
- Full process: see
doc/Release.adoc.
- Repository: https://github.com/pre-martin/StreamDeckSimHubPlugin
- Documentation: See
/doc/folder (AsciiDoc format) - ADR: Architecture Decision Records in
/doc/ADR/