Skip to content

Commit 62a6331

Browse files
committed
feat: nuget packer
1 parent c9b9eac commit 62a6331

File tree

13 files changed

+238
-26
lines changed

13 files changed

+238
-26
lines changed

docs/OTAPI.USP.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# OTAPI Unified Server Process
2+
3+
**OTAPI.USP** is a secondary IL patch built on top of [OTAPI](https://github.com/SignatureBeef/Open-Terraria-API), enabling **multiple isolated Terraria server instances to run within a single process**.
4+
5+
Unlike standard OTAPI builds, this package introduces a **breaking change** by redirecting access to static fields within the Terraria server to their **instance-scoped equivalents**. This change allows each server instance to operate in a fully isolated execution context—enabling true multi-server concurrency within a single host process.
6+
7+
## Key Features
8+
9+
- 🧩 Built on top of OTAPI (IL-patched `TerrariaServer.exe`)
10+
- ⚠️ Breaking change: static server state is redirected to instance-level context
11+
- 🧵 Supports multiple Terraria server instances running in parallel threads
12+
- 💡 Enables advanced hosting models (e.g., high-density server clusters, custom orchestration, dynamic scaling)
13+
14+
## Compatibility
15+
16+
- Terraria: **1.4.4.9**
17+
- OTAPI: `[INJECT_OTAPI_VERSION]`
18+
19+
## Important Notes
20+
21+
- This package is not drop-in compatible with standard OTAPI mods.
22+
- Due to static field redirection, code that relies on traditional static access (e.g., `Main.time`, `Netplay`) must be adapted to use the appropriate instance context.
23+
- For mod development or integration, consult the API documentation or source examples on GitHub.
24+
25+
## Licensing
26+
27+
This package follows the licensing terms of OTAPI and the original Terraria server executable. See `COPYING.txt` for details.
28+

docs/OTAPI.USP.nuspec

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0"?>
2+
<package >
3+
<metadata>
4+
<id>OTAPI.USP</id>
5+
<title>OTAPI Unified Server Process</title>
6+
<version>[INJECT_VERSION]</version>
7+
<authors>CedaryCat</authors>
8+
<owners>CedaryCat</owners>
9+
<license type="file">COPYING.txt</license>
10+
<projectUrl>https://github.com/CedaryCat/UnifiedServerProcess</projectUrl>
11+
<requireLicenseAcceptance>false</requireLicenseAcceptance>
12+
<description>
13+
An additional IL patch layered on top of OTAPI, enabling multiple Terraria server instances to run concurrently within a single process.
14+
</description>
15+
<summary>Unified Server Process [INJECT_VERSION] on Open Terraria API [INJECT_OTAPI_VERSION] - Terraria 1.4.4.9[INJECT_GIT_HASH]</summary>
16+
<releaseNotes>
17+
Preliminary Terraria 1.4.4.9
18+
</releaseNotes>
19+
<copyright>Copyright 2025-[INJECT_YEAR]</copyright>
20+
<tags>Terraria,OTAPI,MultiServer</tags>
21+
[INJECT_DEPENDENCIES]
22+
</metadata>
23+
</package>
24+

src/Directory.Build.props

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<Project>
2+
<ItemGroup Condition="'$(DisableGitVersion)' != 'true'">
3+
<PackageReference Include="GitVersion.MSBuild" Version="5.12.0" PrivateAssets="All" />
4+
</ItemGroup>
5+
</Project>

src/OTAPI.UnifiedServerProcess.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
Microsoft Visual Studio Solution File, Format Version 12.00
23
# Visual Studio Version 17
34
VisualStudioVersion = 17.14.35906.104
@@ -16,6 +17,17 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrProtocol.SerializerGenera
1617
EndProject
1718
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrProtocol", "TrProtocol\TrProtocol.csproj", "{F252FFD8-5DE7-DAA8-BA3D-341C543C73EE}"
1819
EndProject
20+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "解决方案项", "解决方案项", "{9FA3D6BD-1EC1-3BA5-80CB-CE02773A58D5}"
21+
ProjectSection(SolutionItems) = preProject
22+
Directory.Build.props = Directory.Build.props
23+
EndProjectSection
24+
EndProject
25+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
26+
ProjectSection(SolutionItems) = preProject
27+
..\docs\OTAPI.USP.md = ..\docs\OTAPI.USP.md
28+
..\docs\OTAPI.USP.nuspec = ..\docs\OTAPI.USP.nuspec
29+
EndProjectSection
30+
EndProject
1931
Global
2032
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2133
Debug|Any CPU = Debug|Any CPU
@@ -50,6 +62,9 @@ Global
5062
GlobalSection(SolutionProperties) = preSolution
5163
HideSolutionNode = FALSE
5264
EndGlobalSection
65+
GlobalSection(NestedProjects) = preSolution
66+
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {9FA3D6BD-1EC1-3BA5-80CB-CE02773A58D5}
67+
EndGlobalSection
5368
GlobalSection(ExtensibilityGlobals) = postSolution
5469
SolutionGuid = {A7CFB05D-AB46-4256-92A6-C8723719BBD5}
5570
EndGlobalSection

src/OTAPI.UnifiedServerProcess/Extensions/ModContextExt.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,14 @@
33

44
namespace OTAPI.UnifiedServerProcess.Extensions
55
{
6-
[MonoMod.MonoModIgnore]
76
public static class ModContextExt
87
{
9-
[MonoMod.MonoModIgnore]
108
public static string ExtractResources(this ModContext modcontext, string fileinput) {
119
var dir = GetEmbeddedResourcesDirectory(modcontext, fileinput);
1210
var extractor = new ResourceExtractor();
1311
var embeddedResourcesDir = extractor.Extract(fileinput, dir);
1412
return embeddedResourcesDir;
1513
}
16-
[MonoMod.MonoModIgnore]
1714
public static string GetEmbeddedResourcesDirectory(ModContext modcontext, string fileinput) {
1815
return Path.Combine(modcontext.BaseDirectory, Path.GetDirectoryName(fileinput)!);
1916
}

src/OTAPI.UnifiedServerProcess/Extensions/ModFwModderExt.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
using ModFramework;
22
using ModFramework.Relinker;
33
using Mono.Cecil;
4+
using System;
45
using System.IO;
56

67
namespace OTAPI.UnifiedServerProcess.Extensions
78
{
8-
[MonoMod.MonoModIgnore]
99
public static class ModFwModderExt
1010
{
11-
[MonoMod.MonoModIgnore]
1211
public static void CreateRuntimeHooks(this ModFwModder modder, string output) {
1312
modder.Log("[OTAPI-ProC] Generating OTAPI.Runtime.dll");
1413
var gen = new MonoMod.RuntimeDetour.HookGen.HookGenerator(modder, "OTAPI.Runtime.dll");
@@ -41,5 +40,16 @@ public static void CreateRuntimeHooks(this ModFwModder modder, string output) {
4140

4241
mm.Write();
4342
}
43+
44+
public static void AddEnvMetadata(this ModFwModder modder) {
45+
var commitSha = Utilities.GetGitCommitSha();
46+
var run = Environment.GetEnvironmentVariable("GITHUB_RUN_NUMBER")?.Trim();
47+
48+
if (!string.IsNullOrWhiteSpace(commitSha))
49+
modder.AddMetadata("GitHub.Commit", commitSha);
50+
51+
if (!string.IsNullOrWhiteSpace(run))
52+
modder.AddMetadata("GitHub.Action.RunNo", run);
53+
}
4454
}
4555
}

src/OTAPI.UnifiedServerProcess/ModAssemblyMerger.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,20 @@ namespace OTAPI.UnifiedServerProcess
1313
{
1414
public class ModAssemblyMerger
1515
{
16-
public ModAssemblyMerger(ModContext context, params System.Reflection.Assembly[] mods) {
17-
18-
Dictionary<string, ModuleDefinition> modModules = [];
19-
16+
readonly Dictionary<string, ModuleDefinition> modModules = [];
17+
public ModAssemblyMerger(params System.Reflection.Assembly[] mods) {
18+
foreach (var assembly in mods) {
19+
var mod = AssemblyDefinition.ReadAssembly(assembly.Location);
20+
modModules.TryAdd(mod.FullName, mod.MainModule);
21+
}
22+
}
23+
public void Attach(ModContext context) {
2024
context.OnApply += (progress, modder) => {
2125
if (modder is null) {
2226
return ModContext.EApplyResult.Continue;
2327
}
2428
var module = modder.Module;
2529
if (progress == ModType.PreRead) {
26-
foreach (var assembly in mods) {
27-
var mod = AssemblyDefinition.ReadAssembly(assembly.Location);
28-
modModules.TryAdd(mod.FullName, mod.MainModule);
29-
}
3030
}
3131
else if (progress == ModType.PrePatch) {
3232
var modderTypes = module.GetAllTypes().ToDictionary(x => x.FullName, x => x);
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using ModFramework;
2+
using OTAPI.UnifiedServerProcess.Core.Patching.Framework;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Reflection;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
11+
namespace OTAPI.UnifiedServerProcess
12+
{
13+
public class NugetPackageBuilder(string packageName, string nugetDocPath)
14+
{
15+
string GetNugetVersionFromAssembly(Assembly assembly)
16+
=> assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion;
17+
18+
string GetNugetVersionFromAssembly<TType>()
19+
=> GetNugetVersionFromAssembly(typeof(TType).Assembly);
20+
21+
public readonly string PackageFileName = packageName + ".nupkg";
22+
public readonly string PackageMDFilePath = Path.Combine(nugetDocPath, packageName + ".md");
23+
public readonly string NuspecFilePath = Path.Combine(nugetDocPath, packageName + ".nuspec");
24+
25+
public void Build(ModFwModder modder, string otapiVersion, DirectoryInfo outputDir) {
26+
var nuspec_xml = File.ReadAllText(NuspecFilePath);
27+
var md = File.ReadAllText(PackageMDFilePath);
28+
29+
md = md.Replace("[INJECT_OTAPI_VERSION]", otapiVersion);
30+
nuspec_xml = nuspec_xml.Replace("[INJECT_OTAPI_VERSION]", otapiVersion);
31+
32+
var version = GetNugetVersionFromAssembly<Patcher>();
33+
var gitIndex = version.IndexOf('+');
34+
if (gitIndex > -1) {
35+
var gitCommitSha = version[(gitIndex + 1)..];
36+
version = version[..gitIndex];
37+
nuspec_xml = nuspec_xml.Replace("[INJECT_GIT_HASH]", $" git#{gitCommitSha}");
38+
}
39+
else {
40+
nuspec_xml = nuspec_xml.Replace("[INJECT_GIT_HASH]", "");
41+
}
42+
nuspec_xml = nuspec_xml.Replace("[INJECT_VERSION]", version);
43+
44+
var platforms = new[] { "net9.0" };
45+
var steamworks = modder.Module.AssemblyReferences.First(x => x.Name == "Steamworks.NET");
46+
var newtonsoft = modder.Module.AssemblyReferences.First(x => x.Name == "Newtonsoft.Json");
47+
var dependencies = new[]
48+
{
49+
(typeof(ModFwModder).Assembly.GetName().Name, Version: GetNugetVersionFromAssembly<ModFwModder>()),
50+
(typeof(MonoMod.MonoModder).Assembly.GetName().Name, Version: typeof(MonoMod.MonoModder).Assembly.GetName().Version!.ToString()),
51+
(typeof(MonoMod.RuntimeDetour.Hook).Assembly.GetName().Name, Version: typeof(MonoMod.RuntimeDetour.Hook).Assembly.GetName().Version!.ToString()),
52+
(steamworks.Name, Version: steamworks.Version.ToString()),
53+
(newtonsoft.Name, Version: GetNugetVersionFromAssembly<Newtonsoft.Json.JsonConverter>().Split('+')[0] ),
54+
};
55+
56+
var xml_dependency = string.Join("", dependencies.Select(dep => $"\n\t <dependency id=\"{dep.Name}\" version=\"{dep.Version}\" />"));
57+
var xml_group = string.Join("", platforms.Select(platform => $"\n\t<group targetFramework=\"{platform}\">{xml_dependency}\n\t</group>"));
58+
var xml_dependencies = $"<dependencies>{xml_group}\n </dependencies>";
59+
60+
nuspec_xml = nuspec_xml.Replace("[INJECT_DEPENDENCIES]", xml_dependencies);
61+
62+
nuspec_xml = nuspec_xml.Replace("[INJECT_YEAR]", DateTime.UtcNow.Year.ToString());
63+
64+
File.WriteAllText(Path.Combine(outputDir.FullName, "COPYING.txt"), File.ReadAllText("../../../../../LICENSE"));
65+
File.WriteAllText(packageName + ".md", md);
66+
67+
using (var nuspec = new MemoryStream(Encoding.UTF8.GetBytes(nuspec_xml))) {
68+
var manifest = NuGet.Packaging.Manifest.ReadFrom(nuspec, validateSchema: true);
69+
var packageBuilder = new NuGet.Packaging.PackageBuilder();
70+
packageBuilder.Populate(manifest.Metadata);
71+
72+
packageBuilder.AddFiles(outputDir.FullName, "COPYING.txt", "");
73+
74+
foreach (var platform in platforms) {
75+
var dest = Path.Combine("lib", platform);
76+
packageBuilder.AddFiles(outputDir.FullName, "OTAPI.dll", dest);
77+
packageBuilder.AddFiles(outputDir.FullName, "OTAPI.Runtime.dll", dest);
78+
}
79+
80+
if (File.Exists(PackageFileName))
81+
File.Delete(PackageFileName);
82+
83+
using (var srm = File.OpenWrite(PackageFileName)) {
84+
packageBuilder.Save(srm);
85+
}
86+
}
87+
}
88+
}
89+
}

src/OTAPI.UnifiedServerProcess/PatchExecutor.cs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Collections.Generic;
1111
using System.IO;
1212
using System.Linq;
13+
using System.Reflection;
1314
using System.Runtime.InteropServices;
1415

1516
namespace OTAPI.UnifiedServerProcess
@@ -58,17 +59,19 @@ public static void PatchMonoMod() {
5859
}
5960
}
6061

61-
public bool Patch() {
62-
DirectoryInfo outputDir = new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "output"));
62+
public bool Patch(DirectoryInfo outputDir) {
6363
outputDir.Create();
6464
var output = Path.Combine(outputDir.FullName, "OTAPI.dll");
6565
var hookOutput = Path.Combine(outputDir.FullName, "OTAPI.Runtime.dll");
6666

6767
var input = typeof(Terraria.Main).Assembly.Location;
68+
var version = typeof(Terraria.Main).Assembly
69+
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
70+
?.InformationalVersion ?? throw new NullReferenceException();
6871

6972
var modcontext = new ModContext("Terraria");
70-
modcontext.ReferenceFiles.AddRange(new[]
71-
{
73+
modcontext.ReferenceFiles.AddRange(
74+
[
7275
"ModFramework.dll",
7376
"MonoMod.dll",
7477
"MonoMod.Utils.dll",
@@ -79,18 +82,18 @@ public bool Patch() {
7982
"Steamworks.NET.dll",
8083
input,
8184
typeof(Program).Assembly.Location,
82-
});
85+
]);
8386

8487
using ModFwModder mm = new(modcontext) {
8588
InputPath = input,
8689
OutputPath = output,
8790
MissingDependencyThrow = false,
8891
PublicEverything = false,
8992
LogVerboseEnabled = true,
90-
GACPaths = new string[] { }, // avoid MonoMod looking up the GAC, which causes an exception on .netcore
93+
GACPaths = [], // avoid MonoMod looking up the GAC, which causes an exception on .netcore
9194
};
9295

93-
List<MethodDefinition> virtualMaked = new List<MethodDefinition>();
96+
List<MethodDefinition> virtualMaked = [];
9497

9598
var embeddedResources = modcontext.ExtractResources(input);
9699

@@ -100,7 +103,8 @@ public bool Patch() {
100103

101104
var logger = new DefaultLogger(Logger.DEBUG);
102105

103-
_ = new ModAssemblyMerger(modcontext, typeof(TrProtocol.MessageID).Assembly);
106+
new ModAssemblyMerger(typeof(TrProtocol.MessageID).Assembly)
107+
.Attach(modcontext);
104108

105109
modcontext.OnApply += (modType, modder) => {
106110

@@ -114,10 +118,14 @@ public bool Patch() {
114118
else if (modType == ModType.PreWrite) {
115119
PatchingLogic.Patch(logger, modder.Module);
116120
modder.ModContext.TargetAssemblyName = "OTAPI";
121+
modder.AddEnvMetadata();
117122
}
118123
else if (modType == ModType.Write) {
119124
modder.ModContext = new("OTAPI.Runtime");
120125
modder.CreateRuntimeHooks(hookOutput);
126+
127+
new NugetPackageBuilder("OTAPI.USP", "../../../../../docs")
128+
.Build(modder, version, outputDir);
121129
}
122130
}
123131

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
namespace OTAPI.UnifiedServerProcess
1+
using System;
2+
using System.IO;
3+
using System.Reflection;
4+
5+
namespace OTAPI.UnifiedServerProcess
26
{
37
internal class Program
48
{
59
static void Main(string[] args) {
6-
new PatchExecutor().Patch();
10+
var outputDir = new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "output"));
11+
new PatchExecutor().Patch(outputDir);
712
}
813
}
914
}

0 commit comments

Comments
 (0)