Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion rust/module.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: gameplane.local/module/v1
name: rust
displayName: Rust
version: 1.0.2
version: 1.0.3
game: rust
categories: [Survival, Shooter, PvP]
summary: Rust dedicated server (didstopia/rust-server) with a pty console, Oxide plugin volume, and a save-then-quit stop sequence.
Expand Down
52 changes: 31 additions & 21 deletions rust/template.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# Gameplane GameTemplate for Rust (dedicated server).
#
# Backed by didstopia/rust-server. Rust's RCON is WebSocket-only
# (Facepunch's own `rcon.web`/webrcon) — it does NOT speak the Source RCON
# wire protocol the agent implements, and there's no raw-TCP fallback. So
# rcon.protocol is "none" here; RCON-transport capabilities (players list,
# moderation, actions) are unavailable for Rust in Gameplane today.
# Backed by didstopia/rust-server. Rust's RCON is WebSocket-only — Facepunch's
# own `rcon.web`/webrcon, which does NOT speak the Source RCON wire protocol.
# The agent now implements it (rcon.protocol: websocket), so the console,
# players list, moderation, quiesce and quick actions all work over RCON.
#
# RustDedicated does accept an attached interactive console (every
# Docker/tmux community wrapper drives it this way), which matches
# consoleMode: pty. The clean-stop sequence ("server.save" then "quit") is
# real and documented, and now actually runs: the operator's PTY-stdin stop
# transport writes it to the game container before scaling down, so a stop
# or restart saves the world instead of taking a SIGTERM mid-write.
# The image enables webrcon by default (RUST_RCON_WEB=1) and serves plain
# ws:// (RUST_RCON_SECURE_WEBSOCKET=0), which is what the agent's client
# dials. The password comes from RUST_RCON_PASSWORD, injected by the operator.
#
# playerlist returns a JSON array rather than lines, which is fine: the agent
# applies players.list.entryRegex with FindAll over the whole reply, so the
# DisplayName regex below pulls every player out of the single-line JSON.
#
# Cluster-scoped; apply once per cluster:
# kubectl apply -f modules/rust/template.yaml
Expand Down Expand Up @@ -42,11 +42,9 @@ spec:
containerPort: 28015
protocol: UDP
advertise: true
# Rust's RCON (28016) is WebSocket-over-TCP, not Source RCON — the
# agent can't dial it (see header comment). Declared here only so a
# player who wants to use their own WebRCON client can be routed to it
# via a networking.portOverrides / manual Service edit; Gameplane's own
# console/RCON features never use this port.
# Rust's RCON: WebSocket-over-TCP (webrcon), which the agent now speaks.
# Not advertised — the dashboard reaches it in-pod; exposing it publicly
# would put the RCON password on the internet.
- name: rcon
containerPort: 28016
protocol: TCP
Expand All @@ -61,16 +59,28 @@ spec:
limits: {cpu: "4", memory: 8Gi}

rcon:
protocol: none
protocol: websocket
port: 28016
# The image reads its RCON password from RUST_RCON_PASSWORD (default
# "docker" — the operator generates one and injects it here instead).
passwordEnv: RUST_RCON_PASSWORD

consoleMode: pty
consoleMode: rcon

capabilities:
# Runs over the pty-stdin transport once the operator's PTY-stop path
# lands (see header comment) — both commands are Facepunch/community-
# documented as the clean-shutdown sequence.
# Facepunch/community-documented clean-shutdown sequence. Now runs over
# RCON rather than pty-stdin, so a stop or restart saves the world instead
# of taking a SIGTERM mid-write.
lifecycle:
stop: ["server.save", "quit"]
players:
# playerlist returns a JSON array of player objects; entryRegex is applied
# with FindAll across the whole reply, so this lifts out every DisplayName.
list:
command: "playerlist"
entryRegex: '"DisplayName":"(?P<name>[^"]*)"'
kick: 'kick "{{.Player}}"{{if .Reason}} "{{.Reason}}"{{end}}'
ban: 'ban "{{.Player}}"{{if .Reason}} "{{.Reason}}"{{end}}'
mods:
loaders:
oxide:
Expand Down
13 changes: 10 additions & 3 deletions validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@

CURL_TIMEOUT_SECS = 15

# Wire protocols the agent's rcon package actually implements. Keep in sync with
# the GameTemplate CRD's rcon.protocol enum and agent/internal/rcon/. A protocol
# listed here but not implemented lets a module ship a console that never
# connects, so this list is deliberately conservative.
RCON_PROTOCOLS = ("source", "telnet", "websocket", "none")



@dataclass
class Finding:
Expand Down Expand Up @@ -627,13 +634,13 @@ def rule_rcon_protocol(spec: dict) -> list[Finding]:
# The CRD marks Protocol +kubebuilder:default=source, so an omitted
# key resolves to "source" at apply time — not a missing/invalid value.
return []
if proto not in ("source", "telnet", "none"):
if proto not in RCON_PROTOCOLS:
return [
Finding(
ERROR,
"bad-rcon-protocol",
f"rcon.protocol={proto!r} is not one of source/telnet/none — "
"the agent implements no others.",
f"rcon.protocol={proto!r} is not one of "
f"{'/'.join(sorted(RCON_PROTOCOLS))} — the agent implements no others.",
)
]
return []
Expand Down
Loading