An experiment-friendly Terraria server launcher built on OTAPI USP, bundling per-instance consoles, early publishing helpers, and plugin scaffolding.
UnifierTSL aims to wrap OTAPI's Unified Server Process in a friendlier workflow so you can explore hosting multiple Terraria worlds without juggling ports or fragile scripts. The launcher tries to keep lifecycles in sync, route players automatically, and spin up a dedicated console client per world so inputs stay separated.
The solution currently bundles the launcher, publisher, console client, and sample plugins in one place. Shared services live in UnifiedServerCoordinator, event traffic flows through UnifierApi.EventHub, and PluginHost.PluginOrchestrator works toward hot-swappable integrations without touching the core launcher.
- Supports running multiple Terraria worlds from a single host process with per-instance console windows.
- Includes a lightweight publisher for producing repeatable bundles with sample plugins and configs.
- Provides evolving plugin tooling with hot reload, dependency staging, and metadata helpers.
- Targets Windows, Linux (x64 and ARM), and macOS via the .NET 9.0 toolchain.
Tip: Skim the Quick Start section if you want to launch a world in the next five minutes.
- Overview
- Quick Glance
- Core Capabilities
- Under the Hood
- How It Fits Together
- Quick Start
- Launcher Cheatsheet
- Server Definition Keys
- What's in the Bundle
- Project Layout
- Batteries Included
- Plugin Feature Overview
- Developer Reference
- Publisher CLI Reference
- Keep Exploring
- Multi-world focus: Experiment with hosting multiple isolated Terraria worlds in one process, each with tracked state and resource notes.
- Adjustable control room: Add or retire server contexts while players are connected, aiming to keep everyone on the same listening port.
- Plugin support: Load .NET plugins from a structured
plugins/tree with metadata, JSON/TOML configs, and hot-reload orchestration that is still evolving. - Managed module loading: Collectible
ModuleLoadContextinstances aim to deliver hot reloading, dependency sharing, automatic NuGet acquisition, and platform-aware native resolution. - Shared logging:
UnifierApi.LogCoreexposes pluggable filters, writers, and metadata injectors so diagnostics can be rerouted or enriched without restarts. - Bundled TShock build: Includes the USP-adapted TShock 5.2.2 build so familiar permissions, REST endpoints, SSC, and command tooling are close at hand.
- Dedicated consoles: Spawns a console client per server context through named pipes so each world's input and colored output stay readable.
- Publishing workflow: Helps generate repeatable bundles with RID-specific assets, included plugins, and configuration defaults for consistent deployments.
- Cross-platform targets: Can publish for
win-x64,linux-x64,linux-arm64,linux-arm, andosx-x64by leaning on the .NET SDK pipeline. - High-performance fundamentals: Inherits USP's structural optimizations including IL-transformed Tile types (interface-to-struct conversion), ref/in parameter propagation in Collision, and value-type packet protocols for efficient multi-world coordination.
Curious what powers the launcher? Here is the stack at a glance so you know what you are installing and why it matters.
-
Runtime: .NET 9.0 targeting framework-dependent executables with RID-specific assets generated by the publisher.
-
USP Core: Based on OTAPI.UnifiedServerProcess 1.0.13 to transform the Terraria dedicated server into a unified multi-world host.
-
Key Packages:
Package Version Purpose OTAPI.USP 1.0.13 Unified Server Process core ModFramework 1.1.15 IL modification framework used during patching MonoMod.RuntimeDetour 25.2.3 Runtime method hooking and detouring Tomlyn 0.19.0 TOML configuration parsing for launcher and plugins linq2db 5.4.1 Database abstraction layer leveraged by bundled plugins Microsoft.Data.Sqlite 9.0.0 SQLite provider used by TShock and sample plugins
Heads-up: Keep an eye on the OTAPI and TShock release notes when you upgrade packages so launcher hooks, configs, and plugins keep working together.
Picture UnifierTSL as a coordination layer that keeps several specialist subsystems in sync:
UnifiedServerCoordinatorowns client sockets, server contexts, and packet routing across hosted worlds (src/UnifierTSL/UnifiedServerCoordinator.cs).UnifierApiexposes lifecycle hooks, coordinates argument parsing, and instantiates shared infrastructure such asEventHuband the plugin orchestrator (src/UnifierTSL/UnifierApi.Internal.cs).ServerContextmodels each hosted Terraria world with isolated state so transfers and teardown can occur without cross-contamination (src/UnifierTSL/Servers/).PluginHost.PluginOrchestratororchestrates multipleIPluginHostimplementations to load plugins, register event handlers, and enable hot-reload without requiring changes to the launcher entry point (src/UnifierTSL/PluginHost/).Utilities.CLIparses raw arguments and nestedkey:valuepairs for both the launcher and publisher (src/UnifierTSL/Utilities.cs).LoggingjoinsLogger,RoleLogger, and metadata injectors under one hot-swappable pipeline (src/UnifierTSL/Logging/), letting subsystem roles clone loggers while reusing or overriding the sharedUnifierApi.LogCore.- Network patches located under
src/UnifierTSL/Network/bridge OTAPI primitives with the coordinator's expectations to keep USP packets flowing safely.
Want the big picture flow? Start with
Program.csin each project, then trace intoUnifiedServerCoordinatorto see how instances come online.
Here is the high-level startup order once the launcher kicks off:
- Initialize assembly resolution, localization resources, and USP network patches during launcher boot.
- Bring up global systems such as
UnifiedServerCoordinator, logging, and shared infrastructure while wiring the event hub. - Load plugins in ascending
InitializationOrder, allowing each dependency to observe prior initialization results. - Launch configured servers, begin listening for clients on the unified port, and hand off console duties to isolated client processes.
Prerequisite: Confirm the .NET 9.0 SDK is installed (
dotnet --list-sdks); install or upgrade fromhttps://dotnet.microsoft.com/if the command is missing or reports an older major version.
- Pick Use a Release Bundle if you want a prebuilt package with TShock, sample plugins, and launch scripts ready to copy onto a server.
- Pick Run from Source when you need deep debugging for plugin work, are tweaking the launcher, or wiring everything into CI/CD; plugin authors can also stay outside the repo by targeting the published NuGet packages.
Command contexts: release bundle commands run from the extracted bundle directory, while source workflow commands assume the repository root.
- Download the archive that matches your platform from the Releases tab:
utsl-<rid>-v<version>.zip(Windows) or.tar.gz(Linux/macOS). - Extract the archive on the host. Expect directories such as
lib/,plugins/,config/,app/, and the entry point (UnifierTSL.exefor Windows orUnifierTSLelsewhere). - Launch the server with flags that describe each hosted world:
- Windows (PowerShell)
.\UnifierTSL.exe -lang 7 -port 7777 -password changeme ` -server "name:S1 worldname:S1 gamemode:3 size:1 evil:0 seed:\"for the worthy\"" ` -server "name:S2 worldname:S2 gamemode:2 size:2"
- Linux or macOS
chmod +x UnifierTSL ./UnifierTSL -lang 7 -port 7777 -password changeme \ -server "name:S1 worldname:S1 gamemode:3 size:1 evil:0 seed:\"for the worthy\"" \ -joinserver first
- Windows (PowerShell)
- Drop any custom plugins or configs into the extracted
plugins/andconfig/directories, then restart the launcher to pick them up.
Release availability: If the Releases tab is empty, pull the latest GitHub Actions artifact or build your own bundle using the publisher recipe below.
Use this workflow when you need to inspect, modify, or automate the launcher.
- Clone and restore (skip if you already have the repo)
git clone https://github.com/CedaryCat/UnifierTSL.git cd UnifierTSL dotnet restore src/UnifierTSL.sln - Publish a bundle (recommended for operators)
dotnet run --project src/UnifierTSL.Publisher/UnifierTSL.Publisher.csproj -- \ --rid win-x64 \ --excluded-plugins ExamplePlugin
- Add
-c Releasefor production builds. - Replace
win-x64with the RID you target (such aslinux-x64orosx-x64). - Output lands in
src/UnifierTSL.Publisher/bin/<Configuration>/net9.0/utsl-<RID>.zip, a zipped copy of the publisher output. - Prefer the RID for the platform running the publisher so the generated AppHost matches the local .NET runtime.
- Add
- Smoke test locally
Useful for quick validation before publishing.
dotnet run --project src/UnifierTSL/UnifierTSL.csproj -- \ -port 7777 -password changeme -server "name:Dev worldname:Dev" - Observe console isolation
- Start the launcher (step 3) and note that it spawns one console client process per hosted world.
- Manual execution of
UnifierTSL.ConsoleClientis not supported; it expects the pipe identifiers supplied by the launcher during process creation.
For more information on the output behaviors that Publisher can control? Refer to the Publisher Output Behavior section in the plugin-dev-doc
These flags feed the launcher everything it needs to start, secure, and route worlds.
| Flag(s) | Description | Values | Default or Notes |
|---|---|---|---|
-listen, -port |
Set the TCP port used by UnifiedServerCoordinator |
Integer between 1 and 65535 | Prompts on STDIN until a valid value is supplied if omitted |
-password |
Set the connection password shared with clients | Any string, quotes allowed | Prompts on STDIN if omitted |
-autostart, -addserver, -server |
Queue one or more server definitions for launch | Repeatable; each value uses key:value pairs described below |
Invalid definitions are rejected with a console warning |
-joinserver |
Register a low priority handler that selects the server a player joins by default | first, f, random, rnd, or r |
Only the first valid value is applied |
-culture, -lang, -language |
Override the Terraria localization used for server text | Integer IDs accepted by GameCulture._legacyCultures |
Defaults to the game culture configured on the host |
Important: Unless a plugin registers a preferred join server through
EventHub.Coordinator.SwitchJoinServer, pass-joinserver first(orrandom) so connecting players are routed into a valid server instance. Without it, players will be kicked with the message "Unable to locate an available server to join."
Each -server (or -autostart/-addserver) entry is a space separated list of key:value pairs. Supported keys are parsed in UnifierApi.AutoStartServer.
Quick shorthand: wrap values containing spaces (for example world names or seeds) in quotes so the shell keeps each
key:valuepair together.
| Key | Purpose | Accepted Values | Notes |
|---|---|---|---|
name |
Friendly server identifier | Unique string | Required; conflicts cause the entry to be skipped |
worldname |
World name to create or load | Unique string | Required; conflicts with existing worlds abort the entry |
seed |
Generation seed | Any string | Optional; defaults to empty |
gamemode or difficulty |
World difficulty | 0-3, normal, expert, master, creative, or shorthand n, e, m, c |
Defaults to 2 (Master) |
size |
World size | 1-3, small, medium, large, or shorthand s, m, l |
Defaults to 3 (Large) |
evil |
World evil setting | 0-2, random, corruption, crimson |
Defaults to 0 (Random) |
The publisher will build the deployment package in the bin/<Config>/net9.0/utsl-<rid> directory. Its general structure is as follows, which can be used for a quick sanity check:
UnifierTSL.exe / UnifierTSL # Platform specific launcher entry point
app/
UnifierTSL.ConsoleClient.* # Console client binaries spawned by the launcher per server context
config/
TShockAPI/ # TShock configs, database, SSC, MOTD, rules
ExamplePlugin/ # Sample plugin configuration (remove if unused)
CommandTeleport/ # Additional plugin state and settings
lib/ # Shared managed libraries
plugins/
ExamplePlugin.dll
CommandTeleport.dll
TShockAPI/
TShockAPI.dll
dependencies.json # Generated at runtime after ModuleLoadContext resolves and stages dependencies
runtimes/ # Platform specific native assets (for example win-x64/)
start.bat / launch.sh # Helper scripts illustrating CLI usage (create locally as needed)
Note: Keep
plugins/andconfig/aligned so each plugin can locate configuration and dependency files after deployment.
Work from these folders when navigating the repository:
src/UnifierTSL.slnbrings together the launcher, console client, publisher, and sample plugins.src/UnifierTSL/hosts runtime entry points plus subsystems such asModule/,PluginHost/,Servers/, andNetwork/.src/UnifierTSL.ConsoleClient/contains the per-instance console isolation client and its named pipe protocol; the launcher is responsible for invoking it.src/UnifierTSL.Publisher/packages bundles tobin/<Config>/net9.0/utsl-<rid>.zipwith RID targeted assets.src/Plugins/provides maintained examples (ExamplePlugin,CommandTeleport,TShockAPI) that serve as scaffolds for new integrations.doc/stores project documentation, including this overview and design notes.
UnifierTSL ships with these companion projects so you do not have to stitch tooling together yourself:
- Launcher (
src/UnifierTSL/) handles USP hosting, world lifecycle management, and plugin bootstrap throughUnifiedServerCoordinator. - Console client (
src/UnifierTSL.ConsoleClient/) isolates each server context's console I/O through named pipes, keeping simultaneous sessions readable. - Logging core (
src/UnifierTSL/Logging/) centralizes the sharedLoggerso filters, writers, and metadata injectors can be swapped or extended by plugins viaUnifierApi.CreateLogger. - Publisher (
src/UnifierTSL.Publisher/) builds self-contained distributables and can exclude plugins per run.
Dropping a plugin DLL into plugins/ is enough for UnifierTSL to discover it, but the loader immediately tidies files so the runtime can manage hot reloads and dependencies. If a file vanishes from the location you copied it to, check the subfolders below—UnifierTSL has simply moved it into the layout it expects.
- Core modules (
[CoreModule]): Act as the anchor for related code. The loader createsplugins/<ModuleName>/, keeps the DLL there, and gives it a dedicated collectibleAssemblyLoadContext. Core modules can declare NuGet or embedded dependencies with[ModuleDependencies]; the loader extracts them intolib/and tracks versions independencies.json. - Dependent modules (
[RequiresCoreModule("MainName")]): Must point at an existing core module. They are moved into the core module’s folder and loaded through the sameAssemblyLoadContext, automatically reusing the dependencies the core module declared. They cannot add new dependency declarations of their own. - Independent modules (no
[CoreModule]or[RequiresCoreModule]): Load in their own context and cannot be targeted by dependent modules. Otherwise they behave like a self-contained core module—if they declare[ModuleDependencies], they get a private folder and dependency staging; if not, they stay wherever you copied them.
- Initial scans cover both the root and existing subdirectories. If the loader spots a core attribute, dependency attribute, or
RequiresCoreModule, it moves the DLL (and its PDB) into the appropriate folder before loading it. - Core modules and independent modules with dependency declarations end up as
plugins/<ModuleName>/<ModuleName>.dll, accompanied bylib/folders when dependencies are present. - Dependent modules sit alongside their core module but keep their own file name, so features such as
ExamplePlugin.Features.dlltravel with the ExamplePlugin directory while remaining easy to identify. - Independent modules without dependency declarations remain exactly where you placed them until you add metadata that changes how they should load.
- Every plugin gets a private configuration directory under
config/, named after the DLL without its extension. That is whyExamplePlugin.Features.dllstores settings inconfig/ExamplePlugin.Features/, separate fromconfig/ExamplePlugin/. - Plugins decide whether a manual edit takes effect immediately. The configuration system raises file-change notifications, but auto-reloading is opt-in; many plugins persist the new values only after they call
TriggerReloadOnExternalChange(true)or offer an in-game reload command. The bundled ExamplePlugin enables instant reload and logs changes, while the integrated TShock build also taps into UnifierTSL's auto reload (upstream TShock still follows its own flow). - If a plugin does not automatically reload, restart the plugin or the launcher to apply edits safely.
- ExamplePlugin.dll: A core module that bootstraps configuration helpers and exposes shared tools for satellite plugins.
- ExamplePlugin.Features.dll: A dependent module that extends the main plugin and loads only after ExamplePlugin finishes initializing.
- CommandTeleport.dll: An independent plugin that hooks into the multi-world coordinator; it can stand alone or declare its own dependencies.
- TShockAPI.dll: A core module that stages database drivers and HTTP components inside its
lib/directory, making them available to its dependents and any runtime assembly resolution.
- You can drop managed assemblies—such as
Newtonsoft.Json.dll—directly intoplugins/. They are discovered like any other module even if they are not a plugin entry point. - When another module requests an assembly, UnifierTSL first prefers an exact version match from already loaded modules, then looks at the dependencies declared by the requesting module, and finally falls back to name-only matches across the staged assemblies. This strategy avoids loading the same DLL into multiple
AssemblyLoadContextinstances and helps reduce memory usage for shared libraries.
Keep these commands in your terminal history while working on the project:
dotnet restore src/UnifierTSL.slnrestores all solution dependencies.dotnet build src/UnifierTSL.sln -c Debug [-warnaserror]builds the workspace with optional analyzer enforcement.dotnet run --project src/UnifierTSL/UnifierTSL.csprojlaunches the server; omit arguments to accept the interactive prompts, or append--followed by CLI flags such as-port 7777.dotnet run --project src/UnifierTSL.Publisher/UnifierTSL.Publisher.csproj -- --rid win-x64produces a distributable bundle undersrc/UnifierTSL.Publisher/bin/<Config>/net9.0/utsl-win-x64.zip; combine with--excluded-plugins ExamplePluginas needed.dotnet run --project src/UnifierTSL.ConsoleClient/UnifierTSL.ConsoleClient.csprojrequires the pipe arguments injected by the launcher; run the launcher instead to exercise console isolation.
Testing roadmap: No automated tests exist yet. Start with
dotnet new xunit -n UnifierTSL.Tests -o tests/UnifierTSL.Tests, mirror runtime namespaces (for exampleModule/toModuleTests/), and rundotnet test src/UnifierTSL.slnafter new work.
Use these switches to tailor bundles without editing code:
| Flag | Description | Values | Notes |
|---|---|---|---|
--rid |
Target runtime identifier passed to CoreAppBuilder, AppToolsPublisher, and plugin bundlers |
Required single value such as win-x64, linux-x64, osx-x64 |
Throws if omitted or provided more than once (Program.cs) |
--excluded-plugins |
Comma separated list of plugin names to skip | Optional; accepts multiple occurrences or comma lists | Parsed into trimmed entries before invoking PluginsBuilder |
Tip: Combine
--ridwith theDOTNET_CLI_TELEMETRY_OPTOUTenvironment variable in CI so your build logs stay quiet and reproducible.
Round out your knowledge with these guides:
- Developer Overview for in-depth technical details about the architecture, subsystems, and implementation patterns.
- Plugin Development Guide for comprehensive plugin authoring, configuration management, and hot-reload mechanics.
- OTAPI Unified Server Process for background on the multi-server runtime model.
- Upstream TShock Repository for permissions, REST management, SSC, and administration best practices.
- DeepWiki AI Analysis for AI-generated project exploration (reference only, generally accurate).