Erste Version

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 17:26:23 +01:00
co-authored by Claude Opus 5.5
commit 35f2d1330d
23 changed files with 1981 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Threading;
namespace BSO.Net
{
/// <summary>Setzt aus einem Byte-Strom Nachrichten zusammen: [int Laenge][byte Typ][Payload].</summary>
public class FrameParser
{
const int MaxFrame = 1024 * 1024;
byte[] buffer = new byte[64 * 1024];
int count;
public void Append(byte[] data, int length)
{
EnsureCapacity(count + length);
Buffer.BlockCopy(data, 0, buffer, count, length);
count += length;
}
public 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 { Type = (MsgType)buffer[offset + 4], Payload = payload });
offset += 4 + length;
}
if (offset > 0)
{
Buffer.BlockCopy(buffer, offset, buffer, 0, count - offset);
count -= offset;
}
}
public static byte[] Frame(MsgType type, byte[] payload)
{
payload ??= Array.Empty<byte>();
var frame = new byte[5 + payload.Length];
int length = payload.Length + 1; // Little Endian wie BitConverter
frame[0] = (byte)length; frame[1] = (byte)(length >> 8); frame[2] = (byte)(length >> 16); frame[3] = (byte)(length >> 24);
frame[4] = (byte)type;
Buffer.BlockCopy(payload, 0, frame, 5, payload.Length);
return frame;
}
void EnsureCapacity(int needed)
{
if (needed <= buffer.Length) return;
var bigger = new byte[Math.Max(needed, buffer.Length * 2)];
Buffer.BlockCopy(buffer, 0, bigger, 0, count);
buffer = bigger;
}
}
/// <summary>
/// TCP-Verbindung wie bei MSCO: Lesen ist nicht-blockierend und wird ueber Poll() abgefragt (im Spiel im Hauptthread),
/// Senden laeuft ueber eine Warteschlange und einen eigenen Thread - eine kurz volle Leitung haelt das Spiel nicht an.
/// Kleine Pakete werden dabei zusammengefasst.
/// </summary>
public class Connection
{
volatile bool closed;
volatile string closeReason;
public bool IsClosed => closed;
public string CloseReason => closeReason;
// Mehr als das unterwegs = die Gegenseite nimmt nichts mehr an
const long MaxQueuedBytes = 16L * 1024 * 1024;
const int MaxBatchBytes = 256 * 1024;
readonly TcpClient client;
readonly NetworkStream stream;
readonly FrameParser parser = new();
readonly byte[] readBuffer = new byte[64 * 1024];
readonly Queue<byte[]> outgoing = new();
readonly object gate = new();
long queuedBytes;
readonly Thread writer;
public Connection(TcpClient client)
{
this.client = client;
client.NoDelay = true;
stream = client.GetStream();
writer = new Thread(WriteLoop) { IsBackground = true, Name = "BSO send" };
writer.Start();
}
/// <summary>Sende-Thread: wartet auf Pakete und schreibt sie (gebuendelt) in die Verbindung.</summary>
void WriteLoop()
{
var batch = new MemoryStream(MaxBatchBytes);
try
{
while (true)
{
batch.SetLength(0);
lock (gate)
{
while (outgoing.Count == 0 && !closed) Monitor.Wait(gate);
if (outgoing.Count == 0) return; // geschlossen und nichts mehr zu senden
do
{
byte[] frame = outgoing.Dequeue();
queuedBytes -= frame.Length;
batch.Write(frame, 0, frame.Length);
} while (outgoing.Count > 0 && batch.Length + outgoing.Peek().Length <= MaxBatchBytes);
Monitor.PulseAll(gate); // Close() wartet evtl. aufs Leerwerden
}
stream.Write(batch.GetBuffer(), 0, (int)batch.Length);
}
}
catch (Exception e)
{
if (!closed) Close("Netzwerkfehler: " + e.Message);
}
}
readonly List<Message> polled = new(); // wiederverwendet: der Aufrufer geht sie sofort durch
public List<Message> Poll()
{
var messages = polled;
messages.Clear();
if (IsClosed) return messages;
try
{
// Verbindung von der Gegenseite geschlossen?
if (client.Client.Poll(0, SelectMode.SelectRead) && client.Available == 0)
{
Close("Verbindung getrennt");
return messages;
}
while (stream.DataAvailable)
{
int read = stream.Read(readBuffer, 0, readBuffer.Length);
if (read <= 0) break;
parser.Append(readBuffer, read);
}
parser.Parse(messages);
}
catch (Exception e)
{
Close("Netzwerkfehler: " + e.Message);
}
return messages;
}
public void Send(MsgType type, byte[] payload)
{
if (closed) return;
byte[] frame = FrameParser.Frame(type, payload);
bool tooMuch;
lock (gate)
{
tooMuch = queuedBytes + frame.Length > MaxQueuedBytes;
if (!tooMuch)
{
outgoing.Enqueue(frame);
queuedBytes += frame.Length;
Monitor.Pulse(gate);
}
}
if (tooMuch) Close("Verbindung zu langsam - zu viele Daten warten aufs Senden");
}
public void Close(string reason)
{
lock (gate)
{
if (closed) return;
// Was schon in der Warteschlange liegt (z.B. eine Abschiedsnachricht), kurz noch rausgehen lassen
var until = DateTime.UtcNow.AddMilliseconds(300);
while (outgoing.Count > 0 && DateTime.UtcNow < until && Thread.CurrentThread != writer)
Monitor.Wait(gate, 50);
closed = true;
closeReason = reason;
outgoing.Clear();
queuedBytes = 0;
Monitor.PulseAll(gate);
}
try { client.Close(); } catch { }
}
}
}
+36
View File
@@ -0,0 +1,36 @@
using System.IO;
namespace BSO.Net
{
/// <summary>
/// BinaryReader fuer Daten aus dem Netz (wie bei MSCO): jede Laengenangabe wird gegen die tatsaechlich vorhandenen
/// Bytes geprueft - eine gefaelschte Laenge darf nicht zu einer riesigen Speicheranforderung fuehren.
/// </summary>
public sealed class PacketReader : BinaryReader
{
public PacketReader(byte[] data) : base(new MemoryStream(data ?? new byte[0], false)) { }
long Remaining => BaseStream.Length - BaseStream.Position;
public override byte[] ReadBytes(int count)
{
if (count < 0 || count > Remaining) throw new EndOfStreamException("Invalid length " + count);
return base.ReadBytes(count);
}
public override char[] ReadChars(int count)
{
if (count < 0 || count > Remaining) throw new EndOfStreamException("Invalid length " + count);
return base.ReadChars(count);
}
public override string ReadString()
{
long start = BaseStream.Position;
int length = Read7BitEncodedInt();
if (length < 0 || length > Remaining) throw new EndOfStreamException("Invalid string length " + length);
BaseStream.Position = start;
return base.ReadString();
}
}
}
+88
View File
@@ -0,0 +1,88 @@
using System;
using System.IO;
namespace BSO.Net
{
/// <summary>
/// Netzwerkprotokoll von Mod und Server - beide binden diese Dateien ein (Shared\*.cs), es gibt also nur eine
/// Fassung. Aufbau wie bei MSCO: [int Laenge][byte Typ][Daten], Daten mit BinaryWriter, Positionen in cm (Engine).
/// </summary>
public static class Protocol
{
/// <summary>Bei jeder inkompatiblen Aenderung erhoehen - der Server lehnt andere Versionen ab.</summary>
public const int Version = 1;
public const int DefaultPort = 7777;
}
public enum MsgType : byte
{
Hello = 1, // Client -> Server: int Protokoll, string Mod-Version, string Name
Welcome = 2, // Server -> Client: int eigene ID, string Servername, int n, je [int ID, string Name]
Reject = 3, // Server -> Client: string Grund
PlayerState = 4, // Client -> Server: PlayerState; Server -> Client: int ID + PlayerState
PlayerJoined = 5, // Server -> Client: int ID, string Name
PlayerLeft = 6, // Server -> Client: int ID
Ping = 7, // long Zeitstempel (Stopwatch-Ticks des Senders)
Pong = 8, // Antwort auf Ping mit demselben Zeitstempel (Laufzeit messen)
Info = 9, // Abfrage ohne Hello (spaeter Server-Browser, jetzt --probe)
InfoReply = 10 // string Servername, string Version, int Spieler, int max. Spieler
}
public enum PlayerMode : byte
{
Menu = 0, // noch keine Szene / Ladebildschirm
OnFoot = 1, // Spielfigur (KeyControlledCharacter)
InBus = 2 // sitzt im eigenen Bus (CarVehicle)
}
public struct Message
{
public MsgType Type;
public byte[] Payload;
public BinaryReader Reader() => new PacketReader(Payload);
}
/// <summary>Was jeder Spieler ~10x pro Sekunde von sich schickt. Drehung wie die Engine: Grad (Gier, Nick, Roll).</summary>
public struct PlayerState
{
public PlayerMode Mode;
public float X, Y, Z;
public float Yaw, Pitch, Roll;
public float Speed; // cm/s
public void Write(BinaryWriter w)
{
w.Write((byte)Mode);
w.Write(X); w.Write(Y); w.Write(Z);
w.Write(Yaw); w.Write(Pitch); w.Write(Roll);
w.Write(Speed);
}
public static PlayerState Read(BinaryReader r) => new PlayerState
{
Mode = (PlayerMode)r.ReadByte(),
X = r.ReadSingle(), Y = r.ReadSingle(), Z = r.ReadSingle(),
Yaw = r.ReadSingle(), Pitch = r.ReadSingle(), Roll = r.ReadSingle(),
Speed = r.ReadSingle()
};
public float DistanceTo(PlayerState o)
{
float dx = X - o.X, dy = Y - o.Y, dz = Z - o.Z;
return MathF.Sqrt(dx * dx + dy * dy + dz * dz);
}
}
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();
}
}
}