WebPanelBridge 0.1.0: live server status for simpleadmin-web over RCON
This commit is contained in:
commit
d02c8454df
5 changed files with 257 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
bin/
|
||||
obj/
|
||||
release-out/
|
||||
44
README.md
Normal file
44
README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# WebPanelBridge
|
||||
|
||||
Reports live match state to [simpleadmin-web](../../go/simpleadmin-web) over RCON: the hostname,
|
||||
map, team scores, warmup state, and each connected player's userid, name, SteamID64, team,
|
||||
kills, deaths and ping. It changes nothing in the game.
|
||||
|
||||
Built for CounterStrikeSharp **1.0.375** (`net10.0`) on Metamod:Source 2.0.0.1469. No other
|
||||
dependencies.
|
||||
|
||||
## The command
|
||||
|
||||
`css_webpanel_status` is server-only: it runs from the server console or RCON, never from a
|
||||
player. It prints one JSON object per line, each prefixed with `wpb `, then `wpb end`:
|
||||
|
||||
```
|
||||
wpb {"v":1,"hostname":"zio.sh | Random Skills","map":"de_mirage","maxPlayers":24,"scoreT":7,"scoreCt":9,"warmup":false}
|
||||
wpb {"userid":2,"name":"astra","steamid":"76561198012345601","team":3,"kills":21,"deaths":9,"ping":12,"bot":false}
|
||||
wpb end
|
||||
```
|
||||
|
||||
`team` is 1 for spectators, 2 for T and 3 for CT. Bots have `steamid` "0". `maxPlayers` is
|
||||
`sv_visiblemaxplayers` when that's set, otherwise the slot count.
|
||||
|
||||
The prefix lets the panel pick these lines out of anything else the server prints into the same
|
||||
RCON response. `v` is the format version; the panel rejects output it doesn't recognise.
|
||||
|
||||
## Install
|
||||
|
||||
Build with `cd plugins/WebPanelBridge && ../../build.sh`. Then copy the contents of
|
||||
`compiled/WebPanelBridge/` to `game/csgo/addons/counterstrikesharp/plugins/WebPanelBridge/` on the
|
||||
server.
|
||||
|
||||
Or install a release, which unpacks into `game/csgo/`:
|
||||
|
||||
```
|
||||
tar -xzf WebPanelBridge-<tag>.tar.gz -C /srv/cs2/game/csgo
|
||||
```
|
||||
|
||||
To publish one, commit, bump `ModuleVersion`, then run
|
||||
`FORGEJO_TOKEN=... ./release.sh v<ModuleVersion>`. It rebuilds the plugin from the committed
|
||||
source in the SDK container, tags HEAD, and uploads `WebPanelBridge-<tag>.tar.gz` to the git.zio.sh
|
||||
release. The plugin has no config.
|
||||
|
||||
Check it from RCON with `css_webpanel_status`.
|
||||
79
WebPanelBridge.cs
Normal file
79
WebPanelBridge.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using CounterStrikeSharp.API.Modules.Cvars;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
|
||||
namespace WebPanelBridge;
|
||||
|
||||
// Reports live match state to simpleadmin-web over RCON. The server console command prints one
|
||||
// JSON object per line, each prefixed with "wpb ", so the web panel can pick them out of whatever
|
||||
// else lands in the RCON response. Nothing here changes game state.
|
||||
public class WebPanelBridge : BasePlugin
|
||||
{
|
||||
public override string ModuleName => "WebPanelBridge";
|
||||
public override string ModuleVersion => "0.1.0";
|
||||
public override string ModuleAuthor => "astra";
|
||||
public override string ModuleDescription => "Live server status for simpleadmin-web, over RCON";
|
||||
|
||||
private const string Prefix = "wpb ";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
};
|
||||
|
||||
private record ServerLine(int V, string Hostname, string Map, int MaxPlayers, int ScoreT, int ScoreCt, bool Warmup);
|
||||
|
||||
private record PlayerLine(int Userid, string Name, string Steamid, int Team, int Kills, int Deaths, uint Ping, bool Bot);
|
||||
|
||||
[ConsoleCommand("css_webpanel_status", "Prints live server status as JSON lines for simpleadmin-web")]
|
||||
[CommandHelper(whoCanExecute: CommandUsage.SERVER_ONLY)]
|
||||
public void OnStatus(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
int scoreT = 0, scoreCt = 0;
|
||||
foreach (var team in Utilities.FindAllEntitiesByDesignerName<CCSTeam>("cs_team_manager"))
|
||||
{
|
||||
if (team.TeamNum == (byte)CsTeam.Terrorist)
|
||||
scoreT = team.Score;
|
||||
else if (team.TeamNum == (byte)CsTeam.CounterTerrorist)
|
||||
scoreCt = team.Score;
|
||||
}
|
||||
|
||||
var warmup = Utilities.FindAllEntitiesByDesignerName<CCSGameRulesProxy>("cs_gamerules")
|
||||
.FirstOrDefault()?.GameRules?.WarmupPeriod ?? false;
|
||||
|
||||
var maxPlayers = Server.MaxPlayers;
|
||||
var visible = ConVar.Find("sv_visiblemaxplayers")?.GetPrimitiveValue<int>() ?? -1;
|
||||
if (visible > 0)
|
||||
maxPlayers = visible;
|
||||
|
||||
var server = new ServerLine(1, ConVar.Find("hostname")?.StringValue ?? "", Server.MapName, maxPlayers,
|
||||
scoreT, scoreCt, warmup);
|
||||
command.ReplyToCommand(Prefix + JsonSerializer.Serialize(server, JsonOptions));
|
||||
|
||||
foreach (var player in Utilities.GetPlayers())
|
||||
{
|
||||
if (player is not { IsValid: true, IsHLTV: false } || player.Connected != PlayerConnectedState.Connected)
|
||||
continue;
|
||||
|
||||
var stats = player.ActionTrackingServices?.MatchStats;
|
||||
var line = new PlayerLine(
|
||||
player.UserId ?? -1,
|
||||
player.PlayerName,
|
||||
player.IsBot ? "0" : player.SteamID.ToString(),
|
||||
player.TeamNum,
|
||||
stats?.Kills ?? 0,
|
||||
stats?.Deaths ?? 0,
|
||||
player.Ping,
|
||||
player.IsBot);
|
||||
command.ReplyToCommand(Prefix + JsonSerializer.Serialize(line, JsonOptions));
|
||||
}
|
||||
|
||||
command.ReplyToCommand(Prefix + "end");
|
||||
}
|
||||
}
|
||||
17
WebPanelBridge.csproj
Normal file
17
WebPanelBridge.csproj
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>WebPanelBridge</AssemblyName>
|
||||
<RootNamespace>WebPanelBridge</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Compile-time only: the server already has CounterStrikeSharp.API.dll -->
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.375">
|
||||
<PrivateAssets>none</PrivateAssets>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
114
release.sh
Executable file
114
release.sh
Executable file
|
|
@ -0,0 +1,114 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build WebPanelBridge, package it as one tarball and publish it as a Forgejo release.
|
||||
#
|
||||
# ./release.sh <tag> e.g. ./release.sh v0.14.0
|
||||
#
|
||||
# Commit first: the plugin is rebuilt here from the committed source, so the release always matches
|
||||
# the tag. The tag is created on HEAD and pushed to origin if it doesn't exist yet. Re-running with
|
||||
# the same tag replaces that release's attachment.
|
||||
#
|
||||
# Asset: WebPanelBridge-<tag>.tar.gz, laid out like the server's game/csgo/ - extract it there:
|
||||
# tar -xzf WebPanelBridge-<tag>.tar.gz -C /srv/cs2/game/csgo
|
||||
#
|
||||
# addons/counterstrikesharp/plugins/WebPanelBridge/ WebPanelBridge.dll, .pdb, .deps.json
|
||||
#
|
||||
# No configs/: the plugin has no settings.
|
||||
set -euo pipefail
|
||||
|
||||
FORGEJO_URL="https://git.zio.sh"
|
||||
OWNER="cs2"
|
||||
REPO="WebPanelBridge"
|
||||
# Placeholder - replace, or set FORGEJO_TOKEN in the environment instead. Needs write:repository.
|
||||
# Only uploading needs it; downloading from the public repo doesn't. Don't commit a real token.
|
||||
FORGEJO_TOKEN="${FORGEJO_TOKEN:-CHANGE_ME_FORGEJO_TOKEN}"
|
||||
|
||||
# Only these ship from the publish output. CounterStrikeSharp.API.dll is excluded by the csproj
|
||||
# (ExcludeAssets=runtime); the server already provides it.
|
||||
PLUGIN_FILES=(
|
||||
WebPanelBridge.dll
|
||||
WebPanelBridge.pdb
|
||||
WebPanelBridge.deps.json
|
||||
)
|
||||
|
||||
TAG="${1:-}"
|
||||
if [[ -z "$TAG" ]]; then
|
||||
echo "usage: $0 <tag> (e.g. v0.1.0)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$FORGEJO_TOKEN" == "CHANGE_ME_FORGEJO_TOKEN" ]]; then
|
||||
echo "Set FORGEJO_TOKEN (edit release.sh or export it) first." >&2
|
||||
exit 1
|
||||
fi
|
||||
for tool in podman jq curl git; do
|
||||
command -v "$tool" >/dev/null || { echo "'$tool' is required." >&2; exit 1; }
|
||||
done
|
||||
|
||||
cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
|
||||
|
||||
if [[ -n "$(git status --porcelain --untracked-files=no)" ]]; then
|
||||
echo "Tracked files have uncommitted changes; commit them so the tag matches the release." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The tag should name the version the plugin reports in `css_plugins list`.
|
||||
version="$(sed -n 's/.*ModuleVersion => "\(.*\)";.*/\1/p' WebPanelBridge.cs)"
|
||||
if [[ "${TAG#v}" != "$version" ]]; then
|
||||
echo "Tag $TAG doesn't match ModuleVersion $version in WebPanelBridge.cs." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Build -----------------------------------------------------------------------
|
||||
OUT="release-out"
|
||||
PUBLISH="$OUT/publish"
|
||||
rm -rf bin obj "$OUT"
|
||||
podman run --rm -v "$(pwd)":/src:Z -w /src \
|
||||
mcr.microsoft.com/dotnet/sdk:10.0 dotnet publish -c Release -o "/src/$PUBLISH"
|
||||
|
||||
# --- Stage (layout of game/csgo/) and package ----------------------------------------
|
||||
STAGE="$OUT/stage"
|
||||
PLUGIN_DIR="$STAGE/addons/counterstrikesharp/plugins/WebPanelBridge"
|
||||
mkdir -p "$PLUGIN_DIR"
|
||||
for file in "${PLUGIN_FILES[@]}"; do
|
||||
cp "$PUBLISH/$file" "$PLUGIN_DIR/"
|
||||
done
|
||||
|
||||
TARBALL="WebPanelBridge-${TAG}.tar.gz"
|
||||
tar -czf "$OUT/$TARBALL" -C "$STAGE" addons
|
||||
rm -rf "$STAGE" "$PUBLISH"
|
||||
|
||||
echo "Packaged:"
|
||||
tar -tzf "$OUT/$TARBALL"
|
||||
|
||||
# --- Tag ---------------------------------------------------------------------------
|
||||
if ! git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
|
||||
git tag -a "$TAG" -m "$TAG"
|
||||
fi
|
||||
git push origin "refs/tags/$TAG"
|
||||
|
||||
# --- Release -----------------------------------------------------------------------
|
||||
API="$FORGEJO_URL/api/v1/repos/$OWNER/$REPO"
|
||||
AUTH=(-H "Authorization: token $FORGEJO_TOKEN")
|
||||
|
||||
release_json="$(curl -fsS "${AUTH[@]}" "$API/releases/tags/$TAG" 2>/dev/null || true)"
|
||||
if [[ -z "$release_json" ]]; then
|
||||
body="$(git log -1 --format=%B "$TAG")"
|
||||
release_json="$(jq -n --arg tag "$TAG" --arg body "$body" \
|
||||
'{tag_name: $tag, name: $tag, body: $body, draft: false, prerelease: false}' |
|
||||
curl -fsS "${AUTH[@]}" -H "Content-Type: application/json" -X POST --data @- "$API/releases")"
|
||||
echo "Created release $TAG"
|
||||
else
|
||||
echo "Release $TAG already exists, replacing its attachment"
|
||||
fi
|
||||
release_id="$(jq -r '.id' <<<"$release_json")"
|
||||
|
||||
existing_id="$(jq -r --arg n "$TARBALL" '.assets[]? | select(.name == $n) | .id' <<<"$release_json")"
|
||||
if [[ -n "$existing_id" ]]; then
|
||||
curl -fsS "${AUTH[@]}" -X DELETE "$API/releases/$release_id/assets/$existing_id" >/dev/null
|
||||
fi
|
||||
curl -fsS "${AUTH[@]}" -X POST -F "attachment=@$OUT/$TARBALL" \
|
||||
"$API/releases/$release_id/assets?name=$TARBALL" >/dev/null
|
||||
echo "Uploaded $TARBALL"
|
||||
|
||||
echo
|
||||
echo "Download URL:"
|
||||
echo " $FORGEJO_URL/$OWNER/$REPO/releases/download/$TAG/$TARBALL"
|
||||
Loading…
Add table
Add a link
Reference in a new issue