@bingine/cemem 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/PORT-SCOPE.md +169 -0
  2. package/README.md +451 -2
  3. package/dist/cjs/addr/expression.js +650 -0
  4. package/dist/cjs/addr/expression.js.map +1 -0
  5. package/dist/cjs/addresslist/memory-record.js +612 -0
  6. package/dist/cjs/addresslist/memory-record.js.map +1 -0
  7. package/dist/cjs/asm/assembler.js +354 -0
  8. package/dist/cjs/asm/assembler.js.map +1 -0
  9. package/dist/cjs/asm/autoassembler-directives.js +362 -0
  10. package/dist/cjs/asm/autoassembler-directives.js.map +1 -0
  11. package/dist/cjs/asm/autoassembler.js +2163 -0
  12. package/dist/cjs/asm/autoassembler.js.map +1 -0
  13. package/dist/cjs/asm/backend.js +394 -0
  14. package/dist/cjs/asm/backend.js.map +1 -0
  15. package/dist/cjs/asm/disassembler.js +715 -0
  16. package/dist/cjs/asm/disassembler.js.map +1 -0
  17. package/dist/cjs/backend/dbk-backend.js +683 -0
  18. package/dist/cjs/backend/dbk-backend.js.map +1 -0
  19. package/dist/cjs/backend/native-ce-backend.js +667 -0
  20. package/dist/cjs/backend/native-ce-backend.js.map +1 -0
  21. package/dist/cjs/backend/self-backend.js +449 -0
  22. package/dist/cjs/backend/self-backend.js.map +1 -0
  23. package/dist/cjs/backend/types.js +48 -0
  24. package/dist/cjs/backend/types.js.map +1 -0
  25. package/dist/cjs/cheattable/ct-format.js +533 -0
  26. package/dist/cjs/cheattable/ct-format.js.map +1 -0
  27. package/dist/cjs/index.js +219 -0
  28. package/dist/cjs/index.js.map +1 -0
  29. package/dist/cjs/lua/aa.js +230 -0
  30. package/dist/cjs/lua/aa.js.map +1 -0
  31. package/dist/cjs/lua/addressing.js +118 -0
  32. package/dist/cjs/lua/addressing.js.map +1 -0
  33. package/dist/cjs/lua/api.js +736 -0
  34. package/dist/cjs/lua/api.js.map +1 -0
  35. package/dist/cjs/lua/asm.js +220 -0
  36. package/dist/cjs/lua/asm.js.map +1 -0
  37. package/dist/cjs/lua/bridge.js +526 -0
  38. package/dist/cjs/lua/bridge.js.map +1 -0
  39. package/dist/cjs/lua/marshal.js +562 -0
  40. package/dist/cjs/lua/marshal.js.map +1 -0
  41. package/dist/cjs/lua/memory.js +213 -0
  42. package/dist/cjs/lua/memory.js.map +1 -0
  43. package/dist/cjs/lua/objects.js +1070 -0
  44. package/dist/cjs/lua/objects.js.map +1 -0
  45. package/dist/cjs/lua/scan.js +543 -0
  46. package/dist/cjs/lua/scan.js.map +1 -0
  47. package/dist/cjs/lua/types.js +103 -0
  48. package/dist/cjs/lua/types.js.map +1 -0
  49. package/dist/cjs/scanner/matcher.js +781 -0
  50. package/dist/cjs/scanner/matcher.js.map +1 -0
  51. package/dist/cjs/scanner/pointer-scan.js +546 -0
  52. package/dist/cjs/scanner/pointer-scan.js.map +1 -0
  53. package/dist/cjs/scanner/scanner.js +513 -0
  54. package/dist/cjs/scanner/scanner.js.map +1 -0
  55. package/dist/cjs/scanner/shared-layout.js +207 -0
  56. package/dist/cjs/scanner/shared-layout.js.map +1 -0
  57. package/dist/cjs/types.js +190 -0
  58. package/dist/cjs/types.js.map +1 -0
  59. package/dist/cjs/value-codec.js +515 -0
  60. package/dist/cjs/value-codec.js.map +1 -0
  61. package/dist/cjs/win/process.js +475 -0
  62. package/dist/cjs/win/process.js.map +1 -0
  63. package/dist/cjs/win/scan-worker.js +84 -0
  64. package/dist/cjs/win/scan-worker.js.map +1 -0
  65. package/dist/cjs/win/winapi.js +403 -0
  66. package/dist/cjs/win/winapi.js.map +1 -0
  67. package/dist/types/addr/expression.d.ts +98 -0
  68. package/dist/types/addr/expression.d.ts.map +1 -0
  69. package/dist/types/addresslist/memory-record.d.ts +218 -0
  70. package/dist/types/addresslist/memory-record.d.ts.map +1 -0
  71. package/dist/types/asm/assembler.d.ts +106 -0
  72. package/dist/types/asm/assembler.d.ts.map +1 -0
  73. package/dist/types/asm/autoassembler-directives.d.ts +118 -0
  74. package/dist/types/asm/autoassembler-directives.d.ts.map +1 -0
  75. package/dist/types/asm/autoassembler.d.ts +434 -0
  76. package/dist/types/asm/autoassembler.d.ts.map +1 -0
  77. package/dist/types/asm/backend.d.ts +167 -0
  78. package/dist/types/asm/backend.d.ts.map +1 -0
  79. package/dist/types/asm/disassembler.d.ts +194 -0
  80. package/dist/types/asm/disassembler.d.ts.map +1 -0
  81. package/dist/types/backend/dbk-backend.d.ts +353 -0
  82. package/dist/types/backend/dbk-backend.d.ts.map +1 -0
  83. package/dist/types/backend/native-ce-backend.d.ts +213 -0
  84. package/dist/types/backend/native-ce-backend.d.ts.map +1 -0
  85. package/dist/types/backend/self-backend.d.ts +64 -0
  86. package/dist/types/backend/self-backend.d.ts.map +1 -0
  87. package/dist/types/backend/types.d.ts +176 -0
  88. package/dist/types/backend/types.d.ts.map +1 -0
  89. package/dist/types/cheattable/ct-format.d.ts +100 -0
  90. package/dist/types/cheattable/ct-format.d.ts.map +1 -0
  91. package/dist/types/index.d.ts +54 -0
  92. package/dist/types/index.d.ts.map +1 -0
  93. package/dist/types/lua/aa.d.ts +107 -0
  94. package/dist/types/lua/aa.d.ts.map +1 -0
  95. package/dist/types/lua/addressing.d.ts +41 -0
  96. package/dist/types/lua/addressing.d.ts.map +1 -0
  97. package/dist/types/lua/api.d.ts +46 -0
  98. package/dist/types/lua/api.d.ts.map +1 -0
  99. package/dist/types/lua/asm.d.ts +42 -0
  100. package/dist/types/lua/asm.d.ts.map +1 -0
  101. package/dist/types/lua/bridge.d.ts +129 -0
  102. package/dist/types/lua/bridge.d.ts.map +1 -0
  103. package/dist/types/lua/marshal.d.ts +244 -0
  104. package/dist/types/lua/marshal.d.ts.map +1 -0
  105. package/dist/types/lua/memory.d.ts +61 -0
  106. package/dist/types/lua/memory.d.ts.map +1 -0
  107. package/dist/types/lua/objects.d.ts +210 -0
  108. package/dist/types/lua/objects.d.ts.map +1 -0
  109. package/dist/types/lua/scan.d.ts +106 -0
  110. package/dist/types/lua/scan.d.ts.map +1 -0
  111. package/dist/types/lua/types.d.ts +146 -0
  112. package/dist/types/lua/types.d.ts.map +1 -0
  113. package/dist/types/scanner/matcher.d.ts +122 -0
  114. package/dist/types/scanner/matcher.d.ts.map +1 -0
  115. package/dist/types/scanner/pointer-scan.d.ts +187 -0
  116. package/dist/types/scanner/pointer-scan.d.ts.map +1 -0
  117. package/dist/types/scanner/scanner.d.ts +150 -0
  118. package/dist/types/scanner/scanner.d.ts.map +1 -0
  119. package/dist/types/scanner/shared-layout.d.ts +96 -0
  120. package/dist/types/scanner/shared-layout.d.ts.map +1 -0
  121. package/dist/types/types.d.ts +239 -0
  122. package/dist/types/types.d.ts.map +1 -0
  123. package/dist/types/value-codec.d.ts +134 -0
  124. package/dist/types/value-codec.d.ts.map +1 -0
  125. package/dist/types/win/process.d.ts +190 -0
  126. package/dist/types/win/process.d.ts.map +1 -0
  127. package/dist/types/win/scan-worker.d.ts +51 -0
  128. package/dist/types/win/scan-worker.d.ts.map +1 -0
  129. package/dist/types/win/winapi.d.ts +144 -0
  130. package/dist/types/win/winapi.d.ts.map +1 -0
  131. package/docs/assembler.md +371 -0
  132. package/docs/autoassembler.md +423 -0
  133. package/docs/backend-dbk.md +305 -0
  134. package/docs/backends.md +273 -0
  135. package/docs/evidence/atomics-wait-io.js +20 -0
  136. package/docs/evidence/mini-repro.js +98 -0
  137. package/docs/lua-api.md +718 -0
  138. package/package.json +60 -4
package/PORT-SCOPE.md ADDED
@@ -0,0 +1,169 @@
1
+ # Cheat Engine → Node.js 移植范围说明(PORT-SCOPE)
2
+
3
+ 本文件说明:**哪些部分真正移植到了 Node.js、哪些在 Node 中不存在实现路径、以及为什么**。
4
+
5
+ 原始代码规模(`cheat-engine-master.zip` 实测统计):
6
+
7
+ | 部分 | 行数 | 说明 |
8
+ | --- | --- | --- |
9
+ | `Cheat Engine/`(Object Pascal / Lazarus) | 462,717 | 含 529 个 `.pas`、164 个 `.lfm` 窗体、C/C++ 内核驱动 |
10
+ | `dbvm/` | 65,545 | DBVM hypervisor(VT-x / AMD-V) |
11
+ | `lua/` | 17,039 | 随包 Lua 运行时(C) |
12
+ | `DBKKernel/` | 14,307 | Windows 内核驱动 |
13
+ | `DBVM UEFI/` | 2,337 | UEFI 引导层 |
14
+ | **合计** | **≈562,000** | |
15
+
16
+ **逐行等价重写整个 Cheat Engine 到 Node.js 是不可行的**,原因不是工作量,而是下列部分
17
+ 在 Node/JS 的用户态运行时里**不存在实现路径**。因此本移植的策略是:
18
+ **把可移植的算法与协议层做到真实可用、语义逐行对齐;对不可移植部分给出明确边界与替代方案。**
19
+
20
+ ---
21
+
22
+ ## 一、已移植并验证可用
23
+
24
+ 全部经**真实跨进程**测试验证(目标进程为 `test/fixture-target.js`,用 VirtualAlloc 分配可预测内存)。
25
+ `npm test` → **61 个测试全部通过**。
26
+
27
+ | 模块 | 对应 CE 源文件 | 状态 |
28
+ | --- | --- | --- |
29
+ | 变量类型与枚举 | `memscan.pas` 的 `TVariableType`(17 种) | ✅ 逐一对应 |
30
+ | 值编解码、浮点舍入策略 | `memscan.pas` 比较函数族、`parsers.pas` | ✅ 含 FPC 的偶舍入、AOB 半字节通配 |
31
+ | 内存扫描引擎 | `memscan.pas`(TMemScan / TScanner / TScanController) | ✅ 14 种扫描选项 × 6 种数值类型 + 字符串/Unicode/AOB/二进制/全部类型/分组长 |
32
+ | 指针扫描 | `pointerscancontroller.pas`、`pointerscanstructures.pas` | ✅ 指针映射 + 逐级回溯 + 结果可解析回目标 |
33
+ | 地址表达式解析 | `addressparser.pas` | ✅ 模块相对、解引用、算术、GetModuleBase 等 |
34
+ | 地址表与内存记录 | `MemoryRecordUnit.pas`、`addresslist.pas` | ✅ 记录/偏移链/锁定/快照 |
35
+ | Cheat Table (.CT) 读写 | `MemoryRecordUnit.pas` 的 XML 写出 | ✅ 元素名/属性/引号形式对齐,往返不丢信息 |
36
+ | 多线程并行扫描 | CE 的 TScanner 多线程 | ✅ worker + SharedArrayBuffer,已校验与单线程结果**完全一致** |
37
+ | 反汇编器 | `disassembler.pas`(经 binutils 路径) | ✅ 后端 Capstone,字段对齐 `TLastDisassembleData` |
38
+ | ARM/Thumb/ARM64 反汇编 | `disassemblerarm64.pas`、`disassemblerthumb.pas`、`disassemblerarm32*.pas` | ✅ 后端 Capstone,含条件码/PC 相对/数据宽度 |
39
+ | 汇编器 | `Assemblerunit.pas`(经 binutils 路径) | ✅ 后端 Keystone,含符号解析与逐行地址映射 |
40
+ | ARM 汇编 | `assemblerarm.pas`(1,074 行) | ✅ 后端 Keystone,A32/Thumb/AArch64 |
41
+ | Auto Assembler | `autoassembler.pas` | ✅ 见 [docs/autoassembler.md](docs/autoassembler.md) |
42
+ | Lua 脚本桥 | `LuaHandler.pas` / `LuaCaller.pas` | ✅ 61 个 CE Lua API 函数,Lua 5.4(wasmoon),int64 不丢精度 |
43
+ | 用户态内存后端 | `NewKernelHandler.pas` / `ProcessHandlerUnit.pas` | ✅ 自研后端(默认) |
44
+ | `ceserver` 协议客户端 | `ceserver/ceserver.h`+`ceserver.c` | ✅ 原生 CE 兼容层(异步) |
45
+
46
+ **反汇编/汇编的实现策略**:CE 在 Windows 上**优先调用 binutils 的 objdump/as**
47
+ (`disassembler.pas` 里的 `if defaultBinutils<>nil` 分支 + `binutils.pas` 的
48
+ `CreateProcess objdump`),自带的 Pascal 实现只是无 binutils 时的回退路径。
49
+ 本移植沿用同一架构,用 Capstone(反汇编)+ Keystone(汇编)做后端,
50
+ 因此覆盖面(SSE/AVX 等)与正确性都优于手抄指令表;详见
51
+ [docs/assembler.md](docs/assembler.md)。**未覆盖 ARM/ARM64**
52
+ (CE 另有 `disassemblerarm64.pas` 4,608 行等)。
53
+
54
+ **实测性能**(本机):读内存 2.4~3.3 GB/s;`VirtualQueryEx` 枚举 553 个区域 2ms;
55
+ DWord 精确扫描 64MB 约 310ms;并行扫描与单线程结果集逐地址一致。
56
+
57
+ ---
58
+
59
+ ## 二、无法移植(Node 中不存在实现路径)
60
+
61
+ 这些不是"还没做",而是**物理上做不到**——它们要求 ring-0 特权或硬件虚拟化。
62
+
63
+ ### 1. 内核驱动(`DBKKernel/`、`Cheat Engine/dbk32/`,约 14,000 行)
64
+
65
+ CE 通过自签名的 `dbk64.sys` 内核驱动实现:读写任意进程物理内存、绕过
66
+ `ObRegisterCallbacks` 与 PPL 保护、句柄提权、内核态断点、内核符号枚举。
67
+
68
+ - **为什么不行**:内核驱动必须运行在 ring-0,由 Windows 内核加载并校验签名。
69
+ Node.js 代码运行在用户态,无法安装内核回调、无法访问内核地址空间。
70
+ 这不是 API 缺失问题,而是特权级隔离。
71
+ - **替代方案**:本移植的 `SelfBackend` 覆盖了 CE 的"无驱动回退路径"能做的全部事情
72
+ (`OpenProcess` + `ReadProcessMemory` + `VirtualQueryEx`)。
73
+ 受保护进程(反作弊、PPL 进程)读不到——这与 CE 在**未加载驱动**时的行为一致。
74
+ 真需要内核能力时,可选用 CE 官方的 `ceserver`(见 `NativeCeBackend`)。
75
+
76
+ ### 2. DBVM hypervisor(`dbvm/`、`DBVM UEFI/`,约 68,000 行)
77
+
78
+ CE 自研的虚拟机监控器,实现硬件断点、隐身、指令级追踪、CR3 切换、
79
+ 物理内存访问而无需驱动签名。
80
+
81
+ - **为什么不行**:需要 VT-x / AMD-V 虚拟化扩展与 UEFI 启动层,
82
+ 直接把 CPU 置于 VMX root 模式。JS 运行时无法执行 `VMXON`/`VMLAUNCH`,
83
+ 也无法在 UEFI 阶段加载代码。
84
+ - **替代方案**:无。需要这类能力请使用原生 CE。
85
+
86
+ ### 3. LCL/VCL 图形界面(164 个 `.lfm` 窗体)
87
+
88
+ CE 的主界面、内存查看器、反汇编器窗口、结构体编辑器等都是 Lazarus 窗体。
89
+
90
+ - **为什么不行**:Node 没有 VCL/LCL 对应物。
91
+ - **替代方案**:本移植只提供**类库 + TypeScript 类型**(按你的选择,不做窗口化)。
92
+ 需要 GUI 时可由调用方自行套 Web UI 或 Electron —— 引擎层不依赖任何 UI。
93
+
94
+ ### 4. 调试器相关(`WindowsDebugger.pas`、`KernelDebugger.pas`、VEH、注入)
95
+
96
+ CE 的断点、单步、调用栈、代码注入、`CreateRemoteThread` 注入。
97
+
98
+ - **为什么不行**:其中**用户态调试 API 部分是可移植的**
99
+ (`DebugActiveProcess`/`WaitForDebugEvent`),但需要重写整个调试事件循环与线程上下文管理;
100
+ **内核态部分**(`KernelDebugger.pas`)不可移植。
101
+ - **替代方案**:本轮未实现。注意 `Win32` 的 `DebugActiveProcess` 无法附加到
102
+ 自身进程,且受保护进程不可附加。
103
+
104
+ ### 5. .NET / Mono / Java 数据收集器(`DotNetDataCollector/`、`MonoDataCollector/`、`Java/`)
105
+
106
+ - **为什么不行**:这些注入 .NET/Mono/JVM 进程并读取其对象图,
107
+ 依赖运行时特定的内部结构。
108
+ - **替代方案**:与本移植的纯内存扫描正交,可单独实现(例如走 ClrMD 的 .NET 绑定)。
109
+
110
+ ### 6. Speedhack / D3D hook / 输入法 / 音频(`speedhack/`、`Direct x mess/`、`xmplayer/`)
111
+
112
+ - **为什么不行**:需要 API hooking(`SetWindowsHookEx`、IAT/inline hook)与
113
+ DirectX COM 接口调用,Node 无对应能力。
114
+ - **替代方案**:无。
115
+
116
+ ---
117
+
118
+ ## 三、两种调用方式
119
+
120
+ 按你的要求,本移植提供**统一接口 + 两个可切换后端**:
121
+
122
+ | | `SelfBackend`(默认) | `NativeCeBackend` |
123
+ | --- | --- | --- |
124
+ | 原理 | 本进程内直接调 Win32 API | 连接 CE 官方 `ceserver` 的 TCP 协议 |
125
+ | 依赖 | 无(koffi 绑定) | 需要运行中的 `ceserver` |
126
+ | 同步性 | **同步**(直接返回值) | **异步**(返回 Promise) |
127
+ | 受保护进程 | 读不到(无驱动) | 取决于 ceserver 权限与扩展 |
128
+ | 适用 | 开箱即用、兼容性最强 | 需要原生 CE 的能力时 |
129
+
130
+ 两者共用同一套上层 API(扫描、地址表、.CT、地址表达式)。
131
+ 详细用法与"为什么原生后端必须异步"的论证见 [docs/backends.md](docs/backends.md)。
132
+
133
+ ### 关于"同步调用原生 CE"的结论
134
+
135
+ **已证明不可实现**,两条路都被实测堵死(最小复现保留在 `docs/evidence/` 下):
136
+
137
+ 1. 主线程 `Promise + Atomics.wait` → 阻塞卡死事件循环,socket 回调不执行
138
+ (`docs/evidence/atomics-wait-io.js`);
139
+ 2. socket 挪到 worker 线程、主线程仍 `Atomics.wait` → **同样死锁**,
140
+ 因为 worker 被阻塞后它自己的事件循环也停了(`docs/evidence/mini-repro.js`)。
141
+
142
+ 根本原因:`Atomics.wait` 与 socket I/O **无法在任何同一线程内共存**。
143
+ 因此原生 CE 后端诚实地暴露为异步 API,而不是用子进程或忙轮询伪造同步。
144
+
145
+ ---
146
+
147
+ ## 四、与 CE 的已知语义差异
148
+
149
+ 以下差异是**有意为之**的,均已写入对应代码注释或文档:
150
+
151
+ 1. **浮点极值舍入的区间开闭**:CE 自身在 `SingleExact` 用闭区间 `[min,max]`、
152
+ 在 `DoubleExact` 用开区间 `(min,max)`。本移植**保持这个不一致**以对齐 CE 行为。
153
+ 2. **`.scandata` 指针扫描文件格式**:CE 用自定义二进制 PSCAN 格式;
154
+ 本移植用内存缓存 + JSON,**与 CE 二进制格式不兼容**。
155
+ 3. **地址表 `.CT` 格式兼容**:读写双向兼容(元素顺序、引号形式、未知元素保留),
156
+ 但不支持 Auto Assembler 脚本段的执行。
157
+ 4. **Lua 桥的部分 API 语义**:`readInteger` 默认有符号、`readBytes` 默认返回表、
158
+ `sleep` 阻塞事件循环等,详见 [docs/lua-api.md](docs/lua-api.md) 的差异章节。
159
+ 5. **`NativeCeBackend` 未实现线程枚举**:ceserver 的
160
+ `CMD_CREATETOOLHELP32SNAPSHOTEX(35)` 响应布局随版本变化,为可靠性起见不做猜测。
161
+
162
+ ---
163
+
164
+ ## 五、结论
165
+
166
+ - 需要 **CE 的扫描/指针/地址表/Lua 能力** → 本移植已覆盖,可直接用。
167
+ - 需要 **内核驱动 / DBVM / 反汇编 / 调试器 / GUI** → 请使用原生 CE;
168
+ 或者在 Node 侧通过 `NativeCeBackend` 调用一个运行中的 CE 服务端。
169
+ - 需要**逐行等价**的完整 CE → 不可行,原因见上。
package/README.md CHANGED
@@ -1,3 +1,452 @@
1
- # Temporary Holding Version
1
+ # node-cheat-engine
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Cheat Engine 核心引擎的 Node.js 移植版** —— 跨进程内存扫描 / 修改引擎,
4
+ 算法语义逐行对齐 Cheat Engine 源码,提供 npm 类库与 TypeScript 类型。
5
+
6
+ > ⚠️ **先读这个**:本移植**不是**整个 Cheat Engine 的逐行重写。
7
+ > 原始代码约 **562,000 行**,其中约 **8 万行是 ring-0 内核驱动与 hypervisor**,
8
+ > 在 Node 用户态里**不存在实现路径**。已移植与未移植部分的完整清单、
9
+ > 以及每一项"为什么做不到",见 **[PORT-SCOPE.md](PORT-SCOPE.md)**。
10
+
11
+ ## 特性
12
+
13
+ - **完整的内存扫描引擎**:14 种扫描选项 × 6 种数值类型,另含
14
+ 字符串 / Unicode / AOB(支持半字节通配)/ 二进制位串 / 全部类型 / 分组长扫描
15
+ - **浮点语义对齐**:复刻 FPC 的偶舍入(banker's rounding)与 CE 的
16
+ `rtRounded` / `rtExtremeRounded` / `rtTruncated` 三种策略(**含 CE 自身
17
+ 在 Single 用闭区间、Double 用开区间的不一致行为**)
18
+ - **指针扫描**:指针映射 + 逐级回溯,结果可用 `applyPointerPath` 验证
19
+ - **地址表与 `.CT` 读写**:与 CE 的 Cheat Table XML 双向兼容,未知元素往返不丢
20
+ - **地址表达式**:`"game.exe"+1234`、`[[base]+10]+8`、`GetModuleBase(...)` 等
21
+ - **反汇编 / 汇编**:x86/x64 **及 ARM / Thumb / ARM64** 反汇编
22
+ (字段对齐 CE 的 `TLastDisassembleData`)与汇编(含符号解析、逐行地址映射)
23
+ - **Auto Assembler**:CE 脚本的 `alloc`/`label`/`aobscan`/`[ENABLE]`/`[DISABLE]` 等,
24
+ x86/x64 与 ARM 目标都支持
25
+ - **Lua 脚本桥**:61 个 CE Lua API 函数 + `autoAssemble`/`autoAssembleCheck`
26
+ (Lua 5.4,int64 全程不丢精度)
27
+ - **并行扫描**:worker + SharedArrayBuffer,已校验与单线程结果**逐地址一致**
28
+ - **三种可切换后端**:自研引擎(同步、开箱即用)、原生 CE(连接 `ceserver`)、DBK 内核驱动(读受保护进程)
29
+ - **无 GUI 依赖**:纯类库,可嵌入任何 Node 程序
30
+
31
+ ## 安装
32
+
33
+ ```bash
34
+ npm install node-cheat-engine
35
+ ```
36
+
37
+ 要求:Node.js ≥ 18,Windows x64(使用 Win32 原生 API)。
38
+
39
+ ## 快速开始
40
+
41
+ ```js
42
+ const {
43
+ Process,
44
+ ScanSession,
45
+ VariableType,
46
+ ScanOption,
47
+ RoundingType,
48
+ } = require('node-cheat-engine');
49
+
50
+ // 1) 打开目标进程(默认使用自研后端)
51
+ const proc = Process.openByName('notepad.exe');
52
+ console.log(`已附加:${proc.name} (pid ${proc.pid})`);
53
+
54
+ try {
55
+ // 2) 首次扫描:找 int32 值 1000
56
+ const scan = new ScanSession(proc);
57
+ scan.firstScan({
58
+ variableType: VariableType.DWord,
59
+ scanOption: ScanOption.ExactValue,
60
+ roundingType: RoundingType.Rounded,
61
+ floatAccuracy: 0,
62
+ value1: '1000',
63
+ includeImage: false, // 不扫模块映像区,减少噪声
64
+ includeMapped: false,
65
+ });
66
+ console.log(`首次扫描命中 ${scan.resultCount} 个`);
67
+
68
+ // 3) 游戏里让数值变化后,再次扫描缩小范围
69
+ scan.nextScan({
70
+ variableType: VariableType.DWord,
71
+ scanOption: ScanOption.DecreasedValue, // 减少了
72
+ roundingType: RoundingType.Rounded,
73
+ floatAccuracy: 0,
74
+ includeImage: false,
75
+ includeMapped: false,
76
+ });
77
+ console.log(`再次扫描后剩 ${scan.resultCount} 个`);
78
+
79
+ // 4) 对结果写入新值
80
+ for (const addr of scan.results.slice(0, 5)) {
81
+ const buf = Buffer.alloc(4);
82
+ buf.writeInt32LE(999999, 0);
83
+ proc.writeMemory(addr, buf);
84
+ }
85
+ } finally {
86
+ proc.close();
87
+ }
88
+ ```
89
+
90
+ ## 常用片段
91
+
92
+ ### AOB 扫描(含通配)
93
+
94
+ ```js
95
+ const { Process, ScanSession, VariableType, ScanOption, RoundingType } = require('node-cheat-engine');
96
+
97
+ const proc = Process.open(1234);
98
+ const scan = new ScanSession(proc);
99
+ scan.firstScan({
100
+ variableType: VariableType.ByteArray,
101
+ scanOption: ScanOption.ExactValue,
102
+ roundingType: RoundingType.Rounded,
103
+ floatAccuracy: 0,
104
+ value1: 'DE AD BE EF 90 90 ?? 7*', // ?? = 通配字节;7* = 高半字节必须为 7
105
+ });
106
+ console.log(scan.results.map((a) => '0x' + a.toString(16)));
107
+ proc.close();
108
+ ```
109
+
110
+ ### 指针扫描
111
+
112
+ ```js
113
+ const { Process, PointerScanner, applyPointerPath, formatPointerPath } = require('node-cheat-engine');
114
+
115
+ const proc = Process.open(1234);
116
+ const ps = new PointerScanner(proc, { maxLevel: 5, maxOffset: 2048, staticOnly: true });
117
+ const paths = ps.scan(0x1234abcdn);
118
+
119
+ for (const p of paths.slice(0, 10)) {
120
+ console.log(formatPointerPath(p));
121
+ // 验证:沿路径解析应回到目标地址
122
+ console.assert(applyPointerPath(p, (a) => ps.readPointer(a)) === 0x1234abcdn);
123
+ }
124
+ proc.close();
125
+ ```
126
+
127
+ ### 读写 Cheat Table
128
+
129
+ ```js
130
+ const fs = require('node:fs');
131
+ const { loadCheatTable, saveCheatTable, Process } = require('node-cheat-engine');
132
+
133
+ const proc = Process.open(1234);
134
+ const { list } = loadCheatTable(fs.readFileSync('my.CT', 'utf8'), proc);
135
+
136
+ // 读所有条目的当前值
137
+ for (const row of list.snapshot()) {
138
+ console.log(row.description, row.addressText, '=', row.value, row.error ?? '');
139
+ }
140
+
141
+ // 改一个值并锁定
142
+ const rec = list.getByDescription('生命值');
143
+ rec.value = '9999';
144
+ rec.frozen = true;
145
+ setInterval(() => list.tickFrozen(), 100); // 宿主自行驱动锁定写回
146
+
147
+ proc.close();
148
+ ```
149
+
150
+ ### Lua 脚本
151
+
152
+ ```js
153
+ const { LuaEngine } = require('node-cheat-engine');
154
+
155
+ const lua = new LuaEngine();
156
+ await lua.doString(`
157
+ openProcess("notepad.exe")
158
+ local v = readInteger("0x7ff000001000")
159
+ print("读到 " .. v)
160
+ `);
161
+ lua.dispose();
162
+ ```
163
+
164
+ 完整 API 见 [docs/lua-api.md](docs/lua-api.md)。
165
+
166
+ ### 反汇编与汇编
167
+
168
+ ```js
169
+ const {
170
+ ensureAsmBackend, disassembleOne, formatDisassembled,
171
+ assembleToBytes, instructionText,
172
+ } = require('node-cheat-engine');
173
+
174
+ await ensureAsmBackend(); // WASM 异步初始化,必须先调用
175
+
176
+ // 反汇编
177
+ const insn = disassembleOne(Buffer.from('488b0510203040', 'hex'), 0x7ff000001000n);
178
+ console.log(insn.opcode); // 'mov'
179
+ console.log(insn.parameters); // 'rax, qword ptr [rip + 0x40302010]'
180
+ console.log(insn.dataSize); // 8
181
+ console.log(formatDisassembled(insn));
182
+ // 00007FF000001000 - 48 8B 05 10 20 30 40 - mov rax, qword ptr [rip + 0x40302010]
183
+
184
+ // 汇编
185
+ const bytes = assembleToBytes('mov rax, 1\nret', { address: 0x1000n });
186
+ console.log(bytes.toString('hex')); // '48c7c001000000c3'
187
+
188
+ // 往返必须用 instructionText(含 rep/lock 前缀)
189
+ const rep = disassembleOne(Buffer.from('f3a4', 'hex'), 0x1000n);
190
+ instructionText(rep); // 'rep movsb'
191
+ assembleToBytes(instructionText(rep), { address: 0x1000n }); // f3a4
192
+ ```
193
+
194
+ 后端是 Capstone + Keystone —— **CE 自己也是调 binutils 的 objdump/as**,
195
+ 自带 Pascal 实现只是回退路径(详见 [docs/assembler.md](docs/assembler.md))。
196
+
197
+ **ARM / Thumb / ARM64** 用 `arch` 参数切换:
198
+
199
+ ```js
200
+ const { assembleToBytes, disassemble, instructionText } = require('node-cheat-engine');
201
+
202
+ // ARM64
203
+ const a64 = assembleToBytes('stp x29, x30, [sp, #-16]!\nret', {
204
+ address: 0x400000n, arch: 'arm64',
205
+ });
206
+ disassemble(a64, 0x400000n, 4, { arch: 'arm64' })
207
+ .forEach((i) => console.log(instructionText(i)));
208
+ // stp x29, x30, [sp, #-0x10]!
209
+ // ret
210
+
211
+ // Thumb(2/4 字节变长)
212
+ assembleToBytes('movs r0, #1\nbx lr', { address: 0x1000n, arch: 'thumb' });
213
+
214
+ // 大端 ARM
215
+ assembleToBytes('mov r0, r1', { address: 0x1000n, arch: 'arm', endianness: 'big' });
216
+ ```
217
+
218
+ > **数字进制**:汇编器里裸数字是**十进制**,十六进制写 `0x100` 或 `100h`
219
+ > (CE 帮助文档里十六进制一律带 `h` 后缀,与 GNU as 一致,Keystone 也支持 `h`)。
220
+ > 但 `db`/`dw`/`dd`/`dq` 的裸数字**是十六进制**(`db 89 01` 就是 `0x89 0x01`)。
221
+ > 这两套约定不同,别混。
222
+
223
+ ### Lua 里调用 Auto Assembler
224
+
225
+ ```js
226
+ const { LuaEngine } = require('node-cheat-engine');
227
+
228
+ const lua = new LuaEngine();
229
+
230
+ // 无进程时自动进入"只检查"模式;因此示例用绝对地址
231
+ // (alloc 块名标签需要真实进程才能解析,见下方 Auto Assembler 一节)
232
+ await lua.doString(`
233
+ local ok = autoAssemble([[
234
+ [ENABLE]
235
+ define(hpAddr, 00401000)
236
+ hpAddr:
237
+ mov dword ptr [rbp-4], 9999
238
+ ret
239
+ ]])
240
+ if not ok then print("失败: " .. tostring(autoAssembleError())) end
241
+ `);
242
+
243
+ // 有进程时可用 alloc / aobscan / 模块相对地址
244
+ lua.attachProcess(require('node-cheat-engine').Process.open(1234));
245
+ await lua.doString(`
246
+ local ok = autoAssemble([[
247
+ [ENABLE]
248
+ aobscan(hp, 89 5D FC 8B 45 08)
249
+ alloc(newmem, 256)
250
+ label(returnhere)
251
+ newmem:
252
+ mov dword ptr [rbp-4], 9999
253
+ jmp returnhere
254
+ returnhere:
255
+ ]])
256
+ if not ok then print("失败: " .. tostring(autoAssembleError())) end
257
+ `);
258
+ lua.dispose();
259
+ ```
260
+
261
+ `autoAssemble(script [, enable])` 与 `autoAssembleCheck(script [, enable])`
262
+ 返回布尔值(不抛 Lua 错误),失败详情用 `autoAssembleError()` 取(含行号)。
263
+
264
+ ### Auto Assembler(CE 脚本)
265
+
266
+ `autoAssemble` 是**独立函数**,脚本内容与进程都通过参数传入:
267
+
268
+ ```js
269
+ const { Process, autoAssemble, ensureAsmBackend } = require('node-cheat-engine');
270
+
271
+ await ensureAsmBackend();
272
+ const proc = Process.openByName('notepad.exe');
273
+
274
+ const script = `
275
+ [ENABLE]
276
+ aobscanmodule(hp, notepad.exe, 89 5D FC 8B 45 08)
277
+ alloc(newmem, 256)
278
+ label(returnhere)
279
+
280
+ newmem:
281
+ mov dword ptr [rbp-4], 9999
282
+ jmp returnhere
283
+
284
+ "notepad.exe"+1234:
285
+ jmp newmem
286
+ returnhere:
287
+
288
+ [DISABLE]
289
+ "notepad.exe"+1234:
290
+ mov [rbp-4], ebx
291
+ `;
292
+
293
+ // 写入([ENABLE] 段)
294
+ const on = autoAssemble(script, { process: proc, enable: true });
295
+ console.log('分配:', on.allocations, '写入条目:', on.entries.length);
296
+
297
+ // 还原([DISABLE] 段):用 ENABLE 前记录的原始字节精确还原
298
+ autoAssemble(script, { process: proc, enable: false });
299
+
300
+ proc.close();
301
+ ```
302
+
303
+ **只做语法检查**(不分配、不写内存)——省略 `process` 即自动进入该模式:
304
+
305
+ ```js
306
+ // 注意:无进程时无法求值地址表达式、也无法真正 alloc,
307
+ // 因此脚本必须用**绝对数字地址**定位,不能用 "mod.exe"+xxx 或 alloc 块名。
308
+ const checked = autoAssemble(`
309
+ [ENABLE]
310
+ define(hpAddr, 00401000)
311
+ hpAddr:
312
+ mov dword ptr [rbp-4], 9999
313
+ `, { syntaxCheckOnly: true });
314
+
315
+ console.log('语法检查通过,将写入', checked.entries.length, '处');
316
+ ```
317
+
318
+ 支持:`[ENABLE]`/`[DISABLE]`、`alloc`/`dealloc`/`realloc`/`globalalloc`、
319
+ `label`/`define`/`registersymbol`、`aobscan`(+`module`/`region`/`unique`)、
320
+ `createthread`(仅参数校验,Node 无 `CreateRemoteThread`)、`include`、
321
+ `db`/`dw`/`dd`/`dq`、`nop`/`align`、`readmem`/`loadbinary`。
322
+
323
+ **关键机制**:`[ENABLE]` 写入前逐字节记录原始字节,`[DISABLE]` 逆序精确还原——
324
+ 即使 `[DISABLE]` 段为空也能还原。失败时 `AutoAssemblerError` 会带**行号**。
325
+ 详见 [docs/autoassembler.md](docs/autoassembler.md)。
326
+
327
+ > 注意:引擎层的 Auto Assembler 已完整可用,但**尚未包成 Lua 全局函数**
328
+ > `autoAssemble` / `autoAssembleCheck`,因此从 Lua 脚本里还调不到。
329
+
330
+ ### 切换到原生 CE 后端
331
+
332
+ ```js
333
+ const { NativeCeBackend, openProcessAsync } = require('node-cheat-engine');
334
+
335
+ const backend = new NativeCeBackend({ host: '127.0.0.1', port: 52736 });
336
+ console.log('ceserver:', await backend.getVersion());
337
+
338
+ const proc = await openProcessAsync(1234, { backend });
339
+ const buf = await proc.readMemoryAsync(0x7ff000001000n, 16); // 注意 await
340
+
341
+ await proc.closeAsync();
342
+ await backend.dispose();
343
+ ```
344
+
345
+ 两种后端的对比、以及**为什么原生 CE 后端必须是异步的**(附最小复现),
346
+ 见 [docs/backends.md](docs/backends.md)。
347
+
348
+ ### 第三种:DBK 内核驱动后端
349
+
350
+ 还有一个 `DbkBackend`,通过 CE 自己的 DBK 内核驱动 ioctl 读写 ——
351
+ 这是**读受保护进程(PPL/反作弊)的唯一途径**(用户态连句柄都打不开)。
352
+
353
+ ```js
354
+ const { DbkBackend, Process } = require('node-cheat-engine');
355
+
356
+ const backend = new DbkBackend({ cheatEngineDir: 'C:\\Program Files\\Cheat Engine' });
357
+ console.log(backend.probe()); // 设备能否打开、驱动版本
358
+
359
+ const proc = Process.open(1245, { backend });
360
+ proc.readMemory(0x7ff000001000n, 64); // ← 走内核 ioctl
361
+ ```
362
+
363
+ **前提**:驱动必须先加载(需要管理员权限;可用 DSH 插件的 `ce_kernel` 工具装)。
364
+
365
+ > ⚠️ **诚实说明**:这个后端的 ioctl 码与输入结构是**逐行核对 CE 源码**
366
+ > (`DBK32functions.pas`)实现的,但**驱动侧实际期待的结构布局未经真机验证** ——
367
+ > 因为验证前提是加载驱动,而那需要管理员权限。
368
+ > 协议细节、已知风险与排查方法见 [docs/backend-dbk.md](docs/backend-dbk.md)。
369
+ > 提供 `probe()` 让你能在装好驱动的机器上确认连通性。
370
+
371
+ ## 两条调用路径怎么选
372
+
373
+ | 场景 | 选择 |
374
+ | --- | --- |
375
+ | 想开箱即用、依赖最少 | `SelfBackend`(默认,同步) |
376
+ | 需要复用已部署的原生 CE / ceserver | `NativeCeBackend`(异步) |
377
+ | 目标进程受保护(反作弊/PPL) | `DbkBackend`(需要内核驱动 + 管理员);或提权后的 ceserver |
378
+ | 想写一套代码适配两者 | 统一用 `*Async` 方法 + `await`(对两种后端都安全) |
379
+
380
+ ## 开发
381
+
382
+ ```bash
383
+ npm install
384
+ npm run build # 编译到 dist/
385
+ npm test # 169 个测试,全部真实跨进程运行
386
+ npm run smoke # 16 项冒烟检查(人类可读输出)
387
+ ```
388
+
389
+ 测试文件:
390
+
391
+ | 文件 | 覆盖 | 用例 |
392
+ | --- | --- | --- |
393
+ | `test/scanner.test.js` | 扫描选项、类型、并行一致性、指针扫描、matcher 纯函数 | 21 |
394
+ | `test/asm.test.js` | 反汇编/汇编(5 种架构)、动态语义往返、端到端写入目标进程 | 39 |
395
+ | `test/autoassembler.test.js` | AA 全指令、ENABLE/DISABLE 还原、行号错误 | 45 |
396
+ | `test/cheattable.test.js` | 地址表达式、MemoryRecord、地址表、`.CT` 往返 | 7 |
397
+ | `test/backend.test.js` | 两个后端、ceserver 协议逐字段核对 | 20 |
398
+ | `test/dbk-backend.test.js` | DBK 内核驱动后端:ioctl 码、packed 结构、设备名解析、分配、错误路径 | 24 |
399
+ | `test/lua-bridge.test.js` | Lua 桥、int64 精度、AOBScan、autoAssemble、错误处理 | 13 |
400
+
401
+ 测试目标是 `test/fixture-target.js` —— 一个用 `VirtualAlloc` 分配可预测内存的
402
+ 真实子进程(含固定偏移的各类变量、指针链、AOB 特征),
403
+ 因此所有断言都是**真实跨进程**验证,而不是内存内的模拟。
404
+
405
+ ## 架构
406
+
407
+ ```
408
+ src/
409
+ types.ts 变量类型/扫描选项等枚举(对齐 memscan.pas)
410
+ value-codec.ts 值编解码、roundTo、AOB 解析
411
+ backend/ 内存后端抽象
412
+ types.ts MemoryBackend 接口(同步/异步)
413
+ self-backend.ts 自研后端(Win32 直调,同步)
414
+ native-ce-backend.ts 原生 CE 后端(ceserver TCP,异步)
415
+ win/
416
+ winapi.ts koffi 绑定
417
+ process.ts 后端无关的 Process 门面
418
+ scan-worker.ts 并行扫描 worker
419
+ scanner/
420
+ matcher.ts 比较规则编译与块扫描(对齐 TScanner)
421
+ scanner.ts ScanSession(对齐 TMemScan/TScanController)
422
+ shared-layout.ts 跨线程共享内存布局
423
+ pointer-scan.ts 指针扫描
424
+ addr/expression.ts 地址表达式解析
425
+ asm/ 反汇编与汇编
426
+ backend.ts Capstone/Keystone 封装 + 精度守卫
427
+ disassembler.ts 反汇编,字段对齐 TLastDisassembleData
428
+ assembler.ts 汇编,符号解析 + 逐行地址映射
429
+ autoassembler.ts Auto Assembler(alloc/label/aobscan/[ENABLE])
430
+ addresslist/memory-record.ts 地址表与内存记录
431
+ cheattable/ct-format.ts .CT 读写
432
+ lua/ Lua 脚本桥(61 个 CE API)
433
+ index.ts 公共 API 入口
434
+ ```
435
+
436
+ ## 已知限制
437
+
438
+ - 仅支持 Windows x64(依赖 Win32 原生 API)
439
+ - **无内核驱动**:受保护进程(反作弊、PPL)读不到 —— 这是与 CE 未加载驱动时相同的限制
440
+ - **无 GUI**:只提供类库(按设计要求,不做窗口化)
441
+ - **ARM 的 ThumbEE/Jazelle 与 ARM64 的 SVE2 全指令集**未覆盖
442
+ (反汇编/汇编后端支持到哪就是哪)
443
+ - **无反汇编视图级别的注释**:CE 的 `disassemblerComments.pas`(433 行,
444
+ 给 `call`/`mov` 自动加注释)未移植
445
+ - 原生 CE 后端**必须异步**,且需要自行准备 `ceserver`
446
+ - 首次扫描会把命中块的原始内存留在快照里(默认上限 1.5GB),
447
+ 用于「再次扫描」的旧值比较;不需要时可调小 `maxSnapshotBytes`
448
+ - 反汇编/汇编需要 `await ensureAsmBackend()`(WASM 异步初始化)
449
+
450
+ ## 许可
451
+
452
+ MIT。本移植为独立实现;Cheat Engine 本身版权归其作者,见原仓库 `LICENSE`。