Communitygithub.com

Wend4r/s2r-skills

用于处理 Source 2 资源并开发相关代码的 Agent 技能集合

s2r-skills 是什么?

该技能集合专为 AI Agent(如 Claude Code、Cursor 等)设计,专注于 Source 2 引擎资源处理与代码开发。核心内容围绕 `custom_hud_layout` 实体展开,该实体允许地图或服务端插件通过 Panorama XML 和 VCSS 创建自定义 HUD。与普通 Panorama 文档不同,自定义 HUD 使用严格的白名单验证器,仅允许四种面板类型和固定属性集,不支持 `style`、`<script>` 或事件处理程序——所有动态行为均由服务端驱动。 技能文档详细介绍了两种 HUD 类型(覆盖型和光标型)、完整的管道流程、XML 和 CSS 的编写规范,以及从 Web 开发迁移到 Source 2 时的常见陷阱和对应写法。它强调“服务端渲染、无客户端脚本”的思维方式,仅通过切换面板类和设置字符串来驱动更新。此外,还提供了完整的属性白名单、调试日志信息以及一个从零开始的占领点指示器示例。 该技能适用于需要自定义游戏 UI 的 Source 2 模组开发者,以及希望自动生成或修改此类资源的 AI Agent。支持平台包括 Counter-Strike 2 等使用 Source 2 引擎的游戏。

兼容平台~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 都是以下两种类型之一,区别仅在于每个玩家的调用:

覆盖型 — 默认光标型 — 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 图层。

如果你来自 Web 开发

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 — 颜色仅限十六进制,透明度为最后一个字节
background-image 中的 linear-gradient(...)background-**color** 中的 gradient( ... )
@font-face将 .ttf 放入 panorama/fonts/;无需声明
:hover:hover — 少数完全按预期工作的功能之一
JS 事件处理程序服务端事件 OnCustomHudClicked,通过按钮的 id 键绑定
data-* 由 JS 读取{s:name} 对话框变量,由服务端设置
循环或 <template>两者都不存在——在标记或样式表中枚举行
calc(),自定义属性两者都不存在——在样式表中枚举值

来自 Web 开发的两个习惯会浪费大量时间。使用计算值:没有 calc(),没有变量,服务端无法发送数字,因此进度条是每步一个类,坐标是规则中的字面量。使用客户端行为:自打开的下拉菜单、自切换的标签页、自验证的表单——这些都需要往返才能实现,因此设计交互时应围绕点击到服务端和类返回。

设计师最关心的部分完好无损:盒模型、对齐、颜色、排版、阴影、圆角、过渡和关键帧动画的行为与 Web 非常接近,视觉工作可以直接迁移。请参阅 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;
}

/* 揭示状态。两个属性都列在上面的过渡中,因此添加和移除类会双向动画。 */
.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

Keyvalue值
layoutpanorama/layout/custom_game/capture_point.vxml

4. 从服务端驱动(cs_script 下的服务端 JS)——参见 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", "占领该点");
}

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 日志通道,严重级别为警告:

消息原因
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 属性、单位、flow/align 值集、预处理器...

相关技能