Skip to content

Commit d59fa50

Browse files
committed
feature: game validator
0 parents  commit d59fa50

11 files changed

Lines changed: 270 additions & 0 deletions

File tree

.dockerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
__pycache__/
2+
*.pyc
3+
.env
4+
.git
5+
gamedata/ccs.gamedata.json

.github/workflows/build.yaml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Build
2+
3+
on:
4+
push:
5+
branches:
6+
- "main"
7+
workflow_dispatch:
8+
9+
jobs:
10+
build-push-and-release:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Set up Docker Buildx
16+
uses: docker/setup-buildx-action@v3
17+
18+
- name: Login to GitHub Container Registry
19+
uses: docker/login-action@v3
20+
with:
21+
registry: ghcr.io
22+
username: ${{ github.actor }}
23+
password: ${{ secrets.GITHUB_TOKEN }}
24+
25+
- name: Build and Push Docker image
26+
uses: docker/build-push-action@v6
27+
with:
28+
push: true
29+
tags: |
30+
ghcr.io/${{ github.repository_owner }}/gamedata-validator:latest
31+
ghcr.io/${{ github.repository_owner }}/gamedata-validator:${{ github.sha }}
32+
# Registry-backed cache: a dedicated :buildcache tag on ghcr stores
33+
# the layer cache across runs (no 10GB GHA-cache ceiling, survives
34+
# branch/PR boundaries). First build is a full rebuild; every
35+
# subsequent build reuses unchanged layers.
36+
cache-from: |
37+
type=registry,ref=ghcr.io/${{ github.repository_owner }}/gamedata-validator:buildcache
38+
cache-to: |
39+
type=registry,ref=ghcr.io/${{ github.repository_owner }}/gamedata-validator:buildcache,mode=max
40+
41+
- name: Delete Package Versions
42+
uses: actions/delete-package-versions@v5
43+
with:
44+
package-name: gamedata-validator
45+
package-type: container
46+
min-versions-to-keep: 8
47+
ignore-versions: '^buildcache-*'

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
__pycache__/
2+
*.pyc
3+
.env
4+
gamedata/ccs.gamedata.json

Dockerfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM python:3.12-slim
2+
3+
ENV PYTHONUNBUFFERED=1
4+
WORKDIR /app
5+
6+
RUN apt-get update && \
7+
apt-get install -y --no-install-recommends libstdc++6 && \
8+
rm -rf /var/lib/apt/lists/* && \
9+
pip install --no-cache-dir commentjson
10+
11+
COPY lib/s2binlib.so ./lib/s2binlib.so
12+
COPY main.py s2binlib.py ./
13+
COPY gamedata ./gamedata
14+
15+
ARG CCS_GAMEDATA_REF=main
16+
RUN python -c "import urllib.request; urllib.request.urlretrieve('https://raw.githubusercontent.com/roflmuffin/CounterStrikeSharp/${CCS_GAMEDATA_REF}/configs/addons/counterstrikesharp/gamedata/gamedata.json', 'gamedata/ccs.gamedata.json')"
17+
18+
ENTRYPOINT ["python", "main.py"]

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# gamedata-validator
2+
3+
Validates the CS2 byte-pattern **signatures** our server plugins rely on against an existing CS2
4+
install. When Valve ships an update, a signature can silently stop matching — the hook just stops
5+
firing. This scans the game binaries and reports any signature that no longer resolves.
6+
7+
It runs against a game-server node's already-downloaded install (mounted at
8+
`/serverdata/serverfiles/game`), so it does **not** download anything and only needs the Linux
9+
binaries that the node already has.
10+
11+
## What it checks
12+
13+
Two sets, both in [CounterStrikeSharp `gamedata.json`](https://github.com/roflmuffin/CounterStrikeSharp/blob/main/configs/addons/counterstrikesharp/gamedata/gamedata.json)
14+
format:
15+
16+
- `fivestack` — our own custom signatures (`gamedata/fivestack.gamedata.json`). Mirror of the source
17+
of truth in `game-server/gamedata/fivestack.gamedata.json`; keep them in sync.
18+
- `upstream-ccs` — upstream CounterStrikeSharp gamedata, fetched into `gamedata/ccs.gamedata.json`
19+
at image build time.
20+
21+
Only `signatures` entries with a `linux` pattern are scanned (`offsets`-only entries are skipped).
22+
23+
## Result
24+
25+
Prints a single machine-readable line and exits non-zero if any signature does not resolve to
26+
exactly one match (`count != 1`):
27+
28+
```
29+
GAMEDATA_VALIDATION_RESULT {"build_id":..., "status":"pass|fail", "broken":[...], "results":[...]}
30+
```
31+
32+
## Run
33+
34+
```bash
35+
pip install -r requirements.txt
36+
python main.py --game-path /path/to/cs2/game # dir containing csgo/ and bin/
37+
```
38+
39+
Flags: `--build-id <id>` (label only), `--gamedata name=path` (repeatable) to override the sets.
40+
41+
## Docker
42+
43+
```bash
44+
docker build -t ghcr.io/5stackgg/gamedata-validator:latest .
45+
docker run --rm -v /serverdata/serverfiles:/serverdata/serverfiles:ro \
46+
ghcr.io/5stackgg/gamedata-validator:latest
47+
```
48+
49+
`./push-latest.sh` builds and pushes from a local checkout. CI publishes
50+
`ghcr.io/5stackgg/gamedata-validator:latest` on push to `main`.
51+
52+
## Layout
53+
54+
| Path | Purpose |
55+
| --- | --- |
56+
| `main.py` | load gamedata → scan the install → report |
57+
| `s2binlib.py`, `lib/s2binlib.so` | native pattern scanner |
58+
| `gamedata/fivestack.gamedata.json` | our custom signatures |

gamedata/fivestack.gamedata.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"FiveStack_ConnectClient": {
3+
"signatures": {
4+
"library": "engine2",
5+
"linux": "55 48 89 E5 41 57 41 56 41 89 CE 41 55 41 54 4D 89 CC 53 48 89 D3 48 81 EC ? ? ? ? 8B 45 20",
6+
"windows": "4C 8B CE 8B D3 ? ? ? ?"
7+
}
8+
}
9+
}

lib/s2binlib.so

5.09 MB
Binary file not shown.

main.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import argparse
2+
import json
3+
import sys
4+
from os import path
5+
6+
import commentjson
7+
import s2binlib
8+
9+
SETS = {
10+
"fivestack": "gamedata/fivestack.gamedata.json",
11+
"upstream-ccs": "gamedata/ccs.gamedata.json",
12+
}
13+
14+
15+
def load_signatures(file_path):
16+
signatures = {}
17+
with open(file_path) as f:
18+
for name, entry in commentjson.load(f).items():
19+
sig = entry.get("signatures") if isinstance(entry, dict) else None
20+
lib = sig and (sig.get("library") or sig.get("lib"))
21+
if lib and sig.get("linux"):
22+
signatures[name] = {"lib": lib, "linux": sig["linux"]}
23+
return signatures
24+
25+
26+
def main():
27+
parser = argparse.ArgumentParser(
28+
description="Validate CS2 gamedata signatures against a game install"
29+
)
30+
parser.add_argument("--game-path", default="/serverdata/serverfiles/game")
31+
parser.add_argument("--build-id", type=int, default=None)
32+
args = parser.parse_args()
33+
34+
s2binlib.initialize(args.game_path, "csgo", "linux")
35+
36+
results = []
37+
for set_name, file_path in SETS.items():
38+
if not path.exists(file_path):
39+
print(f"[skip] missing {file_path}", flush=True)
40+
continue
41+
for name, sig in load_signatures(file_path).items():
42+
_, count = s2binlib.pattern_scan(sig["lib"], sig["linux"])
43+
results.append(
44+
{"set": set_name, "signature": name, "count": count, "ok": count == 1}
45+
)
46+
47+
broken = [r for r in results if not r["ok"]]
48+
result = {
49+
"build_id": args.build_id,
50+
"status": "fail" if broken else "pass",
51+
"broken": broken,
52+
"results": results,
53+
}
54+
55+
print("GAMEDATA_VALIDATION_RESULT " + json.dumps(result), flush=True)
56+
sys.exit(1 if broken else 0)
57+
58+
59+
if __name__ == "__main__":
60+
main()

push-latest.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
IMAGE="ghcr.io/5stackgg/gamedata-validator"
5+
CACHE_REF="${IMAGE}:buildcache"
6+
SHA="$(git rev-parse HEAD)"
7+
8+
if [ "$#" -gt 0 ]; then
9+
TAGS=( "$@" )
10+
else
11+
# shellcheck disable=SC2206
12+
TAGS=( ${TAGS:-latest} )
13+
fi
14+
15+
cd "$(dirname "$0")"
16+
17+
tag_args=()
18+
for t in "${TAGS[@]}"; do
19+
tag_args+=( --tag "${IMAGE}:${t}" )
20+
done
21+
tag_args+=( --tag "${IMAGE}:${SHA}" )
22+
23+
echo "building + pushing ${IMAGE} with tags: ${TAGS[*]} ${SHA}"
24+
docker buildx build \
25+
--platform linux/amd64 \
26+
--push \
27+
"${tag_args[@]}" \
28+
--cache-from "type=registry,ref=${CACHE_REF}" \
29+
--cache-to "type=registry,ref=${CACHE_REF},mode=max" \
30+
.

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
commentjson

0 commit comments

Comments
 (0)