diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 4d9b8a8..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,1086 +0,0 @@ -# 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!** 🚀