Skip to content

KardsSim ​

把 KARDS 客户端的蓝图字节码逐条翻译成 C#,得到一套不依赖游戏本体、可复现、 可以拿去训练 AI 的本地对局模拟器。

关键取舍:不做文本解析,也不读反编译出来的伪代码。 效果来源是游戏自己编译出来的 Kismet AST —— 每张卡的行为就是它自己那份字节码, 没有"读卡面文字猜效果"的猜测成分。

项值
语言 / 运行时C# · .NET 10
依赖UAssetAPI 1.1.0(Kismet AST)+ KismetDecompiler(控制流边界修正)
直译产物1671 个资产 · 6434 个函数 · 45.6 万行 · 21 MB
宿主原语172 个条目(见 06 · 推荐 hook 点)
仓库https://github.com/CCB-TEAM/kards-sim

为什么可以直译 ​

普通反编译要费大力气重建控制流图(CFG),是因为汇编里只有 jmp 和地址。 但 Kismet 字节码自带结构化跳转:EX_Jump / EX_JumpIfNot / EX_ComputedJump 都是带偏移的表达式节点。所以

Kismet:   if (!cond) goto 0x1234;   body;   0x1234: next;
C#:       if (!cond) goto L_1234;   body;   L_1234: next;

是逐条 1:1 的映射,不需要重建 CFG。 EX_PushExecutionFlow / EX_PopExecutionFlow 用一个真的 Stack<int> + switch 承载, 连"跳转段栈"这种最麻烦的结构都不用猜。

这样做出来的产物不好读(全是 L_0A23: 和 goto),但它不需要好读 —— 它是给机器执行的。

架构 ​

层职责
KismetDecompiler修正反编译器的控制流边界(EX_ComputedJump 作为块切分点与 halting 边)
KardsTranspilerKismet AST → C#,一个函数一个静态方法,一条语句一个 label
Generated/1671 个资产的直译产物(已入库,克隆即可编译,不需要游戏资产)
Kismet/运行时:IHost 接口 + 值模型 + 通用库函数(Array/Map/String/Math)
Bridge/hook 缝:把直译产物接到对局引擎(分派顺序与推荐切点见 06)
Engine/对局规则、动作合法性、回合流程、触发点时机
Server/HTTP 接口,给不同 AI 后端用

把直译产物接进引擎的这一层(Bridge/)是全部工作量所在 —— 也就是"还原到什么程度就够了,剩下的可以直接 hook"的那条缝。 缝具体切在哪、有哪些推荐 hook 点、哪些地方明确不要 hook, 单独写了一章:06 · 宿主该切在哪一层。

怎么跑 ​

bash
# 自对弈,打印宿主 API 覆盖率 + 触发点完整性检查
dotnet run --project KardsSim -c Release -- --mode selfplay --games 500

# 用社区推荐卡组自对弈(预设名 / 卡组码 / 卡牌 id 列表都认;--swap-sides 每局交换左右)
dotnet run --project KardsSim -c Release -- --mode selfplay --games 200 \
    --left-deck jp-pol-boom --right-deck ger-usa --swap-sides

# 机制单测(带断言,能失败)
dotnet run --project KardsSim -c Release -- --mode tests

# 卡组码审计:22 套推荐卡组,哪些这套模拟器能打完整
dotnet run --project KardsSim -c Release -- --mode decks

# HTTP 接口,默认 http://127.0.0.1:8642/
dotnet run --project KardsSim -c Release -- --mode serve

九个审计模式 ​

跑多少局自对弈都不能证明"完备"—— 随机对局抽不到那些卡,等于没测。 所以除了对局,另做了九个专门查完备性的模式:

模式查什么
apicheck静态全扫所有 H.Call 目标,判断有没有地方接;区分「卡牌逻辑可达」与「仅 UI 可达」
smoke逐卡强制触发它注册的每一个触发点,绕开随机采样
triggerhits运行期实测每个触发点命中次数
triggers对照「卡牌注册 / 引擎发出 / 游戏侧分派器」三张表
rules客户端规则库(cardsCheckFunctions)的否决理由 vs 引擎侧现状
paramaudit引擎原语实现 vs UHT 签名:找「是 void 带 out、实现却 return」
byref扫直译产物里「out 槽零初始化、函数体却先读」的函数(详见 07)
repro1端到端复现:击杀 → 变老兵 → 打 HQ 3 点
decks卡组码解析与覆盖度:22 套推荐卡组逐条对账
tests机制断言(能失败的,不是"跑通没炸")

这些会给出互相矛盾的结论,而矛盾本身才是有用的 —— 详见专题 审计方法论。

当前状态 ​

指标值
直译覆盖1671 个资产 / 6434 个函数,0 空体
编译45.6 万行 0 错误
自对弈0 异常 / 0 卡死 / 0 非法动作 / 0% 未实现调用
全卡强制演练 1638 张0 异常 / 0 未实现调用
触发点覆盖61 / 61
宿主 API卡牌逻辑可达的缺口 0 个
机制断言153 / 153
卡组码22 套社区推荐卡组全部能解析,6 套能完整还原 39 张(其余缺 1~4 张,是本仓库卡牌导出不全)

已知未完成 ​

  • 卡牌导出不全:_input/Cards 比线上客户端少约 600 个资产(_bal 平衡版尤其缺), 于是部分推荐卡组只能还原 35/39 张。补齐导出后重跑转译即可,解析器本身已经就绪。
  • 没有 UI 就无法复现的选择点:部分卡的 Develop 结果在客户端是UI 直接做的, 它们没有自己的 OnHandTargetSelected。选择能被发起和结算,但效果不落地。
  • 已知怪相(与客户端同源):一批牌一起撤退时,先搬走的那张不再场上, 后搬的光环卡按"我左边有谁"找不到它,+2 会留在已经回手牌的那张牌上。 要修得让"撤销某个来源的加成"按来源查全场。
  • 接官服(自己写私服 里的协议逆向)需要一个真实玩家的 对局抓包才能对齐"客户端权威"的边界。

相关 ​