177 lines
5.6 KiB
C#
177 lines
5.6 KiB
C#
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
|
|
{
|
|
// Spieler schicken nur kleine Pakete (Positionen, Aktionen); der Spielstand geht nur vom Server zum Spieler
|
|
const int MaxFrame = 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;
|
|
}
|
|
}
|
|
}
|