|
| 1 | +# Developing on OTAPI Unified Server Process (USP) |
| 2 | + |
| 3 | +## Quick Start |
| 4 | +- Install: Use the NuGet package `OTAPI.USP` to get `OTAPI.dll` and `OTAPI.Runtime.dll`. |
| 5 | +- Build: Restore and build locally if you need to work from source. |
| 6 | +- Run: See the Build from Source section for commands. |
| 7 | + |
| 8 | +## Table of Contents |
| 9 | +- [OTAPI Ecosystem and USP](#otapi-ecosystem-and-usp) |
| 10 | +- [Install via NuGet (recommended)](#install-via-nuget-recommended) |
| 11 | +- [Build from Source (secondary)](#build-from-source-secondary) |
| 12 | +- [Core Concepts](#core-concepts) |
| 13 | +- [Creating and Using a Context](#creating-and-using-a-context) |
| 14 | +- [World Data: TileProvider](#world-data-tileprovider) |
| 15 | +- [Protocol: TrProtocol (IL-merged)](#protocol-trprotocol-il-merged) |
| 16 | +- [Collision (pre-patch ref parameters for performance)](#collision-pre-patch-ref-parameters-for-performance) |
| 17 | +- [Networking in Multi-Server: GlobalNetwork](#networking-in-multi-server-globalnetwork) |
| 18 | +- [Hooks and Extensions](#hooks-and-extensions) |
| 19 | +- [Compatibility Note (USP vs. TSAPI/TShock)](#compatibility-note-usp-vs-tsapitshock) |
| 20 | +- [Next Steps](#next-steps) |
| 21 | +- [References](#references) |
| 22 | + |
| 23 | +This guide explains how to build against USP’s context‑bound server core, how to migrate code from older OTAPI/TShock style statics, and how to leverage TrProtocol, TileProvider, and Collision efficiently. It also clarifies where USP sits in the OTAPI ecosystem. |
| 24 | + |
| 25 | + |
| 26 | +## OTAPI Ecosystem and USP |
| 27 | +- Historical flow: Launchers like TSAPI (TShock.Launcher) start the server, TShock runs as a plugin; both depend on the OTAPI NuGet package that ships `OTAPI.dll` and `OTAPI.Runtime.dll`. |
| 28 | +- What USP does: `OTAPI.UnifiedServerProcess` performs a second IL patch over an existing OTAPI.dll and regenerates a new `OTAPI.Runtime.dll`, while keeping the original assembly names for compatibility with tooling and habits. |
| 29 | + |
| 30 | + |
| 31 | +## Install via NuGet (recommended) |
| 32 | +Use the published package `OTAPI.USP` which includes both `OTAPI.dll` and `OTAPI.Runtime.dll`. |
| 33 | + |
| 34 | +``` |
| 35 | +cd YourProject |
| 36 | +dotnet add package OTAPI.USP |
| 37 | +``` |
| 38 | + |
| 39 | +After restore/build, the assemblies are available from NuGet; you don’t need to build USP locally. If you do build USP, note that `OTAPI.dll` and `OTAPI.Runtime.dll` are emitted to the build configuration’s `output` folder under the project bin. |
| 40 | + |
| 41 | +## Build from Source (secondary) |
| 42 | +``` |
| 43 | +cd OTAPI.UnifiedServerProcess |
| 44 | +dotnet restore src/OTAPI.UnifiedServerProcess.sln |
| 45 | +dotnet build src/OTAPI.UnifiedServerProcess.sln -c Release |
| 46 | +dotnet run -c Debug -p src/OTAPI.UnifiedServerProcess |
| 47 | +# Output: src/OTAPI.UnifiedServerProcess/bin/Debug/net9.0/output/OTAPI.dll (+ OTAPI.Runtime.dll) |
| 48 | +``` |
| 49 | + |
| 50 | + |
| 51 | +## Core Concepts |
| 52 | +### Root Context |
| 53 | +- The per‑server root object. Systems that used to be static on Terraria (e.g., `Main`, `Netplay`, `NetMessage`) are accessed through the context instance. |
| 54 | +### Context-Aware Hooks |
| 55 | +- On.* hooks operate with or within the context. Signatures and targets may differ from older OTAPI. |
| 56 | +### Multi-Server Isolation |
| 57 | +- Avoid direct static access to `Terraria.*` when correctness across multiple servers matters. Prefer context-bound access to maintain isolation. |
| 58 | + |
| 59 | + |
| 60 | +## Creating and Using a Context |
| 61 | +Minimal example based on `ServerContext`: |
| 62 | +```csharp |
| 63 | +using UnifiedServerProcess; |
| 64 | +using Terraria; |
| 65 | + |
| 66 | +public class MyServer : RootContext |
| 67 | +{ |
| 68 | + public MyServer(string name, string worldPath) : base(name) |
| 69 | + { |
| 70 | + Main.ActiveWorldFileData = WorldFile.GetAllMetadata(worldPath, false); |
| 71 | + Main.maxNetPlayers = byte.MaxValue; |
| 72 | + Netplay.ListenPort = -1; // Let a global router own the port if needed |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | +// Create and use the context |
| 77 | +var ctx = new MyServer("World-A", "/path/to/World-A.wld"); |
| 78 | +ctx.Console.WriteLine($"[{ctx.Name}] server booting..."); |
| 79 | +// Access instance‑bound systems through the context |
| 80 | +ctx.Main.gameMode = 3; |
| 81 | +``` |
| 82 | + |
| 83 | +Migrating static access: |
| 84 | +```csharp |
| 85 | +// Before |
| 86 | +Terraria.Main.player[i].active = true; |
| 87 | +Terraria.NetMessage.SendData(25, -1, -1, NetworkText.Empty); |
| 88 | + |
| 89 | +// After (context‑bound) |
| 90 | +ctx.Main.player[i].active = true; |
| 91 | +ctx.NetMessage.SendData(25, -1, -1, Terraria.Localization.NetworkText.Empty); |
| 92 | +``` |
| 93 | + |
| 94 | + |
| 95 | +### World Data: TileProvider |
| 96 | +USP replaces legacy `ITile`/`Tile` usage with `TileData` (struct), `RefTileData` (ref‑like holder), and a context‑bound `TileCollection`. |
| 97 | + |
| 98 | +### Ref-Friendly Iteration: |
| 99 | +```csharp |
| 100 | +// Iterate tiles with ref access to minimize copies |
| 101 | +var tiles = ctx.Main.tile; // TileCollection |
| 102 | +
|
| 103 | +for (int x = 0; x < ctx.Main.maxTilesX; x++) |
| 104 | +for (int y = 0; y < ctx.Main.maxTilesY; y++) |
| 105 | +{ |
| 106 | + ref TileData tile = ref tiles[x, y] // ref TileData |
| 107 | + if (tile.active) { |
| 108 | + tile.type = 1; // Example write |
| 109 | + } |
| 110 | +} |
| 111 | +``` |
| 112 | + |
| 113 | +Passing stable handles across APIs: |
| 114 | +```csharp |
| 115 | +Tuple<int, int, RefTileData> heapStorage = new(0, 0, default) // heap object |
| 116 | +heapStorage.Item1 = 100; |
| 117 | +heapStorage.Item2 = 200; |
| 118 | +heapStorage.Item3 = ctx.Main.tile.GetRefTile(100, 200); // storage reference on heap safety |
| 119 | +
|
| 120 | +ref tile = ref heapStorage.Item3.Data; // RefTileData -> TileData |
| 121 | +``` |
| 122 | + |
| 123 | +Reference: `src/OTAPI.UnifiedServerProcess/Mods/TileProviderMod.cs` |
| 124 | + |
| 125 | + |
| 126 | +## Protocol: TrProtocol (IL-Merged) |
| 127 | +USP IL‑merges `TrProtocol` into the server core and merges public type metadata with OTAPI types. This enables seamless serialization for shared models. For example, `Terraria.DataStructures.PlayerDeathReason` gains generated read/write that operate directly on unmanaged pointers. |
| 128 | + |
| 129 | +- Read/Write behavior |
| 130 | +- The pack entities provide basic pointer-based deserialization: `void ReadContent(ref void* ptr)` and `void WriteContent(ref void* ptr)`. |
| 131 | +- For a packet instance, the packet type is fixed. Therefore deserialization begins from the position dictated by the type metadata: normal packets read starting at the 2-byte length + 1-byte type header; Module packets read from 2-byte length + 1-byte type + 2-byte ModuleType. |
| 132 | +- WriteContent writes the type information into the buffer as part of serialization. |
| 133 | + |
| 134 | +### Serialize/Deserialize Example |
| 135 | +```csharp |
| 136 | +unsafe |
| 137 | +{ |
| 138 | + byte* buf = stackalloc byte[256]; |
| 139 | + void* ptr = buf + 2; |
| 140 | + |
| 141 | + var p = new TrProtocol.NetPackets.SpawnPlayer(_PlayerSlot: 1, ...); |
| 142 | + p.WriteContent(ref ptr); |
| 143 | + short len = (short)((byte*)ptr - buf); |
| 144 | + *((short*)buf) = len; |
| 145 | + |
| 146 | + void* rptr = buf + 2; |
| 147 | + var p2 = new TrProtocol.NetPackets.SpawnPlayer(ref rptr); // |
| 148 | +
|
| 149 | + void* rptr = buf + 2; |
| 150 | + var p3 = new TrProtocol.NetPackets.SpawnPlayer(); // |
| 151 | + p3.ReadContent(ref rptr); |
| 152 | +} |
| 153 | +``` |
| 154 | +- ILengthAware and IExtraData |
| 155 | + - ILengthAware packets know their binary length and thus participate in contexts where trailing or compressed data exists (e.g., `ExtraData`). Such packets explicitly implement `ReadContent(ref void* ptr, void* end_ptr)` to read up to the end boundary, and `WriteContent(ref void* ptr)` to write payloads. The plain `ReadContent(ref void* ptr)` API is hidden via explicit interface implementation to avoid surface API that ignores the boundary. |
| 156 | + - IExtraData extends ILengthAware and adds an `ExtraData` byte[] property. The generator will add this property on types that implement `IExtraData` and handle copying any remaining bytes into it during deserialization. |
| 157 | + - For example, `TileSection` (which uses compression) relies on knowing its data range and thus participates in the length-aware path. |
| 158 | + - These rules are enforced by the generated serializers. They explicitly hide the non-boundary `ReadContent` via explicit implementation like: |
| 159 | + ``` csharp |
| 160 | + /// <summary> |
| 161 | + /// This operation is not supported and always throws a System.NotSupportedException. |
| 162 | + /// </summary> |
| 163 | + [Obsolete] |
| 164 | + void IBinarySerializable.ReadContent(ref void* ptr) => throw new NotSupportedException(); |
| 165 | + ``` |
| 166 | + |
| 167 | + // Public surface |
| 168 | + void ReadContent(ref void* ptr, void* end_ptr); |
| 169 | +- ISideSpecific and server/client roles |
| 170 | + - Data packets implementing ISideSpecific require the correct execution role before de/serialization. The IsServerSide property must reflect the current context; there is no extra parameter for ReadContent/WriteContent to indicate the role. Some packets use attributes such as [A2BOnly] to designate end-specific logic. |
| 171 | + - NetTextModule demonstrates this with two fields: |
| 172 | + ``` csharp |
| 173 | + [C2SOnly] |
| 174 | + public TextC2S? TextC2S; |
| 175 | + [S2COnly] |
| 176 | + public TextS2C? TextS2C; |
| 177 | + ``` |
| 178 | + - Developers manually ensure `IsServerSide` is set correctly on ISideSpecific packets. The source generator does not modify that behavior. |
| 179 | + |
| 180 | +- Constructors and ergonomics |
| 181 | + - Packets expose convenient constructors generated by the source generator. The simple constructor maps to the public fields/properties (a direct one-to-one assignment). |
| 182 | + - For ISideSpecific packets, an additional constructor variant accepts a boolean `isServerSide` to set `IsServerSide` accordingly when desired. |
| 183 | + - If a type uses `InitDefaultValue` on fields, the generator will also provide a second constructor that prioritizes parameter order and default values, easing usage for common initialization. |
| 184 | + - Example: WorldData |
| 185 | + ```csharp |
| 186 | + public partial struct WorldData : INetPacket |
| 187 | + { |
| 188 | + public readonly MessageID Type => MessageID.WorldData; |
| 189 | + [InitDefaultValue] public int Time; |
| 190 | + [InitDefaultValue] public BitsByte DayAndMoonInfo; |
| 191 | + [InitDefaultValue] public byte MoonPhase; |
| 192 | + public short MaxTileX; |
| 193 | + public short MaxTileY; |
| 194 | + public short SpawnX; |
| 195 | + public short SpawnY; |
| 196 | + // … |
| 197 | + } |
| 198 | + ``` |
| 199 | + The generator will produce two constructors, for example: |
| 200 | + - `public WorldData(int _Time, BitsByte _DayAndMoonInfo, byte _MoonPhase, short _MaxTileX, ...)` and |
| 201 | + - `public WorldData(short _MaxTileX, short _MaxTileY, ..., int _Time = default, BitsByte _DayAndMoonInfo = default, byte _MoonPhase = default, ...)` |
| 202 | + to simplify user code. |
| 203 | + |
| 204 | +- Example usage remains valid, with the generated constructors reducing boilerplate. |
| 205 | + |
| 206 | +## Collision (pre-patch ref parameters for performance) |
| 207 | +`PatchCollision` transforms `Terraria.Collision` static fields (e.g., `up`, `down`, `left`, `right`, etc.) into method `ref` parameters and updates all call sites. These fields were temporary variables and outputs; rewriting them early avoids deep context field dereferences and improves performance (stack allocation, clearer data flow, fewer indirections). Because of this, Collision internals are no longer runtime‑modified fields and are not converted into context instances. |
| 208 | + |
| 209 | +Usage pattern (illustrative): |
| 210 | + |
| 211 | +```csharp |
| 212 | +using Microsoft.Xna.Framework; |
| 213 | +using Terraria; |
| 214 | + |
| 215 | +float up = 0, down = 0, left = 0, right = 0; |
| 216 | +var vel = new Vector2(4, 0); |
| 217 | +var pos = new Vector2(100, 200); |
| 218 | + |
| 219 | +// Signatures vary by method; pass ref outputs as required |
| 220 | +// e.g., Collision.TileCollision(pos, vel, width, height, fallThrough, canJump, gravDir, ref up, ref down, ref left, ref right); |
| 221 | +``` |
| 222 | + |
| 223 | +Reference: `src/OTAPI.UnifiedServerProcess/Core/PatchCollision.cs` |
| 224 | + |
| 225 | +## Networking in Multi‑Server: GlobalNetwork |
| 226 | +`GlobalNetwork/Network/Router` demonstrates shared sockets and per‑client routing to the correct server: |
| 227 | +- Global arrays: `Router.globalClients`, `Router.globalMsgBuffers` are shared. |
| 228 | +- Per‑client context: `GetClientCurrentlyServer(i)` returns the owning context. |
| 229 | +- Scheduling: Only processes messages for clients belonging to the current server’s context. |
| 230 | +- Transfer: `Router.ServerTransfer(byte plr, ServerContext to)` moves a player by swapping `Main.player[plr]`, resetting sections, and sending join/leave sync. |
| 231 | + |
| 232 | +Reference: `src/OTAPI.UnifiedServerProcess.GlobalNetwork/Network/Router.cs`, `src/OTAPI.UnifiedServerProcess.GlobalNetwork/Servers/ServerContext.cs` |
| 233 | + |
| 234 | + |
| 235 | +## Hooks and Extensions |
| 236 | +- Runtime hooks are in `OTAPI.Runtime.dll` (On.* style), targeting context‑bound or updated systems. Review signatures against your USP build. |
| 237 | +- Prefer hooking the context versions over legacy static targets. |
| 238 | + |
| 239 | + |
| 240 | +## Compatibility Note (USP vs. TSAPI/TShock) |
| 241 | +USP’s context model is incompatible with frameworks that depend on global static state and legacy tile/network APIs. Porting requires: |
| 242 | +- Rewriting static accesses to context members |
| 243 | +- Updating hook targets/signatures to the context versions |
| 244 | +- Migrating world/tile interactions to `TileData`/`RefTileData`/`TileCollection` |
| 245 | +- Adapting to USP’s pruned server execution paths |
| 246 | + |
| 247 | + |
| 248 | +## Next Steps |
| 249 | +- Explore the demo in `src/OTAPI.UnifiedServerProcess.GlobalNetwork`. |
| 250 | +- Design APIs that pass or encapsulate `RootContext` explicitly. |
| 251 | +- Use TrProtocol for custom packets and TileProvider for world operations to maximize performance and clarity. |
0 commit comments