Files
bettergi-scripts-list/repo/js/MiliastraExperiencePlayback/libs/@bettergi+utils.js
2026-01-18 18:51:44 +08:00

539 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { __name } from "./rolldown-runtime.js";
//#region node_modules/@bettergi/utils/dist/workflow.js
/** 默认最大重试次数 */
const defaultMaxAttempts = 5;
/** 默认重试间隔(毫秒) */
const defaultRetryInterval = 1e3;
/**
* 等待直到条件满足或超时,期间执行重试操作
* @param condition 返回条件是否满足的函数
* @param retryAction 每次重试时执行的操作(可选)
* @param options 配置选项
* @returns - true 条件满足
* - false 达到最大重试次数
*/
const waitForAction = async (condition, retryAction, options) => {
const { maxAttempts = defaultMaxAttempts, retryInterval = defaultRetryInterval } = options || {};
for (let i = 0; i < maxAttempts; i++) {
if (i === 0 && condition()) return true;
await retryAction?.();
await sleep(retryInterval);
if (condition()) return true;
}
return false;
};
/**
* 等待某个区域出现,期间执行重试操作
* @param regionProvider 返回区域的函数
* @param retryAction 每次重试时执行的操作(可选)
* @param options 配置选项
* @returns - true 区域出现
* - false 达到最大重试次数
*/
const waitForRegionAppear = async (regionProvider, retryAction, options) => {
return waitForAction(
() => {
const region = regionProvider();
return region != null && region.isExist();
},
retryAction,
options,
);
};
/**
* 等待某个区域消失,期间执行重试操作
* @param regionProvider 返回区域的函数
* @param retryAction 每次重试时执行的操作(可选)
* @param options 配置选项
* @returns - true 区域消失
* - false 达到最大重试次数
*/
const waitForRegionDisappear = async (regionProvider, retryAction, options) => {
return waitForAction(
() => {
const region = regionProvider();
return !region || !region.isExist();
},
retryAction,
options,
);
};
//#endregion
//#region node_modules/@bettergi/utils/dist/asserts.js
/**
* 断言某个区域即将出现,否则抛出异常
* @param regionProvider 返回区域的函数
* @param message 错误信息
* @param retryAction 每次重试时执行的操作(可选)
* @param options 配置选项
*/
const assertRegionAppearing = async (regionProvider, message, retryAction, options) => {
if (!(await waitForRegionAppear(regionProvider, retryAction, options))) throw new Error(message);
};
/**
* 断言某个区域即将消失,否则抛出异常
* @param regionProvider 返回区域的函数
* @param message 错误信息
* @param retryAction 每次重试时执行的操作(可选)
* @param options 配置选项
*/
const assertRegionDisappearing = async (regionProvider, message, retryAction, options) => {
if (!(await waitForRegionDisappear(regionProvider, retryAction, options)))
throw new Error(message);
};
//#endregion
//#region node_modules/@bettergi/utils/dist/exception.js
/**
* 获取错误信息字符串
* @param err 异常对象
* @returns 错误信息字符串
*/
const getErrorMessage = (err) => {
if (err && "message" in err && typeof err.message === "string") return err.message;
return err && typeof err === "object" ? JSON.stringify(err) : "Unknown error";
};
/**
* 判断是否为主机异常
* @param err 异常对象
*/
const isHostException = (err) => {
return err && "hostException" in err;
};
//#endregion
//#region node_modules/@bettergi/utils/dist/mouse.js
/** 使用回放脚本模拟滚动 */
const simulateScroll = async (wheelDelta, times) => {
const script = {
macroEvents: Array(times).fill({
type: 6,
mouseX: 0,
mouseY: wheelDelta,
time: 0,
}),
info: {
name: "",
description: "",
x: 0,
y: 0,
width: 1920,
height: 1080,
recordDpi: 1.5,
},
};
await keyMouseScript.run(JSON.stringify(script));
};
/**
* 鼠标滚轮向下滚动指定高度
* @param height 滚动高度
* @param algorithm 自定义滚动算法函数接收高度参数并返回滚动次数默认算法为每18像素滚动一次
*/
const mouseScrollDown = (height, algorithm = (h) => Math.floor(h / 17.9795)) => {
return simulateScroll(-120, algorithm(height));
};
/**
* 鼠标滚轮向下滚动指定行数
* @param lines 滚动行数
* @param lineHeight 行高默认值为175像素
*/
const mouseScrollDownLines = (lines, lineHeight = 175) => {
return mouseScrollDown(lines * lineHeight);
};
//#endregion
//#region node_modules/@bettergi/utils/dist/ocr.js
const scaleTo1080P = (n) => {
return genshin.scaleTo1080PRatio <= 1 ? n : Math.floor(n / genshin.scaleTo1080PRatio);
};
/**
* 在指定区域内搜索图片
* @param image 图片路径 或 图片Mat
* @param x 水平方向偏移量(像素)
* @param y 垂直方向偏移量(像素)
* @param w 宽度
* @param h 高度
* @param config 识别对象配置
* @returns 如果找到匹配的图片区域,则返回该区域,否则返回 undefined
*/
const findImageWithinBounds = (image, x, y, w, h, config = {}) => {
const ir = captureGameRegion();
try {
const mat = typeof image === "string" ? file.readImageMatSync(image) : image;
const ro = RecognitionObject.templateMatch(mat, x, y, w, h);
if (Object.keys(config).length > 0) Object.assign(ro, config) && ro.initTemplate();
const region = ir.find(ro);
return region.isExist() ? region : void 0;
} catch (err) {
log.warn(`${err.message || err}`);
} finally {
ir.dispose();
}
};
/**
* 在图像区域内查找第一个符合条件的识别区域
* @param ir 图像区域
* @param ro 识别对象
* @param predicate 筛选条件
* @returns 第一个符合条件的识别区域,未找到则返回 undefined
*/
const findFirstRegion = (ir, ro, predicate) => {
const candidates = ir.findMulti(ro);
for (let i = 0; i < candidates.count; i++) if (predicate(candidates[i])) return candidates[i];
};
/**
* 文本匹配
* @param text 待匹配文本
* @param searchText 待搜索文本
* @param options 搜索选项
* @returns 是否匹配
*/
const textMatch = (text, searchText, options) => {
const { ignoreCase = true, contains = false } = options || {};
text = ignoreCase ? text.toLowerCase() : text;
searchText = ignoreCase ? searchText.toLowerCase() : searchText;
return contains ? text.includes(searchText) : text === searchText;
};
/**
* 在指定区域内搜索文本
* @param text 待搜索文本
* @param x 水平方向偏移量(像素)
* @param y 垂直方向偏移量(像素)
* @param w 宽度
* @param h 高度
* @param options 搜索选项
* @param config 识别对象配置
* @returns 如果找到匹配的文本区域,则返回该区域,否则返回 undefined
*/
const findTextWithinBounds = (text, x, y, w, h, options, config = {}) => {
const ir = captureGameRegion();
try {
const ro = RecognitionObject.ocr(x, y, w, h);
if (Object.keys(config).length > 0) Object.assign(ro, config) && ro.initTemplate();
return findFirstRegion(ir, ro, (region) => {
return region.isExist() && textMatch(region.text, text, options);
});
} catch (err) {
log.warn(`${err.message || err}`);
} finally {
ir.dispose();
}
};
/**
* 在列表视图中滚动搜索区域
* @param condition 查找条件
* @param listView 列表视图参数
* @param retryOptions 重试选项
* @param threshold 列表视图变化匹配阈值默认0.9
* @returns 如果找到匹配的区域,则返回该区域,否则返回 undefined
*/
const findWithinListView = async (condition, listView, retryOptions, threshold = 0.9) => {
const { x, y, w, h, lineHeight, scrollLines = 1, paddingX = 10, paddingY = 10 } = listView;
const { maxAttempts = 99, retryInterval = 1200 } = retryOptions || {};
const captureListViewRegion = () => captureGameRegion().deriveCrop(x, y, w, h);
const isReachedBottom = (() => {
let captured;
let lastCaptured;
return () => {
try {
captured = captureListViewRegion();
if (!lastCaptured) return false;
const lc = lastCaptured.deriveCrop(1, 1, lastCaptured.width - 2, lastCaptured.height - 2);
const ro = RecognitionObject.templateMatch(lc.srcMat);
ro.threshold = threshold;
ro.use3Channels = true;
ro.initTemplate();
return captured.find(ro).isExist();
} catch {
return true;
} finally {
lastCaptured = captured;
}
};
})();
let targetRegion;
await waitForAction(
() => {
targetRegion = condition(captureListViewRegion());
return targetRegion?.isExist() || isReachedBottom();
},
async () => {
moveMouseTo(x + w - paddingX, y + paddingY);
await sleep(200);
await mouseScrollDownLines(scrollLines, lineHeight);
},
{
maxAttempts,
retryInterval,
},
);
if (targetRegion?.isExist()) {
const { item1, item2 } = targetRegion.convertPositionToGameCaptureRegion(0, 0);
Object.assign(targetRegion, {
x: scaleTo1080P(item1),
y: scaleTo1080P(item2),
});
return targetRegion;
}
};
/**
* 在列表视图中滚动搜索文本
* @param text 待搜索文本
* @param listView 列表视图参数
* @param matchOptions 搜索选项
* @param retryOptions 重试选项
* @param config 识别对象配置
* @param threshold 列表视图变化匹配阈值默认0.9
* @returns 如果找到匹配的文本区域,则返回该区域,否则返回 undefined
*/
const findTextWithinListView = async (
text,
listView,
matchOptions,
retryOptions,
config = {},
threshold = 0.9,
) => {
const ro = RecognitionObject.ocrThis;
if (Object.keys(config).length > 0) Object.assign(ro, config) && ro.initTemplate();
return findWithinListView(
(lvr) => {
return findFirstRegion(lvr, ro, (region) => {
return region.isExist() && textMatch(region.text, text, matchOptions);
});
},
listView,
retryOptions,
threshold,
);
};
//#endregion
//#region node_modules/@bettergi/utils/dist/misc.js
/**
* 深度合并多个对象
* @param objects 多个对象
* @returns 合并后的对象副本
*/
const deepMerge = (...objects) => {
const isPlainObject = (input) => input?.constructor === Object;
return objects.reduce((result, obj) => {
return Object.entries(obj).reduce((acc, [key, value]) => {
acc[key] =
isPlainObject(acc[key]) && isPlainObject(value) ? deepMerge(acc[key], value) : value;
return acc;
}, result);
}, {});
};
//#endregion
//#region node_modules/@bettergi/utils/dist/time.js
/**
* 获取下一个含当日凌晨4点的时间
*/
const getNextDay4AM = () => {
const now = /* @__PURE__ */ new Date();
const result = new Date(now);
result.setHours(4, 0, 0, 0);
const daysUntilNextDay = now.getHours() < 4 ? 0 : 1;
result.setDate(now.getDate() + daysUntilNextDay);
return result;
};
/**
* 获取下一个含当日周一凌晨4点的时间
*/
const getNextMonday4AM = () => {
const now = /* @__PURE__ */ new Date();
const result = new Date(now);
result.setHours(4, 0, 0, 0);
const currentDay = now.getDay();
const daysUntilNextMonday = currentDay === 1 && now.getHours() < 4 ? 0 : 8 - currentDay;
result.setDate(now.getDate() + daysUntilNextMonday);
return result;
};
/**
* 解析时长
* @param duration 时长(毫秒)
*/
const parseDuration = (duration) => {
return {
h: Math.floor(duration / 36e5),
m: Math.floor((duration % 36e5) / 6e4),
s: Math.floor((duration % 6e4) / 1e3),
ms: Math.floor(duration % 1e3),
};
};
/**
* 将时长转换为时钟字符串
* @param duration 时长(毫秒)
*/
const formatDurationAsClock = (duration) => {
return Object.values(parseDuration(duration))
.slice(0, 3)
.map((num) => String(num).padStart(2, "0"))
.join(":");
};
/**
* 将时长转换为可读格式
* @param duration 时长(毫秒)
*/
const formatDurationAsReadable = (duration) => {
return Object.entries(parseDuration(duration))
.filter(([, value]) => value > 0)
.map(([unit, value]) => `${value}${unit}`)
.join(" ");
};
//#endregion
//#region node_modules/@bettergi/utils/dist/progress.js
/** 进度追踪器 */
var ProgressTracker = class {
total = 0;
current = 0;
startTime = Date.now();
formatter;
interval;
lastPrintTime = 0;
constructor(total, config) {
const { formatter, interval = 3e3 } = config || {};
this.total = total;
this.formatter = formatter || this.defaultFormatter;
this.interval = interval;
}
defaultFormatter = (logger, message, progress) => {
logger(
"[🚧 {pct} ⏳ {eta}]: {msg}",
progress.formatted.percentage.padStart(6),
progress.current > 0 && progress.elapsed > 0 ? progress.formatted.remaining : "--:--:--",
message,
);
};
tick(options) {
const { increment = 1, message, force = false } = options || {};
this.current = Math.min(this.current + increment, this.total);
if (message) this.print(message, force);
return this.current === this.total;
}
complete(message) {
this.current = this.total;
this.print(message, true);
}
reset() {
this.current = 0;
this.startTime = Date.now();
this.lastPrintTime = 0;
}
print(message, force = false, logger = log.info) {
if (force || this.shouldPrint()) {
this.formatter(logger, message, this.getProgress());
this.printed();
}
}
shouldPrint() {
return Date.now() - this.lastPrintTime >= this.interval;
}
printed() {
this.lastPrintTime = Date.now();
}
getProgress() {
const percentage = this.current / this.total;
const elapsed = Date.now() - this.startTime;
const average = this.current > 0 ? elapsed / this.current : 0;
const remaining = (this.total - this.current) * average;
return {
current: this.current,
total: this.total,
percentage,
elapsed,
average,
remaining,
formatted: {
percentage: `${(percentage * 100).toFixed(1)}%`,
elapsed: formatDurationAsReadable(elapsed),
average: formatDurationAsReadable(average),
remaining: formatDurationAsClock(remaining),
},
};
}
};
//#endregion
//#region node_modules/@bettergi/utils/dist/store.js
/**
* 创建一个持久化存储对象,用于管理应用状态数据
* 该函数会创建一个代理对象对该对象的所有属性的修改都会自动同步到相应的JSON文件脚本的 `store` 目录下)中。
* 支持深层嵌套对象的代理。
* @param name 存储对象的名称,将作为文件名(不包扩展名)
*/
const useStore = (name) => {
const filePath = `store/${name}.json`;
const storeData = (() => {
try {
if (
![...file.readPathSync("store")].map((path) => path.replace(/\\/g, "/")).includes(filePath)
)
throw new Error("File does not exist");
const text = file.readTextSync(filePath);
return JSON.parse(text);
} catch {
return {};
}
})();
const createProxy = (targetObject, parentPath = []) => {
if (typeof targetObject !== "object" || targetObject === null) return targetObject;
return new Proxy(targetObject, {
get: (target, key) => {
const value = Reflect.get(target, key);
return typeof value === "object" && value !== null
? createProxy(value, [...parentPath, key])
: value;
},
set: (target, key, value) => {
const success = Reflect.set(target, key, value);
if (success)
Promise.resolve().then(() => {
file.writeTextSync(filePath, JSON.stringify(storeData, null, 2));
});
return success;
},
deleteProperty: (target, key) => {
const success = Reflect.deleteProperty(target, key);
if (success)
Promise.resolve().then(() => {
file.writeTextSync(filePath, JSON.stringify(storeData, null, 2));
});
return success;
},
});
};
return createProxy(storeData);
};
/**
* 创建一个带有默认值的持久化存储对象,用于管理应用状态数据
* @param name 存储对象的名称,将作为文件名(不包扩展名)
* @param defaults 默认值数据对象
*/
const useStoreWithDefaults = (name, defaults) => {
const newStore = useStore(name);
Object.assign(newStore, deepMerge(defaults, newStore));
return newStore;
};
//#endregion
export {
ProgressTracker,
assertRegionAppearing,
assertRegionDisappearing,
findImageWithinBounds,
findTextWithinBounds,
findTextWithinListView,
getErrorMessage,
getNextDay4AM,
getNextMonday4AM,
isHostException,
useStoreWithDefaults,
waitForAction,
};