mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-05-25 23:15:54 +08:00
* update ver3.3.2 1. 更改了MIDI曲谱的曲谱逻辑,现在MIDI曲谱同样支持通过调整BPM来更改播放速度 1. 更新了MIDI制谱器,现在支持多轨道合并(原先仅支持双轨) 1. 简化了MIDI的JSON曲谱格式 1. 更新了已有曲谱的格式 1. 隐藏了MIDI演奏的note信息显示 1. 更新了两首单曲《Sis Puella Magica》- 老旧的诗琴、《尘间星旅(片段)》- 「余音」,风物之诗琴 * update 现在脚本支持在演奏前自动切换对应的乐器(可选) * 更新分辨率相关说明 * Update README.md * Update main.js * update * Update npcMsg.json
1249 lines
53 KiB
JavaScript
1249 lines
53 KiB
JavaScript
(async function () { // 待解决问题: 连音总时值如果为3个四分音符无法表示
|
||
const base_path = "assets/score_file/";
|
||
const regex_name = /(?<=score_file\\)[\s\S]*?(?=.json)/;
|
||
const PlayType = {
|
||
SingleMusicOnce: 0, // 单曲单次执行
|
||
SingleMusicRepeat: 1, // 单曲循环执行
|
||
QueueMusicOnce: 2, // 队列单次执行
|
||
QueueMusicRepeat: 3, // 队列循环执行
|
||
};
|
||
let DEBUG = false;
|
||
/**
|
||
* -------- 工具函数 --------
|
||
*/
|
||
// /**
|
||
// *
|
||
// * @returns {Array} 本地曲谱文件列表
|
||
// */
|
||
// const musicList = () => {
|
||
// const scoreFiles = Array.from(file.readPathSync(base_path)).filter(path => !file.isFolder(path) && path.endsWith(".json"));
|
||
// const localMusicList = scoreFiles.map(path => path.match(regex_name)[0]);
|
||
// return localMusicList;
|
||
// }
|
||
|
||
/**
|
||
* 简洁易用的OCR函数
|
||
* @param x
|
||
* @param y
|
||
* @param w
|
||
* @param h
|
||
* @param multi 是否使用FindMulti
|
||
* @returns {Promise<void>} 返回对应的OCR对象
|
||
*/
|
||
async function Ocr(x, y, w, h, multi = false) {
|
||
let OcrRo = RecognitionObject.Ocr(x, y, w, h);
|
||
let gameRegion = captureGameRegion();
|
||
if (multi) {
|
||
let ocrResult = gameRegion.FindMulti(OcrRo);
|
||
gameRegion.dispose();
|
||
if (ocrResult.count !== 0) {
|
||
let resultList = [];
|
||
for (let i = 0; i < ocrResult.count; i++) {
|
||
resultList.push(ocrResult[i]);
|
||
}
|
||
return resultList;
|
||
} else {
|
||
log.debug(`FindMulti为空: (${x}, ${y}, ${w}, ${h})`);
|
||
return false;
|
||
}
|
||
} else {
|
||
let ocrResult = gameRegion.Find(OcrRo);
|
||
gameRegion.dispose();
|
||
if (ocrResult.isExist()) {
|
||
return ocrResult;
|
||
} else {
|
||
log.debug(`Find为空: (${x}, ${y}, ${w}, ${h})`);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 在指定区域内OCR文本并返回OCR对象
|
||
* @param x
|
||
* @param y
|
||
* @param w
|
||
* @param h
|
||
* @param text 文本
|
||
* @returns {Promise<*>} 找到返回OCR对象,未找到返回false
|
||
* @see Ocr
|
||
*/
|
||
async function ocr_find_area(x, y, w, h, text) {
|
||
const OcrResult = await Ocr(x, y, w, h, true);
|
||
|
||
if (OcrResult) {
|
||
let flag = true;
|
||
for (let i = 0; i < OcrResult.length; i++) {
|
||
if (OcrResult[i].text.includes(text)) {
|
||
flag = false;
|
||
await sleep(200);
|
||
return OcrResult[i];
|
||
}
|
||
}
|
||
if (flag) {
|
||
log.debug(`区域(${x}, ${y}, ${w}, ${h})内未找到文本:${text}`);
|
||
return false;
|
||
}
|
||
} else {
|
||
log.error(`OCR错误,区域内未识别到文本: (${x}, ${y}, ${w}, ${h})`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 向上/下滑动滑块一次(原理,点击紧贴滑块的上/下方)[以下,高/顶表示屏幕上方,低/底表示屏幕下方]
|
||
* @param x 滑块移动区域
|
||
* @param y 滑块移动区域
|
||
* @param w 滑块移动区域
|
||
* @param h 滑块移动区域
|
||
* @param max 滑块最高临界y值,若滑块y值小于此值则认为已经到顶
|
||
* @param min 滑块最低临界y值,若滑块y值大于此值则认为已经到底
|
||
* @param m_x 滑块区域的滑条中心x值
|
||
* @param direction 滑动方向(Up/Down)
|
||
* @param bg 背景颜色(白white/黑black),black时滑块只能拖动
|
||
* @param distance 滑动一页滑块需要滑动的y方向的距离(适用于bg为black),必须大于4
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
async function scroll_page(x, y, w, h, max, min, m_x, direction, bg = "white", distance = 140) {
|
||
let barUpRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`assets/${bg === "white" ? "slide_bar_main_up": "slide_bar_left_up"}.png`), x, y, w, h);
|
||
let barDownRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`assets/${bg === "white" ? "slide_bar_main_down": "slide_bar_left_down"}.png`), x, y, w, h);
|
||
barUpRo.threshold = 0.7;
|
||
barDownRo.threshold = 0.7;
|
||
|
||
let gameRegion = captureGameRegion();
|
||
if (direction.toLowerCase() === "up") {
|
||
let barUpper = gameRegion.Find(barUpRo);
|
||
gameRegion.dispose();
|
||
if (barUpper.isExist()) {
|
||
if (barUpper.y < max) { // 到顶了
|
||
log.info(`滑块已经滑动到顶部(${barUpper.y})...`);
|
||
return false;
|
||
} else {
|
||
if (bg === "white") {
|
||
click(m_x, barUpper.y - 15);
|
||
} else {
|
||
await mouseDrag(m_x, barUpper.y + 4, m_x, barUpper.y - (distance - 4));
|
||
}
|
||
|
||
log.debug(`将滑块向上调一格,当前位置: ${barUpper.y}`);
|
||
}
|
||
} else {
|
||
log.error("未找到滑块: Up");
|
||
return false;
|
||
}
|
||
} else {
|
||
let barLower = gameRegion.Find(barDownRo);
|
||
gameRegion.dispose();
|
||
if (barLower.isExist()) {
|
||
if (barLower.y > min) { // 到底了
|
||
log.info(`滑块已经滑动到底部(${barLower.y})...`);
|
||
return false;
|
||
} else {
|
||
if (bg === "white") {
|
||
click(m_x, barLower.y + 15);
|
||
} else {
|
||
await mouseDrag(m_x, barLower.y + 4, m_x, barLower.y + (distance + 4));
|
||
}
|
||
|
||
log.debug(`将滑块向下调一格,当前位置: ${barLower.y}`);
|
||
}
|
||
} else {
|
||
log.error("未找到滑块: Down");
|
||
return false;
|
||
}
|
||
}
|
||
await sleep(200);
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 向上/下滑动滑块至顶部/底部(原理,点击紧贴滑块的上/下方)[以下,高/顶表示屏幕上方,低/底表示屏幕下方]
|
||
* @param x 滑块移动区域
|
||
* @param y 滑块移动区域
|
||
* @param w 滑块移动区域
|
||
* @param h 滑块移动区域
|
||
* @param max 滑块最高临界y值,若滑块y值小于此值则认为已经到顶
|
||
* @param min 滑块最低临界y值,若滑块y值大于此值则认为已经到底
|
||
* @param max_y 滑块移动区域的最高点y值
|
||
* @param min_y 滑块移动区域的最低点y值
|
||
* @param m_x 滑块区域的滑条中心x值
|
||
* @param side 滑动顶部或底部(Up/Down)
|
||
* @param bg 背景颜色(白white/黑black)
|
||
* @param distance 滑动一页滑块需要滑动的y方向的距离(适用于bg为black),必须大于4
|
||
* @returns {Promise<boolean>}
|
||
* @see scroll_page
|
||
*/
|
||
async function scroll_bar_to_side(x, y, w, h, max, min, max_y, min_y, m_x, side, bg = "white", distance = 140) {
|
||
let barUpRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`assets/${bg === "white" ? "slide_bar_main_up": "slide_bar_left_up"}.png`), x, y, w, h);
|
||
let barDownRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`assets/${bg === "white" ? "slide_bar_main_down": "slide_bar_left_down"}.png`), x, y, w, h);
|
||
barUpRo.threshold = 0.7;
|
||
barDownRo.threshold = 0.7;
|
||
let barUpper_temp = 0;
|
||
|
||
while (true) {
|
||
await sleep(200);
|
||
log.debug(`将滑块滑动至 ${side} `);
|
||
let gameRegion = captureGameRegion();
|
||
if (side.toLowerCase() === "up") {
|
||
let barUpper = gameRegion.Find(barUpRo);
|
||
if (barUpper.y !== barUpper_temp) { // 防止卡死
|
||
barUpper_temp = barUpper.y;
|
||
} else {
|
||
break;
|
||
}
|
||
gameRegion.dispose();
|
||
if (barUpper.isExist()) {
|
||
if (barUpper.y < max) { // 到顶了
|
||
log.info(`滑块已经滑动到顶部(${barUpper.y})...`);
|
||
break;
|
||
} else {
|
||
if (bg === "white") {
|
||
click(m_x, barUpper.y - 15);
|
||
} else {
|
||
await mouseDrag(m_x, barUpper.y + 4, m_x, barUpper.y - (distance - 4));
|
||
}
|
||
log.debug(`将滑块向上调一格,当前位置: ${barUpper.y}`);
|
||
}
|
||
} else {
|
||
log.error("未找到滑块: Up");
|
||
return false;
|
||
}
|
||
} else {
|
||
let barLower = gameRegion.Find(barDownRo);
|
||
gameRegion.dispose();
|
||
if (barLower.isExist()) {
|
||
if (barLower.y > min) { // 到底了
|
||
log.info(`滑块已经滑动到底部(${barLower.y})...`);
|
||
break;
|
||
} else {
|
||
if (bg === "white") {
|
||
click(m_x, barLower.y + 15);
|
||
} else {
|
||
await mouseDrag(m_x, barLower.y + 4, m_x, barLower.y + (distance + 4));
|
||
}
|
||
log.debug(`将滑块向下调一格,当前位置: ${barLower.y}`);
|
||
}
|
||
} else {
|
||
log.error("未找到滑块: Down");
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
await sleep(200);
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* 按照原神物品名长度显示裁剪字符串[主物品显示界面适用](用于OCR)
|
||
*
|
||
* @param string 原字符串
|
||
* @returns {Promise<*|string>} 处理后的字符串
|
||
*/
|
||
async function deal_string(string) {
|
||
if (string.length <= 6) {
|
||
return string; // 如果字符串长度是6位或以下,原形返回
|
||
} else {
|
||
// return string.substring(0, 5) + '..'; // 如果字符串长度超过6位,保留前5位并加上'..'
|
||
return string.substring(0, 5); // 如果字符串长度超过6位,保留前5位
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 读取本地曲谱文件夹下的所有 .json 文件,并返回文件名列表。
|
||
* 同时自动修正不合规的文件名:格式为 000X.任意字符.json(X 为四位数字,不足补零)。
|
||
* 重命名规则:对于不合规文件,分配当前未使用的最小四位数字作为前缀,保留原文件名主体。
|
||
* @returns {Array} 本地曲谱文件列表(合规文件名)
|
||
*/
|
||
const musicList = () => {
|
||
const usedNumbers = new Set();
|
||
const finalList = [];
|
||
|
||
// readPathSync(base_path) 返回完整相对路径
|
||
const entries = Array.from(file.readPathSync(base_path));
|
||
const jsonEntries = entries.filter(entry => !file.isFolder(entry) && entry.endsWith('.json'));
|
||
|
||
// 统计已有编号
|
||
jsonEntries.forEach(entry => {
|
||
const fileName = entry.split(/[/\\]/).pop();
|
||
if (/^\d{4}\..*\.json$/.test(fileName)) {
|
||
usedNumbers.add(parseInt(fileName.substring(0, 4), 10));
|
||
}
|
||
});
|
||
|
||
// 处理每个文件
|
||
jsonEntries.forEach(entry => {
|
||
const fileName = entry.split(/[/\\]/).pop();
|
||
const dirPath = entry.slice(0, entry.length - fileName.length);
|
||
|
||
if (/^\d{4}\..*\.json$/.test(fileName)) {
|
||
// 合规:返回不带 .json 的文件名
|
||
finalList.push(fileName.replace(/\.json$/, ''));
|
||
} else {
|
||
// 不合规:自动补零
|
||
const baseName = fileName.replace(/\.json$/, '');
|
||
|
||
let newNum = 1;
|
||
while (usedNumbers.has(newNum)) newNum++;
|
||
|
||
const newPrefix = newNum.toString().padStart(4, '0');
|
||
const newFileName = `${newPrefix}.${baseName}.json`;
|
||
|
||
const oldPath = entry;
|
||
const newPath = dirPath + newFileName;
|
||
log.debug(`${oldPath} -> ${newPath}`);
|
||
|
||
file.renamePathSync(oldPath, newPath);
|
||
|
||
finalList.push(`${newPrefix}.${baseName}`);
|
||
usedNumbers.add(newNum);
|
||
}
|
||
});
|
||
|
||
// 排序
|
||
finalList.sort((a, b) => {
|
||
const na = parseInt(a.substring(0, 4), 10);
|
||
const nb = parseInt(b.substring(0, 4), 10);
|
||
return na - nb;
|
||
});
|
||
|
||
return finalList; // 返回不带 .json 的文件名
|
||
};
|
||
|
||
|
||
/**
|
||
*
|
||
* 根据乐曲文件名生成乐曲文件路径
|
||
*
|
||
* @param music_name 乐曲文件名
|
||
* @returns {string} 乐曲文件路径
|
||
*/
|
||
function pathJoin(music_name) {
|
||
return base_path + music_name + ".json";
|
||
}
|
||
|
||
/**
|
||
* 获取JS脚本配置
|
||
*
|
||
* @returns {Object} 包含解析后JS脚本配置的对象,具有以下属性:
|
||
* @property {Number} startTime - 目标时间的时间戳
|
||
* @property {Number} playType - 播放模式,使用PlayType枚举
|
||
* @property {Array[String]} musicQueue - 乐曲队列,包含乐曲文件名的数组
|
||
* @property {Number} queueInterval - 乐曲队列间隔时间,单位为秒
|
||
* @property {Number} repeatTimes - 循环执行次数
|
||
* @property {Number} repeatInterval - 循环间隔时间,单位为秒
|
||
* @property {Boolean} debug - 是否启用调试模式
|
||
*
|
||
*/
|
||
function get_settings() {
|
||
const Settings = {
|
||
startTime: 0,
|
||
playType: undefined,
|
||
musicQueue: [],
|
||
queueInterval: 0,
|
||
repeatTimes: 1,
|
||
repeatInterval: 0,
|
||
debug: false
|
||
}
|
||
|
||
|
||
/**
|
||
* @param {String} timeString
|
||
* @returns {Number} 目标时间运行当天的时间戳
|
||
* @example
|
||
* console.log(calTargetTimeStamp('14:30:00')) // at 2025/9/10
|
||
* -> 1757485800000 (2025/9/10 14:30:00)的时间戳
|
||
*/
|
||
const calTargetTimeStamp = (timeString) => {
|
||
const [hours, minutes, seconds] = timeString.replace(/[^0-9:]/g, "").split(':').map(Number);
|
||
|
||
const now = new Date();
|
||
const year = now.getFullYear();
|
||
const month = now.getMonth();
|
||
const day = now.getDate();
|
||
|
||
const localDate = new Date(year, month, day, hours, minutes, seconds);
|
||
return localDate.getTime();
|
||
}
|
||
try {
|
||
// 读取开始时间
|
||
let music_start = typeof (settings.music_start) === 'undefined' ? "00:00:00" : settings.music_start;
|
||
Settings.startTime = calTargetTimeStamp(music_start);
|
||
// 读取播放模式
|
||
let type_select = typeof (settings.type_select) === 'undefined' ? "单曲单次执行" : settings.type_select;
|
||
switch (type_select) {
|
||
case "单曲单次执行":
|
||
Settings.playType = PlayType.SingleMusicOnce;
|
||
break;
|
||
case "单曲循环":
|
||
Settings.playType = PlayType.SingleMusicRepeat;
|
||
break;
|
||
case "队列单次执行":
|
||
Settings.playType = PlayType.QueueMusicOnce;
|
||
break;
|
||
case "队列循环":
|
||
Settings.playType = PlayType.QueueMusicRepeat;
|
||
break;
|
||
default:
|
||
Settings.playType = PlayType.SingleMusicOnce;
|
||
break;
|
||
}
|
||
|
||
// 读取队列间隔时间
|
||
Settings.queueInterval = (typeof (settings.music_interval) === 'undefined') ? (0) : parseInt(settings.music_interval, 10);
|
||
// 读取循环次数
|
||
Settings.repeatTimes = (typeof (settings.music_repeat) === 'undefined') ? (1) : parseInt(settings.music_repeat, 10);
|
||
// 读取循环间隔时间
|
||
Settings.repeatInterval = (typeof (settings.repeat_interval) === 'undefined') ? (0) : parseInt(settings.repeat_interval, 10);
|
||
// 读取乐曲队列 Array[musicName]
|
||
if (Settings.playType === PlayType.SingleMusicOnce || Settings.playType === PlayType.singleRepeat) {
|
||
Settings.musicQueue.push((typeof (settings.music_selector) === 'undefined') ? (undefined) : (settings.music_selector));
|
||
}
|
||
else {
|
||
let music_queue = (typeof (settings.music_queue) === 'undefined') ? (undefined) : (settings.music_queue);
|
||
if (music_queue === undefined) throw new Error("队列执行无序号");
|
||
let musicIndex = Array.from(new Set(music_queue.split(' ').filter(item => item !== ""))); // 去重
|
||
musicList().forEach(music => {
|
||
for (let index = 0; index < musicIndex.length; index += 1) {
|
||
if (music.includes(musicIndex[index])) {
|
||
Settings.musicQueue.push(music);
|
||
musicIndex.splice(index, 1);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
Settings.debug = (typeof (settings.debug) === 'undefined') ? (false) : (settings.debug === "启用");
|
||
return Settings;
|
||
|
||
} catch (error) {
|
||
log.error(`读取JS脚本配置时出错:${error}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
* 读取并解析一个乐谱文件
|
||
*
|
||
* @param music_name {string} 乐曲文件名
|
||
* @returns {Promise<{}|null>}
|
||
* @property {string} name 乐曲名称
|
||
* @property {string} author 作者
|
||
* @property {string} instrument 建议乐器
|
||
* @property {string} description 乐曲描述
|
||
* @property {string} type 乐曲类型
|
||
* @property {number} bpm BPM
|
||
* @property {string} time_signature 拍号
|
||
* @property {string} composer 作曲者
|
||
* @property {string} arranger 编曲者
|
||
* @property {Object[][]} notes 乐谱内容
|
||
*/
|
||
function getMusicInfo(music_name) {
|
||
const MusicInfo = {
|
||
name: undefined, // 乐曲名称
|
||
author: undefined, // 作者
|
||
instrument: undefined, // 乐器
|
||
description: undefined, // 乐曲描述
|
||
type: undefined, // 乐曲类型
|
||
bpm: undefined, // BPM
|
||
time_signature: undefined, // 拍号
|
||
composer: undefined, // 作曲者
|
||
arranger: undefined, // 编曲者
|
||
notes: undefined, // 乐谱内容
|
||
}
|
||
|
||
let music_path = pathJoin(music_name);
|
||
let file_text = ""; // 存储乐曲文件内容
|
||
// 读取并检查文件
|
||
try {
|
||
file_text = file.readTextSync(music_path);
|
||
} catch (error) {
|
||
log.error(`文件无法读取:${music_path}\nerror:${error}`);
|
||
}
|
||
|
||
if (file_text == null) { // 检测文件是否读取
|
||
log.error(`读取文件 ${music_path} 错误,文件为空`);
|
||
return null;
|
||
}
|
||
// else {
|
||
// log.info(`文件读取成功: ${music_path}`);
|
||
// }
|
||
|
||
let music_msg_dic = JSON.parse(file_text);
|
||
let regex_blank = /[\n]/g;
|
||
|
||
MusicInfo.name = (music_msg_dic.name !== undefined) ? (music_msg_dic.name) : ("未知曲名");
|
||
MusicInfo.author = (music_msg_dic.author !== undefined) ? (music_msg_dic.author) : ("未知作者");
|
||
MusicInfo.instrument = (music_msg_dic.instrument !== undefined) ? (music_msg_dic.instrument) : ("风物之诗琴");
|
||
MusicInfo.description = (music_msg_dic.description !== undefined) ? (music_msg_dic.description) : ("无描述");
|
||
MusicInfo.composer = (music_msg_dic.composer !== undefined) ? (music_msg_dic.composer) : ("未知作曲者");
|
||
MusicInfo.arranger = (music_msg_dic.arranger !== undefined) ? (music_msg_dic.arranger) : ("未知编曲者");
|
||
// 必要信息
|
||
MusicInfo.type = (music_msg_dic.type !== undefined) ? (music_msg_dic.type) : ("yuanqin");
|
||
MusicInfo.bpm = (music_msg_dic.bpm !== undefined) ? (music_msg_dic.bpm) : (120);
|
||
MusicInfo.time_signature = (music_msg_dic.time_signature !== undefined) ? (music_msg_dic.time_signature) : ("4/4");
|
||
MusicInfo.ticks = (music_msg_dic.ticks !== undefined) ? (music_msg_dic.ticks) : (480);
|
||
|
||
if (music_msg_dic.notes === undefined) {
|
||
log.error(`文件 ${music_name} 无乐曲信息`);
|
||
return null;
|
||
}
|
||
|
||
switch (MusicInfo.type) {
|
||
case "yuanqin":
|
||
MusicInfo.notes = parseMusicSheet(music_msg_dic.notes.replace(regex_blank, ""));
|
||
break;
|
||
case "midi":
|
||
MusicInfo.notes = music_msg_dic.notes;
|
||
break;
|
||
case "keyboard":
|
||
MusicInfo.notes = keySheetSerialization(music_msg_dic.notes);
|
||
default:
|
||
break;
|
||
}
|
||
|
||
return MusicInfo;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* 执行单音
|
||
*
|
||
* @param key {string}
|
||
*
|
||
*/
|
||
async function play_note(key) {
|
||
keyDown(key);
|
||
keyUp(key);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* 执行和弦
|
||
*
|
||
* @param keys {Array.string}
|
||
*
|
||
*/
|
||
async function play_chord(keys) {
|
||
for (const key of keys) {
|
||
play_note(key);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 音符小节序列演奏
|
||
* @typedef {[Number,[Map]]} Bar
|
||
* @param {Bar[]} bar_list
|
||
* @param {Number} gap 一拍的时长,单位ms
|
||
* @property {Number} barTime 小节时长
|
||
* @property {[Map]} notes 一个小节中所有音符的信息
|
||
*/
|
||
async function listNotePlay(bar_list, gap) {
|
||
/**
|
||
* 按键模拟
|
||
* 不使用await修饰调用,利用javascript特性实现异步弹奏
|
||
*
|
||
* @param {Map} note
|
||
* @param {Number} gap
|
||
* @description offset:小节开始时此音符需要先等待多久,单位为一拍时间
|
||
* @description key:键盘按键
|
||
* @description time:此音符需要持续的时长,单位为一拍时间
|
||
*/
|
||
async function notePlay(note, gap) {
|
||
const wait = note["offset"];
|
||
const key = note["key"];
|
||
const time = note["time"];
|
||
await sleep(Math.floor(wait * gap));
|
||
keyDown(key);
|
||
await sleep(Math.floor(time * gap));
|
||
keyUp(key);
|
||
}
|
||
log.info(`总计 ${bar_list.length} 小节, 预计演奏时长 ${(bar_list.length * gap * bar_list[0][0] / 1000).toFixed(2)}秒`);
|
||
for (let i = 0; i < bar_list.length; i++) {
|
||
let bar = bar_list[i];
|
||
let barTime = bar[0];
|
||
let notes = bar.slice(1);
|
||
for (let j = 0; j < notes.length; j++) {
|
||
let note = notes[j];
|
||
notePlay(note, gap); // 启动音符异步函数
|
||
}
|
||
if (DEBUG) {
|
||
log.info(`${i} / ${bar_list.length} ${(i / bar_list.length * 100).toFixed(2)}%`)
|
||
}
|
||
await sleep(Math.floor(barTime * gap)); // 等待小节结束
|
||
}
|
||
await sleep(Math.floor(gap * 8)); // 额外等待
|
||
}
|
||
|
||
/**
|
||
* 将乐谱键位字符串序列化为按小节分组的音符对象数组
|
||
*
|
||
* 此函数处理自定义记谱字符串,将其解析为音符组,展开嵌套组,合并相邻音符,并按小节分组
|
||
* 每个小节表示为一个数组,首元素为小节长度(固定为4),后接包含键位、偏移量和时间属性的音符对象
|
||
*
|
||
* @param {string} stringSheet - 待序列化的键位乐谱字符串
|
||
* @returns {Array<Array<number|Object>>} - 小节数组,每个小节为数组结构(首元素为长度4,后接音符对象):
|
||
* - { key: string, offset: number, time: number }
|
||
*
|
||
* @example
|
||
* const testString = "(QH) DQ/D-G-/[(HF)A] A /FH(QH) / (QG)SJ>(JG)Q(WJ)G>[G0E00]/-(EA)-DF/(GD)H(GD)F/";
|
||
* const result = keySheetSerialization(testString);
|
||
* // 返回结果: [
|
||
* // [4, { key: 'Q', offset: 0, time: 1 }, ...],
|
||
* // [4, { key: 'D', offset: 0, time: 2 }, ...],
|
||
* // ...
|
||
* // ]
|
||
*/
|
||
function keySheetSerialization(stringSheet) {
|
||
/**
|
||
* 函数是安全的,在处理按键序列时不会触发回溯地狱
|
||
* @param {String} inputString
|
||
* @example
|
||
* const testString = "(QH) DQ/D-G-/[(HF)A] A /FH(QH) / (QG)SJ>(JG)Q(WJ)G>[G0E00]/-(EA)-DF/(GD)H(GD)F/";
|
||
* console.log("原始字符串:", testString);
|
||
* console.log("转换后字符串:", keySheetProcess(testString));
|
||
* input : "(QH) DQ/D-G-/[(HF)A] A /FH(QH) / (QG)SJ>(JG)Q(WJ)G>[G0E00]/-(EA)-DF/(GD)H(GD)F/"
|
||
* output : "(QH)0DQ{D}-G-{[(HF)A]}0A0{F}H(QH)00(QG)SJ(JG)Q(WJ)G[G0E00]-(EA)-DF{GD}H(GD)F"
|
||
*/
|
||
const keySheetProcess = (inputString) => {
|
||
return inputString
|
||
.replace(/\/\(([^)]+)\)/g, '{$1}') // 替换 /(content) 为 {content}
|
||
.replace(/\/([A-Z])/g, '{$1}') // 替换 /X 为 {X}
|
||
.replace(/ /g, "0") // 替换空格为 0
|
||
.replace(/\/\[([^\]]+)\]/g, '{[$1]}') // 替换 /[content] 为 {[content]}
|
||
.replace(/[\/\>]/g, ""); // 删除所有 / 和 >
|
||
};
|
||
|
||
/**
|
||
* @typedef {Array} noteInfo
|
||
* @param {String} processedString 处理完成的字符串,只有A-Z,0,-,()[]{}
|
||
* @returns {[noteInfo[]]}
|
||
*/
|
||
const keySheetParse = (processedString) => {
|
||
const isLeftBrackets = (char) => ((char.length === 1) && (/[\(\[\{]/.test(char)));
|
||
const isRightBrackets = (char) => ((char.length === 1) && (/[\)\]\}]/.test(char)));
|
||
class GroupProcess {
|
||
constructor() {
|
||
this.stack = [{ type: 'ROOT', listKey: [] }];
|
||
this.current = this.stack[0];
|
||
}
|
||
push(char) {
|
||
if (isLeftBrackets(char)) {
|
||
const newGroup = { type: char, listKey: [] };
|
||
this.current.listKey.push(newGroup);
|
||
this.stack.push(newGroup);
|
||
this.current = newGroup;
|
||
}
|
||
else if (isRightBrackets(char)) {
|
||
if (this.stack.length > 1) {
|
||
this.stack.pop();
|
||
this.current = this.stack[this.stack.length - 1];
|
||
}
|
||
}
|
||
else if (char !== '-') {
|
||
this.current.listKey.push(char);
|
||
}
|
||
return this;
|
||
}
|
||
invaildMatch() { return ((this.stack.length === 1) && (this.stack[0].listKey.length !== 0)); }
|
||
clear() {
|
||
this.stack = [{ type: 'ROOT', listKey: [] }];
|
||
this.current = this.stack[0];
|
||
}
|
||
genAll() {
|
||
let out = this.stack[0].listKey[0];
|
||
if ((typeof out) === "string") out = { type: "{", listKey: [out] };
|
||
out.mult = 1;
|
||
this.clear();
|
||
return out;
|
||
}
|
||
}
|
||
|
||
let group = new GroupProcess(); // 处理流程1
|
||
let groupProess = new Array(); // 处理流程2
|
||
for (let i = 0; i < processedString.length; i++) {
|
||
const char = processedString[i];
|
||
if (char !== "-") { group.push(char); }
|
||
else { groupProess[groupProess.length - 1].mult += 1; }
|
||
|
||
if (group.invaildMatch()) { groupProess.push(group.genAll()); }
|
||
}
|
||
// console.dir(groupProess, { depth: null });
|
||
return groupProess;
|
||
}
|
||
|
||
function unfoldGroup(input) {
|
||
const unfoldGroup = [];
|
||
let cumulativeBeats = 0;
|
||
|
||
const processGroup = (group, mult, beats, baseOffset) => {
|
||
let offset = baseOffset;
|
||
if (group.type === '{') offset += 0.001;
|
||
|
||
if (group.type === '[') {
|
||
const unitTime = mult / group.listKey.length;
|
||
group.listKey.forEach((item, i) => {
|
||
const itemOffset = offset + i * unitTime;
|
||
if (typeof item === 'string') {
|
||
if (item !== '0') unfoldGroup.push({ beats, offset: itemOffset, key: item, time: unitTime });
|
||
} else {
|
||
processGroup(item, unitTime, beats, itemOffset);
|
||
}
|
||
});
|
||
} else {
|
||
group.listKey.forEach(item => {
|
||
if (typeof item === 'string') {
|
||
if (item !== '0') unfoldGroup.push({ beats, offset, key: item, time: mult });
|
||
} else {
|
||
processGroup(item, mult, beats, offset);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
input.forEach(group => {
|
||
const groupBeats = cumulativeBeats;
|
||
cumulativeBeats += group.mult;
|
||
processGroup(group, group.mult, groupBeats, 0);
|
||
});
|
||
|
||
return unfoldGroup;
|
||
}
|
||
|
||
function mergeGroup(notes) {
|
||
const buckets = {};
|
||
|
||
notes.forEach(note => {
|
||
if (!buckets[note.key]) {
|
||
buckets[note.key] = [];
|
||
}
|
||
buckets[note.key].push({ ...note });
|
||
});
|
||
|
||
const mergedNotes = [];
|
||
|
||
Object.keys(buckets).forEach(key => {
|
||
const bucket = buckets[key];
|
||
bucket.sort((a, b) => (a.beats + a.offset) - (b.beats + b.offset));
|
||
|
||
let i = 0;
|
||
while (i < bucket.length - 1) {
|
||
const current = bucket[i];
|
||
const next = bucket[i + 1];
|
||
const currentEnd = current.beats + current.time;
|
||
const nextStart = next.beats + next.offset;
|
||
|
||
if (Math.abs(currentEnd - nextStart) < 0.01) {
|
||
current.time += next.time;
|
||
bucket.splice(i + 1, 1);
|
||
} else {
|
||
i++;
|
||
}
|
||
}
|
||
|
||
mergedNotes.push(...bucket);
|
||
});
|
||
|
||
mergedNotes.sort((a, b) => {
|
||
if (a.beats !== b.beats) return a.beats - b.beats;
|
||
return a.offset - b.offset;
|
||
});
|
||
|
||
return mergedNotes;
|
||
}
|
||
let SerializedKey = keySheetProcess(stringSheet);
|
||
SerializedKey = keySheetParse(SerializedKey);
|
||
SerializedKey = unfoldGroup(SerializedKey);
|
||
SerializedKey = mergeGroup(SerializedKey);
|
||
const grouped = [];
|
||
const wholeBeats = Math.floor(SerializedKey[SerializedKey.length - 1].beats / 4) + 1;
|
||
for (let i = 0; i < wholeBeats; i++) {
|
||
grouped.push([4]);
|
||
}
|
||
for (const note of SerializedKey) {
|
||
grouped[Math.floor(note.beats / 4)].push({ offset: note.beats % 4 + note.offset, key: note.key, time: note.time });
|
||
}
|
||
return grouped;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* 解析乐谱字符串(乐谱JSON文件中的notes)
|
||
*
|
||
* 小节之间用|隔开且乐谱中不能有空格,单个小节的解析规则如下:
|
||
* A[4] 表示按下A键,A键视作四分音符
|
||
* (ASD)[4-#] 表示同时按下ASD键,这个和弦视作四分音符的装饰音
|
||
* A[4-3](AS)[4-3](ASD)[4-3] 表示等分四分音符的三连音(-后填3必须要连着写三个这样的音符),按顺序按下A、AS、ASD键
|
||
* @[4] 表示休止符,中括号内标明这是几分休止符,例如这里表示4分休止符
|
||
* 附:
|
||
* 中括号(-前表示音符类型-后用于区分特殊音符):[填4表示4分音符,填16表示16分音符...-填#表示装饰音,填3表示三连音] 例:[16-#]
|
||
*
|
||
* @param sheet {string} 乐谱 [DEBUG]更新midi后这里也会是一个字典
|
||
* @returns {Object[][]}
|
||
*/
|
||
function parseMusicSheet(sheet) {
|
||
let result = [];
|
||
|
||
if (typeof (sheet) === "object") {
|
||
result = sheet;
|
||
} else {
|
||
// 将输入字符串按照小节分割
|
||
let bars = sheet.split('|');
|
||
|
||
// 遍历每个小节
|
||
bars.forEach(bar => {
|
||
let i = 0;
|
||
|
||
// 逐个字符解析小节中的音符及其属性
|
||
while (i < bar.length) {
|
||
let note = ''; // 存储音符
|
||
let type = ''; // 存储音符类型
|
||
let chord = false; // 判断是否为和弦
|
||
let spl = 'none'; // 存储特殊音符属性,默认值为 "none"
|
||
|
||
// 检查是否为和弦(和弦用圆括号包裹)
|
||
if (bar[i] === '(') {
|
||
chord = true;
|
||
i++;
|
||
while (bar[i] !== ')') {
|
||
note += bar[i];
|
||
i++;
|
||
}
|
||
i++; // 跳过闭合圆括号
|
||
} else if (bar[i] === '@') {
|
||
// 处理休止符
|
||
note = '@';
|
||
i++;
|
||
} else {
|
||
note = bar[i];
|
||
i++;
|
||
}
|
||
|
||
// 解析音符类型(用方括号包裹)
|
||
if (bar[i] === '[') {
|
||
i++;
|
||
while (bar[i] !== ']') {
|
||
type += bar[i];
|
||
i++;
|
||
}
|
||
i++; // 跳过闭合方括号
|
||
}
|
||
|
||
// 解析特殊音符属性(如果type中包含'-')
|
||
if (type.includes('-')) {
|
||
let splIndex = type.indexOf('-');
|
||
spl = type.slice(splIndex + 1);
|
||
type = parseInt(type.slice(0, splIndex), 10);
|
||
}
|
||
|
||
// 将解析结果添加到parsedNotes数组中
|
||
result.push({
|
||
"note": note,
|
||
"type": type,
|
||
"chord": chord,
|
||
"spl": spl
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* 根据解析后的乐谱进行演奏
|
||
*
|
||
* @param sheet_list {Object[][]} 解析后的乐谱
|
||
* @param bpm BPM (240)
|
||
* @param ts 拍号 (3/4)
|
||
* @param ticks ticks per beat (MIDI用)
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async function play_sheet(sheet_list, bpm, ts, ticks = 480) {
|
||
/**
|
||
*
|
||
* 计算当前音符的时长(检测音符后是否有装饰音)
|
||
*
|
||
* @param sheet_list {Object[][]} 解析后的乐谱
|
||
* @param symbol_time 每一拍的时间
|
||
* @param symbol 以几分音符为一拍
|
||
* @param note_type 音符类型
|
||
* @param count 当前音符下标
|
||
* @param note_time 当前音符的时长(默认为undefined,不为空时symbol note_type count实效)
|
||
* @returns {number}
|
||
*/
|
||
function cal_time_ornament(sheet_list, symbol_time, symbol, note_type, count, note_time = undefined) {
|
||
try {
|
||
if (note_time === undefined) {
|
||
// 该音符的正常时长
|
||
note_time = Math.round(symbol_time * (symbol / note_type));
|
||
}
|
||
// 装饰音时长
|
||
let ornament_time = Math.round(symbol_time / 16)
|
||
|
||
let check_count = count + 1;
|
||
let ornament_count = 0; // 装饰音计数
|
||
|
||
while (check_count < sheet_list.length) { // 装饰音不可能在曲谱末尾,else会在匹配不到装饰音的循环触发
|
||
if (sheet_list[check_count]["spl"] === "#") {
|
||
ornament_count += 1;
|
||
} else {
|
||
if (ornament_count === 0) {
|
||
return note_time;
|
||
} else {
|
||
// 装饰音占用的时间过长就不预留时间
|
||
if (ornament_time * ornament_count < note_time) {
|
||
return note_time - ornament_time * ornament_count;
|
||
} else {
|
||
return note_time;
|
||
}
|
||
}
|
||
}
|
||
check_count += 1;
|
||
}
|
||
} catch (error) {
|
||
log.error(`出错(cal_time_ornament): ${error}`);
|
||
}
|
||
|
||
}
|
||
// 如果是midi转换的乐谱
|
||
if (typeof(sheet_list) === "string") {
|
||
let play_sheet = sheet_list.split("|");
|
||
let base_time = 60000 / (bpm * ticks); // second per beat - 每tick多少毫秒
|
||
for (let i = 0; i < play_sheet.length; i++) {
|
||
let current_note = play_sheet[i];
|
||
log.debug(`${current_note[0]}-${current_note[1]}-${current_note.slice(2)}`);
|
||
let current_ticks = Math.round(current_note.slice(2));
|
||
let wait_time = Math.round(current_ticks * base_time);
|
||
await sleep(wait_time);
|
||
if (current_note[1] === "@") continue;
|
||
if (current_note[0] === "D") {
|
||
keyDown(current_note[1]);
|
||
} else {
|
||
keyUp(current_note[1]);
|
||
}
|
||
}
|
||
} else {
|
||
// 确定是以几分音符为一拍
|
||
let symbol = parseInt(ts.split("/")[1], 10);
|
||
// 每拍所需的时间
|
||
let symbol_time = Math.round(60000 / bpm);
|
||
// 装饰音时长
|
||
let ornament_time = Math.round(symbol_time / 16)
|
||
// 存储连音
|
||
let temp_legato = [];
|
||
|
||
// test 需要额外计算装饰音时值的影响
|
||
for (let i = 0; i < sheet_list.length; i++) {
|
||
// 显示正在演奏的音符
|
||
if (DEBUG) {
|
||
log.info(`${sheet_list[i]["note"]}[${sheet_list[i]["type"]}-${sheet_list[i]["spl"]}]`);
|
||
}
|
||
if (sheet_list[i]["spl"] === 'none') { // 单音、休止符或和弦
|
||
if (sheet_list[i]["chord"]) {
|
||
await play_chord(sheet_list[i]["note"]); // 和弦
|
||
} else {
|
||
if (sheet_list[i]["note"] === '@') { // 休止符
|
||
// pass
|
||
} else {
|
||
await play_note(sheet_list[i]["note"]); // 单音
|
||
}
|
||
}
|
||
|
||
if (i !== sheet_list.length - 1) {
|
||
await sleep(cal_time_ornament(sheet_list, symbol_time, symbol, sheet_list[i]["type"], i));
|
||
}
|
||
} else if (sheet_list[i]["spl"] === '#') { // 装饰音(不会包含休止符),时值为symbol的时值的1/16
|
||
if (sheet_list[i]["chord"]) {
|
||
await play_chord(sheet_list[i]["note"]); // 和弦
|
||
} else {
|
||
await play_note(sheet_list[i]["note"]); // 单音
|
||
}
|
||
if (i !== sheet_list.length - 1) {
|
||
await sleep(ornament_time);
|
||
}
|
||
} else if (/\.([36$])/.test(sheet_list[i]["spl"])) { // 三连音/六连音(可能包含休止符)
|
||
temp_legato.push({
|
||
"note": sheet_list[i]["note"],
|
||
"chord": sheet_list[i]["chord"],
|
||
"type": sheet_list[i]["type"],
|
||
"spl": sheet_list[i]["spl"]
|
||
});
|
||
|
||
// 演奏连音
|
||
if (sheet_list[i]["spl"].includes("$")) {
|
||
// 连音的总时长
|
||
let time_legato = Math.round(symbol_time * (symbol / sheet_list[i]["type"]));
|
||
// 当前音符类型
|
||
let current_type = parseInt(sheet_list[i]["spl"].split(/\./)[0])
|
||
// 连音的音符数值总和(用于计算当前音符时长)
|
||
let time_all = 0;
|
||
for (let j = 0; j < temp_legato.length; j++) {
|
||
time_all += 1 / parseInt(temp_legato[j]["spl"].split(/\./)[0], 0);
|
||
}
|
||
// 计数
|
||
let count = 0;
|
||
|
||
for (let j = 0; j < temp_legato.length; j++) {
|
||
// 当前音符时长
|
||
let time_current = Math.round(time_legato * (1 / parseInt(temp_legato[j]["spl"].split(/\./)[0], 0)) / time_all);
|
||
|
||
if (temp_legato[j]["chord"]) {
|
||
await play_chord(temp_legato[j]["note"]); // 和弦
|
||
} else {
|
||
if (temp_legato[j]["note"] === '@') { // 休止符
|
||
// pass
|
||
} else {
|
||
await play_note(temp_legato[j]["note"]); // 单音
|
||
}
|
||
}
|
||
if (count < temp_legato.length) {
|
||
await sleep(time_current);
|
||
} else if (count === temp_legato.length - 1) {
|
||
if (i !== sheet_list.length - 1) {
|
||
// 计算连音的最后一个音的时值(计算装饰音)
|
||
await sleep(cal_time_ornament(sheet_list, symbol_time, symbol, sheet_list[i]["type"], i, time_current));
|
||
}
|
||
} else if (i !== sheet_list.length - 1) {
|
||
await sleep(time_current);
|
||
}
|
||
count += 1;
|
||
}
|
||
// 重置连音缓存区
|
||
temp_legato = [];
|
||
}
|
||
} else if (sheet_list[i]["spl"] === '*') { // 附点音符
|
||
if (sheet_list[i]["chord"]) {
|
||
await play_chord(sheet_list[i]["note"]); // 和弦
|
||
} else {
|
||
if (sheet_list[i]["note"] === '@') { // 休止符
|
||
// pass
|
||
} else {
|
||
await play_note(sheet_list[i]["note"]); // 单音
|
||
}
|
||
}
|
||
// 排除尾音
|
||
if (i !== sheet_list.length - 1) {
|
||
await sleep(cal_time_ornament(sheet_list, symbol_time * 1.5, symbol, sheet_list[i]["type"], i));
|
||
}
|
||
} else {
|
||
log.info(`错误: ${sheet_list[i]["spl"]}`);
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
async function waitTargetTime(targetTimeStamp) {
|
||
let now = new Date();
|
||
if (now.getTime() >= targetTimeStamp) return;
|
||
log.info(`等待至目标时间: ${new Date(targetTimeStamp).toLocaleString()}`);
|
||
if ((targetTimeStamp - now.getTime()) > 100) {
|
||
await sleep(targetTimeStamp - now.getTime() - 100);
|
||
}
|
||
while (Date.now() < targetTimeStamp) {
|
||
}
|
||
return;
|
||
}
|
||
|
||
/**
|
||
* 检查本地曲谱文件与主程序配置是否一致,并自动修正配置settings文件。
|
||
*
|
||
* @returns {boolean} 如果一致返回 true,否则返回 false。
|
||
*/
|
||
function checkSheetFile() {
|
||
try {
|
||
// 1. 读取本地所有JSON曲谱文件
|
||
const localMusicList = musicList();
|
||
|
||
// 2. 读取JS脚本配置中的曲谱列表
|
||
const settings = JSON.parse(file.readTextSync("settings.json"));
|
||
let configMusicList = undefined;
|
||
let indexOfMusicSelector = -1;
|
||
for (let i = 0; i < settings.length; i++) {
|
||
if (settings[i].name === "music_selector") {
|
||
indexOfMusicSelector = i;
|
||
configMusicList = settings[i].options;
|
||
break;
|
||
}
|
||
}
|
||
// 3. 核对两个列表是否相同
|
||
const areArraysEqual = (a, b) => {
|
||
if (a.length !== b.length) return false;
|
||
const sortedA = [...a].sort();
|
||
const sortedB = [...b].sort();
|
||
return sortedA.every((item, index) => item === sortedB[index]);
|
||
};
|
||
|
||
if (!areArraysEqual(localMusicList, configMusicList)) {
|
||
// 以本地曲谱为准更新配置
|
||
const updatedSettings = [...settings];
|
||
updatedSettings[indexOfMusicSelector].options = localMusicList;
|
||
file.writeTextSync("settings.json", JSON.stringify(updatedSettings, null, 2));
|
||
log.warn("检测到曲谱文件不一致, 已自动修改settings(以本地曲谱文件为基准)...");
|
||
log.warn("JS脚本配置已更新, 请重新运行脚本!");
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
log.error("检查曲谱文件时发生错误:", error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检测并切换乐器
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async function autoSwitchInstrument(instrument) {
|
||
let switchFlag = true;
|
||
// 解析出需要更换的乐器 [DEBUG]多乐器未适配,目前仅选择第一个
|
||
if (instrument.includes(",")) {
|
||
instrument = instrument.split(",")[0]
|
||
}
|
||
// 确认是否已在正确的乐器界面
|
||
let sRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`assets/setting.png`), 1578, 10, 80, 80);
|
||
let gameRegion = captureGameRegion();
|
||
let result = gameRegion.Find(sRo);
|
||
gameRegion.dispose();
|
||
if (result.isExist()) {
|
||
click(1618, 48);
|
||
for (let i = 0; i < 30; i++) {
|
||
let gameRegion = captureGameRegion();
|
||
let result = gameRegion.Find(sRo);
|
||
if (!(result.isExist())) {
|
||
let insName = await Ocr(1035, 166, 254, 109);
|
||
if (insName && insName.text.includes(instrument)) { // 当前乐器正确
|
||
log.info(`当前乐器:${insName.text} (期望:${instrument})`);
|
||
keyPress("Escape");
|
||
switchFlag = false;
|
||
break;
|
||
} else if (insName && !(insName.text.includes(instrument))) { // 当前乐器错误
|
||
log.info(`当前乐器:${insName.text} (期望:${instrument})`);
|
||
await genshin.returnMainUi();
|
||
break;
|
||
} else {
|
||
log.debug(`设置界面未识别到乐器文本... - ${i}`);
|
||
await sleep(300);
|
||
if (i === 29) {
|
||
log.error("打开设置界面超时...");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
await genshin.returnMainUi();
|
||
}
|
||
|
||
if (switchFlag) {
|
||
// 打开背包-小道具
|
||
keyPress("B");
|
||
await sleep(1000);
|
||
click(1054, 48);
|
||
await sleep(1000);
|
||
// 查找乐器
|
||
let insRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`assets/instruments/${instrument}.png`), 97, 75, 1191, 891);
|
||
for (let i = 0; i < 5; i++) {
|
||
gameRegion = captureGameRegion();
|
||
result = gameRegion.Find(insRo);
|
||
if (result.isExist()) {
|
||
await sleep(500);
|
||
result.click();
|
||
await sleep(500);
|
||
let ocrText = await Ocr(1656, 993, 92, 47);
|
||
if (ocrText && ocrText.text.includes("替换")) {
|
||
click(1686, 1016);
|
||
await sleep(300);
|
||
}
|
||
keyPress("Escape");
|
||
log.info(`乐器更换完成(${instrument}),将在7s后开始演奏...`);
|
||
await sleep(5000);
|
||
keyPress("Z");
|
||
await sleep(2000);
|
||
return true;
|
||
} else {
|
||
await scroll_page(1283, 113, 11, 837, 133, 931, 1288, "Down");
|
||
await sleep(200);
|
||
}
|
||
}
|
||
log.error(`未找到乐器,请确保已经购买了乐器: ${instrument}`);
|
||
await sleep(10000);
|
||
return false;
|
||
} else {
|
||
log.info("将在3s后开始演奏...");
|
||
await sleep(3000);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ------- 主程序 --------
|
||
*/
|
||
async function main() {
|
||
if (!checkSheetFile()) return;
|
||
|
||
let settings_msg = get_settings();
|
||
DEBUG = settings_msg.debug;
|
||
console.log(`${settings_msg}`)
|
||
|
||
const music_infos = [];
|
||
for (const music_name of settings_msg.musicQueue) {
|
||
const music_info = getMusicInfo(music_name);
|
||
if (music_info === null) {
|
||
log.error(`乐曲 ${music_name} 信息有误,已跳过`);
|
||
continue;
|
||
}
|
||
music_infos.push(music_info);
|
||
}
|
||
|
||
|
||
const alwaysRepeat = ((settings_msg.playType === PlayType.SingleMusicRepeat || settings_msg.playType === PlayType.QueueMusicRepeat) && (settings_msg.repeatTimes === 0));
|
||
await waitTargetTime(settings_msg.startTime);
|
||
// try {
|
||
do {
|
||
for (const music_info of music_infos) {
|
||
if (settings.auto_switch) {
|
||
await autoSwitchInstrument(music_info.instrument); // 检测并切换乐器
|
||
} else {
|
||
log.info(`建议演奏乐器:${music_info.instrument}`);
|
||
}
|
||
log.info(`开始演奏: ${music_info.name} - ${music_info.author}`);
|
||
switch (music_info.type) {
|
||
case "yuanqin":
|
||
await play_sheet(music_info.notes, music_info.bpm, music_info.time_signature);
|
||
break;
|
||
case "midi":
|
||
await play_sheet(music_info.notes, music_info.bpm, music_info.time_signature, music_info.ticks);
|
||
break;
|
||
case "keyboard":
|
||
if (DEBUG) {
|
||
log.info(`乐曲已打印至${music_info.name}.json`)
|
||
let info = []
|
||
music_info.notes.forEach((note, index) => {
|
||
info.push([index, ...note]);
|
||
});
|
||
file.writeTextSync(`${music_info.name}.json`, `${JSON.stringify(info)}`);
|
||
}
|
||
await listNotePlay(music_info.notes, (60000 / music_info.bpm));
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
if (settings_msg.queueInterval > 0) await sleep(settings_msg.queueInterval * 1000);
|
||
}
|
||
if (settings_msg.repeatInterval > 0) await sleep(settings_msg.repeatInterval * 1000);
|
||
} while (alwaysRepeat || --settings_msg.repeatTimes > 0);
|
||
// } catch (error) {
|
||
// if (DEBUG) {
|
||
// log.error(`脚本执行错误 ${error} erron.txt 已打印`)
|
||
// file.writeTextSync("erron.txt", `${error.stack}`);
|
||
// }
|
||
// else {
|
||
// log.error(`脚本执行错误 ${error}`)
|
||
// }
|
||
// }
|
||
}
|
||
await main();
|
||
|
||
})();
|