diff --git a/CS2WebSocketTelemetryPlugin/CS2WebSocketTelemetryPlugin.cs b/CS2WebSocketTelemetryPlugin/CS2WebSocketTelemetryPlugin.cs index 5d6d69a..b5dd042 100644 --- a/CS2WebSocketTelemetryPlugin/CS2WebSocketTelemetryPlugin.cs +++ b/CS2WebSocketTelemetryPlugin/CS2WebSocketTelemetryPlugin.cs @@ -1,9 +1,15 @@ -using System; +// CS2WebSocketTelemetryPlugin.cs + +using System; +using System.Collections; using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Net.Sockets; using System.Net.Security; +using System.Reflection; using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; @@ -25,16 +31,16 @@ namespace WsTelemetry; public class WebSocketTelemetryPlugin : BasePlugin { public override string ModuleName => "WS Telemetry"; - public override string ModuleVersion => "1.2.0"; + public override string ModuleVersion => "1.5.2"; public override string ModuleAuthor => "you + ChatGPT"; - public override string ModuleDescription => "WS(S)-Server: broadcastet Spieler-Position/ViewAngles + Nade-Events"; + public override string ModuleDescription => "WS(S)-Server: Spielerpos/aim + Granaten (Tick-Trace/Bounce/Detonate/Path) + Map + Smoke/Fire-Status"; // --- Konfiguration --- private volatile bool _enabled = false; private volatile int _sendHz = 10; private volatile string _mapName = ""; - // Bind-Info: ws://host:port/path oder wss://host:port/path + // WS Bind-Info private volatile string _bindHost = "0.0.0.0"; private volatile int _bindPort = 8081; private volatile string _bindPath = "/telemetry"; @@ -51,6 +57,135 @@ public class WebSocketTelemetryPlugin : BasePlugin private Task? _acceptTask; private volatile bool _serverRunning = false; + // --- Konfigurations-Laden --- + private const string ConfigFileName = "config.json"; + + private sealed class Cfg + { + public string? Url { get; set; } + public string? CertPath { get; set; } + public string? CertPassword { get; set; } + public int? SendHz { get; set; } + } + + + private void LoadAndApplyConfig(bool generateIfMissing = true) + { + try + { + var path = Path.Combine(ModuleDirectory, ConfigFileName); + + if (!File.Exists(path) && generateIfMissing) + { + var example = new Cfg + { + Url = $"{(_useTls ? "wss" : "ws")}://{_bindHost}:{_bindPort}{_bindPath}", + CertPath = string.IsNullOrWhiteSpace(_certPath) ? "cert.pfx" : _certPath, + CertPassword = _certPassword, + SendHz = _sendHz + }; + var jsonEx = JsonSerializer.Serialize(example, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(path, jsonEx, Encoding.UTF8); + Logger.LogInformation($"[WS] Beispiel-Konfiguration erzeugt: {path}"); + } + + if (!File.Exists(path)) + { + Logger.LogWarning($"[WS] Keine {ConfigFileName} gefunden. Verwende Defaults."); + return; + } + + var json = File.ReadAllText(path, Encoding.UTF8); + var cfg = JsonSerializer.Deserialize( + json, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true } + ) ?? new Cfg(); + + // URL anwenden + if (!string.IsNullOrWhiteSpace(cfg.Url)) + { + if (Uri.TryCreate(cfg.Url, UriKind.Absolute, out var uri) && (uri.Scheme == "ws" || uri.Scheme == "wss")) + { + _useTls = uri.Scheme == "wss"; + _bindHost = string.IsNullOrWhiteSpace(uri.Host) ? "0.0.0.0" : uri.Host; + _bindPort = uri.IsDefaultPort ? (_useTls ? 443 : 80) : uri.Port; + _bindPath = string.IsNullOrEmpty(uri.AbsolutePath) ? "/" : uri.AbsolutePath; + if (_bindHost == "127.0.0.1") _bindHost = "0.0.0.0"; + } + else + { + Logger.LogWarning($"[WS] Ungültige URL in {ConfigFileName}: '{cfg.Url}'"); + } + } + + // Zertifikat anwenden + if (!string.IsNullOrWhiteSpace(cfg.CertPath)) + { + _certPath = Path.IsPathRooted(cfg.CertPath) + ? cfg.CertPath + : Path.Combine(ModuleDirectory, cfg.CertPath); + _serverCert = null; // beim nächsten Start neu laden + } + if (cfg.CertPassword != null) + { + _certPassword = cfg.CertPassword; + _serverCert = null; + } + + // Sendefrequenz + if (cfg.SendHz is int hz && hz >= 1 && hz <= 128) _sendHz = hz; + + // Durations bleiben hart verdrahtet + _smokeDurationSec = SMOKE_DURATION_SEC; + _fireDurationSec = FIRE_DURATION_SEC; + + Logger.LogInformation($"[WS] Konfiguration geladen ({_bindHost}:{_bindPort}{_bindPath}, tls={_useTls}, hz={_sendHz})"); + } + catch (Exception ex) + { + Logger.LogError($"[WS] Konnte {ConfigFileName} nicht laden/anwenden: {ex.Message}"); + } + } + + private void SaveConfig() + { + try + { + var path = Path.Combine(ModuleDirectory, ConfigFileName); + + // URL aus aktuellen Feldern zusammensetzen + var url = $"{(_useTls ? "wss" : "ws")}://{_bindHost}:{_bindPort}{_bindPath}"; + + // CertPath möglichst relativ speichern + var cp = _certPath; + try + { + if (!string.IsNullOrWhiteSpace(cp)) + { + if (Path.IsPathRooted(cp) && cp.StartsWith(ModuleDirectory, StringComparison.OrdinalIgnoreCase)) + cp = Path.GetRelativePath(ModuleDirectory, cp); + } + } + catch { /* not fatal */ } + + var cfg = new Cfg + { + Url = url, + CertPath = cp, + CertPassword = _certPassword, + SendHz = _sendHz + }; + + var json = JsonSerializer.Serialize(cfg, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(path, json, Encoding.UTF8); + Logger.LogInformation($"[WS] Konfiguration gespeichert: {path}"); + } + catch (Exception ex) + { + Logger.LogError($"[WS] Konnte Konfiguration nicht speichern: {ex.Message}"); + } + } + private class ClientState { public required TcpClient Tcp; @@ -62,7 +197,7 @@ public class WebSocketTelemetryPlugin : BasePlugin private readonly ConcurrentDictionary _clients = new(); private int _clientSeq = 0; - // --- Outgoing Queue (für Events) --- + // --- Outgoing Queue --- private readonly ConcurrentQueue _outbox = new(); private readonly AutoResetEvent _sendSignal = new(false); @@ -71,16 +206,241 @@ public class WebSocketTelemetryPlugin : BasePlugin private const double MaxFrameDt = 0.25; private DateTime _lastTick = DateTime.UtcNow; - public override void Load(bool hotReload) - { - Logger.LogInformation("[WS] Plugin geladen. Kommandos: css_ws_enable, css_ws_url, css_ws_rate, css_ws_cert, css_ws_certpwd"); - RegisterListener(OnTick); + // --- Nade Runtime-Status --- + private int _seq = 0; // globale Sequenz (für Nade/Trace IDs) - // Mapname initial erfassen und auf Mapwechsel reagieren - _mapName = Server.MapName ?? ""; - RegisterListener(OnMapStart); + // Fest verdrahtete Zeiten (nicht änderbar) + private const double SMOKE_DURATION_SEC = 20.0; + private const double FIRE_DURATION_SEC = 7.0; + + // interner Zugriff bleibt über Felder, die auf die Konstanten gesetzt werden + private double _smokeDurationSec = SMOKE_DURATION_SEC; + private double _fireDurationSec = FIRE_DURATION_SEC; + + private class AoeState + { + public required DateTimeOffset EndAt; + public required float X; + public required float Y; + public required float Z; } + private readonly ConcurrentDictionary _activeSmokes = new(); + private readonly ConcurrentDictionary _activeFires = new(); + + // --- Trajektorien-Tracking --- + private class NadeTrack + { + public int Id; + public ulong SteamId; + public string Nade = ""; + public DateTimeOffset Start; + public DateTimeOffset Last; + public bool Finalized; + + public object? BoundEntity; + public string? BoundName; + public float LastX, LastY, LastZ; + + public int Sent = 0; // bereits gesendete Punkte + public int CurrentSeg = 0; // aktuelle Segment-ID (0-basiert) + + public readonly List Points = new(); + public class Point { public float X; public float Y; public float Z; public long T; public int S; } + } + + // Alle Tracks by id + private readonly ConcurrentDictionary _tracks = new(); + // Letzter aktiver Track je Spieler (für grenade_bounce ohne Typ) + private readonly ConcurrentDictionary _lastTrackByPlayer = new(); + // Aktiver Track je Spieler+Nade (für eindeutiges Finalisieren) + private readonly ConcurrentDictionary _activeTrackByPlayerNade = new(); + + private static string PNKey(ulong steamId, string nade) => $"{steamId}:{nade}"; + + // --- Entity-Suche via Reflection --- + private static MethodInfo? _miFindByDesigner; + private static MethodInfo? _miFindByClass; + + private static IEnumerable FindEntitiesByName(string name) + { + _miFindByDesigner ??= typeof(Utilities).GetMethod("FindAllEntitiesByDesignerName", BindingFlags.Public | BindingFlags.Static); + _miFindByClass ??= typeof(Utilities).GetMethod("FindAllEntitiesByClassname", BindingFlags.Public | BindingFlags.Static); + + IEnumerable? ie = null; + try + { + if (_miFindByDesigner != null) + ie = _miFindByDesigner.Invoke(null, new object[] { name }) as IEnumerable; + if (ie == null && _miFindByClass != null) + ie = _miFindByClass.Invoke(null, new object[] { name }) as IEnumerable; + } + catch { } + + if (ie == null) yield break; + foreach (var e in ie) if (e != null) yield return e; + } + + private static bool TryGetPos(object ent, out float x, out float y, out float z) + { + try + { + dynamic d = ent; + var pos = d.AbsOrigin; + x = (float)pos.X; y = (float)pos.Y; z = (float)pos.Z; + return true; + } + catch + { + x = y = z = 0; + return false; + } + } + private static bool IsEntValid(object ent) + { + try { dynamic d = ent; return d != null && d.IsValid; } + catch { return ent != null; } + } + + // --- Stabiler Aim --- + private readonly ConcurrentDictionary _lastAimByPlayer = new(); + + private static long NowMs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + private static bool IsFinite(float v) => !(float.IsNaN(v) || float.IsInfinity(v)) && Math.Abs(v) < 1e6; + + private static float NormalizeYaw(float yaw) + { + yaw %= 360f; + if (yaw < 0) yaw += 360f; + return yaw; + } + private static float ClampPitch(float pitch) + { + if (pitch < -89f) pitch = -89f; + if (pitch > 89f) pitch = 89f; + return pitch; + } + + // -- Camera-gesteuerte Blickrichtung -- + private static bool TryReadAngles(dynamic src, out float pitch, out float yaw, out float roll) + { + // Fallback-Init, damit out immer gesetzt ist + pitch = 0f; yaw = 0f; roll = 0f; + + try { var a = src.EyeAngles; pitch = (float)a.X; yaw = (float)a.Y; roll = (float)a.Z; if (IsFinite(pitch) && IsFinite(yaw)) return true; } catch { } + try { var a = src.AbsRotation; pitch = (float)a.X; yaw = (float)a.Y; roll = (float)a.Z; if (IsFinite(pitch) && IsFinite(yaw)) return true; } catch { } + try { var a = src.ViewAngles; pitch = (float)a.X; yaw = (float)a.Y; roll = (float)a.Z; if (IsFinite(pitch) && IsFinite(yaw)) return true; } catch { } + try { pitch = (float)src.Pitch; yaw = (float)src.Yaw; roll = 0f; if (IsFinite(pitch) && IsFinite(yaw)) return true; } catch { } + + // bleibt false; out sind bereits gesetzt + return false; + } + + private static bool TryGetCameraAnglesFromRoot(dynamic root, out float pitch, out float yaw, out float roll) + { + // out-Parameter VOR jeglicher Logik initialisieren (fix für CS0177) + pitch = 0f; yaw = 0f; roll = 0f; + + // 1) Aktive Kamera-Entity über CameraServices + try + { + dynamic cs = root.CameraServices; + dynamic h = (cs != null) ? cs.m_hActiveCamera : null; + dynamic cam = (h != null ? h.Value : null) ?? cs?.ActiveCamera ?? root.Camera; + if (cam != null && TryReadAngles(cam, out pitch, out yaw, out roll)) + return true; + } + catch { } + + // 2) Winkel direkt aus CameraServices + try + { + dynamic cs = root.CameraServices; + if (cs != null && TryReadAngles(cs, out pitch, out yaw, out roll)) + return true; + } + catch { } + + // 3) Letzter Versuch: direkt am Root + return TryReadAngles(root, out pitch, out yaw, out roll); + } + + private static bool TryGetCameraAngles(CCSPlayerController p, dynamic pawn, out float pitch, out float yaw, out float roll) + { + // out-Defaults + pitch = 0f; yaw = 0f; roll = 0f; + + if (pawn != null && TryGetCameraAnglesFromRoot(pawn, out pitch, out yaw, out roll)) + return true; + + if (TryGetCameraAnglesFromRoot(p, out pitch, out yaw, out roll)) + return true; + + return false; + } + + private (float pitch, float yaw) StoreAim(CCSPlayerController p, float pitch, float yaw) + { + var sid = p.AuthorizedSteamID?.SteamId64 ?? 0UL; + _lastAimByPlayer[sid] = (pitch, yaw); + return (pitch, yaw); + } + + private (float pitch, float yaw) GetStableAim(CCSPlayerController p, dynamic pawn) + { + // 0) Kamera-Winkel bevorzugen + float cp, cy, cr; + if (TryGetCameraAngles(p, pawn, out cp, out cy, out cr)) + return StoreAim(p, ClampPitch(cp), NormalizeYaw(cy)); + + // 1) Fallback: Pawn EyeAngles + try + { + dynamic a = pawn?.EyeAngles; + float px = (float)a.X, py = (float)a.Y; + if (IsFinite(px) && IsFinite(py)) + return StoreAim(p, ClampPitch(px), NormalizeYaw(py)); + } + catch { } + + // 2) Fallback: AbsRotation (Yaw), Pitch aus letztem Wert + try + { + dynamic r = pawn?.AbsRotation; + float yaw = (float)r.Y; + if (IsFinite(yaw)) + { + var sid = p.AuthorizedSteamID?.SteamId64 ?? 0UL; + float pitch = _lastAimByPlayer.TryGetValue(sid, out var last) ? last.pitch : 0f; + return StoreAim(p, pitch, NormalizeYaw(yaw)); + } + } + catch { } + + // 3) Letzter gültiger Aim oder neutral + { + var sid = p.AuthorizedSteamID?.SteamId64 ?? 0UL; + if (_lastAimByPlayer.TryGetValue(sid, out var last)) return last; + return (0f, 0f); + } + } + + public override void Load(bool hotReload) + { + Logger.LogInformation("[WS] Plugin geladen. Kommandos: css_ws_enable, css_ws_reloadcfg, css_ws_url, css_ws_rate, css_ws_cert, css_ws_certpwd, css_ws_sendmap"); + + RegisterListener(OnTick); + + _mapName = Server.MapName ?? ""; + RegisterListener(OnMapStart); + + // Konfiguration einlesen (ohne enabled) … + LoadAndApplyConfig(); + + // … und automatisch starten (abschaltbar per css_ws_enable 0) + _enabled = true; + StartWebSocket(); + } public override void Unload(bool hotReload) { @@ -94,13 +454,12 @@ public class WebSocketTelemetryPlugin : BasePlugin { type = "map", name = _mapName, - t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + t = NowMs() }); Broadcast(payload); Logger.LogInformation($"[WS] Map gewechselt: '{_mapName}' – an Clients gesendet."); } - // ========================= // Konsolen-Kommandos // ========================= @@ -125,6 +484,25 @@ public class WebSocketTelemetryPlugin : BasePlugin else StopWebSocket(); } + [ConsoleCommand("css_ws_reloadcfg", "Lädt die config.json neu und startet den WS(S)-Server ggf. neu")] + public void CmdReloadCfg(CCSPlayerController? caller, CommandInfo cmd) + { + var wasEnabled = _enabled; + + LoadAndApplyConfig(generateIfMissing: false); + + if (wasEnabled) + { + StopWebSocket(); + StartWebSocket(); + cmd.ReplyToCommand("[WS] Konfiguration neu geladen und Server neu gestartet."); + } + else + { + cmd.ReplyToCommand("[WS] Konfiguration neu geladen. Server ist deaktiviert (css_ws_enable 1 zum Starten)."); + } + } + [ConsoleCommand("css_ws_sendmap", "Sendet die aktuelle Karte an alle verbundenen Clients")] public void CmdSendMap(CCSPlayerController? caller, CommandInfo cmd) { @@ -132,13 +510,12 @@ public class WebSocketTelemetryPlugin : BasePlugin { type = "map", name = _mapName, - t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + t = NowMs() }); Broadcast(payload); cmd.ReplyToCommand($"[WS] Map '{_mapName}' an Clients gesendet."); } - [ConsoleCommand("css_ws_url", "Setzt Bind-Host/Port/Pfad als ws[s]://host:port/path")] [CommandHelper(minArgs: 1, usage: "")] public void CmdUrl(CCSPlayerController? caller, CommandInfo cmd) @@ -159,6 +536,7 @@ public class WebSocketTelemetryPlugin : BasePlugin var scheme = _useTls ? "wss" : "ws"; cmd.ReplyToCommand($"[WS] Bind = {scheme}://{_bindHost}:{_bindPort}{_bindPath}"); + SaveConfig(); if (_enabled) { StopWebSocket(); StartWebSocket(); } } @@ -169,6 +547,7 @@ public class WebSocketTelemetryPlugin : BasePlugin if (int.TryParse(cmd.GetArg(1), out var hz) && hz > 0 && hz <= 128) { _sendHz = hz; + SaveConfig(); cmd.ReplyToCommand($"[WS] Sendefrequenz = {_sendHz} Hz"); } else cmd.ReplyToCommand("[WS] Ungültig. Bereich: 1..128"); @@ -182,6 +561,7 @@ public class WebSocketTelemetryPlugin : BasePlugin _certPath = Path.IsPathRooted(input) ? input : Path.Combine(ModuleDirectory, input); _serverCert = null; // neu laden beim Start cmd.ReplyToCommand($"[WS] Zertifikatspfad gesetzt: '{_certPath}'"); + SaveConfig(); if (_enabled && _useTls) { StopWebSocket(); StartWebSocket(); } } @@ -193,6 +573,7 @@ public class WebSocketTelemetryPlugin : BasePlugin _certPassword = pwd == "-" ? "" : pwd; _serverCert = null; // neu laden beim Start cmd.ReplyToCommand($"[WS] Zertifikatspasswort {(string.IsNullOrEmpty(_certPassword) ? "gelöscht" : "gesetzt")}."); + SaveConfig(); if (_enabled && _useTls) { StopWebSocket(); StartWebSocket(); } } @@ -214,8 +595,18 @@ public class WebSocketTelemetryPlugin : BasePlugin if (_accumulator < targetDt) return; _accumulator = 0; - var list = new System.Collections.Generic.List(); + // verwaiste Tracks aufräumen (Failsafe, 15s) + foreach (var kv in _tracks) + { + var tr = kv.Value; + if (!tr.Finalized && (DateTimeOffset.UtcNow - tr.Last).TotalSeconds > 15) + { + TryFinalizeTrackById(tr.Id, sendEvenIfEmpty: true, nadeOverride: tr.Nade); + } + } + // --- Spieler sammeln --- + var playersList = new List(); foreach (var p in Utilities.GetPlayers()) { try @@ -229,62 +620,577 @@ public class WebSocketTelemetryPlugin : BasePlugin if (pawn == null) continue; var pos = pawn.AbsOrigin; - var ang = pawn.EyeAngles; - list.Add(new + // stabile Blickrichtung + var (aimPitch, aimYaw) = GetStableAim(p, pawn); + + // Bewegungsrichtung aus Velocity (2D) + float dirYaw = 0f, speed2D = 0f; + float vx = 0f, vy = 0f, vz = 0f; + try + { + var vel = pawn.AbsVelocity; + vx = vel.X; vy = vel.Y; vz = vel.Z; + speed2D = MathF.Sqrt(vx * vx + vy * vy); + if (speed2D > 1f) + dirYaw = NormalizeYaw((float)(Math.Atan2(vy, vx) * 180.0 / Math.PI)); + } + catch { } + + // rohe Pawn-Angles (Debug/Kompat) + float rawPitch = 0f, rawYaw = 0f, rawRoll = 0f; + try + { + var ar = pawn.EyeAngles; + rawPitch = ar.X; rawYaw = ar.Y; rawRoll = ar.Z; + } + catch { } + + playersList.Add(new { steamId = p.AuthorizedSteamID?.SteamId64 ?? 0UL, name = p.PlayerName, team = p.TeamNum, pos = new { x = pos.X, y = pos.Y, z = pos.Z }, - view = new { pitch = ang.X, yaw = ang.Y, roll = ang.Z } + + // stabilisierte Blickrichtung (nutzen!) + view = new { pitch = aimPitch, yaw = aimYaw, roll = 0f }, + aim = new { pitch = aimPitch, yaw = aimYaw }, + + // optional: rohe Werte + viewRaw = new { pitch = rawPitch, yaw = rawYaw, roll = rawRoll }, + + move = new + { + dirYaw = speed2D > 1f ? dirYaw : (float?)null, + speed = speed2D, + vel = new { x = vx, y = vy, z = vz } + } }); } catch { } } - if (list.Count == 0) return; + // --- Nade-Trace pro Tick sammeln --- + var traceList = new List(); + + // Sammeln/Updaten der aktiven Projektile (füllt nur Track-Punkte) + SampleActiveNadeTracks(); + + // --- Nade-Path Realtime-Streaming pro Tick --- + foreach (var tr in _tracks.Values) + { + if (tr.Finalized) continue; + + int from, to; + List delta = null!; + lock (tr.Points) + { + from = tr.Sent; + to = tr.Points.Count; + if (to > from) + { + delta = new List(to - from); + for (int i = from; i < to; i++) + { + var p = tr.Points[i]; + delta.Add(new { x = p.X, y = p.Y, z = p.Z, t = p.T, s = p.S }); + } + tr.Sent = to; + } + } + + if (delta != null && delta.Count > 0) + { + Enqueue(new + { + type = "nade", + sub = "path", + id = tr.Id, + nade = tr.Nade, + steamId = tr.SteamId, + t = NowMs(), + points = delta, + final = false + }); + } + } + + // aktive Nades (letzte bekannte Position) + var activeList = new List(); + foreach (var tr in _tracks.Values) + { + if (tr.Finalized) continue; + activeList.Add(new + { + id = tr.Id, + nade = tr.Nade, + steamId = tr.SteamId, + pos = new { x = tr.LastX, y = tr.LastY, z = tr.LastZ } + }); + } + + if (playersList.Count == 0 && traceList.Count == 0 && activeList.Count == 0) return; var payload = new { type = "tick", - t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), - players = list + t = NowMs(), + players = playersList, + nades = (activeList.Count > 0 || traceList.Count > 0) + ? new { trace = traceList, active = activeList } + : null }; Broadcast(JsonSerializer.Serialize(payload)); } + // ========================= + // Sampling der aktiven Projektile + // ========================= + + private static readonly Dictionary _kindToNames = new() + { + ["he"] = new[] { "hegrenade_projectile" }, + ["flash"] = new[] { "flashbang_projectile" }, + ["smoke"] = new[] { "smokegrenade_projectile" }, + ["decoy"] = new[] { "decoy_projectile" }, + ["molotov"] = new[] { "molotov_projectile", "incgrenade_projectile" }, + ["other"] = new[] { "grenade_projectile" } // Fallback + }; + + private IEnumerable<(object ent, string name, float x, float y, float z)> EnumerateProjectilesFor(string kind) + { + if (!_kindToNames.TryGetValue(kind, out var names)) names = _kindToNames["other"]; + + foreach (var nm in names) + { + foreach (var e in FindEntitiesByName(nm)) + { + if (e == null || !IsEntValid(e)) continue; + if (TryGetPos(e, out var x, out var y, out var z)) + yield return (e, nm, x, y, z); + } + } + } + + private void SampleActiveNadeTracks() + { + var neededKinds = _tracks.Values.Where(t => !t.Finalized).Select(t => t.Nade).Distinct().ToList(); + var poolByKind = new Dictionary>(); + foreach (var k in neededKinds) + poolByKind[k] = EnumerateProjectilesFor(k).ToList(); + + var usedEnts = new HashSet(ReferenceEqualityComparer.Instance); + + foreach (var kv in _tracks) + { + var tr = kv.Value; + if (tr.Finalized) continue; + + if (tr.BoundEntity == null) + { + if (!poolByKind.TryGetValue(tr.Nade, out var list) || list.Count == 0) continue; + + var best = list + .Where(tpl => !usedEnts.Contains(tpl.ent)) + .Select(tpl => new { tpl.ent, tpl.name, tpl.x, tpl.y, tpl.z, dist2 = Dist2(tr.LastX, tr.LastY, tr.LastZ, tpl.x, tpl.y, tpl.z) }) + .OrderBy(a => a.dist2) + .FirstOrDefault(); + + // NEU: altersabhängiger Fangradius + sanfter Fallback + var ageSec = (float)(DateTimeOffset.UtcNow - tr.Start).TotalSeconds; + + // wächst schnell an, deckelt bei 4500u (ca. halbe Mapbreite) + var maxDist = MathF.Min(4500f, 300f + ageSec * 2500f); + var maxDist2 = maxDist * maxDist; + + if (best != null && best.dist2 <= maxDist2) + { + tr.BoundEntity = best.ent; + tr.BoundName = best.name; + tr.LastX = best.x; tr.LastY = best.y; tr.LastZ = best.z; + tr.Last = DateTimeOffset.UtcNow; + + var tms = NowMs(); + lock (tr.Points) + tr.Points.Add(new NadeTrack.Point { X = best.x, Y = best.y, Z = best.z, T = tms, S = tr.CurrentSeg }); + + usedEnts.Add(best.ent); + } + else if (best != null && tr.Points.Count <= 1) + { + // Fallback: ganz am Anfang binden wir notfalls trotzdem den nächsten Kandidaten, + // damit sofort Bewegungsdaten fließen (vermeidet "nur Wurf+Detonate"). + tr.BoundEntity = best.ent; + tr.BoundName = best.name; + tr.LastX = best.x; tr.LastY = best.y; tr.LastZ = best.z; + tr.Last = DateTimeOffset.UtcNow; + + var tms = NowMs(); + lock (tr.Points) + tr.Points.Add(new NadeTrack.Point { X = best.x, Y = best.y, Z = best.z, T = tms, S = tr.CurrentSeg }); + + usedEnts.Add(best.ent); + } + } + else + { + var ent = tr.BoundEntity; + if (ent == null || !IsEntValid(ent)) continue; + + if (TryGetPos(ent, out var ex, out var ey, out var ez)) + { + if (Dist2(tr.LastX, tr.LastY, tr.LastZ, ex, ey, ez) > 0.1f * 0.1f) + { + var tms = NowMs(); + lock (tr.Points) + tr.Points.Add(new NadeTrack.Point { X = ex, Y = ey, Z = ez, T = tms, S = tr.CurrentSeg }); + + tr.LastX = ex; tr.LastY = ey; tr.LastZ = ez; + tr.Last = DateTimeOffset.UtcNow; + } + } + } + } + } + + private static float Dist2(float ax, float ay, float az, float bx, float by, float bz) + { + float dx = ax - bx, dy = ay - by, dz = az - bz; + return dx * dx + dy * dy + dz * dz; + } + + private static bool LooksAirburst(NadeTrack tr) + { + if (tr.Points.Count < 2) return false; + var a = tr.Points[^1]; + var b = tr.Points[^2]; + var dtMs = Math.Max(1, a.T - b.T); + var dist = MathF.Sqrt(Dist2(a.X, a.Y, a.Z, b.X, b.Y, b.Z)); + var speed3D = 1000f * dist / dtMs; // units/s + var vz = 1000f * Math.Abs(a.Z - b.Z) / dtMs; // vertikale speed + return speed3D > 250f || vz > 120f; + } + // ========================= // Game-Events: Granaten // ========================= + private static string WeaponToKind(string weapon) + { + var w = (weapon ?? "").ToLowerInvariant(); + return w switch + { + "hegrenade" or "weapon_hegrenade" => "he", + "flashbang" or "weapon_flashbang" => "flash", + "smokegrenade" or "weapon_smokegrenade" => "smoke", + "decoy" or "weapon_decoy" => "decoy", + "molotov" or "weapon_molotov" or "incgrenade" or "weapon_incgrenade" => "molotov", + _ => "other" + }; + } + + private int StartTrack(ulong steamId, string nade, float x, float y, float z) + { + var id = Interlocked.Increment(ref _seq); + var tr = new NadeTrack + { + Id = id, + SteamId = steamId, + Nade = nade, + Start = DateTimeOffset.UtcNow, + Last = DateTimeOffset.UtcNow, + LastX = x, LastY = y, LastZ = z, + Sent = 0, + CurrentSeg = 0 + }; + tr.Points.Add(new NadeTrack.Point { X = x, Y = y, Z = z, T = NowMs(), S = tr.CurrentSeg }); + _tracks[id] = tr; + _lastTrackByPlayer[steamId] = id; + _activeTrackByPlayerNade[PNKey(steamId, nade)] = id; + return id; + } + + private void AppendPointByPlayer(ulong steamId, float x, float y, float z) + { + if (_lastTrackByPlayer.TryGetValue(steamId, out var id) && + _tracks.TryGetValue(id, out var tr) && !tr.Finalized) + { + lock (tr.Points) + { + var tms = NowMs(); + tr.Points.Add(new NadeTrack.Point { X = x, Y = y, Z = z, T = tms, S = tr.CurrentSeg }); + tr.LastX = x; tr.LastY = y; tr.LastZ = z; + tr.Last = DateTimeOffset.UtcNow; + } + } + } + + private void AppendPointByPlayerBounce(ulong steamId, float x, float y, float z) + { + if (_lastTrackByPlayer.TryGetValue(steamId, out var id) && + _tracks.TryGetValue(id, out var tr) && !tr.Finalized) + { + lock (tr.Points) + { + tr.CurrentSeg++; // neues Liniensegment beginnen + var tms = NowMs(); + tr.Points.Add(new NadeTrack.Point { X = x, Y = y, Z = z, T = tms, S = tr.CurrentSeg }); + tr.LastX = x; tr.LastY = y; tr.LastZ = z; + tr.Last = DateTimeOffset.UtcNow; + } + } + } + + private void TryFinalizeTrack(ulong steamId, string nade, float? x, float? y, float? z) + { + var key = PNKey(steamId, nade); + if (!_activeTrackByPlayerNade.TryRemove(key, out var id)) return; + TryFinalizeTrackById(id, true, nade, x, y, z); + _lastTrackByPlayer.TryGetValue(steamId, out var lastId); + if (lastId == id) _lastTrackByPlayer.TryRemove(steamId, out _); + } + + private void TryFinalizeTrackById(int id, bool sendEvenIfEmpty, string? nadeOverride = null, float? x = null, float? y = null, float? z = null) + { + if (!_tracks.TryGetValue(id, out var tr) || tr.Finalized) return; + + if (x.HasValue && y.HasValue && z.HasValue) + { + lock (tr.Points) + { + tr.Points.Add(new NadeTrack.Point { X = x.Value, Y = y.Value, Z = z.Value, T = NowMs(), S = tr.CurrentSeg }); + } + } + + // Rest-Delta senden (falls noch nicht raus) + List? delta = null; + lock (tr.Points) + { + if (tr.Points.Count > tr.Sent) + { + delta = new List(tr.Points.Count - tr.Sent); + for (int i = tr.Sent; i < tr.Points.Count; i++) + { + var p = tr.Points[i]; + delta.Add(new { x = p.X, y = p.Y, z = p.Z, t = p.T, s = p.S }); + } + tr.Sent = tr.Points.Count; + } + } + + tr.Finalized = true; + + // Finale Nachricht + Enqueue(new + { + type = "nade", + sub = "path", + id = tr.Id, + nade = nadeOverride ?? tr.Nade, + steamId = tr.SteamId, + t = NowMs(), + points = (delta != null && delta.Count > 0) ? delta : null, + final = true + }); + + _tracks.TryRemove(id, out _); + } + [GameEventHandler] public HookResult OnGrenadeThrown(EventGrenadeThrown ev, GameEventInfo info) { try { var p = ev.Userid; + var pawn = p?.PlayerPawn?.Value; + var pos = pawn?.AbsOrigin; + var (gaPitch, gaYaw) = GetStableAim(p, pawn); + + string weapon = ev.Weapon ?? ""; + string kind = WeaponToKind(weapon); + + float tx = pos?.X ?? 0f, ty = pos?.Y ?? 0f, tz = pos?.Z ?? 0f; + var id = StartTrack(p?.AuthorizedSteamID?.SteamId64 ?? 0UL, kind, tx, ty, tz); + + try { SampleActiveNadeTracks(); } catch { } + Enqueue(new { type = "nade", sub = "thrown", - t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + id, + nade = kind, + t = NowMs(), steamId = p?.AuthorizedSteamID?.SteamId64 ?? 0UL, name = p?.PlayerName ?? "", - weapon = ev.Weapon + weapon = weapon, + throwPos = new { x = tx, y = ty, z = tz }, + throwAim = new { pitch = gaPitch, yaw = NormalizeYaw(gaYaw) } }); } catch { } return HookResult.Continue; } - [GameEventHandler] public HookResult OnHeDetonate (EventHegrenadeDetonate ev, GameEventInfo info) => EnqueueNadeDet(ev, "he"); - [GameEventHandler] public HookResult OnFlashDetonate (EventFlashbangDetonate ev, GameEventInfo info) => EnqueueNadeDet(ev, "flash"); - [GameEventHandler] public HookResult OnSmokeDetonate (EventSmokegrenadeDetonate ev, GameEventInfo info) => EnqueueNadeDet(ev, "smoke"); - [GameEventHandler] public HookResult OnDecoyDetonate (EventDecoyDetonate ev, GameEventInfo info) => EnqueueNadeDet(ev, "decoy"); - [GameEventHandler] public HookResult OnMolotovDetonate (EventMolotovDetonate ev, GameEventInfo info) => EnqueueNadeDet(ev, "molotov"); + // Bounces -> Trajektorie weiter aufzeichnen (Punkt erscheint im nächsten Tick) + [GameEventHandler] + public HookResult OnGrenadeBounce(EventGrenadeBounce ev, GameEventInfo info) + { + try + { + dynamic d = ev; + var p = d.Userid as CCSPlayerController; + AppendPointByPlayerBounce(p?.AuthorizedSteamID?.SteamId64 ?? 0UL, (float)d.X, (float)d.Y, (float)d.Z); + } + catch { } + return HookResult.Continue; + } + + [GameEventHandler] public HookResult OnHeDetonate (EventHegrenadeDetonate ev, GameEventInfo info) + { TryFinalizeDet(ev, "he"); return EnqueueNadeDet(ev, "he"); } + + [GameEventHandler] public HookResult OnFlashDetonate (EventFlashbangDetonate ev, GameEventInfo info) + { TryFinalizeDet(ev, "flash"); return EnqueueNadeDet(ev, "flash"); } + + [GameEventHandler] public HookResult OnDecoyDetonate (EventDecoyDetonate ev, GameEventInfo info) + { TryFinalizeDet(ev, "decoy"); return EnqueueNadeDet(ev, "decoy"); } + + private void TryFinalizeDet(GameEvent ev, string kind) + { + try + { + dynamic d = ev; + var p = d.Userid as CCSPlayerController; + TryFinalizeTrack(p?.AuthorizedSteamID?.SteamId64 ?? 0UL, kind, (float)d.X, (float)d.Y, (float)d.Z); + } + catch { } + } + + // Smoke: Detonate + start/end mit ETA (+ Pfadabschluss) + [GameEventHandler] + public HookResult OnSmokeDetonate(EventSmokegrenadeDetonate ev, GameEventInfo info) + { + try + { + dynamic d = ev; + var p = d.Userid as CCSPlayerController; + + TryFinalizeTrack(p?.AuthorizedSteamID?.SteamId64 ?? 0UL, "smoke", (float)d.X, (float)d.Y, (float)d.Z); + + Enqueue(new + { + type = "nade", + sub = "detonate", + nade = "smoke", + t = NowMs(), + steamId = p?.AuthorizedSteamID?.SteamId64 ?? 0UL, + name = p?.PlayerName ?? "", + pos = new { x = (float)d.X, y = (float)d.Y, z = (float)d.Z } + }); + + var id = Interlocked.Increment(ref _seq); + var endAt = DateTimeOffset.UtcNow.AddSeconds(_smokeDurationSec); + var state = new AoeState { EndAt = endAt, X = (float)d.X, Y = (float)d.Y, Z = (float)d.Z }; + _activeSmokes[id] = state; + + Enqueue(new + { + type = "smoke", + state = "start", + id, + t = NowMs(), + endAt = endAt.ToUnixTimeMilliseconds(), + pos = new { x = state.X, y = state.Y, z = state.Z } + }); + + var token = _serverCts?.Token ?? CancellationToken.None; + _ = Task.Run(async () => + { + try + { + await Task.Delay(TimeSpan.FromSeconds(_smokeDurationSec), token); + if (_activeSmokes.TryRemove(id, out _)) + { + Enqueue(new { type = "smoke", state = "end", id, t = NowMs() }); + } + } + catch { } + }, token); + } + catch { } + return HookResult.Continue; + } + + // Molotov: Detonate + start/end mit ETA (+ Pfadabschluss) – nur start, wenn kein Airburst + [GameEventHandler] + public HookResult OnMolotovDetonate(EventMolotovDetonate ev, GameEventInfo info) + { + try + { + dynamic d = ev; + var p = d.Userid as CCSPlayerController; + var sid = p?.AuthorizedSteamID?.SteamId64 ?? 0UL; + + bool airburst = false; + if (_activeTrackByPlayerNade.TryGetValue(PNKey(sid, "molotov"), out var tid) && + _tracks.TryGetValue(tid, out var trProbe)) + { + airburst = LooksAirburst(trProbe); + } + + TryFinalizeTrack(sid, "molotov", (float)d.X, (float)d.Y, (float)d.Z); + + Enqueue(new + { + type = "nade", + sub = "detonate", + nade = "molotov", + t = NowMs(), + steamId = sid, + name = p?.PlayerName ?? "", + pos = new { x = (float)d.X, y = (float)d.Y, z = (float)d.Z } + }); + + if (!airburst) + { + var id = Interlocked.Increment(ref _seq); + var endAt = DateTimeOffset.UtcNow.AddSeconds(_fireDurationSec); + var state = new AoeState { EndAt = endAt, X = (float)d.X, Y = (float)d.Y, Z = (float)d.Z }; + _activeFires[id] = state; + + Enqueue(new + { + type = "fire", + state = "start", + id, + t = NowMs(), + endAt = endAt.ToUnixTimeMilliseconds(), + pos = new { x = state.X, y = state.Y, z = state.Z } + }); + + var token = _serverCts?.Token ?? CancellationToken.None; + _ = Task.Run(async () => + { + try + { + await Task.Delay(TimeSpan.FromSeconds(_fireDurationSec), token); + if (_activeFires.TryRemove(id, out _)) + { + Enqueue(new { type = "fire", state = "end", id, t = NowMs() }); + } + } + catch { } + }, token); + } + } + catch { } + return HookResult.Continue; + } + + // Generischer Detonate-Helper für HE/Flash/Decoy (Einzel-Event bleibt) private HookResult EnqueueNadeDet(GameEvent ev, string kind) { try @@ -297,7 +1203,7 @@ public class WebSocketTelemetryPlugin : BasePlugin type = "nade", sub = "detonate", nade = kind, - t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + t = NowMs(), steamId = p?.AuthorizedSteamID?.SteamId64 ?? 0UL, name = p?.PlayerName ?? "", pos = new { x = (float)d.X, y = (float)d.Y, z = (float)d.Z } @@ -315,14 +1221,25 @@ public class WebSocketTelemetryPlugin : BasePlugin return HookResult.Continue; } + private static bool LooksWrongPfxPassword(Exception ex) + { + try + { + var s = (ex?.Message ?? "").ToLowerInvariant(); + return s.Contains("password") || s.Contains("kennwort"); + } + catch { return false; } + } + private bool TryLoadCertificate(out string usedPath) { usedPath = _certPath; try { - string pluginDir = ModuleDirectory; // CSS stellt das bereit - // Wenn kein Pfad konfiguriert: im Plugin-Ordner suchen + string pluginDir = ModuleDirectory; + + // Pfad ermitteln if (string.IsNullOrWhiteSpace(usedPath)) { var def = Path.Combine(pluginDir, "cert.pfx"); @@ -333,35 +1250,58 @@ public class WebSocketTelemetryPlugin : BasePlugin else { var files = Directory.GetFiles(pluginDir, "*.pfx", SearchOption.TopDirectoryOnly); - if (files.Length > 0) - usedPath = files[0]; + if (files.Length > 0) usedPath = files[0]; } } - else + else if (!Path.IsPathRooted(usedPath)) { - // Relativen Pfad relativ zum Plugin-Ordner interpretieren - if (!Path.IsPathRooted(usedPath)) - usedPath = Path.Combine(pluginDir, usedPath); + usedPath = Path.Combine(pluginDir, usedPath); } if (string.IsNullOrWhiteSpace(usedPath) || !File.Exists(usedPath)) { - Logger.LogWarning($"[WS] Kein PFX gefunden im Plugin-Ordner ({pluginDir}). " + - "Lege z.B. 'cert.pfx' dort ab oder setze mit css_ws_cert ."); + Logger.LogWarning($"[WS] Kein PFX gefunden im Plugin-Ordner ({pluginDir}). Lege z.B. 'cert.pfx' dort ab oder setze mit css_ws_cert ."); return false; } - _serverCert = new X509Certificate2( - usedPath, - string.IsNullOrEmpty(_certPassword) ? null : _certPassword, - X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.Exportable - ); - - // Erfolgs-Hinweis try { - Logger.LogInformation($"[WS] TLS-Zertifikat geladen: {Path.GetFileName(usedPath)} | " + - $"Subject: {_serverCert.Subject} | Gültig bis: {_serverCert.NotAfter:u}"); + _serverCert = new X509Certificate2( + usedPath, + string.IsNullOrEmpty(_certPassword) ? null : _certPassword, + X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.Exportable + ); + } + catch (CryptographicException cex) when (LooksWrongPfxPassword(cex)) + { + var pwdState = string.IsNullOrEmpty(_certPassword) ? "leer" : "gesetzt"; + Logger.LogError($"[WS] TLS-PFX konnte nicht geöffnet werden: vermutlich falsches Passwort. " + + $"Pfad: {usedPath}. CertPassword (aus config.json) ist {pwdState}. " + + $"Setze es mit 'css_ws_certpwd ' oder trage es in {ConfigFileName} unter 'CertPassword' ein."); + _serverCert = null; + return false; + } + catch (CryptographicException cex) + { + Logger.LogError($"[WS] TLS-PFX Fehler ({usedPath}): {cex.Message}"); + _serverCert = null; + return false; + } + + if (_serverCert == null) + return false; + + if (!_serverCert.HasPrivateKey) + { + Logger.LogError($"[WS] Zertifikat geladen, aber ohne Private Key: {Path.GetFileName(usedPath)}. " + + $"Bitte PFX mit privatem Schlüssel verwenden."); + _serverCert = null; + return false; + } + + try + { + Logger.LogInformation($"[WS] TLS-Zertifikat geladen: {Path.GetFileName(usedPath)} | Subject: {_serverCert.Subject} | Gültig bis: {_serverCert.NotAfter:u}"); } catch { @@ -390,7 +1330,7 @@ public class WebSocketTelemetryPlugin : BasePlugin { if (_useTls) { - if (!TryLoadCertificate(out var usedPath)) + if (!TryLoadCertificate(out var _)) throw new Exception("TLS aktiv, aber kein funktionsfähiges PFX gefunden."); } @@ -406,15 +1346,8 @@ public class WebSocketTelemetryPlugin : BasePlugin var scheme = _useTls ? "wss" : "ws"; Logger.LogInformation($"[WS] Server lauscht auf {scheme}://{_bindHost}:{_bindPort}{_bindPath}"); - // beim Start aktuelle Map an bereits verbundene (falls Hot-Reload) senden _mapName = string.IsNullOrEmpty(Server.MapName) ? _mapName : Server.MapName!; - var startPayload = JsonSerializer.Serialize(new - { - type = "map", - name = _mapName, - t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - }); - Broadcast(startPayload); + Broadcast(JsonSerializer.Serialize(new { type = "map", name = _mapName, t = NowMs() })); _acceptTask = Task.Run(async () => { @@ -437,7 +1370,6 @@ public class WebSocketTelemetryPlugin : BasePlugin } }); - // Sender, der die Outbox entleert _ = Task.Run(async () => { var ct = _serverCts!.Token; @@ -503,12 +1435,10 @@ public class WebSocketTelemetryPlugin : BasePlugin var id = Interlocked.Increment(ref _clientSeq); tcp.NoDelay = true; - // Basis-Stream var baseStream = tcp.GetStream(); baseStream.ReadTimeout = 10000; baseStream.WriteTimeout = 10000; - // Optional TLS Stream stream = baseStream; SslStream? ssl = null; @@ -517,12 +1447,21 @@ public class WebSocketTelemetryPlugin : BasePlugin if (_useTls) { ssl = new SslStream(baseStream, leaveInnerStreamOpen: false); - await ssl.AuthenticateAsServerAsync( - _serverCert!, - clientCertificateRequired: false, - enabledSslProtocols: SslProtocols.Tls13 | SslProtocols.Tls12, - checkCertificateRevocation: false - ); + try + { + await ssl.AuthenticateAsServerAsync( + _serverCert!, + clientCertificateRequired: false, + enabledSslProtocols: SslProtocols.Tls13 | SslProtocols.Tls12, + checkCertificateRevocation: false + ); + } + catch (AuthenticationException aex) + { + Logger.LogError($"[WS] TLS-Handshake fehlgeschlagen: {aex.Message}. " + + $"Tipp: Stimmt das PFX und das CertPassword aus {ConfigFileName}?"); + throw; + } stream = ssl; } @@ -537,18 +1476,46 @@ public class WebSocketTelemetryPlugin : BasePlugin Logger.LogInformation($"[WS] Client #{id} verbunden. Aktive: {_clients.Count}"); - // Dem neuen Client die aktuelle Map sofort schicken try { - var mapPayload = JsonSerializer.Serialize(new + var nowMs = NowMs(); + SendTextFrame(state, JsonSerializer.Serialize(new { type = "map", name = _mapName, t = nowMs })); + + foreach (var kv in _activeSmokes) { - type = "map", - name = _mapName, - t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - }); - SendTextFrame(state, mapPayload); + var aoe = kv.Value; + if (aoe.EndAt > DateTimeOffset.UtcNow) + { + SendTextFrame(state, JsonSerializer.Serialize(new + { + type = "smoke", + state = "start", + id = kv.Key, + t = nowMs, + endAt = aoe.EndAt.ToUnixTimeMilliseconds(), + pos = new { x = aoe.X, y = aoe.Y, z = aoe.Z } + })); + } + } + + foreach (var kv in _activeFires) + { + var aoe = kv.Value; + if (aoe.EndAt > DateTimeOffset.UtcNow) + { + SendTextFrame(state, JsonSerializer.Serialize(new + { + type = "fire", + state = "start", + id = kv.Key, + t = nowMs, + endAt = aoe.EndAt.ToUnixTimeMilliseconds(), + pos = new { x = aoe.X, y = aoe.Y, z = aoe.Z } + })); + } + } } - catch { /* wenn der Client beim Connect gleich schließt, ignorieren */ } + catch { } await ReceiveLoop(state, serverCt); } @@ -602,7 +1569,7 @@ public class WebSocketTelemetryPlugin : BasePlugin return false; var firstLineEnd = header.IndexOf("\r\n", StringComparison.Ordinal); - var firstLine = firstLineEnd > 0 ? header[..firstLineEnd] : header; + var firstLine = firstLineEnd > 0 ? header[..firstLineEnd] : header; var parts = firstLine.Split(' '); if (parts.Length < 2) return false; @@ -804,4 +1771,12 @@ public class WebSocketTelemetryPlugin : BasePlugin } return Task.CompletedTask; } + + // --- Hilfsklasse für Referenz-HashSet --- + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + public static readonly ReferenceEqualityComparer Instance = new(); + public new bool Equals(object x, object y) => ReferenceEquals(x, y); + public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); + } } diff --git a/CS2WebSocketTelemetryPlugin/bin/Release/net8.0/CS2WebSocketTelemetryPlugin.dll b/CS2WebSocketTelemetryPlugin/bin/Release/net8.0/CS2WebSocketTelemetryPlugin.dll index 305923e..d25d40a 100644 Binary files a/CS2WebSocketTelemetryPlugin/bin/Release/net8.0/CS2WebSocketTelemetryPlugin.dll and b/CS2WebSocketTelemetryPlugin/bin/Release/net8.0/CS2WebSocketTelemetryPlugin.dll differ diff --git a/CS2WebSocketTelemetryPlugin/bin/Release/net8.0/CS2WebSocketTelemetryPlugin.pdb b/CS2WebSocketTelemetryPlugin/bin/Release/net8.0/CS2WebSocketTelemetryPlugin.pdb index eaadb5a..a7ed5fa 100644 Binary files a/CS2WebSocketTelemetryPlugin/bin/Release/net8.0/CS2WebSocketTelemetryPlugin.pdb and b/CS2WebSocketTelemetryPlugin/bin/Release/net8.0/CS2WebSocketTelemetryPlugin.pdb differ diff --git a/CS2WebSocketTelemetryPlugin/bin/Release/net8.0/config.json b/CS2WebSocketTelemetryPlugin/bin/Release/net8.0/config.json new file mode 100644 index 0000000..47804f2 --- /dev/null +++ b/CS2WebSocketTelemetryPlugin/bin/Release/net8.0/config.json @@ -0,0 +1,6 @@ +{ + "url": "wss://ws.ironieopen.de:8081/telemetry", + "certPath": "cert.pfx", + "certPassword": "Timmy0104199?", + "sendHz": 10 +} diff --git a/CS2WebSocketTelemetryPlugin/obj/Debug/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfo.cs b/CS2WebSocketTelemetryPlugin/obj/Debug/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfo.cs index c3d6012..c296190 100644 --- a/CS2WebSocketTelemetryPlugin/obj/Debug/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfo.cs +++ b/CS2WebSocketTelemetryPlugin/obj/Debug/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("CS2WebSocketTelemetryPlugin")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+08276e22250976ff18ae026798b2df874d63f90e")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+612f8d5be47e3677a339fa180b79358a0e275fa9")] [assembly: System.Reflection.AssemblyProductAttribute("CS2WebSocketTelemetryPlugin")] [assembly: System.Reflection.AssemblyTitleAttribute("CS2WebSocketTelemetryPlugin")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/CS2WebSocketTelemetryPlugin/obj/Debug/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfoInputs.cache b/CS2WebSocketTelemetryPlugin/obj/Debug/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfoInputs.cache index b56aeb4..3a2c5be 100644 --- a/CS2WebSocketTelemetryPlugin/obj/Debug/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfoInputs.cache +++ b/CS2WebSocketTelemetryPlugin/obj/Debug/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfoInputs.cache @@ -1 +1 @@ -68f13c78af2a58422b82cfaa263d9b46172a592b61c37cbd97fab317d2314a32 +4e5ece0659d7221baa43c2c4dd2c6c38941819653cdedff805ef4e8598d48d5b diff --git a/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfo.cs b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfo.cs index e79af2f..0f606c7 100644 --- a/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfo.cs +++ b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("CS2WebSocketTelemetryPlugin")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+08276e22250976ff18ae026798b2df874d63f90e")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+612f8d5be47e3677a339fa180b79358a0e275fa9")] [assembly: System.Reflection.AssemblyProductAttribute("CS2WebSocketTelemetryPlugin")] [assembly: System.Reflection.AssemblyTitleAttribute("CS2WebSocketTelemetryPlugin")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfoInputs.cache b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfoInputs.cache index 79be6c3..d932ffe 100644 --- a/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfoInputs.cache +++ b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfoInputs.cache @@ -1 +1 @@ -0ad44a906a3869e34c4dff4559f2e11887b2e69d68c7294d8bcab8fc4b86a6ab +4806324ce3160c37004a41ae85c8c6e06de1aaed85bc7956669d94d6c6918040 diff --git a/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.dll b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.dll index 305923e..d25d40a 100644 Binary files a/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.dll and b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.dll differ diff --git a/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.pdb b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.pdb index eaadb5a..a7ed5fa 100644 Binary files a/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.pdb and b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.pdb differ diff --git a/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/ref/CS2WebSocketTelemetryPlugin.dll b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/ref/CS2WebSocketTelemetryPlugin.dll index 6e680eb..6c5d888 100644 Binary files a/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/ref/CS2WebSocketTelemetryPlugin.dll and b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/ref/CS2WebSocketTelemetryPlugin.dll differ diff --git a/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/refint/CS2WebSocketTelemetryPlugin.dll b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/refint/CS2WebSocketTelemetryPlugin.dll index 6e680eb..6c5d888 100644 Binary files a/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/refint/CS2WebSocketTelemetryPlugin.dll and b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/refint/CS2WebSocketTelemetryPlugin.dll differ