Terbin is a three-project C# solution (Documentation Index
Fetch the complete documentation index at: https://mintlify.com/FarlandsModdingTeam/TerbinProyect/llms.txt
Use this file to discover all available pages before exploring further.
TerbinProyect.sln). Each project has a single, well-scoped responsibility: TerbinLibrary supplies all reusable infrastructure, TerbinService uses that infrastructure to run a long-lived mod-management daemon, and SimulateClient exercises the service interactively during development. Understanding where each piece lives — and why — makes it straightforward to extend the system with new commands, new managers, or new clients.
Project Breakdown
TerbinLibrary
TerbinLibrary is the core SDK and the only project that the other two reference. It is organized into the following namespaces:-
TerbinLibrary.Communication—TerbinCommunicator, the top-level class every consumer instantiates. It owns theNamedPipeServerStreamorNamedPipeClientStream, aConcurrentQueue<PacketRequest>send queue, aSemaphoreSlimsend signal, and aConcurrentDictionaryof pendingTaskCompletionSource<PacketRequest>entries for in-flightCommunicate()calls. Background tasks for sending and receiving run continuously afterConnect()or after the first client connects. -
TerbinLibrary.Communication.Packets— The wire types:Header— An unmanaged[StructLayout(LayoutKind.Sequential, Pack = 1)]struct carryingIdRequest(ushort),OrderRequest(ushort),Status(CodeStatus), andIdMemory(byte).PacketRequest— The full packet: aHeader, anIdArrayaction key, and abyte[]payload. ImplementsIStructSerializablefor zero-copy binary serialization over the pipe.IdArray— A variable-length byte array that identifies the command to dispatch. ImplementsIEquatable<IEnumerable<byte>>and provides content-basedGetHashCodeso it works correctly as aConcurrentDictionarykey.InfoResponse— A lightweight response struct withIdRequest,Status,ActionMethod, andPayload. Use the static factory methods (CreateSucces,Create,CreateInteralError,CreateCancelled) to build responses in handlers.
-
TerbinLibrary.Protocol—TerbinProtocol(protocol constants),CodeStatus(HTTP-inspired status codes, e.g.Succes = 200,Execute = 300,ActionNotFound = 440),CodeTerbinProtocol(built-in action bytes 0–9:Stop,Response,Load,Prolong,Solicit,ExceptionAlert), andCodeTerbinMemory. -
TerbinLibrary.Execution— The reflection-based dispatch pipeline:TerbinExecutableAttribute— Marks a handler method with one or more action bytes.TerbinExecutableHelper— Validates method signatures (Header,byte[],CancellationToken→Task<InfoResponse?>) and performs the assembly scan.ExecutableDispatcher— Thread-safe dispatcher backed by aConcurrentDictionary<IEquatable<IEnumerable<byte>>, List<TerbinExecutableDelegate>>.TerbinExecutableManager— Static façade overExecutableDispatcherprovidingRegister,Unregister,DispatchAsync, andRegisterFromAssembly.TerbinExecutor— Bootstrapper that callsRegisterInternal()(registering the library’s own built-in handlers:Load,Solicit,Prolong,Response) and exposesRegister(Assembly)for application-level handlers.
-
TerbinLibrary.Memory— Three classes cooperate for large-payload fragmentation:TerbinMemory— An individual slot that holds an ordered collection of byte fragments and can reassemble them into a complete payload.TerbinMemoryManager— A static registry ofTerbinMemoryslots, keyed bybyteID, withGetFreeStore,Store,OverwriteStore,TryGetResult, andReleaseoperations.TerbinMemoryHelper— Stateless helpers invoked byTerbinCommunicator.handleReceiveto detect whether an incoming packet belongs to a fragmented sequence (TryGetMemoryStream) and to release a slot after reassembly (TryReleaseMemory).
-
TerbinLibrary.Configuration—TerbinConfigurationholds the canonical config-key constants (RUTE_FARLANDS,RUTE_INSTANCES,RUTE_STORAGE_PLUGINS,RUTE_STEAM,RUTE_PROTON,RUTE_PROTON_TMP) used byManager.Configurationwhen reading and writingconfig.json. -
TerbinLibrary.Serialize— Binary serialization utilities:Serialineitor— Static helpers for serializing/deserializing primitives, arrays, and enums to/frombyte[].BufferWriter/BufferReader— Low-levelSpan<byte>-backed helpers used byIStructSerializable.WriteToandReadFromimplementations.IStructSerializable— Interface (defined inSerialineitor.cs) implemented byPacketRequestandIdArray; requiresGetSize(),WriteTo(Span<byte>), andReadFrom(ReadOnlySpan<byte>).
-
TerbinLibrary(root) —CodeServices,CodeServicesSection,InternalErrors, and related enumerations that define the vocabulary for service/section routing and error reporting. (CodeStatusandTerbinErrorCodeare defined inTerbinLibrary.Protocol.)
TerbinService
TerbinService is the background daemon. It is a .NET Generic Host application with a single hosted service,Worker, that creates the named-pipe server and owns the application lifetime. Internally it is divided into three layers:
Managers own all business logic and mutable state. Each manager is a static class responsible for one domain:
| Manager | Responsibility |
|---|---|
ManagerPlugin | Download, install, uninstall, and query BepInEx plugins |
ManagerInstances | Create, register, duplicate, and remove Farlands mod instances |
ManagerGames | Track Farlands game installations and launch the game |
ManagerConfiguration | Read and write config.json (game path, instance path, plugin storage path) |
ManagerIndex | Maintain the manifest index across all registered instances |
ManagerManifest | Create, update, and remove per-instance and per-plugin manifest entries |
ManegerStoragePlugin | Manage the centralized plugin storage directory |
ManagerInjectors | Handle BepInEx injector installation and state |
ManagerNode | Manage node-based dependency trees |
[TerbinExecutable]-decorated static methods whose entire purpose is to deserialize pParameters, call the corresponding manager, and return an InfoResponse. No business logic lives inside a service:
| Service | Delegates to |
|---|---|
ServicePlugins | ManagerPlugin |
ServiceInstances | ManagerInstances |
ServiceGames | ManagerGames |
ServiceConfiguration | ManagerConfiguration |
ServicePluginStorage | ManegerStoragePlugin |
| Helper | Responsibility |
|---|---|
ManagerSteam / SteamLocator | Locate Steam installations and library paths |
ManagerProton | Detect and configure Proton compatibility layers |
ManagerFarlands | Launch the Farlands executable (wraps Steam/Proton paths) |
TerbinService/config/config.json. The three core keys (rute_farlands, rute_instances, rute_plugins) are written on first run if absent; the Steam and Proton keys are populated automatically when the service detects those installations:
TerbinLibrary.Configuration.TerbinConfiguration (RUTE_FARLANDS, RUTE_INSTANCES, RUTE_STORAGE_PLUGINS, RUTE_STEAM, RUTE_PROTON, RUTE_PROTON_TMP) and consumed by Manager.Configuration.GetConfg(string pKey).
File-system constants (manifest file names, BepInEx plugin paths) live in TerbinServiceConst:
SimulateClient
SimulateClient is a development console application. It connects to the running service over the sameTerbinPipe, registers its own [TerbinExecutable] handlers (so the server can push packets back to it), and presents an interactive loop that parses "ClassName -MethodName" commands and invokes the matching static method via reflection. It is the fastest way to exercise a new service method end-to-end without writing a dedicated test harness.
Data Flow
The following walk-through traces a singleCommunicate() call from client to server and back.
var action = new IdArray((byte)CodeServices.Execute, (byte)CodeServicesSection.Plugin);
PacketRequest response = await communicator.Communicate(action, payloadBytes);
Communicate() auto-generates a ushort request ID via MiniID.NewS, registers a TaskCompletionSource<PacketRequest> in _pendingRequests, and calls the internal send() method.HandleSendSigle enqueues a PacketRequest with OrderRequest = TerbinProtocol.ORDER_SINGLE (0).HandleSendFragment first sends a CheckExecution probe, then requests a memory slot from the server via SoliciteRequestMemory(), uploads the fragments one by one with Load(), and finally enqueues the last fragment with OrderRequest = TerbinProtocol.FINAL_PACKET (ushort.MaxValue).// TerbinLibrary/Communication/TerbinCommunicator.cs
private async Task<PacketRequest?> send(
IdArray pActionMethod, byte[] pPayload, CodeStatus pStatus, ushort pId)
{
PacketRequest? error = null;
if (pPayload.Length <= TerbinProtocol.MAX_PLD)
_ = HandleSendSigle(pActionMethod, pPayload, pId, pStatus);
else
error = await HandleSendFragment(pActionMethod, pPayload, pId, pStatus);
return error; // Returns null if everything is correct.
}
The background
manageSend task waits on _signal, dequeues the next PacketRequest, and writes it to the pipe via StreamWriteStruct.WriteAsycn<PacketRequest>(). The PacketRequest.WriteTo(Span<byte>) method serializes the Header, IdArray, and payload into a contiguous byte buffer.The server’s background
manageReceive task reads bytes from its end of the pipe via StreamReadStruct.ReadAsycn<PacketRequest>(). The deserialized PacketRequest is passed to handleReceive(), which calls TerbinMemoryHelper.TryGetMemoryStream(). If the packet has a non-zero IdMemory field, the helper looks up the matching TerbinMemory slot, retrieves all stored fragments in order, and returns the reassembled payload. Single-packet messages pass through unchanged.// TerbinService/Worker.cs
communicator.OnRecive += async (pCapsule) =>
{
CurrentContext.Value = new InfoLocalThreads { Communicator = communicator };
return await TerbinExecutableManager.DispatchAsync(
pCapsule.Head, pCapsule.Payload, pCapsule.ActionMethod);
};
DispatchAsync looks up pCapsule.ActionMethod in the dispatcher’s ConcurrentDictionary. If found, it checks pHead.Status:Execute (300) — normal execution path.CheckExecution (303) — returns InfoResponse.CreateSucces(pHead.IdRequest) immediately without running any handler logic. Used to verify a handler exists before sending a large fragmented payload.CancelByAction (305) — cancels the CancellationTokenSource associated with the action.ActionNotFound — returns InfoResponse.Create(pHead.IdRequest, CodeStatus.ActionNotFound).For normal execution,
TerbinExecutableHelper.ExecutionList runs all registered handlers for that action concurrently and returns the first non-null InfoResponse?.The
[TerbinExecutable]-decorated method returns an InfoResponse?. If non-null, handleReceive calls communicator.Reply(rCap.Value), which routes through send() with ActionMethod = CodeTerbinProtocol.Response (byte 1). The response packet travels back over the pipe to the client.On the client side,
manageReceive reads the response packet. Because its ActionMethod is CodeTerbinProtocol.Response, the built-in TerbinExecutor.Response handler is invoked:// TerbinLibrary/Execution/TerbinExecutor.cs
[TerbinExecutable((byte)CodeTerbinProtocol.Response)]
public static async Task<InfoResponse?> Response(
Header pHead, byte[] pParameters, CancellationToken pToken)
{
if (pToken.IsCancellationRequested) return null;
if (pHead.Status == CodeStatus.ExecutionException)
{
// Deserialize and print the remote exception; do not unblock any pending request.
ExceptionDTO dto = (ExceptionDTO)new ExceptionDTO().Read(pParameters);
Console.Error(dto.ToString());
return null;
}
_communicator?.GiveResponse(
new PacketRequest(pHead, new IdArray((byte)CodeTerbinProtocol.Response), pParameters));
return null;
}
Directory Structure
The manager/service split is intentional and important. Managers own the business logic and all mutable state — they are the authoritative source of truth for plugins, instances, game paths, and configuration. Services are pure IPC dispatch glue: they receive a
byte[] payload, deserialize it, call the right manager method, and return an InfoResponse. This separation means you can unit-test a manager without spinning up a pipe, and you can swap out the serialization format inside a service without touching any domain logic. When adding a new feature, always ask: does this logic belong in the manager (business rules, state) or in the service (IPC translation)?