diff --git a/CS2WebSocketTelemetryPlugin/CS2WebSocketTelemetryPlugin.cs b/CS2WebSocketTelemetryPlugin/CS2WebSocketTelemetryPlugin.cs index dc961cc..2361f0b 100644 --- a/CS2WebSocketTelemetryPlugin/CS2WebSocketTelemetryPlugin.cs +++ b/CS2WebSocketTelemetryPlugin/CS2WebSocketTelemetryPlugin.cs @@ -7,6 +7,7 @@ using System.IO; using System.Net; using System.Net.Security; using System.Net.Sockets; +using System.Numerics; using System.Reflection; using System.Security.Authentication; using System.Security.Cryptography; @@ -28,9 +29,9 @@ namespace WsTelemetry; public class WebSocketTelemetryPlugin : BasePlugin { public override string ModuleName => "WS Telemetry"; - public override string ModuleVersion => "1.6.0"; + public override string ModuleVersion => "1.7.0"; public override string ModuleAuthor => "you + ChatGPT"; - public override string ModuleDescription => "WS(S)-Server: Spielerpositionen + Blickrichtung + Map"; + public override string ModuleDescription => "WS(S)-Server: Spielerpositionen + Blickrichtung + Map + Nade Trajectories (Prediction + Backfill)"; // --- Konfiguration --- private volatile bool _enabled = false; @@ -63,8 +64,14 @@ public class WebSocketTelemetryPlugin : BasePlugin public string? CertPath { get; set; } public string? CertPassword { get; set; } public int? SendHz { get; set; } + public bool? Predict { get; set; } + public int? PredPoints { get; set; } } + // Prediction-Optionen + private volatile bool _predictEnabled = true; + private volatile int _predPoints = 24; + private void LoadAndApplyConfig(bool generateIfMissing = true) { try @@ -78,7 +85,9 @@ public class WebSocketTelemetryPlugin : BasePlugin Url = $"{(_useTls ? "wss" : "ws")}://{_bindHost}:{_bindPort}{_bindPath}", CertPath = string.IsNullOrWhiteSpace(_certPath) ? "cert.pfx" : _certPath, CertPassword = _certPassword, - SendHz = _sendHz + SendHz = _sendHz, + Predict = _predictEnabled, + PredPoints = _predPoints }; var jsonEx = JsonSerializer.Serialize(example, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(path, jsonEx, Encoding.UTF8); @@ -131,7 +140,11 @@ public class WebSocketTelemetryPlugin : BasePlugin // Sendefrequenz if (cfg.SendHz is int hz && hz >= 1 && hz <= 128) _sendHz = hz; - Logger.LogInformation($"[WS] Konfiguration geladen ({_bindHost}:{_bindPort}{_bindPath}, tls={_useTls}, hz={_sendHz})"); + // Prediction + if (cfg.Predict.HasValue) _predictEnabled = cfg.Predict.Value; + if (cfg.PredPoints is int pp && pp >= 8 && pp <= 64) _predPoints = pp; + + Logger.LogInformation($"[WS] Konfiguration geladen ({_bindHost}:{_bindPort}{_bindPath}, tls={_useTls}, hz={_sendHz}, predict={_predictEnabled}, predPoints={_predPoints})"); } catch (Exception ex) { @@ -163,7 +176,9 @@ public class WebSocketTelemetryPlugin : BasePlugin Url = url, CertPath = cp, CertPassword = _certPassword, - SendHz = _sendHz + SendHz = _sendHz, + Predict = _predictEnabled, + PredPoints = _predPoints }; var json = JsonSerializer.Serialize(cfg, new JsonSerializerOptions { WriteIndented = true }); @@ -216,7 +231,7 @@ public class WebSocketTelemetryPlugin : BasePlugin } // ========================= - // Blickrichtung (Client-Kamera) – EyeAngles bevorzugt + // Blickrichtung (Client-Kamera) // ========================= private static bool IsAlive(dynamic pawn) @@ -235,13 +250,12 @@ public class WebSocketTelemetryPlugin : BasePlugin 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 { } // Modell-/Knotenorientierung 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 { 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 { } return false; } - /// Winkel vorzugsweise vom SceneNode; Fallback: NodeToWorld.Angles, root.AbsRotation. private static bool TryGetAnglesFromSceneNode(dynamic root, out float pitch, out float yaw, out float roll) { pitch = 0f; yaw = 0f; roll = 0f; @@ -261,18 +275,15 @@ public class WebSocketTelemetryPlugin : BasePlugin return false; } - /// Observer-Target via Pawn (nicht Controller!), robust über verschiedene Felder. private static dynamic? TryGetObserverTargetFromPawn(dynamic pawn) { - // typische Felder am Observer-Pawn / Observer-Komponente - try { var os = pawn.ObserverServices; var h = os?.m_hObserverTarget; var v = (h != null ? (h.Value ?? h) : null); if (v != null) return v; } catch { } - try { var h = pawn.m_hObserverTarget; var v = (h != null ? (h.Value ?? h) : null); if (v != null) return v; } catch { } + try { var os = pawn.ObserverServices; var h = os?.m_hObserverTarget; var v = (h != null ? (h.Value ?? h) : null); if (v != null) return v; } catch { } + try { var h = pawn.m_hObserverTarget; var v = (h != null ? (h.Value ?? h) : null); if (v != null) return v; } catch { } try { var h = pawn.m_hLastObserverTarget; var v = (h != null ? (h.Value ?? h) : null); if (v != null) return v; } catch { } - try { var v = pawn.ObserverTarget; if (v != null) return (v.Value ?? v); } catch { } + try { var v = pawn.ObserverTarget; if (v != null) return (v.Value ?? v); } catch { } return null; } - /// Irgendeine Entität in einen Pawn auflösen (Controller→Pawn, Entity→Pawn, bereits Pawn). private static dynamic? AsPawn(dynamic entity) { if (entity == null) return null; @@ -283,7 +294,6 @@ public class WebSocketTelemetryPlugin : BasePlugin return null; } - /// Pawn, dessen Kamera wir darstellen: lebend → eigener Pawn; sonst → Observer-Target (vom Pawn aus). private static bool TryGetClientCameraPawn(CCSPlayerController ctrl, dynamic pawn, out dynamic camPawn) { camPawn = null; @@ -294,7 +304,6 @@ public class WebSocketTelemetryPlugin : BasePlugin return false; } - /// Endgültige View-Winkel (EyeAngles bevorzugt, sonst SceneNode/AbsRotation, dann Fallbacks). private static bool TryGetViewAngles(CCSPlayerController ctrl, dynamic pawn, out float pitch, out float yaw, out float roll) { pitch = 0f; yaw = 0f; roll = 0f; @@ -302,7 +311,6 @@ public class WebSocketTelemetryPlugin : BasePlugin dynamic camPawn; if (TryGetClientCameraPawn(ctrl, pawn, out camPawn)) { - // 1) Echte Kamera-/Aim-Winkel try { var a = camPawn.EyeAngles; @@ -311,7 +319,6 @@ public class WebSocketTelemetryPlugin : BasePlugin } catch { } - // 2) Modellorientierung als Fallback if (TryGetAnglesFromSceneNode(camPawn, out pitch, out yaw, out roll)) return true; @@ -324,7 +331,6 @@ public class WebSocketTelemetryPlugin : BasePlugin catch { } } - // 3) letzte Fallbacks if (TryReadAngles(pawn, out pitch, out yaw, out roll)) return true; return TryReadAngles(ctrl, out pitch, out yaw, out roll); } @@ -339,19 +345,18 @@ public class WebSocketTelemetryPlugin : BasePlugin private static bool IsTiny(float v) => MathF.Abs(v) <= 1e-5f; private static bool IsTinyPair(float a, float b) => IsTiny(a) && IsTiny(b); - private (float pitch, float yaw) GetStableAim(CCSPlayerController p, dynamic pawn) + private (float pitch, float yaw) GetStableAim(CCSPlayerController p, object pawnObj) { + dynamic pawn = pawnObj; // intern weiterhin dynamisch arbeiten float vp=0, vy=0, vr=0; - // 1) Bevorzugt: echte Kamera-/View-Winkel if (TryGetViewAngles(p, pawn, out vp, out vy, out vr)) { vp = ClampPitch(vp); vy = NormalizeYaw(vy); - if (!IsTinyPair(vp, vy)) // 0/0 ist verdächtig → nicht akzeptieren + if (!IsTinyPair(vp, vy)) return StoreAim(p, vp, vy); } - // 2) Fallback: Pawn EyeAngles (falls vorhanden) try { dynamic a = pawn?.EyeAngles; @@ -365,7 +370,6 @@ public class WebSocketTelemetryPlugin : BasePlugin } catch { } - // 3) Fallback: Modell-/Feet-Yaw aus AbsRotation try { dynamic r = pawn?.AbsRotation; @@ -379,7 +383,6 @@ public class WebSocketTelemetryPlugin : BasePlugin } catch { } - // 4) Fallback: Bewegungsrichtung (nur wenn sich der Spieler bewegt) try { var vel = pawn?.AbsVelocity; @@ -395,7 +398,6 @@ public class WebSocketTelemetryPlugin : BasePlugin } catch { } - // 5) Letzter gültiger Aim oder neutral { var sid = p.AuthorizedSteamID?.SteamId64 ?? 0UL; if (_lastAimByPlayer.TryGetValue(sid, out var last)) return last; @@ -412,7 +414,6 @@ public class WebSocketTelemetryPlugin : BasePlugin float yaw = (float)ea.Y; float roll = (float)ea.Z; - // wie üblich begrenzen/normalisieren if (IsFinite(pitch) && IsFinite(yaw)) return (ClampPitch(pitch), NormalizeYaw(yaw), roll); } @@ -420,16 +421,44 @@ public class WebSocketTelemetryPlugin : BasePlugin return (0f, 0f, 0f); } + private static bool TryGetEyePosition(dynamic pawn, out Vector3 eye) + { + // Beste Schätzungen + try { var v = pawn?.EyePosition; eye = new Vector3((float)v.X, (float)v.Y, (float)v.Z); return true; } catch {} + try { var v = pawn?.AbsOrigin; eye = new Vector3((float)v.X, (float)v.Y, (float)v.Z) + new Vector3(0,0,64f); return true; } catch {} + eye = default; return false; + } + + private static float GetTickInterval() + { + // Kompatibler Fallback: 64 Tick (falls deine API keine globale Tickrate liefert) + // Wenn du eine verlässliche Quelle hast (z. B. ConVar oder API), trage sie hier ein. + return 1.0f / 64.0f; + } + + // ========================= + // Lifecycle + // ========================= public override void Load(bool hotReload) { - Logger.LogInformation("[WS] Plugin geladen. Kommandos: css_ws_enable, css_ws_restart, css_ws_reloadcfg, css_ws_url, css_ws_rate, css_ws_cert, css_ws_certpwd, css_ws_sendmap"); + Logger.LogInformation("[WS] Plugin geladen. Kommandos: css_ws_enable, css_ws_restart, css_ws_reloadcfg, css_ws_url, css_ws_rate, css_ws_cert, css_ws_certpwd, css_ws_sendmap, css_ws_pred"); RegisterListener(OnTick); _mapName = Server.MapName ?? ""; RegisterListener(OnMapStart); + RegisterListener(OnEntityCreated); + RegisterListener(OnEntityDeleted); + + RegisterEventHandler(OnSmokeDetonate); + RegisterEventHandler(OnHeDetonate); + RegisterEventHandler(OnFlashDetonate); + RegisterEventHandler(OnMolotovDetonate); + RegisterEventHandler(OnDecoyStart); + RegisterEventHandler(OnDecoyDetonate); + LoadAndApplyConfig(); _enabled = true; @@ -478,6 +507,19 @@ public class WebSocketTelemetryPlugin : BasePlugin else StopWebSocket(); } + [ConsoleCommand("css_ws_pred", "Prediction der Nade-Trajektorie aktivieren/deaktivieren (1|0), Punkte optional")] + [CommandHelper(minArgs: 1, usage: "<1|0> [points 8..64]")] + public void CmdPred(CCSPlayerController? caller, CommandInfo cmd) + { + var on = cmd.GetArg(1); + _predictEnabled = on == "1" || on.Equals("true", StringComparison.OrdinalIgnoreCase); + if (cmd.ArgCount >= 3 && int.TryParse(cmd.GetArg(2), out var pts) && pts >= 8 && pts <= 64) + _predPoints = pts; + + cmd.ReplyToCommand($"[WS] Prediction: {(_predictEnabled ? "an" : "aus")} (points={_predPoints})"); + SaveConfig(); + } + [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) { @@ -503,15 +545,12 @@ public class WebSocketTelemetryPlugin : BasePlugin { try { - // Config immer neu laden (ohne Beispieldatei zu erzeugen) LoadAndApplyConfig(generateIfMissing: false); - // internen Zustand zurücksetzen _lastTick = DateTime.UtcNow; _accumulator = 0; while (_outbox.TryDequeue(out _)) { } - // Server neu starten (nur wenn derzeit/enabled) var wasEnabled = _enabled; StopWebSocket(); if (wasEnabled) @@ -633,12 +672,12 @@ public class WebSocketTelemetryPlugin : BasePlugin var pawn = pawnHandle.Value; if (pawn == null) continue; - // --- Position (SceneNode.AbsOrigin bevorzugt) + // Position float posX, posY, posZ; try { var node = pawn?.CBodyComponent?.SceneNode; - var org = node != null ? node.AbsOrigin : pawn.AbsOrigin; + var org = node != null ? node.AbsOrigin : pawn.AbsOrigin; posX = (float)org.X; posY = (float)org.Y; posZ = (float)org.Z; @@ -651,40 +690,63 @@ public class WebSocketTelemetryPlugin : BasePlugin posZ = (float)org.Z; } - // --- Blickrichtung (Yaw aus EyeAngles, bereits normalisiert/geclamped) - var (_, eyeYaw, _) = ReadEyeAngles(pawn); - float yawDeg = eyeYaw; - - // --- viewAngle wie im Beispiel: AbsRotation (Pitch/Yaw/Roll) + // viewAngle exemplarisch aus AbsRotation float angX = 0f, angY = 0f, angZ = 0f; try { var ang = pawn.AbsRotation; - angX = (float)ang.X; // pitch - angY = (float)ang.Y; // yaw - angZ = (float)ang.Z; // roll + angX = (float)ang.X; + angY = (float)ang.Y; + angZ = (float)ang.Z; } catch { } - // --- Alive-Status bool isAlive = true; try { int ls = (int)pawn.LifeState; isAlive = (ls == 0); } catch { } if (!isAlive) { try { isAlive = ((int)pawn.Health) > 0; } catch { } } - // --- Minimales Player-Objekt + viewAngle + Vector3 eyePos; + TryGetEyePosition(pawn, out eyePos); // nutzt deine vorhandene TryGetEyePosition + + Vector3 pVel = Vector3.Zero; + try + { + var v = pawn.AbsVelocity; + pVel = new Vector3((float)v.X, (float)v.Y, (float)v.Z); + } + catch { /* ok */ } + + // Aim sinnvoll clampen/normalisieren + float spPitch = ClampPitch(angX); + float spYaw = NormalizeYaw(angY); + + var sid = p.AuthorizedSteamID?.SteamId64 ?? 0UL; + _lastPlayerSnap[sid] = new PlayerSnap { + Origin = new Vector3(posX, posY, posZ), + Eye = eyePos == default ? new Vector3(posX, posY, posZ + 64f) : eyePos, + Vel = pVel, + Pitch = spPitch, + Yaw = spYaw, + T = NowMs() + }; + + playersList.Add(new { - steamId = p.AuthorizedSteamID?.SteamId64 ?? 0UL, - name = p.PlayerName, - team = p.TeamNum, - pos = new { x = posX, y = posY, z = posZ }, + steamId = p.AuthorizedSteamID?.SteamId64 ?? 0UL, + name = p.PlayerName, + team = p.TeamNum, + pos = new { x = posX, y = posY, z = posZ }, viewAngle = new { pitch = angX, yaw = angY, roll = angZ }, - alive = isAlive + alive = isAlive }); } - catch { /* Spieler überspringen bei Fehlern */ } + catch { } } + // Nade-Update + Backfill + UpdateNadesInTick(); + if (playersList.Count == 0) return; var payload = new @@ -697,7 +759,756 @@ public class WebSocketTelemetryPlugin : BasePlugin Broadcast(JsonSerializer.Serialize(payload)); } + // ========================= + // Grenade tracking + // ========================= + private sealed class NadeInfo + { + public required int Id; + public required string Kind; + public CEntityInstance? Ent; // kann kurzzeitig null sein (nur Meta) + public ulong OwnerSteamId; + public long CreatedMs; + + public bool Announced; // create schon gesendet? + public (float x,float y,float z) LastPos; + public long LastT; + + // Backfill-Puffer + public bool FirstValidSeen; + public (long t, (float x,float y,float z) pos) FirstSample; + public bool SecondValidSeen; + public (long t, (float x,float y,float z) pos) SecondSample; + public bool FixSent; + + // Prediction + public bool PredSent; + } + + private int _nadeSeq = 0; + private readonly ConcurrentDictionary _nades = new(); // Id -> info + + private struct PlayerSnap + { + public Vector3 Origin; + public Vector3 Eye; + public Vector3 Vel; + public float Pitch; + public float Yaw; + public long T; + } + private readonly ConcurrentDictionary _lastPlayerSnap = new(); + + + private static bool IsValidPos((float x, float y, float z) p) + { + if (!float.IsFinite(p.x) || !float.IsFinite(p.y) || !float.IsFinite(p.z)) + return false; + if (Math.Abs(p.x) < 1f && Math.Abs(p.y) < 1f && Math.Abs(p.z) < 1f) + return false; + if (Math.Abs(p.x) > 100000f || Math.Abs(p.y) > 100000f || Math.Abs(p.z) > 100000f) + return false; + return true; + } + + // ========================= + // Helpers + // ========================= + + private static (float x, float y, float z) ReadInitialNadePos(dynamic ent) + { + try { var v = ent.m_vInitialPosition; return ((float)v.X, (float)v.Y, (float)v.Z); } catch {} + try { var v = ent.InitialPosition; return ((float)v.X, (float)v.Y, (float)v.Z); } catch {} + return (0f, 0f, 0f); + } + + private static (float x, float y, float z) ReadInitialNadeVel(dynamic ent) + { + try { var v = ent.m_vInitialVelocity; return ((float)v.X, (float)v.Y, (float)v.Z); } catch {} + try { var v = ent.InitialVelocity; return ((float)v.X, (float)v.Y, (float)v.Z); } catch {} + return (0f, 0f, 0f); + } + + private static bool IsGrenadeDesignerName(string? name) + { + if (string.IsNullOrEmpty(name)) return false; + name = name.ToLowerInvariant(); + return name.Contains("grenade_projectile") || + name.Contains("flashbang_projectile") || + name.Contains("smokegrenade_projectile") || + name.Contains("molotov_projectile") || + name.Contains("decoy_projectile"); + } + + private static string KindFromDesignerName(string name) + { + name = name.ToLowerInvariant(); + if (name.Contains("hegrenade")) return "he"; + if (name.Contains("flashbang")) return "flash"; + if (name.Contains("smokegrenade")) return "smoke"; + if (name.Contains("molotov")) return "molotov"; + if (name.Contains("decoy")) return "decoy"; + return "other"; + } + + private static (float pitch, float yaw, float roll) ReadAbsAngles(dynamic ent) + { + try { + var node = ent?.GameSceneNode; + if (node != null) { + try { var r = node.AbsRotation; return ((float)r.X, (float)r.Y, (float)r.Z); } catch {} + try { var tf = node.NodeToWorld; var a = tf.Angles; return ((float)a.X, (float)a.Y, (float)a.Z); } catch {} + } + } catch {} + + try { var r = ent?.AbsRotation; return ((float)r.X, (float)r.Y, (float)r.Z); } catch {} + + return (0f, 0f, 0f); + } + + private static (float x, float y, float z) ReadAbsOrigin(dynamic ent) + { + try { + var node = ent?.GameSceneNode; + if (node != null) { + try { var o = node.AbsOrigin; return ((float)o.X, (float)o.Y, (float)o.Z); } catch {} + try { var o = node.m_vecAbsOrigin; return ((float)o.X, (float)o.Y, (float)o.Z); } catch {} + try { var tf = node.NodeToWorld; var a = tf.Origin; return ((float)a.X, (float)a.Y, (float)a.Z); } catch {} + } + } catch {} + + try { + var bodyComp = ent?.CBodyComponent; + if (bodyComp != null) { + var nd = bodyComp.SceneNode; + if (nd != null) { + try { var o = nd.AbsOrigin; return ((float)o.X, (float)o.Y, (float)o.Z); } catch {} + try { var o = nd.m_vecAbsOrigin; return ((float)o.X, (float)o.Y, (float)o.Z); } catch {} + } + } + } catch {} + + try { var o = ent?.AbsOrigin; return ((float)o.X, (float)o.Y, (float)o.Z); } catch {} + try { var o = ent?.m_vecAbsOrigin; return ((float)o.X, (float)o.Y, (float)o.Z); } catch {} + + try { + var transform = ent?.m_pGameSceneNode?.m_nodeToWorld; + if (transform != null) { + return ((float)transform.m_vOrigin.X, (float)transform.m_vOrigin.Y, (float)transform.m_vOrigin.Z); + } + } catch {} + + try { + var physics = ent?.Physics; + if (physics != null) { + try { var o = physics.Origin; return ((float)o.X, (float)o.Y, (float)o.Z); } catch {} + try { var o = physics.Position; return ((float)o.X, (float)o.Y, (float)o.Z); } catch {} + } + } catch {} + + return ReadInitialNadePos(ent); + } + + private static (float x, float y, float z) ReadAbsVelocity(dynamic ent) + { + try { var v = ent?.AbsVelocity; return ((float)v.X, (float)v.Y, (float)v.Z); } catch {} + try { var v = ent?.m_vecAbsVelocity; return ((float)v.X, (float)v.Y, (float)v.Z); } catch {} + try { var v = ent?.Physics?.Velocity;return ((float)v.X, (float)v.Y, (float)v.Z); } catch {} + + return ReadInitialNadeVel(ent); + } + + private static ulong TryGetThrowerSteamId(dynamic projectileBase) + { + try + { + dynamic p = projectileBase; // dynamisch binden + + // 1) Thrower (Pawn) -> Controller -> SteamID + try + { + dynamic th = p.m_hThrower; // kann ein Handle sein + dynamic thVal = th is null ? null : (th.Value ?? th); + dynamic throwerPawn = thVal ?? p.Thrower; + + if (throwerPawn != null) + { + try + { + dynamic ctrl = throwerPawn.Controller; + if (ctrl != null) + return (ulong)(ctrl.AuthorizedSteamID?.SteamId64 ?? 0UL); + } + catch { /* weiter probieren */ } + } + } + catch { /* weiter */ } + + // 2) OwnerEntity (Pawn/Entity) -> Controller -> SteamID + try + { + dynamic oh = p.m_hOwnerEntity; + dynamic ohVal = oh is null ? null : (oh.Value ?? oh); + dynamic ownerEnt = ohVal ?? p.OwnerEntity; + + if (ownerEnt != null) + { + try + { + dynamic ctrl = ownerEnt.Controller; + if (ctrl != null) + return (ulong)(ctrl.AuthorizedSteamID?.SteamId64 ?? 0UL); + } + catch { } + } + } + catch { } + } + catch { } + + return 0UL; + } + + + // ========================= + // Prediction (vereinfachte Ballistik) + // ========================= + + private static Vector3 AnglesToForward(float pitchDeg, float yawDeg) + { + // Pitch: Up ist negativ in Source-Notation, deshalb Minus + float pitch = -pitchDeg * (float)Math.PI / 180f; + float yaw = yawDeg * (float)Math.PI / 180f; + float cp = MathF.Cos(pitch); + return new Vector3( + cp * MathF.Cos(yaw), + cp * MathF.Sin(yaw), + MathF.Sin(pitch) + ); + } + + // ========================= + // Hooks + // ========================= + + private void OnEntityCreated(CEntityInstance ent) + { + try + { + var name = ent.DesignerName ?? ""; + if (!IsGrenadeDesignerName(name)) return; + + var kind = KindFromDesignerName(name); + var id = Interlocked.Increment(ref _nadeSeq); + var owner = TryGetThrowerSteamId(ent); + + var info = new NadeInfo { + Id = id, Kind = kind, Ent = ent, OwnerSteamId = owner, + CreatedMs = NowMs(), + Announced = false, + LastPos = (0,0,0), + LastT = 0, + FirstValidSeen = false, + SecondValidSeen = false, + FixSent = false, + PredSent = false + }; + _nades[id] = info; + + // Prediction mit Retry; fällt auf Spieler-Snapshot zurück, wenn Projectile-Netvars leer sind + if (_predictEnabled) + { + const int maxTries = 10; // bis zu ~10 Versuche + const float tryInterval = 0.016f; // alle ~1 Frame (bei ~64 tick) + int attempt = 0; + + void TryPredict() + { + static string V3(Vector3 v) => $"({v.X:F2},{v.Y:F2},{v.Z:F2})"; + static string T3((float x,float y,float z) t) => $"({t.x:F2},{t.y:F2},{t.z:F2})"; + + try + { + attempt++; + Logger.LogInformation($"[WS-PRED] fire id={id} kind={kind}: attempt {attempt}/{maxTries}"); + + if (ent == null || !ent.IsValid || !_nades.ContainsKey(id)) + { + Logger.LogWarning($"[WS-PRED] id={id} kind={kind}: entity invalid or gone -> abort"); + return; + } + + if (attempt == 1) + { + try + { + var dn = ent.DesignerName ?? "(null)"; + var typeName = ent.GetType().Name; + Logger.LogInformation($"[WS-PRED] id={id} kind={kind}: DesignerName='{dn}', CLR='{typeName}' owner={owner}"); + } + catch { } + } + + // 1) Pos/Vel aus Netvars (oder initial) lesen + var pAbs = ReadAbsOrigin(ent); + bool pAbsValid = IsValidPos(pAbs); + Logger.LogInformation($"[WS-PRED] id={id} kind={kind}: AbsOrigin={T3(pAbs)} valid={pAbsValid}"); + + (float x,float y,float z) p0 = pAbs; + if (!pAbsValid) + { + var pInit = ReadInitialNadePos(ent); + bool pInitValid = IsValidPos(pInit); + Logger.LogInformation($"[WS-PRED] id={id} kind={kind}: InitialPos={T3(pInit)} valid={pInitValid}"); + if (pInitValid) p0 = pInit; + } + + var vAbs = ReadAbsVelocity(ent); + var vInit = ReadInitialNadeVel(ent); + Logger.LogInformation($"[WS-PRED] id={id} kind={kind}: AbsVel=({vAbs.x:F2},{vAbs.y:F2},{vAbs.z:F2}) | InitVel=({vInit.x:F2},{vInit.y:F2},{vInit.z:F2})"); + + bool posOk = IsValidPos(p0); + bool velOk = !(vAbs.x==0 && vAbs.y==0 && vAbs.z==0) || !(vInit.x==0 && vInit.y==0 && vInit.z==0); + + Vector3 start3 = posOk ? new Vector3(p0.x, p0.y, p0.z) : Vector3.Zero; + Vector3 v0 = Vector3.Zero; + if (!(vAbs.x==0 && vAbs.y==0 && vAbs.z==0)) v0 = new Vector3(vAbs.x, vAbs.y, vAbs.z); + else if (!(vInit.x==0 && vInit.y==0 && vInit.z==0)) v0 = new Vector3(vInit.x, vInit.y, vInit.z); + + // 2) Fallback: Spieler-Snapshot (Owner → jüngster Spieler) + if (start3 == Vector3.Zero || v0 == Vector3.Zero) + { + bool haveSnap = false; + PlayerSnap snap = default; + + if (owner != 0 && _lastPlayerSnap.TryGetValue(owner, out snap)) + { + haveSnap = true; + Logger.LogInformation($"[WS-PRED] id={id} kind={kind}: using owner snap steamid={owner}"); + } + else + { + // jüngster Snapshot eines Spielers + ulong picked = 0; long bestT = 0; + foreach (var kv in _lastPlayerSnap) + { + if (kv.Value.T > bestT) + { + picked = kv.Key; bestT = kv.Value.T; snap = kv.Value; + } + } + if (picked != 0) { haveSnap = true; Logger.LogInformation($"[WS-PRED] id={id} kind={kind}: using most recent player snap steamid={picked}"); } + } + + if (haveSnap) + { + var snapStart = snap.Eye == default ? snap.Origin + new Vector3(0,0,64f) : snap.Eye; + var dir = AnglesToForward(snap.Pitch, snap.Yaw); + + if (start3 == Vector3.Zero) start3 = snapStart; + if (v0 == Vector3.Zero) v0 = dir * 750f + snap.Vel; + + Logger.LogInformation($"[WS-PRED] id={id} kind={kind}: snap start={V3(start3)} aim=({snap.Pitch:F1},{snap.Yaw:F1}) v0={V3(v0)}"); + } + else + { + // kein Snap — ggf. nochmal warten + if (!posOk && attempt < maxTries) + { + Logger.LogInformation($"[WS-PRED] id={id} kind={kind}: no pos & no snap -> retry in {tryInterval*1000:F0}ms"); + AddTimer(tryInterval, TryPredict); + return; + } + } + } + + // Wenn immer noch kein Start: letzter Versuch (nochmal warten) + if (start3 == Vector3.Zero) + { + if (attempt < maxTries) + { + Logger.LogInformation($"[WS-PRED] id={id} kind={kind}: start still unknown -> retry in {tryInterval*1000:F0}ms"); + AddTimer(tryInterval, TryPredict); + return; + } + Logger.LogWarning($"[WS-PRED] id={id} kind={kind}: giving up (no start after {attempt} tries)"); + return; + } + + // Velocity notfalls aus Richtung schätzen (falls bis hier noch 0) + if (v0 == Vector3.Zero) + { + // Schätze Richtung aus Start → (falls möglich) nächsten Tick – sonst nimm Eye-Aim aus jüngstem Snap + Vector3 estDir = Vector3.UnitX; // dummy + bool gotDir = false; + // quickest: nimm jüngsten Snap + ulong bestSid=0; long bestTs=0; PlayerSnap bestSnap=default; + foreach (var kv in _lastPlayerSnap) if (kv.Value.T > bestTs) { bestSid=kv.Key; bestTs=kv.Value.T; bestSnap=kv.Value; } + if (bestSid!=0) + { + estDir = AnglesToForward(bestSnap.Pitch, bestSnap.Yaw); + gotDir = true; + } + if (!gotDir) estDir = Vector3.UnitX; + + v0 = estDir * 750f; + Logger.LogInformation($"[WS-PRED] id={id} kind={kind}: v0 missing -> estimated v0={V3(v0)}"); + } + + float dt = GetTickInterval(); + Logger.LogInformation($"[WS-PRED] id={id} kind={kind}: start={V3(start3)} v0={V3(v0)} dt={dt:F4} points={_predPoints}"); + + var pts = PredictPathFromState(start3, v0, _predPoints, dt, 800f, includeStart: true); + if (pts.Count == 0) + { + if (attempt < maxTries) + { + Logger.LogInformation($"[WS-PRED] id={id} kind={kind}: 0 points -> retry in {tryInterval*1000:F0}ms"); + AddTimer(tryInterval, TryPredict); + return; + } + Logger.LogWarning($"[WS-PRED] id={id} kind={kind}: 0 points after {attempt} tries -> give up"); + return; + } + + Broadcast(JsonSerializer.Serialize(new + { + type = "nade_pred", + t = NowMs(), + id = id, + kind = kind, + owner = owner, + points = pts + })); + Logger.LogInformation($"[WS-PRED] id={id} kind={kind}: nade_pred broadcast sent ({pts.Count} pts)"); + + if (_nades.TryGetValue(id, out var n)) n.PredSent = true; + } + catch (Exception exOuter) + { + Logger.LogError($"[WS-PRED] id={id} kind={kind}: outer exception: {exOuter.Message}"); + } + } + + AddTimer(tryInterval, TryPredict); + } + + // Kleiner Delay, bis Netvars sicher da sind → create senden + AddTimer(0.05f, () => + { + try + { + if (ent == null || !ent.IsValid || !_nades.ContainsKey(id)) return; + + var pos = ReadAbsOrigin(ent); + var vel = ReadAbsVelocity(ent); + + if (!IsValidPos(pos)) + pos = ReadInitialNadePos(ent); + + if (!IsValidPos(pos)) + return; + + var ang = ReadAbsAngles(ent); + + if (_nades.TryGetValue(id, out var n)) + { + n.LastPos = pos; + n.LastT = NowMs(); + n.Announced = true; + n.FirstValidSeen = true; + n.FirstSample = (n.LastT, pos); + } + + Broadcast(JsonSerializer.Serialize(new { + type = "nade_create", + t = NowMs(), + id = id, + kind = kind, + owner= owner, + pos = new { x = pos.x, y = pos.y, z = pos.z }, + vel = new { x = vel.x, y = vel.y, z = vel.z }, + ang = new { pitch = ang.pitch, yaw = ang.yaw, roll = ang.roll } + })); + } + catch { /* ignore */ } + }); + } + catch { /* ignore */ } + } + + + private static List PredictPathFromState( + Vector3 start, Vector3 v0, int points, float dt, float gravityAbs = 800f, bool includeStart = true) + { + var list = new List(points); + Vector3 p = start; + Vector3 v = v0; + Vector3 a = new Vector3(0, 0, -gravityAbs); + + if (includeStart) + list.Add(new { x = p.X, y = p.Y, z = p.Z }); // Punkt 0 = exakter Start + + for (int i = includeStart ? 1 : 0; i < points; i++) + { + p += v * dt + 0.5f * a * dt * dt; + v += a * dt; + list.Add(new { x = p.X, y = p.Y, z = p.Z }); + if (MathF.Abs(p.X) > 200000f || MathF.Abs(p.Y) > 200000f || MathF.Abs(p.Z) > 200000f) + break; + } + return list; + } + + + private void UpdateNadesInTick() + { + if (_nades.IsEmpty) return; + + var nades = new List(); + var toRemove = new List(); + var now = NowMs(); + + foreach (var kv in _nades) + { + var n = kv.Value; + try + { + if (n.Ent == null || !n.Ent.IsValid) + { + // bis Detonate/Event + continue; + } + + var pos = ReadAbsOrigin(n.Ent); + var vel = ReadAbsVelocity(n.Ent); + + if (!IsValidPos(pos)) + continue; + + // --- NEU: Prediction hier nachreichen, sobald wir zum ersten Mal echte Daten sehen --- + if (_predictEnabled && !n.PredSent) + { + try + { + float dt = GetTickInterval(); + var start3 = new Vector3(pos.x, pos.y, pos.z); + var v0 = new Vector3(vel.x, vel.y, vel.z); + + Logger.LogInformation( + $"[WS-PRED] late id={n.Id} kind={n.Kind}: start=({start3.X:F2},{start3.Y:F2},{start3.Z:F2}) " + + $"v0=({v0.X:F2},{v0.Y:F2},{v0.Z:F2}) dt={dt:F4} points={_predPoints}"); + + var pts = PredictPathFromState(start3, v0, _predPoints, dt, 800f, includeStart: true); + + if (pts.Count > 0) + { + Broadcast(JsonSerializer.Serialize(new + { + type = "nade_pred", + t = NowMs(), + id = n.Id, + kind = n.Kind, + owner = n.OwnerSteamId, + points = pts + })); + n.PredSent = true; + Logger.LogInformation($"[WS-PRED] late id={n.Id} kind={n.Kind}: nade_pred broadcast sent ({pts.Count} pts)"); + } + else + { + Logger.LogWarning($"[WS-PRED] late id={n.Id} kind={n.Kind}: 0 points (skip)"); + } + } + catch (Exception ex) + { + Logger.LogError($"[WS-PRED] late id={n.Id} kind={n.Kind}: exception: {ex.Message}"); + } + } + // --- ENDE NEU --- + + // First/Second-Sample Logik für Backfill (deins, unverändert) … + if (IsValidPos(pos)) + { + if (!n.FirstValidSeen) + { + n.FirstValidSeen = true; + n.FirstSample = (now, pos); + } + else if (!n.SecondValidSeen) + { + n.SecondValidSeen = true; + n.SecondSample = (now, pos); + var initVel = ReadInitialNadeVel(n.Ent); + float dtf = Math.Max(GetTickInterval(), ((n.SecondSample.t - n.FirstSample.t) / 1000f)); + Vector3 p1 = new Vector3(n.FirstSample.pos.x, n.FirstSample.pos.y, n.FirstSample.pos.z); + Vector3 v1; + + if (initVel.x != 0 || initVel.y != 0 || initVel.z != 0) + v1 = new Vector3(initVel.x, initVel.y, initVel.z); + else + { + Vector3 p2 = new Vector3(n.SecondSample.pos.x, n.SecondSample.pos.y, n.SecondSample.pos.z); + float dtt = Math.Max(0.001f, (n.SecondSample.t - n.FirstSample.t) / 1000f); + v1 = (p2 - p1) / dtt; + } + + Vector3 a = new Vector3(0, 0, -800f); + Vector3 p0 = p1 - v1 * dtf - 0.5f * a * dtf * dtf; + + if (!n.FixSent && IsFinite(p0.X) && IsFinite(p0.Y) && IsFinite(p0.Z)) + { + n.FixSent = true; + Broadcast(JsonSerializer.Serialize(new + { + type = "nade_fix_start", + t = NowMs(), + id = n.Id, + pos = new { x = p0.X, y = p0.Y, z = p0.Z } + })); + } + } + } + + // Falls create noch nicht raus ist, jetzt nachholen (deins, unverändert) … + if (!n.Announced && IsValidPos(pos)) + { + var ang = ReadAbsAngles(n.Ent); + n.Announced = true; + n.LastPos = pos; + n.LastT = now; + + Broadcast(JsonSerializer.Serialize(new + { + type = "nade_create", + t = now, + id = n.Id, + kind = n.Kind, + owner = n.OwnerSteamId, + pos = new { x = pos.x, y = pos.y, z = pos.z }, + vel = new { x = vel.x, y = vel.y, z = vel.z }, + ang = new { pitch = ang.pitch, yaw = ang.yaw, roll = ang.roll } + })); + } + + nades.Add(new + { + id = n.Id, + kind = n.Kind, + owner = n.OwnerSteamId, + pos = new { x = pos.x, y = pos.y, z = pos.z }, + vel = new { x = vel.x, y = vel.y, z = vel.z } + }); + + n.LastPos = pos; + n.LastT = now; + } + catch (Exception) + { + toRemove.Add(kv.Key); + } + } + + if (nades.Count > 0) + { + var nadePayload = new + { + type = "nades", + t = NowMs(), + nades + }; + Broadcast(JsonSerializer.Serialize(nadePayload)); + } + + foreach (var id in toRemove) + _nades.TryRemove(id, out _); + } + + + private void OnEntityDeleted(CEntityInstance ent) + { + try + { + foreach (var kv in _nades) + { + if (ReferenceEquals(kv.Value.Ent, ent)) + { + _nades.TryRemove(kv.Key, out _); + break; + } + } + } + catch { } + } + + // ========================= + // Detonation Handler / Cleanup + // ========================= + + private HookResult OnSmokeDetonate(EventSmokegrenadeDetonate ev, GameEventInfo info) + { + return HandleExplodeGeneric("smoke", ev.X, ev.Y, ev.Z); + } + private HookResult OnHeDetonate(EventHegrenadeDetonate ev, GameEventInfo info) + { + return HandleExplodeGeneric("he", ev.X, ev.Y, ev.Z); + } + private HookResult OnFlashDetonate(EventFlashbangDetonate ev, GameEventInfo info) + { + return HandleExplodeGeneric("flash", ev.X, ev.Y, ev.Z); + } + private HookResult OnMolotovDetonate(EventMolotovDetonate ev, GameEventInfo info) + { + return HandleExplodeGeneric("molotov", ev.X, ev.Y, ev.Z); + } + private HookResult OnDecoyStart(EventDecoyStarted ev, GameEventInfo info) + { + Broadcast(JsonSerializer.Serialize(new { + type = "nade_decoy_start", t = NowMs(), + pos = new { x = ev.X, y = ev.Y, z = ev.Z } + })); + return HookResult.Continue; + } + private HookResult OnDecoyDetonate(EventDecoyDetonate ev, GameEventInfo info) + { + return HandleExplodeGeneric("decoy", ev.X, ev.Y, ev.Z); + } + + private HookResult HandleExplodeGeneric(string kind, float x, float y, float z) + { + int removeId = -1; + foreach (var kv in _nades) + { + if (kv.Value.Kind == kind) + { + removeId = kv.Key; + break; + } + } + if (removeId != -1 && _nades.TryRemove(removeId, out var info)) + { + Broadcast(JsonSerializer.Serialize(new { + type = "nade_explode", + t = NowMs(), + id = info.Id, + kind = info.Kind, + owner = info.OwnerSteamId, + pos = new { x, y, z } + })); + } + else + { + Broadcast(JsonSerializer.Serialize(new { + type = "nade_explode", + t = NowMs(), + id = (int?)null, + kind, + pos = new { x, y, z } + })); + } + return HookResult.Continue; + } // ========================= // WS(S)-Server / Broadcast @@ -1070,8 +1881,8 @@ public class WebSocketTelemetryPlugin : BasePlugin lock (c.SendLock) { - var buf = ms.GetBuffer(); - c.Stream.Write(buf, 0, (int)ms.Length); + var buf = ms.ToArray(); + c.Stream.Write(buf, 0, buf.Length); c.Stream.Flush(); } } diff --git a/CS2WebSocketTelemetryPlugin/bin/Release/net8.0/CS2WebSocketTelemetryPlugin.dll b/CS2WebSocketTelemetryPlugin/bin/Release/net8.0/CS2WebSocketTelemetryPlugin.dll index 69a1e2e..3eeba42 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 e441688..267151a 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 deleted file mode 100644 index 47804f2..0000000 --- a/CS2WebSocketTelemetryPlugin/bin/Release/net8.0/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "url": "wss://ws.ironieopen.de:8081/telemetry", - "certPath": "cert.pfx", - "certPassword": "Timmy0104199?", - "sendHz": 10 -} diff --git a/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfo.cs b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfo.cs index 1d2c16f..5973c9a 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+43a13a88f47d899ca8efcc8a2093f5bd4191cd48")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+ef5b771986e375fbf520c64f76d67dd104f3f5d7")] [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 new file mode 100644 index 0000000..a78e45c --- /dev/null +++ b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +2ecdd85e0196dc62d4eabd8acdca165a11046a2240dfca955a2362f9105bf305 diff --git a/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.dll b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/CS2WebSocketTelemetryPlugin.dll index 69a1e2e..3eeba42 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 e441688..267151a 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 748acab..9687164 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 748acab..9687164 100644 Binary files a/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/refint/CS2WebSocketTelemetryPlugin.dll and b/CS2WebSocketTelemetryPlugin/obj/Release/net8.0/refint/CS2WebSocketTelemetryPlugin.dll differ