mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-03-16 03:33:25 +08:00
* js:带状态机的账号切换 使用状态机管理账号切换过程中的各种页面 提高流程稳定性并方便管理和新增异常处理 支持自动截图保存,双模式切换账号 * Update main.js 将状态切换需要用到的函数挂载到globalThis,以适配AutoJs作为其中的账号切换工具 * 修复提到的问题 * Update main.js 修复提到的问题 * Update main.js * Update main.js * Update main.js
1165 lines
44 KiB
JavaScript
1165 lines
44 KiB
JavaScript
// 全局状态机配置对象
|
||
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}`);
|
||
}
|
||
}
|