npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

koishi-plugin-simc

v0.9.1

Published

本地 SimulationCraft 模拟插件(简化版 Raidbots):QuickSim / GearCompare / TopGear / Advanced,调用本机 SimC 二进制,不依赖任何网页 API。

Readme

koishi-plugin-simc(本地 SimC 模拟插件 / 简化版 Raidbots)

调用本机自行编译的 SimulationCraft(SimC)二进制完成魔兽世界角色模拟。不调用 Raidbots 网页 API、不联网。

⚠️ 仅为 SimC 理论模拟,不等于实战结果。治疗专精模拟结果不可靠,建议改用 QE-Live。


命令

| 命令 | 说明 | | --- | --- | | simc.quicksim [参数] | 先下模拟参数(可留空),再把字符串给它(网页粘贴 / 发 txt)。别名 simc.quick | | simc.compare [参数] | 同上,配置多组用单独一行 --- 分隔 | | simc.topgear [参数] | 同上,内容为「基础角色 + --- + 备选装备」 | | simc.advanced <script> | 高级执行,直接运行原始 SimC 脚本并返回原始输出 | | simc.status | 查看模拟队列状态(运行中 / 排队中 / 最大并发) | | simc.cores [参数] | 线程数自检:带上自己的角色字符串,串行实测几档线程数的墙钟,给出 simThreadBudget 该填多少的建议(见下文「线程数怎么定」) | | simc.diag | 全量自检:配置 / 二进制 / 【算力】/ 专精图标 / 卡片发送逐项验证 |

通用选项(QuickSim / Compare / TopGear):

  • -f, --fight-style <style>:战斗类型,见下
  • -l, --length <秒>:战斗时长
  • -e, --enemies <数量>:敌人数量(AoE 场景)
  • -i, --iterations <次数>:固定迭代次数
  • -s, --smart:SmartSim 模式(用 target_error 代替固定迭代)
  • -n, --no-buffs:关闭团队 buff
  • -b, --binary <路径>:临时指定本次使用的 SimC 二进制(即可选不同版本)

战斗类型枚举:Patchwerk / CastingPatchwerk / HecticAddCleave / CleaveAdd / LightMovement / HeavyMovement / DungeonSlice (以 src/types.ts 的 FIGHT_STYLES 为准,它必须与 SimC 源码 engine/util/util.cpp 的 fight_style_string() 对齐——ExecutePatchwerk 在 1210 已被 SimC 删除)

两步走:先下参数,再给字符串(推荐)

SimC 角色字符串长到只能走网页或 txt 文件,没法和参数挤在同一条消息里。所以顺序是「先参数、后字符串」:

① simc.quicksim 无团队buff 无合剂 5目标 木桩战     ← 不写参数 = 用插件默认配置
② 机器人:🎛️ 已记住本次模拟参数(…)/请打开网页粘贴字符串,或把字符串存成 txt 发到本频道
③ 打开它给的网页粘贴字符串(表单会按 ① 的参数预填),或把导出的 txt 发进聊天
④ 收到字符串 → 按 ① 的参数开跑,结果卡片发回本频道
  • 参数会被记 15 分钟,一次有效(用完即清),所以「发一次命令、之后每个 txt 都被套用旧参数」不会发生。
  • 参数按 平台 + 频道 + 用户 隔离,别人发的 txt 不会吃掉你的参数。
  • 没先发命令就发 txt:会提示你补发命令并开跑(不会静默按默认参数跑掉)。想退回「丢文件直接跑」的老用法,把 autoFileRequirePending 设成 false。
  • 网页那条路(5151)也和聊天共用同一套参数:命令里下的参数会在网页表单上预填,页面表达不了的三项(单项团队增益 / 消耗品整类开关 / 点名某一款消耗品)由服务端合并。

也可以在命令里直接贴字符串一步到位(老用法,仍然支持):字符串 + 换行 + 中文选项,或只贴字符串。

  • 写法:带汉字的行一定是中文参数;纯 ASCII 但没有 = 的行(smart / no buff)也算中文参数;剩下的才当角色字符串。# 注释行和 --- 分隔始终算角色字符串。
  • 支持:
    • 战斗类型 木桩战 / 大秘境 / 频繁移动 / 顺劈 / 混乱 / 施法木桩(长词优先,不会被截成半截)

    • 目标数 5目标 / 五个目标 / 目标5(1–50)、战斗时长 3分钟 / 180秒、迭代次数 20000次 / 1万次

    • 团队增益总开关:无团队buff / 要团队buff;否定写法一并支持 别开 / 不要开 / 不开 / 屏蔽 / no

    • 职业自带增益:直接写职业名。法师团队buff = 只开奥术智慧、战士团队buff = 只开战斗怒吼 + 致死之伤。单写 法师 则只叠一层奥术智慧、不动总开关。(圣骑士 / 死亡骑士 / 术士 在 SimC 那 12 项里确实没有专属项,会明确告诉你,不会瞎猜)

    • 单关某一职业:不要法师团队buff = 只关奥术智慧,其余团队增益照样开(不动总开关——这句的言下之意就是「其他的还要」)

    • 其余全开 / 全关:其他buff都要 / 其他buff都不要(常和上一行连用:不要法师团队buff其他buff都要)

    • 单项增益:嗜血 / 英勇 / 有嗜血 / 奥术智慧 / 战斗怒吼 / 野性印记 / 天怒图腾 / 青铜龙的祝福 …;爆发 映射到嗜血类团队增益(SimC 没有「禁用爆发技能」这个开关,回显里会说明)

    • 消耗品整类:无合剂 / 无食物 / 无药水 / 无符文(只能关,因为默认就是按最优配置带满——写肯定式会告诉你这句)

    • 点名喝哪一款合剂:喝暴击合剂 / 喝急速合剂 / 喝精通合剂 / 喝全能合剂。写「属性 + 合剂」就行,不用记物品名。当前资料片(12.1)的对应关系与依据:

      | 你写的 | 实际下发的合剂 | 属性 | | --- | --- | --- | | 喝暴击合剂 | 破碎残阳合剂 flask_of_the_shattered_sun_2 | 暴击 | | 喝急速合剂 | 血骑士合剂 flask_of_the_blood_knights_2 | 急速 | | 喝精通合剂 | 魔导师合剂 flask_of_the_magisters_2 | 精通 | | 喝全能合剂 | 萨拉斯抗性合剂 flask_of_thalassian_resistance_2 | 全能 |

    • 点名消耗品(直接写名字,中文官方名或英文名都行):喝圣光潜力 / 吃鲁莽药水 / 喝液态光泽 / 虚触强化符文 / 银月城浮华大餐

    • 智能 / 高精度(SmartSim)

  • 未识别的片段一定会回显出来(不会默默忽略),所以写完看一眼回显就知道有没有生效。收文件那条路会把生效参数写在进度卡片的副标题上。
  • 一句话里可以连写多个概念(爆发合剂 会拆成「爆发」+「合剂」分别处理),空格有没有都行。
  • 换装(把 A 换成 B):直接写 原装备名换成新装备名,例 唤波者的海石换成盖博的无底袋。
    • 名字中文官方名 / 英文名都行,走离线物品库(零请求);换成 / 换为 / 替换成 / 换掉 都认,带不带空格、带不带「把」都行。

    • 装等从哪来,按这个优先级:① 显式写数字(无底袋(321) / 无底袋 321 / 无底袋装等321)→ 用你写的;② 写「难度 + 升级次数」(无底袋英雄6/6)→ 查下面的升级轨道表换算;③ 都不写 → 继承被换掉那件的装等。数字和难度同时写时以数字为准。

    • 模式语义不同:quicksim / compare = 直接把那一行换掉(compare 的每一组都换);topgear = 新装备作为同槽位的候选,跟原装备做对比(这才是 TopGear 的用法)。

    • 换装要等字符串到了才应用 —— 要先按物品 id 找到字符串里那一行,所以它天然配合上面「先下参数、后给字符串」那套流程。

    • 升级轨道表(至暗之夜 S2 / 12.1):难度 × 升级次数 → 装等。老兵=随机、勇士=普通、神话=史诗(即团本那四档「随机 / 普通 / 英雄 / 史诗」);神话的 7~9 档是「晋升」,分母照游戏里写 /6。

      | 难度 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 老兵 / 随机 | 279 | 282 | 285 | 289 | 292 | 295 | | | | | 勇士 / 普通 | 292 | 295 | 298 | 302 | 305 | 308 | | | | | 英雄 | 305 | 308 | 311 | 315 | 318 | 321 | | | | | 神话 / 史诗 | 318 | 321 | 324 | 328 | 331 | 334 | 337 | 340 | 344 |

      例:海石换成无底袋英雄6/6 → 写成 ilevel=321;…神话9/6 → ilevel=344。

    • 这个说法写不全时会退回「继承」并说清原因,不瞎猜:只写 英雄 没写 6/6 → 定位不了(各难度装等会重叠,英雄 6/6 与神话 2/6 都是 321);写 英雄9/6 → 英雄只有 6 档。

    • 表在 src/itemLevel.ts 的 UPGRADE_TRACKS(原来在 gearSwap.ts,为了让纯文本解析器 nlpArgs.ts 也能共用而拆出来 —— 它不许 import 到物品索引)。⚠️ 相邻档不是等差(285→289 是唯一的 +4),所以整表写死,别改成公式算。_gearswap_test.js 的 [G] 段逐格对着整理图断言,改表必红。

    • 表里没有的轨道(冒险者 / 探险者)不装懂:照旧剥掉并说明。

    • 换不成的情况一律回显:名字搜不到、同名的有多件、部位装不上、你给的字符串里没有那一件。

  • 饰品 / 戒指对比(一次试一对):饰品对比 A、B、C、D(也认 饰物/指环,以及反语序 对比饰品 …)。
    • 为什么单列:饰品和戒指各是两个槽(trinket1+trinket2、finger1+finger2),两个槽可以各放一件不同的装备。所以这两个槽的候选合并成一个池、按「两件一起换」组合,不能当两个独立槽各搜各的 —— 那样会把其中一个槽永远钉在身上那件(这是老版本的 bug)。
    • 说了「X 不变」→ 钉住 X 所在的那个槽,候选逐件换到另一个槽(N 组)。例:饰品对比 破损的阿曼尼战旗、盖博的无底袋、米利克萨之牙、乌拉特克贪婪之心都神话6/6 虫群之瘤不变 → 4 组,每组都是「候选 + 虫群之瘤」。
    • 没说「不变」→ 两两组合都试(C(N,2) 组),把身上那两件也当候选一起给上时也一样参与组合。
    • 候选写法和「换装」同源:中文 / 英文名、名字+id、纯数字 id 都行;装等同样按「显式数字 > 难度+升级次数 > 继承」来(继承取两个槽里较高的那个装等,避免看着偏低)。
    • 列表末尾那一条升级描述 = 整组默认值(… 、乌拉特克贪婪之心都神话6/6 或 … A、B 都神话6/6)——「加不加『都』都是这几件全都要」是用户的明确要求,所以尾巴那条一律按整组算;单件自己写了的以单件为准(A英雄3/6、B、C神话6/6 → A 用 3/6,B/C 用 6/6)。回显里会写「整组升级描述:神话6/6」,不会让你猜。
    • 「都」是语法标记,会被摘掉(不摘就会去搜「乌拉特克贪婪之心都」这种不存在的名字)。认不出的描述(英雄9/6 越界、只写 英雄)也照样把「都」摘掉,但不当整组默认值,并明说一句原因 —— 绝不静默。
    • 组合数上限由 topGearMaxCombinations 管(默认 200);超了直接报错而不是偷偷截断 —— 对比表少一件会让人看错结论。
    • 一件都对不上就不展开、不跑,逐条把原因列出来(❌ 开头)。
    • 旧资料片 / 时光漫游物品按中文名直接写就行:本地索引只收 id≥225000 的当前资料片装备,但命令入口会拿「本地搜不到」的名字去 wago 的 ItemSparse 按中文名反查(filter[Display_lang],单值可用、整条响应约 2KB),查到后落进别名表 <assetsDir>/item-alias.json —— 同一个名字一辈子只联网查一次,之后纯离线。写 米利克萨之牙158367 或直接 158367 也照样认(多条路都通)。
    • ⚠️ 你说了「X 不变」但那件不在身上这两个槽里时,不会退回「两两组合」(那样会把你不想换的那件也换掉),而是报错让你核对名字。

网页表单(推荐:粘贴超长 SimC 字符串)

聊天里发不下完整 SimC 字符串——完整导出含背包装备可达 6000+ 字符,远超 QQ 单条文本上限(约 2000)、KOOK 文本上限(约 5000~8000)。本插件内置一个网页表单,浏览器打开即可粘贴,绕开聊天字数限制,且 QQ / KOOK 通用。

前提:启用 @cordisjs/plugin-server(你的 koishi.yml 已启用,监听端口如 5140)。插件内部通过 inject: ['server'] 声明对该服务的依赖,server 就绪后才会挂载 /simc 路由;若未启用 server,网页功能自动禁用、聊天命令不受影响。

  • 打开 http://<host>:<port>/simc(路由前缀由 webBasePath 配置,默认 /simc)。
  • 选择模式(快速模拟 / 装备对比 / 最优配装 / 高级执行),粘贴字符串,点“开始模拟”。
  • 对比用单独一行 --- 分隔多组;最优配装用 --- 分隔基础角色与备选装备(同聊天命令格式)。
  • 结果直接在页面显示,与聊天命令共用同一套队列 / 子进程 / 解析逻辑(零重复代码)。

换装备 / 加装备(试你身上没有的装备)

右栏「换装备 / 加装备」面板用来把手上没有的装备换上去试(例:把身上的饰品换成另一件):

  1. 选部位(角色字符串里已有的会标注「当前已有」,没有的部位也能选 = 加一件);
  2. 搜名字——中文、英文都行,优先全部离线:英文名来自 SimC 自带的物品库,中文名来自客户端字符串表导出的索引(当前 6129 件全部覆盖,例:搜「无底袋」→ 盖博的无底袋 / 搜「海石」→ 唤波者的海石)。本地没有再问暴雪官方接口,官方接口按中文名实测 0 命中,所以最后还会补一次 wago 的中文名反查(命中后记进别名表,下次纯离线);也可以直接填数字 id;
  3. 点结果 → 填装等 → 点「写入角色字符串」。

写入 = 直接改写左边角色字符串里那一行(没有该部位就插到装备段末尾),所以快速模拟 / 装备对比 / 最优配装三条链路全都吃得下,改完还能「撤销」。

⚠️ 装等一定要填对:SimC 物品库里的 level 是基础装等,不等于实际装等(例:无底袋在 DBC 里是 36,实际 277)。换已有部位时默认沿用当前装等;新增部位必须手填。 需要精确控制 bonus_id / 附魔 / 宝石时,用面板里的「手动贴装备行」,整行原样覆盖。

最优配装(TopGear)勾选网格:饰品 / 戒指是「一对」

粘贴「基础角色 + --- + 备选装备」后,右栏会把每个部位列成一个分组供勾选。饰品和戒指会渲染成一个分组(trinket1+trinket2、finger1+finger2 合并),按两两组合跑,而不是两个槽各搜各的:

  • 分组头写着「两个槽 · 两两组合」,张数就是 C(勾选件数, 2);
  • 组里那两行「当前装备」默认不勾、但可以勾(勾上就变成「候选 + 身上那件」一起比)—— 普通部位那行「当前」是锁死的基准,饰品组不锁,正是为了不再出现「钉死一个槽」;
  • 预计模拟次数按「基准 1 + 普通部位(候选 + 基准回跑)+ Σ 组合数 + 最佳组合确认 1」算,勾选数量实时显示;
  • 只勾 1 件会明确提示「凑不成一对(要两件不同的)」,且该件不参与任何组合。

对应聊天侧就是 饰品对比 A、B、C…(语义完全一致:说了「X 不变」→ N 组,没说 → C(N,2) 组)。

配置见上表 web* 项:webEnabled(开关,默认开)、webBasePath(路由前缀,默认 /simc)、webToken(访问令牌,留空不鉴权;配置后表单需填对应令牌,错误返回 403)。

⚠️ 安全提示:simc 是 CPU 密集型,网页端点默认对局域网开放。公网暴露请用反向代理 + 鉴权,或设 webEnabled=false。


配置项(必须填 simcBinary)

| 配置 | 默认 | 说明 | | --- | --- | --- | | simcBinary | (必填) | SimC 可执行文件绝对路径 | | maxConcurrent | 2 | 最大同时运行的模拟任务数(防 CPU 打满)。每个任务拿几个 SimC 线程由插件按真实并发自动分配(见下),所以调大它不会让单任务变慢 | | simThreads | 0 | 每个模拟固定使用的线程数。0(推荐)= 交给插件按真实并发自动分配;填正数则自动分配关闭。⚠️ 它是「每任务固定 N」,并发时总共占 N × maxConcurrent 个线程(填 32 且并发 2 = 64 线程抢 32 核)。想控制总量请用 simThreadBudget | | simThreadBudget | 0 | 本机总共允许 SimC 使用的线程数。0 = 自动取逻辑核数。填了之后按真实并发均分:单任务吃满这个数,两个任务同时跑时各一半 —— 并发时不会超订。典型用途:① 开了超线程的服务器(如 16 物理核 / 32 逻辑核的 Xeon Gold 6226R)想让 SimC 只用物理核(填 16,把 SMT 让给别的服务);② 给别的服务留余量(32 线程的机器只给 24)。该填多少跑 simc.cores 实测(见下) | | defaultIterations | 10000 | 默认迭代次数 | | defaultFightStyle | Patchwerk | 默认战斗类型 | | defaultFightLength | 300 | 默认战斗时长(秒) | | defaultEnemyCount | 1 | 默认敌人数量 | | defaultTeamBuffs | true | 默认开启团队 buff | | smartSimTargetError | 0.1 | SmartSim 目标误差 | | taskTimeout | 300000 | 单模拟超时(毫秒),超时杀进程 | | topGearMaxCombinations | 200 | TopGear 最大组合数,超过报错 | | tempDir | 系统临时目录 | 临时文件目录 | | webEnabled | true | 是否启用网页表单(需启用 server 插件)。用于粘贴超长 SimC 字符串,绕开聊天字数限制 | | webBasePath | /simc | 网页表单路由前缀,例如 /simc,访问地址即 http(s)://host:port/simc | | webToken | (空) | 网页 API 访问令牌;留空则不鉴权(仅建议本机/内网使用)。配置后表单需填写对应令牌 | | remoteEnabled | true | 是否启用专属提交网站(5151)。命令回的「打开模拟页面」链接走这条;关掉后只能靠发 txt | | remotePort | 5151 | 专属提交网站监听端口 | | remoteBaseUrl | (空) | 生成提交链接用的对外地址(同局域网访问时填主机名/IP) | | kookCard | true | KOOK 原生卡片开关;卡片显示异常时的逃生开关(关掉一律走纯文本) | | autoFileSim | true | 收到配置文件(.txt/.simc)时自动跑一次模拟 | | autoFileExt | txt,simc,simcprofile,prof | 触发自动模拟的文件后缀(逗号分隔) | | autoFileMaxSize | 256 | 自动模拟的文件体积上限(KB) | | autoFileCooldown | 20 | 同一用户两次自动模拟的最小间隔(秒);0 = 不限制 | | autoFileRequirePending | true | 要求先发一条命令(simc.quicksim 等)才能触发收文件模拟 —— 也就是「先下参数、后给字符串」。设 false 退回老行为:丢文件就按默认参数直接跑 |


架构与防护(重点)

  1. 不在主线程同步执行:所有 SimC 调用进入内存任务队列(queue.ts),异步执行,绝不阻塞 Koishi。
  2. 并发限制 + 线程按真实并发现算:maxConcurrent(默认 2)限制同时运行的 SimC 进程数,防止 CPU 耗尽。每个任务拿几个 SimC 线程由队列的 threadBudget() 按此刻真实并发决定(分母是 running,不含排队中的任务 —— 否则 TopGear 一次排 50 个会把每个压到 1 线程):
    • 空闲时单任务 = 线程总量(simThreadBudget,未填时 = 逻辑核数);2 个任务同时在跑 = 先到的拿满、后到的拿一半。总墙钟与「平均分」基本相当,但先到的那个能更早出结果;代价是重叠期间约 1.5× 超订,实测约 7% 的开销(2 任务各吃满平均 23.1s vs 各分一半 21.5s)。
    • simThreads > 0 时强制固定,动态分配关闭;simThreadBudget > 0 时分子换成运维声明的总量(总量,不是每任务量 —— 并发时自动均分,不超订)。
    • 为什么这么改:SimC 线程数近乎线性加速(6 核实测 1 线程 46.8s → 3 线程 16.5s → 6 线程 9.1s)。旧算法是 cores / maxConcurrent(按最坏情况预留),maxConcurrent=2 时单任务只用一半的核 —— 白白慢一倍。真机端到端(_thread_e2e_test.js):20.2s → 9.0s(2.25×),DPS 差 0.12%(只是快了,结果没变)。
    • 也试过「一个进程 + profileset 跑多候选」,实测更慢(3 候选:profileset 41.9s vs 3 个独立进程串行 26.8s),所以维持「多进程 + 队列」。
    • ⚠️ 为什么线程总量必须能手填:Node 在 Windows 上读不到物理核数 —— os.cpus() 每一项只有 model / speed / times,没有 physicalId / coreId(那是 Linux 的字段)。所以 16 物理核 / 32 逻辑核的 Xeon Gold 6226R 只会被数成 32,插件自己分辨不出来。想让 SimC 只用物理核(把 SMT 让给别的服务)就得显式填 simThreadBudget: 16。该填多少别猜 —— 跑 simc.cores(见下文「线程数怎么定」)。
    • 另注:单任务给到 2 倍逻辑核都不变慢(6 核实测 6 线程 10.5s、12 线程 10.1s),所以线程总量偏大不会让单个模拟变慢,只会让并发时互相抢核。
  3. 超时即杀:每个任务有独立超时;超时通过 AbortController 触发 child.kill('SIGKILL'),杜绝僵尸进程。
  4. TopGear 组合爆炸防护:算法是逐分组线性的 —— 普通部位每组「候选数 + 基准 1 次」,饰品/戒指这种成对槽位(两个槽一起换)每组 C(N,2) 次,总量是 Σ 而不是 ∏。组数(含基准组)超过 topGearMaxCombinations 直接报错,提示减少候选或改用「XX不变」把其中一个槽钉住(那就只要 N 组)。
  5. 套装约束:--set-pieces 2|4 强制达到指定数量套装件;候选行行尾加 #tier 标记为套装件(写入实际 profile 前会被剥离,不影响 SimC 解析)。
  6. 治疗专精提示:role === "heal" 或已知治疗专精会被识别并提示结果不可靠、建议 QE-Live。
  7. JSON 输出:通过 json2=<path> 让 SimC 输出结构化 JSON(Report 2.0 格式),只解析 JSON,不解析纯文本日志。
  8. 错误处理:二进制缺失 / 参数非法 / 超时 / 非 0 退出 / JSON 解析失败,均有友好中文提示,不暴露内部路径与堆栈。

若需要跨进程 / 多实例统一排队,可把 queue.ts 的 SimQueue 替换为 BullMQ,对外接口保持一致即可(本插件默认零依赖内存队列,部署最简单)。


部署注意事项

1. 自行编译 SimC 二进制(必须)

本插件不内置 SimC。你需要自己编译:

# 以 Midnight 源码为例(路径见 external/simc/packages/simc-midnight)
git clone <simc 仓库> simc-src
cd simc-src
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j"$(nproc)"            # 产物:build/simc  (Windows 下为 simc.exe)

把生成的 simc(或 simc.exe)绝对路径填到配置 simcBinary。

首次编译要多久:本机实测 约 10 分钟(175 个编译单元,MSBuild 串行,未开多核)。 之后不要重头来过 —— 用 2.1 的一键更新脚本,只同步真正变化的文件,避免全量重编。

Windows 编译(本机 win32 / MSVC):先装 Visual Studio 2022 生成工具(勾选“使用 C++ 的桌面开发”工作负载,含 MSVC + Windows SDK)+ CMake(两者均可用 winget install 安装)。PowerShell 5.1 不支持 &&,用 ; 或每条单独回车:

mkdir build; cd build
cmake .. -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=Release -DBUILD_GUI=OFF -DBUILD_TESTING=OFF
cmake --build . --config Release

产物在 build\Release\simc.exe。务必加 -DBUILD_GUI=OFF,否则会因缺少 Qt 而 configure 失败;本插件只用 CLI 二进制,不需要 GUI。把该 exe 绝对路径(如 C:\...\simc-midnight\build\Release\simc.exe)填到 simcBinary。

2. 更新 SimC(游戏 hotfix / 版本更新后必做)

SimC 依赖游戏 DBC / 物品数据库。每次魔兽大版本(如 11.x → 12.x Midnight)或在线修正(hotfix)后, 必须更新 SimC 源码并重新编译,否则模拟的装备/天赋/数值会过时甚至报错。 这也是为什么不调用网页 API——本地二进制必须跟版本走。

2.1 一键更新脚本

插件根目录带一个 _simc_update.js:

node _simc_update.js --check        # 只看有没有新版(约 2 秒,不下载不编译)
node _simc_update.js --diff         # 看改了哪些文件、预计重编几个 cpp(约 2 秒,不下载)
node _simc_update.js --dry-run      # 真的解压包比对源码树,看会改动哪些文件(不落盘)
node _simc_update.js                # 同步源码 + 增量编译(推荐)
node _simc_update.js --sync-only    # 只同步源码,编译留到后面(会复用已缓存的包)
node _simc_update.js --keep-extra   # 不删「本地有、上游没有」的文件
node _simc_update.js --zip <路径>   # 复用已下好的源码包
node _simc_update.js --rollback     # 新版有问题时,回滚到更新前的 exe 备份

它做的事:拉取 GitHub midnight 分支最新源码 → 只覆盖内容真正有变化的文件 (未变文件的 mtime 保持不动,从而命中 CMake 增量编译)→ 编译 → 记录版本号。 simc.exe 会在编译前先备份成 simc.exe.bak,所以链接失败也还能 --rollback。

本地多出来的文件会被删掉:SimC 用 source_files/*.props 维护显式源文件清单(不是 GLOB), 所以上游删文件时必须跟着删。但脚本会把删除清单全部打印出来, 不想动就加 --keep-extra(2026-09-15 那次要删 2 个:上游真删的 MID2_Paladin_Protection_Lightsmith.simc,以及本地自己复制出来的 profiles/__simc_test_ci.simc)。

为什么这么设计(下面每个数字都是实测的,改之前请先复测):

| 环节 | 实测 | 结论 | | --- | --- | --- | | 官方 nightly 站 downloads.simulationcraft.org | ~17 KB/s(win64 包 115 MB → 近 2 小时) | ❌ 国内不可行 | | git clone --depth 1 | 9 分钟只下 22 MB | ❌ | | GitHub codeload 源码包(49.4 MB) | ~4 MB/s(十几秒) | ✅ 走这条 | | 7z 解压 49.4 MB 包 | 3.1 秒 | ✅ | | 智能同步 1160 个文件(逐字节比对 + 只覆盖变化项) | 2.9 秒(只有 100 个真被改写) | ✅ 同步不是瓶颈 | | 编译耗时 | 不是常数,看改了哪些头文件 —— 见 2.2 | ⚠️ | | 全量编译 175 个 TU | 10.5 分钟(MSBuild 串行,未开 /MP) | 最坏情况也就这个量级 |

为什么不用 git clone:本机实测 git clone --depth 1 跑 9 分钟才下 22 MB (pack 解包 + SHA-1 校验 + 落盘 5000+ 文件),而 zip 走 HTTPS 只要十几秒。 更关键的是:zip 方案能自己控制哪些文件的 mtime 被更新,这正是增量编译生效的前提; git reset --hard 无法保证跳过未变文件。

不要走官方 nightly:本机 exe 是 9-11 自己编译的,而官方 1210 nightly 构建于 9-08 —— 走 nightly 反而可能降级。

更新后不需要重启 koishi:插件每次模拟都是现 spawn 一个 simc.exe 进程 (见 src/simc.ts),所以覆盖 exe 后下一次模拟就直接生效。 脚本在编译前会检查有没有 simc.exe 还在跑(Windows 下运行中的 exe 无法被覆盖)。

2.2 「这次要编译多久」取决于头文件,不取决于改了哪几个文件

这是最容易误判的一点。按后缀数「改了几个 .cpp / 几个 .hpp」会严重低估: C++ 里改一个 .cpp 只重编它自己,但改一个被广泛 include 的 .h,会让所有间接依赖它的 .cpp 全部重编。

真实例子(2026-09-15 那次 69587 → 69814):

  • 总共只动了 102 个文件
  • 其中 engine/dbc/data_enums.hh 只改了 43 行、只有 4 KB
  • 但它的依赖链短得刺眼:data_enums.hh → simulationcraft.hpp(只有 2 跳), 而后者是每个 cpp 的公共头
  • 结果:145 个 cpp 被这一个小文件拖下水,全次共 161 / 175 个 cpp —— 等于全量编译

所以脚本不按后缀猜,而是真的把 #include 依赖图建出来做反向闭包 (558 个节点 / 0.3 秒),并给出「元凶排行」:

→ 预计需要重新编译:161 个 cpp(会被编译的一共 175 个,占 92%)
   这几个改动决定了重编量(越靠前影响越大):
      145 个  engine/dbc/data_enums.hh
       10 个  engine/sim/profileset.hpp
        4 个  engine/class_modules/paladin/sc_paladin.hpp

由此把更新分成两类,用 --diff 两秒就能分辨是哪一类:

| 类型 | 长什么样 | 耗时 | | --- | --- | --- | | 轻量(APL / 配装改动) | 只改 ActionPriorityLists/、profiles/、少量 class_modules/*.cpp | 秒级 ~ 分钟级 | | 重量(游戏数据更新) | 提交信息带 [live] Game data update (Build xxxxx),必动 data_enums.hh 与 dbc/generated/*.inc | 接近全量编译(~10 分钟) |

实践建议:暴雪 hotfix 很频繁,但一周里大多数提交只是某个职业的 APL 微调 (2026-09-15 那批 29 个提交里,职业相关全是 Paladin / Warlock / Hunter / Evoker / Druid / Shaman, 一个 DK 的都没有)。所以先 --diff 看一眼——只有出现 [live] Game data update, 或者改动命中你玩的专精时才值得跑;否则攒着一起更新。

qt/(GUI,25 个 cpp)在 BUILD_GUI=OFF 时不计入分母,脚本会读 build/CMakeCache.txt 自动判断。 顺带一提:simc.vcxproj 里没有 MultiProcessorCompilation 也没有 /MP, 所以 MSBuild 是逐个 cpp 串行编译的 —— 这才是「全量 10.5 分钟」的原因。 真嫌慢可以考虑给 CMake 加 /MP,但未验证:/MP 与 /showIncludes 式的依赖追踪有冲突风险, 一旦弄坏会退化成「改了头文件却不重编」这种静默错误。

2.3 更新后建议顺带自检的三件事

SimC 的数据变了,插件的静态表可能跟着过期:

  1. 战斗类型白名单 src/types.ts 的 FIGHT_STYLES —— 必须与 SimC 源码 engine/util/util.cpp 的 fight_style_string() 对齐。 SimC 删过选项(例如 ExecutePatchwerk 在 1210 已移除),照抄旧列表会让用户选了就报错。

  2. 附魔 / 宝石 / 消耗品目录 src/loadoutData.ts —— 由 _gen_loadout_table.js 自动生成(扫描 SimC 官方 profile + DBC 英文名 + 暴雪 API 中文名 + 本机 simc 逐条真机验证)。

    但不是每次更新都要重跑(它要联网查暴雪 API、还要逐条真跑 simc,比较慢)。 判据是:生成器的输入有没有引入新的 id / token,即

    • profiles/generators/{MID1,MID2,PreRaids}/**/*.simc 里新出现的 enchant_id= / gem_id=, 以及 flask= / food= / potion= / augmentation= / temporary_enchant= 这些消耗品 token

    只有出现了、而目录里查不到的,才需要重跑:

    node _gen_loadout_table.js

    实测参考:69587 → 69814 那次,附魔/宝石/物品数据全是「只改文件头 build 号」的 churn (26 MB 的 item_data.inc 只差一行),profiles/generators/ 里 Paladin / Warlock 虽有英雄天赋更名,但涉及的 9 个 enchant_id + 5 个 gem_id 全都已在目录里、 消耗品 token 净增 0 个 → 不必重跑。

  3. 合剂 → 属性别名表 src/nlpArgs.ts 的 FLASK_STAT_ALIAS —— 「喝暴击合剂」能落到那一款合剂上,靠的就是这张表。跨资料片换合剂时它会静默失效 (新合剂不在表里 → 「暴击合剂」进未识别;旧 token 也早已不存在)。 不用靠人记得检查:_nlp_test.js 的 [K] 段会回头拿 SimC 自带的 engine/dbc/generated/item_effect.inc(物品→法术)与 spelltext_data.inc(法术→ "Increases your Critical Strike by…")重新推一遍,新合剂没进表就会红。 所以「更新后跑一次 _nlp_test.js」就能知道要不要补表。

三处都检查完后,按项目惯例走完整流程:tsc --noEmit → esbuild 打包 lib/index.js → 重启 koishi (改插件代码需要重启,改 simc 二进制不需要)。

3. 不实现 Droptimizer

完整 Droptimizer 需要完整的装备掉落数据库与权重迭代,复杂度极高,本插件暂不做。

4. 线程数怎么定(simThreadBudget)—— 跑 simc.cores,别猜

插件默认把逻辑核数(os.cpus().length)当线程总量。这在两种机器上都不准:

  1. 开了超线程的服务器(如 16 物理核 / 32 逻辑核的 Xeon Gold 6226R)—— 逻辑核数会是 32,但 SimC 是浮点密集负载,超线程的收益很小。想让 SimC 只用物理核、把 SMT 让给别的服务,就得手填 simThreadBudget: 16。
  2. 共享服务器 —— 想给别的服务留余量(32 线程的机器只给 24)。

为什么插件不能自己算:Node 在 Windows 上读不到物理核数 —— os.cpus() 每一项只有 model / speed / times,没有 physicalId / coreId(那是 Linux 的字段)。所以插件无法区分「16 物理核」和「32 逻辑核」。

该填多少用 simc.cores 实测(它也是唯一可靠的办法 —— 墙钟-线程曲线在哪一档转平取决于物理核数、SMT、内存带宽、有没有别的服务在抢核):

simc.cores 5目标 木桩 <粘贴你的角色字符串>
simc.cores -n 8000 <角色字符串>     # 迭代数调大更准(默认 4000)

它会串行跑 总量/2、总量、总量×2 三档并报墙钟,然后给建议值。几个设计要点:

  • 串行跑:并发会让不同档互相干扰,测出来就不是单任务性能了;跑之前还会检查队列是否空闲。
  • 先预热一次(结果丢弃):正式机 simc.exe 有 120MB,不预热的话第一个被测的档位会白背一次冷启动开销,曲线是假的。
  • 报出固定开销:每档墙钟里含多少是「进程启动 + SimC 初始化」,并据此判断这次曲线可不可信 —— 「各档差异」小于「一次空跑的抖动」时会明确说不可信、让你调大 -n,而不是给一个假精确的数字。
  • 建议值不会超过逻辑核数:超订只在单任务独占时没坏处,一旦两个任务同时跑就会互相抢核(实测超订 2× 比均分慢 7%)。所以推荐规则是「在不超过逻辑核数的档位里,取落在最快档 3% 以内、线程数最小的那个」。

5. 资源与超时调优建议

  • 单机 / 独占(推荐):什么都不用改 —— simThreads=0 / simThreadBudget=0 时单任务自动吃满线程总量,多任务自动均分。核多就是快。
  • 共享服务器:把 simThreadBudget 设成你愿意让出的线程数(如 24),或 maxConcurrent 调小(如 1)。
  • ⛔ 别用 simThreads 当「限流开关」:它是「每任务固定 N」,并发时总共占 N × maxConcurrent(填 32 且并发 2 = 64 线程抢 32 核)。要限总量用 simThreadBudget。simc.diag 的【算力】段会在检测到超订时直接给警告和改法。
  • 一条命令看全部数字:simc.diag 的【算力】段会把「逻辑核 / 线程总量(及来源)/ 最大并发 / 每任务实际线程数 / 是否关掉了动态分配 / 是否超订」打出来。
  • TopGear 组合很多时:调大 taskTimeout,并合理减少每个槽位的候选数量。
  • 避免长时间迭代:固定迭代建议 ≤ 25000;追求精度用 --smart 而非盲目拉高迭代。

TopGear 输入示例

<基础角色 SimC 字符串,从游戏内导出>
---
head=,id=190001,bonus_id=...
head=,id=190002,bonus_id=...
chest=,id=190003,bonus_id=... #tier
chest=,id=190004,bonus_id=... #tier

--set-pieces 4 时,只有包含 ≥4 件 #tier 的组合会被模拟。

⚠️ --- 之后的候选段不认 # 开头(# 是角色字符串里背包段/注释的写法,候选段是普通行)。网页勾选后拼回来的正是去掉 # 的装备行,两边形态一致。

饰品 / 戒指:直接写中文一句就行(比手写两段装备行省事,也让「一个槽不能被钉死」这条语义由代码保证):

simc.topgear <基础角色字符串>
不要团队buff 暴击合剂 圣光潜力 无嗜血 5目标 木桩 饰品对比破损的阿曼尼战旗、盖博的无底袋、米利克萨之牙、乌拉特克贪婪之心都神话6/6 虫群之瘤不变
  • 末尾那条 都神话6/6 是整组默认值(这几件全按 334 算),不写「都」也一样;单件自己写了的以单件为准。
  • 米利克萨之牙(id 158367,旧资料片)不用写 id:入口会按中文名联网反查一次并记住。想写成 米利克萨之牙158367 也行。

真机实测(血 DK 血坦 / 5 目标 / 团队增益全关 / 120 秒,基准身上 trinket2 = 虫群之瘤):

| 组 | DPS | 相对基准 | | --- | --- | --- | | 乌拉特克贪婪之心 + 虫群之瘤 | 231,119 | 最高 | | 米利克萨之牙 + 虫群之瘤 | 228,958 | -0.94% | | 破损的阿曼尼战旗 + 虫群之瘤 | 222,137 | -3.89% | | 盖博的无底袋 + 虫群之瘤 | 221,146 | -4.32% | | 当前装备(基准) | 217,694 | -5.81% |


已知坑点(开发者向)

  • 成对槽位(trinket1/2、finger1/2)不能用 slot === cand.slot 去反查候选行。成对组的候选要同时换两个槽,按槽位反查只能找到其中一个 → 最佳组合确认时会漏掉一个槽,结果里留下身上那件、拼出重复装备。正解是候选自带 slots[] / lines[],buildProfile(base, slots, lines) 一次换多个槽(_pair_test.js 的 [A][C] 钉住这条)。
  • 组合的装等取两件里较小的那个(Math.min)。取第一件会在两件装等不同时给出偏高的假象。
  • 网页模板字符串里的内联 JS,反斜杠必须双写(split('\\n')、/^\\s*/)。漏一个轻则 SyntaxError → 整段脚本全哑、页面按钮全没反应,重则静默语义错(/^\s*/ 变成 /^s*/)。只断言「HTML 里有某个函数名」抓不到这类错,必须 new Function(内联脚本) 真解析一遍 —— _web_form_test.js / _swap_test.js / _loadout_test.js 都这么干。
  • FORM_HTML 的属性引号漏一个收尾引号,后面整段会被吞进属性里。这类问题只有真解析 DOM 才发现:用 jsdom + runScripts: 'dangerously' 把整页跑起来(_web_form_test.js 的 [B] E2E 段就是这么抓到一次真 bug 的)。
  • SimC 的 DPS 误差字段在 JSON 里叫 dpse(不是 dps_error)。
  • 多人模拟时 players 数组长度 > 1,每个玩家各自有 stats 技能数组;JSON 里没有单一 Sample Ability Log 时间线(那是 HTML 报告专属)。
  • 治疗专精 dps 可能为 0 / 缺省,解析时按 0 处理并打治疗警告。
  • SimC 偶发非 0 退出但仍产出可用 JSON;本插件先尝试读 JSON,失败才按错误上报。
  • 跨大版本时 json2 字段名可能微调,解析器对新字段缺失做了兜底。
  • 装备行的 level 不是装等:engine/dbc/generated/item_data.inc 里的 level 是基础装等(现代 WoW 实际装等由升级曲线决定,例:Relentless Rider's Crown DBC 里 197、官方 profile 写 289)。所以本地物品索引不存装等,换装备时要么用户给数字、要么写「难度 + 升级次数」由 UPGRADE_TRACKS 换算(见上面「换装」一节)。
  • slot=,id=N,ilevel=M 不需要 bonus_id:实测 4 例全部 exit 0 且 DPS 各异,官方 profile 自己也这么写。bonus_id 只影响升级等级 / 词缀 / 插槽,手动换件对比给装等就够。
  • 本地物品索引裁剪规则(_gen_item_index.js):id>=225000 + 品质 ≥ 绿 + 基础装等 ≥ 21,滤掉 6400 件幻化 / 玩具 / 新手杂物,11.5 万条 → 6129 件(253 KB)。同名物品留 id 最大的。
  • SimC 不回显命令行,所以真机断言「desired_targets 生效」要数 *** Targets *** 段里的 Target: xxx 行数(1 目标 = 1 行、5 目标 = 5 行)。
  • 中文物品名的唯一可行来源是 wago 整表导出:暴雪官方搜索接口(/data/wow/search/item)需要 OAuth 凭据,没配 bnetId/bnetSecret 时静默返回空——所以「搜中文」绝不能建成走官方接口。wago 的 ItemSparse 客户端字符串表不需要鉴权、可整表导出(48MB / 约 30 秒)。
  • wago 的两个限制(都实测过):filter[ID]=a,b,c 不支持批量(返回空表),columns= 不裁剪列。所以要么整表下载自己挑列,要么逐条查(6129 件 ≈ 8 分钟,别这么干)。
  • 但 filter[Display_lang]=<中文名> 单值反查可用,而且很便宜(整条响应约 2KB、几百毫秒,还带 InventoryType → 槽位)。这是「旧资料片物品按中文名搜到」的唯一可行路:暴雪官方搜索按中文名实测 0 命中(?name.zhCN= 返回空,static-cn 还要额外权限 403),wago 按 id 批量也不行,只有这条通。查到的结果落进别名表 <assetsDir>/item-alias.json,所以每个名字一辈子只查一次。列号一律按表头名查(ID / Display_lang / InventoryType,共 103 列,加字段是常事)。
  • 中文名索引需要重新生成的时机:游戏更新后本地物品库出现索引里没有的新物品时。步骤:node _gen_item_index.js → node _gen_item_zh.js --refresh。
  • 专精图标是「内联 + 外部」双层(specIcons.ts,40 张 PNG / 305KB → base64 后 408KB)。原因:图标缺失是完全静默的降级(卡片照发、只是没图),而正式机的部署方式不确定(该仓库没有 git remote,clone / rsync / npm 都可能)。所以 40 张全部编进产物,specIconBuffer() 的顺序是「外部文件(插件自带 assets/specs → config.assetsDir)→ 内联表」,任何部署方式都不会丢。⚠️ 外部查找必须遍历两个目录 —— 早期版本把「插件自带只读素材」和「可写缓存」绑成同一个变量,运维一改 config.assetsDir(只为把翻译缓存放共享盘)图标就全没了,而且不报错。_icon_test.js 里有对这条的回归。
  • assets/ 要进版本库:package.json 的 files 含 assets/specs,包的 .gitignore 只忽略运行时缓存(assets/*.json、assets/*.log)并显式 !assets/specs/**。别用宽泛的 assets/ 规则把它一起忽略掉。
  • 把组级默认值拼到候选上时,不能直接 名字 + 描述 + id:候选可能是 米利克萨之牙158367 这种「名字+id」写法,直接拼会变成 米利克萨之牙158367神话6/6 —— 尾部 5~6 位 id 不在末尾了,splitTrailingId 切不出来,整串被当成物品名去搜,报「搜不到「米利克萨之牙158367」」。正解是先把 id 切出来、把描述插在名字与 id 之间(gearPool.ts 的 withGroupLevel)。这个是真机端到端(_pool_e2e_test.js)抓到的,单元测试当时是绿的。
  • 索引体积不要紧:lib/index.js 现在 1.3MB(含 408KB 内联图标),V8 编译耗时一次性、实测几十毫秒,不用为体积做压缩。