### Manage Per-Player Data with CustomDataStore Source: https://context7.com/northwood-studios/labapi/llms.txt Attach typed, automatically managed data to player instances using CustomDataStore. This example defines and uses a PlayerStats store. ```csharp using LabApi.Features.Stores; using LabApi.Features.Wrappers; // Define the store public class PlayerStats : CustomDataStore { public int Kills { get; set; } public int Deaths { get; set; } public float DamageDealt { get; set; } public string LastRole { get; set; } = "None"; public PlayerStats(Player owner) : base(owner) { } protected override void OnInstanceCreated() => Logger.Info($"Stats store created for {Owner.DisplayName}"); protected override void OnInstanceDestroyed() => Logger.Info($"Stats store destroyed for {Owner.DisplayName}"); } // Access or create the store for any player PlayerStats stats = PlayerStats.Get(player); stats.Kills++; stats.DamageDealt += 35.5f; // From an event handler PlayerEvents.Death += ev => { PlayerStats s = PlayerStats.Get(ev.Player); s.Deaths++; ev.Player.SendHint($"You have died {s.Deaths} time(s).", 5); }; PlayerEvents.Hurting += ev => { if (ev.Attacker != null) PlayerStats.Get(ev.Attacker).DamageDealt += ev.DamageAmount; }; ``` -------------------------------- ### Custom Command Implementation Source: https://context7.com/northwood-studios/labapi/llms.txt Demonstrates how to implement custom commands using `ICommand` and the `[CommandHandler]` attribute for registration in Remote Admin, server console, or client command lists. Includes an example `HealCommand`. ```APIDOC ## Custom Commands — `ICommand` with `[CommandHandler]` Implement `CommandSystem.ICommand` and decorate with `[CommandHandler]` to register commands in Remote Admin, the server console, or the client `.` command list. LabAPI automatically discovers and registers them from the plugin assembly. ```csharp using System; using CommandSystem; using LabApi.Features.Wrappers; // Remote Admin + Client dot-command [CommandHandler(typeof(RemoteAdminCommandHandler))] [CommandHandler(typeof(ClientCommandHandler))] public class HealCommand : ICommand { public string Command => "heal"; public string[] Aliases => ["hp"]; public string Description => "Heals the target player to full health."; public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) { // Permission check if (!sender.CheckPermission(PlayerPermissions.PlayersManagement)) { response = "Insufficient permissions (PlayersManagement required)."; return false; } if (arguments.Count == 0) { response = "Usage: heal "; return false; } if (!int.TryParse(arguments.At(0), out int id) || Player.Get(id) is not Player target) { response = $"Player '{arguments.At(0)}' not found."; return false; } target.Health = target.MaxHealth; response = $"Healed {target.DisplayName} to {target.MaxHealth} HP."; return true; } } ``` ``` -------------------------------- ### Implement Custom Permissions Provider Source: https://context7.com/northwood-studios/labapi/llms.txt Extend LabAPI's permission system by implementing IPermissionsProvider and registering it. This example shows a database-backed provider. ```csharp using LabApi.Features.Permissions; using LabApi.Features.Wrappers; // --- Implement a custom provider --- public class DatabasePermissionsProvider : IPermissionsProvider { public string[] GetPermissions(Player player) => FetchFromDatabase(player.UserId); // your logic public bool HasPermissions(Player player, params string[] permissions) => permissions.All(p => GetPermissions(player).Contains(p)); public bool HasAnyPermission(Player player, params string[] permissions) => permissions.Any(p => GetPermissions(player).Contains(p)); public void AddPermissions(Player player, params string[] permissions) { /* ... */ } public void RemovePermissions(Player player, params string[] permissions) { /* ... */ } public void ReloadPermissions() { /* re-fetch from DB */ } private string[] FetchFromDatabase(string userId) => []; } // Register in Plugin.Enable(): PermissionsManager.RegisterProvider(); // Usage anywhere: if (player.HasPermissions("admin.ban", "admin.kick")) Logger.Info($"{player.DisplayName} is a senior admin."); string[] allPerms = player.GetPermissions(); ``` -------------------------------- ### Subscribe to an Event Handler Source: https://github.com/northwood-studios/labapi/wiki/Using-C Event handlers must accept an event argument parameter. This example shows the basic signature for an event handler. ```csharp void OnWaveRespawned(WaveRespawnedEventArgs args) { // Your code } ``` -------------------------------- ### Get All Player Permissions Source: https://github.com/northwood-studios/labapi/wiki/Permissions Retrieve an array containing all permissions assigned to a player using the GetPermissions method. ```csharp // Retrieves an array with all the permissions of the given player. string[] allTheirPerms = player.GetPermissions(); ``` -------------------------------- ### Create Nested Commands with ParentCommand Source: https://context7.com/northwood-studios/labapi/llms.txt Use ParentCommand to group related sub-commands under a single root command. This example shows how to create a 'myadmin' command with 'heal' and 'killall' sub-commands. ```csharp using System; using CommandSystem; [CommandHandler(typeof(RemoteAdminCommandHandler))] public class AdminParentCommand : ParentCommand { public AdminParentCommand() => LoadGeneratedCommands(); public override string Command => "myadmin"; public override string[] Aliases => ["ma"]; public override string Description => "Admin utility commands."; public override void LoadGeneratedCommands() { RegisterCommand(new HealCommand()); RegisterCommand(new KillAllCommand()); } protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) { response = "Use: myadmin [args]"; return false; } } // Usage in RA console: "myadmin heal 3" or "myadmin killall" ``` -------------------------------- ### SCP-Specific Event Handlers Source: https://context7.com/northwood-studios/labapi/llms.txt Examples of subscribing to and handling events related to SCP-049, SCP-096, and SCP-914. These events allow for custom logic to be executed when specific SCP actions occur. ```APIDOC ## SCP-Specific Events — `Scp049Events`, `Scp096Events`, `Scp914Events`, etc. Each SCP has its own static event handler class. Below is a representative sample covering SCP-049, SCP-096, and SCP-914. ```csharp using LabApi.Events.Arguments.Scp049Events; using LabApi.Events.Arguments.Scp096Events; using LabApi.Events.Arguments.Scp914Events; using LabApi.Events.Handlers; // SCP-049 resurrects a player Scp049Events.ResurrectingBody += ev => { Logger.Info($"SCP-049 is resurrecting {ev.Target.DisplayName}."); // ev.IsAllowed = false; // prevent resurrection }; // SCP-096 enters rage Scp096Events.Enraging += ev => { Logger.Info($"SCP-096 enraging! Current targets: {ev.Scp096.TargetsCount}"); }; // SCP-914 processes an item Scp914Events.ProcessingPickup += ev => { Logger.Info($"914 processing {ev.Pickup.Type} on setting {ev.KnobSetting}"); // Modify output: ev.OutputPosition = someOtherPosition; }; // SCP-914 processes a player Scp914Events.ProcessingPlayer += ev => { Logger.Info($"914 upgrading {ev.Player.DisplayName} on setting {ev.KnobSetting}"); // ev.IsAllowed = false; to prevent processing }; // Warhead events LabApi.Events.Handlers.WarheadEvents.Starting += ev => { Logger.Info($"Warhead starting! Activated by {ev.Player?.DisplayName ?? "auto"}"); // ev.IsAllowed = false; to cancel detonation }; ``` ``` -------------------------------- ### Define IPermissionsProvider Interface Source: https://github.com/northwood-studios/labapi/wiki/Custom-Provider Implement this interface to create a custom provider for managing user permissions externally. It defines methods for getting, checking, adding, and removing permissions. ```csharp /// /// Represents a provider of user permissions. /// public interface IPermissionsProvider { /// /// Retrieves all the permissions of the given . /// /// The player to retrieve the permissions for. /// An array of all the permissions of the given . public string[] GetPermissions(Player player); /// /// Whether the given has all the given . /// /// The player to check the permissions for. /// The permissions to check. /// True if the has all the ; otherwise, false. public bool HasPermissions(Player player, params string[] permissions); /// /// Whether the given has any of the given . /// /// The player to check the permissions for. /// The permissions to check. /// True if the has any of the ; otherwise, false. public bool HasAnyPermission(Player player, params string[] permissions); /// /// Adds all the given to the given . /// /// The player to add the permissions to. /// The permissions to add. public void AddPermissions(Player player, params string[] permissions); /// /// Removes all the given from the given . /// /// The player to remove the permissions from. /// The permissions to remove. public void RemovePermissions(Player player, params string[] permissions); } ``` -------------------------------- ### Round Wrapper Source: https://context7.com/northwood-studios/labapi/llms.txt Allows querying and controlling the current round's state, including checking if the round has started or is in progress, locking the round, and forcing its end or restart. ```APIDOC ## Round Wrapper — Round Lifecycle Static class for querying and controlling the current round state. ```csharp using LabApi.Features.Wrappers; if (!Round.IsRoundStarted) Logger.Info("Lobby phase."); if (Round.IsRoundInProgress) { Round.IsLocked = true; // prevent round from ending Logger.Info($"Round time: {Round.Duration.TotalSeconds}s elapsed."); } // Force end the round Round.EndRound(RoundSummary.LeadingTeam.FacilityForces); // Restart Round.Restart(); // Lock / unlock lobby Round.IsLobbyLocked = true; ``` ``` -------------------------------- ### Modify Event Arguments Source: https://github.com/northwood-studios/labapi/wiki/Using-C Properties of event arguments that have setters can be freely edited. This example demonstrates changing the `NewRole` property. ```csharp void OnPlayerChangingRole(PlayerChangingRoleEventArgs args) { // PlayerChangingRoleEventArgs.NewRole has a setter. args.NewRole = RoleTypeId.Tutorial; // In this case example, if I set it to Tutorial // it will always set the player role to Tutorial when it changes! } ``` -------------------------------- ### Safely Load Configuration with TryLoadConfig Source: https://github.com/northwood-studios/labapi/wiki/Advanced-Configurations Use `TryLoadConfig` to attempt loading a configuration and check for errors without crashing. This is useful for validating user-provided settings and deciding whether to enable the plugin. ```csharp private bool _hasIncorrectSettings = false; public override void LoadConfigs() { base.LoadConfigs(); // We could, for example, avoid to enable the plugin at all. _hasIncorrectSettings = !this.TryLoadConfig("lvls.yml", out LevelsConfig); } public override void Enable() { // Enable is called after the settings are loaded. // We can directly check if the user has incorrect settings. if (_hasIncorrectSettings) { Logger.Error("Detected incorrect settings, not loading"); return; } ``` -------------------------------- ### Plugin Entry Point Source: https://github.com/northwood-studios/labapi/wiki/Writing-Your-First-Plugin Override the Plugin::Enable() method to define the entry point for your plugin. This method is executed when the plugin is loaded. ```csharp { public override void Enable() { // This will run when the plugin loads! } } ``` -------------------------------- ### Load Configuration from File Source: https://github.com/northwood-studios/labapi/wiki/Advanced-Configurations Use `LoadConfig` to read a configuration from a file. If the file does not exist, it will be created with default values. This method returns null if the configuration file is invalid. ```csharp public class MyFirstPlugin : Plugin { // LevelingConfig is a serializable class. public LevelingConfig LevelsConfig; public override void LoadConfigs() { base.LoadConfigs(); // This will read the LevelingConfig configuration from the lvls.yml file // In case the file doesn't exist, it will be created with its default values LevelsConfig = this.LoadConfig("lvls.yml"); } } ``` -------------------------------- ### Define a Serializable Configuration Class Source: https://github.com/northwood-studios/labapi/wiki/Adding-Configurations-(P2) Use serializable classes for configurations. Ensure settings are defined as properties, not fields, as only properties are serialized. Default values can be assigned directly. ```csharp public class MyPluginConfigurationClass { public int MyConfigurableInt { get; set; } = 10; // 10 will be the default value } ``` -------------------------------- ### Server Wrapper Source: https://context7.com/northwood-studios/labapi/llms.txt Provides access to server-level properties like port, IP address, player counts, and TPS. Also includes methods for administrative tasks such as setting max players, broadcast messages, restarting, shutting down, and managing player bans. ```APIDOC ## Server Wrapper — Server State and Control Static class exposing server-level properties and administration methods. ```csharp using LabApi.Features.Wrappers; // Get server info Logger.Info($"Port: {Server.Port}, IP: {Server.IpAddress}"); Logger.Info($"Players: {Server.PlayerCount}/{Server.MaxPlayers}"); Logger.Info($"TPS: {Server.Tps}/{Server.MaxTps}"); // Set server properties Server.MaxPlayers = 30; Server.SpawnProtectDuration = 5f; // Broadcast to all players Server.SendBroadcast("Server restart in 5 minutes!", 10); // Restart / stop Server.Restart(); // Server.Shutdown(); // Ban management Server.BanPlayer(targetPlayer, issuingPlayer, "Griefing", duration: 86400); // 24h // Whitelist / reserved slots Whitelist.Add("76561198000000000@steam"); ReservedSlots.Add("76561198000000001@steam"); ``` ``` -------------------------------- ### Enable Plugin with Simple Configuration Source: https://github.com/northwood-studios/labapi/wiki/Adding-Configurations-(P1) Inherit from Plugin and access configuration via 'this.Config'. The configuration object is automatically loaded and defaults are used if the file is broken. Call 'SaveConfig()' to persist changes. ```csharp public class MyFirstPlugin : Plugin { public override void Enable() { // this.Config will have my loaded configuration! Logger.Info("MyConfigurableInt is " + Config.MyConfigurableInt); // We can also easily save our configuration! Config.MyConfigurableInt = 20; SaveConfig(); } } ``` -------------------------------- ### Check Player Permissions Source: https://github.com/northwood-studios/labapi/wiki/Permissions Use the HasPermissions extension to check if a player has a specific permission or multiple permissions. Use HasAnyPermission to check if a player has at least one of the specified permissions. ```csharp // We can check for one specific permission. bool hasPerm = player.HasPermissions("myplugin.permission"); // Or for multiple at the same time bool hasPerms = player.HasPermissions("perm1", "perm2"); // In the case of multiple permissions we can check if the player has any of them bool hasAnyPerm = player.HasAnyPermission("perm1", "perm2"); ``` -------------------------------- ### Create Parent Command Source: https://github.com/northwood-studios/labapi/wiki/Commands Create a parent command by inheriting from ParentCommand. Implement the Command, Aliases, Description properties and the ExecuteParent method. This command acts as a container for child commands. ```csharp [CommandHandler(typeof(RemoteAdminCommandHandler))] public class TestParentCommand : ParentCommand { public override string Command { get; } = "parent"; public override string[] Aliases { get; } = Array.Empty(); public override string Description { get; } = "Test parent command."; protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) { // In the case that child commands weren't found this will run. // It is a great moment to display the available subcommands. response = "Subcommands: test, test2, test3."; return false; // Indicates failure (e.g. red text in RA). } } ``` -------------------------------- ### Subscribe to Server-Wide Events in C# Source: https://context7.com/northwood-studios/labapi/llms.txt Handle server lifecycle events such as waiting for players, round start/end, wave respawning, CASSIE announcements, map generation, and ban issuing. Event arguments allow for modification or cancellation. ```csharp using LabApi.Events.Arguments.ServerEvents; using LabApi.Events.Handlers; ServerEvents.WaitingForPlayers += () => Logger.Info("Waiting for players..."); ServerEvents.RoundStarted += () => Logger.Info("Round started!"); ServerEvents.RoundEnded += ev => Logger.Info($"Round ended. Winner: {ev.LeadingTeam}"); ServerEvents.RoundRestarted += () => Logger.Info("Round restarted."); ServerEvents.WaveRespawning += ev => { Logger.Info($"Respawn wave incoming. Team: {ev.Wave}"); // ev.IsAllowed = false; to cancel the wave }; ServerEvents.CassieAnnouncing += ev => { Logger.Info($"CASSIE: '{ev.Words}'"); // Modify: ev.Words = "custom announcement"; }; ServerEvents.MapGenerated += ev => { int roomCount = LabApi.Features.Wrappers.Map.Rooms.Count; Logger.Info($"Map generated with {roomCount} rooms. Seed: {LabApi.Features.Wrappers.Map.Seed}"); }; ServerEvents.BanIssuing += ev => { Logger.Info($"Ban issued to {ev.Target} by {ev.Issuer}. Duration: {ev.Duration}s."); // ev.IsAllowed = false; to block the ban }; ``` -------------------------------- ### Round Lifecycle Control Source: https://context7.com/northwood-studios/labapi/llms.txt Query the current round state, such as whether it has started or is in progress. Control round behavior by locking the round to prevent ending or unlocking the lobby. Force the round to end with a specified summary or restart the round. ```csharp using LabApi.Features.Wrappers; if (!Round.IsRoundStarted) Logger.Info("Lobby phase."); if (Round.IsRoundInProgress) { Round.IsLocked = true; // prevent round from ending Logger.Info($"Round time: {Round.Duration.TotalSeconds}s elapsed."); } // Force end the round Round.EndRound(RoundSummary.LeadingTeam.FacilityForces); // Restart Round.Restart(); // Lock / unlock lobby Round.IsLobbyLocked = true; ``` -------------------------------- ### Create Individual Command Source: https://github.com/northwood-studios/labapi/wiki/Commands Define an individual command by inheriting from ICommand and specifying the command handler type. The Command, Aliases, Description properties and Execute method must be implemented. ```csharp [CommandHandler(typeof(ClientCommandHandler))] [CommandHandler(typeof(RemoteAdminCommandHandler))] [CommandHandler(typeof(GameConsoleCommandHandler))] public class TestCommand : ICommand { public string Command { get; } = "Test"; // The command used in the console. public string[] Aliases { get; } = Array.Empty(); // The desired aliases. public string Description { get; } = "Test command"; // A small description. public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) { response = "This was a test."; // You need to return a response return true; // Return true if the command was executed or false if not (missing permissions...). } } ``` -------------------------------- ### Item and Pickup Wrappers Source: https://context7.com/northwood-studios/labapi/llms.txt Provides typed facades for in-hand items and world-spawned pickups, enabling easier manipulation and interaction with these game objects. ```APIDOC ## `Item` and `Pickup` Wrappers Typed facades for in-hand items (`Item` hierarchy) and world-spawned pickups (`Pickup` hierarchy). ```csharp using LabApi.Features.Wrappers; using InventorySystem.Items; using PlayerRoles; // Access player's current item if (player.CurrentItem is FirearmItem firearm) { Logger.Info($"Firearm: {firearm.ItemTypeId}, Ammo: {firearm.Ammo}"); firearm.Ammo = firearm.MaxAmmo; // top up } // Iterate player inventory foreach (Item item in player.Items) Logger.Info($" Item: {item.ItemTypeId} (serial {item.Serial})"); // Give items player.AddItem(ItemType.KeycardO5); player.AddItem(ItemType.GunLogicer); // Drop an item into the world Pickup? dropped = player.DropItem(player.CurrentItem); if (dropped != null) Logger.Info($"Dropped {dropped.Type} at {dropped.Position}"); // Spawn a pickup at a world position Pickup spawned = Pickup.Create(ItemType.MicroHID, new UnityEngine.Vector3(0, 10, 0)); spawned.Spawn(); // Interact with a specific pickup type if (spawned is FirearmPickup fp) fp.Ammo = 50; // Remove a pickup from the world spawned.Destroy(); ``` ``` -------------------------------- ### Define Plugin Metadata Source: https://github.com/northwood-studios/labapi/wiki/Writing-Your-First-Plugin Implement the Plugin abstract class to define essential metadata for your plugin, such as its name, description, author, and version. ```csharp public class MyFirstPlugin : Plugin { // The name of the plugin public override string Name { get; } = "My First Plugin"; // The description of the plugin public override string Description { get; } = "This plugin does magic!"; // The author of the plugin public override string Author { get; } = "Me!" // The current version of the plugin public override Version Version { get; } = new Version(1, 0, 0, 0); // The required version of LabAPI (usually the version the plugin was built with) public override Version RequiredApiVersion { get; } = new (LabApiProperties.CompiledVersion); } ``` -------------------------------- ### Basic Plugin with Typed Configuration Source: https://context7.com/northwood-studios/labapi/llms.txt Implement a plugin by subclassing Plugin and overriding abstract members. The loader automatically handles configuration deserialization from YAML and plugin lifecycle events (Enable/Disable). ```csharp using System; using LabApi.Events.Arguments.PlayerEvents; using LabApi.Events.Handlers; using LabApi.Features; using LabApi.Loader.Features.Plugins; // --- Config class (YAML-serialised automatically) --- public class MyPluginConfig { public string WelcomeMessage { get; set; } = "Welcome to the server!"; public int BroadcastDuration { get; set; } = 10; public bool EnableWelcome { get; set; } = true; } // --- Plugin entry point --- public class MyPlugin : Plugin { public override string Name => "MyPlugin"; public override string Description => "Greets players when they join."; public override string Author => "YourName"; public override Version Version => new(1, 0, 0); // Must match the LabAPI major version the plugin was compiled against public override Version RequiredApiVersion => new Version(LabApiProperties.CompiledVersion); public override void Enable() { PlayerEvents.Joined += OnPlayerJoined; } public override void Disable() { PlayerEvents.Joined -= OnPlayerJoined; } private void OnPlayerJoined(PlayerJoinedEventArgs ev) { if (!Config.EnableWelcome) return; ev.Player.SendBroadcast(Config.WelcomeMessage, (ushort)Config.BroadcastDuration); } } // Loader auto-discovers MyPlugin.dll, reads Plugins/global/MyPlugin/config.yml, // deserialises it into MyPluginConfig and calls Enable(). ``` -------------------------------- ### Custom Command Implementation Source: https://context7.com/northwood-studios/labapi/llms.txt Implement the `ICommand` interface and use the `[CommandHandler]` attribute to register commands for Remote Admin, the server console, or client commands. The `Execute` method handles command logic, argument parsing, permission checks, and response generation. ```csharp using System; using CommandSystem; using LabApi.Features.Wrappers; // Remote Admin + Client dot-command [CommandHandler(typeof(RemoteAdminCommandHandler))] [CommandHandler(typeof(ClientCommandHandler))] public class HealCommand : ICommand { public string Command => "heal"; public string[] Aliases => ["hp"]; public string Description => "Heals the target player to full health."; public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) { // Permission check if (!sender.CheckPermission(PlayerPermissions.PlayersManagement)) { response = "Insufficient permissions (PlayersManagement required)."; return false; } if (arguments.Count == 0) { response = "Usage: heal "; return false; } if (!int.TryParse(arguments.At(0), out int id) || Player.Get(id) is not Player target) { response = $"Player '{arguments.At(0)}' not found."; return false; } target.Health = target.MaxHealth; response = $"Healed {target.DisplayName} to {target.MaxHealth} HP."; return true; } } ``` -------------------------------- ### Admin Parent Command Source: https://context7.com/northwood-studios/labapi/llms.txt Demonstrates how to group sub-commands under a parent command using `ParentCommand`. This allows for organized command structures, like the 'myadmin' command with 'heal' and 'killall' sub-commands. ```APIDOC ## Parent Commands — Nested Command Trees Group multiple sub-commands under one root command using `ParentCommand`. ```csharp using System; using CommandSystem; [CommandHandler(typeof(RemoteAdminCommandHandler))] public class AdminParentCommand : ParentCommand { public AdminParentCommand() => LoadGeneratedCommands(); public override string Command => "myadmin"; public override string[] Aliases => ["ma"]; public override string Description => "Admin utility commands."; public override void LoadGeneratedCommands() { RegisterCommand(new HealCommand()); RegisterCommand(new KillAllCommand()); } protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) { response = "Use: myadmin [args]"; return false; } } // Usage in RA console: "myadmin heal 3" or "myadmin killall" ``` ``` -------------------------------- ### Map Wrapper Source: https://context7.com/northwood-studios/labapi/llms.txt Provides read-only access to all spawned facility objects, including rooms, doors, generators, pickups, and teslas. Allows manipulation of doors (open/lock) and checking map properties like the seed. ```APIDOC ## Map Wrapper — Facility Objects Static class providing read-only access to every spawned facility object. ```csharp using LabApi.Features.Wrappers; // Rooms foreach (Room room in Map.Rooms) Logger.Info($"Room: {room.Name} Zone: {room.Zone}"); // Doors foreach (Door door in Map.Doors) { door.IsOpen = true; // open all doors door.IsLocked = false; } // Generators int activated = Map.Generators.Count(g => g.IsActivated); Logger.Info($"{activated}/{Map.Generators.Count} generators activated."); // Pickups foreach (Pickup pickup in Map.Pickups) Logger.Info($"Pickup: {pickup.Type} at {pickup.Position}"); // Teslas — enable/disable by zone foreach (Tesla tesla in Map.Teslas) tesla.IsDisabled = true; // Seed Logger.Info($"Map seed: {Map.Seed}"); ``` ``` -------------------------------- ### Manage Player Items and World Pickups Source: https://context7.com/northwood-studios/labapi/llms.txt Access and modify player inventories, drop items, spawn pickups in the world, and interact with specific pickup types. Ensure proper null checks for dropped items. ```csharp using LabApi.Features.Wrappers; using InventorySystem.Items; using PlayerRoles; // Access player's current item if (player.CurrentItem is FirearmItem firearm) { Logger.Info($"Firearm: {firearm.ItemTypeId}, Ammo: {firearm.Ammo}"); firearm.Ammo = firearm.MaxAmmo; // top up } // Iterate player inventory foreach (Item item in player.Items) Logger.Info($" Item: {item.ItemTypeId} (serial {item.Serial})"); // Give items player.AddItem(ItemType.KeycardO5); player.AddItem(ItemType.GunLogicer); // Drop an item into the world Pickup? dropped = player.DropItem(player.CurrentItem); if (dropped != null) Logger.Info($"Dropped {dropped.Type} at {dropped.Position}"); // Spawn a pickup at a world position Pickup spawned = Pickup.Create(ItemType.MicroHID, new UnityEngine.Vector3(0, 10, 0)); spawned.Spawn(); // Interact with a specific pickup type if (spawned is FirearmPickup fp) fp.Ammo = 50; // Remove a pickup from the world spawned.Destroy(); ``` -------------------------------- ### Save Configuration to File Source: https://github.com/northwood-studios/labapi/wiki/Advanced-Configurations Use `SaveConfig` to write the current configuration to a file, overwriting its previous content. This is useful for persisting changes made to plugin settings. ```csharp public override void Enable() { // We can edit the configuration LevelingConfiguration.NeededExp = 50; // And we call this.SaveConfig to save our changes to the configuration file! this.SaveConfig("lvls.yml", LevelingConfiguration); } ``` -------------------------------- ### Plugin Loader API Source: https://context7.com/northwood-studios/labapi/llms.txt Provides an overview of the `PluginLoader` API, which manages the discovery, instantiation, and lifecycle of plugins. It exposes properties for inspecting the loaded state and methods for loading additional plugins and dependencies at runtime. ```APIDOC ## `PluginLoader` — Loader API The static loader that discovers, instantiates, and manages plugin lifecycle. Primarily internal, but exposes useful properties and methods for advanced use. ```csharp using LabApi.Loader; using LabApi.Loader.Features.Plugins; // Inspect loaded state Logger.Info($"LabAPI initialized: {PluginLoader.Initialized}"); Logger.Info($"Loaded plugins: {PluginLoader.Plugins.Count}"); Logger.Info($"Enabled plugins: {PluginLoader.EnabledPlugins.Count}"); // Iterate plugins foreach (Plugin plugin in PluginLoader.EnabledPlugins) Logger.Info($" - {plugin.Name} v{plugin.Version} by {plugin.Author}"); // Load extra plugins at runtime from a directory var extraDir = new System.IO.DirectoryInfo("/path/to/extra-plugins"); PluginLoader.LoadPlugins(extraDir.GetFiles("*.dll")); // Load extra dependencies var depDir = new System.IO.DirectoryInfo("/path/to/deps"); PluginLoader.LoadDependencies(depDir.GetFiles("*.dll")); // Global LabAPI config (LabApi-.yml) Logger.Info($"Plugin paths: {string.Join(", ", PluginLoader.Config.PluginPaths)}"); Logger.Info($"Load unsupported plugins: {PluginLoader.Config.LoadUnsupportedPlugins}"); ``` ``` -------------------------------- ### Custom Permission Providers Source: https://context7.com/northwood-studios/labapi/llms.txt Shows how to implement custom permission providers by inheriting from `IPermissionsProvider` and registering them with `PermissionsManager`. This allows for flexible permission systems, such as database-backed permissions. ```APIDOC ## `PermissionsManager` — Custom Permission Providers LabAPI ships a default file-based permissions provider. Plugins can register additional providers by implementing `IPermissionsProvider` and calling `PermissionsManager.RegisterProvider()`. ```csharp using LabApi.Features.Permissions; using LabApi.Features.Wrappers; // --- Implement a custom provider --- public class DatabasePermissionsProvider : IPermissionsProvider { public string[] GetPermissions(Player player) => FetchFromDatabase(player.UserId); // your logic public bool HasPermissions(Player player, params string[] permissions) => permissions.All(p => GetPermissions(player).Contains(p)); public bool HasAnyPermission(Player player, params string[] permissions) => permissions.Any(p => GetPermissions(player).Contains(p)); public void AddPermissions(Player player, params string[] permissions) { /* ... */ } public void RemovePermissions(Player player, params string[] permissions) { /* ... */ } public void ReloadPermissions() { /* re-fetch from DB */ } private string[] FetchFromDatabase(string userId) => []; } // Register in Plugin.Enable(): PermissionsManager.RegisterProvider(); // Usage anywhere: if (player.HasPermissions("admin.ban", "admin.kick")) Logger.Info($"{player.DisplayName} is a senior admin."); string[] allPerms = player.GetPermissions(); ``` ``` -------------------------------- ### Per-Player Data Storage Source: https://context7.com/northwood-studios/labapi/llms.txt Illustrates how to use `CustomDataStore` for managing player-specific data that is automatically handled during player connections and disconnections. This is useful for storing game stats, player progress, or other session-bound information. ```APIDOC ## `CustomDataStore` — Per-Player Data Storage Attach typed data to a `Player` instance that is automatically cleaned up when the player disconnects. ```csharp using LabApi.Features.Stores; using LabApi.Features.Wrappers; // Define the store public class PlayerStats : CustomDataStore { public int Kills { get; set; } public int Deaths { get; set; } public float DamageDealt { get; set; } public string LastRole { get; set; } = "None"; public PlayerStats(Player owner) : base(owner) { } protected override void OnInstanceCreated() => Logger.Info($"Stats store created for {Owner.DisplayName}"); protected override void OnInstanceDestroyed() => Logger.Info($"Stats store destroyed for {Owner.DisplayName}"); } // Access or create the store for any player PlayerStats stats = PlayerStats.Get(player); stats.Kills++; stats.DamageDealt += 35.5f; // From an event handler PlayerEvents.Death += ev => { PlayerStats s = PlayerStats.Get(ev.Player); s.Deaths++; ev.Player.SendHint($"You have died {s.Deaths} time(s).", 5); }; PlayerEvents.Hurting += ev => { if (ev.Attacker != null) PlayerStats.Get(ev.Attacker).DamageDealt += ev.DamageAmount; }; ``` ``` -------------------------------- ### Subscribe to Server Events in C# Source: https://github.com/northwood-studios/labapi/wiki/Using-C Subscribe to server events when your plugin is enabled to react to game events. Ensure the event handler method exists. ```csharp public override void Enable() { ServerEvents.WaveRespawned += OnWaveRespawned } ``` -------------------------------- ### Accessing Facility Objects Source: https://context7.com/northwood-studios/labapi/llms.txt Iterate through all spawned facility objects including rooms, doors, generators, and pickups. Modify properties of doors (open/lock state) and generators (activation state). Retrieve read-only information like the map seed. ```csharp using LabApi.Features.Wrappers; // Rooms foreach (Room room in Map.Rooms) Logger.Info($"Room: {room.Name} Zone: {room.Zone}"); // Doors foreach (Door door in Map.Doors) { door.IsOpen = true; // open all doors door.IsLocked = false; } // Generators int activated = Map.Generators.Count(g => g.IsActivated); Logger.Info($"{activated}/{Map.Generators.Count} generators activated."); // Pickups foreach (Pickup pickup in Map.Pickups) Logger.Info($"Pickup: {pickup.Type} at {pickup.Position}"); // Teslas — enable/disable by zone foreach (Tesla tesla in Map.Teslas) tesla.IsDisabled = true; // Seed Logger.Info($"Map seed: {Map.Seed}"); ``` -------------------------------- ### Interact with PluginLoader API Source: https://context7.com/northwood-studios/labapi/llms.txt Use the static PluginLoader to inspect the loaded state of plugins, iterate through enabled plugins, and load additional plugins or dependencies at runtime. ```csharp using LabApi.Loader; using LabApi.Loader.Features.Plugins; // Inspect loaded state Logger.Info($"LabAPI initialized: {PluginLoader.Initialized}"); Logger.Info($"Loaded plugins: {PluginLoader.Plugins.Count}"); Logger.Info($"Enabled plugins: {PluginLoader.EnabledPlugins.Count}"); // Iterate plugins foreach (Plugin plugin in PluginLoader.EnabledPlugins) Logger.Info($" - {plugin.Name} v{plugin.Version} by {plugin.Author}"); // Load extra plugins at runtime from a directory var extraDir = new System.IO.DirectoryInfo("/path/to/extra-plugins"); PluginLoader.LoadPlugins(extraDir.GetFiles("*.dll")); // Load extra dependencies var depDir = new System.IO.DirectoryInfo("/path/to/deps"); PluginLoader.LoadDependencies(depDir.GetFiles("*.dll")); // Global LabAPI config (LabApi-.yml) Logger.Info($"Plugin paths: {string.Join(", ", PluginLoader.Config.PluginPaths)}"); Logger.Info($"Load unsupported plugins: {PluginLoader.Config.LoadUnsupportedPlugins}"); ``` -------------------------------- ### Create and Run a Coroutine with MEC Source: https://github.com/northwood-studios/labapi/wiki/M.E.C. Define coroutines using IEnumerator and run them with Timing.RunCoroutine. Coroutines can pause execution for specific durations or frames using Timing.WaitForSeconds and Timing.WaitForOneFrame. ```csharp using MEC; public IEnumerator MyCoroutine(Player player) // You can pass arguments { player.Role = RandomRoles.GetOne(); yield return Timing.WaitForSeconds(5); // Stops execution for 5 seconds player.ShowBroadcast("See you in the next frame!"); yield return Timing.WaitForOneFrame; // Stops execution for 1 frame player.ShowBroadcast("There you are!") } ``` ```csharp public void OnPlayerChangingRole(PlayerChangingRoleEventArgs args) { Timing.RunCoroutine(MyCoroutine(args.Player)); } ``` -------------------------------- ### Door and Gate Wrappers Source: https://context7.com/northwood-studios/labapi/llms.txt Wrappers for facility doors and gates, allowing control over their lock state, open/close status, damage, and keycard permissions. ```APIDOC ## `Door` / `Gate` Wrappers — Facility Doors Wrappers for all door types in the facility. Supports lock state, open/close, damage, and keycard permissions. ```csharp using LabApi.Features.Wrappers; using Interactables.Interobjects.DoorUtils; // Open all heavy containment doors foreach (Door door in Map.Doors) { if (door.Zone == MapGeneration.FacilityZone.HeavyContainment) { door.IsOpen = true; door.IsLocked = false; } } // Get a specific door by name if (Door.TryGet("HCZ_EZ_CHECKPOINT_A", out Door? checkpoint)) { checkpoint.IsLocked = true; checkpoint.IsOpen = false; } // Breakable door destruction foreach (Door door in Map.Doors) { if (door is BreakableDoor breakable && !breakable.IsDestroyed) { breakable.Health -= 100f; // deal damage to door } } // Gate (large doors like SCP-914 entrance) foreach (Door door in Map.Doors) { if (door is Gate gate) { gate.IsOpen = true; Logger.Info($"Gate {gate.Name} opened."); } } // Door events (in ServerEvents) LabApi.Events.Handlers.ServerEvents.DoorLockChanged += ev => Logger.Info($"Door {ev.Door.Name} lock changed to: {ev.Door.IsLocked}"); ``` ``` -------------------------------- ### Server State and Control Source: https://context7.com/northwood-studios/labapi/llms.txt Access server properties like port, IP, player counts, and TPS. Modify server settings such as max players and spawn protection. Use methods for broadcasting messages, restarting, shutting down, and managing player bans and whitelists. ```csharp using LabApi.Features.Wrappers; Logger.Info($"Port: {Server.Port}, IP: {Server.IpAddress}"); Logger.Info($"Players: {Server.PlayerCount}/{Server.MaxPlayers}"); Logger.Info($"TPS: {Server.Tps}/{Server.MaxTps}"); Server.MaxPlayers = 30; Server.SpawnProtectDuration = 5f; // Broadcast to all players Server.SendBroadcast("Server restart in 5 minutes!", 10); // Restart / stop Server.Restart(); // Server.Shutdown(); // Ban management Server.BanPlayer(targetPlayer, issuingPlayer, "Griefing", duration: 86400); // 24h // Whitelist / reserved slots Whitelist.Add("76561198000000000@steam"); ReservedSlots.Add("76561198000000001@steam"); ``` -------------------------------- ### Access Permissions by Provider Source: https://github.com/northwood-studios/labapi/wiki/Permissions Check permissions for a player within a specific provider using GetProvider().HasPermissions. Retrieve all permissions categorized by provider using GetPermissionsByProvider. ```csharp // Gets the permissions of a player in a certain provider. PermissionsManager.GetProvider().HasPermissions(player, "myplugin.perm"); // Gets all the permissions divided by provider. Dictionary playerPermissions = player.GetPermissionsByProvider(); ``` -------------------------------- ### Handle SCP-Specific Events Source: https://context7.com/northwood-studios/labapi/llms.txt Subscribe to static event handler classes for various SCPs to react to in-game events. Use the event arguments to modify or cancel actions. ```csharp using LabApi.Events.Arguments.Scp049Events; using LabApi.Events.Arguments.Scp096Events; using LabApi.Events.Arguments.Scp914Events; using LabApi.Events.Handlers; // SCP-049 resurrects a player Scp049Events.ResurrectingBody += ev => { Logger.Info($"SCP-049 is resurrecting {ev.Target.DisplayName}."); // ev.IsAllowed = false; // prevent resurrection }; // SCP-096 enters rage Scp096Events.Enraging += ev => { Logger.Info($"SCP-096 enraging! Current targets: {ev.Scp096.TargetsCount}"); }; // SCP-914 processes an item Scp914Events.ProcessingPickup += ev => { Logger.Info($"914 processing {ev.Pickup.Type} on setting {ev.KnobSetting}"); // Modify output: ev.OutputPosition = someOtherPosition; }; // SCP-914 processes a player Scp914Events.ProcessingPlayer += ev => { Logger.Info($"914 upgrading {ev.Player.DisplayName} on setting {ev.KnobSetting}"); // ev.IsAllowed = false; to prevent processing }; // Warhead events LabApi.Events.Handlers.WarheadEvents.Starting += ev => { Logger.Info($"Warhead starting! Activated by {ev.Player?.DisplayName ?? "auto"}"); // ev.IsAllowed = false; to cancel detonation }; ``` -------------------------------- ### ServerEvents Source: https://context7.com/northwood-studios/labapi/llms.txt Handle server-wide events such as round start/end, CASSIE announcements, and wave respawning. Some events allow cancellation or modification. ```APIDOC ## ServerEvents — Server-Wide Event Handlers Static partial class for server lifecycle events: round state, CASSIE, bans, waves, map generation, and more. ```csharp using LabApi.Events.Arguments.ServerEvents; using LabApi.Events.Handlers; ServerEvents.WaitingForPlayers += () => Logger.Info("Waiting for players..."); ServerEvents.RoundStarted += () => Logger.Info("Round started!"); ServerEvents.RoundEnded += ev => Logger.Info($"Round ended. Winner: {ev.LeadingTeam}"); ServerEvents.RoundRestarted += () => Logger.Info("Round restarted."); ServerEvents.WaveRespawning += ev => { Logger.Info($"Respawn wave incoming. Team: {ev.Wave}"); // ev.IsAllowed = false; to cancel the wave }; ServerEvents.CassieAnnouncing += ev => { Logger.Info($"CASSIE: '{ev.Words}'"); // Modify: ev.Words = "custom announcement"; }; ServerEvents.MapGenerated += ev => { int roomCount = LabApi.Features.Wrappers.Map.Rooms.Count; Logger.Info($"Map generated with {roomCount} rooms. Seed: {LabApi.Features.Wrappers.Map.Seed}"); }; ServerEvents.BanIssuing += ev => { Logger.Info($"Ban issued to {ev.Target} by {ev.Issuer}. Duration: {ev.Duration}s."); // ev.IsAllowed = false; to block the ban }; ``` ``` -------------------------------- ### PlayerEvents Source: https://context7.com/northwood-studios/labapi/llms.txt Subscribe to player actions like joining, leaving, hurting, and changing roles. Events can be cancellable or informational. ```APIDOC ## PlayerEvents — Player Event Handlers A static partial class exposing C# events for every player action. Events follow the naming pattern `*ing` (cancellable, modifiable before the action) and `*ed` (informational, after the action). Subscribe with `+=` / unsubscribe with `-=`. ```csharp using LabApi.Events.Arguments.PlayerEvents; using LabApi.Events.Handlers; // Subscribe in Enable(), unsubscribe in Disable() PlayerEvents.Joined += ev => Logger.Info($"{ev.Player.DisplayName} joined"); PlayerEvents.Left += ev => Logger.Info($"{ev.Player.DisplayName} left"); PlayerEvents.Hurting += OnHurting; PlayerEvents.ChangingRole += OnChangingRole; PlayerEvents.DroppingItem += OnDropping; PlayerEvents.SendingVoiceMessage += ev => { /* inspect voice traffic */ }; private static void OnHurting(PlayerHurtingEventArgs ev) { // Halve all damage dealt ev.DamageAmount *= 0.5f; // Cancel entirely: ev.IsAllowed = false; } private static void OnChangingRole(PlayerChangingRoleEventArgs ev) { if (ev.NewRole == PlayerRoles.RoleTypeId.Scp096) ev.IsAllowed = false; // prevent SCP-096 spawning } private static void OnDropping(PlayerDroppingItemEventArgs ev) { Logger.Info($"{ev.Player.DisplayName} dropped {ev.Item.ItemTypeId}"); } // Other commonly used PlayerEvents: // PlayerEvents.Banning, PlayerEvents.Kicking, PlayerEvents.Spawning, // PlayerEvents.PickingUpItem, PlayerEvents.ShootingWeapon, PlayerEvents.Dying, // PlayerEvents.Death, PlayerEvents.Escaping, PlayerEvents.InteractingDoor, ... ``` ``` -------------------------------- ### Implement a Continuous Loop with MEC Coroutine Source: https://github.com/northwood-studios/labapi/wiki/M.E.C. Create infinite loops using a while(true) or for(;;) structure within a coroutine, combined with Timing.WaitForSeconds to control the loop's frequency. ```csharp public IEnumerator MyLoopCoroutine() { while (true) // or for(;;) { Map.ShowBroadcast("See you all in 5 minutes!"); yield return Timing.WaitForSeconds(600); // wait for 5 minutes } } ```