242 lines
9.2 KiB
C#
242 lines
9.2 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, 3 bring, 4 unban, 5 reset world, 6 backup, 7 restore backup), int Spieler-ID, string Grund/Name
|
|
AdminResult = 27, // Server -> Client: string Meldung
|
|
Chat = 28, // Chatnachricht (string)
|
|
VarSync = 29, // Fahrzeugzustand: int n, je [int key, byte typ, wert]
|
|
Teleport = 30, // Admin holt einen Spieler zu sich: 3 float Position
|
|
Pong = 31, // Antwort auf Ping mit demselben Zeitstempel (Laufzeit messen)
|
|
SpawnItem = 32, // Admin spawnt ein Item: string Item-Name, 3 float Position, 4 float Drehung
|
|
Reconnect = 33, // Server -> Client: string Meldung; Client geht ins Menü und verbindet sich neu (z.B. Welt zurückgesetzt)
|
|
SaveRequest = 34, // Server -> Client: int Anfrage; bitte im Spiel speichern und den Spielstand hochladen
|
|
SaveUploadPart = 35, // Client -> Server: int Anfrage, string Datei, int Gesamtgröße, int Offset, int Länge, bytes
|
|
SaveUploadDone = 36, // Client -> Server: int Anfrage, int Anzahl Dateien
|
|
ItemAppeared = 37, // neues Item bei einem Spieler (Einkauf ...): int Objekt-ID, string Objektname, 3 float Pos, 4 float Rot
|
|
ObjDestroy = 38, // Item verbraucht/zerstört (gegessen, getrunken ...): int Objekt-ID
|
|
VehicleDamage = 39, // Unfallschaden (verformte Karosserie): int Mesh-Schlüssel, int Eckpunkte, int n, je [int Index, 3 float]
|
|
TrafficStates = 40, // Verkehr, nur von der Welt-Autorität: int n, je [int Schlüssel, 3 float Pos, 4 float Rot, 3 float Geschw.]
|
|
WorldVars = 41, // Jobs/Laden: int n, je [int Schlüssel, byte Typ, Wert]
|
|
HingeStates = 42, // Autotüren/Haube/Kofferraum: int n, je [int Schlüssel, 3 float Pos, 4 float Rot relativ zum Auto]
|
|
VehicleControls = 43 // Zündung/Motor/Licht ...: int n, je [int FSM-Schlüssel, string Zustand]
|
|
}
|
|
|
|
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;
|
|
bool closing; // Meldung gesendet, warten bis der Spieler sie gelesen hat (siehe CloseAfterSend)
|
|
DateTime closeDeadline;
|
|
string closingReason;
|
|
readonly byte[] readBuffer = new byte[64 * 1024];
|
|
readonly List<Message> received = new();
|
|
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() ?? "?";
|
|
}
|
|
|
|
/// <summary>Socket zum Warten auf Daten (Socket.Select im Server-Loop).</summary>
|
|
public Socket Socket => client.Client;
|
|
|
|
/// <summary>
|
|
/// Neue Nachrichten. Die Liste wird beim nächsten Aufruf wiederverwendet (keine Allokation pro Durchlauf).
|
|
/// </summary>
|
|
public List<Message> Poll()
|
|
{
|
|
var messages = received;
|
|
messages.Clear();
|
|
if (IsClosed) return messages;
|
|
if (closing)
|
|
{
|
|
DrainWhileClosing();
|
|
return messages;
|
|
}
|
|
try
|
|
{
|
|
if (client.Available == 0)
|
|
{
|
|
// Getrennt? "lesbar, aber keine Daten" = Gegenseite hat geschlossen
|
|
if (client.Client.Poll(0, SelectMode.SelectRead) && client.Available == 0) Close("Connection closed");
|
|
return messages;
|
|
}
|
|
while (client.Available > 0)
|
|
{
|
|
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) => SendFrame(Wire.Frame(type, payload));
|
|
|
|
/// <summary>Fertig gerahmte Nachricht senden (Broadcast: einmal rahmen, an alle schicken).</summary>
|
|
public void SendFrame(byte[] frame)
|
|
{
|
|
if (IsClosed || closing) return;
|
|
try
|
|
{
|
|
stream.Write(frame, 0, frame.Length);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Close("Network error: " + e.Message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sauber trennen, nachdem eine letzte Meldung gesendet wurde (Kick/Ban-Grund, Neu-Verbinden, Shutdown).
|
|
/// Sofortiges Close() mit noch ungelesenen Daten des Spielers (er schickt ständig Positionen) führt zu einem
|
|
/// TCP-Reset - dann geht die gerade gesendete Meldung verloren. Deshalb: nur die Sende-Richtung schließen,
|
|
/// eingehende Daten noch kurz lesen und verwerfen, dann erst schließen.
|
|
/// </summary>
|
|
public void CloseAfterSend(string reason)
|
|
{
|
|
if (IsClosed || closing) return;
|
|
closing = true;
|
|
closingReason = reason;
|
|
closeDeadline = DateTime.UtcNow.AddSeconds(3);
|
|
try { client.Client.Shutdown(SocketShutdown.Send); } catch { Close(reason); }
|
|
}
|
|
|
|
public bool IsClosing => closing && !IsClosed;
|
|
|
|
void DrainWhileClosing()
|
|
{
|
|
try
|
|
{
|
|
while (stream.DataAvailable)
|
|
if (stream.Read(readBuffer, 0, readBuffer.Length) <= 0) break;
|
|
bool remoteClosed = client.Client.Poll(0, SelectMode.SelectRead) && client.Available == 0;
|
|
if (remoteClosed || DateTime.UtcNow >= closeDeadline) Close(closingReason);
|
|
}
|
|
catch
|
|
{
|
|
Close(closingReason);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|