Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GitHub Downloads (all assets, all releases) Discord

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# .cs files
  • 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 GameObject instances
  • 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:

  1. The old script instance is disabled.
  2. The new source is compiled into a new assembly.
  3. The new assembly is loaded.
  4. 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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages