Skip to content

CedaryCat/UnifierTSL

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

223 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

UnifierTSL

Languages: English | 简体中文

UnifierTSL

Quick Start Releases Plugin Guide Architecture

Build Docs Check .NET 9.0 License: GPL-3.0

Run multiple Terraria worlds from one launcher,
keep each world in its own context, and handle routing, data sharing, and plugin-driven extension inside the same OTAPI USP runtime.


Quick Overview

📑 Table of Contents


📖 Overview

UnifierTSL wraps OTAPI Unified Server Process into a runtime you can run directly to host multiple Terraria worlds in one launcher process.

In traditional multi-process multi-world stacks, building a cluster of cooperating worlds usually means extra cross-process routing, state synchronization, and serialization design. Moving players between instances often relies on packet relays and side channels; when plugin-attached data, temporary state, or runtime objects need to cross worlds, problems that could otherwise stay in-process often have to be rewritten as protocols and synchronization flows.

Compared with approaches that push this coordination outside process boundaries, Unifier, based on OTAPI USP, keeps join routing, world switching, and extension hooks inside the same runtime plane and treats cross-world coordination as a first-class concern from the start. The launcher manages multi-world lifecycle centrally, lets each world run independently and in parallel in its own ServerContext, and provides a dedicated console per world so I/O stays isolated. UnifiedServerCoordinator handles coordination, UnifierApi.EventHub carries event traffic, and PluginHost.PluginOrchestrator runs plugin hosting. This shared listener-and-coordination model reduces the extra overhead and complexity introduced by cross-process relays, making cross-world interaction, data interchange, and unified operations easier while still leaving enough routing control to define the default join target and take over later world-switch flows.

From the player's side, this still behaves like a normal Terraria entry point: clients connect to one shared listener port, and UnifiedServerCoordinator routes each connection to the selected world inside the same process. 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 reachable directions, even though the launcher does not currently ship them as default out-of-the-box features, you can still expect usable example plugins to land under plugins/ over time.


✨ Core Capabilities

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 Independent, auto-reconnecting console I/O windows for each world context, plus semantic readline prompts and live status bars
🚀 RID-targeted publishing Publisher produces reproducible, runtime-specific directory trees

⚖️ Multi-World Architecture Comparison

Here, proxy-based broadly refers to the common family of designs built around a front proxy, multiple independent backend server processes, packet-level relays or rewriting, and optional shared storage or control-plane components.

Deployment and Lifecycle
Dimension UnifierTSL (single-process unified coordination) Proxy-based (front proxy + multiple backend processes) Stronger Fit
Failure isolation Worlds share one host process; isolation between contexts exists at the thread/runtime-context level Process-level isolation; one backend crash usually does not directly take down other backends or the proxy itself Proxy-based
Instance-level restart World contexts can be started and stopped freely A single backend can be restarted, replaced, or moved while the front entry point stays up Depends on the operational goal
Plugin hot reload The runtime provides standard support; whether a plugin can hot reload depends on the plugin's own design, and players are usually unaffected Backend plugins can be reloaded cleanly by restarting only the target backend instance, though that may require moving players to a temporary backend Depends on the operational goal
Horizontal elasticity The core design targets multi-world coordination inside one host; instances can be created freely, but they still share one process resource pool Naturally suited to cross-machine, cross-container, or cross-device scale-out Proxy-based
Existing plugin ecosystem reuse Requires adaptation to the Unifier runtime model Directly uses the existing plugin ecosystem Proxy-based
Runtime Coordination and Transfer
Dimension UnifierTSL (single-process unified coordination) Proxy-based (front proxy + multiple backend processes) Stronger Fit
World customizability World contexts are fully customizable and can be changed dynamically at runtime Depends on the backend implementation; in practice, most setups are relatively static after startup UnifierTSL
Server-side consistency World existence, routing targets, and connection ownership are resolved directly inside one runtime, so the consistency surface is narrower Depends on cross-process topology, registration, shared storage, or an external control plane, so the race surface is wider UnifierTSL
State awareness and switch orchestration One coordinator can see source world, target world, and player connection state together, so fallback and recovery logic can be centralized Target readiness, transfer failures, disconnect recovery, and rollback may require cross-process coordination UnifierTSL
Data transfer and entity migration Temporary state, plugin-attached data, and runtime objects can be transferred or coordinated directly inside one runtime State often has to be serialized, protocolized, or moved through shared databases or custom packets first UnifierTSL
Connection-state maintenance One listener keeps ownership of the client connection throughout world switches The proxy must maintain both client-side and backend-side connections and recover from failures on either side UnifierTSL
Cross-world plugin interoperability Feels more like “one plugin working across many ServerContext instances,” with direct reuse of in-process events and APIs Feels more like distributed-systems work, usually requiring message protocols, shared storage, or sync layers first UnifierTSL
Operations and System Shape
Dimension UnifierTSL (single-process unified coordination) Proxy-based (front proxy + multiple backend processes) Stronger Fit
Unified management cost Entry point, world lifecycle, default join policy, and config application all live on one coordination plane Management is usually split across the proxy, backend instances, and external orchestration pieces UnifierTSL
Monitoring and observability Logs, status bars, event flow, and per-world runtime metrics aggregate naturally Observability is often split across proxy, backend, and external control layers, so correlation costs more UnifierTSL
Debugging and incident handling A single process gives you a more continuous timeline for debugging, log correlation, and reproduction Failures can be spread across proxy, backend, shared storage, and deployment orchestration layers UnifierTSL
Network and serialization overhead Critical coordination paths can stay in-process and avoid extra hops or auxiliary protocols Player transfers and extended state propagation may introduce extra packets plus related interception strategies, or rely on side channels UnifierTSL
Single-node capability density A single host can already cover routing, coordination, linkage, plugin interoperability, and unified operations A single node is more of a routing shell, with heavier behavior often shifted toward backends or external systems UnifierTSL

It is important to note that UnifierTSL and proxy-based topologies are not mutually exclusive. Because UnifierTSL already organizes many worlds behind a single external listener, it can still sit behind a higher-level gateway or proxy as a stronger single-node backend. In the end, the better choice depends on your needs: proxy-based stacks have a higher ceiling for cross-device, cross-process scaling and isolation, so if you prefer elastic multi-machine deployment with basic cross-server features, proxy-based is likely the better fit. By contrast, UnifierTSL has more natural advantages in single-node multi-world consistency, migration, and coordination. If you want to build a more structured and interactive multi-world group, UnifierTSL is likely the more interesting option to try today.


📊 Version Matrix

The baseline values below come straight from project files and restored package assets used by this repository:

Component Version Source
Target framework .NET 9.0 src/UnifierTSL/*.csproj
Terraria 1.4.5.6 OTAPI.dll from the OTAPI USP package referenced by this project
OTAPI USP 1.1.0-pre-release-upstream.30 src/UnifierTSL/UnifierTSL.csproj
TShock and dependency details
Item Value
Bundled TShock version 6.1.0
Sync branch general-devel
Sync commit 1afaeb514343ca547abceeb357654603d1e2a456
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

🏗 Runtime Architecture

Architecture flow

If you want the real boot order, it looks like this:

  1. Program.Main initializes assembly resolver, applies pre-run CLI language overrides, and prints runtime version details.
  2. Initializer.Initialize() prepares Terraria/USP runtime state and loads core hooks (UnifiedNetworkPatcher, UnifiedServerCoordinator, ServerContext setup).
  3. UnifierApi.PrepareRuntime(args) loads config/config.json, merges launcher file settings with CLI overrides, and configures the durable logging backend.
  4. UnifierApi.InitializeCore() creates EventHub, builds PluginOrchestrator, runs PluginHosts.InitializeAllAsync(), installs the launcher console host (TerminalLauncherConsoleHost by default), and applies the resolved launcher defaults (join mode + initial auto-start worlds).
  5. UnifierApi.CompleteLauncherInitialization() resolves interactive listen/password inputs, syncs the effective runtime snapshot, and raises launcher initialized events.
  6. 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

Pick Your Path

If you already know why you're here, jump in from the track that matches your role:

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

🚀 Quick Start

If your main goal is "get a launcher up and see worlds come online," start here.

Prerequisites

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)

Option A: Use a Release Bundle

If you just want to run it, this is the shortest path.

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 first

Windows note (SmartScreen/Defender reputation): On some machines, first launch of app/UnifierTSL.ConsoleClient.exe may 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 relaunch UnifierTSL.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 first

Option B: Run from Source

Take this path if you want local debugging, CI integration, or your own Publisher output.

1. Clone and restore:

git clone https://github.com/CedaryCat/UnifierTSL.git
cd UnifierTSL
dotnet restore src/UnifierTSL.slnx

2. Build:

dotnet build src/UnifierTSL.slnx -c Debug

3. (Optional) Produce local Publisher output:

dotnet run --project src/UnifierTSL.Publisher/UnifierTSL.Publisher.csproj -- \
  --excluded-plugins ExamplePlugin,ExamplePlugin.Features

If --rid is omitted, Publisher infers the current host RID automatically. For reproducible packaging or cross-host instructions, passing --rid explicitly is still recommended, for example --rid win-x64.

4. Run a launcher smoke test:

dotnet run --project src/UnifierTSL/UnifierTSL.csproj -- \
  -port 7777 -password changeme \
  -server "name:Dev worldname:Dev" \
  -joinserver first

Note: Default Publisher output directory is src/UnifierTSL.Publisher/bin/<Configuration>/net9.0/utsl-<rid>/. UnifierTSL.ConsoleClient should only be launched by the launcher; pipe arguments are injected automatically.

5. (Optional, simplest Visual Studio debug flow) Use the bundled launch profiles:

  1. Set startup project to UnifierTSL.Publisher and run it once.
  2. The bundled Publisher profile writes to src/UnifierTSL.Publisher/bin/Debug/net9.0/utsl-publish/ because it uses --use-rid-folder false --clean-output-dir false --output-path "utsl-publish".
  3. Switch startup project to UnifierTSL, choose the Executable launch profile, and start debugging.
  4. That profile runs the published launcher from utsl-publish and debugs the published program directly.

What Happens on First Boot

  • On the first successful launch, config/config.json is created automatically and stores the effective launcher startup snapshot. CLI arguments still win for the launch you are doing right now.
  • Plugin configs live under config/<PluginName>/. For the bundled TShock port, that root is config/TShockAPI/; it also stores other TShock runtime files such as tshock.sqlite when SQLite is enabled, so in practice it fills the same role as the standalone TShock tshock/ directory.
  • Published bundles start with a flat plugins/ directory. During startup, the module loader may reshuffle modules into subfolders when dependency or core-module metadata says it should.
  • If everything went well, you should see the shared listener bind, the configured worlds start, the launcher status output begin updating, and, under the default console I/O implementation, one dedicated console window appear for each world.

Bundled TShock Notes

  • The bundled TShock here is a migration for the UnifierTSL / OTAPI USP runtime. Its lower-level logic is reimplemented by prioritizing UTSL/USP-native runtime APIs, event surfaces, packet models, and similar built-in capabilities, without maintaining an extra compatibility layer, while still aiming to keep the behavior and operator experience of TShock's higher-level features as close to upstream TShock as possible within a multi-world, single-process runtime model.
  • This port is maintained to keep tracking upstream TShock. You can inspect the current migration baseline directly in src/Plugins/TShockAPI/TShockAPI.csproj, especially MainlineSyncBranch, MainlineSyncCommit, and MainlineVersion.
  • Launcher settings stay in config/config.json, while the bundled TShock uses its own config-and-data root under config/TShockAPI/, separate from the launcher root config. This is also where other TShock runtime files live, such as tshock.sqlite when SQLite is enabled, so this directory effectively plays the same role as the standalone TShock tshock/ folder.
  • config/TShockAPI/config.json holds global TShock defaults, while config/TShockAPI/config.override.json stores per-server override patches keyed by configured server name, for example "S1": { "MaxSlots": 16 }. config/TShockAPI/sscconfig.json remains a separate file for SSC settings.
  • Because the runtime hosts multiple worlds at once, some TShock data access that is usually implicit in a single-world flow becomes explicit here; for example, warp-related code paths resolve entries with an explicit worldId instead of only relying on the current global world state.
  • Editing config.json or config.override.json externally updates the watched config handles and reapplies runtime TShock server settings. /reload still matters because it additionally refreshes permissions, regions, bans, whitelist-backed state, and the classic TShock reload flow. Some changes still require a restart.
  • Finally, thanks to the TShock project and its contributors for the functionality, design work, and ecosystem this migration builds upon.

🎮 Launcher Reference

Command-Line Flags

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, --auto-start-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
-colorful, --colorful, --no-colorful Toggle vivid ANSI status-bar rendering on interactive terminals true / false, on / off, 1 / 0; --no-colorful disables true
-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 first or random.

Launcher Config File

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:

  1. config/config.json
  2. CLI overrides (then persisted back to config/config.json as the effective startup snapshot)
  3. Interactive prompts for a missing port/password

On interactive terminals, missing port/password prompts use semantic readline with ghost text, rotating suggestions, and live validation/status lines; non-interactive hosts fall back automatically.

launcher.consoleStatus controls command-line status rendering. launcher.colorfulConsoleStatus still toggles the vivid ANSI palette, while launcher.consoleStatus.bandwidthUnit selects bytes (KB/s -> MB/s -> GB/s -> TB/s, default) or bits (Kbps -> Mbps -> Gbps -> Tbps), and launcher.consoleStatus.bandwidthRolloverThreshold controls when the formatter promotes to the next unit family step (default: 500.0).

Default console-status values
Key Unit Default Description
targetUps UPS 60.0 Target update rate used as the baseline for TPS health checks
healthyUpsDeviation UPS delta 2.0 Maximum absolute deviation from targetUps that still counts as healthy
warningUpsDeviation UPS delta 5.0 Maximum absolute deviation from targetUps that still counts as warning before turning bad
utilHealthyMax ratio (0.0-1.0) 0.55 Highest busy-utilization value that still counts as healthy
utilWarningMax ratio (0.0-1.0) 0.80 Highest busy-utilization value that still counts as warning before turning bad
onlineWarnRemainingSlots slots 5 Remaining player slots at or below this value turn the online indicator to warning
onlineBadRemainingSlots slots 0 Remaining player slots at or below this value turn the online indicator to bad/full
bandwidthUnit enum bytes Bandwidth display family: bytes (KB/s -> MB/s -> GB/s -> TB/s) or bits (Kbps -> Mbps -> Gbps -> Tbps)
bandwidthRolloverThreshold current display unit 500.0 Value at or above this threshold promotes the formatter to the next bandwidth unit
upWarnKBps KB/s 800.0 Server upstream bandwidth threshold that turns the network indicator to warning
upBadKBps KB/s 1600.0 Server upstream bandwidth threshold that turns the network indicator to bad
downWarnKBps KB/s 50.0 Server downstream bandwidth threshold that turns the network indicator to warning
downBadKBps KB/s 100.0 Server downstream bandwidth threshold that turns the network indicator to bad
launcherUpWarnKBps KB/s 2400.0 Launcher upstream bandwidth threshold that turns the network indicator to warning
launcherUpBadKBps KB/s 4800.0 Launcher upstream bandwidth threshold that turns the network indicator to bad
launcherDownWarnKBps KB/s 150.0 Launcher downstream bandwidth threshold that turns the network indicator to warning
launcherDownBadKBps KB/s 300.0 Launcher downstream bandwidth threshold that turns the network indicator to bad

After UnifiedServerCoordinator.Launch(...) succeeds, the launcher begins watching config/config.json for safe hot reloads:

  • Live-applied: launcher.serverPassword, launcher.joinServer, additive launcher.autoStartServers, launcher.listenPort (listener rebind), launcher.colorfulConsoleStatus, launcher.consoleStatus

Server Definition Keys

Each -server value is whitespace-separated key:value pairs parsed by LauncherRuntimeOps during startup config merge:

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 03, normal, expert, master, creative master
size World size 13, small, medium, large large
evil World evil type 02, random, corruption, crimson random

-servermerge behavior:

  • replace (default): clean replacement; config entries not present in CLI are removed.
  • overwrite: keep config entries, but CLI entries with the same name replace them.
  • append: keep config entries, only add CLI entries whose name does not exist.
  • World-name conflicts are resolved by priority (higher-priority entry kept, lower-priority entry ignored with warning).

📦 Publisher Reference

CLI Flags

Flag Description Values Default
--rid Target runtime identifier. If omitted, Publisher infers the current host RID; explicit input is still recommended e.g. win-x64, linux-x64, osx-x64 Auto-detected from current host
--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).

Output Lifecycle

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

🗂 Project Layout

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/

🔌 Plugin System

Plugin Loading Flow

graph LR
    A["Scan plugins/"] --> B["Preload module metadata"]
    B --> C{"Module attributes"}
    C -->|Core or deps declared| D["Stage to plugins/&lt;Module&gt;/"]
    C -->|Requires core| E["Stage to plugins/&lt;CoreModule&gt;/"]
    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/&lt;PluginName&gt;/"]
Loading

Key Concepts

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


🛠 Developer Guide

Common Commands

# 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 the current host (RID auto-detected)
dotnet run --project src/UnifierTSL.Publisher/UnifierTSL.Publisher.csproj -- \
  --excluded-plugins ExamplePlugin,ExamplePlugin.Features

# Produce publisher output for a specific RID (recommended for reproducible packaging)
dotnet run --project src/UnifierTSL.Publisher/UnifierTSL.Publisher.csproj -- \
  --rid win-x64

Supported Platforms

This table reflects the currently maintained/documented packaging targets, not every RID Publisher can attempt to infer.

RID Status
win-x64 ✅ Supported
linux-x64 ✅ Supported
linux-arm64 ❌ Not supported yet
linux-arm ⚠️ Partial support / needs manual verification
osx-x64 ✅ Supported

📚 Resources

Resource Link
Developer Overview docs/dev-overview.md
Plugin Development Guide docs/dev-plugin.md
Branch Workflow Guide docs/branch-setup-guide.md
Branch Workflow Quick Reference docs/branch-strategy-quick-reference.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

About

Single-process multi-world Terraria server runtime built on OTAPI USP—an alternative to proxy stacks, with per-instance consoles, hot-reloadable plugins, and seamless in-process player routing service.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors