Communitygithub.com

Wend4r/s2r-skills

一個用於處理 Source 2 資源和開發相關程式碼的代理技能集。

s2r-skills 是什麼?

此技能集合專為使用 Source 2 引擎(如 CS2)工作的開發者設計,提供建立自訂 HUD 佈局的完整指引。它詳細說明了 `custom_hud_layout` 實體的運作方式、受限制的 XML/VCSS 標記管道,以及伺服器端驅動行為的機制。該技能的核心價值在於解釋了 Panorama UI 系統的獨特限制(僅允許四種面板類型、無客戶端腳本),並提供了從網頁開發轉換的對照表、常見陷阱和除錯資訊。它支援透過 Hammer 編輯器或伺服器端 JS 腳本建立和操作 HUD,並包含完整的範例(如佔點進度指示器),展示了如何利用類別切換和對話變數來實現動態效果。

相容平台~Claude Code~Codex CLI✓Cursor
npx skills add Wend4r/s2r-skills

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Source 2 | 自訂 HUD 佈局

custom_hud_layout 實體 (C++ CCSCustomHudLayout) 讓地圖或伺服器端外掛程式,能夠使用 Panorama XML + VCSS 為玩家呈現一個 HUD。

請先閱讀此說明: 自訂 HUD 並非一般的 Panorama 文件。載入的 XML 會經過一個專用的白名單驗證器,該驗證器僅允許四種面板類型和一組固定的屬性。標記中沒有 style=、沒有 <script>,也沒有事件處理器。所有動態行為均由伺服器驅動。

完整管線

custom_hud_layout (伺服器端, keyvalue layout="...")
   │  m_strLayout 會複製到客戶端
   ▼
CSGOCustomHuds (Panorama 視窗) → layout/hud/customhuds.xml
   └─ <Panel class="WindowRoot" hittest="false">
        └─ <CSGOCustomHud id="CustomHud" style="width:100%; height:100%; flow-children:none;">
             └─ CSGOCustomHudLayoutRoot   ← 由 C++ 建立,每個實體一個,
                  └─ 你的 XML                沒有 id 和 class

返回路徑 (點擊):玩家訊息 CS_UM_CustomHudClicked (390) → 伺服器端 JS 事件 OnCustomHudClicked,攜帶 { player, layout, buttonId }。

兩種 HUD

每個自訂 HUD 都屬於以下兩種之一,差別在於一個適用於單一玩家的函式呼叫:

覆蓋層 (Overlay) — 預設值游標 (Cursor) — SetInputCaptureEnabled(slot, true)
它是什麼螢幕上的像素玩家可以使用的螢幕
滑鼠無;準星繼續瞄準游標出現在 HUD 上
點擊沒有東西可以點擊Button 的點擊會送達伺服器
:hover永遠不匹配可以運作,且不會消耗伺服器資源
用途計時器、記分板、擊殺提示、橫幅商店、投票對話框、選擇器、選單

每次佈局建立時,輸入捕捉都會是關閉的,因此需要在實體生成後再次開啟 — 請參閱 entity.md 中的輸入捕捉。在已捕捉的 HUD 中,hittest="false" 決定游標可以穿透哪些面板;請將它寫在所有面板上,然後從必須攔截滑鼠的少數容器上移除它。

HUD 實際可以包含的內容

標記詞彙只有四個標籤,但你可以透過它們放入的內容比看起來更多:

  • SVG 圖示 (.vsvg),在任何 UI 縮放下都保持銳利,可在執行時使用 wash-color 著色;
  • 你自己的字型,將 .ttf 檔案放入附加元件的 panorama/fonts/ 目錄;
  • 每位玩家各自的本地化文字,透過 text="#Token" 在客戶端以每個玩家自己的語言解析,可使用你自己的字串或整個基礎遊戲目錄;
  • 基礎遊戲的素材,透過 s2r:// 路徑引用,無需在你的下載內容中添加任何東西;
  • 動畫和轉場,包括由類別切換驅動的進入/退出配對;
  • 影片,作為普通 Panel 上的 background-image 圖層。

如果你來自網頁開發

Panorama 的形狀類似 HTML/CSS,這既是幫助也是陷阱:語法看似熟悉,然後某個屬性會默默地不產生任何效果。可行的思維模型是 一個完全沒有客戶端腳本的伺服器端渲染頁面,其中僅有的兩種更新是切換面板上的類別和設定面板上的字串。其他所有東西 — 每個變體、每個狀態、每個分支 — 都預先建立好,僅僅是被顯示出來。

你會這樣寫在這裡你要這樣寫
<div><Panel>
<span>、文字節點<Label text="..."> — 文字存在於屬性中,不在標籤之間
<img src><Image src>,或任何面板上的 background-image
<button onclick><Button id> — 點擊會送到伺服器,而不是腳本
style="..."無;class 是唯一的樣式鉤子
display: nonevisibility: collapse
visibility: hiddenopacity: 0 — 仍在佈局中,仍然可以點擊
position: absolutex / y 加上 ignore-parent-flow: true
flexbox、grid父元素上的 flow-children,子元素上的 horizontal-align / vertical-align
gap子元素上的 margin
filter: blur(4px)blur: gaussian( 4, 4, 1 )
rgba(0, 0, 0, 0.5)#00000080 — 顏色只能是十六進位,Alpha 通道是最後一個字節
background-image 中的 linear-gradient(...)background-**color** 中的 gradient( ... )
@font-face將 .ttf 放入 panorama/fonts/;無需宣告
:hover:hover — 少數幾個完全如預期運作的東西之一
JS 事件處理器伺服器事件 OnCustomHudClicked,由按鈕的 id 作為鍵值
由 JS 讀取的 data-*{s:name} 對話變數,從伺服器設定
迴圈或 <template>兩者都不存在 — 在標記或樣式表中列舉行數
calc()、自訂屬性兩者都不存在 — 在樣式表中列舉值

來自網頁工作的兩個習慣會浪費你在這裡的大部分時間。習慣使用計算值:沒有 calc()、沒有變數,伺服器也無法發送數字,因此進度條是每個步驟一個類別,座標則是規則中的字面值。以及習慣使用客戶端行為:會自己打開的下拉選單、會自己切換的分頁、會自己驗證的表單 — 這些都不可能不經過一次往返就發生,因此請圍繞「點擊送到伺服器,類別返回客戶端」來設計互動。

完好無損保留的是設計師最關心的部分:盒模型、對齊、顏色、字體排印、陰影、圓角、轉場和關鍵影格動畫的行為都與網頁足夠接近,視覺工作可以直接轉移。請參閱 references/css.md 以了解完整的屬性集合和所有缺失內容的清單。

快速入門

一個完整的範例:佔點指示器 — 一個固定在螢幕底部、帶有徑向進度環的唯讀條,完全由對話變數和類別切換驅動。它不需要輸入捕捉,因為裡面沒有可點擊的東西。

原始檔案位於附加元件內容根目錄下的 panorama/layout/custom_game/ 和 panorama/styles/custom_game/;編譯器會將它們轉換為 .vxml_c / .vcss_c:

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

1. 標記 — panorama/layout/custom_game/capture_point.xml

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

	<!--
		外層面板不能有 id — 載入器會分配它,編譯器會拒絕。
		讓它保持為一個匿名的全螢幕包裝器,並將真正的根元素放在裡面。
	-->
	<Panel class="cp-screen" hittest="false">

		<!-- 預設隱藏:實體一生成就會向**所有**玩家顯示此佈局,因此初始狀態必須是空的。 -->
		<Panel id="CaptureRoot" class="cp-root hidden" hittest="false">

			<Panel class="cp-ring">
				<Panel class="cp-ring-track" hittest="false" />
				<!-- 伺服器會將 cp-fill 的類別重寫為 cp-fill-0 … cp-fill-100;
				     樣式表會將每個步驟轉換為 clip: radial() 掃描。 -->
				<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. 樣式 — panorama/styles/custom_game/capture_point.css (完全沒有行內樣式)

/* 全螢幕包裝器。noclip 允許環的光暈溢出面板邊緣而不被剪裁;
   包裝器本身永遠不會進行點擊測試。 */
.cp-screen
{
	width: 100%;
	height: 100%;
	overflow: noclip;
}

/* 條本身,停放在底部邊緣上方。它從不可見且偏移開始,伺服器
   會添加 "show" 來播放顯現動畫 — 這就是為什麼這是 .hidden 的替代方案,
   而不是附加:收攏的面板脫離佈局,沒有東西可以從中開始動畫。 */
.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;
}

/* 顯示狀態。兩個屬性都列在上面的 transition 中,因此添加和
   移除類別會朝兩個方向產生動畫。 */
.cp-root.show
{
	opacity: 1;
	transform: translateY(0px);
}

/* 完全從佈局中移除面板,因此相鄰元素會靠攏而不是留下空隙。
   沒有其他東西會收攏面板 — opacity: 0 會保留空間。 */
.hidden
{
	visibility: collapse;
}

/* 兩個環形圖像疊在內部的方形盒子。兩者都填滿它,因此它們完全對齊。 */
.cp-ring
{
	width: 56px;
	height: 56px;
}

/* 底層的未填充環,調暗以便上方的填充部分被解讀為進度。素材來自
   透過 s2r:// 編譯的紋理 — 標準形式。 */
.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;
}

/* 上方的相同素材,由 wash-color 重新著色,並由下方的 .cp-fill-* 規則
   一次顯示一個扇形區段。為一個灰色來源著色比為每個隊伍傳送一個紋理更好。 */
.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;
}

/* 伺服器可以選擇的每個步驟一個規則,因為它可以切換類別但不能發送數字。
   clip 沒有佈局成本並且可以動畫,因此更多步驟是便宜的。 */
.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 );
}

/* 環右側的文字欄,與環垂直居中對齊。 */
.cp-copy
{
	flow-children: down;
	vertical-align: center;
	margin-left: 12px;
}

/* 點名 — 玩家最先讀到的行。 */
.cp-name
{
	font-size: 20px;
	font-weight: bold;
	color: #ffffffff;
}

/* 輔助行,使用 Alpha 而非第二種顏色來降低視覺重量。 */
.cp-hint
{
	font-size: 14px;
	color: #ffffff99;
}

/* 隊伍強調色。伺服器在根元素上設定 team-ct 或 team-t,而後代選擇器
   會重新繪製填充 — 因此隊伍顏色和進度保持彼此獨立。 */
.cp-root.team-ct .cp-ring-fill
{
	wash-color: #56a0ddff;
}

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

3. 實體 — 在 Hammer 中或透過 CreateEntityByName

鍵值值
layoutpanorama/layout/custom_game/capture_point.vxml

4. 從伺服器驅動它 (伺服器端 JS,位於 cs_script 下) — 請參閱 entity.md

const hud = Instance.FindEntityByName("capture_hud"); // JS 包裝類別:CustomHudLayout

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

	// 切換步驟類別:清除舊的,設定新的
	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", "Hold the point");
}

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

由於伺服器只能切換類別和設定字串 — 它不能發送數字 — 像進度環這樣的連續值必須被量化為每個步驟一個類別。對於一個佔點環來說,五個步驟就足夠了;更平滑的條只需要更多規則。

常見拒絕情況

你想要什麼為什麼會失敗
<TextEntry>、<ProgressBar>、<Movie>、<ToggleButton> 或其他任何標籤只有 Panel、Label、Image、Button — 對於影片,使用 background-image 圖層而不是 <Movie>
元素上的 style="..."沒有面板類型有 style 屬性
onactivate="..." 或任何標記事件處理器沒有;點擊會透過 id 送達伺服器
<scripts>、<snippets>、<snippet>在 AST 節點類型層級被拒絕
引入另一個 .xml 的 <include>只能引用 .vcss 資源
作為屬性的 dialogvariable="..."伺服器透過 SetDialogVariableString 設定變數
<Button> 上的 hittestButton 只接受 id 和 class

完整允許的集合:xml.md。

除錯

驗證器會將訊息記錄到 custom_hud 日誌通道,嚴重性為 warning:

訊息原因
Layout xml is an invalid resource name "%s"layout 無法解析為資源名稱
Failed to load layout '%s'.檔案遺失或未編譯
Layout contains disallowed node '%s' (type: %d).<scripts>/<snippets>/<snippet> 等
Layout contains disallowed panel type '%s'.超出四個允許範圍的標籤
Layout contains disallowed attribute %s for panel type '%s'.屬性不在該標籤的清單中
Layout contains reference to disallowed resource type '%s'.<include> 目標是非 .vcss 資源
Layout xml did not pass CustomHud validation "%s"最終拒絕
Layout is invalid.一個屬性節點的父節點不是面板

驗證失敗時,面板永遠不會被建立 — 顯示空白畫面,沒有部分渲染。將一個即時佈局熱載入為無效狀態會銷毀現有的 HUD。

參考資料

檔案內容
xml.md完整的標籤/屬性白名單、AST 節點類型、驗證器規則
css.md全部 140 個 Panorama CSS 屬性、單位、流向/對齊值集合、預先…

相關技能