Communitygithub.com

Wend4r/s2r-skills

Eine Sammlung von Agenten-Skills für die Arbeit mit Source 2 Ressourcen und die Entwicklung zugehöriger Codebasis.

Was ist s2r-skills?

Dieses Skill-Bundle ist ein umfassender Leitfaden für die Entwicklung von benutzerdefinierten HUDs in Source 2 (z. B. in Counter-Strike 2). Es konzentriert sich auf die `custom_hud_layout`-Entität, die es ermöglicht, serverseitig gesteuerte Panorama-HUDs zu erstellen, die ohne clientseitiges JavaScript auskommen. Der Skill erklärt die strengen Validierungsregeln, die nur vier Panel-Typen (`Panel`, `Label`, `Image`, `Button`) zulassen und dynamische Inhalte ausschließlich über Klassentoggles und `SetDialogVariableString`-Aufrufe vom Server steuern. Er behandelt zwei Betriebsarten (Overlay und Cursor), eine vollständige Referenztabelle für den Vergleich mit Web-Entwicklung, eine vollständige Fehlerliste und ein praktisches Schritt-für-Schritt-Beispiel (ein Punktmarkierungs-Indikator). Das Skill-Bundle ist ideal für Modder und Entwickler, die Source 2 Addons mit interaktiven, aber strikt validierten Panorama-GUI-Komponenten erweitern möchten.

Funktioniert mit~Claude Code~Codex CLI✓Cursor
npx skills add Wend4r/s2r-skills

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Source 2 | Benutzerdefiniertes HUD-Layout

Die Entität custom_hud_layout (C++ CCSCustomHudLayout) ermöglicht es einer Map oder einem serverseitigen Plugin, Spielern ein HUD anzuzeigen, das in Panorama XML + VCSS erstellt wurde.

Lesen Sie dies zuerst: Ein benutzerdefiniertes HUD ist kein gewöhnliches Panorama-Dokument. Das geladene XML wird durch einen speziellen Whitelist-Validator geprüft, der nur vier Panel-Typen und einen festen Attributsatz zulässt. Es gibt kein style=, kein <script> und keine Event-Handler im Markup. Das gesamte dynamische Verhalten wird vom Server gesteuert.

Die vollständige Pipeline

custom_hud_layout (Server, keyvalue layout="...")
   │  m_strLayout wird zum Client repliziert
   ▼
CSGOCustomHuds (Panorama-Fenster) → layout/hud/customhuds.xml
   └─ <Panel class="WindowRoot" hittest="false">
        └─ <CSGOCustomHud id="CustomHud" style="width:100%; height:100%; flow-children:none;">
             └─ CSGOCustomHudLayoutRoot   ← erstellt aus C++, eine pro Entität,
                  └─ IHR XML                ohne id und ohne class

Rückweg (ein Klick): Benutzernachricht CS_UM_CustomHudClicked (390) → serverseitiges JS-Ereignis OnCustomHudClicked mit { player, layout, buttonId }.

Zwei Arten von HUDs

Jedes benutzerdefinierte HUD ist eines von zwei Dingen, und der Unterschied ist ein einziger Aufruf pro Spieler:

Overlay – der StandardCursor – SetInputCaptureEnabled(slot, true)
Was es istPixel auf dem Bildschirmein Bildschirm, den der Spieler nutzen kann
Mauskeine; das Fadenkreuz zielt weiterhinein Cursor erscheint über dem HUD
Klicksnichts ist anklickbarKlicks auf Button erreichen den Server
:hovertrifft nie zufunktioniert, und kostet den Server nichts
AnwendungTimer, Anzeigetafeln, Kill Feeds, BannerShops, Abstimmungsdialoge, Auswahlen, Menüs

Capture ist jedes Mal ausgeschaltet, wenn das Layout erstellt wird, also muss es nach dem Erscheinen der Entität wieder eingeschaltet werden – siehe entity.md, Input capture. Innerhalb eines captured HUDs entscheidet hittest="false", welche Panels der Cursor durchdringt; schreiben Sie es auf alles und entfernen Sie es von den wenigen Containern, die die Maus abfangen müssen.

Was ein HUD tatsächlich enthalten kann

Das Markup-Vokabular umfasst vier Tags, aber was Sie durch sie hindurch einsetzen können, ist umfangreicher als es aussieht:

  • SVG-Symbole (.vsvg), die bei jeder UI-Skalierung scharf bleiben und zur Laufzeit mit wash-color getönt werden;
  • eigene Schriftarten, indem Sie .ttf-Dateien in das panorama/fonts/-Verzeichnis des Addons legen;
  • spielerspezifischer lokalisierter Text über text="#Token", clientseitig in der jeweiligen Sprache des Spielers aufgelöst, unter Verwendung eigener Zeichenfolgen oder des gesamten Basisspiel-Katalogs;
  • die Grafik des Basisspiels, referenziert über den s2r://-Pfad, ohne dass etwas zum Download hinzugefügt werden muss;
  • Animationen und Übergänge, einschließlich Ein-/Aus-Paaren, die durch Klassen-Toggles gesteuert werden;
  • Video als background-image-Ebene auf einem gewöhnlichen Panel.

Wenn Sie aus dem Web kommen

Panorama hat die Form von HTML/CSS, was sowohl hilfreich als auch eine Falle ist: Die Syntax wirkt vertraut, und dann tut eine Eigenschaft stillschweigend nichts. Das mentale Modell, das funktioniert, ist eine serverseitig gerenderte Seite ohne jegliches Client-Script, bei der die einzigen beiden Aktualisierungen, die es gibt, eine Klasse auf einem Panel umschalten und eine Zeichenkette auf einem Panel setzen sind. Alles andere – jede Variante, jeder Zustand, jede Verzweigung – wird vorab erstellt und nur angezeigt.

Sie würden schreibenHier schreiben Sie
<div><Panel>
<span>, ein Textknoten<Label text="..."> – Text lebt in einem Attribut, nicht zwischen Tags
<img src><Image src>, oder ein background-image auf einem beliebigen Panel
<button onclick><Button id> – der Klick geht an den Server, nicht an ein Script
style="..."nichts; class ist der einzige Styling-Hook
display: nonevisibility: collapse
visibility: hiddenopacity: 0 – noch im Layout, noch klickbar
position: absolutex / y plus ignore-parent-flow: true
flexbox, gridflow-children auf dem Eltern-Element, horizontal-align / vertical-align auf dem Kind-Element
gapmargin auf den Kindern
filter: blur(4px)blur: gaussian( 4, 4, 1 )
rgba(0, 0, 0, 0.5)#00000080 – Farben sind nur hexadezimal, Alpha ist das letzte Byte
linear-gradient(...) in background-imagegradient( ... ) in background-**color**
@font-facelegen Sie die .ttf in panorama/fonts/ ab; nichts zu deklarieren
:hover:hover – eines der wenigen Dinge, das genau wie erwartet funktioniert
ein JS-Event-Handlerdas Server-Ereignis OnCustomHudClicked, gekoppelt an die id des Buttons
data-* gelesen von JS{s:name} Dialogvariablen, vom Server gesetzt
eine Schleife oder <template>beides existiert nicht – zählen Sie die Zeilen im Markup oder im Stylesheet auf
calc(), benutzerdefinierte Eigenschaftenbeides existiert nicht – zählen Sie die Werte im Stylesheet auf

Zwei Gewohnheiten aus der Webentwicklung verursachen hier die meiste Zeitverschwendung. Nach einem berechneten Wert greifen: es gibt kein calc(), keine Variablen, und der Server kann keine Zahl senden, also ist ein Fortschrittsbalken eine Klasse pro Schritt und eine Koordinate ein Literal in einer Regel. Und nach Client-Verhalten greifen: eine Dropdown-Liste, die sich selbst öffnet, eine Registerkartenleiste, die sich selbst umschaltet, ein Formular, das sich selbst validiert – nichts davon kann ohne einen Roundtrip passieren. Gestalten Sie die Interaktion also so, dass ein Klick zum Server geht und eine Klasse zurückkommt.

Was intakt bleibt, ist der Teil, der den Designern am wichtigsten ist: das Boxmodell, Ausrichtung, Farbe, Typografie, Schatten, abgerundete Ecken, Übergänge und Keyframe-Animationen verhalten sich dem Web so ähnlich, dass visuelle Arbeit direkt übertragbar ist. Siehe references/css.md für die vollständige Eigenschaftsliste und die vollständige Liste der nicht vorhandenen Funktionen.

Schnellstart

Ein gearbeitetes Beispiel: ein Capturpunkt-Indikator – ein schreibgeschützter Streifen, der am unteren Rand des Bildschirms mit einem radialen Fortschrittsring verankert ist, vollständig gesteuert durch Dialogvariablen und Klassen-Toggles. Er benötigt keine Input-Erfassung, da nichts darin anklickbar ist.

Die erstellten Quellen befinden sich unter panorama/layout/custom_game/ und panorama/styles/custom_game/ im Content-Root des Addons; der Compiler wandelt sie in .vxml_c / .vcss_c um:

panorama/layout/custom_game/capture_point.xml
panorama/styles/custom_game/capture_point.css

1. Markup – panorama/layout/custom_game/capture_point.xml

<root>
	<styles>
		<include src="s2r://panorama/styles/custom_game/capture_point.vcss_c" />
	</styles>

	<!--
		Das äußere Panel darf KEINE id tragen – der Zuweiser weist sie zu, und der Compiler
		lehnt eine ab. Belassen Sie es als anonymen Vollbild-Wrapper und setzen Sie die eigentliche Wurzel hinein.
	-->
	<Panel class="cp-screen" hittest="false">

		<!-- Startet versteckt: Die Entität zeigt dieses Layout JEDEM Spieler in dem Moment, in dem es erscheint, der erstellte Zustand muss also der leere sein. -->
		<Panel id="CaptureRoot" class="cp-root hidden" hittest="false">

			<Panel class="cp-ring">
				<Panel class="cp-ring-track" hittest="false" />
				<!-- Der Server schreibt die Klasse von cp-fill in cp-fill-0 … cp-fill-100 um;
				     das Stylesheet wandelt jeden Schritt in einen clip: radial()-Bereich um. -->
				<Panel id="CaptureFill" class="cp-ring-fill cp-fill-0" hittest="false" />
				<Label id="CapturePct" class="cp-pct" text="{s:pct}" />
			</Panel>

			<Panel class="cp-copy">
				<Label id="CaptureName" class="cp-name" text="{s:point_name}" />
				<Label id="CaptureHint" class="cp-hint" text="{s:hint}" />
			</Panel>

		</Panel>
	</Panel>
</root>

2. Styles – panorama/styles/custom_game/capture_point.css (es gibt überhaupt keine Inline-Styles)

/* Vollbild-Wrapper. noclip lässt das Leuchten des Rings über den Panelrand hinausragen, anstatt
   abgeschnitten zu werden; der Wrapper selbst wird niemals einem Treffertest unterzogen. */
.cp-screen
{
	width: 100%;
	height: 100%;
	overflow: noclip;
}

/* Der Streifen selbst, oberhalb der unteren Kante positioniert. Er beginnt unsichtbar und versetzt, und der
   Server fügt "show" hinzu, um die Enthüllung abzuspielen – weshalb dies eine Alternative zu .hidden ist,
   keine Ergänzung: Ein collapsed Panel ist aus dem Layout herausgenommen und hat nichts, von dem aus es animieren könnte. */
.cp-root
{
	flow-children: right;
	horizontal-align: center;
	vertical-align: bottom;
	margin-bottom: 96px;
	opacity: 0;
	transform: translateY(20px);
	transition: opacity 0.15s ease-out 0.0s, transform 0.15s ease-out 0.0s;
}

/* Der enthüllte Zustand. Beide Eigenschaften sind im obigen Übergang aufgeführt, so dass das Hinzufügen und
   Entfernen der Klasse in jede Richtung animiert. */
.cp-root.show
{
	opacity: 1;
	transform: translateY(0px);
}

/* Entfernt ein Panel vollständig aus dem Layout, sodass sich benachbarte Elemente schließen, anstatt ein Loch zu hinterlassen.
   Nichts anderes collapsed ein Panel – opacity: 0 reserviert den Platz. */
.hidden
{
	visibility: collapse;
}

/* Rechteckige Box, in der die beiden Ringbilder übereinander gestapelt werden. Beide füllen sie aus, sodass sie genau übereinander liegen. */
.cp-ring
{
	width: 56px;
	height: 56px;
}

/* Der ungefüllte Ring darunter, abgedunkelt, damit die Füllung oben als Fortschritt lesbar ist. Bildmaterial kommt
   aus einer kompilierten Textur über s2r:// – die Standardform. */
.cp-ring-track
{
	width: 100%;
	height: 100%;
	background-image: url( "s2r://panorama/images/custom_game/ring_png.vtex" );
	background-size: contain;
	background-repeat: no-repeat;
	opacity: 0.25;
}

/* Dasselbe Bildmaterial oben, durch wash-color neu eingefärbt und nach und nach durch die
   .cp-fill-*-Regeln unten freigelegt. Eine graue Quelle zu tönen ist besser, als eine Textur pro Team auszuliefern. */
.cp-ring-fill
{
	width: 100%;
	height: 100%;
	background-image: url( "s2r://panorama/images/custom_game/ring_png.vtex" );
	background-size: contain;
	background-repeat: no-repeat;
	wash-color: #4caf6dff;
}

/* Eine Regel pro Schritt, den der Server auswählen kann, da er eine Klasse umschalten, aber keine Zahl senden kann.
   clip ist layoutkostenfrei und animiert, daher sind mehr Schritte kostengünstig. */
.cp-fill-0
{
	clip: radial( 50% 50%, 0deg, 0deg );
}

.cp-fill-25
{
	clip: radial( 50% 50%, 0deg, 90deg );
}

.cp-fill-50
{
	clip: radial( 50% 50%, 0deg, 180deg );
}

.cp-fill-75
{
	clip: radial( 50% 50%, 0deg, 270deg );
}

.cp-fill-100
{
	clip: radial( 50% 50%, 0deg, 360deg );
}

/* Textspalte rechts vom Ring, zentriert dazu. */
.cp-copy
{
	flow-children: down;
	vertical-align: center;
	margin-left: 12px;
}

/* Punktname – die Zeile, die der Spieler zuerst liest. */
.cp-name
{
	font-size: 20px;
	font-weight: bold;
	color: #ffffffff;
}

/* Ergänzungszeile, mit Alpha zurückgehalten anstatt mit einer zweiten Farbe. */
.cp-hint
{
	font-size: 14px;
	color: #ffffff99;
}

/* Team-Akzent. Der Server setzt team-ct oder team-t auf die Wurzel, und der Nachkommen-Selektor
   übermalt die Füllung – Teamfarbe und Fortschritt bleiben also unabhängig voneinander. */
.cp-root.team-ct .cp-ring-fill
{
	wash-color: #56a0ddff;
}

.cp-root.team-t .cp-ring-fill
{
	wash-color: #f0a531ff;
}

3. Entität – in Hammer oder über CreateEntityByName

SchlüsselwertWert
layoutpanorama/layout/custom_game/capture_point.vxml

4. Vom Server aus steuern (serverseitiges JS unter cs_script) – siehe entity.md

const hud = Instance.FindEntityByName("capture_hud"); // JS-Wrapper-Klasse: CustomHudLayout

function showCapture(slot, name, pct, team) {
	hud.SetHasClassForPlayer(slot, "CaptureRoot", "show", true);
	hud.SetHasClassForPlayer(slot, "CaptureRoot", "team-" + team, true);

	// die Schrittklasse wechseln: die alte löschen, die neue setzen
	hud.SetHasClassForPlayer(slot, "CaptureFill", "cp-fill-" + roundTo25(pct), true);

	hud.SetDialogVariableStringForPlayer(slot, "CapturePct", "pct", pct + "%");
	hud.SetDialogVariableStringForPlayer(slot, "CaptureName", "point_name", name);
	hud.SetDialogVariableStringForPlayer(slot, "CaptureHint", "hint", "Halte den Punkt");
}

function hideCapture(slot) {
	hud.SetHasClassForPlayer(slot, "CaptureRoot", "show", false);
}

Da der Server nur Klassen umschalten und Zeichenketten setzen kann – er kann keine Zahl senden – muss ein kontinuierlicher Wert wie ein Fortschrittsring in eine Klasse pro Schritt quantisiert werden. Fünf Schritte sind für einen Captur-Ring ausreichend; ein gleichmäßigerer Balken braucht nur mehr Regeln.

Häufige Ablehnungen

Was Sie wollenWarum es fehlschlägt
<TextEntry>, <ProgressBar>, <Movie>, <ToggleButton>, jedes andere Tagnur Panel, Label, Image, Button – für Video verwenden Sie eine background-image-Ebene anstelle von <Movie>
style="..." auf einem Elementkein Panel-Typ hat ein style-Attribut
onactivate="..." oder ein anderer Markup-Event-Handlerkeine; Klicks erreichen den Server, gekoppelt an id
<scripts>, <snippets>, <snippet>auf AST-Knotentyp-Ebene zurückgewiesen
<include> einer anderen .xmlnur eine .vcss-Ressource kann referenziert werden
dialogvariable="..." als Attributder Server setzt Variablen über SetDialogVariableString
hittest auf <Button>Button akzeptiert nur id und class

Vollständiger erlaubter Satz: xml.md.

Fehlersuche

Der Validator protokolliert im custom_hud-Logging-Kanal auf Warnstufe:

NachrichtUrsache
Layout xml is an invalid resource name "%s"layout löst sich nicht in einen Ressourcennamen auf
Failed to load layout '%s'.Datei fehlt oder nicht kompiliert
Layout contains disallowed node '%s' (type: %d).<scripts>/<snippets>/<snippet> usw.
Layout contains disallowed panel type '%s'.Tag außerhalb der vier erlaubten
Layout contains disallowed attribute %s for panel type '%s'.Attribut nicht in der Liste dieses Tags
Layout contains reference to disallowed resource type '%s'.<include> zielt auf eine Nicht-.vcss-Ressource ab
Layout xml did not pass CustomHud validation "%s"endgültige Zurückweisung
Layout is invalid.ein Attributknoten, dessen Eltern-Element kein Panel ist

Bei einem Validierungsfehler wird das Panel nie erstellt – ein leerer Bildschirm, kein teilweises Rendern. Das heiße Neuladen eines Live-Layouts in einen ungültigen Zustand zerstört das vorhandene HUD.

Referenzen

DateiInhalt
xml.mdVollständige Tag-/Attribut-Whitelist, AST-Knotentypen, Validatorregeln
css.mdAlle 140 Panorama CSS-Eigenschaften, Einheiten, Flow-/Align-Wertsätze, Vorbereitung …

… [für die Übersetzung gekürzt]

Verwandte Skills