Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
b31f181
feature: nade practice plugin
lukepolo Aug 19, 2026
2fc59f8
practice plugin dev hot-reload
lukepolo Aug 19, 2026
fca9645
rename nades to utility; squash utility migrations
lukepolo Aug 19, 2026
2b25918
tail utility workloads
lukepolo Aug 19, 2026
b70bd7b
drop the pre-rename plugin copy
lukepolo Aug 20, 2026
b5c8e13
set sv_password from the practice session; load at cold boot; log con…
lukepolo Aug 20, 2026
277dab7
put practice servers into a practice game state
lukepolo Aug 20, 2026
067ba91
record throws reliably; survive a map change
lukepolo Aug 20, 2026
82049f7
greet players with the commands when they join a team
lukepolo Aug 20, 2026
90caa50
keep practice players alive and stocked
lukepolo Aug 20, 2026
e13d1da
report who is on a practice server; save the stance not the release p…
lukepolo Aug 20, 2026
3d0b16f
never sit in warmup on a practice server
lukepolo Aug 20, 2026
58a34ca
aim goes on the eyes, not the body
lukepolo Aug 20, 2026
3eafd9b
stop rebuilding the transmit filter every frame
lukepolo Aug 20, 2026
abd9212
the load path puts the aim on the eyes too
lukepolo Aug 20, 2026
dffa819
record the standstill a throw is set up from, not the takeoff point
lukepolo Aug 20, 2026
b1a4aff
let practice servers hand players valve's annotation editor
lukepolo Aug 20, 2026
0e4b28b
mark the stance, the aim and the landing in world
lukepolo Aug 20, 2026
3eaeeca
say why the library is empty instead of blaming the query
lukepolo Aug 20, 2026
c7d9067
aim at a reticle on the wall and say how to throw once you are on it
lukepolo Aug 20, 2026
b309cd6
draw the markers as outlines instead of glowing slabs
lukepolo Aug 20, 2026
112d7fc
keep the reticle visible at range
lukepolo Aug 20, 2026
e886445
trace the aim from where the player stands, not from where the grenad…
lukepolo Aug 20, 2026
3ada309
hang the reticle in front of the stance instead of on a distant ceiling
lukepolo Aug 20, 2026
2a2299f
put the stance marker on the floor a player could stand on
lukepolo Aug 20, 2026
2141110
start the ground trace above the point so it can actually hit
lukepolo Aug 20, 2026
2196993
ground the markers before the player is standing on the spot
lukepolo Aug 20, 2026
0dc715d
draw the stance where the player actually lands
lukepolo Aug 20, 2026
dc48d5d
print where you are against what the lineup says
lukepolo Aug 20, 2026
b06edf7
say which position a recorded throw chose and why
lukepolo Aug 20, 2026
588288b
prefer the throw's own takeoff over a standstill walked away from
lukepolo Aug 20, 2026
4e8fe09
show a lineup the moment it is saved
lukepolo Aug 20, 2026
d739a58
keep the lineup on screen and make the aim exact
lukepolo Aug 20, 2026
be1e7d5
load every lineup on the map at once
lukepolo Aug 20, 2026
2e85557
mark the exact aim point with a crosshair
lukepolo Aug 20, 2026
ec5539e
light up every throw from the spot you step into
lukepolo Aug 20, 2026
e23a5c3
carry the aim trace past the player to the wall it points at
lukepolo Aug 20, 2026
bf1185e
let the player point at the ring they mean
lukepolo Aug 20, 2026
1c757d9
name what you are looking at and float the grenade over the spot
lukepolo Aug 20, 2026
9e3c9e6
give each player their own selection markers
lukepolo Aug 20, 2026
4850f3e
add a picker for the throws on one spot
lukepolo Aug 20, 2026
ef5b4d8
precache the grenade models so they are not error models
lukepolo Aug 20, 2026
d3f37ae
pick the throw you are looking toward and hang its label the right wa…
lukepolo Aug 20, 2026
cceb24c
set the model before spawning and learn the real path from a thrown g…
lukepolo Aug 20, 2026
0079090
keep the practice trajectory trail up long enough to walk back to it
lukepolo Aug 20, 2026
c2c9ae0
show every throw from a spot and weight the one you are looking toward
lukepolo Aug 20, 2026
6a5f35b
spawn the lineup grenade model through keyvalues
lukepolo Aug 20, 2026
b3d34ca
ask for a trajectory as the player who wants it
lukepolo Aug 20, 2026
6dfa6c0
learn grenade models at map load and stop them blocking the spot
lukepolo Aug 20, 2026
25c0b3e
one grenade model per spot, not per lineup
lukepolo Aug 20, 2026
04dd10f
colour the crosshair by how far off the aim is
lukepolo Aug 20, 2026
133892f
send the physics seed when saving a lineup
lukepolo Aug 21, 2026
bf79b9b
draw lineups the 5stack way
lukepolo Aug 21, 2026
3af0865
say which half of a lineup is wrong
lukepolo Aug 21, 2026
a121d30
guard entity placement and anchor the gate to the lineup
lukepolo Aug 21, 2026
1036d1e
revert the in-world redraw back to the working build
lukepolo Aug 21, 2026
5adea5b
stop calling CheckTransmit on swiftly 1.4.4
lukepolo Aug 21, 2026
146bbee
reset the practice plugin to the last working build
lukepolo Aug 21, 2026
09eb911
put every engine-touching feature behind a bisect switch
lukepolo Aug 21, 2026
f6d47df
bisect: markers on
lukepolo Aug 21, 2026
875c589
bisect: grenade models on
lukepolo Aug 21, 2026
d661629
spawn the model probes on the main thread
lukepolo Aug 21, 2026
0340fb7
bisect: harvest off, props on
lukepolo Aug 21, 2026
1fc70b2
drop the weapon-probe model harvest
lukepolo Aug 21, 2026
efac1da
bisect: ghosts on
lukepolo Aug 21, 2026
5432663
bisect: real projectiles on
lukepolo Aug 21, 2026
a18548f
make labels face the reader and drop transmit blocking
lukepolo Aug 21, 2026
6344761
rebuild markers to change colour and add a stance beacon
lukepolo Aug 21, 2026
2c5c490
reserve green for inside tolerance and cap the library draw
lukepolo Aug 21, 2026
08427e0
glow the utility models and probe live beam recolouring
lukepolo Aug 21, 2026
ed29103
recolour beams by input, skip players in marker traces, keep glow on …
lukepolo Aug 21, 2026
d89e56c
ground reticle for the stance, marker-proof aim trace, flight line stays
lukepolo Aug 21, 2026
eee2df2
name on point, quieter ground, reticles that never move
lukepolo Aug 21, 2026
071c10c
flight lines are opt-in
lukepolo Aug 21, 2026
28653c6
drop the unused dashed-line helpers
lukepolo Aug 21, 2026
f429741
one precise needle per throw and nothing to read off the floor
lukepolo Aug 21, 2026
4c0a0c8
fade the crosshair once the player is on it
lukepolo Aug 21, 2026
e14064b
give the ground circle the sky reticle's crosshair
lukepolo Aug 21, 2026
03302cc
trace the aim from the lineup's spot, not the player's
lukepolo Aug 21, 2026
c7674da
tag every marker and sweep the world on load, map change, unload and …
lukepolo Aug 21, 2026
fb1a39d
three-row lineup window that always names the throw
lukepolo Aug 21, 2026
157f9c2
split the lineup hud into a card and a steps line
lukepolo Aug 21, 2026
8049987
stop the hud strobing and teach every technique properly
lukepolo Aug 21, 2026
4116d84
quieten the panel keepalive and clear with a space
lukepolo Aug 21, 2026
3f5472b
drop the panels when the player leaves the spot
lukepolo Aug 21, 2026
1f45a7e
clear panels instantly instead of blanking them for a minute
lukepolo Aug 21, 2026
d1d8e0f
let the steps line lapse rather than blanking it
lukepolo Aug 21, 2026
ea7b037
swap the panels so the flashing one carries the changing message
lukepolo Aug 21, 2026
9f8e88c
keep word gaps in the html instruction
lukepolo Aug 21, 2026
c604ec8
give the lineup name its own hud channel
lukepolo Aug 21, 2026
294b371
name on the panel that renders, and draw the library on join
lukepolo Aug 21, 2026
507b0ae
title to the quiet panel, details to the flashing one
lukepolo Aug 21, 2026
72f401c
put the lineup on the quiet panel and add an alerts sampler
lukepolo Aug 21, 2026
d038f9e
put the lineup name on the alert channel
lukepolo Aug 21, 2026
bd5d2c8
title case the name and the throw details
lukepolo Aug 21, 2026
6d0a218
tell a player about next and prev once
lukepolo Aug 21, 2026
59efc03
repeat the tip on a landed throw and drop the alerts helper
lukepolo Aug 21, 2026
2ad1b87
remove the pick and where commands
lukepolo Aug 21, 2026
00eed4f
add jump and make refreshed lineups walkable
lukepolo Aug 21, 2026
50e3efa
remove the ghost trail
lukepolo Aug 21, 2026
2ac8823
drill in reps of three and show where you are in it
lukepolo Aug 21, 2026
12ca25d
leave the player where they are between drill reps
lukepolo Aug 21, 2026
08eae4f
draw only the lineup a drill is asking for
lukepolo Aug 21, 2026
0faa955
keep the panels on the drilled lineup
lukepolo Aug 21, 2026
370c9ca
let drill toggle off and add cancel
lukepolo Aug 21, 2026
6208c29
let the panel drill a chosen set
lukepolo Aug 21, 2026
85757be
wip
lukepolo Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,12 +46,16 @@ jobs:
- 'Directory.Build.props'
- '.github/workflows/ci.yaml'
- '.github/workflows/plugin-build.yaml'
# each game-server image builds both its match plugin and the
# practice plugin, so either source tree rebuilds it
css:
- *shared
- 'apps/counterstrikesharp/**'
- 'apps/utility-css/**'
sw:
- *shared
- 'apps/swiftly/**'
- 'apps/utility-sw/**'
validator:
- 'apps/gamedata-validator/**'
- 'shared/gamedata/**'
Expand All@@ -71,6 +75,7 @@ jobs:
version_tag_regex: '^(?:css-)?v0\.0\.(\d+)$'
channel: ${{ needs.changes.outputs.channel }}
run_tests: true
extra_test_dir: apps/utility-css
secrets: inherit

swiftly:
Expand All@@ -86,6 +91,7 @@ jobs:
version_floor: 41
channel: ${{ needs.changes.outputs.channel }}
run_tests: true
extra_test_dir: apps/utility-sw
secrets: inherit

gamedata-validator:
Expand Down
14 changes: 13 additions & 1 deletion .github/workflows/plugin-build.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,11 @@ on:
required: false
default: false
type: boolean
extra_test_dir:
description: "second app whose suite ships in this image (the practice plugin)"
required: false
default: ""
type: string

jobs:
build:
Expand All@@ -47,7 +52,14 @@ jobs:

- name: Test
if: inputs.run_tests
run: dotnet test ${{ inputs.app_dir }}/test/FiveStack.Tests.csproj -c Release
env:
APP_DIR: ${{ inputs.app_dir }}
EXTRA_TEST_DIR: ${{ inputs.extra_test_dir }}
run: |
dotnet test "$APP_DIR/test/FiveStack.Tests.csproj" -c Release
if [ -n "$EXTRA_TEST_DIR" ]; then
dotnet test "$EXTRA_TEST_DIR/test/FiveStack.Tests.csproj" -c Release
fi

- name: Resolve version and tags
id: resolve
Expand Down
30 changes: 29 additions & 1 deletion apps/counterstrikesharp/Dockerfile
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
FROM registry.gitlab.steamos.cloud/steamrt/sniper/platform:latest-container-runtime-depot AS build
FROM registry.gitlab.steamos.cloud/steamrt/sniper/platform:latest-container-runtime-depot AS dotnet-sdk

# Install .NET SDK 10.0
RUN apt-get update && \
Expand All@@ -14,6 +14,8 @@ RUN apt-get update && \
ENV PATH="/usr/share/dotnet:${PATH}"
ENV DOTNET_ROOT="/usr/share/dotnet"

FROM dotnet-sdk AS build

WORKDIR /mod

COPY Directory.Build.props ./
Expand All@@ -35,6 +37,30 @@ RUN rm /mod/release/CounterStrikeSharp.API.dll

COPY apps/counterstrikesharp/src/lang /mod/release/lang

# The practice plugin is the other half of this image: a server runs either it
# or the match plugin, never both, so it is built here and symlinked into place
# only when INSTALL_UTILITY_PRACTICE_PLUGIN is set.
FROM dotnet-sdk AS utility-build

WORKDIR /mod

COPY Directory.Build.props ./
COPY apps/utility-css/src/UtilityPractice.csproj apps/utility-css/src/

RUN dotnet restore apps/utility-css/src/UtilityPractice.csproj

COPY shared shared
COPY apps/utility-css apps/utility-css

ARG RELEASE_VERSION
ENV RELEASE_VERSION=${RELEASE_VERSION}

RUN sed -i "s/__RELEASE_VERSION__/${RELEASE_VERSION}/" apps/utility-css/src/UtilityPracticePlugin.cs

RUN dotnet build -c Release apps/utility-css/src/UtilityPractice.csproj -o release

RUN rm -f /mod/release/CounterStrikeSharp.API.dll

# New stage for creating the zip file
FROM debian:bookworm-slim AS zip-creator

Expand All@@ -60,6 +86,7 @@ ENV AUTOLOAD_PLUGINS=true
ENV PLUGINS_DIR="/opt/custom-plugins"

ENV INSTALL_5STACK_PLUGIN=true
ENV INSTALL_UTILITY_PRACTICE_PLUGIN=false

ENV GAME_ID="730"
ENV GAME_PARAMS=""
Expand DownExpand Up@@ -125,6 +152,7 @@ COPY apps/counterstrikesharp/cfg /opt/server-cfg
COPY shared/scripts /opt/scripts
COPY apps/counterstrikesharp/scripts /opt/scripts
COPY --from=build /mod/release /opt/mod
COPY --from=utility-build /mod/release /opt/utility-practice

RUN mv /opt/metamod/addons /opt/addons && \
cp -R /opt/counterstrikesharp/addons/metamod /opt/addons && \
Expand Down
1 change: 1 addition & 0 deletions apps/counterstrikesharp/Dockerfile.dev
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ ENV AUTOLOAD_PLUGINS=true
ENV PLUGINS_DIR="/opt/custom-plugins"

ENV INSTALL_5STACK_PLUGIN=true
ENV INSTALL_UTILITY_PRACTICE_PLUGIN=false

ENV GAME_ID="730"
ENV GAME_PARAMS=""
Expand Down
17 changes: 16 additions & 1 deletion apps/counterstrikesharp/scripts/setup.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,7 +66,10 @@ fi

ln -s "/opt/custom-plugins/addons/counterstrikesharp/configs" "${INSTANCE_SERVER_DIR}/game/csgo/addons/counterstrikesharp/configs"

if [ "$SERVER_TYPE" != "Ranked" ]; then
# Public-server treatment: every installed custom plugin gets symlinked in,
# which is what "load on every match" is meant to gate. A practice server is
# 5stack-managed like a ranked one -- it runs its own plugin and nothing else.
if [ "$SERVER_TYPE" != "Ranked" ] && [ "$SERVER_TYPE" != "Practice" ]; then
if [ ! -d "/opt/custom-plugins" ]; then
mkdir -p "/opt/custom-plugins"
fi
Expand DownExpand Up@@ -129,6 +132,18 @@ if $INSTALL_5STACK_PLUGIN = true ; then
fi
fi

# A practice server runs this instead of the match plugin, never alongside it.
if $INSTALL_UTILITY_PRACTICE_PLUGIN = true ; then
echo "---Install Utility Practice---"
if [ "${DEV_SWAPPED}" == "1" ]; then
# css and sw dev builds share the dev volume; each uses its own subfolder
mkdir -p "/opt/dev/utility-css"
ln -s "/opt/dev/utility-css" "${INSTANCE_SERVER_DIR}/game/csgo/addons/counterstrikesharp/plugins/UtilityPractice"
else
ln -s "/opt/utility-practice" "${INSTANCE_SERVER_DIR}/game/csgo/addons/counterstrikesharp/plugins/UtilityPractice"
fi
fi

if [ ! -e "$INSTANCE_SERVER_DIR/game/csgo/addons/counterstrikesharp/configs/core.json" ]; then
cp "/opt/server-cfg/core.json" "$INSTANCE_SERVER_DIR/game/csgo/addons/counterstrikesharp/configs"
fi
Expand Down
28 changes: 27 additions & 1 deletion apps/swiftly/Dockerfile
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
FROM registry.gitlab.steamos.cloud/steamrt/sniper/platform:latest-container-runtime-depot AS build
FROM registry.gitlab.steamos.cloud/steamrt/sniper/platform:latest-container-runtime-depot AS dotnet-sdk

RUN apt-get update && \
apt-get install -y --no-install-recommends \
Expand All@@ -13,6 +13,8 @@ RUN apt-get update && \
ENV PATH="/usr/share/dotnet:${PATH}"
ENV DOTNET_ROOT="/usr/share/dotnet"

FROM dotnet-sdk AS build

WORKDIR /mod

COPY Directory.Build.props ./
Expand All@@ -30,6 +32,28 @@ RUN sed -i "s/__RELEASE_VERSION__/${RELEASE_VERSION}/" apps/swiftly/src/FiveStac

RUN dotnet publish -c Release apps/swiftly/src/FiveStack.csproj -o /mod/release

# The practice plugin is the other half of this image: a server runs either it
# or the match plugin, never both, so it is built here and symlinked into place
# only when INSTALL_UTILITY_PRACTICE_PLUGIN is set.
FROM dotnet-sdk AS utility-build

WORKDIR /mod

COPY Directory.Build.props ./
COPY apps/utility-sw/src/UtilityPractice.csproj apps/utility-sw/src/

RUN dotnet restore apps/utility-sw/src/UtilityPractice.csproj

COPY shared shared
COPY apps/utility-sw apps/utility-sw

ARG RELEASE_VERSION
ENV RELEASE_VERSION=${RELEASE_VERSION}

RUN sed -i "s/__RELEASE_VERSION__/${RELEASE_VERSION}/" apps/utility-sw/src/UtilityPracticePlugin.cs

RUN dotnet publish -c Release apps/utility-sw/src/UtilityPractice.csproj -o /mod/release

FROM debian:bookworm-slim AS zip-creator

WORKDIR /zip-content
Expand All@@ -54,6 +78,7 @@ ENV AUTOLOAD_PLUGINS=true
ENV PLUGINS_DIR="/opt/custom-plugins"

ENV INSTALL_5STACK_PLUGIN=true
ENV INSTALL_UTILITY_PRACTICE_PLUGIN=false

ENV GAME_ID="730"
ENV GAME_PARAMS=""
Expand DownExpand Up@@ -132,6 +157,7 @@ COPY apps/swiftly/cfg /opt/server-cfg
COPY shared/scripts /opt/scripts
COPY apps/swiftly/scripts /opt/scripts
COPY --from=build /mod/release /opt/mod
COPY --from=utility-build /mod/release /opt/utility-practice

RUN cp -R /opt/swiftlys2/swiftlys2-linux-${SWIFTLYS2_VERSION}-with-runtimes/addons /opt/addons && \
rm -rf /opt/swiftlys2
Expand Down
30 changes: 29 additions & 1 deletion apps/swiftly/scripts/setup.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,7 +89,10 @@ if [ "$ENABLE_CSS_COMPAT" = "true" ]; then
ln -s "/opt/custom-plugins/addons/counterstrikesharp/configs" "${INSTANCE_SERVER_DIR}/game/csgo/addons/counterstrikesharp/configs"
fi

if [ "$SERVER_TYPE" != "Ranked" ]; then
# Public-server treatment: every installed custom plugin gets symlinked in,
# which is what "load on every match" is meant to gate. A practice server is
# 5stack-managed like a ranked one -- it runs its own plugin and nothing else.
if [ "$SERVER_TYPE" != "Ranked" ] && [ "$SERVER_TYPE" != "Practice" ]; then
if [ ! -d "/opt/custom-plugins" ]; then
mkdir -p "/opt/custom-plugins"
fi
Expand DownExpand Up@@ -178,6 +181,31 @@ if $INSTALL_5STACK_PLUGIN = true ; then
fi
fi

# A practice server runs this instead of the match plugin, never alongside it.
if $INSTALL_UTILITY_PRACTICE_PLUGIN = true ; then
echo "---Install Utility Practice---"
UTILITY_PRACTICE_PLUGIN_DIR="${INSTANCE_SERVER_DIR}/game/csgo/addons/swiftlys2/plugins/UtilityPractice"
if [ "${DEV_SWAPPED}" == "1" ]; then
# AutoHotReload's recursive FileSystemWatcher does not follow symlinked plugin
# dirs, so hot reload needs the dev volume mounted straight onto this path
# (see the dev deployment). Without the mount, fall back to a symlink.
mkdir -p "$UTILITY_PRACTICE_PLUGIN_DIR"
if mountpoint -q "$UTILITY_PRACTICE_PLUGIN_DIR"; then
echo "---Utility Practice dev: plugin dir is a mounted volume (hot reload enabled)---"
else
# css and sw dev builds share the dev volume; each uses its own subfolder
rmdir "$UTILITY_PRACTICE_PLUGIN_DIR" 2>/dev/null
mkdir -p "/opt/dev/utility-sw"
ln -s "/opt/dev/utility-sw" "$UTILITY_PRACTICE_PLUGIN_DIR"
echo "---Utility Practice dev: symlinked /opt/dev/utility-sw (hot reload OFF; use 'sw plugins reload UtilityPractice')---"
fi
elif [ ! -e "$UTILITY_PRACTICE_PLUGIN_DIR" ]; then
ln -s "/opt/utility-practice" "$UTILITY_PRACTICE_PLUGIN_DIR"
else
echo "---Utility Practice: plugin dir already present, skipping /opt/utility-practice symlink---"
fi
fi

if [ ! -e "$INSTANCE_SERVER_DIR/game/csgo/addons/swiftlys2/configs/core.jsonc" ]; then
cp "/opt/server-cfg/core.jsonc" "$INSTANCE_SERVER_DIR/game/csgo/addons/swiftlys2/configs"
fi
Expand Down
34 changes: 34 additions & 0 deletions apps/utility-css/scripts/dev.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
#!/bin/bash

apt update
apt-get install inotify-tools -y

# Variable to store the PID of dotnet watch process
dotnet_watch_pid=""

# Function to kill the dotnet watch build process
kill_dotnet_watch() {
if [ -n "$dotnet_watch_pid" ]; then
kill "$dotnet_watch_pid"
exit
fi
}
dotnet build apps/utility-css/src

dotnet watch build --project apps/utility-css/src &
dotnet_watch_pid=$!

# Set up trap to kill dotnet watch process on script exit
trap kill_dotnet_watch EXIT

directory_to_watch="/opt/5stack/apps/utility-css/src/bin/Debug/net10.0"

# Matches the path apps/counterstrikesharp/scripts/setup.sh symlinks in.
DEV_DIR="/opt/dev/utility-css"
mkdir -p "$DEV_DIR"

while true; do
rm -f "$directory_to_watch/CounterStrikeSharp.API.dll"
inotifywait -r -e modify,create,delete,move "$directory_to_watch"
cp -r "$directory_to_watch"/* "$DEV_DIR"
done
Loading
Loading