# Lightless Sync Development Guidelines This document outlines the coding standards, architectural patterns, and best practices for contributing to the Lightless Sync project. ## Table of Contents 1. [Project Structure](#project-structure) 2. [Coding Standards](#coding-standards) 3. [Architecture Patterns](#architecture-patterns) 4. [Dependency Injection](#dependency-injection) 5. [Mediator Pattern](#mediator-pattern) 6. [Service Development](#service-development) 7. [UI Development](#ui-development) 8. [Dalamud Integration](#dalamud-integration) 9. [Performance Considerations](#performance-considerations) 10. [Testing](#testing) 11. [Common Patterns](#common-patterns) --- ## Project Structure ### Directory Organization ``` LightlessSync/ ├── Changelog/ # Version history and update notes ├── FileCache/ # File caching and management ├── Interop/ # Game interop and IPC with other plugins │ └── Ipc/ # IPC callers for external plugins ├── LightlessConfiguration/ # Configuration management ├── PlayerData/ # Player data handling and pairs │ ├── Factories/ │ ├── Handlers/ │ ├── Pairs/ │ └── Services/ ├── Services/ # Core business logic services │ ├── CharaData/ # Character data management │ ├── Events/ # Event aggregation │ ├── Mediator/ # Mediator pattern implementation │ └── ServerConfiguration/ ├── UI/ # ImGui-based user interfaces │ ├── Components/ # Reusable UI components │ ├── Handlers/ # UI event handlers │ └── Models/ # UI data models ├── Utils/ # Utility classes └── WebAPI/ # SignalR and HTTP client logic ├── Files/ # File transfer operations └── SignalR/ # SignalR hub connections ``` ### File Organization Rules - **One primary class per file**: File name must match the primary type name - **Partial classes**: Use for large classes (e.g., `CharaDataManager.cs`, `CharaDataManager.Upload.cs`) - **Nested types**: Keep in the same file unless they grow large enough to warrant separation - **Interfaces**: Prefix with `I` (e.g., `IIpcCaller`, `IMediatorSubscriber`) --- ## Coding Standards ### General C# Conventions Follow the `.editorconfig` settings: ```ini # Indentation indent_size = 4 tab_width = 4 end_of_line = crlf # Naming - Interfaces: IPascalCase (begins with I) - Types: PascalCase - Methods: PascalCase - Properties: PascalCase - Private fields: _camelCase (underscore prefix) - Local variables: camelCase - Parameters: camelCase - Constants: PascalCase or UPPER_CASE ``` ### Code Style Preferences #### Namespaces ```csharp // Use block-scoped namespaces (not file-scoped) namespace LightlessSync.Services; public class MyService { // ... } ``` #### Braces ```csharp // Always use braces for control statements if (condition) { DoSomething(); } // Even for single-line statements if (condition) { return; } ``` #### Expression-Bodied Members ```csharp // Prefer expression bodies for properties and accessors public string Name { get; init; } = string.Empty; public bool IsEnabled => _config.Current.Enabled; // Avoid for methods, constructors, and operators public void DoWork() { // Full method body } ``` #### Object Initialization ```csharp // Prefer object initializers var notification = new LightlessNotification { Id = "example", Title = "Example", Message = "This is an example", Type = NotificationType.Info }; // Prefer collection initializers var list = new List { "item1", "item2", "item3" }; ``` #### Null Handling ```csharp // Prefer null coalescing var value = possiblyNull ?? defaultValue; // Prefer null propagation var length = text?.Length ?? 0; // Prefer pattern matching for null checks if (value is null) { return; } // Use nullable reference types public string? OptionalValue { get; set; } public string RequiredValue { get; set; } = string.Empty; ``` #### Modern C# Features ```csharp // Use target-typed new expressions when type is apparent List items = new(); Dictionary map = new(); // Use primary constructors sparingly (prefer traditional constructors for clarity) // AVOID: public class MyService(ILogger logger) : IService // Use record types for DTOs and immutable data public record UserData(string Name, string Id); public record struct PairRequestEntry(string HashedCid, string MessageTemplate, DateTime ReceivedAt); // Use pattern matching if (obj is MyType { Property: "value" } typedObj) { // Use typedObj } ``` --- ## Architecture Patterns ### Mediator Pattern (Core Communication) The Lightless Sync uses a **custom mediator pattern** for decoupled communication between components. #### Key Components 1. **LightlessMediator**: Central message bus 2. **MessageBase**: Base class for all messages 3. **IMediatorSubscriber**: Interface for subscribers 4. **MediatorSubscriberBase**: Base class with auto-cleanup #### Creating Messages ```csharp // In Services/Mediator/Messages.cs public record MyCustomMessage(string Data, int Value) : MessageBase; // For messages that need to stay on the same thread public record SynchronousMessage : MessageBase { public override bool KeepThreadContext => true; } ``` #### Subscribing to Messages ```csharp public class MyService : DisposableMediatorSubscriberBase, IHostedService { public MyService(ILogger logger, LightlessMediator mediator) : base(logger, mediator) { } public Task StartAsync(CancellationToken cancellationToken) { // Subscribe to messages Mediator.Subscribe(this, HandleMyMessage); Mediator.Subscribe(this, HandleAnotherMessage); return Task.CompletedTask; } private void HandleMyMessage(MyCustomMessage message) { Logger.LogDebug("Received: {Data}, {Value}", message.Data, message.Value); // Handle the message } public Task StopAsync(CancellationToken cancellationToken) { // Cleanup is automatic with DisposableMediatorSubscriberBase return Task.CompletedTask; } } ``` #### Publishing Messages ```csharp // Publish to all subscribers Mediator.Publish(new MyCustomMessage("test", 42)); // Publish from non-mediator classes _mediator.Publish(new NotificationMessage( "Title", "Message", NotificationType.Info)); ``` #### Message Guidelines - **Use records** for messages (immutable and concise) - **Keep messages simple**: Only carry data, no logic - **Name clearly**: `Message` (e.g., `PairRequestReceivedMessage`) - **Thread-safe data**: Messages are processed asynchronously unless `KeepThreadContext = true` - **Avoid circular dependencies**: Messages should flow in one direction --- ## Dependency Injection ### Service Registration (Plugin.cs) ```csharp // In Plugin.cs constructor: .ConfigureServices(collection => { // Singleton services (shared state, long-lived) collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); // Scoped services (per-resolution scope) collection.AddScoped(); collection.AddScoped(); // Hosted services (background services with lifecycle) collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); // Lazy dependencies (avoid circular dependencies) collection.AddSingleton(s => new Lazy(() => s.GetRequiredService())); // Factory pattern collection.AddSingleton(); }) ``` ### Service Patterns #### Standard Service ```csharp public class MyService { private readonly ILogger _logger; private readonly SomeDependency _dependency; public MyService( ILogger logger, SomeDependency dependency) { _logger = logger; _dependency = dependency; } public void DoWork() { _logger.LogInformation("Working..."); _dependency.PerformAction(); } } ``` #### Hosted Service (Background Service) ```csharp public class MyHostedService : IHostedService, IMediatorSubscriber { private readonly ILogger _logger; private readonly LightlessMediator _mediator; public LightlessMediator Mediator => _mediator; public MyHostedService( ILogger logger, LightlessMediator mediator) { _logger = logger; _mediator = mediator; } public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting {Service}", nameof(MyHostedService)); // Subscribe to mediator messages _mediator.Subscribe(this, HandleSomeMessage); // Initialize resources return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Stopping {Service}", nameof(MyHostedService)); // Cleanup _mediator.UnsubscribeAll(this); return Task.CompletedTask; } private void HandleSomeMessage(SomeMessage msg) { // Handle message } } ``` #### Mediator Subscriber Service ```csharp public sealed class MyService : DisposableMediatorSubscriberBase { private readonly SomeDependency _dependency; public MyService( ILogger logger, LightlessMediator mediator, SomeDependency dependency) : base(logger, mediator) { _dependency = dependency; // Subscribe in constructor or in separate Init method Mediator.Subscribe(this, HandleMessage); } private void HandleMessage(SomeMessage msg) { Logger.LogDebug("Handling message: {Msg}", msg); // Process message } // Dispose is handled by base class } ``` --- ## Service Development ### Service Responsibilities Services should follow **Single Responsibility Principle**: - **NotificationService**: Handles all in-game notifications - **BroadcastService**: Manages Lightfinder broadcast state - **PairRequestService**: Manages incoming pair requests - **DalamudUtilService**: Utility methods for Dalamud framework operations ### Service Guidelines 1. **Logging**: Always use `ILogger` with appropriate log levels 2. **Error Handling**: Wrap risky operations in try-catch, log exceptions 3. **Async/Await**: Use `ConfigureAwait(false)` for non-UI operations 4. **Thread Safety**: Use locks, `ConcurrentDictionary`, or `SemaphoreSlim` for shared state 5. **Disposal**: Implement `IDisposable` or inherit from `DisposableMediatorSubscriberBase` ### Example Service Template ```csharp using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace LightlessSync.Services; public sealed class TemplateService : DisposableMediatorSubscriberBase, IHostedService { private readonly ILogger _logger; private readonly SomeDependency _dependency; private readonly SemaphoreSlim _lock = new(1); public TemplateService( ILogger logger, LightlessMediator mediator, SomeDependency dependency) : base(logger, mediator) { _logger = logger; _dependency = dependency; } public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting {Service}", nameof(TemplateService)); Mediator.Subscribe(this, HandleSomeMessage); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Stopping {Service}", nameof(TemplateService)); _lock.Dispose(); return Task.CompletedTask; } private async void HandleSomeMessage(SomeMessage msg) { await _lock.WaitAsync().ConfigureAwait(false); try { _logger.LogDebug("Processing message: {Msg}", msg); // Do work await _dependency.ProcessAsync(msg).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "Failed to process message"); } finally { _lock.Release(); } } protected override void Dispose(bool disposing) { if (disposing) { _lock?.Dispose(); } base.Dispose(disposing); } } ``` --- ## UI Development ### UI Base Classes All UI windows inherit from `WindowMediatorSubscriberBase`: ```csharp public class MyWindow : WindowMediatorSubscriberBase { private readonly UiSharedService _uiShared; public MyWindow( ILogger logger, LightlessMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollector) : base(logger, mediator, "My Window Title", performanceCollector) { _uiShared = uiShared; // Window configuration Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse; SizeConstraints = new WindowSizeConstraints { MinimumSize = new Vector2(600, 400), MaximumSize = new Vector2(800, 600) }; } protected override void DrawInternal() { // ImGui drawing code here ImGui.Text("Hello, World!"); } } ``` ### UI Guidelines 1. **Separation of Concerns**: UI should only handle rendering and user input 2. **Use UiSharedService**: Centralized UI utilities (icons, buttons, styling) 3. **Performance**: Use `PerformanceCollectorService` to track rendering performance 4. **Disposal**: Windows are scoped; don't store state that needs to persist 5. **Mediator**: Use mediator to communicate with services 6. **ImGui Best Practices**: - Use `ImRaii` for push/pop operations - Always pair `Begin`/`End` calls - Use `ImGuiHelpers.ScaledDummy()` for responsive spacing - Check `IsOpen` property for visibility ### ImGui Patterns ```csharp // Using ImRaii for automatic cleanup using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"))) using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f)) { if (ImGui.Button("Styled Button")) { // Handle click } } // Tab bars using (var tabBar = ImRaii.TabBar("###my_tabs")) { if (!tabBar) return; using (var tab1 = ImRaii.TabItem("Tab 1")) { if (tab1) { ImGui.Text("Tab 1 content"); } } using (var tab2 = ImRaii.TabItem("Tab 2")) { if (tab2) { ImGui.Text("Tab 2 content"); } } } // Child windows for scrolling regions using (var child = ImRaii.Child("###content", new Vector2(0, -40), false, ImGuiWindowFlags.AlwaysVerticalScrollbar)) { if (!child) return; // Scrollable content here } ``` --- ## Dalamud Integration ### Dalamud Services Dalamud provides various services accessible through constructor injection: ```csharp // Common Dalamud services: IClientState clientState // Player and world state IFramework framework // Game framework events IObjectTable objectTable // Game objects (players, NPCs, etc.) IChatGui chatGui // Chat window access ICommandManager commandManager // Command registration IDataManager gameData // Game data sheets IPluginLog pluginLog // Logging INotificationManager notificationManager // Toast notifications IGameGui gameGui // Game UI access ITextureProvider textureProvider // Icon/texture loading ``` ### Framework Threading **CRITICAL**: Dalamud operates on the **game's main thread**. ```csharp // Safe: Already on framework thread private void OnFrameworkUpdate(IFramework framework) { // Can safely access game state var player = _clientState.LocalPlayer; } // Unsafe: Called from background thread private async Task BackgroundWork() { // DO NOT access game state here! // Use DalamudUtilService to run on framework thread await _dalamudUtil.RunOnFrameworkThread(() => { // Now safe to access game state var player = _clientState.LocalPlayer; }); } ``` ### Game Object Access ```csharp // Always check for null var player = _clientState.LocalPlayer; if (player == null) { return; } // Iterating game objects foreach (var obj in _objectTable) { if (obj is IPlayerCharacter pc) { // Handle player character } } // Finding specific objects var target = _targetManager.Target; if (target is IPlayerCharacter targetPlayer) { // Handle targeted player } ``` ### IPC with Other Plugins Use the IPC pattern for communicating with other Dalamud plugins: ```csharp public sealed class IpcCallerExample : DisposableMediatorSubscriberBase, IIpcCaller { private readonly IDalamudPluginInterface _pi; private readonly ICallGateSubscriber _apiVersion; public string Name => "ExamplePlugin"; public bool Available { get; private set; } public IpcCallerExample( ILogger logger, IDalamudPluginInterface pluginInterface, LightlessMediator mediator) : base(logger, mediator) { _pi = pluginInterface; try { _apiVersion = _pi.GetIpcSubscriber("ExamplePlugin.GetApiVersion"); CheckAvailability(); } catch (Exception ex) { Logger.LogWarning(ex, "Failed to initialize IPC for ExamplePlugin"); } } private void CheckAvailability() { try { var version = _apiVersion.InvokeFunc(); Available = version >= 1; Logger.LogInformation("ExamplePlugin available, version: {Version}", version); } catch { Available = false; } } public void DoSomething() { if (!Available) { Logger.LogWarning("ExamplePlugin not available"); return; } // Call IPC methods } } ``` --- ## Performance Considerations ### Framework Updates The game runs at ~60 FPS. Avoid heavy operations in framework updates: ```csharp Mediator.Subscribe(this, OnTick); private void OnTick(PriorityFrameworkUpdateMessage _) { // Throttle expensive operations if ((DateTime.UtcNow - _lastCheck).TotalSeconds < 1) { return; } _lastCheck = DateTime.UtcNow; // Do lightweight work only } ``` ### Async Operations ```csharp // Good: Non-blocking async public async Task FetchDataAsync() { var result = await _httpClient.GetAsync(url).ConfigureAwait(false); return await result.Content.ReadFromJsonAsync().ConfigureAwait(false); } // Bad: Blocking async public Data FetchDataBlocking() { return FetchDataAsync().GetAwaiter().GetResult(); // Blocks thread! } ``` ### Collection Performance ```csharp // Good: Thread-safe concurrent collections private readonly ConcurrentDictionary _cache = new(); // Good: Lock-protected regular collections private readonly List _items = new(); private readonly object _itemsLock = new(); public void AddItem(Item item) { lock (_itemsLock) { _items.Add(item); } } // Good: SemaphoreSlim for async locks private readonly SemaphoreSlim _asyncLock = new(1); public async Task ProcessAsync() { await _asyncLock.WaitAsync().ConfigureAwait(false); try { // Critical section } finally { _asyncLock.Release(); } } ``` ### Memory Management ```csharp // Dispose pattern for unmanaged resources public class ResourceManager : IDisposable { private bool _disposed; private readonly SemaphoreSlim _semaphore = new(1); public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { // Dispose managed resources _semaphore?.Dispose(); } // Free unmanaged resources _disposed = true; } } ``` --- ## Testing ### Unit Testing ```csharp [TestClass] public class MyServiceTests { private Mock> _mockLogger; private Mock _mockMediator; private MyService _service; [TestInitialize] public void Setup() { _mockLogger = new Mock>(); _mockMediator = new Mock(); _service = new MyService(_mockLogger.Object, _mockMediator.Object); } [TestMethod] public void DoWork_WithValidInput_ReturnsExpectedResult() { // Arrange var input = "test"; // Act var result = _service.DoWork(input); // Assert Assert.AreEqual("expected", result); } } ``` ### Integration Testing Test with real Dalamud services in a controlled environment: ```csharp // Create test harness that mimics Dalamud environment public class DalamudTestHarness { public IClientState ClientState { get; } public IFramework Framework { get; } // ... other services public void SimulateFrameworkUpdate() { // Trigger framework update event } } ``` --- ## Common Patterns ### Notification Pattern ```csharp // Simple notification Mediator.Publish(new NotificationMessage( "Title", "Message body", NotificationType.Info)); // Rich notification with actions var notification = new LightlessNotification { Id = "unique_id", Title = "Action Required", Message = "Do you want to proceed?", Type = NotificationType.Warning, Duration = TimeSpan.FromSeconds(10), Actions = new List { new() { Id = "confirm", Label = "Confirm", Icon = FontAwesomeIcon.Check, Color = UIColors.Get("LightlessGreen"), IsPrimary = true, OnClick = (n) => { _logger.LogInformation("User confirmed"); DoAction(); n.IsDismissed = true; } }, new() { Id = "cancel", Label = "Cancel", Icon = FontAwesomeIcon.Times, OnClick = (n) => n.IsDismissed = true } } }; Mediator.Publish(new LightlessNotificationMessage(notification)); ``` ### Configuration Pattern ```csharp public class MyConfigService : ConfigurationServiceBase { public MyConfigService(string configDirectory) : base(Path.Combine(configDirectory, "myconfig.json")) { } } // Usage _configService.Current.SomeSetting = newValue; _configService.Save(); ``` ### Factory Pattern ```csharp public class GameObjectHandlerFactory { private readonly DalamudUtilService _dalamudUtil; private readonly LightlessMediator _mediator; public GameObjectHandlerFactory( DalamudUtilService dalamudUtil, LightlessMediator mediator) { _dalamudUtil = dalamudUtil; _mediator = mediator; } public GameObjectHandler Create(IntPtr address, string identifier) { return new GameObjectHandler( _dalamudUtil, _mediator, () => address, identifier); } } ``` --- ## Error Handling ### Logging Guidelines ```csharp // Debug: Verbose information for debugging _logger.LogDebug("Processing request {RequestId}", requestId); // Information: General informational messages _logger.LogInformation("User {UserId} connected", userId); // Warning: Recoverable issues _logger.LogWarning("Cache miss for {Key}, will fetch from server", key); // Error: Errors that need attention _logger.LogError(ex, "Failed to process message {MessageId}", messageId); // Critical: Application-breaking errors _logger.LogCritical(ex, "Database connection failed"); ``` ### Exception Handling ```csharp public async Task DoWorkAsync() { try { // Risky operation return await PerformOperationAsync().ConfigureAwait(false); } catch (HttpRequestException ex) { _logger.LogError(ex, "Network error during operation"); // Show user-friendly notification Mediator.Publish(new NotificationMessage( "Network Error", "Failed to connect to server. Please check your connection.", NotificationType.Error)); return Result.Failure; } catch (Exception ex) { _logger.LogError(ex, "Unexpected error during operation"); throw; // Re-throw unexpected exceptions } } ``` --- ## Best Practices Summary ### DO ✅ - Use `DisposableMediatorSubscriberBase` for services that subscribe to mediator - Use `IHostedService` for background services - Always inject `ILogger` for logging - Use `ConfigureAwait(false)` for async operations not requiring UI thread - Dispose of resources properly (SemaphoreSlim, HttpClient, etc.) - Check for null when accessing Dalamud game objects - Use thread-safe collections for shared state - Keep UI logic separate from business logic - Use mediator for cross-component communication - Write unit tests for business logic - Document public APIs with XML comments ### DON'T ❌ - Don't access game objects from background threads without `RunOnFrameworkThread` - Don't block async methods with `.Result` or `.Wait()` - Don't store UI state in services (services are often singletons) - Don't use `async void` except for event handlers - Don't catch `Exception` without re-throwing or logging - Don't create circular dependencies between services - Don't perform heavy operations in framework updates - Don't forget to unsubscribe from mediator messages - Don't hardcode file paths (use `IDalamudPluginInterface.ConfigDirectory`) - Don't use primary constructors (prefer traditional for clarity) --- ## Code Review Checklist Before submitting a pull request, ensure: - [ ] Code follows naming conventions - [ ] All new services are registered in `Plugin.cs` - [ ] Mediator messages are defined in `Messages.cs` - [ ] Logging is appropriate and informative - [ ] Thread safety is considered for shared state - [ ] Disposable resources are properly disposed - [ ] Async operations use `ConfigureAwait(false)` - [ ] UI code is separated from business logic - [ ] No direct game object access from background threads - [ ] Error handling is comprehensive - [ ] Performance impact is considered - [ ] Comments explain "why", not "what" - [ ] No compiler warnings - [ ] Tests pass (if applicable) --- ## Resources - [Dalamud Documentation](https://dalamud.dev) - [ImGui.NET Documentation](https://github.com/mellinoe/ImGui.NET) - [C# Coding Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions) - [.NET API Guidelines](https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/api-guidelines) --- **Happy Coding!** 🚀