ScriptLoader
ScriptLoader is a LabAPI plugin for SCP: Secret Laboratory that compiles and loads regular C# .cs files while the server is running.
ScriptLoader
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.
Features
- 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
Requirements
- SCP: Secret Laboratory Dedicated Server
- LabAPI
- .NET Framework 4.8
Installation
Place the plugin inside the LabAPI plugin directory:
LabAPI/
└── plugins/
└── 7777/
└── ScriptLoader.dll
The port directory may be different depending on your server configuration.
Script directory
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.
Script lifecycle
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
Example script
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.
Automatic reloading
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.
Safe replacement behavior
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.
Using LabAPI events
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.
Multiple scripts in one file
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
Script cleanup
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.
Compiler references
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.
Adding custom dependencies
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.
Building
Clone the repository and build the release configuration:
dotnet restore
dotnet build --configuration Release
The 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 Release
Embedded dependencies
ScriptLoader 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.
Runtime limitations
Assemblies cannot be individually unloaded
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.
Script execution is not sandboxed
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.
Main-thread blocking
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.
Troubleshooting
VTable setup of type CSharpCompilation failed
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.
Prepared 0 compiler references
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.
Script compiles but does not load
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
{
}
Script remains active after deletion
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.
Script reloads several times after one save
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
Recommended script practices
- 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
Example use cases
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.
License
MIT License
Disclaimer
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.
Author
Details
Published 12 days ago
Refreshed 21 minutes ago