diff --git a/repo/js/AccountSwitchStateMachine/README.md b/repo/js/AccountSwitchStateMachine/README.md new file mode 100644 index 000000000..c12b605f9 --- /dev/null +++ b/repo/js/AccountSwitchStateMachine/README.md @@ -0,0 +1,225 @@ +# 账号切换工具使用说明 + +## 快速开始 + +### 方式一:账号图片识别(推荐) + +1. **准备账号图片** + - **推荐方式**:使用脚本的"截图模式"自动获取并保存截图(详见截图模式部分) + - **手动方式**:在 `accounts/` 目录下放置账号截图 + - 图片命名为 `{UID}.png`,如 `284073800.png` + - 截图应包含账号选择界面中该账号的明显标识 + +2. **配置脚本** + - 打开BetterGI配置面板 + - 填写目标账号UID + - 确保"跳过搜索直接输入账号密码"未勾选 + +3. **运行脚本** + - 脚本会自动定位到账号选择界面 + - 识别并点击对应账号图片 + - 自动进入游戏并返回主界面 + +### 方式二:账号密码输入 + +1. **配置脚本** + - 打开BetterGI配置面板 + - 填写目标账号UID(用于通知显示) + - 填写账号和密码 + - 勾选"跳过搜索直接输入账号密码" + +2. **运行脚本** + - 脚本会自动进入账号密码输入界面 + - 自动点击同意按钮、输入账号密码 + - 自动进入游戏并返回主界面 + +### 说明 +- **两种方式可同时使用**:方式二作为方式一的补充,当图片识别失败时会自动回退到账号密码方式 +- **推荐配置**:同时填写账号密码作为备用,提高切换成功率 + +## 截图模式 + +截图模式是一个辅助功能,用于自动获取并保存账号图片,方便后续使用账号图片识别方式进行切换。 + +**使用步骤**: +1. **配置脚本** + - 打开BetterGI配置面板 + - 填写目标账号UID(优先使用,用于命名图片) + - 勾选"截图模式" + +2. **运行脚本** + - 脚本会优先使用配置中的"要切换到的账号UID"命名图片 + - 如果未填写该项,会尝试自动识别当前账号的UID + - 导航到账号选择界面 + - 自动截图并保存账号图片 + - 返回主界面并发送成功通知 + +**保存位置**: +- 图片保存在 `accounts/` 目录下 +- 文件名格式为 `{UID}.png` + +## 配置说明 + +在BetterGI的配置面板中进行以下设置: + +### 1. 目标账号UID +- 输入要切换到的账号UID +- 用于识别账号图片文件名、通知显示和截图模式命名 + +### 2. 账号(可选) +- 当无法通过图片识别找到账号时备用 +- 建议填写,以防图片识别失败 + +### 3. 密码(可选) +- 当无法通过图片识别找到账号时备用 +- 建议填写,以防图片识别失败 + +### 4. 跳过搜索直接输入账号密码 +- **勾选**:不尝试查找账号图片,直接使用账号密码登录 +- **不勾选**:优先尝试通过账号图片识别切换,失败时回退到账号密码方式 + +### 5. UID校验 +- **勾选**:在切换前后检查当前账号UID,辅助判断切换是否成功 +- **不勾选**:不进行UID校验 + +### 6. 截图模式 +- **勾选**:进入截图模式,自动截图保存对应UID的账号图片 +- **不勾选**:正常执行账号切换流程 + +## 常见问题 + +### Q: 账号图片识别失败怎么办? +A: 确保: +1. 图片存放在 `accounts/` 目录下 +2. 图片命名为正确的UID格式 +3. 截图清晰,包含账号的明显标识 +4. 同时填写账号密码作为备用方案 + +### Q: 切换失败后会怎样? +A: 脚本会尝试返回主界面,并发送失败通知。如果配置了账号密码,建议手动检查账号状态。 + +### Q: 为什么需要填写UID? +A: UID用于: +1. 确定账号图片文件名 +2. 在通知中显示切换的账号信息 +3. 截图模式下命名保存的图片文件 + +## 注意事项 + +1. 首次使用前建议先手动测试一遍切换流程 +2. 确保游戏分辨率为1920x1080 +3. 账号图片建议截取账号选择界面中该账号的清晰截图 +4. 密码输入时不会显示,属于正常现象 +5. 截图模式下,默认使用设置的目标UID,未设置时会尝试自动识别 + +## 问题反馈 + +如果在使用过程中遇到预料之外的弹窗或其他问题,脚本可能无法处理,请按照以下方式反馈: + +1. **截图保存**:截取全屏原图,确保包含完整的游戏界面和弹窗 +2. **联系作者**:QQ号 718135749 +3. **提供信息**:简要描述遇到的问题和操作步骤 + +感谢您的支持! + +
+项目介绍(点击展开) + +## 项目介绍 + +基于状态机实现的自动账号切换脚本,支持通过账号图片识别或手动输入账号密码两种方式切换原神账号。 + +### 功能特性 + +- **双模式切换**:支持账号图片自动识别和账号密码手动输入两种方式 +- **状态机管理**:使用有限状态机管理切换流程,稳定可靠 +- **自动返回**:切换失败时自动尝试返回原账号 +- **通知提醒**:切换成功或失败时发送系统通知 +- **循环防护**:防止状态循环卡死 +- **UID校验**:支持切换前后检查当前账号UID,提高切换准确性 +- **截图模式**:自动截图保存账号图片,方便后续切换 + +### 目录结构 + +``` +AccountSwitchStateMachine/ +├── main.js # 主脚本文件 +├── manifest.json # 脚本清单 +├── settings.json # 配置定义 +├── assets/ +│ ├── states.json # 状态机配置 +│ └── RecognitionObjects/ # 状态识别图片 +│ ├── Paimon.png +│ ├── Menu.png +│ ├── PrepareToLogOut.png +│ ├── SwitchAccount.png +│ ├── Exit.png +│ ├── EnterGame.png +│ ├── LoginOther.png +│ ├── SelectAccount.png +│ ├── EnterAccountAndPassword.png +│ ├── Agree.png +│ ├── EnterAccount.png +│ ├── EnterPassword.png +│ ├── EnterGame2.png +│ ├── Login.png +│ └── Confirm.png +└── accounts/ # 账号图片存放目录 + └── {uid}.png # 以UID命名的账号截图 +``` + +### 工作流程 + +``` +开始 +│ +├─► 截图模式(勾选时) +│ ├─► 使用设置的UID或识别当前UID +│ ├─► 导航到账号选择界面 +│ ├─► 截图保存账号图片 +│ └─► 返回主界面 +│ ├─► 成功 → 发送成功通知 +│ └─► 失败 → 发送失败通知 +│ +├─► 尝试账号图片方式(未勾选跳过时) +│ ├─► 预加载账号图片 +│ ├─► 切换到选择账号界面 +│ ├─► 查找并点击账号图片 +│ └─► 切换到主界面 +│ ├─► 成功 → 发送成功通知 +│ └─► 失败 → 回退到账号密码方式 +│ +└─► 账号密码方式 + ├─► 检查账号密码 + ├─► 切换到输入账号密码界面 + ├─► 点击同意按钮 + ├─► 输入账号密码 + ├─► 点击进入游戏 + └─► 切换到主界面 + ├─► 成功 → 发送成功通知 + └─► 失败 → 尝试返回原账号 → 发送失败通知 +``` + +### 状态说明 + +| 状态名 | 说明 | +|--------|------| +| mainUI | 主界面(派蒙图标) | +| menuUI | 菜单界面(ESC菜单) | +| prepareToLogOut | 预备退出登录界面 | +| loginScreen | 登录界面(切换账号按钮) | +| exitAccount | 退出账号界面 | +| enterGame | 进入游戏或登录其他账号界面 | +| selectAccount | 选择账号界面 | +| enterAccountAndPassword | 输入账号密码界面 | +| noAccount | 无账号登录界面 | + +### 更新日志 + +#### v1.0 +- 初始版本 +- 实现状态机管理 +- 支持账号图片和账号密码两种切换方式 +- 添加自动返回和通知功能 + +
\ No newline at end of file diff --git a/repo/js/AccountSwitchStateMachine/accounts/示例图片.png b/repo/js/AccountSwitchStateMachine/accounts/示例图片.png new file mode 100644 index 000000000..012e420ef Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/accounts/示例图片.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Agree.png b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Agree.png new file mode 100644 index 000000000..394c4a6cf Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Agree.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Confirm.png b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Confirm.png new file mode 100644 index 000000000..96f28ee70 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Confirm.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/EnterAccount.png b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/EnterAccount.png new file mode 100644 index 000000000..d6c7bccf8 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/EnterAccount.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/EnterAccountAndPassword.png b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/EnterAccountAndPassword.png new file mode 100644 index 000000000..db64c11f1 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/EnterAccountAndPassword.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/EnterGame.png b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/EnterGame.png new file mode 100644 index 000000000..1021d2b36 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/EnterGame.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/EnterGame2.png b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/EnterGame2.png new file mode 100644 index 000000000..621ce701b Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/EnterGame2.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/EnterPassword.png b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/EnterPassword.png new file mode 100644 index 000000000..df91475fe Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/EnterPassword.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Exit.png b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Exit.png new file mode 100644 index 000000000..4a874d317 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Exit.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Login.png b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Login.png new file mode 100644 index 000000000..25327a114 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Login.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/LoginOther.png b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/LoginOther.png new file mode 100644 index 000000000..a28f23e45 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/LoginOther.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Menu.png b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Menu.png new file mode 100644 index 000000000..0d41a3cd9 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Menu.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Paimon.png b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Paimon.png new file mode 100644 index 000000000..690f1d079 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/Paimon.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/PrepareToLogOut.png b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/PrepareToLogOut.png new file mode 100644 index 000000000..fe94b0178 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/PrepareToLogOut.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/SelectAccount.png b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/SelectAccount.png new file mode 100644 index 000000000..5550489fc Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/SelectAccount.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/SwitchAccount.png b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/SwitchAccount.png new file mode 100644 index 000000000..b895ab391 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/RecognitionObjects/SwitchAccount.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/states.json b/repo/js/AccountSwitchStateMachine/assets/states.json new file mode 100644 index 000000000..158c1e18e --- /dev/null +++ b/repo/js/AccountSwitchStateMachine/assets/states.json @@ -0,0 +1,412 @@ +[ + { + "name": "mainUI", + "description": "处于主界面", + "transitions": [ + { + "targetState": "menuUI", + "action": "keyPress('VK_ESCAPE'); await sleep(1000);" + } + ], + "detection": { + "conditions": [ + { + "id": "paimon", + "template": "assets/RecognitionObjects/Paimon.png", + "region": { + "x": 0, + "y": 0, + "width": 150, + "height": 150 + } + } + ], + "logic": "paimon" + } + }, + { + "name": "menuUI", + "description": "处于菜单界面", + "transitions": [ + { + "targetState": "mainUI", + "action": "await genshin.returnMainUi();" + }, + { + "targetState": "prepareToLogOut", + "action": "click(50, 1025); await findAndClick('assets/RecognitionObjects/PrepareToLogOut.png', false);" + } + ], + "detection": { + "conditions": [ + { + "id": "menu", + "template": "assets/RecognitionObjects/Menu.png", + "region": { + "x": 0, + "y": 0, + "width": 150, + "height": 150 + } + }, + { + "id": "prepareToLogOut", + "template": "assets/RecognitionObjects/PrepareToLogOut.png", + "region": { + "x": 646, + "y": 488, + "width": 639, + "height": 97 + } + } + ], + "logic": "menu && !prepareToLogOut" + } + }, + { + "name": "prepareToLogOut", + "description": "预备退出登录", + "transitions": [ + { + "targetState": "menuUI", + "action": "await findAndClick('assets/RecognitionObjects/PrepareToLogOut.png');" + }, + { + "targetState": "loginScreen", + "action": "await findAndClick('assets/RecognitionObjects/PrepareToLogOut.png'); await sleep(500); await findAndClick('assets/RecognitionObjects/SwitchAccount.png', false, 30000);" + } + ], + "detection": { + "conditions": [ + { + "id": "prepareToLogOut", + "template": "assets/RecognitionObjects/PrepareToLogOut.png", + "region": { + "x": 646, + "y": 488, + "width": 639, + "height": 97 + } + } + ], + "logic": "prepareToLogOut" + } + }, + { + "name": "loginScreen", + "description": "登录界面", + "transitions": [ + { + "targetState": "exitAccount", + "action": "await findAndClick('assets/RecognitionObjects/SwitchAccount.png');" + }, + { + "targetState": "mainUI", + "action": "click(10, 10); let attempts = 0; const maxAttempts = 30; while (!await findAndClick('assets/RecognitionObjects/Paimon.png', false, 200) && attempts < maxAttempts) { attempts++; await sleep(5000); click(10, 10); }" + } + ], + "detection": { + "conditions": [ + { + "id": "switchAccount", + "template": "assets/RecognitionObjects/SwitchAccount.png", + "region": { + "x": 1783, + "y": 940, + "width": 100, + "height": 100 + } + }, + { + "id": "exit", + "template": "assets/RecognitionObjects/Exit.png", + "region": { + "x": 911, + "y": 600, + "width": 341, + "height": 173 + } + }, + { + "id": "confirm", + "template": "assets/RecognitionObjects/Confirm.png", + "region": { + "x": 734, + "y": 826, + "width": 439, + "height": 116 + } + } + ], + "logic": "switchAccount && !exit && !confirm" + } + }, + { + "name": "exitAccount", + "description": "退出账号界面", + "transitions": [ + { + "targetState": "enterGame", + "action": "await findAndClick('assets/RecognitionObjects/Exit.png');" + } + ], + "detection": { + "conditions": [ + { + "id": "exit", + "template": "assets/RecognitionObjects/Exit.png", + "region": { + "x": 911, + "y": 600, + "width": 341, + "height": 173 + } + }, + { + "id": "confirm", + "template": "assets/RecognitionObjects/Confirm.png", + "region": { + "x": 734, + "y": 826, + "width": 439, + "height": 116 + } + } + ], + "logic": "exit && !confirm" + } + }, + { + "name": "enterGame", + "description": "进入游戏或登录其他账号", + "transitions": [ + { + "targetState": "selectAccount", + "action": "click(974,496);" + }, + { + "targetState": "enterAccountAndPassword", + "action": "await findAndClick('assets/RecognitionObjects/LoginOther.png');" + }, + { + "targetState": "loginScreen", + "action": "await findAndClick('assets/RecognitionObjects/EnterGame.png');" + } + ], + "detection": { + "conditions": [ + { + "id": "enterGame", + "template": "assets/RecognitionObjects/EnterGame.png", + "region": { + "x": 643, + "y": 565, + "width": 652, + "height": 186 + } + }, + { + "id": "loginOther", + "template": "assets/RecognitionObjects/LoginOther.png", + "region": { + "x": 643, + "y": 565, + "width": 652, + "height": 186 + } + }, + { + "id": "confirm", + "template": "assets/RecognitionObjects/Confirm.png", + "region": { + "x": 734, + "y": 826, + "width": 439, + "height": 116 + } + } + ], + "logic": "enterGame && loginOther && !confirm" + } + }, + { + "name": "selectAccount", + "description": "选择账号界面", + "transitions": [ + { + "targetState": "enterGame", + "action": "click(957, 404);" + } + ], + "detection": { + "conditions": [ + { + "id": "enterGame", + "template": "assets/RecognitionObjects/EnterGame.png", + "region": { + "x": 643, + "y": 565, + "width": 652, + "height": 186 + } + }, + { + "id": "loginOther", + "template": "assets/RecognitionObjects/LoginOther.png", + "region": { + "x": 643, + "y": 565, + "width": 652, + "height": 186 + } + }, + { + "id": "selectAccount", + "template": "assets/RecognitionObjects/SelectAccount.png", + "region": { + "x": 642, + "y": 441, + "width": 644, + "height": 580 + } + }, + { + "id": "confirm", + "template": "assets/RecognitionObjects/Confirm.png", + "region": { + "x": 734, + "y": 826, + "width": 439, + "height": 116 + } + } + ], + "logic": "!enterGame && !loginOther && selectAccount && !confirm" + } + }, + { + "name": "enterAccountAndPassword", + "description": "输入账号密码界面", + "transitions": [ + { + "targetState": "noAccount", + "action": "click(1260, 259);" + } + ], + "detection": { + "conditions": [ + { + "id": "enterAccountAndPassword", + "template": "assets/RecognitionObjects/EnterAccountAndPassword.png", + "region": { + "x": 0, + "y": 0, + "width": 1920, + "height": 1080 + } + }, + { + "id": "confirm", + "template": "assets/RecognitionObjects/Confirm.png", + "region": { + "x": 734, + "y": 826, + "width": 439, + "height": 116 + } + } + ], + "logic": "enterAccountAndPassword && !confirm" + } + }, + { + "name": "ageConfirmation", + "description": "适龄提示界面", + "transitions": [ + { + "targetState": "noAccount", + "action": "await findAndClick('assets/RecognitionObjects/Confirm.png');" + } + ], + "detection": { + "conditions": [ + { + "id": "confirm", + "template": "assets/RecognitionObjects/Confirm.png", + "region": { + "x": 734, + "y": 826, + "width": 439, + "height": 116 + } + } + ], + "logic": "confirm" + } + }, + { + "name": "noAccount", + "description": "无账号登录界面", + "transitions": [ + { + "targetState": "enterGame", + "action": "await findAndClick('assets/RecognitionObjects/Login.png');" + } + ], + "detection": { + "conditions": [ + { + "id": "login", + "template": "assets/RecognitionObjects/Login.png", + "region": { + "x": 1776, + "y": 934, + "width": 100, + "height": 100 + } + }, + { + "id": "enterGame", + "template": "assets/RecognitionObjects/EnterGame.png", + "region": { + "x": 643, + "y": 565, + "width": 652, + "height": 186 + } + }, + { + "id": "selectAccount", + "template": "assets/RecognitionObjects/SelectAccount.png", + "region": { + "x": 642, + "y": 441, + "width": 644, + "height": 580 + } + }, + { + "id": "enterAccountAndPassword", + "template": "assets/RecognitionObjects/EnterAccountAndPassword.png", + "region": { + "x": 0, + "y": 0, + "width": 1920, + "height": 1080 + } + }, + { + "id": "confirm", + "template": "assets/RecognitionObjects/Confirm.png", + "region": { + "x": 734, + "y": 826, + "width": 439, + "height": 116 + } + } + ], + "logic": "login && !enterGame && !selectAccount && !enterAccountAndPassword && !confirm" + } + } +] diff --git a/repo/js/AccountSwitchStateMachine/assets/uid图片/0.png b/repo/js/AccountSwitchStateMachine/assets/uid图片/0.png new file mode 100644 index 000000000..aefdf65e5 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/uid图片/0.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/uid图片/1.png b/repo/js/AccountSwitchStateMachine/assets/uid图片/1.png new file mode 100644 index 000000000..10f9297d7 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/uid图片/1.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/uid图片/2.png b/repo/js/AccountSwitchStateMachine/assets/uid图片/2.png new file mode 100644 index 000000000..1f893f481 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/uid图片/2.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/uid图片/3.png b/repo/js/AccountSwitchStateMachine/assets/uid图片/3.png new file mode 100644 index 000000000..f93f4f2b8 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/uid图片/3.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/uid图片/4.png b/repo/js/AccountSwitchStateMachine/assets/uid图片/4.png new file mode 100644 index 000000000..00a9a5f0f Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/uid图片/4.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/uid图片/5.png b/repo/js/AccountSwitchStateMachine/assets/uid图片/5.png new file mode 100644 index 000000000..31506f2ef Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/uid图片/5.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/uid图片/6.png b/repo/js/AccountSwitchStateMachine/assets/uid图片/6.png new file mode 100644 index 000000000..0a3f9e869 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/uid图片/6.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/uid图片/7.png b/repo/js/AccountSwitchStateMachine/assets/uid图片/7.png new file mode 100644 index 000000000..51e92ee50 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/uid图片/7.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/uid图片/8.png b/repo/js/AccountSwitchStateMachine/assets/uid图片/8.png new file mode 100644 index 000000000..fd404f0e2 Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/uid图片/8.png differ diff --git a/repo/js/AccountSwitchStateMachine/assets/uid图片/9.png b/repo/js/AccountSwitchStateMachine/assets/uid图片/9.png new file mode 100644 index 000000000..48483735d Binary files /dev/null and b/repo/js/AccountSwitchStateMachine/assets/uid图片/9.png differ diff --git a/repo/js/AccountSwitchStateMachine/main.js b/repo/js/AccountSwitchStateMachine/main.js new file mode 100644 index 000000000..8af4ba5a5 --- /dev/null +++ b/repo/js/AccountSwitchStateMachine/main.js @@ -0,0 +1,1164 @@ +// 全局状态机配置对象 +let stateMachineConfig = null; + +// 全局游戏画面区域 +let gameRegion = null; + +// 主逻辑 +(async function () { + // 只有当未开启截图模式时,才检查 verifyUid 和 targetUid + if (!settings.screenshotMode && settings.verifyUid && !settings.targetUid) { + const msg = '已启用UID校验,但未填写目标UID,请检查配置'; + log.error(msg); + notification.error(msg); + return; + } + // 读取状态配置文件 + let statesData; + try { + const statesJsonText = file.readTextSync('assets/states.json'); + statesData = JSON.parse(statesJsonText); + } catch (e) { + log.error(`读取 states.json 失败:${e.message},状态机无法正常工作`); + throw e; + } + + if (!statesData || !Array.isArray(statesData)) { + const errorMsg = 'states.json 格式错误:应为状态对象数组'; + log.error(errorMsg); + throw new Error(errorMsg); + } + + // 将状态数组转换为对象,以状态名称为key + stateMachineConfig = {}; + for (const state of statesData) { + stateMachineConfig[state.name] = state; + } + log.info(`成功加载 ${statesData.length} 个状态配置`); + + // 检查是否进入截图模式 + if (settings.screenshotMode) { + await handleScreenshotMode(); + return; + } + + // 检查是否需要UID校验 + if (settings.verifyUid) { + log.info('开始:验证当前账号UID'); + const currentUid = await verifyCurrentUid(); + if (isUidMatch(currentUid, settings.targetUid)) { + log.info(`当前账号UID ${currentUid} 与目标UID ${settings.targetUid} 匹配,无需切换`); + notification.Send(`当前账号UID ${currentUid} 与目标UID ${settings.targetUid} 匹配,无需切换`); + return; + } + } + + // 根据是否开启UID校验决定最大尝试次数 + let attempts = 0; + const maxAttempts = settings.verifyUid ? 2 : 1; + let switchSuccess = false; + let notificationMessage = null; + + while (attempts < maxAttempts) { + attempts++; + log.info(`开始第 ${attempts} 次账号切换尝试`); + + // 用于保存通知消息 + let currentNotificationMessage = null; + let currentSwitchSuccess = false; + + // 判断是否跳过搜索,直接使用账号密码 + let useAccountPassword = settings.skipSearch; + + if (!useAccountPassword) { + // 尝试预加载账号图片模板 + let accountImageMat = null; + const accountImagePath = `accounts/${settings.targetUid}.png`; + + try { + accountImageMat = file.ReadImageMatSync(accountImagePath); + log.info(`成功预加载账号图片:${accountImagePath}`); + } catch (e) { + log.warn(`预加载账号图片失败:${accountImagePath},错误:${e.message}`); + log.warn('将使用账号密码方式登录'); + useAccountPassword = true; + } + + // 如果图片预加载成功,尝试使用图片查找方式 + if (!useAccountPassword) { + // 尝试查找账号图片分支 + log.info('开始:尝试使用账号图片查找方式'); + + // 首先定位到"进入游戏或登录其他账号"状态 + log.info('开始:尝试切换到 enterGame 状态'); + const enterGameResult = await goToState('enterGame'); + if (!enterGameResult) { + log.warn('失败:未能到达 enterGame 状态,将尝试使用账号密码方式'); + useAccountPassword = true; + } else { + log.info('成功到达 enterGame 状态'); + + // 检查当前界面是否存在目标账号图片(适用于只有一个账号的情况) + log.info(`尝试在 enterGame 状态查找账号图片:${accountImagePath}`); + const accountRo = RecognitionObject.TemplateMatch(accountImageMat, 0, 0, 1920, 1080); + accountRo.Threshold = parseFloat(settings.accountImageThreshold) || 0.9; + accountRo.InitTemplate(); + const uidFoundInEnterGame = await findAndClick(accountRo, false, 1000); + + if (uidFoundInEnterGame) { + log.info(`在 enterGame 状态找到账号图片:${settings.targetUid}.png`); + // 点击进入游戏按钮 + await findAndClick('assets/RecognitionObjects/EnterGame.png', true, 1000); + + // 定位到主界面 + log.info('开始:尝试切换到 mainUI 状态'); + const mainUIResult = await goToState('mainUI'); + if (mainUIResult) { + log.info('成功到达 mainUI 状态,账号切换完成'); + currentSwitchSuccess = true; + currentNotificationMessage = `使用账号图片方式成功切换到uid为${settings.targetUid}的账号`; + } else { + log.warn('失败:未能到达 mainUI 状态,将尝试使用账号密码方式'); + useAccountPassword = true; + } + } else { + log.info('在 enterGame 状态未找到账号图片,尝试展开账号列表'); + + // 定位到选择账号界面 + log.info('开始:尝试切换到 selectAccount 状态'); + const selectAccountResult = await goToState('selectAccount'); + if (!selectAccountResult) { + log.warn('失败:未能到达 selectAccount 状态,将尝试使用账号密码方式'); + useAccountPassword = true; + } else { + log.info('成功到达 selectAccount 状态'); + + // 使用预加载的图片模板进行查找 + log.info(`尝试查找并点击账号图片:${accountImagePath}`); + const accountRo = RecognitionObject.TemplateMatch(accountImageMat, 0, 0, 1920, 1080); + accountRo.Threshold = parseFloat(settings.accountImageThreshold) || 0.9; + accountRo.InitTemplate(); + const uidFound = await findAndClick(accountRo, true, 5000); + + if (uidFound) { + log.info(`成功点击账号图片:${settings.targetUid}.png`); + + // 定位到主界面 + log.info('开始:尝试切换到 mainUI 状态'); + const mainUIResult = await goToState('mainUI'); + if (mainUIResult) { + log.info('成功到达 mainUI 状态,账号切换完成'); + currentSwitchSuccess = true; + currentNotificationMessage = `使用账号图片方式成功切换到uid为${settings.targetUid}的账号`; + } else { + log.warn('失败:未能到达 mainUI 状态,将尝试使用账号密码方式'); + useAccountPassword = true; + } + } else { + log.warn(`未找到账号图片:${settings.targetUid}.png,将尝试使用账号密码方式`); + useAccountPassword = true; + } + } + } + } + } + } else { + log.info('已勾选跳过搜索,直接使用账号密码登录'); + } + + // 账号密码分支 + if (useAccountPassword) { + log.info('开始:使用账号密码登录方式'); + + // 检查账号密码是否为空 + if (!settings.account || !settings.password) { + log.error('账号或密码为空,无法使用账号密码方式登录'); + log.error(`账号:${settings.account ? '已设置' : '未设置'},密码:${settings.password ? '已设置' : '未设置'}`); + currentNotificationMessage = '切换失败,账号或密码为空'; + } else { + // 切换到"输入账号密码"状态 + log.info('开始:尝试切换到 enterAccountAndPassword 状态'); + const result = await goToState('enterAccountAndPassword'); + if (!result) { + log.warn('失败:未能到达 enterAccountAndPassword 状态'); + currentNotificationMessage = '切换失败,未能到达输入账号密码界面'; + } else { + log.info('成功到达 enterAccountAndPassword 状态'); + + // 循环点击同意按钮,直到找不到为止 + log.info('开始循环点击同意按钮'); + let agreeFound = true; + while (agreeFound) { + // 创建带阈值的识别对象 + const agreeRo = RecognitionObject.TemplateMatch( + file.ReadImageMatSync('assets/RecognitionObjects/Agree.png'), + 0, 0, 1920, 1080 + ); + agreeRo.Threshold = 0.9; + agreeFound = await findAndClick(agreeRo, true, 1000); + if (agreeFound) { + log.info('点击了同意按钮,继续查找'); + await sleep(500); + } else { + log.info('未找到同意按钮,循环结束'); + } + } + + // 输入账号 + log.info('开始输入账号'); + let accountInputFound = true; + while (accountInputFound) { + accountInputFound = await findAndClick('assets/RecognitionObjects/EnterAccount.png', true, 1000); + if (accountInputFound) { + log.info('点击了账号输入框'); + await sleep(100); + // 输入账号 + inputText(settings.account); + log.info('已输入账号'); + await sleep(500); + // 检查输入框是否还存在(验证输入是否成功) + const checkRo = RecognitionObject.TemplateMatch( + file.ReadImageMatSync('assets/RecognitionObjects/EnterAccount.png'), + 0, 0, 1920, 1080 + ); + let tempRegion = null; + try { + tempRegion = captureGameRegion(); + accountInputFound = tempRegion.find(checkRo).isExist(); + } catch (e) { + log.error(`检查账号输入框时出错:${e.message}`); + accountInputFound = false; + } finally { + if (tempRegion) { + tempRegion.dispose(); + } + } + if (accountInputFound) { + log.warn('账号输入框仍然存在,可能输入失败,重试'); + } else { + log.info('账号输入完成'); + } + } else { + log.warn('未找到账号输入框'); + } + } + + // 输入密码 + log.info('开始输入密码'); + let passwordInputFound = true; + while (passwordInputFound) { + passwordInputFound = await findAndClick('assets/RecognitionObjects/EnterPassword.png', true, 1000); + if (passwordInputFound) { + log.info('点击了密码输入框'); + await sleep(100); + // 输入密码 + inputText(settings.password); + log.info('已输入密码'); + await sleep(500); + // 检查输入框是否还存在(验证输入是否成功) + const checkRo = RecognitionObject.TemplateMatch( + file.ReadImageMatSync('assets/RecognitionObjects/EnterPassword.png'), + 0, 0, 1920, 1080 + ); + let tempRegion = null; + try { + tempRegion = captureGameRegion(); + passwordInputFound = tempRegion.find(checkRo).isExist(); + } catch (e) { + log.error(`检查密码输入框时出错:${e.message}`); + passwordInputFound = false; + } finally { + if (tempRegion) { + tempRegion.dispose(); + } + } + if (passwordInputFound) { + log.warn('密码输入框仍然存在,可能输入失败,重试'); + } else { + log.info('密码输入完成'); + } + } else { + log.warn('未找到密码输入框'); + } + } + + // 点击进入游戏按钮,直到消失 + log.info('开始点击进入游戏按钮'); + let enterGame2Found = true; + while (enterGame2Found) { + enterGame2Found = await findAndClick('assets/RecognitionObjects/EnterGame2.png', true, 1000); + if (enterGame2Found) { + log.info('点击了进入游戏按钮,继续查找'); + await sleep(500); + } else { + log.info('未找到进入游戏按钮,循环结束'); + } + } + + // 切换到主界面 + log.info('开始:尝试切换到 mainUI 状态'); + const mainUIResult = await goToState('mainUI'); + if (mainUIResult) { + log.info('成功到达 mainUI 状态,账号切换完成'); + currentSwitchSuccess = true; + currentNotificationMessage = `使用账号密码方式成功切换到uid为${settings.targetUid}的账号`; + } else { + log.warn('失败:未能到达 mainUI 状态'); + currentNotificationMessage = '切换失败,未能到达主界面'; + } + } + } + } + + // 统一处理:回到主界面 + if (!currentSwitchSuccess) { + log.info('尝试返回主界面'); + const backToMainResult = await goToState('mainUI'); + if (backToMainResult) { + log.info('已返回主界面'); + } else { + log.warn('未能返回主界面'); + } + } + + // 检查是否需要UID校验 + if (settings.verifyUid) { + log.info('开始:验证切换后的账号UID'); + const currentUid = await verifyCurrentUid(); + if (isUidMatch(currentUid, settings.targetUid)) { + log.info(`切换后的账号UID ${currentUid} 与目标UID ${settings.targetUid} 匹配,切换成功`); + currentSwitchSuccess = true; + if (!currentNotificationMessage) { + currentNotificationMessage = `成功切换到uid为${settings.targetUid}的账号`; + } + } else { + log.warn(`切换后的账号UID ${currentUid} 与目标UID ${settings.targetUid} 不匹配,需要重新尝试`); + currentSwitchSuccess = false; + currentNotificationMessage = `切换失败,当前账号UID ${currentUid} 与目标UID ${settings.targetUid} 不匹配`; + } + } + + // 如果切换成功,跳出循环 + if (currentSwitchSuccess) { + switchSuccess = true; + notificationMessage = currentNotificationMessage; + break; + } + + // 如果是最后一次尝试,保存失败消息 + if (attempts === maxAttempts) { + notificationMessage = currentNotificationMessage; + } + } + + // 发送通知 + if (notificationMessage) { + if (switchSuccess) { + notification.Send(notificationMessage); + } else { + notification.error(notificationMessage); + } + } +})(); + +/** + * 判断当前所属状态 + * 使用广度优先遍历(BFS)从上一个状态开始查找,提高查找效率 + * + * @param {string|null} previousState - 上一个状态,null表示首次运行或未知状态 + * @returns {string|null} 当前状态标识,如果无法确定则返回null + */ +async function determineCurrentState(previousState = null) { + if (!stateMachineConfig) { + log.error('状态机配置未加载'); + return null; + } + + // 构建BFS遍历序列 + const stateSequence = buildBFSSequence(previousState); + + // 最多尝试3次 + const maxAttempts = 30; + let currentMousePos = null; // 保存当前鼠标位置,初始为空表示未知 + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + // 遍历所有状态进行匹配 + for (const stateName of stateSequence) { + const state = stateMachineConfig[stateName]; + if (!state || !state.detection) continue; + + // 评估该状态的检测条件,传入当前鼠标位置 + const result = await evaluateDetectionConditions(state.detection, currentMousePos); + if (result.isMatch) { + return stateName; + } + // 更新鼠标位置 + currentMousePos = result.mousePos; + } + } catch (e) { + log.error(`识别状态时出错:${e.message}`); + } + + if (attempt < maxAttempts) { + log.warn(`第 ${attempt} 次尝试未识别到当前状态,1秒后重试...`); + await sleep(1000); + } + } + + log.error(`经过 ${maxAttempts} 次尝试仍无法识别当前状态`); + return null; +} + +/** + * 构建BFS遍历序列 + * 从previousState开始广度优先遍历,然后处理剩余未遍历节点 + * + * @param {string|null} previousState - 起始状态 + * @returns {string[]} 状态遍历序列 + */ +function buildBFSSequence(previousState = null) { + if (!stateMachineConfig) { + log.error('状态机配置未加载'); + return []; + } + + const allStates = Object.keys(stateMachineConfig); + if (allStates.length === 0) return []; + + const visited = new Set(); + const sequence = []; + const queue = []; + + // 如果有指定起始状态,从它开始BFS + if (previousState && stateMachineConfig[previousState]) { + queue.push(previousState); + visited.add(previousState); + } + + // BFS遍历 + while (queue.length > 0) { + const currentState = queue.shift(); + sequence.push(currentState); + + const state = stateMachineConfig[currentState]; + if (state && state.transitions) { + for (const transition of state.transitions) { + const targetState = transition.targetState; + if (targetState && !visited.has(targetState) && stateMachineConfig[targetState]) { + visited.add(targetState); + queue.push(targetState); + } + } + } + } + + // 处理未遍历到的节点(从第一个未遍历节点开始继续BFS) + for (const stateName of allStates) { + if (!visited.has(stateName)) { + // 从这个未遍历节点开始新的BFS + const subQueue = [stateName]; + visited.add(stateName); + + while (subQueue.length > 0) { + const currentState = subQueue.shift(); + sequence.push(currentState); + + const state = stateMachineConfig[currentState]; + if (state && state.transitions) { + for (const transition of state.transitions) { + const targetState = transition.targetState; + if (targetState && !visited.has(targetState) && stateMachineConfig[targetState]) { + visited.add(targetState); + subQueue.push(targetState); + } + } + } + } + } + } + + return sequence; +} + +/** + * 评估状态的检测条件 + * 根据配置中的conditions和logic表达式判断当前是否处于该状态 + * + * @param {Object} detection - 检测配置对象,包含conditions数组和logic表达式 + * @returns {boolean} 是否匹配该状态 + */ +async function evaluateDetectionConditions(detection, currentMousePos) { + if (!detection || !detection.conditions || detection.conditions.length === 0) { + return { isMatch: false, mousePos: currentMousePos }; + } + + // 检查鼠标位置是否需要移动 + let newMousePos = currentMousePos; + + // 检查当前鼠标位置是否离所有识别区域均超过50x50 + const isMouseSafe = checkMousePosition(currentMousePos, detection.conditions); + + if (!isMouseSafe) { + // 寻找合适的鼠标位置 + newMousePos = findSafeMousePosition(detection.conditions); + if (newMousePos) { + moveMouseTo(newMousePos.x, newMousePos.y); + await sleep(50); + } else { + // 没有找到合适的位置,移动到默认位置 + moveMouseTo(10, 10); + await sleep(50); + newMousePos = { x: 10, y: 10 }; + } + } + + // 先检查并释放gameRegion + if (gameRegion) { + gameRegion.dispose(); + } + + // 捕获游戏画面 + gameRegion = captureGameRegion(); + + try { + // 计算每个条件的值 + const conditionValues = {}; + + for (const condition of detection.conditions) { + const { id, template, region } = condition; + + // 创建识别对象 + const templatePath = template; + const ro = RecognitionObject.TemplateMatch( + file.ReadImageMatSync(templatePath), + region.x, region.y, region.width, region.height + ); + + // 执行识别 + const result = gameRegion.find(ro); + conditionValues[id] = result.isExist(); + } + + // 使用logic表达式计算最终结果 + // 将logic表达式中的条件ID替换为实际值 + let logicExpression = detection.logic; + for (const [key, value] of Object.entries(conditionValues)) { + // 使用正则替换完整的条件名,避免部分匹配 + const regex = new RegExp(`\\b${key}\\b`, 'g'); + logicExpression = logicExpression.replace(regex, value); + } + + // 计算逻辑表达式 + try { + const isMatch = eval(logicExpression); + return { isMatch, mousePos: newMousePos }; + } catch (e) { + log.error(`逻辑表达式计算失败: ${detection.logic} -> ${logicExpression}, 错误: ${e.message}`); + return { isMatch: false, mousePos: newMousePos }; + } + } catch (e) { + log.error(`评估检测条件时出错:${e.message}`); + return { isMatch: false, mousePos: newMousePos }; + } finally { + if (gameRegion) { + gameRegion.dispose(); + } + } +} + +/** + * 检查鼠标位置是否离所有识别区域均超过50x50 + * @param {Object|null} mousePos - 当前鼠标位置 {x, y} + * @param {Array} conditions - 检测条件数组 + * @returns {boolean} 是否安全 + */ +function checkMousePosition(mousePos, conditions) { + if (!mousePos) { + return false; // 未知位置,认为不安全 + } + + for (const condition of conditions) { + const { region } = condition; + const { x, y, width, height } = region; + + // 检查鼠标是否在识别区域附近50像素内 + if (mousePos.x >= x - 50 && mousePos.x <= x + width + 50 && + mousePos.y >= y - 50 && mousePos.y <= y + height + 50) { + return false; + } + } + + return true; +} + +/** + * 在10x10到1910x1070间隔10x10的点阵中寻找安全的鼠标位置 + * @param {Array} conditions - 检测条件数组 + * @returns {Object|null} 安全的鼠标位置 {x, y},如果没有找到返回null + */ +function findSafeMousePosition(conditions) { + for (let x = 10; x <= 1910; x += 10) { + for (let y = 10; y <= 1070; y += 10) { + let isSafe = true; + + for (const condition of conditions) { + const { region } = condition; + const { x: rx, y: ry, width, height } = region; + + // 检查该点是否在识别区域附近50像素内 + if (x >= rx - 50 && x <= rx + width + 50 && + y >= ry - 50 && y <= ry + height + 50) { + isSafe = false; + break; + } + } + + if (isSafe) { + return { x, y }; + } + } + } + + return null; // 没有找到安全位置 +} + +/** + * 前往指定状态 + * 判断当前状态后,执行相应操作前往目标状态,每步执行后重新判断当前状态 + * + * @param {string} targetState - 目标状态名称 + * @param {string|null} previousState - 上一个状态(可选),用于判断状态起点 + * @returns {boolean} 是否成功到达目标状态 + */ +async function goToState(targetState, previousState = null) { + if (!stateMachineConfig) { + log.error('状态机配置未加载'); + return false; + } + + if (!stateMachineConfig[targetState]) { + log.error(`目标状态 ${targetState} 不存在于配置中`); + return false; + } + + const maxSteps = 20; // 最大步骤数,防止无限循环 + let currentState = previousState; + let steps = 0; + + // 防护机制:记录每个状态的尝试次数 + const stateAttemptCount = new Map(); + const maxStateAttempts = 5; // 同一状态最多尝试5次 + + while (steps < maxSteps) { + // 判断当前状态 + const detectedState = await determineCurrentState(currentState); + + if (!detectedState) { + log.error('无法识别当前状态,停止状态切换'); + return false; + } + + // 检查是否已到达目标状态 + if (detectedState === targetState) { + log.info(`已到达目标状态: ${targetState}`); + return true; + } + + // 防护机制:检查当前状态的尝试次数 + const attemptCount = stateAttemptCount.get(detectedState) || 0; + if (attemptCount >= maxStateAttempts) { + log.error(`状态 ${detectedState} 已连续尝试 ${attemptCount} 次,超过最大限制,停止状态切换`); + log.error('可能陷入状态循环,请检查状态配置或界面状态'); + return false; + } + stateAttemptCount.set(detectedState, attemptCount + 1); + + // 查找从当前状态到目标状态的路径 + const path = findPath(detectedState, targetState); + if (!path || path.length === 0) { + log.error(`无法找到从 ${detectedState} 到 ${targetState} 的路径`); + return false; + } + + // 执行路径中的第一步 + const nextState = path[0]; + const transition = stateMachineConfig[detectedState].transitions.find( + t => t.targetState === nextState + ); + + if (!transition) { + log.error(`从 ${detectedState} 到 ${nextState} 的转移未定义`); + return false; + } + + // 只在第一次尝试或重试时显示转移信息 + if (attemptCount === 0) { + log.info(`${detectedState} -> ${nextState} (目标: ${targetState})`); + } else { + log.warn(`重试: ${detectedState} -> ${nextState} (第 ${attemptCount + 1} 次)`); + } + try { + // 使用 new Function 创建异步函数并执行 + const actionFunc = new Function('return (async () => { ' + transition.action + ' })()'); + await actionFunc(); + } catch (e) { + log.error(`执行转移操作失败: ${e.message}`); + return false; + } + + // 等待一小段时间让界面响应 + await sleep(500); + + currentState = detectedState; + steps++; + } + + log.error(`超过最大步骤数 ${maxSteps},停止状态切换`); + return false; +} + +/** + * 查找从起始状态到目标状态的路径 + * 使用BFS算法查找最短路径 + * + * @param {string} startState - 起始状态 + * @param {string} targetState - 目标状态 + * @returns {string[]|null} 路径数组(不包含起始状态,包含目标状态),如果无法到达则返回null + */ +function findPath(startState, targetState) { + if (!stateMachineConfig[startState] || !stateMachineConfig[targetState]) { + return null; + } + + if (startState === targetState) { + return []; + } + + // BFS查找最短路径 + const queue = [[startState]]; + const visited = new Set([startState]); + + while (queue.length > 0) { + const path = queue.shift(); + const currentState = path[path.length - 1]; + + const state = stateMachineConfig[currentState]; + if (!state || !state.transitions) continue; + + for (const transition of state.transitions) { + const nextState = transition.targetState; + + if (nextState === targetState) { + // 找到路径 + return [...path.slice(1), nextState]; + } + + if (!visited.has(nextState) && stateMachineConfig[nextState]) { + visited.add(nextState); + queue.push([...path, nextState]); + } + } + } + + return null; // 无法到达目标状态 +} + +/** + * 通用找图/找RO并可选点击(支持单图片文件路径、单RO、图片文件路径数组、RO数组) + * @param {string|string[]|RecognitionObject|RecognitionObject[]} target + * @param {boolean} [doClick=true] 是否点击 + * @param {number} [timeout=3000] 识别时间上限(ms) + * @param {number} [interval=50] 识别间隔(ms) + * @param {number} [retType=0] 0-返回布尔;1-返回 Region 结果 + * @param {number} [preClickDelay=50] 点击前等待 + * @param {number} [postClickDelay=50] 点击后等待 + * @returns {boolean|Region} 根据 retType 返回是否成功或最终 Region + */ +async function findAndClick(target, + doClick = true, + timeout = 3000, + interval = 50, + retType = 0, + preClickDelay = 50, + postClickDelay = 50) { + // 建立识别目标的对象,将 mat 和 ro 分别挂载到对象上 + let targetObjs = []; + try { + // 1. 统一处理目标,保存 mat 和 ro 的对应关系 + if (Array.isArray(target)) { + targetObjs = new Array(target.length); + for (let i = 0; i < target.length; i++) { + const t = target[i]; + targetObjs[i] = {}; + if (typeof t === 'string') { + targetObjs[i].mat = file.ReadImageMatSync(t); + targetObjs[i].ro = RecognitionObject.TemplateMatch(targetObjs[i].mat); + } else { + targetObjs[i].ro = t; + } + } + } else { + targetObjs = new Array(1); + targetObjs[0] = {}; + if (typeof target === 'string') { + targetObjs[0].mat = file.ReadImageMatSync(target); + targetObjs[0].ro = RecognitionObject.TemplateMatch(targetObjs[0].mat); + } else { + targetObjs[0].ro = target; + } + } + + const start = Date.now(); + let found = null; + + while (Date.now() - start <= timeout) { + const gameRegion = captureGameRegion(); + try { + // 依次尝试每一个 ro + for (let i = 0; i < targetObjs.length; i++) { + const res = gameRegion.find(targetObjs[i].ro); + if (!res.isEmpty()) { // 找到 + found = res; + if (doClick) { + await sleep(preClickDelay); + res.click(); + await sleep(postClickDelay); + } + break; // 成功即跳出 for + } + } + if (found) break; // 成功即跳出 while + } finally { + gameRegion.dispose(); + } + await sleep(interval); // 没找到时等待 + } + + // 3. 按需返回 + return retType === 0 ? !!found : (found || null); + + } catch (error) { + log.error(`执行通用识图时出现错误:${error.message}`); + return retType === 0 ? false : null; + } finally { + // 遍历对象释放 mat + for (let i = 0; i < targetObjs.length; i++) { + if (targetObjs[i] && targetObjs[i].mat) { + try { + targetObjs[i].mat.dispose(); + } catch (e) { + log.error(`释放 Mat 对象时出错:${e.message}`); + } + } + } + } +} + +// 将函数挂载到 globalThis,供 new Function 创建的作用域访问 +//globalThis.findAndClick = findAndClick; + +/** + * 数字模板匹配 + * + * @param {string} numberPngFilePath - 存放 0.png ~ 9.png 的文件夹路径(不含文件名) + * @param {number} x - 待识别区域的左上角 x 坐标,默认 0 + * @param {number} y - 待识别区域的左上角 y 坐标,默认 0 + * @param {number} w - 待识别区域的宽度,默认 1920 + * @param {number} h - 待识别区域的高度,默认 1080 + * @param {number} maxThreshold - 模板匹配起始阈值,默认 0.95(最高可信度) + * @param {number} minThreshold - 模板匹配最低阈值,默认 0.8(最低可信度) + * @param {number} splitCount - 在 maxThreshold 与 minThreshold 之间做几次等间隔阈值递减,默认 5 + * @param {number} maxOverlap - 非极大抑制时允许的最大重叠像素,默认 2;只要 x 或 y 方向重叠大于该值即视为重复框 + * + * @returns {number} 识别出的整数;若没有任何有效数字框则返回 -1 + * + * @example + * const mora = await numberTemplateMatch('摩拉数字', 860, 70, 200, 40); + * if (mora >= 0) console.log(`当前摩拉:${mora}`); + */ +async function numberTemplateMatch( + numberPngFilePath, + x = 0, y = 0, w = 1920, h = 1080, + maxThreshold = 0.95, + minThreshold = 0.8, + splitCount = 5, + maxOverlap = 2 +) { + let targetObjs = new Array(10); // 0-9 共10个数字模板 + for (let i = 0; i <= 9; i++) { + targetObjs[i] = {}; + targetObjs[i].mat = file.ReadImageMatSync(`${numberPngFilePath}/${i}.png`); + targetObjs[i].ro = RecognitionObject.TemplateMatch(targetObjs[i].mat, x, y, w, h); + } + + function setThreshold(objs, newThreshold) { + for (let i = 0; i < objs.length; i++) { + if (objs[i] && objs[i].ro) { + objs[i].ro.Threshold = newThreshold; + objs[i].ro.InitTemplate(); + } + } + } + + let gameRegion; + const allCandidates = []; + + try { + gameRegion = captureGameRegion(); + + /* 1. splitCount 次等间隔阈值递减 */ + for (let k = 0; k < splitCount; k++) { + const curThr = maxThreshold - (maxThreshold - minThreshold) * k / Math.max(splitCount - 1, 1); + setThreshold(targetObjs, curThr); + + /* 2. 9-0 每个模板跑一遍,所有框都收 */ + for (let digit = 9; digit >= 0; digit--) { + try { + const res = gameRegion.findMulti(targetObjs[digit].ro); + if (res.count === 0) continue; + + for (let i = 0; i < res.count; i++) { + const box = res[i]; + allCandidates.push({ + digit: digit, + x: box.x, + y: box.y, + w: box.width, + h: box.height, + thr: curThr + }); + } + } catch (e) { + log.error(`识别数字 ${digit} 时出错:${e.message}`); + } + } + } + } catch (error) { + log.error(`识别数字过程中出现错误:${error.message}`); + } finally { + if (gameRegion) gameRegion.dispose(); + // 释放数字模板的 mat 对象 + for (let i = 0; i < targetObjs.length; i++) { + if (targetObjs[i] && targetObjs[i].mat) { + try { + targetObjs[i].mat.dispose(); + } catch (e) { + log.error(`释放数字模板 Mat 对象时出错:${e.message}`); + } + } + } + } + + /* 3. 无结果提前返回 -1 */ + if (allCandidates.length === 0) { + return -1; + } + + /* 4. 非极大抑制(必须 x、y 两个方向重叠都 > maxOverlap 才视为重复) */ + const adopted = []; + for (const c of allCandidates) { + let overlap = false; + for (const a of adopted) { + const xOverlap = Math.max(0, Math.min(c.x + c.w, a.x + a.w) - Math.max(c.x, a.x)); + const yOverlap = Math.max(0, Math.min(c.y + c.h, a.y + a.h) - Math.max(c.y, a.y)); + if (xOverlap > maxOverlap && yOverlap > maxOverlap) { + overlap = true; + break; + } + } + if (!overlap) { + adopted.push(c); + //log.info(`在 [${c.x},${c.y},${c.w},${c.h}] 找到数字 ${c.digit},匹配阈值=${c.thr}`); + } + } + + /* 5. 按 x 排序,拼整数;仍无有效框时返回 -1 */ + if (adopted.length === 0) return -1; + adopted.sort((a, b) => a.x - b.x); + + return adopted.reduce((num, item) => num * 10 + item.digit, 0); +} + +/** + * 计算两个字符串的相似度 + * 使用Levenshtein距离算法 + * + * @param {string} str1 - 第一个字符串 + * @param {string} str2 - 第二个字符串 + * @returns {number} 相似度,范围0-1 + */ +function calculateSimilarity(str1, str2) { + const len1 = str1.length; + const len2 = str2.length; + + // 创建距离矩阵 + const matrix = Array(len1 + 1).fill().map(() => Array(len2 + 1).fill(0)); + + // 初始化第一行和第一列 + for (let i = 0; i <= len1; i++) { + matrix[i][0] = i; + } + for (let j = 0; j <= len2; j++) { + matrix[0][j] = j; + } + + // 计算距离 + for (let i = 1; i <= len1; i++) { + for (let j = 1; j <= len2; j++) { + const cost = str1[i - 1] === str2[j - 1] ? 0 : 1; + matrix[i][j] = Math.min( + matrix[i - 1][j] + 1, // 删除 + matrix[i][j - 1] + 1, // 插入 + matrix[i - 1][j - 1] + cost // 替换 + ); + } + } + + // 计算相似度 + const maxLen = Math.max(len1, len2); + const distance = matrix[len1][len2]; + const similarity = 1 - (distance / maxLen); + + return similarity; +} + +/** + * 检查UID是否匹配 + * 考虑到识别误差,当相似度大于等于8/9时认为匹配成功 + * + * @param {number} currentUid - 当前识别的UID + * @param {string} targetUid - 目标UID + * @returns {boolean} 是否匹配成功 + */ +function isUidMatch(currentUid, targetUid) { + if (currentUid < 0) { + return false; + } + + const currentUidStr = currentUid.toString(); + const targetUidStr = targetUid.toString(); + + // 计算相似度 + const similarity = calculateSimilarity(currentUidStr, targetUidStr); + + // 输出相似度信息 + log.info(`UID相似度:${(similarity * 100).toFixed(2)}% (${currentUidStr} vs ${targetUidStr})`); + + // 相似度大于等于8/9时认为匹配成功 + return similarity >= 8 / 9; +} + +/** + * 校验当前账号UID + * + * @returns {number} 当前账号UID,若识别失败返回-1 + */ +async function verifyCurrentUid() { + try { + // 尝试返回主界面 + await genshin.returnMainUi(); + + // 尝试使用状态机进入主界面 + await goToState('mainUI'); + + // 无论尝试是否成功,按一次G键 + keyPress('VK_G'); + await sleep(1000); + + // 识别UID,识别区域是1727 1050 160 30 + const uid = await numberTemplateMatch('assets/uid图片', 1727, 1050, 160, 30); + + if (uid >= 0) { + log.info(`成功识别当前账号UID:${uid}`); + } else { + log.warn('未能识别当前账号UID'); + } + await genshin.returnMainUi(); + return uid; + } catch (e) { + log.error(`校验UID时出错:${e.message}`); + return -1; + } +} + +/** + * 处理截图模式 + * 前往主界面识别UID,然后前往"进入游戏或登录其他账号"界面,截图保存对应UID图片 + */ +async function handleScreenshotMode() { + try { + log.info('进入截图模式'); + + // 1. 确定要使用的UID + let uidStr = settings.targetUid; + + if (!uidStr) { + log.info('未设置目标UID,尝试识别当前账号UID'); + const currentUid = await verifyCurrentUid(); + + if (currentUid < 0) { + log.error('未能识别当前账号UID,截图模式失败'); + notification.error('截图模式失败:未能识别当前账号UID'); + return; + } + + uidStr = currentUid.toString(); + log.info(`识别到当前账号UID:${uidStr}`); + } else { + log.info(`使用设置的目标UID:${uidStr}`); + } + + // 2. 前往"进入游戏或登录其他账号"界面 + log.info('开始:前往进入游戏或登录其他账号界面'); + + // 直接尝试进入enterGame状态 + const enterGameResult = await goToState('enterGame'); + + if (!enterGameResult) { + log.error('未能到达进入游戏或登录其他账号界面,截图模式失败'); + notification.error('截图模式失败:未能到达进入游戏或登录其他账号界面'); + return; + } + + log.info('成功到达进入游戏或登录其他账号界面'); + + // 3. 截图保存对应UID图片 + log.info('开始:截图保存账号图片'); + + // 截图区域:780 481 150 27 + const CAP_X = 780; + const CAP_Y = 481; + const CAP_W = 150; + const CAP_H = 27; + + // 保存路径 + const TARGET_DIR = 'accounts'; + const fullPath = TARGET_DIR + '/' + uidStr + '.png'; + + // 捕获游戏画面 + gameRegion = captureGameRegion(); + try { + const mat = gameRegion.DeriveCrop(CAP_X, CAP_Y, CAP_W, CAP_H).SrcMat; + + // 保存图片 + file.WriteImageSync(fullPath, mat); + mat.dispose(); + } finally { + gameRegion.dispose(); + gameRegion = null; + } + + log.info(`成功保存账号图片:${fullPath}`); + notification.Send(`截图模式成功:已保存账号图片 ${uidStr}.png`); + + // 4. 返回主界面 + log.info('开始:返回主界面'); + await goToState('mainUI'); + log.info('已返回主界面'); + + } catch (e) { + log.error(`截图模式出错:${e.message}`); + notification.error(`截图模式失败:${e.message}`); + } +} diff --git a/repo/js/AccountSwitchStateMachine/manifest.json b/repo/js/AccountSwitchStateMachine/manifest.json new file mode 100644 index 000000000..4db86314d --- /dev/null +++ b/repo/js/AccountSwitchStateMachine/manifest.json @@ -0,0 +1,13 @@ +{ + "manifest_version": 1, + "name": "带状态机的账号切换", + "version": "1.0", + "description": "", + "authors": [ + { + "name": "mno" + } + ], + "settings_ui": "settings.json", + "main": "main.js" +} \ No newline at end of file diff --git a/repo/js/AccountSwitchStateMachine/settings.json b/repo/js/AccountSwitchStateMachine/settings.json new file mode 100644 index 000000000..6b88f356d --- /dev/null +++ b/repo/js/AccountSwitchStateMachine/settings.json @@ -0,0 +1,32 @@ +[ + { + "name": "targetUid", + "type": "input-text", + "label": "要切换到的账号UID" + }, + { + "name": "account", + "type": "input-text", + "label": "账号(非必要,查找不到账号图片时备用)" + }, + { + "name": "password", + "type": "input-text", + "label": "密码(非必要,查找不到账号图片时备用)" + }, + { + "name": "skipSearch", + "type": "checkbox", + "label": "勾选后不尝试查找账号图片,直接使用账号密码登录" + }, + { + "name": "verifyUid", + "type": "checkbox", + "label": "勾选后在切换前后检查当前账号UID,辅助判断切换是否成功" + }, + { + "name": "screenshotMode", + "type": "checkbox", + "label": "勾选后进入截图模式,自动截图保存对应UID的账号图片" + } +]