ScriptLoader is a LabAPI plugin for SCP: Secret Laboratory that compiles and loads regular C# .cs files while the server is running.
Scripts can use LabAPI events, player wrappers, Unity types, Mirror networking, and other assemblies available to the SCP:SL server.
ScriptLoader automatically detects when scripts are created, modified, renamed, or deleted, allowing server developers to update gameplay logic without rebuilding the main plugin.
- Loads regular C#
.csfiles - Compiles scripts at runtime using Roslyn
- Automatically detects script file changes
- Loads newly created scripts
- Reloads modified scripts
- Unloads deleted scripts
- Handles renamed scripts
- Keeps the previous working version active when compilation fails
- Reports compiler errors with file names, line numbers, and columns
- Runs script loading and unloading on Unity's main thread
- Supports multiple script classes in one file
- Provides a simple script lifecycle API
- Includes a Remote Admin reload command
- Supports scripts inside subdirectories
- Uses a configurable file-change debounce delay
- SCP: Secret Laboratory Dedicated Server
- LabAPI
- .NET Framework 4.8
Place the plugin inside the LabAPI plugin directory:
LabAPI/
└── plugins/
└── 7777/
└── ScriptLoader.dll
The port directory may be different depending on your server configuration.
ScriptLoader creates and watches:
LabAPI/scripts/
Example structure:
LabAPI/
└── scripts/
├── WelcomeScript.cs
├── AutomaticBroadcasts.cs
│
├── Events/
│ ├── PlayerDeathMessages.cs
│ └── RoundMessages.cs
│
└── Gamemodes/
└── InfectionGamemode.cs
All .cs files inside this directory and its subdirectories are detected automatically.
Every script must implement IScript:
public interface IScript
{
string Name { get; }
void Enable();
void Disable();
}Enable is called after the script has compiled and its instance has been created.
Disable is called when:
- The script file is modified
- The script file is deleted
- The script file is renamed
- Scripts are manually reloaded
- ScriptLoader is disabled
- The server shuts down normally
Create:
LabAPI/scripts/WelcomeScript.cs
Add:
using LabApi.Events.Arguments.PlayerEvents;
using LabApi.Events.Handlers;
using ScriptLoader;
public sealed class WelcomeScript : IScript
{
public string Name => "Welcome Script";
public void Enable()
{
PlayerEvents.Joined += OnPlayerJoined;
}
public void Disable()
{
PlayerEvents.Joined -= OnPlayerJoined;
}
private void OnPlayerJoined(PlayerJoinedEventArgs ev)
{
ev.Player.SendBroadcast(
"<color=orange>Welcome to the server!</color>",
10);
}
}After saving the file, ScriptLoader automatically compiles and enables it.
ScriptLoader uses FileSystemWatcher to detect changes.
File system callbacks only queue changes. Compilation and script lifecycle methods are processed later on Unity's main thread.
This prevents scripts from interacting with LabAPI or Unity objects from a background filesystem thread.
| Action | Result |
|---|---|
Create a .cs file |
The new script is compiled and enabled |
Modify a .cs file |
The updated version is compiled and replaces the old version |
Delete a .cs file |
The active script instance is disabled and removed |
Rename a .cs file |
The old path is unloaded and the new path is compiled |
| Introduce a compiler error | The error is logged and the previous version remains active |
A short debounce delay is used because many editors generate multiple filesystem events during a single save operation.
ScriptLoader compiles a modified file before disabling its current version.
When compilation fails:
[ScriptLoader] Compilation failed for 'WelcomeScript.cs':
WelcomeScript.cs(24,18): CS1002: ; expected
[ScriptLoader] Keeping previous version of 'WelcomeScript.cs' active.
The previously loaded version continues running until a valid replacement is saved or the file is deleted.
If compilation succeeds but the new script throws an exception during Enable, the failed instance is cleaned up and the error is logged.
Scripts can subscribe to regular LabAPI events:
using LabApi.Events.Arguments.ServerEvents;
using LabApi.Events.Handlers;
using ScriptLoader;
public sealed class RoundScript : IScript
{
public string Name => "Round Script";
public void Enable()
{
ServerEvents.RoundStarted += OnRoundStarted;
ServerEvents.RoundEnded += OnRoundEnded;
}
public void Disable()
{
ServerEvents.RoundStarted -= OnRoundStarted;
ServerEvents.RoundEnded -= OnRoundEnded;
}
private void OnRoundStarted()
{
// Round start logic.
}
private void OnRoundEnded(RoundEndedEventArgs ev)
{
// Round end logic.
}
}Always unsubscribe every event in Disable.
One .cs file may contain multiple public classes implementing IScript:
using ScriptLoader;
public sealed class FirstScript : IScript
{
public string Name => "First Script";
public void Enable()
{
}
public void Disable()
{
}
}
public sealed class SecondScript : IScript
{
public string Name => "Second Script";
public void Enable()
{
}
public void Disable()
{
}
}Both classes are instantiated and enabled.
Each script class must:
- Be public
- Be non-abstract
- Implement
IScript - Have a public parameterless constructor
A script must clean up everything it creates or registers.
The Disable method should remove:
- LabAPI event subscriptions
- Static delegate subscriptions
- MEC coroutines
- Unity coroutines
- Timers
- Background tasks
- Registered commands
- Spawned
GameObjectinstances - Network objects
- Temporary files
- Other persistent resources
Example:
public void Enable()
{
PlayerEvents.Joined += OnJoined;
ServerEvents.RoundStarted += OnRoundStarted;
}
public void Disable()
{
PlayerEvents.Joined -= OnJoined;
ServerEvents.RoundStarted -= OnRoundStarted;
}Failing to unsubscribe an event may cause the previous script version to continue receiving events after a reload.
ScriptLoader prepares Roslyn metadata references from assemblies available to the server.
These may include:
- LabAPI
- Assembly-CSharp
- Unity assemblies
- Mirror
- CommandSystem
- Northwood libraries
- ScriptLoader
- Managed SCP:SL dependencies
- Additional LabAPI dependency assemblies
This allows scripts to directly reference server APIs:
using LabApi.Features.Wrappers;
using Mirror;
using UnityEngine;The exact available APIs depend on the installed SCP:SL and LabAPI versions.
Place additional managed assemblies inside the LabAPI dependency directory:
LabAPI/dependencies/7777/
Restart the server after adding or replacing a dependency.
Example:
LabAPI/
└── dependencies/
└── 7777/
└── MyCustomApi.dll
A script can then reference its namespaces:
using MyCustomApi;
using ScriptLoader;
public sealed class CustomApiScript : IScript
{
public string Name => "Custom API Script";
public void Enable()
{
MyApi.Initialize();
}
public void Disable()
{
MyApi.Shutdown();
}
}Adding a new assembly while the server is running may not automatically update the compiler reference cache. Restart the server or use a future dependency-cache refresh feature.
Clone the repository and build the release configuration:
dotnet restore
dotnet build --configuration ReleaseThe compiled plugin will normally be located at:
bin/Release/net48/ScriptLoader.dll
To perform a clean build:
Remove-Item -Recurse -Force .\bin, .\obj -ErrorAction SilentlyContinue
dotnet restore
dotnet build --configuration ReleaseScriptLoader may be built with Roslyn dependencies embedded into the main plugin assembly using a tool such as Costura.Fody.
In an embedded build, only this file may be required:
ScriptLoader.dll
Do not keep loose copies of the same embedded Roslyn assemblies in multiple LabAPI dependency directories. A loose assembly may be loaded before the embedded resolver and cause version conflicts.
Embedding dependencies simplifies distribution, but it does not guarantee isolation from assemblies already loaded by SCP:SL or Unity's Mono runtime.
SCP:SL currently runs on .NET Framework/Mono-style assembly loading.
After a script is compiled and loaded, its generated assembly cannot be individually unloaded from the current application domain.
When a script is updated:
- The old script instance is disabled.
- The new source is compiled into a new assembly.
- The new assembly is loaded.
- The old assembly remains allocated until the server process restarts.
This means frequent successful recompilation may slowly increase memory usage.
For normal development and occasional live updates, this is generally manageable. During intensive development, periodically restart the server.
Scripts execute inside the SCP:SL server process and have the same permissions as the server.
A script can potentially:
- Read or modify files
- Access player information
- Open network connections
- Start processes
- Stop the server
- Use reflection
- Run infinite loops
- Block the main thread
- Access loaded plugins and dependencies
Only install scripts from trusted developers.
ScriptLoader is not a security sandbox.
Compilation and lifecycle operations are processed on the Unity main thread to keep LabAPI and Unity access safe.
Large scripts or large groups of scripts may cause a temporary server frame delay while compiling.
Avoid performing slow work inside:
Enable()
Disable()For heavy operations, use carefully managed asynchronous work without accessing Unity objects from background threads.
Example:
System.TypeLoadException:
VTable setup of type
Microsoft.CodeAnalysis.CSharp.CSharpCompilation failed
This normally indicates incompatible Roslyn dependencies.
Verify that all Roslyn-related DLLs come from the same build and that duplicate versions are not installed in multiple locations.
Check:
LabAPI/dependencies/global/
LabAPI/dependencies/7777/
LabAPI/plugins/7777/
SCPSL_Data/Managed/
Common conflicting assemblies include:
Microsoft.CodeAnalysis.dll
Microsoft.CodeAnalysis.CSharp.dll
System.Collections.Immutable.dll
System.Reflection.Metadata.dll
System.Memory.dll
System.Runtime.CompilerServices.Unsafe.dll
System.Threading.Tasks.Extensions.dll
Remove old duplicate copies, deploy one compatible dependency set, and restart the server.
This means Roslyn could not create metadata references for the server assemblies.
The cause is usually an earlier Roslyn runtime failure. Look above the message in the server console for the first TypeLoadException, FileLoadException, or missing assembly error.
Ensure that the script class:
Is public
Is not abstract
Implements IScript
Has a public parameterless constructor
Correct:
public sealed class TestScript : IScript
{
public string Name => "Test";
public void Enable()
{
}
public void Disable()
{
}
}Incorrect:
internal sealed class TestScript : IScript
{
}The script probably did not unregister an event, stop a coroutine, cancel a timer, or destroy an object inside Disable.
Review the script cleanup implementation.
ScriptLoader debounces filesystem events, but some editors save files through several rename, delete, and create operations.
Increase the configured debounce delay, for example:
750 ms
to:
1500 ms
- Keep scripts small and focused
- Always implement complete cleanup
- Avoid blocking the main thread
- Avoid storing LabAPI wrappers indefinitely
- Validate players before acting on delayed operations
- Catch exceptions around optional gameplay logic
- Avoid unnecessary static state
- Restart the server periodically during heavy development
- Keep security-sensitive logic inside a compiled trusted plugin API
- Treat every installed script as fully trusted code
ScriptLoader can be used for:
- Welcome messages
- Automatic broadcasts
- Round event logic
- Temporary event gamemodes
- Player join and leave handling
- Custom administrative utilities
- Map modifications
- Role assignment logic
- SCP behavior experiments
- Server testing tools
- SiteLink integration scripts
- Rapid plugin prototyping
For larger or security-sensitive systems, a normal compiled LabAPI plugin is recommended.
MIT License
ScriptLoader is an unofficial server modification and is not affiliated with Northwood Studios.
SCP: Secret Laboratory and related names and assets belong to their respective owners.
Runtime C# compilation and script execution can affect server stability and security. Server owners are responsible for reviewing every script they install.