Host multiple Terraria worlds in one launcher process,
keep worlds isolated, and keep extending behavior with plugins and publisher tooling on OTAPI USP.
- Overview
- Core Capabilities
- Version Matrix
- Architecture
- Quick Start
- Launcher Reference
- Publisher Reference
- Project Layout
- Plugin System
- Developer Guide
- Resources
UnifierTSL wraps OTAPI Unified Server Process into a runtime you can run directly to host multiple Terraria worlds in one launcher process.
The launcher handles world lifecycle, player join routing, and spins up a dedicated console client per world context so each world's I/O stays separate.
Compared with classic single-world servers or packet-routed multi-process world stacks, Unifier keeps join routing, world handoff, and extension hooks in one runtime surface instead of scattering that logic across process boundaries.
UnifiedServerCoordinator handles coordination, UnifierApi.EventHub carries event traffic, and PluginHost.PluginOrchestrator runs plugin hosting.
With shared connection and state surfaces, you can operate worlds together and build tighter cross-world interactions, while policy-based routing and transfer hooks still leave room for world-level fallback behavior.
If you push this model further, you can build more gameplay-driven setups: fully connected multi-instance world clusters, elastic worlds that load or unload region-sized shards on demand, or private worlds tuned per player for logic and resource budgets.
These are achievable directions, not out-of-the-box defaults.
Some heavier implementations may stay outside launcher core, but you can expect practical sample plugins for these patterns to land over time in the plugins/ ecosystem.
| Feature | Description |
|---|---|
| 🖥 Multi-world coordination | Run and isolate multiple worlds in a single runtime process |
| 🧱 Struct-based tile storage | World tiles use struct TileData instead of ITile for lower memory use and faster reads/writes |
| 🔀 Live routing control | Set default join strategies and re-route players through coordinator events at runtime |
| 🔌 Plugin hosting | Load .NET modules from plugins/ and handle config registration plus dependency extraction |
| 📦 Collectible module contexts | ModuleLoadContext gives you unloadable plugin domains and staged dependency handling |
| 📝 Shared logging pipeline | UnifierApi.LogCore supports custom filters, writers, and metadata injectors |
| 🛡 Bundled TShock port | Ships with a USP-adapted TShock baseline ready for use |
| 💻 Per-context console isolation | Console client processes spawned via named pipe protocol |
| 🚀 RID-targeted publishing | Publisher produces reproducible, runtime-specific directory trees |
The baseline values below come straight from project files and runtime version helpers in this repository:
| Component | Version | Source |
|---|---|---|
| Target framework | .NET 9.0 |
src/UnifierTSL/*.csproj |
| Terraria | 1.4.5.5 |
src/UnifierTSL/VersionHelper.cs (assembly file version from OTAPI/Terraria runtime) |
| OTAPI USP | 1.1.0-pre-release-upstream.25 |
src/UnifierTSL/UnifierTSL.csproj |
TShock and dependency details
| Item | Value |
|---|---|
| Bundled TShock version | 5.9.9 |
| Sync branch | general-devel |
| Sync commit | dab27acb4bf827924803f57918a7023231e43ab3 |
| Source | src/Plugins/TShockAPI/TShockAPI.csproj |
Additional dependency baselines:
| Package | Version | Source |
|---|---|---|
| ModFramework | 1.1.15 |
src/UnifierTSL/UnifierTSL.csproj |
| MonoMod.RuntimeDetour | 25.2.3 |
src/UnifierTSL/UnifierTSL.csproj |
| Tomlyn | 0.19.0 |
src/UnifierTSL/UnifierTSL.csproj |
| linq2db | 5.4.1 |
src/UnifierTSL/UnifierTSL.csproj |
| Microsoft.Data.Sqlite | 9.0.0 |
src/UnifierTSL/UnifierTSL.csproj |
Actual runtime startup flow:
Program.Maininitializes assembly resolver, applies pre-run CLI language overrides, and prints runtime version details.Initializer.Initialize()prepares Terraria/USP runtime state and loads core hooks (UnifiedNetworkPatcher,UnifiedServerCoordinator,ServerContextsetup).UnifierApi.PrepareRuntime(args)loadsconfig/config.json, merges launcher file settings with CLI overrides, and configures the durable logging backend.UnifierApi.InitializeCore()createsEventHub, buildsPluginOrchestrator, runsPluginHosts.InitializeAllAsync(), and applies the resolved launcher defaults (join mode + initial auto-start worlds).UnifierApi.CompleteLauncherInitialization()resolves interactive listen/password inputs, syncs the effective runtime snapshot, and raises launcher initialized events.UnifiedServerCoordinator.Launch(...)opens the shared listener;UnifierApi.StartRootConfigMonitoring()then enables root-config hot reload before title updates, coordinator started event, and chat input loop begin.
Runtime responsibilities at a glance
| Component | Responsibilities |
|---|---|
Program.cs |
Starts the launcher and bootstraps the runtime |
UnifierApi |
Initializes event hub, plugin orchestration, and launcher argument handling |
UnifiedServerCoordinator |
Manages listening socket, client coordination, and world routing |
ServerContext |
Keeps each hosted world's runtime state isolated |
PluginHost + module loader |
Handles plugin discovery, loading, and dependency staging |
| Role | Start Here | Why |
|---|---|---|
| 🖥 Server operator | Quick Start ↓ | Bring up a usable multi-world host with minimal setup |
| 🔌 Plugin developer | Plugin Development Guide | Build and migrate modules with the same config/events/deps flow the launcher uses |
Choose the requirement set that matches how you plan to run UnifierTSL:
| Workflow | Requirements |
|---|---|
| Release bundles only | .NET 9 Runtime on the target host |
| From source / Publisher | .NET 9 SDK + msgfmt in PATH (for .mo files) |
1. Download the release asset that matches your platform from GitHub Releases:
| Platform | File pattern |
|---|---|
| Windows | utsl-<rid>-v<semver>.zip |
| Linux / macOS | utsl-<rid>-v<semver>.tar.gz |
2. Extract and launch:
Windows (PowerShell)
.\UnifierTSL.exe -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" `
-joinserver firstWindows note (SmartScreen/Defender reputation): On some machines, first launch of
app/UnifierTSL.ConsoleClient.exemay be blocked as an unknown publisher or unrecognized app. If this happens, the main launcher console can appear stuck in loading because it keeps retrying the per-world console startup. Allow the executable (or trust the extracted folder), then relaunchUnifierTSL.exe.
Linux / macOS
chmod +x UnifierTSL
./UnifierTSL -port 7777 -password changeme \
-server "name:S1 worldname:S1 gamemode:3 size:1 evil:0 seed:\"for the worthy\"" \
-joinserver firstUse this path for local debugging, CI integration, or custom bundle output.
1. Clone and restore:
git clone https://github.com/CedaryCat/UnifierTSL.git
cd UnifierTSL
dotnet restore src/UnifierTSL.slnx2. Build:
dotnet build src/UnifierTSL.slnx -c Debug3. (Optional) Produce local Publisher output:
dotnet run --project src/UnifierTSL.Publisher/UnifierTSL.Publisher.csproj -- \
--rid win-x64 \
--excluded-plugins ExamplePlugin,ExamplePlugin.Features4. Run a launcher smoke test:
dotnet run --project src/UnifierTSL/UnifierTSL.csproj -- \
-port 7777 -password changeme \
-server "name:Dev worldname:Dev" \
-joinserver firstNote: Default Publisher output directory is
src/UnifierTSL.Publisher/bin/<Configuration>/net9.0/utsl-<rid>/.UnifierTSL.ConsoleClientshould only be launched by the launcher; pipe arguments are injected automatically.
| Flag(s) | Description | Accepted Values | Default |
|---|---|---|---|
-listen, -port |
Coordinator TCP port | Integer | Prompts on STDIN |
-password |
Shared client password | Any string | Prompts on STDIN |
-autostart, -addserver, -server |
Add server definitions | Repeatable key:value pairs |
— |
-servermerge, --server-merge |
How CLI -server entries merge with config |
replace / overwrite / append |
replace |
-joinserver |
Default join strategy | first / f / random / rnd / r |
— |
-logmode, --log-mode |
Durable launcher log backend | txt / none / sqlite |
txt |
-culture, -lang, -language |
Override Terraria language | Legacy culture ID or name | Host culture |
Tip: If no plugin takes over join behavior through
EventHub.Coordinator.SwitchJoinServer, use-joinserver firstorrandom.
The launcher root config is config/config.json. It is separate from plugin configs (config/<PluginName>/...), and the legacy root-level config.json is intentionally ignored.
Startup precedence is:
config/config.json- CLI overrides (then persisted back to
config/config.jsonas the effective startup snapshot) - Interactive prompts for a missing port/password
After UnifiedServerCoordinator.Launch(...) succeeds, the launcher begins watching config/config.json for safe hot reloads:
- Live-applied:
launcher.serverPassword,launcher.joinServer, additivelauncher.autoStartServers,launcher.listenPort(listener rebind)
Each -server value is whitespace-separated key:value pairs parsed by UnifierApi.AutoStartServer:
| Key | Purpose | Accepted Values | Default |
|---|---|---|---|
name |
Friendly server identifier | Unique string | Required |
worldname |
World name to load/generate | Unique string | Required |
seed |
Generation seed | Any string | — |
gamemode / difficulty |
World difficulty | 0–3, normal, expert, master, creative |
2 |
size |
World size | 1–3, small, medium, large |
3 |
evil |
World evil type | 0–2, random, corruption, crimson |
0 |
-servermerge behavior:
replace(default): clean replacement; config entries not present in CLI are removed.overwrite: keep config entries, but CLI entries with the samenamereplace them.append: keep config entries, only add CLI entries whosenamedoes not exist.- World-name conflicts are resolved by priority (higher-priority entry kept, lower-priority entry ignored with warning).
| Flag | Description | Values | Default |
|---|---|---|---|
--rid |
Target runtime identifier | e.g. win-x64, linux-x64, osx-x64 |
Required |
--excluded-plugins |
Plugin projects to skip | Comma-separated or repeated | — |
--output-path |
Base output directory | Absolute or relative path | src/.../bin/<Config>/net9.0 |
--use-rid-folder |
Append utsl-<rid> folder |
true / false |
true |
--clean-output-dir |
Clear existing output first | true / false |
true |
Publisher builds framework-dependent outputs (SelfContained=false).
Initial Publisher output (local)
Publisher writes a directory tree (not an archive):
utsl-<rid>/
├── UnifierTSL(.exe)
├── UnifierTSL.pdb
├── app/
│ ├── UnifierTSL.ConsoleClient(.exe)
│ └── UnifierTSL.ConsoleClient.pdb
├── i18n/
├── lib/
├── plugins/
│ ├── TShockAPI.dll
│ ├── TShockAPI.pdb
│ ├── CommandTeleport.dll
│ └── CommandTeleport.pdb
└── runtimes/
Runtime-reorganized plugin layout (after first boot)
On startup, the module loader may rearrange plugin files into module folders based on attributes ([CoreModule], [RequiresCoreModule], and dependency declarations):
plugins/
├── TShockAPI/
│ ├── TShockAPI.dll
│ ├── dependencies.json
│ └── lib/
└── CommandTeleport.dll
config/
├── config.json
├── TShockAPI/
└── CommandTeleport/
dependencies.json is generated or updated by dependency staging logic during module loading.
CI artifact and release naming
GitHub Actions uses two naming layers:
| Layer | Pattern |
|---|---|
| Workflow artifacts | utsl-<rid>-<semver> |
| Release archives (Windows) | utsl-<rid>-v<semver>.zip |
| Release archives (Linux/macOS) | utsl-<rid>-v<semver>.tar.gz |
| Component | Purpose |
|---|---|
Launcher (UnifierTSL) |
Runtime entry point for world bootstrap, routing, and coordinator lifecycle |
Console Client (UnifierTSL.ConsoleClient) |
One console process per world, connected by named pipes |
Publisher (UnifierTSL.Publisher) |
Builds RID-targeted deployment directory outputs |
Plugins (src/Plugins/) |
Modules maintained in-repo (TShockAPI, CommandTeleport, examples) |
Docs (docs/) |
Runtime, plugin, and migration docs |
.
├── src/
│ ├── UnifierTSL.slnx
│ ├── UnifierTSL/
│ │ ├── Module/
│ │ ├── PluginHost/
│ │ ├── Servers/
│ │ ├── Network/
│ │ └── Logging/
│ ├── UnifierTSL.ConsoleClient/
│ ├── UnifierTSL.Publisher/
│ └── Plugins/
│ ├── TShockAPI/
│ ├── CommandTeleport/
│ ├── ExamplePlugin/
│ └── ExamplePlugin.Features/
└── docs/
graph LR
A["Scan plugins/"] --> B["Preload module metadata"]
B --> C{"Module attributes"}
C -->|Core or deps declared| D["Stage to plugins/<Module>/"]
C -->|Requires core| E["Stage to plugins/<CoreModule>/"]
C -->|None| F["Keep in plugins/ root"]
D --> G["Load collectible module contexts"]
E --> G
F --> G
G --> H["Extract deps when declared (lib/ + dependencies.json)"]
H --> I["Discover IPlugin entry points"]
I --> J["Initialize plugins (BeforeGlobalInitialize -> InitializeAsync)"]
J --> K["Plugins may register config/<PluginName>/"]
| Concept | Description |
|---|---|
| Module preloading | ModuleAssemblyLoader reads assembly metadata and stages file locations before plugin instantiation |
[CoreModule] |
Marks a module for a dedicated folder and core module context anchor |
[RequiresCoreModule("...")] |
Loads this module under the specified core module context |
| Dependency staging | Modules with declared dependencies extract into lib/ and track status in dependencies.json |
| Plugin initialization | Dotnet host runs BeforeGlobalInitialize first, then InitializeAsync in sorted plugin order |
| Config registration | Configs stored in config/<PluginName>/, supports auto-reload (TriggerReloadOnExternalChange(true)) |
| Collectible contexts | ModuleLoadContext enables unloadable plugin domains |
→ Full guide: Plugin Development Guide
# Restore dependencies
dotnet restore src/UnifierTSL.slnx
# Build (Debug)
dotnet build src/UnifierTSL.slnx -c Debug
# Run launcher with test world
dotnet run --project src/UnifierTSL/UnifierTSL.csproj -- \
-port 7777 -password changeme -joinserver first
# Produce publisher output for Windows x64
dotnet run --project src/UnifierTSL.Publisher/UnifierTSL.Publisher.csproj -- \
--rid win-x64
# Run tests (when available)
dotnet test src/UnifierTSL.slnxNote: Automated tests are not included in the repository.
| RID | Status |
|---|---|
win-x64 |
✅ Supported |
linux-x64 |
✅ Supported |
linux-arm64 |
✅ Supported |
linux-arm |
✅ Supported |
osx-x64 |
✅ Supported |
| Resource | Link |
|---|---|
| Developer Overview | docs/dev-overview.md |
| Plugin Development Guide | docs/dev-plugin.md |
| OTAPI Unified Server Process | GitHub |
| Upstream TShock | GitHub |
| DeepWiki AI Analysis | deepwiki.com (reference only) |
Made with ❤️ by the UnifierTSL contributors · Licensed under GPL-3.0