# Conflicts:
#	CS2WebSocketTelemetryPlugin/obj/Debug/net8.0/CS2WebSocketTelemetryPlugin.AssemblyInfo.cs
This commit is contained in:
Linrador 2025-09-22 07:55:56 +02:00
commit a8e6f612ee
11 changed files with 220 additions and 29 deletions

View File

@ -21,6 +21,7 @@ using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Cvars;
using Microsoft.Extensions.Logging;
namespace WsTelemetry;
@ -46,11 +47,16 @@ public class MetaWebSocketPlugin : BasePlugin
private X509Certificate2? _serverCert;
private volatile string _mapName = "";
private volatile string _serverName = "";
private int _scoreCT = 0;
private int _scoreT = 0;
private TcpListener? _listener;
private CancellationTokenSource? _serverCts;
private Task? _acceptTask;
private volatile bool _serverRunning = false;
private volatile string _phase = "unknown";
private readonly ConcurrentDictionary<int, ClientState> _clients = new();
private int _clientSeq = 0;
@ -63,6 +69,14 @@ public class MetaWebSocketPlugin : BasePlugin
public string? CertPassword { get; set; } // optional
}
private void SetPhase(string p)
{
var s = (p ?? "unknown").ToLowerInvariant();
if (s == _phase) return;
_phase = s;
Broadcast(JsonSerializer.Serialize(new { type = "phase", phase = _phase, t = NowMs() }));
}
private sealed class ClientState
{
public required TcpClient Tcp;
@ -74,17 +88,44 @@ public class MetaWebSocketPlugin : BasePlugin
// ------------- Helpers -------------
private static long NowMs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
private string BuildMapPayload()
{
return JsonSerializer.Serialize(new {
type = "map",
name = _mapName,
serverName = _serverName,
t = NowMs()
});
}
private void TrySafeInitialPush()
{
try
{
var mn = Server.MapName; // jetzt sicher, weil NextFrame/MapStart
if (!string.IsNullOrEmpty(mn))
RefreshServerName();
// Map-Name aus dem Server holen (erst im NextFrame sicher verfügbar)
var mn = Server.MapName;
if (!string.IsNullOrWhiteSpace(mn))
{
_mapName = mn!;
Broadcast(JsonSerializer.Serialize(new { type = "map", name = _mapName, t = NowMs() }));
// Map + (aktuelle) Phase in EINEM Payload
Broadcast(BuildMapPayload());
}
SendFullPlayerList(); // nur hier oder aus Events heraus aufrufen
// Vollständige Spielerliste
SendFullPlayerList();
// Phase separat noch einmal senden (falls ein Client erst danach connected)
Broadcast(JsonSerializer.Serialize(new {
type = "phase",
phase = _phase,
t = NowMs()
}));
// Optional: aktuellen Score mitschicken (falls du BroadcastScore() implementiert hast)
BroadcastScore();
}
catch (Exception ex)
{
@ -92,6 +133,26 @@ public class MetaWebSocketPlugin : BasePlugin
}
}
private void RefreshServerName()
{
try
{
// Standard in Source/CS2
var s = ConVar.Find("hostname")?.StringValue;
if (!string.IsNullOrWhiteSpace(s))
{
_serverName = s!;
return;
}
// Fallback (vereinzelt genutzt)
s = ConVar.Find("host_name")?.StringValue;
if (!string.IsNullOrWhiteSpace(s))
_serverName = s!;
}
catch { /* ignore */ }
}
private void LoadAndApplyConfig(bool generateIfMissing = true)
{
try
@ -197,6 +258,18 @@ public class MetaWebSocketPlugin : BasePlugin
RegisterEventHandler<EventPlayerConnectFull>(OnPlayerConnectFull);
RegisterEventHandler<EventPlayerDisconnect>(OnPlayerDisconnect);
// Runden-/Bomben-Phase
RegisterEventHandler<EventRoundStart>(OnRoundStart);
RegisterEventHandler<EventRoundFreezeEnd>(OnRoundFreezeEnd);
RegisterEventHandler<EventRoundEnd>(OnRoundEnd);
RegisterEventHandler<EventBombPlanted>(OnBombPlanted);
RegisterEventHandler<EventBombDefused>(OnBombDefused);
RegisterEventHandler<EventBombExploded>(OnBombExploded);
// (optional, falls verfügbar)
// RegisterEventHandler<EventWarmupStart>(OnWarmupStart);
// RegisterEventHandler<EventWarmupEnd>(OnWarmupEnd);
LoadAndApplyConfig();
if (_enabled) StartWebSocket();
@ -217,12 +290,21 @@ public class MetaWebSocketPlugin : BasePlugin
private void OnMapStart(string newMap)
{
_mapName = newMap ?? Server.MapName ?? "";
Broadcast(JsonSerializer.Serialize(new { type = "map", name = _mapName, t = NowMs() }));
// Nach Mapstart zusätzlich die Spielerliste pushen
// Score neu starten
_scoreCT = 0;
_scoreT = 0;
RefreshServerName();
SetPhase("warmup");
Broadcast(BuildMapPayload());
SendFullPlayerList();
BroadcastScore(); // gleich initial 0:0 senden
Logger.LogInformation($"[WS] Map gewechselt: '{(_mapName ?? "")}' Meta gesendet.");
}
private HookResult OnPlayerConnectFull(EventPlayerConnectFull ev, GameEventInfo info)
{
try
@ -274,6 +356,75 @@ public class MetaWebSocketPlugin : BasePlugin
return HookResult.Continue;
}
private HookResult OnRoundStart(EventRoundStart ev, GameEventInfo info)
{
// Start der Runde == Freezezeit
SetPhase("freezetime");
BroadcastScore();
return HookResult.Continue;
}
private HookResult OnRoundFreezeEnd(EventRoundFreezeEnd ev, GameEventInfo info)
{
// Ende Freeze -> Live
SetPhase("live");
BroadcastScore();
return HookResult.Continue;
}
private HookResult OnRoundEnd(EventRoundEnd ev, GameEventInfo info)
{
try
{
// Viele Builds haben ev.Winner (2 = T, 3 = CT).
// Falls in deinem Build anders, kannst du unten noch "WinnerTeam" per Reflection abfragen.
int? winner = null;
// direkt zugreifen, wenn Property existiert
try { winner = ev.Winner; } catch { /* Property evtl. nicht vorhanden */ }
// Fallback per Reflection (WinnerTeam / Team / …)
if (!winner.HasValue)
{
winner = TryGetIntProp(ev, "Winner")
?? TryGetIntProp(ev, "WinnerTeam")
?? TryGetIntProp(ev, "Team");
}
if (winner == 3) _scoreCT++;
else if (winner == 2) _scoreT++;
}
catch { /* ignore */ }
SetPhase("over");
BroadcastScore();
return HookResult.Continue;
}
private HookResult OnBombPlanted(EventBombPlanted ev, GameEventInfo info)
{
SetPhase("bomb");
BroadcastScore();
return HookResult.Continue;
}
private HookResult OnBombDefused(EventBombDefused ev, GameEventInfo info)
{
// Bombe ist weg, Runde läuft weiter bis RoundEnd
SetPhase("live");
return HookResult.Continue;
}
private HookResult OnBombExploded(EventBombExploded ev, GameEventInfo info)
{
SetPhase("over");
return HookResult.Continue;
}
// (optional)
// private HookResult OnWarmupStart(EventWarmupStart ev, GameEventInfo info) { SetPhase("warmup"); return HookResult.Continue; }
// private HookResult OnWarmupEnd(EventWarmupEnd ev, GameEventInfo info) { SetPhase("freezetime"); return HookResult.Continue; }
// ------------- Console Commands -------------
[ConsoleCommand("css_meta_enable", "WS(S)-Server aktivieren/deaktivieren: css_meta_enable 1|0")]
[CommandHelper(minArgs: 1, usage: "<1|0>")]
@ -385,7 +536,7 @@ public class MetaWebSocketPlugin : BasePlugin
[ConsoleCommand("css_meta_sendmap", "Sendet die aktuelle Karte an alle verbundenen Clients")]
public void CmdSendMap(CCSPlayerController? caller, CommandInfo cmd)
{
Broadcast(JsonSerializer.Serialize(new { type = "map", name = _mapName, t = NowMs() }));
Broadcast(BuildMapPayload());
cmd.ReplyToCommand($"[WS] Map '{_mapName}' an Clients gesendet.");
}
@ -552,14 +703,16 @@ public class MetaWebSocketPlugin : BasePlugin
try
{
if (!_clients.ContainsKey(id) || state.Cts.IsCancellationRequested) return;
var nowMs = NowMs();
SendTextFrame(state, JsonSerializer.Serialize(new { type = "map", name = _mapName, t = nowMs }));
var buf = BuildPlayersPayload(); // jetzt safe
SendTextFrame(state, BuildMapPayload()); // ✅ Map ohne Phase
var buf = BuildPlayersPayload();
SendTextFrame(state, buf);
// ✅ Phase EINMAL separat für den neu verbundenen Client
SendTextFrame(state, JsonSerializer.Serialize(new { type = "phase", phase = _phase, t = NowMs() }));
}
catch { /* ignore */ }
});
await ReceiveLoop(state, serverCt);
}
catch (OperationCanceledException) { }
@ -599,6 +752,27 @@ public class MetaWebSocketPlugin : BasePlugin
return JsonSerializer.Serialize(new { type = "players", t = NowMs(), players = list });
}
// kleiner Helper für obige Reflection-Zugriffe
private static int? TryGetIntProp(object obj, string prop)
{
try
{
var pi = obj.GetType().GetProperty(prop, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (pi != null)
{
var v = pi.GetValue(obj);
if (v is int i) return i;
}
}
catch { }
return null;
}
private void BroadcastScore()
{
Broadcast(JsonSerializer.Serialize(new { type = "score", ct = _scoreCT, t = _scoreT, tms = NowMs() }));
}
private void Broadcast(string text)
{
foreach (var kv in _clients)

View File

@ -5,18 +5,18 @@
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\Rother\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\Chris\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.0</NuGetToolVersion>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.11.1</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\Rother\.nuget\packages\" />
<SourceRoot Include="C:\Users\Chris\.nuget\packages\" />
<SourceRoot Include="C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages\" />
</ItemGroup>
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)microsoft.extensions.configuration.usersecrets\8.0.0\buildTransitive\net6.0\Microsoft.Extensions.Configuration.UserSecrets.props" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.configuration.usersecrets\8.0.0\buildTransitive\net6.0\Microsoft.Extensions.Configuration.UserSecrets.props')" />
</ImportGroup>
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<PkgMicrosoft_DotNet_ApiCompat_Task Condition=" '$(PkgMicrosoft_DotNet_ApiCompat_Task)' == '' ">C:\Users\Rother\.nuget\packages\microsoft.dotnet.apicompat.task\8.0.203</PkgMicrosoft_DotNet_ApiCompat_Task>
<PkgMicrosoft_DotNet_ApiCompat_Task Condition=" '$(PkgMicrosoft_DotNet_ApiCompat_Task)' == '' ">C:\Users\Chris\.nuget\packages\microsoft.dotnet.apicompat.task\8.0.203</PkgMicrosoft_DotNet_ApiCompat_Task>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,22 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
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+20e18fca62bdfacbfc20e6e98cbe220dc389b3cb")]
[assembly: System.Reflection.AssemblyProductAttribute("CS2WebSocketTelemetryPlugin")]
[assembly: System.Reflection.AssemblyTitleAttribute("CS2WebSocketTelemetryPlugin")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// Von der MSBuild WriteCodeFragment-Klasse generiert.

View File

@ -8,8 +8,6 @@ build_property.PlatformNeutralAssembly =
build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = CS2WebSocketTelemetryPlugin
build_property.ProjectDir = C:\Users\Rother\fork\ironie-cs2-websocket-plugin\CS2WebSocketTelemetryPlugin\
build_property.ProjectDir = C:\Users\Chris\fork\ironie-cs2-websocket-plugin\CS2WebSocketTelemetryPlugin\
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.EffectiveAnalysisLevelStyle = 8.0
build_property.EnableCodeStyleSeverity =

View File

@ -2555,31 +2555,29 @@
]
},
"packageFolders": {
"C:\\Users\\Rother\\.nuget\\packages\\": {},
"C:\\Users\\Chris\\.nuget\\packages\\": {},
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages": {}
},
"project": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "C:\\Users\\Rother\\fork\\ironie-cs2-websocket-plugin\\CS2WebSocketTelemetryPlugin\\CS2WebSocketTelemetryPlugin.csproj",
"projectUniqueName": "C:\\Users\\Chris\\fork\\ironie-cs2-websocket-plugin\\CS2WebSocketTelemetryPlugin\\CS2WebSocketTelemetryPlugin.csproj",
"projectName": "CS2WebSocketTelemetryPlugin",
"projectPath": "C:\\Users\\Rother\\fork\\ironie-cs2-websocket-plugin\\CS2WebSocketTelemetryPlugin\\CS2WebSocketTelemetryPlugin.csproj",
"packagesPath": "C:\\Users\\Rother\\.nuget\\packages\\",
"outputPath": "C:\\Users\\Rother\\fork\\ironie-cs2-websocket-plugin\\CS2WebSocketTelemetryPlugin\\obj\\",
"projectPath": "C:\\Users\\Chris\\fork\\ironie-cs2-websocket-plugin\\CS2WebSocketTelemetryPlugin\\CS2WebSocketTelemetryPlugin.csproj",
"packagesPath": "C:\\Users\\Chris\\.nuget\\packages\\",
"outputPath": "C:\\Users\\Chris\\fork\\ironie-cs2-websocket-plugin\\CS2WebSocketTelemetryPlugin\\obj\\",
"projectStyle": "PackageReference",
"fallbackFolders": [
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
],
"configFilePaths": [
"C:\\Users\\Rother\\AppData\\Roaming\\NuGet\\NuGet.Config",
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config",
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
"C:\\Users\\Chris\\AppData\\Roaming\\NuGet\\NuGet.Config",
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config"
],
"originalTargetFrameworks": [
"net8.0"
],
"sources": {
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
@ -2597,8 +2595,7 @@
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "9.0.300"
}
},
"frameworks": {
"net8.0": {
@ -2625,7 +2622,7 @@
"privateAssets": "all"
}
},
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.408/PortableRuntimeIdentifierGraph.json"
}
}
}