1087 lines
27 KiB
Markdown
1087 lines
27 KiB
Markdown
# 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<string> { "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<string> items = new();
|
|
Dictionary<string, int> 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<MyService> logger, LightlessMediator mediator)
|
|
: base(logger, mediator)
|
|
{
|
|
}
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
// Subscribe to messages
|
|
Mediator.Subscribe<MyCustomMessage>(this, HandleMyMessage);
|
|
Mediator.Subscribe<AnotherMessage>(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**: `<Action><Subject>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<LightlessMediator>();
|
|
collection.AddSingleton<ApiController>();
|
|
collection.AddSingleton<PairManager>();
|
|
|
|
// Scoped services (per-resolution scope)
|
|
collection.AddScoped<UiService>();
|
|
collection.AddScoped<CacheMonitor>();
|
|
|
|
// Hosted services (background services with lifecycle)
|
|
collection.AddHostedService(p => p.GetRequiredService<LightlessMediator>());
|
|
collection.AddHostedService(p => p.GetRequiredService<NotificationService>());
|
|
|
|
// Lazy dependencies (avoid circular dependencies)
|
|
collection.AddSingleton(s => new Lazy<ApiController>(() => s.GetRequiredService<ApiController>()));
|
|
|
|
// Factory pattern
|
|
collection.AddSingleton<GameObjectHandlerFactory>();
|
|
})
|
|
```
|
|
|
|
### Service Patterns
|
|
|
|
#### Standard Service
|
|
```csharp
|
|
public class MyService
|
|
{
|
|
private readonly ILogger<MyService> _logger;
|
|
private readonly SomeDependency _dependency;
|
|
|
|
public MyService(
|
|
ILogger<MyService> 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<MyHostedService> _logger;
|
|
private readonly LightlessMediator _mediator;
|
|
|
|
public LightlessMediator Mediator => _mediator;
|
|
|
|
public MyHostedService(
|
|
ILogger<MyHostedService> logger,
|
|
LightlessMediator mediator)
|
|
{
|
|
_logger = logger;
|
|
_mediator = mediator;
|
|
}
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
_logger.LogInformation("Starting {Service}", nameof(MyHostedService));
|
|
|
|
// Subscribe to mediator messages
|
|
_mediator.Subscribe<SomeMessage>(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<MyService> logger,
|
|
LightlessMediator mediator,
|
|
SomeDependency dependency)
|
|
: base(logger, mediator)
|
|
{
|
|
_dependency = dependency;
|
|
|
|
// Subscribe in constructor or in separate Init method
|
|
Mediator.Subscribe<SomeMessage>(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<T>` 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<TemplateService> _logger;
|
|
private readonly SomeDependency _dependency;
|
|
private readonly SemaphoreSlim _lock = new(1);
|
|
|
|
public TemplateService(
|
|
ILogger<TemplateService> 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<SomeMessage>(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<MyWindow> 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<int> _apiVersion;
|
|
|
|
public string Name => "ExamplePlugin";
|
|
public bool Available { get; private set; }
|
|
|
|
public IpcCallerExample(
|
|
ILogger<IpcCallerExample> logger,
|
|
IDalamudPluginInterface pluginInterface,
|
|
LightlessMediator mediator)
|
|
: base(logger, mediator)
|
|
{
|
|
_pi = pluginInterface;
|
|
|
|
try
|
|
{
|
|
_apiVersion = _pi.GetIpcSubscriber<int>("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<PriorityFrameworkUpdateMessage>(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<Data> FetchDataAsync()
|
|
{
|
|
var result = await _httpClient.GetAsync(url).ConfigureAwait(false);
|
|
return await result.Content.ReadFromJsonAsync<Data>().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<string, Data> _cache = new();
|
|
|
|
// Good: Lock-protected regular collections
|
|
private readonly List<Item> _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<ILogger<MyService>> _mockLogger;
|
|
private Mock<LightlessMediator> _mockMediator;
|
|
private MyService _service;
|
|
|
|
[TestInitialize]
|
|
public void Setup()
|
|
{
|
|
_mockLogger = new Mock<ILogger<MyService>>();
|
|
_mockMediator = new Mock<LightlessMediator>();
|
|
_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<LightlessNotificationAction>
|
|
{
|
|
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<MyConfig>
|
|
{
|
|
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<Result> 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<T>` 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!** 🚀
|