异环 (NTE) macOS 上的钓鱼 / 麻将工具。Frida 读取游戏内存,通过 CGEvent 或 frida widget 回调下发输入。
- 钓鱼:全自动。挂上之后从抛竿、提竿、minigame 拉鱼到关结算页全部自己处理,不需要任何人工介入。
- 麻将:半自动。事件能识别(摸/打/换三张/听牌等),缺一门按钮自动按,但换三张选 3 张牌、平时出牌、碰/胡/杠都需要手动点。
cd htbot
pip install -r requirements.txt依赖:frida 16+(已在 17 上验证)、pyobjc-framework-Quartz、mss、numpy。Python 3.10 还需要 pip install tomli。
第一次运行先复制配置模板:
cp config.example.toml config.toml合成按键需要 Accessibility 权限。打开"系统设置 → 隐私与安全性 → 辅助功能",把运行 Python 的终端(Terminal / iTerm / VS Code 等)加进去并启用,然后重启终端。
钓鱼有 6 个状态: NONE → SELECTPOINT → THROWROD → BAIT → PLAYER_SURE → THROWUP,然后回到 SELECTPOINT 循环。状态从 AHTPlayerState 内存里的 EFishingType 字段读。
每次启动游戏,actor 地址都会因 ASLR 重新随机化。但只需要在第一次进入钓鱼场景时抓一次 — 探针装了常驻 hook 在 OnTick 上,实例如果被销毁重建会自动追踪到新地址。
进游戏走到钓鱼点旁,运行:
python -m utils.find_actors fishing回游戏按 F 进钓点。终端几秒内会输出:
✓ player_state @ 0x...
✓ fish_main @ 0x...
地址自动写回 config.toml,工具自动退出。保持游戏运行,不要关。
#点击开始钓鱼后运行
python -m htbot --no-dry-run --max-sec 1800正常一轮的日志大致是:
state NONE -> SELECTPOINT
SELECTPOINT 抛竿 (延迟 437ms)
state SELECTPOINT -> THROWROD
state THROWROD -> BAIT
BAIT 反应 187ms 后提竿
state BAIT -> PLAYER_SURE
minigame interval=BELOW → 按 D
minigame interval=IN_INTERVAL → 静止
minigame interval=APPROACHING → 按 A
state PLAYER_SURE -> THROWUP
钓上 1 条! 累计 1 条
不依赖图像识别,而是直接调用游戏内的 UHTUI_FishMain::RodIsInFishInterval() 取真值。返回 0/1/2/3 对应 NO_FISH / IN_INTERVAL / 鱼钩偏左 / 鱼钩偏右,分别处理为释放 / 按 D 拉右 / 按 A 拉左。
但这只是 4 态粗信号,鱼钩会停在区间边缘 — 一进区间就松手,残留动量越过中点又出去,然后反向拉回,反复横跳。所以在此基础上又读取 rod 和 interval 的连续 X 坐标,瞄区间中点,加 Schmitt 双阈值:中心 ±4% 内才松手,偏移超过 14% 才按方向键,中间过渡区维持上一次动作。这样自然摇晃不会触发反复 press / release。
[fishing.input]
mode = "callback" # 默认callback 通过 frida RPC 直接调用游戏内 UMG 按钮的 C++ 回调函数,不经过 macOS 输入子系统。游戏窗口可以放在副屏、缩到角落、切到另一个 Space,都不影响。唯一不行的是把游戏完全最小化到 Dock — UE 会暂停 tick,什么都不跑。锁屏 / 系统睡眠同理。
mode = "key" 是另一个选项,走 CGEvent 合成键盘事件,要求游戏在前台。除非 frida 那条路出问题,否则没必要切。
钓上鱼会弹一个"钓鱼成功"结算页(显示鱼名 + 经验)。callback 模式下不能用 CGEvent 点屏幕中央来关掉,所以加了专门的 hook:从 UHTUI_FishingSuccess::SetFishingResult 触发的实例捕获 widget 指针,然后调用它的 OnCloseBtnClick。key 模式下直接 mouse_click 屏幕中央即可。
异环游戏内的"小小雀魂"模式,底层实际是川麻血流 (AHTChuanMahjong* 系列),不是雀魂式日麻。所以没有"吃",有"缺一门"和"换三张"。
| 阶段 | 状态 |
|---|---|
| 摸牌跟踪 | 自动累加到本地手牌 |
| 牌河观察 | 自动 |
| 换三张:决策选哪门 + 推荐给哪 3 张 | 决策出来打日志,3 张牌需手动点 |
| 缺一门按钮 | 换三张事件后 30 秒自动点 |
| 听牌提示 | 显示日志 |
| 鸣牌响应(碰 / 胡 / 杠 / 过) | 手动 |
| 平时出牌 | 手动(bot 给推荐) |
进对局之前或大厅都可以(Tick hook 会刷新到当前 actor),运行:
python -m utils.find_actors mahjong抓 table_actor_addr + ui_form_addr 写回 config。然后把 [mahjong] enabled = true,启动:
python -m htbot mahjong --no-dry-run --max-sec 600进对局后,起手 13 张牌都会被记录。换三张阶段 bot 会建议选哪门 — 自己点 3 张 + 确认,30 秒后 bot 自动按对应的缺一门按钮。之后正常出牌,每次轮到自己时 bot 推荐打哪张,手动点对应的牌即可。Hu / 碰 / 杠 只在听牌提示阶段有日志输出,实际按钮需要手动点。
python -m htbot [mode] [options]
mode fish | mahjong | auto (默认读 config.toml)
--once 一次性快照,打印后退出
--dry-run 强制 dry-run,只打日志不发输入
--no-dry-run 关闭 dry-run,真发输入
--no-frida 禁用 frida(纯视觉,minigame 没了 frida 决策几乎不可用)
--max-sec N 会话上限秒数,默认 7200
--log-level DEBUG / INFO / WARNING / ERROR
python -m utils.find_actors fishing # 抓钓鱼 actor 地址,自动写 config
python -m utils.find_actors mahjong # 抓麻将 actor (table + ui_form)
python -m utils.probe_fishing_state # 钓鱼状态字段诊断
python -m utils.test_input # 测合成键鼠 (调试 Accessibility 权限)
state 一直显示 NONE。重启游戏后 ASLR 让旧地址失效,重新跑 find_actors fishing。
attach 异环 失败。frida 没连上。先 frida-ps | grep 异环 确认进程在。如果是 macOS 权限问题,可能需要禁用 SIP 或使用签名过的 frida-server。最后兜底是 --no-frida 走纯视觉,但钓鱼意义不大。
F / A / D 不响应游戏。Accessibility 权限没给。系统设置 → 隐私与安全性 → 辅助功能,加上你的终端,重启终端后再跑。
A / D 在控制角色而不是鱼钩。当前不在 PLAYER_SURE 状态(还没进 minigame),或游戏窗口不在前台(key 模式下要前台,可改用 callback 模式绕开)。
SELECTPOINT 反复"卡住"。鱼饵用完了,手动补充。
Frida 17 报 Memory.readU8 not a function。旧 API 在 17 移除,本仓库已全部改成 instance method (ptr.readU8())。如果还报错,拉最新版。
find_actors 抓到 0x100 这种垃圾值。已加 sanity check 拒绝 < 4GB 的指针,重跑即可。
pyobjc-framework-CoreGraphics 未找到。PyObjC 没这个独立包,已从 requirements.txt 移除。pip install -r requirements.txt 重装即可(CoreGraphics 包含在 pyobjc-framework-Quartz 中)。
之前做过一轮 audit:客户端没有反 Frida / 反 hook / OS 自省检测,钓鱼麻将均不触发已知的 game-state tripwire。但服务端有行为分析,所以 humanize 模块给所有动作加了高斯抖动 — 反应延迟、按键时长、随机分神、会话节奏。
htbot/
README.md
config.example.toml
config.toml # find_actors 自动填地址
requirements.txt
htbot/
__main__.py # CLI 入口
core/ # 共享:frida bridge / 输入 / vision / humanize / config / 主调度
modules/
fishing/ # 状态机 + minigame 闭环 + callback 输入
mahjong/ # 事件驱动 + 半自动
frida_scripts/
fishing_probe.js
mahjong_probe.js
utils/ # 抓地址 / 诊断脚本
仅在 macOS 上验证过 — input 走 CGEvent + pyobjc。Linux / Windows 需要重写 input 层。
每次游戏更新可能让 frida 脚本里的函数地址 / 字段偏移失效,要重新 nm 一遍并修改 frida_scripts/*.js 里的常量。config.toml 里的 actor 地址不需要动(只是初始 seed,自动追踪会刷新)。
游戏完全最小化到 Dock 时 UE 暂停 tick,所有逻辑停止运行。需要后台跑就用小窗或副屏。