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/LICENSE +21 -0
- package/README.md +70 -2
- package/dist/index.js +1686 -0
- package/package.json +38 -4
- package/patches/servuo-pub57-hosting.patch +218 -0
- package/patches/sphere-hosting.patch +269 -0
- package/templates/servuo/Dockerfile +14 -0
- package/templates/servuo/railway-start.sh +36 -0
- package/templates/servuo/start.sh +23 -0
- package/templates/sphere/Dockerfile +22 -0
- package/templates/sphere/railway-start.sh +39 -0
- package/templates/sphere/start.sh +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gemwright",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|