WPF desktop app (.NET 10) for inspecting, creating, and reconstructing ReScene (SRR/SRS) files. Uses MVVM with CommunityToolkit.Mvvm 8.4. The shared library (ReScene.Lib) is a Git submodule at ReScene.Lib/ containing RAR, SRR, and Core modules in a single project.
dotnet build # Build entire solution
dotnet test ReScene.Lib/ReScene.Lib.Tests # Run all library tests
dotnet test # Run all tests
dotnet run --project ReScene.NET # Run the appReScene.NET/ # Solution root
├── ReScene.NET/ # WPF app project (net10.0-windows)
│ ├── Views/ # XAML views + code-behind (.xaml + .xaml.cs)
│ ├── ViewModels/ # CommunityToolkit.Mvvm partial classes
│ ├── Models/ # Plain data classes (DTOs)
│ ├── Services/ # Business logic, interface + implementation pairs
│ ├── Controls/ # Custom WPF controls (DependencyProperty-based)
│ ├── Converters/ # IValueConverter implementations
│ ├── Helpers/ # Static utility classes (e.g., DarkTitleBar)
│ └── Resources/ # Tokens.xaml (design tokens), icons
├── ReScene.Lib/ # Git submodule
│ ├── ReScene.Lib/ # Single library project
│ │ ├── RAR/ # RAR 4.x/5.x header parsing, patching
│ │ ├── SRR/ # SRR/SRS file format reading and writing
│ │ └── Core/ # Brute-force orchestration, reconstruction
│ └── ReScene.Lib.Tests/ # xUnit tests
└── docs/resources/ # Screenshots for README
<TargetFramework>net10.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>- Implicit usings are enabled — do not add
using System;,using System.Collections.Generic;, etc. System.IOis explicitly added via<Using Include="System.IO" />in the csproj- Nullable reference types are enabled globally — always use
?for nullable types
- 4-space indentation in all
.csfiles (no tabs) - 2-space indentation in all
.xamlfiles (no tabs) - Allman brace style — opening brace on its own line:
// CORRECT — Allman style
if (path != null)
{
LoadFile(path);
}
// WRONG — K&R style
if (path != null) {
LoadFile(path);
}- Always use braces for
if/else/for/while/foreachbodies, even single-statement ones:
// CORRECT — always use braces
if (path is null)
{
return;
}
if (path is not null)
{
OutputPath = path;
}
// WRONG — braceless bodies
if (path is null) return;
if (path is not null)
OutputPath = path;- No trailing commas in object initializers or collection expressions:
// CORRECT
var entry = new VersionEntry
{
VersionName = label,
Arguments = args
};
// WRONG — no trailing comma
var entry = new VersionEntry
{
VersionName = label,
Arguments = args,
};Always use multi-line format for XML doc comments. Never use single-line /// <summary>Text</summary>:
// CORRECT — always multi-line
/// <summary>
/// Gets or sets the block CRC value.
/// </summary>
public ushort Crc { get; set; }
// WRONG — never use single-line format
/// <summary>Gets or sets the absolute path to the file on disk.</summary>
public string FullPath { get; set; } = string.Empty;<param>and<returns>tags also use multi-line format- Always add a blank line before each XML doc comment block
- Do not add XML doc comments to private members
- Between methods: Always one blank line
- Between property groups: One blank line between logical groups
- After opening brace: No blank line after
{for class/method bodies - After closing brace: Always a blank line after
}before the next statement, unless the next line is},else,catch,finally, orwhile(do-while):
// CORRECT — blank line after }
if (condition)
{
DoSomething();
}
NextStatement();
// CORRECT — no blank line before else/catch/finally
if (condition)
{
DoSomething();
}
else
{
DoOther();
}
// CORRECT — no blank line before closing }
if (condition)
{
DoSomething();
}- Between logical sections: Blank line between logical chunks in long methods, typically with a comment
Avoid inline magic numbers. Extract repeated or non-obvious numeric literals to named constants:
// CORRECT — named constant
private const int MaxLacingHeaderSize = 256;
byte[] header = new byte[MaxLacingHeaderSize];
// CORRECT — readable expression for buffer sizes
byte[] buffer = new byte[32 * 1024 * 1024];
// WRONG — unexplained magic number
byte[] buffer = new byte[33554432];
byte[] header = new byte[256];Acceptable inline values that don't need constants: 0, 1, -1, 2, 4, 7, 8, 16, 32, 64, 100, 1024.
Use #region / #endregion to group related methods in large files (e.g., #region Commands, #region File Loading). Blank line before #region and after #endregion.
Keep parameters on one line when they fit within ~120 characters. When wrapping, each parameter goes on its own indented line:
// Same line — short enough
public Task<string?> OpenFileAsync(string title, IReadOnlyList<string> filters)
// Wrapped — each on own line
public async Task<SrrCreationResult> CreateAsync(
string outputPath,
IReadOnlyList<string> rarVolumePaths,
IReadOnlyDictionary<string, string>? storedFiles = null,
SrrCreationOptions? options = null,
CancellationToken ct = default)| Element | Convention | Example |
|---|---|---|
| Private fields | _camelCase |
_manager, _srrData, _cts |
| Public properties | PascalCase |
WindowTitle, StatusMessage |
| Methods | PascalCase |
LoadFile, ShowProperties |
| Async methods | PascalCase + Async |
CreateSrrAsync, BrowseInputAsync |
| Constants | PascalCase |
MaxCopyBytes, CharWidth |
| Win32/P/Invoke const | UPPER_SNAKE_CASE |
DWMWA_USE_IMMERSIVE_DARK_MODE |
| Interfaces | IPascalCase |
IFileDialogService, IHexDataSource |
| Local variables | camelCase |
dialog, bytesRead, elapsed |
| Enum values | PascalCase |
LogTarget.System, LogTarget.Phase1 |
| Event handlers | OnEventName |
OnProgress, OnFileCopyProgress |
| Boolean properties | Is/Has/Can prefix |
IsCreating, HasWarning, CanCreate |
| Observable fields | _camelCase |
_windowTitle, _isCreating |
| EventArgs classes | PascalCase + EventArgs |
FileCopyProgressEventArgs |
Always use file-scoped namespaces:
namespace ReScene.NET.ViewModels;Never use block-scoped namespaces.
Sorted by category: System → third-party frameworks → project/domain. Implicit usings cover most System namespaces, so explicit using directives are only needed for non-implicit ones:
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using RAR;
using SRR;
using ReScene.NET.Models;
using ReScene.NET.Services;Use var when the type is obvious or when it reduces noise. Use explicit types when the type is not immediately clear:
// var — type is obvious from new, cast, or well-known methods
var dialog = new OpenFileDialog { Title = "Select File" };
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
var args = Environment.GetCommandLineArgs();
var files = e.Data.GetData(DataFormats.FileDrop) as string[];
var sb = new StringBuilder();
// Explicit type — type is not obvious from the method name
ProcessingResult result = GetProcessingResult();
IReadOnlyList<RARDetailedBlock> blocks = parser.ReadBlocks();Use target-typed new when the type is declared on the left side:
byte[] buffer = new byte[1048576 * 32];
FileStream destStream = new(destPath, FileMode.Create, FileAccess.Write);Use collection expressions [] for empty collections and small inline arrays:
public ObservableCollection<string> LogEntries { get; } = [];
string[] suffixes = ["B", "KB", "MB", "GB", "TB"];Always use explicit access modifiers. Never rely on defaults:
public partial class HomeViewModel : ViewModelBase
{
private readonly IFileDialogService _fileDialog;
internal static class DarkTitleBar { }
}Use for simple one-line members:
public bool HasWarning => !string.IsNullOrEmpty(WarningMessage);
private bool CanCreateSrr() => !IsCreating && !string.IsNullOrWhiteSpace(InputPath);
private void FireProgress(EventArgs e) => Progress?.Invoke(this, e);Use block bodies for anything with multiple statements or conditionals.
Use switch expressions for multi-branch conditional assignments:
ProgressMessage = e.CompletionStatus switch
{
OperationCompletionStatus.Success => "Completed successfully!",
OperationCompletionStatus.Error => "Failed.",
OperationCompletionStatus.Cancelled => "Cancelled.",
_ => "Completed."
};
string defaultName = SelectedTreeNode?.Tag switch
{
SrrStoredFileBlock stored => Path.GetFileName(stored.FileName),
RARDetailedBlock { ItemName: { } name } => name,
_ => "block.bin"
};Use is { } for non-null checks with destructuring. Use property patterns for concise type + state checks:
if (value?.ByteRange is { } range)
{
HexSelectionOffset = range.Offset;
HexSelectionLength = range.Length;
}
if (sender is ReconstructorViewModel { IsRunning: true })
ShowProgressWindow();
if (DataContext is MainWindowViewModel vm)
vm.OpenSceneFile(file);Use string interpolation. Never use string concatenation for building display strings:
$"{e.OperationProgressed:N0} of {e.OperationSize:N0}" // "1,234 of 5,678"
$"{e.Progress:F1}%" // "45.3%"
$"{size:0.##} {suffixes[i]}" // "1.5 GB"
$"{bytesPerSec / (1024 * 1024):F1} MB/s" // "123.4 MB/s"
$"{DateTime.Now:HH:mm:ss} {message}" // "14:30:05 message"
$"{ts.Hours:D2}:{ts.Minutes:D2}:{ts.Seconds:D2}" // "01:23:45"
$"0x{value:X8}" // "0x0000FF00"
$"{b:X2}" // "4A"File sizes use binary divisions (1024), not decimal (1000).
Use StringBuilder for string building in tight loops (e.g., hex rendering), not interpolation.
Use is null / is not null instead of == null / != null:
// CORRECT
if (files is null) return;
if (files is not null) { ... }
// WRONG
if (files == null) return;
if (files != null) { ... }Use null-conditional and null-coalescing operators:
string dir = Path.GetDirectoryName(inputPath) ?? "."; // Null-coalescing
Progress?.Invoke(this, e); // Null-conditional
_cts?.Cancel();Use null-forgiving ! only when guaranteed non-null by surrounding context.
Use string.Empty for default string values, not "":
[ObservableProperty]
private string _inputPath = string.Empty;
public string Name { get; set; } = string.Empty;Use method syntax exclusively. Do not use query syntax (from x in ...):
// CORRECT — method syntax
var sorted = files.OrderBy(f => f.Name).ToList();
var matches = blocks.Where(b => b.Type == BlockType.File).Select(b => b.Name);
// WRONG — query syntax (not used in this codebase)
var sorted = from f in files orderby f.Name select f;Use _ for unused parameters, especially sender in event handlers and code-behind click handlers:
// Event handler — sender not used
private void OnProgress(object? _, BruteForceProgressEventArgs e)
{
ProgressPercent = e.Progress;
}
// Code-behind click handler — sender not used
private void OnExitClick(object _, RoutedEventArgs e)
{
Close();
}
// Lambda — both parameters unused
SourceInitialized += (_, _) => DarkTitleBar.Enable(this);
// Lambda — only sender unused
_srrService.Progress += (_, e) => LogMessage?.Invoke(_, e); // WRONG — forward sender
logger.Logged += (s, e) => LogMessage?.Invoke(s, e); // OK — sender is forwardedNote: In code-behind, WPF event handler signatures require object sender (not object? sender), so use object _ there.
Always use using statements for disposable resources:
using FileStream sourceStream = File.OpenRead(sourcePath);
using (FileStream destStream = new(destPath, FileMode.Create, FileAccess.Write))
{
// ...
}Use [GeneratedRegex] for compile-time regex generation:
[GeneratedRegex(@"(?:win)?(?:rar|wr)(?:-x64|-x32)?-?(\d+)(b\d+)?", RegexOptions.IgnoreCase)]
private static partial Regex VersionLabelRegex();All ViewModels inherit from ViewModelBase and must be partial classes:
public abstract class ViewModelBase : ObservableObject { }
public partial class HomeViewModel : ViewModelBase { }Follow this order within a ViewModel class:
- Constants and static fields
- Private readonly fields (dependencies, services)
- Private fields (Stopwatch, CancellationTokenSource, etc.)
- Constructor(s)
- Observable properties (
[ObservableProperty]fields) - Computed properties (read-only
=>expressions) - Observable collections (
ObservableCollection<T>properties) - Commands (
[RelayCommand]methods) and their CanExecute methods - Property change handlers (
partial void On<Property>Changed) - Event handlers (
OnProgress,OnStatusChanged, etc.) - Private helper methods
- IDisposable implementation (if applicable)
Declare as private fields with [ObservableProperty]. The source generator creates a public PascalCase property:
// Field _windowTitle → generated public property WindowTitle
[ObservableProperty]
private string _windowTitle = "ReScene.NET";
// Notify a command to re-evaluate CanExecute when this property changes
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(CreateSrrCommand))]
private bool _isCreating;
// Notify dependent computed properties when this property changes
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasWarning))]
private string? _warningMessage;
// Computed property (read-only, no backing field, no attribute)
public bool HasWarning => !string.IsNullOrEmpty(WarningMessage);The MVVM Toolkit generates partial void On<PropertyName>Changed hooks. Implement to handle side effects:
partial void OnInputPathChanged(string value)
{
if (!string.IsNullOrWhiteSpace(value))
IsSfvInput = Path.GetExtension(value).Equals(".sfv", StringComparison.OrdinalIgnoreCase);
}
partial void OnSelectedTreeNodeChanged(TreeNodeViewModel? value)
{
Properties.Clear();
if (value?.Tag is RARDetailedBlock detailedBlock)
{
ShowDetailedBlockProperties(detailedBlock);
SetHexBlock(detailedBlock.StartOffset, detailedBlock.TotalSize);
}
}Use [RelayCommand] on private methods. The generated command property name is {MethodName}Command:
// Generates OpenInspectCommand
[RelayCommand]
private async Task OpenInspectAsync() { ... }
// Generates CreateSrrCommand with CanExecute check
[RelayCommand(CanExecute = nameof(CanCreateSrr))]
private async Task CreateSrrAsync() { ... }
private bool CanCreateSrr() => !IsCreating
&& !string.IsNullOrWhiteSpace(InputPath)
&& !string.IsNullOrWhiteSpace(OutputPath);
// Generates OpenRecentFileCommand with parameter
[RelayCommand]
private void OpenRecentFile(RecentFileEntry entry) { ... }Always use ObservableCollection<T> for bindable lists. Initialize with = []:
public ObservableCollection<string> StoredFiles { get; } = [];
public ObservableCollection<PropertyItem> Properties { get; } = [];Use public partial nested classes inheriting from ObservableObject for tightly-coupled display items:
public partial class VersionEntry : ObservableObject
{
[ObservableProperty] private string _versionName = "";
[ObservableProperty] private string _status = "Testing";
[ObservableProperty] private string _arguments = "";
[ObservableProperty] private string _result = "";
}No DI container is used. Services are instantiated manually in App.xaml.cs and passed to the root ViewModel:
// App.xaml.cs
MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(
new SrrCreationService(), new SrsCreationService(), new BruteForceService(),
new FileCompareService(), new FileDialogService(), new RecentFilesService())
};Root ViewModel creates child ViewModels and passes dependencies down.
ViewModels receive dependencies via constructor injection and store as private readonly fields:
public partial class CreatorViewModel : ViewModelBase
{
private readonly ISrrCreationService _srrService;
private readonly IFileDialogService _fileDialog;
public CreatorViewModel(ISrrCreationService srrService, IFileDialogService fileDialog)
{
_srrService = srrService;
_fileDialog = fileDialog;
_srrService.Progress += OnProgress;
}
}Primary constructor syntax is also used in some ViewModels:
public partial class InspectorViewModel(IFileDialogService fileDialog) : ViewModelBase, IDisposable
{
private readonly IFileDialogService _fileDialog = fileDialog;
}MainWindowViewModel has a parameterless constructor that chains to the full constructor for XAML designer support:
public MainWindowViewModel()
: this(new SrrCreationService(), new SrsCreationService(), ...) { }
public MainWindowViewModel(ISrrCreationService srrService, ...) { }- All async methods end with
Asyncsuffix - Never use
async void— always returnasync Task - Do not use
ConfigureAwait(false)in UI code (WPF needs the synchronization context)
private CancellationTokenSource? _cts;
[RelayCommand(CanExecute = nameof(CanCreateSrr))]
private async Task CreateSrrAsync()
{
IsCreating = true;
_cts = new CancellationTokenSource();
try
{
var result = await _srrService.CreateFromSfvAsync(
OutputPath, InputPath, options, _cts.Token);
}
catch (OperationCanceledException)
{
Log("Cancelled.");
}
catch (Exception ex)
{
Log($"Error: {ex.Message}");
}
finally
{
IsCreating = false;
_cts?.Dispose();
_cts = null;
}
}
[RelayCommand]
private void CancelCreation()
{
_cts?.Cancel();
Log("Cancellation requested...");
}- Catch
OperationCanceledExceptionspecifically for cancellation — log a simple "Cancelled." message - Catch general
Exception exfor everything else — logex.Messageto the UI - Do not silently swallow exceptions — always log or display them
- Use try-catch in all
[RelayCommand]async methods that call services (see Cancellation Pattern above)
ViewModels that hold disposable resources (e.g., MemoryMappedDataSource) implement IDisposable. Always call GC.SuppressFinalize(this) in Dispose() — even when there is no finalizer. This is a .NET best practice (CA1816) that protects against future changes and subclasses.
Non-inheritable classes — simple Dispose():
public partial class FileCompareViewModel(...) : ViewModelBase, IDisposable
{
public void Dispose()
{
_leftFileSource?.Dispose();
_rightFileSource?.Dispose();
GC.SuppressFinalize(this);
}
}Inheritable classes — full Dispose(bool) pattern:
public partial class InspectorViewModel(IFileDialogService fileDialog) : ViewModelBase, IDisposable
{
private bool _disposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
_dataSource?.Dispose();
}
_disposed = true;
}
}The owning window or parent ViewModel calls Dispose() during cleanup (e.g., in OnClosing).
Every service has an interface (IFooService) and implementation (FooService):
public interface IFileDialogService
{
Task<string?> OpenFileAsync(string title, IReadOnlyList<string> filters);
Task<string?> SaveFileAsync(string title, string defaultExtension,
IReadOnlyList<string> filters, string? defaultFileName = null);
Task<string?> OpenFolderAsync(string title);
}
public class FileDialogService : IFileDialogService
{
public Task<string?> OpenFileAsync(string title, IReadOnlyList<string> filters)
{
var dialog = new OpenFileDialog
{
Title = title,
Filter = BuildFilter(filters),
Multiselect = false
};
return Task.FromResult(dialog.ShowDialog() == true ? dialog.FileName : null);
}
}Services expose events for progress. ViewModels subscribe in constructors:
// Service
public event EventHandler<BruteForceProgressEventArgs>? Progress;
public event EventHandler<FileCopyProgressEventArgs>? FileCopyProgress;
// ViewModel constructor
_bruteForceService.Progress += OnProgress;
_bruteForceService.FileCopyProgress += OnFileCopyProgress;Filter format is "Description|*.ext1;*.ext2", passed as IReadOnlyList<string>:
string? path = await _fileDialog.OpenFileAsync("Select Input File",
["SFV Files|*.sfv", "RAR Files|*.rar", "All Files|*.*"]);Service events fire on background threads. Use Dispatcher to update UI properties.
Use when you need all updates to complete before returning:
private void OnProgress(object? sender, BruteForceProgressEventArgs e)
{
Application.Current.Dispatcher.Invoke(() =>
{
ProgressPercent = e.Progress;
PhaseDescription = e.PhaseDescription;
});
}Use for deferred execution, especially for high-frequency progress events and opening modals:
private void OnFileCopyProgress(object? sender, FileCopyProgressEventArgs e)
{
Application.Current.Dispatcher.BeginInvoke(() =>
{
if (!IsCopying)
{
IsCopying = true;
_copyStopwatch.Restart();
}
CopyProgressPercent = (double)e.BytesCopied / e.TotalBytes * 100;
});
}Never open modal dialogs directly in PropertyChanged handlers. Always defer with BeginInvoke:
private void Vm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ReconstructorViewModel.IsRunning)
&& sender is ReconstructorViewModel { IsRunning: true })
{
Dispatcher.BeginInvoke(DispatcherPriority.Normal, () =>
{
var window = new BruteForceProgressWindow
{
Owner = Window.GetWindow(this),
DataContext = DataContext
};
window.ShowDialog();
});
}
}Simple DTOs with auto-properties and initializers. No constructor logic:
public class PropertyItem
{
public string Name { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
public ByteRange? ByteRange { get; set; }
public bool HasByteRange => ByteRange != null;
public bool IsIndented { get; set; }
public bool IsDifferent { get; set; }
}Do not use record types for models — this codebase uses classes with mutable properties.
Implement IValueConverter. Register in App.xaml as StaticResource. Throw NotSupportedException for unsupported ConvertBack:
public class InverseBoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool b)
return b ? Visibility.Collapsed : Visibility.Visible;
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}Registration in App.xaml:
<local:InverseBoolToVisibilityConverter x:Key="InverseBoolToVisibility" />Use the standard WPF DependencyProperty registration pattern:
public static readonly DependencyProperty DataSourceProperty =
DependencyProperty.Register(
nameof(DataSource),
typeof(IHexDataSource),
typeof(HexViewControl),
new PropertyMetadata(null, OnDataChanged));
public IHexDataSource? DataSource
{
get => (IHexDataSource?)GetValue(DataSourceProperty);
set => SetValue(DataSourceProperty, value);
}With optional coercion:
public static readonly DependencyProperty BytesPerLineProperty =
DependencyProperty.Register(nameof(BytesPerLine), typeof(int), typeof(HexViewControl),
new PropertyMetadata(16, OnBytesPerLineChanged, CoerceBytesPerLine));
private static object CoerceBytesPerLine(DependencyObject d, object baseValue)
{
int val = (int)baseValue;
return Math.Max(1, Math.Min(val, 128));
}Keep code-behind minimal. Only the following belongs in code-behind:
- TreeView selection — WPF TreeView doesn't support two-way SelectedItem binding:
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) { if (DataContext is InspectorViewModel vm) vm.SelectedTreeNode = e.NewValue as TreeNodeViewModel; }
- Drag-and-drop handling (DragOver, Drop events)
- Window lifecycle — OnClosing cleanup, OnContentRendered for command-line args
- Platform-specific —
DarkTitleBar.Enable(this)in SourceInitialized - Modal window management — opening/closing progress dialogs from PropertyChanged
- Event cleanup — always unsubscribe from events in OnClosing:
protected override void OnClosing(CancelEventArgs e) { if (DataContext is ReconstructorViewModel vm) vm.PropertyChanged -= Vm_PropertyChanged; base.OnClosing(e); }
Access ViewModel from code-behind using pattern matching:
if (DataContext is MainWindowViewModel vm)
vm.OpenSceneFile(file);Never put business logic, data transformation, or state management in code-behind.
- 2-space indentation (not 4, not tabs)
- Attributes on separate lines when there are 3 or more
- Section comments with decorative borders:
<!-- ── Section Name ─────────────────────────────────── -->
-
DynamicResourcefor all theme-aware values (brushes, colors, font sizes, spacing):<TextBlock FontSize="{DynamicResource FontSizeBody}" Foreground="{DynamicResource ForegroundPrimary}" />
-
StaticResourcefor styles, converters, and non-theme resources:<Button Style="{StaticResource PrimaryButton}" /> <Border Style="{StaticResource PanelSection}" /> <TextBlock Visibility="{Binding IsVisible, Converter={StaticResource BoolToVisibility}}" />
All tokens defined in Resources/Tokens.xaml. Always use tokens — never hardcode colors, font sizes, or spacing:
<!-- Typography -->
<FontFamily x:Key="UIFontFamily">Segoe UI</FontFamily>
<FontFamily x:Key="MonoFontFamily">Cascadia Mono, Consolas, Courier New, monospace</FontFamily>
<sys:Double x:Key="FontSizeH1">20</sys:Double>
<sys:Double x:Key="FontSizeH2">16</sys:Double>
<sys:Double x:Key="FontSizeBody">14</sys:Double>
<sys:Double x:Key="FontSizeCaption">12</sys:Double>
<!-- Spacing -->
<Thickness x:Key="SpacingMD">8</Thickness>
<Thickness x:Key="PageMargin">12</Thickness>
<!-- Colors (dark theme) -->
<SolidColorBrush x:Key="WindowBackground" Color="#FF1E1E1E" />
<SolidColorBrush x:Key="SurfaceBackground" Color="#FF252526" />
<SolidColorBrush x:Key="ForegroundPrimary" Color="#FFD4D4D4" />
<SolidColorBrush x:Key="ForegroundSecondary" Color="#FF999999" />
<SolidColorBrush x:Key="AccentPrimary" Color="#FF0078D4" />
<SolidColorBrush x:Key="AccentError" Color="#FFF44747" />
<SolidColorBrush x:Key="DiffRowBackground" Color="#33F44747" /><!-- Direct binding -->
<TextBlock Text="{Binding StatusMessage}" />
<!-- Command binding ([RelayCommand] on Method → MethodCommand) -->
<Button Command="{Binding BrowseInputCommand}" Content="Browse..." />
<!-- Command with parameter -->
<MenuItem Command="{Binding OpenRecentFileCommand}" CommandParameter="{Binding}" />
<!-- Two-way (explicit only when needed — TextBox Text is already TwoWay by default) -->
<TreeViewItem IsSelected="{Binding IsSelected, Mode=TwoWay}"
IsExpanded="{Binding IsExpanded, Mode=TwoWay}" />
<!-- Real-time input (default UpdateSourceTrigger for TextBox is LostFocus) -->
<TextBox Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged}" />
<!-- Value converter -->
<TextBlock Visibility="{Binding HasWarning, Converter={StaticResource BoolToVisibility}}" />Page-level — DockPanel with top/bottom docked, content fills remaining space:
<DockPanel Margin="{DynamicResource PageMargin}">
<TextBlock DockPanel.Dock="Top" Text="Description" />
<Border DockPanel.Dock="Bottom"><!-- status bar --></Border>
<Grid><!-- main content fills remaining space --></Grid>
</DockPanel>Structured layouts — Grid with explicit definitions:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Content-sized -->
<RowDefinition Height="*" MinHeight="100" /> <!-- Fill -->
<RowDefinition Height="Auto" /> <!-- Splitter -->
<RowDefinition Height="2*" /> <!-- Proportional fill -->
</Grid.RowDefinitions>
</Grid>Resizable panels — GridSplitter between rows/columns:
<GridSplitter Grid.Row="1"
Height="{DynamicResource SplitterHeight}"
HorizontalAlignment="Stretch"
ResizeBehavior="PreviousAndNext" />Panel containers — Border with PanelSection style wrapping DockPanel with header:
<Border Style="{StaticResource PanelSection}">
<DockPanel>
<Border DockPanel.Dock="Top" Style="{StaticResource PanelHeaderBar}">
<TextBlock Style="{StaticResource PanelHeaderText}" Text="Section Title" />
</Border>
<!-- panel content -->
</DockPanel>
</Border>TabControl — Views with child ViewModel DataContext:
<TabControl SelectedIndex="{Binding SelectedTabIndex}" Padding="0">
<TabItem Header="Home">
<v:HomeView DataContext="{Binding Home}" />
</TabItem>
<TabItem Header="Inspector">
<v:InspectorView DataContext="{Binding Inspector}" />
</TabItem>
</TabControl>Standard settings: IsReadOnly="True", AutoGenerateColumns="False", SelectionMode="Single", BorderThickness="0". Use DataTrigger on row style for conditional formatting (e.g., IsDifferent → DiffRowBackground/AccentError). Base row styles on {StaticResource {x:Type DataGridRow}}.
Use HierarchicalDataTemplate with ItemsSource="{Binding Children}". Bind IsSelected/IsExpanded as TwoWay in ItemContainerStyle. Base on {StaticResource {x:Type TreeViewItem}}. Use DataTrigger for conditional foreground (e.g., IsDifferent → AccentError).
<Window x:Class="ReScene.NET.Views.BruteForceProgressWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Brute Force Progress"
Width="750" Height="600"
MinWidth="600" MinHeight="450"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource WindowBackground}"
Foreground="{DynamicResource ForegroundPrimary}"
FontFamily="{DynamicResource UIFontFamily}"
ResizeMode="CanResize">Every window must call DarkTitleBar.Enable(this) in SourceInitialized:
SourceInitialized += (_, _) => DarkTitleBar.Enable(this);KeyBindings must reference commands on the Window's DataContext directly — no dotted paths like Inspector.ExportBlockCommand:
<Window.InputBindings>
<KeyBinding Gesture="Ctrl+O" Command="{Binding OpenFileCommand}" />
<KeyBinding Gesture="Ctrl+E" Command="{Binding ExportStoredFileCommand}" />
</Window.InputBindings>If the actual command lives on a child ViewModel, create a forwarding command on MainWindowViewModel:
[RelayCommand]
private async Task ExportStoredFileAsync() => await Inspector.ExportBlockCommand.ExecuteAsync(null);ViewModel log entries use HH:mm:ss timestamp prefix:
private void Log(string message)
{
string entry = $"{DateTime.Now:HH:mm:ss} {message}";
SystemLog = SystemLog.Length == 0 ? entry : SystemLog + Environment.NewLine + entry;
}Static utility classes go in Helpers/. Use internal static (e.g., DarkTitleBar).
- Commit messages:
type: short description(types:feat,fix,refactor,docs,test,chore) - Do not commit
.env, credentials, or user-specific files - Submodule (
ReScene.Lib): update withgit submodule update --remotewhen needed - Line endings: CRLF (Windows project)
Event handlers subscribed to service events use the On prefix with the event name:
_bruteForceService.Progress += OnProgress;
_bruteForceService.FileCopyProgress += OnFileCopyProgress;
_srrService.Progress += OnProgress;Code-behind event handlers use the On prefix describing the action:
// CORRECT
private void OnLoaded(object _, RoutedEventArgs e) { }
private void OnStopCloseClick(object _, RoutedEventArgs e) { }
private void OnCancelClick(object _, RoutedEventArgs e) { }
private void OnVmPropertyChanged(object? sender, PropertyChangedEventArgs e) { }
private void OnDragOver(object _, DragEventArgs e) { }
// WRONG — WinForms-style naming
private void BtnCancel_Click(object sender, RoutedEventArgs e) { }
private void Vm_PropertyChanged(object? sender, PropertyChangedEventArgs e) { }- Add XML doc comments, type annotations, or comments to code you didn't write or change
- Add comments where the logic is self-evident
- Over-engineer or add abstractions for single-use code
- Create new files unless necessary — prefer editing existing ones
- Use
async void— alwaysasync Task - Swallow exceptions silently — always log
ex.Message - Change formatting or style in lines you didn't modify
- Hardcode colors, font sizes, or spacing — use design tokens from Tokens.xaml
- Use block-scoped namespaces — always file-scoped
- Use string concatenation for display strings — use interpolation
- Use LINQ query syntax — use method syntax
- Use record types for models — use classes with mutable properties
- Open modal dialogs directly in PropertyChanged handlers — defer with BeginInvoke
- Put business logic in code-behind — only UI-specific plumbing belongs there
- Use
ConfigureAwait(false)in UI code - Add trailing commas in object/collection initializers
- Use
== nullor!= null— useis null/is not nullinstead - Omit braces on
if/else/for/while/foreachbodies — always use braces, even for single statements - Omit blank line after
}before the next statement (except beforeelse/catch/finally/}) - Leave unused parameters named — use
_discard for unusedsender,e, etc. - Name event handlers with WinForms-style
BtnFoo_Click— useOnFooClickinstead - Skip
GC.SuppressFinalize(this)in Dispose — always include it