Skip to content

Commit d22a001

Browse files
authored
Merge pull request #1031 from Csantucci/multiplayer-server-official
Multiplayer Server, forked from Open Rails Ultimate; blueprint https://blueprints.launchpad.net/or/+spec/multiplayer-server
2 parents 05d5892 + c097fd2 commit d22a001

File tree

7 files changed

+424
-0
lines changed

7 files changed

+424
-0
lines changed
Loading

Source/Documentation/Manual/multiplayer.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,27 @@ Additional info on using the Public Server
312312
time frame, it will re-enter the game in the position where he was at the
313313
moment of the crash.
314314

315+
.. _standalone-multiplayer-server:
316+
317+
Standalone multiplayer server
318+
=============================
319+
320+
If preferred, a standalone multiplayer server may be run on a local computer. It must be
321+
started before the players enter the game.
322+
To start it, click on the *Tools* button in the main menu window, and then select
323+
*MultiPlayer Server*. A window as shown below will appear.
324+
325+
.. image:: images/multiplayerserver.png
326+
:align: center
327+
:scale: 160%
328+
329+
First only the three first lines will be shown. As soon as the first player enters the game
330+
(he must select the URL and the port of the standalone multiplayer server; furthermore he must select
331+
the *Client* button, enter a User Name and click on *Start MP*) , he becomes appointed as the dispatcher,
332+
and the standalone multiplayer server starts displaying all messages sent and received.
333+
At this point all other players can enter the game.
334+
335+
315336
Save and resume
316337
===============
317338

Source/MultiPlayerServer/Assembly.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
using System;
2+
3+
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Tests.Orts")]
4+
[assembly: CLSCompliant(false)]

Source/MultiPlayerServer/Host.cs

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
//
2+
// Code forked from Open Rails Ultimate (now FreeTrainSimulator)
3+
//
4+
using System;
5+
using System.Buffers;
6+
using System.Collections.Generic;
7+
using System.IO.Pipelines;
8+
using System.Linq;
9+
using System.Net;
10+
using System.Net.Sockets;
11+
using System.Text;
12+
using System.Threading.Tasks;
13+
14+
namespace MultiPlayerServer
15+
{
16+
//whoever connects first, will become dispatcher(server) by sending a "SERVER YOU" message
17+
//if a clients sends a "SERVER MakeMeServer", this client should be appointed new server
18+
//track which clients are leaving - if the client was the server, send a new "SERVER WhoCanBeServer" announcement
19+
//if there is no response within xx seconds, appoint a new server by sending "SERVER YOU" to the first/last/random remaining client
20+
public class Host
21+
{
22+
private readonly int port;
23+
24+
private static readonly Encoding encoding = Encoding.Unicode;
25+
private static readonly int charSize = encoding.GetByteCount("0");
26+
private readonly Dictionary<string, TcpClient> onlinePlayers = new Dictionary<string, TcpClient>();
27+
private static readonly byte[] initData = encoding.GetBytes("10: SERVER YOU");
28+
private static readonly byte[] serverChallenge = encoding.GetBytes(" 21: SERVER WhoCanBeServer");
29+
private static readonly byte[] blankToken = encoding.GetBytes(" ");
30+
private static readonly byte[] playerToken = encoding.GetBytes(": PLAYER ");
31+
private static readonly byte[] quitToken = encoding.GetBytes(": QUIT ");
32+
private string currentServer;
33+
34+
public Host(int port)
35+
{
36+
this.port = port;
37+
}
38+
39+
public async Task Run()
40+
{
41+
try
42+
{
43+
TcpListener listener = new TcpListener(IPAddress.Any, port);
44+
listener.Start();
45+
#pragma warning disable CA1303 // Do not pass literals as localized parameters
46+
Console.WriteLine($"MultiPlayer Server is now running on port {port}");
47+
Console.WriteLine("Taken from OR Ultimate (now FreeTrainSimulator)");
48+
Console.WriteLine("Hit <enter> to stop service");
49+
Console.WriteLine();
50+
#pragma warning restore CA1303 // Do not pass literals as localized parameters
51+
while (true)
52+
{
53+
try
54+
{
55+
Pipe pipe = new Pipe();
56+
57+
TcpClient tcpClient = await listener.AcceptTcpClientAsync().ConfigureAwait(false);
58+
_ = PipeFillAsync(tcpClient, pipe.Writer);
59+
_ = PipeReadAsync(tcpClient, pipe.Reader);
60+
}
61+
catch (Exception ex)
62+
{
63+
Console.WriteLine(ex.Message);
64+
throw new InvalidOperationException("Invalid Program state, aborting.", ex);
65+
}
66+
}
67+
}
68+
catch (SocketException socketException)
69+
{
70+
Console.WriteLine(socketException.Message);
71+
throw;
72+
}
73+
}
74+
75+
private async Task PipeFillAsync(TcpClient tcpClient, PipeWriter writer)
76+
{
77+
const int minimumBufferSize = 1024;
78+
_ = currentServer;
79+
NetworkStream networkStream = tcpClient.GetStream();
80+
81+
while (tcpClient.Connected)
82+
{
83+
Memory<byte> memory = writer.GetMemory(minimumBufferSize);
84+
85+
int bytesRead = await networkStream.ReadAsync(memory).ConfigureAwait(false);
86+
if (bytesRead == 0)
87+
{
88+
break;
89+
}
90+
writer.Advance(bytesRead);
91+
92+
FlushResult result = await writer.FlushAsync().ConfigureAwait(false);
93+
94+
if (result.IsCompleted)
95+
{
96+
break;
97+
}
98+
}
99+
await writer.CompleteAsync().ConfigureAwait(false);
100+
}
101+
102+
private bool ReadPlayerName(in ReadOnlySequence<byte> sequence, ref string playerName, out SequencePosition bytesProcessed)
103+
{
104+
Span<byte> playerSeparator = playerToken.AsSpan();
105+
Span<byte> blankSeparator = blankToken.AsSpan();
106+
107+
SequenceReader<byte> reader = new SequenceReader<byte>(sequence);
108+
109+
if (reader.TryReadTo(out ReadOnlySequence<byte> playerPreface, playerSeparator))
110+
{
111+
if (reader.TryReadTo(out ReadOnlySequence<byte> playerNameSequence, blankSeparator))
112+
{
113+
int maxDigits = 4;
114+
if (playerPreface.GetIntFromEnd(ref maxDigits, out int length, encoding))
115+
{
116+
ReadOnlySequence<byte> before = sequence.Slice(0, playerPreface.Length - maxDigits * charSize);
117+
foreach (ReadOnlyMemory<byte> message in before)
118+
{
119+
if (message.Length > 0)
120+
Broadcast(playerName, message);
121+
}
122+
reader.Rewind(playerSeparator.Length + playerNameSequence.Length + maxDigits * charSize);
123+
124+
if (reader.Remaining >= length * charSize)
125+
{
126+
string newPlayerName = playerNameSequence.GetString(encoding);
127+
ReadOnlySequence<byte> playerMessage = reader.Sequence.Slice(before.Length, (length + maxDigits + 2) * charSize);
128+
if (currentServer != playerName)
129+
{
130+
foreach (ReadOnlyMemory<byte> message in playerMessage)
131+
{
132+
SendMessage(currentServer, message).Wait();
133+
}
134+
}
135+
playerName = newPlayerName;
136+
bytesProcessed = sequence.GetPosition(before.Length + playerMessage.Length);
137+
return true;
138+
}
139+
}
140+
}
141+
}
142+
bytesProcessed = sequence.GetPosition(sequence.Length);
143+
return false;
144+
}
145+
146+
private static string ReadQuitMessage(ReadOnlySequence<byte> sequence)
147+
{
148+
Span<byte> quitSeparator = quitToken.AsSpan();
149+
Span<byte> blankSeparator = blankToken.AsSpan();
150+
151+
SequenceReader<byte> reader = new SequenceReader<byte>(sequence);
152+
153+
if (reader.TryReadTo(out ReadOnlySequence<byte> _, quitSeparator))
154+
{
155+
if (reader.TryReadTo(out ReadOnlySequence<byte> playerName, blankSeparator))
156+
{
157+
return playerName.GetString(encoding);
158+
}
159+
}
160+
return null;
161+
}
162+
163+
private async Task PipeReadAsync(TcpClient tcpClient, PipeReader reader)
164+
{
165+
string playerName = tcpClient.Client.RemoteEndPoint.ToString();
166+
bool playerNameSet = false;
167+
string quitPlayer;
168+
onlinePlayers.Add(playerName, tcpClient);
169+
if (onlinePlayers.Count == 1)
170+
{
171+
currentServer = playerName;
172+
await SendMessage(playerName, initData).ConfigureAwait(false);
173+
}
174+
175+
while (tcpClient.Client.Connected)
176+
{
177+
ReadResult result = await reader.ReadAsync().ConfigureAwait(false);
178+
179+
ReadOnlySequence<byte> buffer = result.Buffer;
180+
181+
if (!playerNameSet)
182+
{
183+
string player = playerName;
184+
if (ReadPlayerName(buffer, ref player, out SequencePosition bytesProcessed))
185+
{
186+
onlinePlayers.Remove(playerName);
187+
if (currentServer == playerName)
188+
currentServer = playerName = player;
189+
else
190+
playerName = player;
191+
onlinePlayers.Add(playerName, tcpClient);
192+
playerNameSet = true;
193+
}
194+
reader.AdvanceTo(bytesProcessed);
195+
}
196+
else
197+
{
198+
if (!string.IsNullOrEmpty(quitPlayer = ReadQuitMessage(buffer)) && playerName == quitPlayer)
199+
break;
200+
201+
foreach (ReadOnlyMemory<byte> message in buffer)
202+
{
203+
Broadcast(playerName, message);
204+
}
205+
reader.AdvanceTo(buffer.End);
206+
}
207+
208+
if (result.IsCompleted)
209+
{
210+
break;
211+
}
212+
}
213+
214+
await RemovePlayer(playerName).ConfigureAwait(false);
215+
216+
await reader.CompleteAsync().ConfigureAwait(false);
217+
}
218+
219+
private void Broadcast(string playerName, ReadOnlyMemory<byte> buffer)
220+
{
221+
Console.WriteLine(encoding.GetString(buffer.Span).Replace("\r", Environment.NewLine, StringComparison.OrdinalIgnoreCase));
222+
Parallel.ForEach(onlinePlayers.Keys, async player =>
223+
{
224+
if (player != playerName)
225+
{
226+
try
227+
{
228+
TcpClient client = onlinePlayers[player];
229+
NetworkStream clientStream = client.GetStream();
230+
await clientStream.WriteAsync(buffer).ConfigureAwait(false);
231+
await clientStream.FlushAsync().ConfigureAwait(false);
232+
}
233+
catch (Exception ex) when (ex is System.IO.IOException || ex is SocketException || ex is InvalidOperationException)
234+
{
235+
if (playerName != null)
236+
await RemovePlayer(playerName).ConfigureAwait(false);
237+
}
238+
}
239+
});
240+
}
241+
242+
private async Task SendMessage(string playerName, ReadOnlyMemory<byte> buffer)
243+
{
244+
Console.WriteLine(encoding.GetString(buffer.Span).Replace("\r", Environment.NewLine, StringComparison.OrdinalIgnoreCase));
245+
try
246+
{
247+
TcpClient client = onlinePlayers[playerName];
248+
NetworkStream clientStream = client.GetStream();
249+
await clientStream.WriteAsync(buffer).ConfigureAwait(false);
250+
await clientStream.FlushAsync().ConfigureAwait(false);
251+
}
252+
catch (Exception ex) when (ex is System.IO.IOException || ex is SocketException || ex is InvalidOperationException)
253+
{
254+
if (playerName != null)
255+
await RemovePlayer(playerName).ConfigureAwait(false);
256+
}
257+
}
258+
259+
private async Task RemovePlayer(string playerName)
260+
{
261+
if (onlinePlayers.Remove(playerName))
262+
{
263+
string lostMessage = $"LOST { playerName}";
264+
byte[] lostPlayer = encoding.GetBytes($" {lostMessage.Length}: {lostMessage}");
265+
Broadcast(playerName, lostPlayer);
266+
if (currentServer == playerName)
267+
{
268+
Broadcast(playerName, serverChallenge);
269+
await Task.Delay(5000).ConfigureAwait(false);
270+
if (onlinePlayers.Count > 0)
271+
{
272+
Broadcast(null, lostPlayer);
273+
currentServer = onlinePlayers.Keys.First();
274+
string appointmentMessage = $"SERVER {currentServer}";
275+
lostPlayer = encoding.GetBytes($" {appointmentMessage.Length}: {appointmentMessage}");
276+
Broadcast(null, lostPlayer);
277+
}
278+
}
279+
}
280+
}
281+
}
282+
283+
public static class ReadOnlySequenceExtensions
284+
{
285+
public static bool GetIntFromEnd(in this ReadOnlySequence<byte> payload, ref int maxDigits, out int result, Encoding encoding = null)
286+
{
287+
if (encoding == null) encoding = Encoding.UTF8;
288+
int charSize = encoding.GetByteCount("0");
289+
290+
if (maxDigits > 0)
291+
{
292+
if (maxDigits * charSize > payload.Length)
293+
maxDigits = (int)payload.Length / charSize;
294+
SequencePosition position = payload.GetPosition(payload.Length - maxDigits * charSize);
295+
if (payload.TryGet(ref position, out ReadOnlyMemory<byte> lengthIndicator, false))
296+
{
297+
if (int.TryParse(encoding.GetString(lengthIndicator.Span), out result))
298+
return true;
299+
else
300+
{
301+
maxDigits--;
302+
return GetIntFromEnd(payload, ref maxDigits, out result, encoding);
303+
}
304+
}
305+
}
306+
result = 0;
307+
return false;
308+
}
309+
310+
public static string GetString(in this ReadOnlySequence<byte> payload, Encoding encoding = null)
311+
{
312+
if (encoding == null) encoding = Encoding.UTF8;
313+
314+
return payload.IsSingleSegment ? encoding.GetString(payload.FirstSpan)
315+
: GetStringInternal(payload, encoding);
316+
317+
static string GetStringInternal(in ReadOnlySequence<byte> payload, Encoding encoding)
318+
{
319+
// linearize
320+
int length = checked((int)payload.Length);
321+
byte[] oversized = ArrayPool<byte>.Shared.Rent(length);
322+
try
323+
{
324+
payload.CopyTo(oversized);
325+
return encoding.GetString(oversized, 0, length);
326+
}
327+
finally
328+
{
329+
ArrayPool<byte>.Shared.Return(oversized);
330+
}
331+
}
332+
}
333+
}
334+
335+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework Condition="'$(BuildDotNet)' == 'true'">net6-windows</TargetFramework>
4+
<TargetFramework Condition="'$(TargetFramework)' == ''">net5-windows</TargetFramework>
5+
<OutputType>Exe</OutputType>
6+
<ApplicationIcon>..\ORTS.ico</ApplicationIcon>
7+
<IsPublishable>False</IsPublishable>
8+
<AssemblyTitle>Open Rails MultiPlayer Server</AssemblyTitle>
9+
<Description>Open Rails Transport Simulator</Description>
10+
<Company>Open Rails</Company>
11+
<Product>Open Rails</Product>
12+
<Copyright>Copyright © 2009 - 2022</Copyright>
13+
<PackageLicenseExpression>GPL-3.0-or-later</PackageLicenseExpression>
14+
</PropertyGroup>
15+
<PropertyGroup>
16+
<ApplicationManifest>..\Launcher\app.manifest</ApplicationManifest>
17+
</PropertyGroup>
18+
<ItemGroup>
19+
<PackageReference Include="System.IO.Pipelines" Version="5.0.0.0" />
20+
</ItemGroup>
21+
</Project>

0 commit comments

Comments
 (0)