A modern, high-performance plugin framework for SCP: Secret Laboratory that integrates seamlessly with LabAPI.
- High Performance: Optimized event system and minimal overhead
- Modular Architecture: Clean separation of concerns with independent modules
- Developer Friendly: Intuitive APIs, comprehensive documentation, and helpful debugging tools
- LabAPI Integration: Deep integration with the official LabAPI framework
- Advanced Command System: Powerful command registration with automatic parameter parsing
- Dependency Injection: Built-in DI container for loose coupling and testability
- Flexible Configuration: JSON-based configuration with runtime updates
- Comprehensive Logging: Multi-level logging with customizable outputs
- Plugin Management: Dynamic plugin loading and unloading
- Event-Driven: Robust event system for extensibility
LabFramework follows a layered architecture:
- Event Bus: High-performance async event system
- Logging Service: Flexible logging with multiple outputs
- Configuration Service: JSON-based configuration management
- Dependency Injection: Lightweight DI container
- Plugin System: Base classes and interfaces for plugins
- Player Wrapper: Simplified player interaction
- Item Wrapper: Enhanced item management
- Game Events: Framework-specific event definitions
- API Abstractions: Clean abstractions over LabAPI
- Commands (
LabFramework.Commands
): Advanced command system - Permissions (
LabFramework.Permissions
): Hierarchical permission management - Custom Items (
LabFramework.CustomItems
): Custom item creation and management - Custom Roles (
LabFramework.CustomRoles
): Custom role system - Updater (
LabFramework.Updater
): Automatic update system
- Download the latest release from GitHub
- Extract to your SCP:SL server directory
- Ensure LabAPI is installed and working
- Start your server
using System.Threading.Tasks;
using LabFramework.Core;
using LabFramework.Commands;
public class MyPlugin : BasePlugin
{
public override string Name => "My Plugin";
public override string Version => "1.0.0";
public override string Author => "Your Name";
public override string Description => "My awesome plugin";
public override async Task OnLoadAsync()
{
await base.OnLoadAsync();
// Register commands
var commandService = ServiceContainer.Resolve<ICommandService>();
commandService.RegisterCommands(this);
Logger.LogInformation("My plugin loaded!");
}
[Command("hello", "Says hello")]
public CommandResult HelloCommand(CommandContext context)
{
return CommandResult.Successful("Hello from my plugin!");
}
}
Create a config.json
file in your server directory:
{
"LogLevel": "Information",
"EnableAutoUpdates": true,
"PluginDirectory": "plugins",
"CustomSettings": {
"MyPlugin": {
"EnableFeature": true,
"MaxUsers": 100
}
}
}
The command system supports:
- Automatic Parameter Parsing: Convert string arguments to typed parameters
- Optional Parameters: Default values for optional parameters
- Permission Checking: Role-based command access
- Aliases: Multiple names for the same command
- Context Restrictions: Console-only or player-only commands
- Async Support: Full async/await support
[Command("teleport", "Teleport to coordinates", permission: "admin.teleport")]
public CommandResult TeleportCommand(CommandContext context,
[CommandParameter("x", "X coordinate")] float x,
[CommandParameter("y", "Y coordinate")] float y,
[CommandParameter("z", "Z coordinate")] float z)
{
// Implementation here
return CommandResult.Successful($"Teleported to {x}, {y}, {z}");
}
[Command("give", "Give item to player")]
public async Task<CommandResult> GiveItemCommand(CommandContext context,
[CommandParameter("player", "Target player")] string playerName,
[CommandParameter("item", "Item type")] string itemType,
[CommandParameter("amount", "Amount to give", isOptional: true, defaultValue: 1)] int amount)
{
// Async implementation here
await SomeAsyncOperation();
return CommandResult.Successful($"Gave {amount} {itemType} to {playerName}");
}
Subscribe to events using the event bus:
public override async Task OnLoadAsync()
{
await base.OnLoadAsync();
// Subscribe to player events
EventBus.Subscribe<PlayerJoinedEvent>(OnPlayerJoined);
EventBus.Subscribe<PlayerLeftEvent>(OnPlayerLeft);
}
private async Task OnPlayerJoined(PlayerJoinedEvent eventArgs)
{
Logger.LogInformation($"Player {eventArgs.PlayerName} joined the server");
// Send welcome message
var player = GetPlayer(eventArgs.PlayerId);
player?.SendMessage("Welcome to the server!");
}
- .NET 8.0 SDK or later
- LabAPI reference (for integration)
# Clone the repository
git clone https://github.com/CrazyLab/LabFramework.git
cd LabFramework
# Restore dependencies
dotnet restore
# Build the solution
dotnet build --configuration Release
# Run tests (if available)
dotnet test
LabFramework/
├── src/
│ ├── LabFramework.Core/ # Core framework components
│ ├── LabFramework.LabAPI/ # LabAPI integration layer
│ ├── LabFramework.Commands/ # Command system
│ ├── LabFramework.Permissions/ # Permission management
│ ├── LabFramework.CustomItems/ # Custom item system
│ ├── LabFramework.CustomRoles/ # Custom role system
│ ├── LabFramework.Updater/ # Auto-update system
│ └── LabFramework.PluginAPI/ # Plugin development API
├── examples/ # Example plugins
├── tests/ # Unit tests
├── docs/ # Documentation
└── LabFramework.sln # Solution file
We welcome contributions! Please see our Contributing Guide for details.
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.
- Documentation: Wiki
- Issues: GitHub Issues
- Discord: Join our Discord
Feature | LabFramework | EXILED |
---|---|---|
Performance | âÂ�âÂ�âÂ�âÂ�âÂďż˝ | âÂ�âÂ�âÂďż˝ |
LabAPI Integration | âÂ�âÂ�âÂ�âÂ�âÂďż˝ | âÂ�âÂďż˝ |
Command System | âÂ�âÂ�âÂ�âÂ�âÂďż˝ | âÂ�âÂ�âÂďż˝ |
Documentation | âÂ�âÂ�âÂ�âÂ�âÂďż˝ | âÂ�âÂ�âÂďż˝ |
Developer Experience | âÂ�âÂ�âÂ�âÂ�âÂďż˝ | âÂ�âÂ�âÂďż˝ |
Modularity | âÂ�âÂ�âÂ�âÂ�âÂďż˝ | âÂ�âÂ�âÂ�âÂďż˝ |
Testing Support | âÂ�âÂ�âÂ�âÂ�âÂďż˝ | âÂ�âÂďż˝ |
- Complete LabAPI integration
- Advanced permission system
- Custom item editor
- Web-based administration panel
- Plugin marketplace
- Performance monitoring tools
- Multi-language support
LabFramework - The next generation SCP:SL plugin framework
- Core Components
- Event System
- Command System
- Configuration System
- Logging System
- Dependency Injection
- LabAPI Integration
- Plugin Development
The main framework class that coordinates all services.
public class LabFrameworkCore
{
public static LabFrameworkCore Instance { get; }
public IServiceContainer ServiceContainer { get; }
public IEventBus EventBus { get; }
public ILoggingService Logger { get; }
public IConfigurationService Configuration { get; }
public Task InitializeAsync();
public Task ShutdownAsync();
public Task RegisterPluginAsync(IPlugin plugin);
public Task UnregisterPluginAsync(IPlugin plugin);
}
Base interface for all plugins.
public interface IPlugin
{
string Name { get; }
string Version { get; }
string Author { get; }
string Description { get; }
Task OnLoadAsync();
Task OnUnloadAsync();
}
Base implementation of IPlugin with common functionality.
public abstract class BasePlugin : IPlugin
{
protected IEventBus EventBus { get; }
protected ILoggingService Logger { get; }
protected IConfigurationService Configuration { get; }
protected IServiceContainer ServiceContainer { get; }
public virtual Task OnLoadAsync();
public virtual Task OnUnloadAsync();
}
Base interface for all events.
public interface IEvent
{
string EventId { get; }
DateTime Timestamp { get; }
bool IsCancellable { get; }
bool IsCancelled { get; set; }
}
Base implementation of IEvent.
public abstract class BaseEvent : IEvent
{
public string EventId { get; }
public DateTime Timestamp { get; }
public virtual bool IsCancellable => false;
public bool IsCancelled { get; set; }
}
Event bus for publishing and subscribing to events.
public interface IEventBus
{
void Subscribe<T>(EventHandler<T> handler) where T : IEvent;
void Unsubscribe<T>(EventHandler<T> handler) where T : IEvent;
Task PublishAsync<T>(T eventArgs) where T : IEvent;
void Publish<T>(T eventArgs) where T : IEvent;
}
public delegate Task EventHandler<in T>(T eventArgs) where T : IEvent;
// Subscribe to an event
EventBus.Subscribe<PlayerJoinedEvent>(OnPlayerJoined);
// Event handler
private async Task OnPlayerJoined(PlayerJoinedEvent eventArgs)
{
Logger.LogInformation($"Player {eventArgs.PlayerName} joined");
}
// Publish an event
await EventBus.PublishAsync(new PlayerJoinedEvent(playerId, playerName));
Marks a method as a command.
[AttributeUsage(AttributeTargets.Method)]
public class CommandAttribute : Attribute
{
public CommandAttribute(string name, string description = null,
string permission = null, bool consoleOnly = false,
bool playerOnly = false, params string[] aliases);
}
Defines command parameters.
[AttributeUsage(AttributeTargets.Parameter)]
public class CommandParameterAttribute : Attribute
{
public CommandParameterAttribute(string name, string description = null,
bool isOptional = false, object defaultValue = null);
}
Provides context for command execution.
public class CommandContext
{
public string SenderId { get; set; }
public string SenderName { get; set; }
public string[] Arguments { get; set; }
public string RawInput { get; set; }
public bool IsConsole { get; set; }
public object Sender { get; set; }
}
Represents the result of command execution.
public class CommandResult
{
public bool Success { get; set; }
public string Message { get; set; }
public Exception Exception { get; set; }
public static CommandResult Successful(string message = null);
public static CommandResult Failed(string message, Exception exception = null);
}
Service for managing commands.
public interface ICommandService
{
void RegisterCommands(object commandHandler);
void UnregisterCommands(object commandHandler);
Task<CommandResult> ExecuteCommandAsync(CommandContext context);
IEnumerable<CommandInfo> GetCommands();
CommandInfo GetCommand(string name);
}
[Command("teleport", "Teleport to coordinates", permission: "admin.teleport")]
public CommandResult TeleportCommand(CommandContext context,
[CommandParameter("x", "X coordinate")] float x,
[CommandParameter("y", "Y coordinate")] float y,
[CommandParameter("z", "Z coordinate")] float z)
{
// Implementation
return CommandResult.Successful($"Teleported to {x}, {y}, {z}");
}
Service for managing configuration.
public interface IConfigurationService
{
T GetValue<T>(string key, T defaultValue = default);
void SetValue<T>(string key, T value);
Task LoadFromFileAsync(string filePath);
Task SaveToFileAsync(string filePath);
bool HasKey(string key);
void RemoveKey(string key);
}
// Get configuration value
var maxUsers = Configuration.GetValue<int>("MaxUsers", 100);
// Set configuration value
Configuration.SetValue("MaxUsers", 150);
// Load from file
await Configuration.LoadFromFileAsync("config.json");
// Save to file
await Configuration.SaveToFileAsync("config.json");
public enum LogLevel
{
Trace = 0,
Debug = 1,
Information = 2,
Warning = 3,
Error = 4,
Critical = 5,
None = 6
}
Service for logging messages.
public interface ILoggingService
{
void Log(LogLevel level, string message, Exception exception = null);
void LogTrace(string message);
void LogDebug(string message);
void LogInformation(string message);
void LogWarning(string message);
void LogError(string message, Exception exception = null);
void LogCritical(string message, Exception exception = null);
}
Logger.LogInformation("Plugin loaded successfully");
Logger.LogWarning("Configuration value not found, using default");
Logger.LogError("Failed to connect to database", exception);
public enum ServiceLifetime
{
Transient, // New instance every time
Singleton, // Single instance
Scoped // Single instance per scope
}
Container for dependency injection.
public interface IServiceContainer
{
void Register<TService, TImplementation>(ServiceLifetime lifetime = ServiceLifetime.Transient)
where TImplementation : class, TService;
void Register<TService>(Func<IServiceProvider, TService> factory,
ServiceLifetime lifetime = ServiceLifetime.Transient);
void RegisterSingleton<TService>(TService instance);
TService Resolve<TService>();
object Resolve(Type serviceType);
bool TryResolve<TService>(out TService service);
}
// Register services
ServiceContainer.Register<IMyService, MyService>(ServiceLifetime.Singleton);
ServiceContainer.Register<IDatabase>(provider => new Database("connection"),
ServiceLifetime.Singleton);
// Resolve services
var myService = ServiceContainer.Resolve<IMyService>();
Simplified wrapper around LabAPI player functionality.
public class PlayerWrapper
{
public string Id { get; }
public string Name { get; }
public string Role { get; }
public int Health { get; }
public bool IsAlive { get; }
public Vector3 Position { get; }
public List<ItemWrapper> Inventory { get; }
public void SendMessage(string message, int duration = 5);
public void Teleport(Vector3 position);
public void SetHealth(int health);
public void GiveItem(string itemType, int amount = 1);
public void RemoveItem(string itemType, int amount = 1);
public void SetRole(string role);
public void Kill(string reason = "Unknown");
public void Kick(string reason = "Kicked by administrator");
public void Ban(int duration, string reason = "Banned by administrator");
}
Simplified wrapper around LabAPI item functionality.
public class ItemWrapper
{
public string Id { get; }
public string Type { get; }
public string Name { get; }
public int Durability { get; }
public Dictionary<string, object> Properties { get; }
public void SetProperty(string key, object value);
public T GetProperty<T>(string key, T defaultValue = default);
}
Pre-defined events for common game occurrences.
public class PlayerJoinedEvent : BaseEvent
{
public string PlayerId { get; }
public string PlayerName { get; }
public DateTime JoinTime { get; }
}
public class PlayerLeftEvent : BaseEvent
{
public string PlayerId { get; }
public string PlayerName { get; }
public DateTime LeaveTime { get; }
}
public class RoundStartedEvent : BaseEvent
{
public int RoundNumber { get; }
public int PlayerCount { get; }
}
public class RoundEndedEvent : BaseEvent
{
public int RoundNumber { get; }
public string WinningTeam { get; }
public TimeSpan RoundDuration { get; }
}
- Create a class that inherits from
BasePlugin
- Implement required properties
- Override
OnLoadAsync
andOnUnloadAsync
methods - Add command methods with
[Command]
attribute
public class MyPlugin : BasePlugin
{
public override string Name => "My Plugin";
public override string Version => "1.0.0";
public override string Author => "Your Name";
public override string Description => "My awesome plugin";
public override async Task OnLoadAsync()
{
await base.OnLoadAsync();
// Register commands
var commandService = ServiceContainer.Resolve<ICommandService>();
commandService.RegisterCommands(this);
// Subscribe to events
EventBus.Subscribe<PlayerJoinedEvent>(OnPlayerJoined);
Logger.LogInformation("My plugin loaded!");
}
public override async Task OnUnloadAsync()
{
// Cleanup
var commandService = ServiceContainer.Resolve<ICommandService>();
commandService.UnregisterCommands(this);
await base.OnUnloadAsync();
}
private async Task OnPlayerJoined(PlayerJoinedEvent eventArgs)
{
Logger.LogInformation($"Player {eventArgs.PlayerName} joined");
}
[Command("hello", "Says hello")]
public CommandResult HelloCommand(CommandContext context)
{
return CommandResult.Successful("Hello from my plugin!");
}
}
- Error Handling: Always wrap potentially failing code in try-catch blocks
- Resource Cleanup: Properly dispose of resources in
OnUnloadAsync
- Configuration: Use the configuration service for plugin settings
- Logging: Use appropriate log levels for different types of messages
- Performance: Avoid blocking operations in event handlers
- Testing: Write unit tests for your plugin logic
Plugin/
MyPlugin.cs # Main plugin class
Commands/ # Command classes
AdminCommands.cs
PlayerCommands.cs
├── Events/ # Event handlers
PlayerEvents.cs
RoundEvents.cs
Services/ # Plugin services
DatabaseService.cs
ApiService.cs
├── Models/ # Data models
PlayerData.cs
Config/ # Configuration classes
PluginConfig.cs
README.md # Plugin documentation
This documentation provides a comprehensive overview of the LabFramework API. For more detailed examples and tutorials, please refer to the example plugins and the framework's GitHub repository.
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Introduction
- Getting Started
- Core Concepts
- Plugin Development
- Command System
- Permission System
- Custom Items
- Event System
- Configuration Management
- Best Practices
- Troubleshooting
- Advanced Topics
LabFramework is a next-generation plugin framework designed specifically for SCP: Secret Laboratory servers. Built from the ground up with performance, modularity, and developer experience in mind, LabFramework provides a comprehensive platform for creating powerful server modifications while maintaining seamless integration with the official LabAPI.
Traditional plugin frameworks often suffer from performance bottlenecks, complex APIs, and limited extensibility. LabFramework addresses these challenges by implementing modern software engineering principles and leveraging the latest .NET technologies to deliver a framework that is both powerful and easy to use.
The framework's architecture is built around several core principles that set it apart from existing solutions. First, performance is paramount - every component has been optimized for high-throughput scenarios with minimal memory allocation and efficient resource utilization. Second, modularity ensures that developers can use only the components they need, reducing complexity and improving maintainability. Third, the developer experience has been carefully crafted to minimize boilerplate code while providing powerful abstractions that make complex tasks simple.
LabFramework's integration with LabAPI goes beyond simple wrapper functions. The framework provides intelligent abstractions that simplify common operations while still allowing direct access to the underlying LabAPI when needed. This approach ensures that developers can leverage the full power of LabAPI without being constrained by framework limitations.
The framework includes a comprehensive set of features designed to address the most common needs of SCP:SL server administrators and plugin developers. The event system provides high-performance asynchronous event handling with support for cancellable events and priority-based execution. The command system offers automatic parameter parsing, permission integration, and support for both synchronous and asynchronous command execution.
Permission management is handled through a hierarchical system that supports groups, inheritance, and fine-grained access control. The custom items system allows developers to create complex interactive items with custom behaviors, while the configuration system provides runtime updates and type-safe access to settings.
All of these features are built on top of a lightweight dependency injection container that promotes loose coupling and testability. The framework also includes comprehensive logging capabilities with multiple output targets and configurable log levels.
Before you begin developing with LabFramework, ensure that your development environment meets the following requirements:
- .NET 4.8 or later installed on your development machine
- A SCP: Secret Laboratory server with LabAPI installed and configured
- Basic familiarity with C# programming and asynchronous programming concepts
- Understanding of SCP:SL game mechanics and server administration
Installing LabFramework on your server is straightforward and can be accomplished through several methods. The recommended approach is to use the provided installation script, which automates the entire process and ensures proper configuration.
To install LabFramework using the installation script, download the latest release from the GitHub repository and extract it to a temporary directory. Open a terminal or command prompt and navigate to the extracted directory. Run the installation script with appropriate permissions:
chmod +x install.sh
./install.sh
The installation script will prompt you for your server directory and automatically detect your LabAPI installation. It will create the necessary directory structure, copy framework files, and configure the startup scripts.
For manual installation, create a LabFramework
directory in your server root and copy the framework DLL files to this location. Create subdirectories for plugins
, configs
, and logs
. Update your server startup script to include the LabFramework initialization code.
Creating a new LabFramework plugin project requires setting up the proper project structure and dependencies. Start by creating a new .NET class library project using the dotnet CLI or your preferred IDE:
dotnet new classlib -n MyAwesomePlugin
cd MyAwesomePlugin
Add references to the LabFramework components you'll be using. At minimum, most plugins will need the Core and Commands packages:
dotnet add reference path/to/LabFramework.Core.dll
dotnet add reference path/to/LabFramework.Commands.dll
Configure your project file to target .NET 8.0 and enable nullable reference types for better code safety:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
</Project>
Creating your first plugin is an excellent way to understand the framework's basic concepts and development workflow. A simple plugin demonstrates the essential patterns you'll use in more complex projects.
Start by creating a class that inherits from BasePlugin
. This base class provides access to all framework services and handles the plugin lifecycle:
using LabFramework.Core;
using LabFramework.Commands;
public class MyFirstPlugin : BasePlugin
{
public override string Name => "My First Plugin";
public override string Version => "1.0.0";
public override string Author => "Your Name";
public override string Description => "A simple example plugin";
public override async Task OnLoadAsync()
{
await base.OnLoadAsync();
// Register commands
var commandService = ServiceContainer.Resolve<ICommandService>();
commandService.RegisterCommands(this);
Logger.LogInformation("My first plugin has been loaded!");
}
public override async Task OnUnloadAsync()
{
// Clean up resources
var commandService = ServiceContainer.Resolve<ICommandService>();
commandService.UnregisterCommands(this);
await base.OnUnloadAsync();
}
[Command("hello", "Says hello to the user")]
public CommandResult HelloCommand(CommandContext context)
{
return CommandResult.Successful($"Hello, {context.SenderName}!");
}
}
This example demonstrates several important concepts. The plugin class inherits from BasePlugin
and implements the required properties and methods. The OnLoadAsync
method is called when the plugin is loaded and is where you should initialize your plugin's functionality. The OnUnloadAsync
method is called when the plugin is unloaded and should clean up any resources.
The [Command]
attribute marks methods as commands that can be executed by players or administrators. The command system automatically handles parameter parsing and validation, making it easy to create powerful administrative tools.
Understanding LabFramework's core concepts is essential for effective plugin development. These concepts form the foundation upon which all framework functionality is built and provide the architectural patterns that ensure consistency and maintainability across the entire ecosystem.
LabFramework uses dependency injection as a core architectural pattern to promote loose coupling and testability. The service container manages the lifecycle of framework services and provides a centralized location for service registration and resolution.
The dependency injection pattern allows components to declare their dependencies through constructor parameters or service resolution, rather than creating dependencies directly. This approach makes code more modular, testable, and maintainable by reducing tight coupling between components.
Services in LabFramework are registered with specific lifetimes that control how instances are created and managed. Singleton services are created once and reused throughout the application lifetime, making them suitable for stateful services like configuration and logging. Transient services are created each time they're requested, making them suitable for stateless operations. Scoped services are created once per scope, which is useful for request-specific operations.
To register a service, use the service container's registration methods:
ServiceContainer.Register<IMyService, MyService>(ServiceLifetime.Singleton);
ServiceContainer.RegisterSingleton<IDatabase>(new DatabaseConnection("connection-string"));
To resolve services, use the container's resolution methods:
var myService = ServiceContainer.Resolve<IMyService>();
var database = ServiceContainer.Resolve<IDatabase>();
Every plugin in LabFramework follows a well-defined lifecycle that ensures proper initialization and cleanup. Understanding this lifecycle is crucial for developing robust plugins that integrate seamlessly with the framework.
The plugin lifecycle begins when the framework discovers and loads the plugin assembly. The framework creates an instance of the plugin class and calls the OnLoadAsync
method. This method should perform all necessary initialization, including service registration, event subscription, and resource allocation.
During the plugin's active lifetime, it can respond to events, handle commands, and interact with other framework components. The plugin should maintain its state and provide its functionality to users and other plugins.
When the plugin is unloaded, either due to server shutdown or manual unloading, the framework calls the OnUnloadAsync
method. This method should clean up all resources, unregister services, and unsubscribe from events to prevent memory leaks and ensure clean shutdown.
The asynchronous nature of the lifecycle methods allows plugins to perform complex initialization and cleanup operations without blocking the server. This is particularly important for plugins that need to establish database connections, load large configuration files, or perform network operations during startup.
LabFramework provides a powerful configuration system that supports runtime updates, type safety, and hierarchical configuration structures. The configuration system is designed to handle both framework-level settings and plugin-specific configuration with equal ease.
Configuration values are stored in a hierarchical key-value structure that supports nested objects and arrays. The system uses JSON as the primary serialization format, providing human-readable configuration files that are easy to edit and version control.
Type safety is ensured through generic methods that automatically handle type conversion and validation. The configuration system can convert between compatible types and provides meaningful error messages when conversion fails.
Runtime updates allow configuration changes to take effect without restarting the server. Plugins can subscribe to configuration change events to respond immediately to updated settings:
Configuration.SetValue("MyPlugin.MaxUsers", 150);
var maxUsers = Configuration.GetValue<int>("MyPlugin.MaxUsers", 100);
// Subscribe to configuration changes
Configuration.OnValueChanged += (key, oldValue, newValue) =>
{
if (key.StartsWith("MyPlugin."))
{
Logger.LogInformation($"Configuration updated: {key} = {newValue}");
}
};
Robust error handling and comprehensive logging are essential for maintaining stable and debuggable server environments. LabFramework provides sophisticated logging capabilities that support multiple output targets, configurable log levels, and structured logging.
The logging system supports six log levels: Trace, Debug, Information, Warning, Error, and Critical. Each level serves a specific purpose in the debugging and monitoring workflow. Trace and Debug levels are used for detailed diagnostic information during development. Information level provides general operational messages. Warning level indicates potential issues that don't prevent operation. Error level indicates failures that affect functionality. Critical level indicates severe failures that may cause system instability.
Structured logging allows log messages to include additional context information that can be used for filtering, searching, and analysis. The logging system automatically includes timestamp, log level, and source information with each message.
Exception handling should follow established patterns to ensure consistent error reporting and recovery. Catch specific exception types when possible and provide meaningful error messages that help with troubleshooting:
try
{
await SomeRiskyOperation();
}
catch (SpecificException ex)
{
Logger.LogError($"Specific error occurred: {ex.Message}", ex);
// Handle specific error case
}
catch (Exception ex)
{
Logger.LogError($"Unexpected error: {ex.Message}", ex);
// Handle general error case
}
Developing plugins for LabFramework involves understanding the framework's architecture, following established patterns, and leveraging the provided tools and services. This section provides comprehensive guidance for creating robust, maintainable, and performant plugins.
A well-designed plugin follows a clear architectural pattern that separates concerns and promotes maintainability. The recommended architecture consists of several layers, each with specific responsibilities and clear interfaces.
The presentation layer handles user interaction through commands, events, and user interfaces. This layer should be thin and focused on translating user input into business logic calls. Commands should validate input and delegate actual work to service classes.
The business logic layer contains the core functionality of your plugin. This layer should be independent of the presentation layer and focus on implementing the plugin's features. Business logic should be organized into service classes that can be easily tested and reused.
The data access layer handles persistence and external system integration. This layer should abstract the details of data storage and provide clean interfaces for the business logic layer. Whether you're using files, databases, or web APIs, the data access layer should hide these implementation details.
The configuration layer manages plugin settings and provides type-safe access to configuration values. This layer should handle configuration validation and provide sensible defaults for all settings.
Plugins often need to register their own services with the dependency injection container to provide functionality to other plugins or framework components. Service registration should be performed during plugin initialization and follow established patterns for service lifetime management.
When registering services, consider the appropriate lifetime for each service. Singleton services are suitable for stateful services that maintain data across requests. Transient services are appropriate for stateless operations that don't maintain state. Scoped services are useful for request-specific operations that need to maintain state within a single operation.
Service interfaces should be designed to be stable and extensible. Avoid exposing implementation details through interfaces and prefer composition over inheritance when designing service hierarchies:
public interface IPlayerDataService
{
Task<PlayerData> GetPlayerDataAsync(string playerId);
Task SavePlayerDataAsync(PlayerData data);
Task<IEnumerable<PlayerData>> GetTopPlayersAsync(int count);
}
public class DatabasePlayerDataService : IPlayerDataService
{
private readonly IDatabase _database;
private readonly ILoggingService _logger;
public DatabasePlayerDataService(IDatabase database, ILoggingService logger)
{
_database = database;
_logger = logger;
}
// Implementation details...
}
// Register in plugin OnLoadAsync
ServiceContainer.Register<IPlayerDataService, DatabasePlayerDataService>(ServiceLifetime.Singleton);
Events are the primary mechanism for responding to game state changes and implementing reactive functionality. LabFramework's event system is designed for high performance and supports both synchronous and asynchronous event handlers.
Event handlers should be lightweight and avoid blocking operations when possible. Long-running operations should be offloaded to background tasks or queues to prevent impacting server performance. Event handlers should also be defensive and handle exceptions gracefully to prevent one plugin from affecting others.
Event subscription should be performed during plugin initialization, and unsubscription should be performed during plugin cleanup to prevent memory leaks:
public override async Task OnLoadAsync()
{
await base.OnLoadAsync();
EventBus.Subscribe<PlayerJoinedEvent>(OnPlayerJoined);
EventBus.Subscribe<PlayerLeftEvent>(OnPlayerLeft);
}
public override async Task OnUnloadAsync()
{
EventBus.Unsubscribe<PlayerJoinedEvent>(OnPlayerJoined);
EventBus.Unsubscribe<PlayerLeftEvent>(OnPlayerLeft);
await base.OnUnloadAsync();
}
private async Task OnPlayerJoined(PlayerJoinedEvent eventArgs)
{
try
{
var playerData = await _playerDataService.GetPlayerDataAsync(eventArgs.PlayerId);
if (playerData == null)
{
playerData = new PlayerData { PlayerId = eventArgs.PlayerId };
await _playerDataService.SavePlayerDataAsync(playerData);
}
Logger.LogInformation($"Player {eventArgs.PlayerName} joined with {playerData.PlayTime} total play time");
}
catch (Exception ex)
{
Logger.LogError($"Error handling player join event: {ex.Message}", ex);
}
}
Proper resource management is crucial for maintaining server stability and performance. Plugins should carefully manage memory usage, file handles, network connections, and other system resources to prevent resource leaks and ensure clean shutdown.
Implement the IDisposable
pattern for classes that manage unmanaged resources or need explicit cleanup. Use using
statements or using
declarations to ensure resources are properly disposed of even when exceptions occur.
For long-running operations, consider implementing cancellation support using CancellationToken
to allow graceful shutdown when the plugin is unloaded:
public class BackgroundTaskService : IDisposable
{
private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly Task _backgroundTask;
public BackgroundTaskService()
{
_backgroundTask = RunBackgroundTaskAsync(_cancellationTokenSource.Token);
}
private async Task RunBackgroundTaskAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
await DoWorkAsync();
await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Logger.LogError($"Background task error: {ex.Message}", ex);
}
}
}
public void Dispose()
{
_cancellationTokenSource.Cancel();
_backgroundTask.Wait(TimeSpan.FromSeconds(5));
_cancellationTokenSource.Dispose();
}
}
The command system is one of LabFramework's most powerful features, providing a sophisticated framework for creating administrative tools and player interactions. The system handles parameter parsing, validation, permission checking, and execution in a unified and extensible manner.
Commands are defined using the [Command]
attribute on methods within classes that are registered with the command service. The attribute accepts several parameters that control the command's behavior and accessibility.
The command name is the primary identifier used to invoke the command. Names should be descriptive and follow consistent naming conventions. The description provides help text that is displayed to users when they request command information.
Permission strings control who can execute the command. The permission system supports hierarchical permissions and group-based access control. Commands without permission requirements can be executed by anyone.
Aliases provide alternative names for commands, allowing users to use shorter or more familiar names. Aliases are particularly useful for frequently used commands or when migrating from other systems.
Console-only and player-only restrictions control the execution context for commands. Console-only commands can only be executed from the server console, making them suitable for administrative operations. Player-only commands can only be executed by connected players, making them suitable for player-specific operations:
[Command("teleport", "Teleport a player to coordinates",
permission: "admin.teleport", aliases: new[] { "tp", "goto" })]
public async Task<CommandResult> TeleportCommand(CommandContext context,
[CommandParameter("player", "Target player name")] string playerName,
[CommandParameter("x", "X coordinate")] float x,
[CommandParameter("y", "Y coordinate")] float y,
[CommandParameter("z", "Z coordinate")] float z)
{
var player = await FindPlayerAsync(playerName);
if (player == null)
{
return CommandResult.Failed($"Player '{playerName}' not found");
}
player.Teleport(new Vector3(x, y, z));
return CommandResult.Successful($"Teleported {player.Name} to ({x}, {y}, {z})");
}
The command system provides automatic parameter parsing and type conversion, eliminating the need for manual string parsing and validation. Parameters are defined using the [CommandParameter]
attribute, which provides metadata about the parameter's purpose and constraints.
Required parameters must be provided by the user and will cause command execution to fail if missing. Optional parameters have default values and can be omitted by the user. The default value is used when the parameter is not provided.
Type conversion is handled automatically for common types including strings, numbers, booleans, and enums. Custom type converters can be registered for complex types or domain-specific objects.
Parameter validation can be implemented through custom attributes or by checking parameter values within the command method. Validation should provide clear error messages that help users understand what went wrong:
[Command("ban", "Ban a player from the server")]
public async Task<CommandResult> BanCommand(CommandContext context,
[CommandParameter("player", "Player to ban")] string playerName,
[CommandParameter("duration", "Ban duration in minutes", isOptional: true, defaultValue: 60)] int duration,
[CommandParameter("reason", "Ban reason", isOptional: true, defaultValue: "No reason provided")] string reason)
{
if (duration <= 0)
{
return CommandResult.Failed("Duration must be greater than 0");
}
if (duration > 43200) // 30 days
{
return CommandResult.Failed("Duration cannot exceed 30 days");
}
var player = await FindPlayerAsync(playerName);
if (player == null)
{
return CommandResult.Failed($"Player '{playerName}' not found");
}
player.Ban(duration, reason);
return CommandResult.Successful($"Banned {player.Name} for {duration} minutes. Reason: {reason}");
}
LabFramework fully supports asynchronous command execution, allowing commands to perform long-running operations without blocking the server. Asynchronous commands should return Task<CommandResult>
instead of CommandResult
.
Asynchronous operations are particularly useful for commands that need to access databases, make web requests, or perform file I/O operations. The command system handles the asynchronous execution transparently and ensures that exceptions are properly caught and reported.
When implementing asynchronous commands, consider providing progress feedback for long-running operations and implement cancellation support where appropriate:
[Command("backup", "Create a server backup", consoleOnly: true)]
public async Task<CommandResult> BackupCommand(CommandContext context,
[CommandParameter("name", "Backup name", isOptional: true)] string backupName = null)
{
backupName ??= $"backup_{DateTime.UtcNow:yyyyMMdd_HHmmss}";
try
{
Logger.LogInformation($"Starting backup: {backupName}");
var backupService = ServiceContainer.Resolve<IBackupService>();
var progress = new Progress<BackupProgress>(p =>
{
Logger.LogInformation($"Backup progress: {p.Percentage}% - {p.CurrentFile}");
});
await backupService.CreateBackupAsync(backupName, progress);
return CommandResult.Successful($"Backup '{backupName}' created successfully");
}
catch (Exception ex)
{
Logger.LogError($"Backup failed: {ex.Message}", ex);
return CommandResult.Failed($"Backup failed: {ex.Message}");
}
}
The CommandContext
provides information about the command execution environment, including the sender, arguments, and execution context. This information is essential for implementing context-aware command behavior.
The sender information includes the user ID, display name, and whether the command was executed from the console. This information can be used to customize command behavior based on who is executing the command.
The arguments array contains the raw command arguments as provided by the user. While the parameter system handles most parsing needs, the raw arguments can be useful for implementing variable-length parameter lists or custom parsing logic.
The execution context indicates whether the command was executed from the console or by a player. This information is automatically used by the framework to enforce console-only and player-only restrictions, but can also be used by command implementations to provide different behavior in different contexts:
[Command("status", "Show server status")]
public CommandResult StatusCommand(CommandContext context)
{
var serverInfo = GetServerInfo();
if (context.IsConsole)
{
// Provide detailed information for console users
return CommandResult.Successful($@"
Server Status:
Players: {serverInfo.PlayerCount}/{serverInfo.MaxPlayers}
Uptime: {serverInfo.Uptime}
Memory: {serverInfo.MemoryUsage:F1} MB
CPU: {serverInfo.CpuUsage:F1}%
Round: {serverInfo.RoundNumber} ({serverInfo.RoundTime})
");
}
else
{
// Provide basic information for players
return CommandResult.Successful($"Players: {serverInfo.PlayerCount}/{serverInfo.MaxPlayers} | Round: {serverInfo.RoundNumber}");
}
}
#-------------------------------------------------------------------------------------------------------------------------------------------------------
Welcome to LabFramework! This guide will help you get up and running with the framework in just a few minutes. By the end of this guide, you'll have a working plugin that demonstrates the core features of LabFramework.
- .NET 4.8 SDK installed
- SCP: Secret Laboratory server with LabAPI
- Basic C# knowledge
- Download the latest LabFramework release
- Extract to your server directory
- Run the installation script:
chmod +x install.sh
./install.sh
Create a new directory for your plugin:
mkdir MyFirstPlugin
cd MyFirstPlugin
dotnet new classlib
Add LabFramework references:
dotnet add reference ../LabFramework/LabFramework.Core.dll
dotnet add reference ../LabFramework/LabFramework.Commands.dll
Replace the contents of Class1.cs
with:
using LabFramework.Core;
using LabFramework.Commands;
using LabFramework.Core.Events;
namespace MyFirstPlugin
{
public class Plugin : BasePlugin
{
public override string Name => "My First Plugin";
public override string Version => "1.0.0";
public override string Author => "Your Name";
public override string Description => "My first LabFramework plugin";
public override async Task OnLoadAsync()
{
await base.OnLoadAsync();
// Register commands
var commandService = ServiceContainer.Resolve<ICommandService>();
commandService.RegisterCommands(this);
// Subscribe to events
EventBus.Subscribe<PlayerJoinedEvent>(OnPlayerJoined);
Logger.LogInformation("My First Plugin loaded successfully!");
}
public override async Task OnUnloadAsync()
{
// Clean up
var commandService = ServiceContainer.Resolve<ICommandService>();
commandService.UnregisterCommands(this);
EventBus.Unsubscribe<PlayerJoinedEvent>(OnPlayerJoined);
await base.OnUnloadAsync();
}
[Command("hello", "Say hello to a player")]
public CommandResult HelloCommand(CommandContext context,
[CommandParameter("name", "Player name")] string name)
{
return CommandResult.Successful($"Hello, {name}! Welcome to the server!");
}
[Command("time", "Show current server time")]
public CommandResult TimeCommand(CommandContext context)
{
return CommandResult.Successful($"Current server time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
}
private async Task OnPlayerJoined(PlayerJoinedEvent eventArgs)
{
Logger.LogInformation($"Player {eventArgs.PlayerName} joined the server!");
// Send welcome message
// eventArgs.Player.SendMessage("Welcome to our server!");
}
}
}
Build your plugin:
dotnet build --configuration Release
Copy the built DLL to your server's plugins directory:
cp bin/Release/net8.0/MyFirstPlugin.dll ../LabFramework/plugins/
- Start your server
- Check the logs for "My First Plugin loaded successfully!"
- Test the commands:
hello John
- Should respond with "Hello, John! Welcome to the server!"time
- Should show the current server time
Now that you have a working plugin, you can:
- Add more commands with different parameter types
- Implement event handlers for game events
- Create custom items and roles
- Set up a permission system
- Add configuration options
Check out the full Developer Guide for detailed information on all framework features!
Plugin not loading: Check that all DLL files are in the correct directory and that there are no compilation errors.
Commands not working: Ensure the command service is properly registered and that you have the necessary permissions.
Events not firing: Make sure you've subscribed to events in the OnLoadAsync
method and that the event types are correct.
For more help, check the troubleshooting section in the Developer Guide or visit our community forums.