587 lines
23 KiB
C#
587 lines
23 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using UnityEngine;
|
|
|
|
namespace MSCO
|
|
{
|
|
public enum MsgType : byte
|
|
{
|
|
Hello = 1, // Client -> Host: Mod-Version + Name
|
|
Welcome = 2, // Host -> Client: eigene ID + Spielerliste
|
|
Reject = 3, // Host -> Client: Grund
|
|
Player = 4, // Spielerposition
|
|
SaveBegin = 5, // Host -> Client: Spielstand kommt
|
|
SaveFile = 6, // Host -> Client: Dateiname + Inhalt
|
|
StartGame = 7, // Host -> Client: jetzt laden
|
|
ObjStates = 8, // Positionen der Objekte, die man gerade besitzt
|
|
ObjOwn = 9, // "Ich übernehme dieses Objekt"
|
|
ObjRelease = 10, // "Objekt liegt still, wieder frei"
|
|
FsmEvent = 11, // Schraube angezogen/gelöst, Teil an-/abgebaut
|
|
PlayerJoined = 12,// Host -> alle: neuer Spieler (ID + Name)
|
|
PlayerLeft = 13, // Host -> alle: Spieler weg (ID)
|
|
VehicleFx = 14, // Lichter und Motor-/Dauergeräusche eines Fahrzeugs
|
|
Ping = 15, // Lebenszeichen (Steam hat keinen Verbindungsabbruch)
|
|
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 (FSM-Pfad + Zustand)
|
|
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, 8 set reset point), 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; ins Menü und neu verbinden (z.B. Welt zurückgesetzt)
|
|
SaveRequest = 34, // Server -> Client: int Anfrage; 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, Tüte ausgepackt ...): int Objekt-ID, string Objektname, 3 float Pos, 4 float Rot
|
|
ObjDestroy = 38, // Item wurde verbraucht/zerstört (gegessen, getrunken ...): int Objekt-ID
|
|
VehicleDamage = 39, // Unfallschaden: int Mesh-Schlüssel, int Eckpunkte gesamt, int n, je [int Index, 3 float]
|
|
TrafficStates = 40, // Verkehr (nur 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 (2 int, 3 bool), Wert]
|
|
HingeStates = 42, // Autotüren/Haube/Kofferraum: int n, je [int Schlüssel, float Scharnier-Winkel]
|
|
VehicleControls = 43, // Zündung/Motor/Licht ...: int n, je [int FSM-Schlüssel, string Zustand]
|
|
PlayerNeeds = 44 // Client -> Server: eigene Bedürfnisse (Hunger, Durst ...): int n, je [string Name, float Wert]
|
|
}
|
|
|
|
public struct Message
|
|
{
|
|
public MsgType Type;
|
|
public byte[] Payload;
|
|
|
|
public BinaryReader Reader()
|
|
{
|
|
return new BinaryReader(new MemoryStream(Payload));
|
|
}
|
|
}
|
|
|
|
public struct PlayerState
|
|
{
|
|
public Vector3 Position;
|
|
public float RotationY;
|
|
public Vector3 CamPos; // Kopf/Kamera in Weltkoordinaten
|
|
public Quaternion CamRot; // Blickrichtung inkl. hoch/runter
|
|
public int HeldId; // ID des gehaltenen Objekts, 0 = nichts
|
|
public Vector3 HeldLocalPos; // relativ zur Kamera
|
|
public Quaternion HeldLocalRot; // relativ zur Kamera
|
|
public bool Seated; // fährt oder sitzt als Beifahrer
|
|
public float FeetY; // Bodenhöhe unter dem Spieler (für Ducken/Lehnen der Figur)
|
|
// Im Fahrzeug: Kopf und Blick relativ zum Fahrzeug (ID des SyncedBody, 0 = nicht im Fahrzeug). Damit sitzt die
|
|
// Figur beim anderen fest auf dem Sitz, statt in Weltkoordinaten hinter dem fahrenden Auto herzuhinken.
|
|
// Steht am Ende der Nachricht: ältere Versionen und der Server lesen es einfach nicht mit.
|
|
public int SeatId;
|
|
public Vector3 SeatHead;
|
|
public Quaternion SeatView;
|
|
|
|
public byte[] Serialize()
|
|
{
|
|
PlayerState s = this;
|
|
return NetSession.Build(w =>
|
|
{
|
|
Write(w, s.Position);
|
|
w.Write(s.RotationY);
|
|
Write(w, s.CamPos);
|
|
Write(w, s.CamRot);
|
|
w.Write(s.Seated);
|
|
w.Write(s.FeetY);
|
|
w.Write(s.HeldId);
|
|
if (s.HeldId != 0)
|
|
{
|
|
Write(w, s.HeldLocalPos);
|
|
Write(w, s.HeldLocalRot);
|
|
}
|
|
w.Write(s.SeatId);
|
|
if (s.SeatId == 0) return;
|
|
Write(w, s.SeatHead);
|
|
Write(w, s.SeatView);
|
|
});
|
|
}
|
|
|
|
public static PlayerState Deserialize(BinaryReader r)
|
|
{
|
|
var s = new PlayerState
|
|
{
|
|
Position = ReadVector(r),
|
|
RotationY = r.ReadSingle(),
|
|
CamPos = ReadVector(r),
|
|
CamRot = ReadQuaternion(r),
|
|
Seated = r.ReadBoolean(),
|
|
FeetY = r.ReadSingle(),
|
|
HeldId = r.ReadInt32(),
|
|
HeldLocalRot = Quaternion.identity,
|
|
SeatView = Quaternion.identity
|
|
};
|
|
if (s.HeldId != 0)
|
|
{
|
|
s.HeldLocalPos = ReadVector(r);
|
|
s.HeldLocalRot = ReadQuaternion(r);
|
|
}
|
|
// Ältere Versionen senden keinen Sitz
|
|
if (r.BaseStream.Length - r.BaseStream.Position >= 4)
|
|
{
|
|
s.SeatId = r.ReadInt32();
|
|
if (s.SeatId != 0)
|
|
{
|
|
s.SeatHead = ReadVector(r);
|
|
s.SeatView = ReadQuaternion(r);
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
static void Write(BinaryWriter w, Vector3 v) { w.Write(v.x); w.Write(v.y); w.Write(v.z); }
|
|
static void Write(BinaryWriter w, Quaternion q) { w.Write(q.x); w.Write(q.y); w.Write(q.z); w.Write(q.w); }
|
|
static Vector3 ReadVector(BinaryReader r) { return new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); }
|
|
static Quaternion ReadQuaternion(BinaryReader r) { return new Quaternion(r.ReadSingle(), r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); }
|
|
}
|
|
|
|
public interface IConnection
|
|
{
|
|
bool IsClosed { get; }
|
|
string CloseReason { get; }
|
|
List<Message> Poll();
|
|
void Send(MsgType type, byte[] payload);
|
|
void Close(string reason);
|
|
}
|
|
|
|
/// <summary>Setzt aus einem Byte-Strom Nachrichten zusammen: [int Länge][byte Typ][Payload].</summary>
|
|
public class FrameParser
|
|
{
|
|
const int MaxFrame = 32 * 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)
|
|
{
|
|
if (payload == null) payload = new byte[0];
|
|
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;
|
|
}
|
|
|
|
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 (LAN / IP). Lesen ist nicht-blockierend und wird über Poll() abgefragt.</summary>
|
|
public class Connection : IConnection
|
|
{
|
|
public bool IsClosed { get; private set; }
|
|
public string CloseReason { get; private set; }
|
|
|
|
readonly TcpClient client;
|
|
readonly NetworkStream stream;
|
|
readonly FrameParser parser = new FrameParser();
|
|
readonly byte[] readBuffer = new byte[64 * 1024];
|
|
|
|
public Connection(TcpClient client)
|
|
{
|
|
this.client = client;
|
|
client.NoDelay = true;
|
|
client.SendBufferSize = 4 * 1024 * 1024;
|
|
stream = client.GetStream();
|
|
}
|
|
|
|
public List<Message> Poll()
|
|
{
|
|
var messages = new List<Message>();
|
|
if (IsClosed) return messages;
|
|
try
|
|
{
|
|
// Verbindung von der Gegenseite geschlossen?
|
|
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;
|
|
parser.Append(readBuffer, read);
|
|
}
|
|
parser.Parse(messages);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Close("Network error: " + e.Message);
|
|
}
|
|
return messages;
|
|
}
|
|
|
|
public void Send(MsgType type, byte[] payload)
|
|
{
|
|
if (IsClosed) return;
|
|
try
|
|
{
|
|
byte[] frame = FrameParser.Frame(type, payload);
|
|
stream.Write(frame, 0, frame.Length); // blockierend, damit große Dateien komplett rausgehen
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Close("Network error: " + e.Message);
|
|
}
|
|
}
|
|
|
|
public void Close(string reason)
|
|
{
|
|
if (IsClosed) return;
|
|
IsClosed = true;
|
|
CloseReason = reason;
|
|
try { client.Close(); } catch { }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verbindung über Steam P2P (Steam kümmert sich um NAT/Relay). Pakete sind auf 1 MB
|
|
/// begrenzt, deshalb werden Nachrichten in Stücke geteilt und beim Empfänger über den
|
|
/// FrameParser wieder zusammengesetzt (der Reliable-Kanal hält die Reihenfolge ein).
|
|
/// </summary>
|
|
public class SteamConnection : IConnection
|
|
{
|
|
const int ChunkSize = 512 * 1024;
|
|
const float Timeout = 20f; // ohne Pakete (auch Pings) gilt die Verbindung als tot
|
|
|
|
public readonly Steamworks.CSteamID Remote;
|
|
public bool IsClosed { get; private set; }
|
|
public string CloseReason { get; private set; }
|
|
|
|
readonly FrameParser parser = new FrameParser();
|
|
float lastReceive;
|
|
|
|
public SteamConnection(Steamworks.CSteamID remote)
|
|
{
|
|
Remote = remote;
|
|
lastReceive = Time.realtimeSinceStartup;
|
|
}
|
|
|
|
public void OnPacket(byte[] data, int length)
|
|
{
|
|
parser.Append(data, length);
|
|
lastReceive = Time.realtimeSinceStartup;
|
|
}
|
|
|
|
public List<Message> Poll()
|
|
{
|
|
var messages = new List<Message>();
|
|
if (IsClosed) return messages;
|
|
try { parser.Parse(messages); }
|
|
catch (Exception e) { Close("Network error: " + e.Message); }
|
|
if (Time.realtimeSinceStartup - lastReceive > Timeout) Close("Timed out");
|
|
return messages;
|
|
}
|
|
|
|
public void Send(MsgType type, byte[] payload)
|
|
{
|
|
if (IsClosed) return;
|
|
byte[] frame = FrameParser.Frame(type, payload);
|
|
for (int offset = 0; offset < frame.Length; offset += ChunkSize)
|
|
{
|
|
int length = Math.Min(ChunkSize, frame.Length - offset);
|
|
byte[] chunk = frame;
|
|
if (offset != 0 || length != frame.Length)
|
|
{
|
|
chunk = new byte[length];
|
|
Buffer.BlockCopy(frame, offset, chunk, 0, length);
|
|
}
|
|
if (!Steamworks.SteamNetworking.SendP2PPacket(Remote, chunk, (uint)length, Steamworks.EP2PSend.k_EP2PSendReliable, 0))
|
|
{
|
|
Close("Steam send failed");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Close(string reason)
|
|
{
|
|
if (IsClosed) return;
|
|
IsClosed = true;
|
|
CloseReason = reason;
|
|
try { Steamworks.SteamNetworking.CloseP2PSessionWithUser(Remote); } catch { }
|
|
}
|
|
}
|
|
|
|
public enum NetEventKind { Connected, Disconnected, Message }
|
|
|
|
public struct NetEvent
|
|
{
|
|
public NetEventKind Kind;
|
|
public int PeerId; // beim Client immer NetSession.HostId
|
|
public Message Message;
|
|
public string Reason;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stern-Topologie: Der Host hält eine Verbindung zu jedem Client,
|
|
/// Clients sind nur mit dem Host verbunden. Der Host leitet Nachrichten weiter.
|
|
/// Transport entweder TCP (LAN/IP) oder Steam P2P.
|
|
/// </summary>
|
|
public class NetSession
|
|
{
|
|
public const int HostId = 0;
|
|
|
|
public bool IsHost { get; private set; }
|
|
public bool IsSteam { get; private set; }
|
|
public bool IsClosed { get; private set; }
|
|
public string CloseReason { get; private set; }
|
|
|
|
// Host
|
|
TcpListener listener;
|
|
readonly Dictionary<int, IConnection> peers = new Dictionary<int, IConnection>();
|
|
readonly Dictionary<ulong, int> steamPeerIds = new Dictionary<ulong, int>();
|
|
Func<Steamworks.CSteamID, bool> allowSteamPeer;
|
|
int nextPeerId = 1;
|
|
|
|
// Client
|
|
TcpClient connectingClient;
|
|
IAsyncResult connecting;
|
|
IConnection server;
|
|
bool pendingConnected;
|
|
|
|
byte[] steamBuffer = new byte[64 * 1024];
|
|
|
|
public static NetSession StartHost(int port)
|
|
{
|
|
var s = new NetSession { IsHost = true };
|
|
s.listener = new TcpListener(IPAddress.Any, port);
|
|
s.listener.Start();
|
|
return s;
|
|
}
|
|
|
|
public static NetSession StartClient(string ip, int port)
|
|
{
|
|
var s = new NetSession();
|
|
s.connectingClient = new TcpClient();
|
|
s.connecting = s.connectingClient.BeginConnect(ip, port, null, null);
|
|
return s;
|
|
}
|
|
|
|
/// <summary>Steam-Host: nimmt Pakete von Spielern an, die allowPeer erlaubt (Lobby-Mitglieder).</summary>
|
|
public static NetSession StartSteamHost(Func<Steamworks.CSteamID, bool> allowPeer)
|
|
{
|
|
return new NetSession { IsHost = true, IsSteam = true, allowSteamPeer = allowPeer };
|
|
}
|
|
|
|
public static NetSession StartSteamClient(Steamworks.CSteamID host)
|
|
{
|
|
return new NetSession { IsSteam = true, server = new SteamConnection(host), pendingConnected = true };
|
|
}
|
|
|
|
public List<NetEvent> Poll()
|
|
{
|
|
var events = new List<NetEvent>();
|
|
if (IsClosed) return events;
|
|
try
|
|
{
|
|
if (IsSteam) PumpSteam(events);
|
|
if (IsHost) PollHost(events);
|
|
else PollClient(events);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Close("Network error: " + e.Message);
|
|
}
|
|
return events;
|
|
}
|
|
|
|
void PumpSteam(List<NetEvent> events)
|
|
{
|
|
uint size;
|
|
while (Steamworks.SteamNetworking.IsP2PPacketAvailable(out size, 0))
|
|
{
|
|
if (steamBuffer.Length < size) steamBuffer = new byte[size];
|
|
uint read;
|
|
Steamworks.CSteamID from;
|
|
if (!Steamworks.SteamNetworking.ReadP2PPacket(steamBuffer, size, out read, out from, 0)) break;
|
|
|
|
if (IsHost)
|
|
{
|
|
int id;
|
|
if (!steamPeerIds.TryGetValue(from.m_SteamID, out id))
|
|
{
|
|
if (allowSteamPeer == null || !allowSteamPeer(from)) continue; // nicht in der Lobby
|
|
id = nextPeerId++;
|
|
steamPeerIds[from.m_SteamID] = id;
|
|
peers[id] = new SteamConnection(from);
|
|
events.Add(new NetEvent { Kind = NetEventKind.Connected, PeerId = id });
|
|
}
|
|
IConnection c;
|
|
if (peers.TryGetValue(id, out c)) ((SteamConnection)c).OnPacket(steamBuffer, (int)read);
|
|
}
|
|
else
|
|
{
|
|
var host = server as SteamConnection;
|
|
if (host != null && from == host.Remote) host.OnPacket(steamBuffer, (int)read);
|
|
}
|
|
}
|
|
}
|
|
|
|
void PollHost(List<NetEvent> events)
|
|
{
|
|
if (listener != null)
|
|
{
|
|
while (listener.Pending())
|
|
{
|
|
int id = nextPeerId++;
|
|
peers[id] = new Connection(listener.AcceptTcpClient());
|
|
events.Add(new NetEvent { Kind = NetEventKind.Connected, PeerId = id });
|
|
}
|
|
}
|
|
|
|
List<int> closed = null;
|
|
foreach (KeyValuePair<int, IConnection> p in peers)
|
|
{
|
|
foreach (Message m in p.Value.Poll())
|
|
events.Add(new NetEvent { Kind = NetEventKind.Message, PeerId = p.Key, Message = m });
|
|
if (p.Value.IsClosed)
|
|
{
|
|
if (closed == null) closed = new List<int>();
|
|
closed.Add(p.Key);
|
|
}
|
|
}
|
|
if (closed == null) return;
|
|
foreach (int id in closed)
|
|
{
|
|
IConnection c = peers[id];
|
|
events.Add(new NetEvent { Kind = NetEventKind.Disconnected, PeerId = id, Reason = c.CloseReason });
|
|
var steamConn = c as SteamConnection;
|
|
if (steamConn != null) steamPeerIds.Remove(steamConn.Remote.m_SteamID);
|
|
peers.Remove(id);
|
|
}
|
|
}
|
|
|
|
void PollClient(List<NetEvent> events)
|
|
{
|
|
if (connecting != null)
|
|
{
|
|
if (!connecting.IsCompleted) return;
|
|
IAsyncResult result = connecting;
|
|
connecting = null;
|
|
connectingClient.EndConnect(result); // wirft, wenn nicht erreichbar
|
|
server = new Connection(connectingClient);
|
|
pendingConnected = true;
|
|
}
|
|
if (server == null) return;
|
|
if (pendingConnected)
|
|
{
|
|
pendingConnected = false;
|
|
events.Add(new NetEvent { Kind = NetEventKind.Connected, PeerId = HostId });
|
|
}
|
|
|
|
foreach (Message m in server.Poll())
|
|
events.Add(new NetEvent { Kind = NetEventKind.Message, PeerId = HostId, Message = m });
|
|
if (server.IsClosed) Close(server.CloseReason);
|
|
}
|
|
|
|
/// <summary>An einen bestimmten Spieler (Client: immer an den Host).</summary>
|
|
public void SendTo(int peerId, MsgType type, byte[] payload = null)
|
|
{
|
|
if (!IsHost) { if (server != null) server.Send(type, payload); return; }
|
|
IConnection c;
|
|
if (peers.TryGetValue(peerId, out c)) c.Send(type, payload);
|
|
}
|
|
|
|
/// <summary>An alle (Client: an den Host). Optional einen Spieler auslassen.</summary>
|
|
public void SendToAll(MsgType type, byte[] payload = null, int except = -1)
|
|
{
|
|
if (!IsHost) { if (server != null) server.Send(type, payload); return; }
|
|
foreach (KeyValuePair<int, IConnection> p in peers)
|
|
if (p.Key != except) p.Value.Send(type, payload);
|
|
}
|
|
|
|
/// <summary>Host: Verbindung zu einem Spieler trennen (wird beim nächsten Poll gemeldet).</summary>
|
|
public void Kick(int peerId, string reason)
|
|
{
|
|
IConnection c;
|
|
if (peers.TryGetValue(peerId, out c)) c.Close(reason);
|
|
}
|
|
|
|
/// <summary>Host: Steam-Spieler hat die Lobby verlassen.</summary>
|
|
public void DropSteamPeer(ulong steamId, string reason)
|
|
{
|
|
int id;
|
|
if (steamPeerIds.TryGetValue(steamId, out id)) Kick(id, reason);
|
|
}
|
|
|
|
public void Close(string reason = "Disconnected")
|
|
{
|
|
if (IsClosed) return;
|
|
IsClosed = true;
|
|
CloseReason = reason;
|
|
foreach (IConnection c in peers.Values) c.Close(reason);
|
|
peers.Clear();
|
|
steamPeerIds.Clear();
|
|
if (server != null) server.Close(reason);
|
|
try { if (connectingClient != null && server == null) connectingClient.Close(); } catch { }
|
|
try { if (listener != null) listener.Stop(); } catch { }
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>Stellt der Nachricht die Absender-ID voran (für weitergeleitete Nachrichten).</summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|