gemwright 0.0.0-stage → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "gemwright",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "description": "Forge your own shard.",
4
+ "version": "0.1.3",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/gokaygurcan/gemwright.git"
8
+ },
9
+ "bin": {
10
+ "gemwright": "dist/index.js"
11
+ },
12
+ "type": "module",
13
+ "files": [
14
+ "dist/index.js",
15
+ "templates/",
16
+ "patches/"
17
+ ],
18
+ "engines": {
19
+ "node": ">=24"
20
+ },
21
+ "scripts": {
22
+ "start": "bun run src/index.ts",
23
+ "format": "biome format --write",
24
+ "typecheck": "bun check",
25
+ "test": "bun test",
26
+ "build": "bun build src/index.ts --target node --format esm --packages external --outfile dist/index.js",
27
+ "prepack": "bun run build"
28
+ },
29
+ "dependencies": {
30
+ "@clack/prompts": "1.8.1",
31
+ "chalk": "6.0.1"
32
+ },
33
+ "devDependencies": {
34
+ "@biomejs/biome": "2.5.15",
35
+ "@types/bun": "1.4.3",
36
+ "@types/node": "26.6.5",
37
+ "typescript": "7.0.2"
38
+ },
39
+ "packageManager": "bun@1.4.3"
40
+ }
@@ -0,0 +1,218 @@
1
+ diff --git a/Scripts/Accounting/AccountAttackLimiter.cs b/Scripts/Accounting/AccountAttackLimiter.cs
2
+ index 231f2d5..58e666a 100644
3
+ --- a/Scripts/Accounting/AccountAttackLimiter.cs
4
+ +++ b/Scripts/Accounting/AccountAttackLimiter.cs
5
+ @@ -23,6 +23,7 @@ namespace Server.Accounting
6
+ public static bool Throttle_Callback(byte packetID, NetState ns, out bool drop)
7
+ {
8
+ drop = false;
9
+ + if (HostingOptions.IsRailwayProxy(ns.Address)) return true;
10
+
11
+ InvalidAccountAccessLog accessLog = FindAccessLog(ns);
12
+
13
+ @@ -54,7 +55,7 @@ namespace Server.Accounting
14
+
15
+ public static void RegisterInvalidAccess(NetState ns)
16
+ {
17
+ - if (ns == null || !Enabled)
18
+ + if (ns == null || !Enabled || HostingOptions.IsRailwayProxy(ns.Address))
19
+ return;
20
+
21
+ InvalidAccountAccessLog accessLog = FindAccessLog(ns);
22
+ diff --git a/Scripts/Accounting/IPLimiter.cs b/Scripts/Accounting/IPLimiter.cs
23
+ index f9539ff..e3cfe30 100644
24
+ --- a/Scripts/Accounting/IPLimiter.cs
25
+ +++ b/Scripts/Accounting/IPLimiter.cs
26
+ @@ -20,6 +20,7 @@ namespace Server.Misc
27
+
28
+ public static bool IsExempt(IPAddress ip)
29
+ {
30
+ + if (HostingOptions.IsRailwayProxy(ip)) return true;
31
+ for (int i = 0; i < Exemptions.Length; i++)
32
+ {
33
+ if (ip.Equals(Exemptions[i]))
34
+ diff --git a/Scripts/Misc/DataPath.cs b/Scripts/Misc/DataPath.cs
35
+ index 65ddeff..4d830ac 100644
36
+ --- a/Scripts/Misc/DataPath.cs
37
+ +++ b/Scripts/Misc/DataPath.cs
38
+ @@ -17,7 +17,7 @@ namespace Server.Misc
39
+ * Example:
40
+ * private static string CustomPath = @"C:\Program Files\Ultima Online";
41
+ */
42
+ - private static readonly string CustomPath = Config.Get(@"DataPath.CustomPath", default(string));
43
+ + private static readonly string CustomPath = HostingOptions.ClientDataPath ?? Config.Get(@"DataPath.CustomPath", default(string));
44
+
45
+ static DataPath()
46
+ {
47
+ diff --git a/Scripts/Misc/ServerList.cs b/Scripts/Misc/ServerList.cs
48
+ index f9ee019..c1b7e90 100644
49
+ --- a/Scripts/Misc/ServerList.cs
50
+ +++ b/Scripts/Misc/ServerList.cs
51
+ @@ -43,7 +43,7 @@ namespace Server.Misc
52
+ address = Address;
53
+ }
54
+
55
+ - e.AddServer(ServerName, new IPEndPoint(address, ipep.Port));
56
+ + e.AddServer(ServerName, HostingOptions.PublicEndpoint ?? new IPEndPoint(address, ipep.Port));
57
+ }
58
+ catch
59
+ {
60
+ diff --git a/Server/HostingOptions.cs b/Server/HostingOptions.cs
61
+ new file mode 100644
62
+ index 0000000..489eef1
63
+ --- /dev/null
64
+ +++ b/Server/HostingOptions.cs
65
+ @@ -0,0 +1,89 @@
66
+ +using System;
67
+ +using System.Globalization;
68
+ +using System.IO;
69
+ +using System.Linq;
70
+ +using System.Net;
71
+ +using System.Net.Sockets;
72
+ +
73
+ +namespace Server
74
+ +{
75
+ + public static class HostingOptions
76
+ + {
77
+ + public static IPEndPoint PublicEndpoint { get; private set; }
78
+ + public static string ClientDataPath { get; private set; }
79
+ + public static string ShutdownFile { get; private set; }
80
+ + public static bool RailwayProxy { get; private set; }
81
+ +
82
+ + public static bool TryParse(string[] args, ref int index)
83
+ + {
84
+ + switch (args[index])
85
+ + {
86
+ + case "--public":
87
+ + PublicEndpoint = ParsePublicEndpoint(NextValue(args, ref index));
88
+ + return true;
89
+ + case "--client-data":
90
+ + ClientDataPath = Path.GetFullPath(NextValue(args, ref index));
91
+ + if (!Directory.Exists(ClientDataPath))
92
+ + throw new ArgumentException("Client data directory does not exist: " + ClientDataPath);
93
+ + return true;
94
+ + case "--shutdown-file":
95
+ + ShutdownFile = Path.GetFullPath(NextValue(args, ref index));
96
+ + if (!Directory.Exists(Path.GetDirectoryName(ShutdownFile)) || File.Exists(ShutdownFile))
97
+ + throw new ArgumentException("Shutdown file requires an existing parent directory and must not already exist.");
98
+ + return true;
99
+ + case "--railway-proxy":
100
+ + RailwayProxy = true;
101
+ + return true;
102
+ + default:
103
+ + return false;
104
+ + }
105
+ + }
106
+ +
107
+ + private static string NextValue(string[] args, ref int index)
108
+ + {
109
+ + var option = args[index];
110
+ + if (++index >= args.Length || String.IsNullOrWhiteSpace(args[index]) || args[index].StartsWith("-"))
111
+ + throw new ArgumentException("Missing value for " + option);
112
+ + return args[index];
113
+ + }
114
+ +
115
+ + public static IPEndPoint ParsePublicEndpoint(string value)
116
+ + {
117
+ + var separator = value.LastIndexOf(':');
118
+ + int port;
119
+ + if (separator <= 0 || !Int32.TryParse(value.Substring(separator + 1), NumberStyles.None,
120
+ + CultureInfo.InvariantCulture, out port) || port < 1 || port > 65535)
121
+ + throw new ArgumentException("--public requires an IPv4 address or hostname followed by :port (1-65535).");
122
+ +
123
+ + var host = value.Substring(0, separator);
124
+ + var address = Dns.GetHostAddresses(host).FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork);
125
+ + if (address == null)
126
+ + throw new ArgumentException("--public requires a hostname with an IPv4 address.");
127
+ + return new IPEndPoint(address, port);
128
+ + }
129
+ +
130
+ + public static bool IsRailwayProxy(IPAddress address)
131
+ + {
132
+ + if (!RailwayProxy || address == null) return false;
133
+ + if (address.IsIPv4MappedToIPv6) address = address.MapToIPv4();
134
+ + if (address.AddressFamily != AddressFamily.InterNetwork) return false;
135
+ + var bytes = address.GetAddressBytes();
136
+ + return bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127;
137
+ + }
138
+ +
139
+ + public static void WatchShutdown()
140
+ + {
141
+ + if (ShutdownFile == null) return;
142
+ + Timer.DelayCall(TimeSpan.FromMilliseconds(250), TimeSpan.FromMilliseconds(250), () =>
143
+ + {
144
+ + if (!File.Exists(ShutdownFile)) return;
145
+ + Console.WriteLine("Hosting: Shutdown requested; saving world before exit.");
146
+ + World.WaitForWriteCompletion();
147
+ + World.Save(true, false);
148
+ + World.WaitForWriteCompletion();
149
+ + File.Delete(ShutdownFile);
150
+ + Core.Kill();
151
+ + });
152
+ + }
153
+ + }
154
+ +}
155
+ diff --git a/Server/Main.cs b/Server/Main.cs
156
+ index 8230c6b..057d10a 100644
157
+ --- a/Server/Main.cs
158
+ +++ b/Server/Main.cs
159
+ @@ -335,8 +335,10 @@ namespace Server
160
+ AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
161
+ AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;
162
+
163
+ - foreach (string a in args)
164
+ + for (int i = 0; i < args.Length; i++)
165
+ {
166
+ + if (HostingOptions.TryParse(args, ref i)) continue;
167
+ + var a = args[i];
168
+ if (Insensitive.Equals(a, "-debug"))
169
+ {
170
+ Debug = true;
171
+ @@ -369,6 +371,10 @@ namespace Server
172
+ {
173
+ Console.WriteLine("An Ultima Online server emulator written in C# - Visit https://www.servuo.com for more information.\n\n");
174
+ Console.WriteLine(System.AppDomain.CurrentDomain.FriendlyName + " [Parameter]\n\n");
175
+ + Console.WriteLine(" --public host:port Advertise an external IPv4 endpoint without changing the listener.");
176
+ + Console.WriteLine(" --client-data path Use client data from this directory without changing config.");
177
+ + Console.WriteLine(" --railway-proxy Exempt 100.64.0.0/10 proxy peers from shared IP limits.");
178
+ + Console.WriteLine(" --shutdown-file path Save and exit when this file appears.");
179
+ Console.WriteLine(" -debug Starting ServUO in Debug Mode. Debug Mode is being used in Core and Scripts to give extended inforamtion during runtime.");
180
+ Console.WriteLine(" -haltonwarning ServUO halts if any warning is raised during compilation of scripts.");
181
+ Console.WriteLine(" -h or -help Displays this help text.");
182
+ @@ -560,6 +566,7 @@ namespace Server
183
+ NetState.Initialize();
184
+
185
+ EventSink.InvokeServerStarted();
186
+ + HostingOptions.WatchShutdown();
187
+
188
+ try
189
+ {
190
+ diff --git a/Server/Persistence/FileOperations.cs b/Server/Persistence/FileOperations.cs
191
+ --- a/Server/Persistence/FileOperations.cs
192
+ +++ b/Server/Persistence/FileOperations.cs
193
+ @@ -81,7 +81,7 @@
194
+ options |= FileOptions.Asynchronous;
195
+ }
196
+
197
+ - if (unbuffered)
198
+ + if (unbuffered && Core.Windows)
199
+ {
200
+ options |= NoBuffering;
201
+ }
202
+ diff --git a/Scripts/Misc/CrashGuard.cs b/Scripts/Misc/CrashGuard.cs
203
+ --- a/Scripts/Misc/CrashGuard.cs
204
+ +++ b/Scripts/Misc/CrashGuard.cs
205
+ @@ -30,6 +30,13 @@ namespace Server.Misc
206
+ if (SaveBackup)
207
+ Backup();
208
+
209
+ + if (HostingOptions.ShutdownFile != null)
210
+ + {
211
+ + Console.WriteLine("Crash: Exiting; restart is managed by the host.");
212
+ + e.Close = true;
213
+ + return;
214
+ + }
215
+ +
216
+ /*if ( Core.Service )
217
+ e.Close = true;
218
+ else */ if (RestartServer)
@@ -0,0 +1,269 @@
1
+ diff --git i/src/graysvr/CAccount.cpp w/src/graysvr/CAccount.cpp
2
+ index 2790d82..61ff636 100644
3
+ --- i/src/graysvr/CAccount.cpp
4
+ +++ w/src/graysvr/CAccount.cpp
5
+ @@ -525,6 +525,7 @@ bool CAccount::CheckPassword(LPCTSTR pszPassword)
6
+ bool CAccount::CheckPasswordTries(CSocketAddressIP SockAddr)
7
+ {
8
+ ADDTOCALLSTACK("CAccount::CheckPasswordTries");
9
+ + if (Gemwright::IsRailwayProxy(SockAddr)) return true;
10
+ bool fReturn = true;
11
+ DWORD dwCurrentIP = SockAddr.GetAddrIP();
12
+ UINT64 uTimeCurrent = CServTime::GetCurrentTime().GetTimeRaw();
13
+ diff --git i/src/graysvr/CClientLog.cpp w/src/graysvr/CClientLog.cpp
14
+ index 70b27a0..e7239c0 100644
15
+ --- i/src/graysvr/CClientLog.cpp
16
+ +++ w/src/graysvr/CClientLog.cpp
17
+ @@ -127,6 +127,7 @@ void CClient::addRelay(const CServerDef *pServ)
18
+ }
19
+
20
+ EXC_SET("customer id");
21
+ + if (Gemwright::HasPublic && pServ == &g_Serv) ipAddr = Gemwright::PublicEndpoint;
22
+ DWORD dwAddrIP = ipAddr.GetAddrIP();
23
+ DWORD dwCustomerID = 0x7F000001;
24
+ if ( g_Cfg.m_fUseAuthID )
25
+ @@ -143,7 +144,7 @@ void CClient::addRelay(const CServerDef *pServ)
26
+ DEBUG_MSG(("%lx:Login_Relay to server %s with AuthID %" FMTDWORD "\n", GetSocketID(), ipAddr.GetAddrStr(), dwCustomerID));
27
+
28
+ EXC_SET("server relay packet");
29
+ - new PacketServerRelay(this, dwAddrIP, pServ->m_ip.GetPort(), dwCustomerID);
30
+ + new PacketServerRelay(this, dwAddrIP, (Gemwright::HasPublic && pServ == &g_Serv) ? Gemwright::PublicEndpoint.GetPort() : pServ->m_ip.GetPort(), dwCustomerID);
31
+
32
+ m_Targ_Mode = CLIMODE_SETUP_RELAY;
33
+ return;
34
+ diff --git i/src/graysvr/CServer.cpp w/src/graysvr/CServer.cpp
35
+ index 266d5dc..735c448 100644
36
+ --- i/src/graysvr/CServer.cpp
37
+ +++ w/src/graysvr/CServer.cpp
38
+ @@ -132,7 +132,7 @@ bool CServer::SocketsInit()
39
+ #endif
40
+
41
+ // Bind socket to specific IP/port
42
+ - CSocketAddress SockAddr = m_ip;
43
+ + CSocketAddress SockAddr = Gemwright::HasListener ? Gemwright::Listener : m_ip;
44
+ if ( m_SocketMain.Bind(SockAddr) == SOCKET_ERROR )
45
+ {
46
+ g_Log.Event(LOGL_FATAL, "Unable to bind listen socket %s port %hu\n", SockAddr.GetAddrStr(), SockAddr.GetPort());
47
+ @@ -298,6 +298,27 @@ bool CServer::GetPublicIP()
48
+ void CServer::OnTick()
49
+ {
50
+ ADDTOCALLSTACK("CServer::OnTick");
51
+ + if (!Gemwright::ShutdownFile.empty() && std::filesystem::exists(Gemwright::ShutdownFile))
52
+ + {
53
+ + g_Log.Event(LOGL_EVENT, "Hosting: Shutdown requested; saving world before exit.\n");
54
+ + const auto previousSave = g_World.m_iSaveCountID;
55
+ + if (!g_World.Save(true) || g_World.IsSaving() || previousSave == g_World.m_iSaveCountID)
56
+ + {
57
+ + g_Log.Event(LOGL_FATAL, "Hosting: Shutdown save failed.\n");
58
+ + SetExitFlag(-10);
59
+ + return;
60
+ + }
61
+ + std::error_code error;
62
+ + if (!std::filesystem::remove(Gemwright::ShutdownFile, error) || error)
63
+ + {
64
+ + g_Log.Event(LOGL_FATAL, "Hosting: Could not acknowledge completed shutdown.\n");
65
+ + SetExitFlag(-10);
66
+ + return;
67
+ + }
68
+ + Gemwright::ShutdownSucceeded = true;
69
+ + SetExitFlag(2);
70
+ + return;
71
+ + }
72
+ EXC_TRY("Tick");
73
+
74
+ #ifndef _WIN32
75
+ diff --git i/src/graysvr/CUnixTerminal.cpp w/src/graysvr/CUnixTerminal.cpp
76
+ index 1788e12..fbf8b05 100644
77
+ --- i/src/graysvr/CUnixTerminal.cpp
78
+ +++ w/src/graysvr/CUnixTerminal.cpp
79
+ @@ -7,7 +7,7 @@
80
+
81
+ CUnixTerminal g_UnixTerminal;
82
+
83
+ -CUnixTerminal::CUnixTerminal()
84
+ +CUnixTerminal::CUnixTerminal() : m_szNextChar(0), m_fPrepared(false)
85
+ {
86
+ prepare();
87
+ }
88
+ @@ -20,6 +20,7 @@ CUnixTerminal::~CUnixTerminal()
89
+ bool CUnixTerminal::isReady()
90
+ {
91
+ ADDTOCALLSTACK("CUnixTerminal::isReady");
92
+ + if (!m_fPrepared) return false;
93
+ if ( m_szNextChar != '\0' )
94
+ return true;
95
+
96
+ @@ -70,7 +71,7 @@ void CUnixTerminal::print(LPCTSTR pszText)
97
+ void CUnixTerminal::prepare()
98
+ {
99
+ ADDTOCALLSTACK("CUnixTerminal::prepare");
100
+ - ASSERT(!m_fPrepared);
101
+ + if (m_fPrepared || !isatty(STDIN_FILENO)) return;
102
+
103
+ // Save existing attributes
104
+ if ( tcgetattr(STDIN_FILENO, &m_terminal) != 0 )
105
+ @@ -93,7 +94,7 @@ void CUnixTerminal::restore()
106
+ {
107
+ ADDTOCALLSTACK("CUnixTerminal::restore");
108
+ EXC_TRY("Restore");
109
+ - ASSERT(m_fPrepared);
110
+ + if (!m_fPrepared) return;
111
+
112
+ // Restore original terminal state
113
+ if ( tcsetattr(STDIN_FILENO, TCSANOW, &m_terminal) != 0 )
114
+ diff --git i/src/graysvr/HostingOptions.h w/src/graysvr/HostingOptions.h
115
+ new file mode 100644
116
+ index 0000000..dfb45c9
117
+ --- /dev/null
118
+ +++ w/src/graysvr/HostingOptions.h
119
+ @@ -0,0 +1,77 @@
120
+ +#ifndef GEMWRIGHT_HOSTING_OPTIONS_H
121
+ +#define GEMWRIGHT_HOSTING_OPTIONS_H
122
+ +#include <cstdlib>
123
+ +#include <filesystem>
124
+ +#include <stdexcept>
125
+ +#include <string>
126
+ +
127
+ +namespace Gemwright
128
+ +{
129
+ + inline bool HasPublic = false;
130
+ + inline bool HasListener = false;
131
+ + inline bool RailwayProxy = false;
132
+ + inline bool ShutdownSucceeded = false;
133
+ + inline CSocketAddress PublicEndpoint;
134
+ + inline CSocketAddress Listener;
135
+ + inline std::string ShutdownFile;
136
+ +
137
+ + inline CSocketAddress ParseEndpoint(const std::string &value)
138
+ + {
139
+ + const auto separator = value.rfind(':');
140
+ + if (separator == std::string::npos || separator == 0)
141
+ + throw std::invalid_argument("Expected an IPv4 hostname or address followed by :port.");
142
+ + const auto portString = value.substr(separator + 1);
143
+ + if (portString.empty() || portString.find_first_not_of("0123456789") != std::string::npos)
144
+ + throw std::invalid_argument("Endpoint port must be an integer from 1 to 65535.");
145
+ + const auto port = std::stoul(portString);
146
+ + if (port == 0 || port > 65535)
147
+ + throw std::invalid_argument("Endpoint port must be from 1 to 65535.");
148
+ + addrinfo hints = {}, *result = nullptr;
149
+ + hints.ai_family = AF_INET;
150
+ + hints.ai_socktype = SOCK_STREAM;
151
+ + if (getaddrinfo(value.substr(0, separator).c_str(), nullptr, &hints, &result) != 0 || !result)
152
+ + throw std::invalid_argument("Could not resolve endpoint IPv4 address.");
153
+ + CSocketAddress endpoint;
154
+ + endpoint.SetAddrIP(reinterpret_cast<sockaddr_in *>(result->ai_addr)->sin_addr.s_addr);
155
+ + endpoint.SetPortNum(static_cast<WORD>(port));
156
+ + freeaddrinfo(result);
157
+ + return endpoint;
158
+ + }
159
+ +
160
+ + inline bool Parse(int argc, char **argv)
161
+ + {
162
+ + for (int index = 1; index < argc; ++index)
163
+ + {
164
+ + const std::string option(argv[index]);
165
+ + if (option == "--help")
166
+ + {
167
+ + std::puts("--listen host:port Bind without changing shard config.");
168
+ + std::puts("--public host:port Advertise the public login relay endpoint.");
169
+ + std::puts("--railway-proxy Exempt shared proxy peers from per-IP limits.");
170
+ + std::puts("--shutdown-file path Save and exit when this marker appears.");
171
+ + return false;
172
+ + }
173
+ + if (option == "--railway-proxy") { RailwayProxy = true; continue; }
174
+ + if (option != "--listen" && option != "--public" && option != "--shutdown-file")
175
+ + throw std::invalid_argument("Unknown launch option: " + option);
176
+ + if (++index >= argc || !argv[index][0] || argv[index][0] == '-')
177
+ + throw std::invalid_argument("Missing value for " + option);
178
+ + if (option == "--public") { PublicEndpoint = ParseEndpoint(argv[index]); HasPublic = true; }
179
+ + else if (option == "--listen") { Listener = ParseEndpoint(argv[index]); HasListener = true; }
180
+ + else
181
+ + {
182
+ + ShutdownFile = argv[index];
183
+ + const auto path = std::filesystem::path(ShutdownFile);
184
+ + if (!path.is_absolute() || !std::filesystem::is_directory(path.parent_path()) || std::filesystem::exists(path))
185
+ + throw std::invalid_argument("Shutdown marker needs an absolute, unused path in an existing directory.");
186
+ + }
187
+ + }
188
+ + return true;
189
+ + }
190
+ +
191
+ + inline bool IsRailwayProxy(const CSocketAddressIP &address)
192
+ + {
193
+ + return RailwayProxy && (ntohl(address.GetAddrIP()) & 0xffc00000U) == 0x64400000U;
194
+ + }
195
+ +}
196
+ +#endif
197
+ diff --git i/src/graysvr/graysvr.cpp w/src/graysvr/graysvr.cpp
198
+ index e5dd2dc..85d0aac 100644
199
+ --- i/src/graysvr/graysvr.cpp
200
+ +++ w/src/graysvr/graysvr.cpp
201
+ @@ -703,6 +703,10 @@ int _cdecl main( int argc, char * argv[] )
202
+ {
203
+ #ifdef EXCEPTIONS_DEBUG
204
+ const char *m_sClassName = SPHERE_TITLE;
205
+ +#endif
206
+ +#ifndef _WIN32
207
+ + try { if (!Gemwright::Parse(argc, argv)) { std::fflush(stdout); std::_Exit(0); } }
208
+ + catch (const std::exception &error) { std::fprintf(stderr, "Hosting: %s\n", error.what()); std::_Exit(1); }
209
+ #endif
210
+ EXC_TRY("Main");
211
+
212
+ @@ -744,7 +748,15 @@ int _cdecl main( int argc, char * argv[] )
213
+ WritePidFile(1);
214
+
215
+ EXC_CATCH;
216
+ - return g_Serv.m_iExitFlag;
217
+ + // Sphere_ExitServer already closes the world, threads and log. Do not re-enter
218
+ + // destroyed subsystems during global teardown in a hosted process.
219
+ + if (!Gemwright::ShutdownFile.empty())
220
+ + {
221
+ + const int status = Gemwright::ShutdownSucceeded ? 0 : (g_Serv.m_iExitFlag ? g_Serv.m_iExitFlag : 1);
222
+ + std::fflush(nullptr);
223
+ + std::_Exit(status);
224
+ + }
225
+ + return g_Serv.m_iExitFlag;
226
+ }
227
+
228
+ #define ADD(a,b) const char * a::m_sClassName = b
229
+ diff --git i/src/graysvr/graysvr.h w/src/graysvr/graysvr.h
230
+ index a74c726..6d9a3c7 100644
231
+ --- i/src/graysvr/graysvr.h
232
+ +++ w/src/graysvr/graysvr.h
233
+ @@ -3,6 +3,7 @@
234
+ #pragma once
235
+
236
+ #include "../common/graycom.h"
237
+ +#include "HostingOptions.h"
238
+
239
+ class CServTime
240
+ {
241
+ diff --git i/src/network/network.cpp w/src/network/network.cpp
242
+ index 2a1be86..7e3b3ba 100644
243
+ --- i/src/network/network.cpp
244
+ +++ w/src/network/network.cpp
245
+ @@ -998,9 +998,9 @@ void NetworkManager::acceptNewConnection(void)
246
+ DEBUGNETWORK(("Incoming connection from '%s' [blocked=%d, ttl=%ld, pings=%ld, connecting=%ld, connected=%ld]\n", ip.m_ip.GetAddrStr(), ip.m_blocked, ip.m_ttl, ip.m_pings, ip.m_connecting, ip.m_connected));
247
+
248
+ // check if ip is allowed to connect
249
+ - if ( ip.checkPing() || // check for ip ban
250
+ + if ( ip.m_blocked || (!Gemwright::IsRailwayProxy(client_addr) && (ip.checkPing() || // check for ip ban
251
+ ( maxIp > 0 && ip.m_connecting > maxIp ) || // check for too many connecting
252
+ - ( climaxIp > 0 && ip.m_connected > climaxIp ) ) // check for too many connected
253
+ + ( climaxIp > 0 && ip.m_connected > climaxIp ) ))) // check for too many connected
254
+ {
255
+ EXC_SET("rejected");
256
+ DEBUGNETWORK(("Closing incoming connection [max ip=%ld, clients max ip=%ld]\n", maxIp, climaxIp));
257
+ diff --git i/src/network/send.cpp w/src/network/send.cpp
258
+ index a828090..62fabac 100644
259
+ --- i/src/network/send.cpp
260
+ +++ w/src/network/send.cpp
261
+ @@ -2926,7 +2926,7 @@ void PacketServerList::writeServerEntry(const CServerRef &server, WORD index, bo
262
+ {
263
+ ADDTOCALLSTACK("PacketServerList::writeServerEntry");
264
+
265
+ - DWORD ip = server->m_ip.GetAddrIP();
266
+ + DWORD ip = (Gemwright::HasPublic && server == &g_Serv) ? Gemwright::PublicEndpoint.GetAddrIP() : server->m_ip.GetAddrIP();
267
+ DWORD dwPercentFull = 0;
268
+ if (server == &g_Serv)
269
+ {
@@ -0,0 +1,14 @@
1
+ FROM mcr.microsoft.com/dotnet/sdk:10.0-noble
2
+ ENV DEBIAN_FRONTEND=noninteractive DOTNET_CLI_TELEMETRY_OPTOUT=1
3
+ RUN apt-get update && \
4
+ apt-get install -y --no-install-recommends mono-devel libgdiplus zlib1g-dev tzdata && \
5
+ rm -rf /var/lib/apt/lists/*
6
+ WORKDIR /opt/servuo
7
+ COPY server/ ./
8
+ RUN dotnet build ServUO.sln -c Release --nologo -v quiet
9
+ COPY seed/ /opt/seed/
10
+ COPY start.sh run-server.sh /opt/gemwright/
11
+ RUN chmod +x /opt/gemwright/*.sh
12
+ WORKDIR /app
13
+ EXPOSE 2593
14
+ ENTRYPOINT ["/opt/gemwright/start.sh"]
@@ -0,0 +1,36 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ mkdir -p /app/data/server
5
+ for directory in Config Scripts Data Server Ultima; do
6
+ rm -rf "/app/data/server/$directory"
7
+ done
8
+ cp -a /opt/servuo/. /app/data/server/
9
+ cd /app/data/server
10
+ if [ ! -e data ]; then ln -s /app/data data; fi
11
+ if [ ! -e /app/data/.initialized ]; then
12
+ if [ -e Saves ]; then
13
+ echo 'Existing saves found without initialization marker; refusing to replace them.' >&2
14
+ exit 1
15
+ fi
16
+ cp -a /opt/seed/Saves .
17
+ touch /app/data/.initialized
18
+ fi
19
+
20
+ trap 'exit 0' TERM INT
21
+ while [ ! -e /app/data/.client-ready ]; do
22
+ if [ -e /app/data/client.tar.gz ]; then
23
+ mkdir -p /app/data/client
24
+ tar -xzf /app/data/client.tar.gz -C /app/data/client
25
+ touch /app/data/.client-ready
26
+ rm /app/data/client.tar.gz
27
+ else
28
+ echo 'Waiting for UO client files on the data volume.'
29
+ sleep 3 &
30
+ wait $! || true
31
+ fi
32
+ done
33
+
34
+ : "${RAILWAY_TCP_PROXY_DOMAIN:?Railway TCP proxy is missing}"
35
+ : "${RAILWAY_TCP_PROXY_PORT:?Railway TCP proxy port is missing}"
36
+ exec /opt/gemwright/run-server.sh --public "$RAILWAY_TCP_PROXY_DOMAIN:$RAILWAY_TCP_PROXY_PORT" --railway-proxy -noconsole
@@ -0,0 +1,23 @@
1
+ #!/bin/sh
2
+ set -u
3
+
4
+ shutdown_dir=$(mktemp -d "${TMPDIR:-/tmp}/gemwright-stop.XXXXXX") || exit 1
5
+ shutdown_file="$shutdown_dir/request"
6
+ trap 'rm -rf "$shutdown_dir"' 0
7
+ shutdown_requested=0
8
+ trap 'shutdown_requested=1; : > "$shutdown_file"' TERM INT
9
+
10
+ mono ./ServUO.exe "$@" --shutdown-file "$shutdown_file" &
11
+ server_pid=$!
12
+
13
+ # A trapped signal interrupts wait before ServUO finishes saving.
14
+ while :; do
15
+ wait "$server_pid"
16
+ status=$?
17
+ if ! kill -0 "$server_pid" 2>/dev/null; then
18
+ if [ "$shutdown_requested" -eq 1 ] && [ "$status" -eq 137 ] && [ ! -e "$shutdown_file" ]; then
19
+ exit 0
20
+ fi
21
+ exit "$status"
22
+ fi
23
+ done
@@ -0,0 +1,22 @@
1
+ FROM ubuntu:24.04 AS build
2
+ ENV DEBIAN_FRONTEND=noninteractive
3
+ RUN apt-get update && \
4
+ apt-get install -y --no-install-recommends g++ gcc make libmysqlclient-dev ca-certificates && \
5
+ rm -rf /var/lib/apt/lists/*
6
+ WORKDIR /source
7
+ COPY server/ ./
8
+ RUN make MAKEFLAGS=-j2 && ./spheresvr --help
9
+
10
+ FROM ubuntu:24.04
11
+ ENV DEBIAN_FRONTEND=noninteractive
12
+ RUN apt-get update && \
13
+ apt-get install -y --no-install-recommends libmysqlclient21 libstdc++6 tzdata ca-certificates && \
14
+ rm -rf /var/lib/apt/lists/*
15
+ COPY --from=build /source/spheresvr /opt/sphere/spheresvr
16
+ COPY shard/ /opt/shard/
17
+ COPY seed/ /opt/seed/
18
+ COPY start.sh run-server.sh /opt/gemwright/
19
+ RUN chmod +x /opt/gemwright/*.sh
20
+ WORKDIR /app
21
+ EXPOSE 2593
22
+ ENTRYPOINT ["/opt/gemwright/start.sh"]
@@ -0,0 +1,39 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ if [ -e /app/data/.initialized ]; then
5
+ echo 'This volume belongs to a ServUO shard; refusing to reuse it for Sphere.' >&2
6
+ exit 1
7
+ fi
8
+ mkdir -p /app/data/server
9
+ cd /app/data/server
10
+ rm -rf scripts
11
+ cp -a /opt/shard/. .
12
+ if [ ! -e data ]; then ln -s /app/data data; fi
13
+ if [ ! -e /app/data/.sphere-initialized ]; then
14
+ if [ -e accounts ] || [ -e save ]; then
15
+ echo 'Existing Sphere state found without initialization marker; refusing to replace it.' >&2
16
+ exit 1
17
+ fi
18
+ cp -a /opt/seed/accounts /opt/seed/save .
19
+ touch /app/data/.sphere-initialized
20
+ fi
21
+ mkdir -p logs
22
+
23
+ trap 'exit 0' TERM INT
24
+ while [ ! -e /app/data/.client-ready ]; do
25
+ if [ -e /app/data/client.tar.gz ]; then
26
+ mkdir -p /app/data/client
27
+ tar -xzf /app/data/client.tar.gz -C /app/data/client
28
+ touch /app/data/.client-ready
29
+ rm /app/data/client.tar.gz
30
+ else
31
+ echo 'Waiting for UO client files on the data volume.'
32
+ sleep 3 &
33
+ wait $! || true
34
+ fi
35
+ done
36
+
37
+ : "${RAILWAY_TCP_PROXY_DOMAIN:?Railway TCP proxy is missing}"
38
+ : "${RAILWAY_TCP_PROXY_PORT:?Railway TCP proxy port is missing}"
39
+ exec /opt/gemwright/run-server.sh --listen 0.0.0.0:2593 --public "$RAILWAY_TCP_PROXY_DOMAIN:$RAILWAY_TCP_PROXY_PORT" --railway-proxy