ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SourceMod 插件开发:spcomp 编译与 VSCode 任务配置实战

SourceMod 插件开发:spcomp 编译与 VSCode 任务配置实战 1. 先搞清楚 spcomp 在整条链路里到底站哪个位置很多人第一次配 SourceMod 编译环境注意力全在 VSCode 上结果配了两小时 tasks.json最后发现卡住的是 spcomp 本身没跑起来。所以这一节我先把链路摊开说清楚后面配 VSCode 才有参照物。1.1 从 .sp 到 .smx中间发生了什么SourceMod 插件源码是.sp文件用的是 SourcePawn 这门语言。它不是解释执行的脚本也不是原生动态库而是需要经过编译器生成一份.smx字节码文件再由服务端的 SourceMod 虚拟机加载执行。也就是说源码改了不重新编译服务端跑的还是老逻辑这是新手最常见的我明明改了代码的根源。编译这一步由spcomp完成它是 SourcePawn 的官方命令行编译器每个 SourceMod 发行包里都自带。你在服务端目录里翻一下addons/sourcemod/scripting/这个路径Windows 下能看到spcomp.exeLinux 下能看到spcomp同时还能看到include/目录、compile.sh、compile.bat这几个文件。这个目录就是官方给你准备好的本地编译工作台。.sp是纯文本编辑器随便用.smx是二进制的字节码包里面塞了常量表、函数入口、本地变量表等信息。VSCode 在这个流程里扮演的角色非常单纯——它是命令的触发器和输出的解析器真正的编译行为全部发生在 spcomp 进程里。想明白这一点后面所有配置都只是怎么把参数喂对和怎么把结果读出来。1.2 为什么非要自己搭本地编译而不是用现成的SourceMod 生态里确实存在在线编译的途径写个小插件贴上去就能出结果。但它有几个绕不开的硬伤第一你的自定义.inc文件传不上去只要插件依赖了自己拆分的头文件在线编译直接报找不到符号第二你没法锁定编译器版本服务器上跑的是 1.11在线可能给你编成 1.12 的产物行为差异能让你调一晚上第三也是最要命的没有任何错误定位能力它只告诉你第几行有问题而你连那一行上下文都得自己数。还有一个更现实的原因插件开发是高频迭代的活。改一行、编一次、重载一次、进游戏看一眼这个循环一天要跑几十上百遍。多花十秒切换到浏览器、上传、下载、再挪文件一天就是十几分钟的纯浪费而且极易打断思路。把编译压到 VSCode 里一个快捷键是投入产出比极高的一件事。1.3 VSCode 在这一环里能给你什么VSCode 的价值不是更漂亮的编辑器而是三件事。一是任务系统它可以把一条命令行固化成一个命名任务绑定CtrlShiftB一键触发。二是问题匹配器它能读懂编译器吐出来的文本把文件(line) : error 001: ...这种格式解析成可点击的条目排在底部的 Problems 面板里双击直接跳到出错的那一行。三是生态扩展SourcePawn 的语法高亮、括号匹配、部分补全、跳转定义都有社区扩展在做市场里搜SourcePawn能搜到几个挑下载量高、最近还在维护的装就行作者名字迭代比较快不用死记。真正决定配置难度的是第二点。很多人的 tasks.json 明明能编译成功但错误永远点不开就是因为 problemMatcher 的正则没对上 spcomp 的实际输出格式。这个我后面会单独开一节拆。2. 别急着写 tasks.json先把 spcomp 命令行跑通我见过太多人在 VSCode 里反复调 JSON改一次跑一次其实是在拿一个不透明的黑盒试错。正确顺序是先在终端里用纯命令行把编译跑通命令确定了再往 JSON 里搬。这样出错的时候你能立刻区分是命令本身不对还是VSCode 传参不对。2.1 先确认你的 spcomp 是哪一个版本服务端addons/sourcemod/scripting/下的 spcomp和你服务端跑的 SourceMod 版本是配套的这是最省事的组合。有人会把旧版本 SourceMod 的 spcomp 拿来编新插件的代码或者反过来短期可能没报错但一旦用到了新版才有的原生函数就会出现编译期正常、运行期报未知符号这种非常难查的问题。判断版本最简单的方式是直接在命令行里不带任何参数运行一次# Windows cd D:\server\tf2\addons\sourcemod\scripting spcomp.exe # Linux cd /home/user/server/tf/addons/sourcemod/scripting chmod x spcomp ./spcomp不带参数时它会打印用法和所有可用选项顺带把版本信息也吐出来。Linux 下第一次跑大概率会碰到权限不够补一句chmod x就好。这个动作看着多余实际上能一次性解决后面三个高频疑惑参数到底是紧贴写还是空格分隔、警告等级的取值范围是多少、默认 include 路径是哪里。提示不同大版本的 spcomp 参数写法确实会有细微差异帮助信息里的写法就是权威答案。本文后续给的所有参数都以帮助里能查到为前提遇到对不上的情况以你本地输出的帮助为准。2.2 一条最小可用命令应该长什么样假设你要编译scripting/目录下的my_plugin.sp把产物丢到addons/sourcemod/plugins/下最朴素的一条命令是# 在 scripting 目录下执行 ./spcomp -i include -o ../plugins/my_plugin.smx my_plugin.spWindows 下把./spcomp换成spcomp.exe其他一样。这条命令能成说明整个链路是通的。这里有个几乎所有人都忽略的机制spcomp 默认会去搜索与自己同级目录下的include/文件夹。因为spcomp和include/都在scripting/里所以你甚至可以把-i include省掉直接spcomp -o ../plugins/my_plugin.smx my_plugin.sp也能编过。官方自带的compile.sh/compile.bat里写的命令本质上就是这么一回事你可以先打开这两个文件扫一眼VSCode 的任务配置就是把里面的内容拆成参数数组思路一脉相承。那我为什么还是建议显式写-i include因为显式声明能让配置脱离当前工作目录这个隐含依赖。当你在 VSCode 里用变量拼绝对路径时cwd和-i谁生效、谁覆盖谁就变成一个必须先想清楚的问题。2.3 include 路径的搜索顺序决定了你踩不踩符号冲突-i是可以重复出现的多个-i会按出现顺序依次加入搜索路径前面的优先。这个顺序在你有多个来源的同名头文件时会直接决定编译结果。举个很实际的场景你在项目里自己写了一份myutils.inc同时服务器自带的 include 里也有一个叫同名的文件不同来源可能来自别的插件作者。如果你的自定义目录排在官方目录后面spcomp 会静默使用官方那份然后你在代码里调用自己定义的函数报undefined symbol而那个符号明明就在你眼前的文件里。这种问题只靠看代码是查不出来的必须意识到搜索顺序的存在。我的习惯是官方 include 放第一顺位自己的项目 include 放第二顺位第三方依赖的 include 放第三顺位。这样官方 API 永远稳定自己的扩展能覆盖同名第三方最弱。配合这个习惯目录也照这个层级摆。2.4 常用参数各自在解决什么问题把这张表存下来配 task 的时候直接对着抄。参数作用开发期建议发布期建议-i追加 include 搜索目录可重复显式写全显式写全-o指定输出.smx路径与文件名输出到 plugins 目录同左-E只做预处理不生成 smx排错时用不用-w警告等级调高尽早暴露问题调高-v输出详细程度调低保持输出干净调低-d调试信息等级适当调高便于定位崩溃行号关闭或最低-O优化等级可低可高拉高-v这一项特别值得说。默认详细程度下spcomp 会打印一堆正在编译 xxx的进度信息输出越长problemMatcher 的匹配成本越高终端里也越难一眼看到真正的错误。开发期把-v调到最低一般是 0只保留错误和警告输出的可读性会好非常多。-E是个被严重低估的参数。它只做预处理把#include全部展开、宏全部替换输出一份巨大的中间文件。当出现undefined symbol或者宏展开结果不符合预期时用-E把结果重定向到文件里搜一下符号名立刻就能看出是哪个头文件没被包含进来、或者宏被展开成了什么鬼样子。这比一行行人肉追 include 快十倍。3. 把命令行翻译成 tasks.json现在命令确定了搬进 VSCode 就只是格式问题。VSCode 的任务定义存在工作区根目录的.vscode/tasks.json里这个目录和文件不会自动生成需要自己建。有一个前提要先想清楚VSCode 的工作区根目录就是你打开的那个文件夹后面所有${workspaceFolder}变量都指向它所以目录怎么摆直接决定了变量怎么写。3.1 目录骨架决定了变量怎么写两种常见的目录组织方式各有取舍。第一种是直接打开服务端的 sourcemod 目录作为工作区也就是workspaceFolder指向addons/sourcemod/。好处是路径最短${workspaceFolder}/scripting/include就能直接定位到官方头文件基本和官方自带脚本的视角一致。坏处是工作区里会混进plugins/、logs/、configs/、translations/一大堆你根本不编辑的目录文件树很吵全局搜索也会被日志文件干扰。第二种是把插件源码单独放在一个开发目录里工作区指向这个目录通过-i把服务端的官方 include 目录挂进来。好处是干净源码、文档、自建头文件都在一个可控空间里可以独立做版本管理。代价是每次换服务器要改一下 include 路径或者把这个路径写进 VSCode 的用户级配置里。我个人长期用的是第二种并且把 spcomp 的绝对路径和服务端 include 的绝对路径抽到用户配置里这样一台机器上开多少个插件的独立工作区都不用重复填// settings.json用户级 { sourcemod.spcompPath: D:/server/tf2/addons/sourcemod/scripting/spcomp.exe, sourcemod.smInclude: D:/server/tf2/addons/sourcemod/scripting/include }然后在 tasks.json 里用${config:sourcemod.spcompPath}取值。这一招在多服务器比如同时维护一个测试服和一个正式服的场景下特别香切换服务器只要改用户配置的两行所有工作区的任务全跟着换。3.2 第一版 tasks.json能跑起来就算成功先把最小可用版本写出来别一上来就堆一堆配置。{ version: 2.0.0, tasks: [ { label: spcomp: 编译当前 .sp, type: process, command: ${config:sourcemod.spcompPath}, args: [ -i${config:sourcemod.smInclude}, -i${workspaceFolder}/scripting/include, -o${workspaceFolder}/plugins/${fileBasenameNoExtension}.smx, -v0, ${file} ], options: { cwd: ${workspaceFolder}/scripting }, group: { kind: build, isDefault: true }, presentation: { reveal: silent, panel: shared, clear: true, showReuseMessage: false } } ] }几个关键点解释一下。type我选了process而不是shell。process模式下 VSCode 直接exec那个可执行文件参数以数组形式原样传递不经过任何 shell 解释路径里有空格也不用你自己加引号也不会遇到 PowerShell 把当调用符这种诡异问题。绝大多数情况下这是更稳的选择只有当你要用管道、重定向或通配符的时候才需要换回shell。group里的isDefault: true是把任务设为默认构建任务设完之后CtrlShiftB直接触发它不用再选。presentation.reveal设成silent是让编译面板不抢焦点——因为错误会进 Problems 面板终端里那串输出你其实不看抢焦点反而打断打字。3.3 参数紧贴写还是分开写这是最容易反复踩的一个点。-i${config:sourcemod.smInclude}这种写法是把参数名和值拼在一起当一个数组元素传过去对应命令行里的-ipath/to/include。另一种写法是拆成两个元素-i, ${config:sourcemod.smInclude}对应-i path/to/include。这两种写法在参数解析上通常是等价的编译器会做兼容处理但不要在同一份 tasks.json 里混用两种风格因为一旦某个参数不支持空格分隔你会得到路径被当成输入文件这种莫名其妙的错误比如 spcomp 去编译一个名叫D:/server/...的文件然后告诉你找不到。判断依据还是那句跑一次裸的 spcomp看帮助里参数写法是-ipath还是-i path。帮助里带尖括号紧贴写的就用紧贴式。3.4 再补一个固定入口的任务${file}有个天然限制它指向当前激活的编辑器标签对应的文件。这意味着你如果正在编辑一个.inc文件随手按下编译快捷键spcomp 会去编译那个.inc——而.inc通常不是一个合法的编译入口输出会是一堆找不到入口点之类的怪错误。所以成熟一点的做法是配两个任务一个编译当前文件用于单文件插件一个编译主入口把路径写死用于多文件工程和自动触发场景。{ label: spcomp: 编译主入口, type: process, command: ${config:sourcemod.spcompPath}, args: [ -i${config:sourcemod.smInclude}, -i${workspaceFolder}/scripting/include, -o${workspaceFolder}/plugins/my_main.smx, -v0, ${workspaceFolder}/scripting/my_main.sp ], options: { cwd: ${workspaceFolder}/scripting }, group: { kind: build }, problemMatcher: [] }两个任务并列存在CtrlShiftB走默认那个需要另一个的时候用CtrlShiftP输入 Run Task 再选。习惯之后日常开发基本只用手动那一个自动触发的场景才用固定入口那个。4. 让错误能点一下就跳过去这是整个配置里最影响幸福感的一环也是翻车率最高的一环。任务能跑通只说明你能编译problemMatcher 配好了才说明你能高效地改错。4.1 先看清楚 spcomp 的错误长什么样不要凭想象写正则先制造一个错误把原始输出抓下来。在原文件里故意删掉一个分号然后跑一次编译你会看到类似这样的文本/path/to/scripting/my_plugin.sp(37) : error 001: expected token: ;, but found -identifier- /path/to/scripting/my_plugin.sp(52) : warning 217: loose indentation /path/to/scripting/my_plugin.sp(18) : fatal error 100: cannot read from file: myutils.inc三种级别error、warning、fatal error。格式高度一致绝对路径 圆括号包住的行号 空格冒号空格 级别 数字编号 冒号 描述文本。这就是正则要拆的东西。注意不同版本 spcomp 输出里行号后面的冒号前后空格数量可能有差异写正则的时候用\s*而不是硬写一个空格容错率高很多。4.2 正则拆解与 fileLocation 的选择按上面的格式一条 pattern 就能覆盖全部三种级别problemMatcher: { owner: sourcepawn, fileLocation: absolute, pattern: [ { regexp: ^(.?)\\((\\d)\\)\\s*:\\s*(fatal error|error|warning)\\s(\\d)\\s*:\\s*(.)$, file: 1, line: 2, severity: 3, code: 4, message: 5 } ] }fileLocation用absolute而不是[relative, ${workspaceFolder}]原因很直接spcomp 打印的路径是它接收到的路径。我们在 args 里传的是${file}也就是绝对路径所以输出自然也是绝对路径直接用 absolute 最省事。如果用 relativeVSCode 会把路径往工作区上拼拼出来的东西根本不存在表现就是错误列表能显示、但双击跳不过去。severity那一项需要处理一下。VSCode 只认error、warning、info这几个值而 spcomp 会输出fatal error这个字符串。直接映射会导致这条被忽略或者当成未知级别。解决办法是用映射表severity: { fatal error: error, error: error, warning: warning }如果你的 VSCode 版本对映射支持不理想退一步的办法是在正则里做重写把 fatal error 归一化成 error或者干脆拆成两条 pattern一条专门匹配 fatal error。实际用下来映射表的方案在新版 VSCode 上是稳的。4.3 完整拼起来的任务长这样{ label: spcomp: 编译当前 .sp, type: process, command: ${config:sourcemod.spcompPath}, args: [ -i${config:sourcemod.smInclude}, -i${workspaceFolder}/scripting/include, -o${workspaceFolder}/plugins/${fileBasenameNoExtension}.smx, -v0, ${file} ], options: { cwd: ${workspaceFolder}/scripting }, group: { kind: build, isDefault: true }, presentation: { reveal: silent, panel: shared, clear: true, showReuseMessage: false }, problemMatcher: { owner: sourcepawn, fileLocation: absolute, pattern: [ { regexp: ^(.?)\\((\\d)\\)\\s*:\\s*(fatal error|error|warning)\\s(\\d)\\s*:\\s*(.)$, file: 1, line: 2, severity: { fatal error: error, error: error, warning: warning }, code: 4, message: 5 } ] } }4.4 matcher 不生效时的排查顺序把排查步骤固化下来以后遇到同类问题不用重新想。第一步确认输出格式和正则真的对上了。在 Problems 面板里什么都没有的时候先看终端里的原始输出。如果原始输出里路径变成了相对路径或者行号那一段格式不一样正则必然失配。第二步确认输出流走的是 stdout 还是 stderr。编译器把诊断信息打到 stderr 是很常见的做法。有些 VSCode 版本会把两种流都送进匹配器有些只处理 stdout。如果你确认格式没问题但就是不匹配就在 shell 模式下加个重定向21把 stderr 合并到 stdout。用type: process时没法做重定向这就是少数需要切回shell的场景。第三步检查是否被-v的冗余输出污染。详细程度调高的时候spcomp 会打印编译进度其中某些行可能看起来像错误格式比如带括号和数字造成误匹配。把-v压到 0问题就消失了。第四步删掉旧问题。VSCode 会缓存上一次任务运行产生的问题列表改完 matcher 之后先手动跑一次干净的任务让列表刷新再判断是不是真的生效了。一个实操小技巧把-E的结果重定向到文件然后用-E编译一遍配合 Problems 面板看预处理后的报错行号。预处理后的行号对应的是展开后的文件跟你的源文件行号对不上这是正常的别被绕进去。它的用途是查符号和宏不是查行号。5. 多文件工程的 include 拆分与批量编译单文件插件写到两三百行就会开始难受全局函数堆在一起改一个功能要滚半天。这时候必然要做拆分而拆分又必然引入新的编译问题。5.1 单入口加 .inc 拆分是 SourcePawn 的主流约定SourcePawn 没有模块系统也没有独立的编译单元概念。它的做法是只有一个文件作为编译入口.sp其余逻辑全部写成.inc由入口文件用#include拉进来。所有被包含的文件在编译期会被拼成一份完整源码所以函数定义不能跨文件重复全局变量也是共享同一命名空间。这个模型很土但很好理解。我通常这样切my_main.sp入口只放#include列表、OnPluginStart、OnPluginEnd这类生命周期回调。my_convars.inc所有ConVar的句柄变量和创建逻辑以及读取配置的函数。my_commands.inc所有控制台命令和聊天命令的注册与处理函数。my_utils.inc字符串处理、队伍名转换、坐标计算这类无状态工具函数。my_msg.inc所有玩家可见文本的格式化函数集中管理文案。入口文件大概是这个样子#pragma semicolon 1 #pragma newdecls required #include sourcemod #include sdktools #include my_convars.inc #include my_utils.inc #include my_msg.inc #include my_commands.inc public void OnPluginStart() { RegisterConVars(); RegisterAllCommands(); }两个 pragma 建议新项目一定加上。semicolon 1强制语句结束写分号能挡掉一类跨行写表达式忘了分号导致的诡异解析错误——这类错误的报错行号往往指在下一行迷惑性极强。newdecls required强制使用明确的声明语法避免隐式声明带来的作用域混乱。注意引号形式系统头文件用尖括号自己的文件用引号。尖括号只会在 include 搜索路径里找引号会优先在当前文件所在目录找其次才是搜索路径。自己的文件用引号可以省掉一部分-i配置。5.2 include 顺序带来的符号冲突拆文件之后#include的书写顺序会变成一个真实存在的坑。因为所有内容最终拼成一份如果 A 文件里定义了一个宏或全局变量B 文件里也在用那么 A 必须排在 B 前面否则 B 编译到一半发现符号不存在。最常见的表现是你在my_utils.inc里定义了一个#define MAX_SLOTS 64然后my_commands.inc里用它做数组大小。如果my_commands.inc被包含在my_utils.inc之前编译直接报数组大小必须是常量。修法只有一条调整顺序把基础定义型文件排前面依赖型文件排后面。我的经验排序是常量与枚举 → 配置与句柄 → 工具函数 → 文本格式化 → 命令与事件处理。基本符合依赖方向很少需要回头改。5.3 批量编译整个目录当你手上攒了十几个插件一个个点开编译太慢。这时候写个脚本批量跑更合适。Linux 下#!/bin/bash SCRIPT_DIR./scripting OUT_DIR./plugins SPCOMP./scripting/spcomp INC./scripting/include for f in $SCRIPT_DIR/*.sp; do name$(basename $f .sp) echo compiling $name $SPCOMP -i$INC -i$SCRIPT_DIR/include -o$OUT_DIR/$name.smx -v0 $f if [ $? -ne 0 ]; then echo !!! $name failed fi doneWindows 批处理echo off setlocal set SPCOMPaddons\sourcemod\scripting\spcomp.exe set INCaddons\sourcemod\scripting\include set SRCscripting set OUTaddons\sourcemod\plugins for %%f in (%SRC%\*.sp) do ( echo compiling %%~nf %SPCOMP% -i%INC% -i%SRC%\include -o%OUT%\%%~nf.smx -v0 %%f ) endlocal在 VSCode 里把这两个脚本各自包成一个 shell 类型的 task需要全量编译的时候跑一次就行。注意批处理里%%f是双百分号这是.bat文件的语法要求直接粘到命令行里反而要写成单百分号别搞混了。一个血泪教训批量脚本里一定要加编译失败的判断和显式输出。脚本跑完只看到一屏幕滚动文字最后没有任何failed提示你会以为全成功了实际上中间某个插件因为少了个分号被跳过产物还是上一次的旧.smx服务端加载的是老逻辑然后在游戏里怎么测都不对。这个坑我踩过不止一次。6. 那些真会咬人的细节路径、编码、版本、预处理前面几节讲的是主干这一节讲的是主干之外那些配好了能用、配错了很难查的地方。这部分内容官方文档基本不会写全是踩出来的。6.1 项目路径别用中文和空格这一条我放在第一位。spcomp 是 C 写的命令行程序在 Windows 上处理路径时受系统区域设置影响遇到非 ASCII 字符中文、日文、带音标的用户名时可能表现出文件明明存在却读不到或者输出的 smx 文件名变成乱码。更隐蔽的是 VSCode 的${file}变量会原样带出路径如果路径里有空格虽然type: process模式下 VSCode 会自动处理引号但如果中途换成 shell 模式或者路径里同时有空格和、(、)这类 shell 元字符很多人的服务器目录叫tf2 (test)这种引号地狱立刻开始。表现是命令拼接后参数错位spcomp 收到一个不存在的输入文件名。结论很简单工作区路径用纯英文、不含空格、不含括号。比如D:/dev/sm-plugins/。这一条成本几乎为零收益是永久消除一整类玄学问题。6.2 文件编码和 BOM 的统一问题SourcePawn 的源码文件应该保存为 UTF-8。问题在于带不带 BOM。两种都能编过但一个工程里最好统一不要一部分带一部分不带。带 BOM 的好处是 Windows 下记事本、部分编辑器打开时能正确识别中文注释不带 BOM 的好处是跟各种命令行工具、文本处理脚本配合时更干净比如用 grep 搜内容时BOM 会让第一个匹配结果带上一串看不见的字符。我的做法是统一用 UTF-8 无 BOM在 VSCode 的settings.json里加一条files.encoding: utf8强制默认编码避免某个文件被自动识别成别的编码保存导致中文注释在某次保存后变成乱码——而乱码一旦落到字符串常量里游戏里就会显示成方框你还得回头找是哪次改动引入的。如果你的插件里有面向玩家显示的文本强烈建议不要在源码里硬编码中文而是走翻译文件translations/目录。一是避免编码问题二是方便其它人做本地化三是插件重载后改文案不用重新编译。6.3 编译器版本必须和服务端版本对齐这个是隐蔽性最高的一个坑因为它在编译期完全没有任何提示。SourceMod 不同版本之间原生 API 的表结构可能变化。用 1.12 的 spcomp 编出来的.smx丢到跑 1.10 的服务器上可能启动时就报插件使用了未知的原生函数也可能能加载但在调用某个函数时直接让插件崩溃。反过来用 1.10 的 spcomp 编那些调用 1.12 新 API 的代码编译期就会报undefined symbol——这一种反而是好事至少它报出来了。我的习惯是每个服务器的编译环境就用那台服务器自己addons/sourcemod/scripting/目录下的 spcomp不做跨服复用。如果同时维护测试服和正式服就配两套${config:sourcemod.spcompPath}切工作区的时候顺便切编译器。多花的这点配置成本换来的是编译通过基本等于能跑这种确定性。6.4 用-E预处理定位 undefined symbolundefined symbol是拆分文件之后最常见的报错成因通常只有三种文件没被 include、include 顺序不对、函数名拼错。三种原因都指向同一个动作看预处理结果。./spcomp -E -iinclude -iscripting/include my_main.sp preprocessed.txt打开preprocessed.txt用编辑器全局搜索那个报错的符号名。搜不到说明对应的.inc压根没被包含进来回头检查#include列表搜到了但出现在使用点之后说明顺序错了把那个文件往前挪搜到了并且在使用点之前那基本就是名字拼错了比如大小写不一致或者少了一个下划线。这个流程比一行行人肉追 include 快得多尤其是当工程里有嵌套 includeA 包含 BB 包含 C的时候人眼根本追不动。6.5 换行符的坑Windows 默认 CRLFLinux 默认 LF。spcomp 对两种都能正常处理所以单纯编译不会出问题。真正的麻烦出现在你把源码传到 Linux 服务器上用脚本处理的时候比如用sed做批量替换CRLF 会让匹配失败因为每行末尾多了一个看不见的\r。团队协作或者跨平台部署的场景下建议在工程根目录放一个.gitattributes把.sp和.inc统一声明成text eollf让版本管理工具自动归一化。这样无论谁在什么系统上编辑落到仓库里都是统一的换行符省掉一大堆在我机器上是好的。7. 编译通过只是开始热重载与调试信息真正的开发循环是改代码 → 编译 → 让服务端用上新版本 → 观察行为。编译只是第三分之一后面两步配不好前面配得再漂亮也快不起来。7.1 覆盖 .smx 之后怎么让服务端真的用上好消息是 SourceMod 在加载插件时会把.smx读进内存不会长期占用文件句柄。所以直接覆盖plugins/下的产物一般不会遇到文件被占用的报错。坏消息是覆盖了文件不代表服务端用上了新代码。运行中的插件还是内存里那份旧字节码必须显式重载sm plugins reload my_plugin插件名不带.smx后缀。执行之后 SourceMod 会先卸载旧实例再加载新文件过程中OnPluginEnd会被调用一次。这就引出一个必须注意的点如果你在OnPluginEnd里做了什么清理比如关闭数据库连接、解除定时器、恢复被修改的实体属性那重载时这些清理会真的执行插件状态是干净的。但如果你依赖的是插件卸载后状态自然保留比如故意不清理的全局数据重载会把它重置掉行为和你预期的不一致。我踩过的一个具体例子插件里用CreateTimer起了个循环定时器句柄存在全局变量里OnPluginEnd里没有 Kill。重载之后新旧两个定时器同时在跑日志里的输出频率翻倍排查了半天才发现是重载导致的重复注册。后来养成的习惯是所有Handle类型的全局变量在OnPluginEnd里老老实实全部释放重载行为就永远是可预期的。还有一个更省事的路子如果只改了一个插件sm plugins reload就够如果同时改了多个用sm plugins refresh之类的批量刷新更高效。具体命令以你服务端sm help的输出为准。7.2 开发版和发布版用不同的编译参数崩溃时的堆栈信息能不能定位到具体行号取决于编译时有没有生成调试信息。所以我的建议是把编译参数分成两套。开发期-d2或该版本支持的最高调试等级优化等级调低。产物体积大一点、运行慢一点但一旦插件崩了捕获到的堆栈能告诉你具体是哪个函数哪一行排查效率天差地别。发布期优化拉满调试信息关掉或降到最低输出详细程度压到 0。产物更小运行更快。在 tasks.json 里就是两个任务的差别。我一般只把开发版任务绑到快捷键上发布版任务放在列表里需要出包的时候手动跑一次然后把产物单独归档。这样不会出现忘了切参数把开发版丢正式服这种事故。7.3 保存即编译的工作流把编译频率提上去之后最自然的想法是保存文件就自动编译。VSCode 本身没有这个能力内置的自动任务只在打开文件夹时触发一次需要靠扩展补。市场里搜Trigger Task on Save这类关键词能找到几个做这件事的扩展配置方式是声明哪类文件保存时触发哪个任务{ triggerTaskOnSave.tasks: { spcomp: 编译主入口: [**/*.sp, **/*.inc] } }这里有个必须注意的细节也是我第一次配的时候翻车的地方触发规则里匹配的是被保存的文件但任务里用的变量是任务自己定义的。也就是说如果你把自动触发的目标指向编译当前 .sp这个任务内部用${file}那么当你保存一个.inc文件时${file}就指向那个.incspcomp 会去编译一个不能独立编译的文件输出一堆莫名其妙的错误。正确做法是自动触发必须指向固定入口的任务也就是上一节里args里写死my_main.sp路径的那个。这样无论你保存的是.sp还是.inc编译的都是同一个人口行为完全确定。还有一点体验上的取舍保存即编译意味着你在敲代码的过程中会频繁触发编译。spcomp 本身很快几百毫秒到一两秒不等取决于 include 的规模。但如果你的工程 include 了几十个第三方头文件每次保存都跑一遍会明显卡顿。这时候可以退一步改成保存后手动按CtrlShiftB配合前面配好的 problemMatcher实际体感差别不大。我个人目前的习惯是写新功能阶段用保存即编译因为改动多、需要频繁验证语法调试阶段手动触发因为这时候改一行就要进游戏验证一次自动编译反而多余。这套配置我从最早的手改compile.bat到后来用 Notepad 的宏再到现在这一版 VSCode 任务中间折腾过好几轮。现在回头看真正决定效率的不是配置有多花哨而是三件事做没做对命令行先在终端里跑通、problemMatcher 的正则跟实际输出严格对齐、自动触发永远指向固定入口。这三件之外的东西都是锦上添花。
返回列表