MSCO dedicated server 2.6: code, Docker, systemd, README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+175
@@ -0,0 +1,175 @@
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace MSCO.Server;
|
||||
|
||||
/// <summary>Muss mit MsgType in der Mod (NetSession.cs) übereinstimmen.</summary>
|
||||
public enum MsgType : byte
|
||||
{
|
||||
Hello = 1,
|
||||
Welcome = 2,
|
||||
Reject = 3,
|
||||
Player = 4,
|
||||
SaveBegin = 5,
|
||||
SaveFile = 6,
|
||||
StartGame = 7,
|
||||
ObjStates = 8,
|
||||
ObjOwn = 9,
|
||||
ObjRelease = 10,
|
||||
FsmEvent = 11,
|
||||
PlayerJoined = 12,
|
||||
PlayerLeft = 13,
|
||||
VehicleFx = 14,
|
||||
Ping = 15,
|
||||
Ready = 16, // Client -> Server: Spiel geladen, Objekte erfasst
|
||||
WorldSnapshot = 17,// Server -> Client: aktueller Weltzustand
|
||||
Info = 18, // Server-Browser fragt Infos ab (ohne Hello)
|
||||
InfoReply = 19,
|
||||
WorldTime = 20, // Uhrzeit + Wetter vom "Weltuhr"-Spieler
|
||||
MoneyDelta = 21, // Geld hat sich geändert (+/- Betrag, gemeinsame Kasse)
|
||||
DoorState = 22, // Tür/Tor/Haube hat den Zustand gewechselt
|
||||
AdminStatus = 23, // Server -> Client: bool, ist Admin
|
||||
AdminListRequest = 24, // Client -> Server: Spielerliste fürs Admin-Menü
|
||||
AdminList = 25, // Server -> Client: int Anzahl, je [int id, string name, string steamId, bool admin]
|
||||
AdminAction = 26, // Client -> Server: byte (1 = kick, 2 = ban), int Spieler-ID, string Grund
|
||||
AdminResult = 27, // Server -> Client: string Meldung
|
||||
Chat = 28, // Chatnachricht (string)
|
||||
VarSync = 29 // Fahrzeugzustand: int n, je [int key, byte typ, wert]
|
||||
}
|
||||
|
||||
public readonly record struct Message(MsgType Type, byte[] Payload)
|
||||
{
|
||||
public BinaryReader Reader() => new(new MemoryStream(Payload));
|
||||
}
|
||||
|
||||
public static class Wire
|
||||
{
|
||||
public static byte[] Build(Action<BinaryWriter> write)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
using var w = new BinaryWriter(ms);
|
||||
write(w);
|
||||
w.Flush();
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
public static byte[] WithSender(int senderId, byte[] payload)
|
||||
{
|
||||
var result = new byte[4 + payload.Length];
|
||||
BitConverter.GetBytes(senderId).CopyTo(result, 0);
|
||||
Buffer.BlockCopy(payload, 0, result, 4, payload.Length);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] Frame(MsgType type, byte[] payload)
|
||||
{
|
||||
payload ??= Array.Empty<byte>();
|
||||
var frame = new byte[5 + payload.Length];
|
||||
BitConverter.GetBytes(payload.Length + 1).CopyTo(frame, 0);
|
||||
frame[4] = (byte)type;
|
||||
Buffer.BlockCopy(payload, 0, frame, 5, payload.Length);
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>TCP-Verbindung zu einem Spieler, gleiches Format wie in der Mod: [int Länge][byte Typ][Payload].</summary>
|
||||
public sealed class Connection
|
||||
{
|
||||
const int MaxFrame = 32 * 1024 * 1024;
|
||||
|
||||
public bool IsClosed { get; private set; }
|
||||
public string CloseReason { get; private set; }
|
||||
public string RemoteAddress { get; }
|
||||
/// <summary>Wann zuletzt etwas ankam (Clients schicken alle 2 s einen Ping).</summary>
|
||||
public DateTime LastReceiveUtc { get; private set; } = DateTime.UtcNow;
|
||||
|
||||
readonly TcpClient client;
|
||||
readonly NetworkStream stream;
|
||||
readonly byte[] readBuffer = new byte[64 * 1024];
|
||||
byte[] buffer = new byte[64 * 1024];
|
||||
int count;
|
||||
|
||||
public Connection(TcpClient client)
|
||||
{
|
||||
this.client = client;
|
||||
client.NoDelay = true;
|
||||
client.SendBufferSize = 4 * 1024 * 1024;
|
||||
stream = client.GetStream();
|
||||
RemoteAddress = client.Client.RemoteEndPoint?.ToString() ?? "?";
|
||||
}
|
||||
|
||||
public List<Message> Poll()
|
||||
{
|
||||
var messages = new List<Message>();
|
||||
if (IsClosed) return messages;
|
||||
try
|
||||
{
|
||||
if (client.Client.Poll(0, SelectMode.SelectRead) && client.Available == 0)
|
||||
{
|
||||
Close("Connection closed");
|
||||
return messages;
|
||||
}
|
||||
while (stream.DataAvailable)
|
||||
{
|
||||
int read = stream.Read(readBuffer, 0, readBuffer.Length);
|
||||
if (read <= 0) break;
|
||||
Append(readBuffer, read);
|
||||
LastReceiveUtc = DateTime.UtcNow;
|
||||
}
|
||||
Parse(messages);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Close("Network error: " + e.Message);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
public void Send(MsgType type, byte[] payload = null)
|
||||
{
|
||||
if (IsClosed) return;
|
||||
try
|
||||
{
|
||||
byte[] frame = Wire.Frame(type, payload);
|
||||
stream.Write(frame, 0, frame.Length);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Close("Network error: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public void Close(string reason)
|
||||
{
|
||||
if (IsClosed) return;
|
||||
IsClosed = true;
|
||||
CloseReason = reason;
|
||||
try { client.Close(); } catch { }
|
||||
}
|
||||
|
||||
void Append(byte[] data, int length)
|
||||
{
|
||||
if (count + length > buffer.Length) Array.Resize(ref buffer, Math.Max(count + length, buffer.Length * 2));
|
||||
Buffer.BlockCopy(data, 0, buffer, count, length);
|
||||
count += length;
|
||||
}
|
||||
|
||||
void Parse(List<Message> messages)
|
||||
{
|
||||
int offset = 0;
|
||||
while (count - offset >= 5)
|
||||
{
|
||||
int length = BitConverter.ToInt32(buffer, offset);
|
||||
if (length < 1 || length > MaxFrame) throw new IOException("Invalid packet");
|
||||
if (count - offset - 4 < length) break;
|
||||
var payload = new byte[length - 1];
|
||||
Buffer.BlockCopy(buffer, offset + 5, payload, 0, payload.Length);
|
||||
messages.Add(new Message((MsgType)buffer[offset + 4], payload));
|
||||
offset += 4 + length;
|
||||
}
|
||||
if (offset > 0)
|
||||
{
|
||||
Buffer.BlockCopy(buffer, offset, buffer, 0, count - offset);
|
||||
count -= offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user