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.
- Overview
- Core Capabilities
- Multi-World Architecture Comparison
- Version Matrix
- Runtime 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.
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.
| 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 |
Here,
proxy-basedbroadly 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.
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 |
If you want the real boot order, it looks like this:
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(), installs the launcher console host (TerminalLauncherConsoleHostby default), 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 |
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 |
If your main goal is "get a launcher up and see worlds come online," start here.
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) |
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 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 firstTake 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.slnx2. Build:
dotnet build src/UnifierTSL.slnx -c Debug3. (Optional) Produce local Publisher output:
dotnet run --project src/UnifierTSL.Publisher/UnifierTSL.Publisher.csproj -- \
--excluded-plugins ExamplePlugin,ExamplePlugin.FeaturesIf --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 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.
5. (Optional, simplest Visual Studio debug flow) Use the bundled launch profiles:
- Set startup project to
UnifierTSL.Publisherand run it once. - 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". - Switch startup project to
UnifierTSL, choose theExecutablelaunch profile, and start debugging. - That profile runs the published launcher from
utsl-publishand debugs the published program directly.
- On the first successful launch,
config/config.jsonis 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 isconfig/TShockAPI/; it also stores other TShock runtime files such astshock.sqlitewhen SQLite is enabled, so in practice it fills the same role as the standalone TShocktshock/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.
- 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, especiallyMainlineSyncBranch,MainlineSyncCommit, andMainlineVersion. - Launcher settings stay in
config/config.json, while the bundled TShock uses its own config-and-data root underconfig/TShockAPI/, separate from the launcher root config. This is also where other TShock runtime files live, such astshock.sqlitewhen SQLite is enabled, so this directory effectively plays the same role as the standalone TShocktshock/folder. config/TShockAPI/config.jsonholds global TShock defaults, whileconfig/TShockAPI/config.override.jsonstores per-server override patches keyed by configured server name, for example"S1": { "MaxSlots": 16 }.config/TShockAPI/sscconfig.jsonremains 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
worldIdinstead of only relying on the current global world state. - Editing
config.jsonorconfig.override.jsonexternally updates the watched config handles and reapplies runtime TShock server settings./reloadstill 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.
| 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 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
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, additivelauncher.autoStartServers,launcher.listenPort(listener rebind),launcher.colorfulConsoleStatus,launcher.consoleStatus
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 | 0–3, normal, expert, master, creative |
master |
size |
World size | 1–3, small, medium, large |
large |
evil |
World evil type | 0–2, 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 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. 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).
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 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-x64This 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 |
|
osx-x64 |
✅ Supported |
| 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