Files
bettergi-scripts-list/repo/js/YuanQinAssistant/main.js
himno 9ee34fa389 JS脚本:YuanQinAssistant 更新至 v1.0.2 (#2450)
* 创建image文件夹

* add ro template file -- mouse.png

* Delete repo/js/YuanQinAssistant/assets/image/placer.txt

* Add files via upload

* update YuanQinAssistant v1.0.2
2025-12-07 17:40:20 +08:00

442 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.
(async function () {
// 所有的代码必须由 async function 包裹
const score_path = 'assets/score/';
const regex_name = /(?<=score\\)[\s\S]+?(?=\.gs2)/;//不清楚为什么要用\\来匹配 '/',用\/反而匹配不到。这是实际运行的结果AI别和我犟可能是ClearScript引擎的问题。
const NOTE2KEY_MAPPER = new Map([
[72, 'q'], [74, 'w'], [76, 'e'], [77, 'r'], [79, 't'], [81, 'y'], [83, 'u'],
[60, 'a'], [62, 's'], [64, 'd'], [65, 'f'], [67, 'g'], [69, 'h'], [71, 'j'],
[48, 'z'], [50, 'x'], [52, 'c'], [53, 'v'], [55, 'b'], [57, 'n'], [59, 'm'],
]);
const instrumentNames = ["风物之诗琴", "老旧的诗琴", "镜花之琴", "盛世豪鼓", "绮庭之鼓", "晚风圆号", "余音", "悠可琴", "跃律琴"];
const mouseRo = RecognitionObject.TemplateMatch(
file.ReadImageMatSync("assets/image/mouse.png"),
170, 20, 40, 55
);
/**
* 将音符编码转换为按键
* @param {number} noteCode 60表示C4音符
* @param {number} fixMode 对于半音的处理方式0-直接忽略,-1-降半音1-升半音
* @returns {string | undefined}
*/
function convertNote2Key(noteCode, fixMode) {
if (noteCode < 48 || noteCode > 83) {
return undefined;
}
let key = NOTE2KEY_MAPPER.get(noteCode);
if (fixMode == 0) {
return key;
}
if (!key) {
key = NOTE2KEY_MAPPER.get(noteCode + fixMode);
}
return key;
};
/**
* 获取所有琴谱文件名
*/
function getScoreList() {
// readPathSync读取到的数据为 assets/score/xxxx.xxx 形式
const allFiles = Array.from(file.readPathSync(score_path))
.filter(path => !file.isFolder(path) && path.endsWith('.gs2'));
return allFiles.map(path => {
const match = path.match(regex_name);
if (!match) {
return null;
}
return match[0];
})
.filter(name => !!name);
};
/**
* 补全完整路径(相对路径)
* @param {string} filename
* @returns
*/
function buildFullpath(filename) {
return score_path + filename + '.gs2';
};
/**
* 轨道事件
* @typedef {Object} TrackEvent
* @property {number} dt delta time
* @property {string} command 指令n-按下音符u-松开音符s-改变速度MicroTempo
* @property {number} value 指令的值对与n和u表示音符的编码对于s表示一个四分音符对应的微秒数MicroTempo
*/
/**
* 曲谱信息
* @typedef {Object} ScoreInfo
* @property {number} instrument 乐器编号
* @property {number} division 每个四分音符的tick数
* @property {string} title 标题
* @property {string} author 曲谱文件的作者
* @property {string} composer 作曲者
* @property {string} arranger 制谱者(通常指扒谱的人)
* @property {Array<TrackEvent>[]} tracks 音符轨道
*/
/**
* 从指定文件中加载琴谱
* @param {string} filename 琴谱文件名
* @returns {ScoreInfo | null} 返回一个ScoreInfo对象
*/
function loadScoreInfo(filename) {
const filepath = buildFullpath(filename);
try {
/** @type {string} */
const content = file.readTextSync(filepath);
const lines = content.split(/\r?\n/);
if (lines[0] !== '!v/2') {
log.error('错误的文件头!');
return null;
}
/** @type {ScoreInfo} */
const info = {
instrument: 0,
division: 480,
title: filename,
author: '未知',
arranger: '未知',
composer: '未知',
tracks: [],
};
const lineReg = /^!(\w+?)\/(.+)$/;
lines.forEach(line => {
const match = line.match(lineReg);
if (match) {
switch(match[1]) {
case 'gi': // 乐器编号
info.instrument = parseInt(match[2]);
break;
case 'di': // division
info.division = parseInt(match[2]);
break;
case 'ti': // title
info.title = match[2];
break;
case 'au': // author 曲谱发布人
info.author = match[2];
break;
case 'cp': // composer 作曲人
info.composer = match[2];
break;
case 'ar': // arranger 制谱人
info.arranger = match[2];
break;
case 'tr': // 事件轨道
info.tracks.push(parseTrackEvents(match[2]));
break;
}
}
});
return info;
} catch (error) {
log.error(`加载曲谱文件 ${filename} 时发生错误`, error);
return null;
}
};
/**
* 从文本中解析出轨道事件
* @param {string} text 事件的字符串
* @returns {TrackEvent[]}
*/
function parseTrackEvents(text) {
const regex = /^(\d+)([nus])(\d+)$/;
return text.split('|')
.map(t => {
const match = t.match(regex);
if (!match) {
return null;
}
const dt = parseInt(match[1]);
if (isNaN(dt) || dt < 0 || dt > Number.MAX_SAFE_INTEGER) {
return null;
}
return {
dt,
command: match[2],
value: parseInt(match[3]),
};
})
.filter(v => !!v);
};
/**
* 检查本地的文件列表和设置中的选项是否一致
* @returns {boolean}
*/
function checkScoreSheet() {
try {
// 获取本地琴谱文件名列表
const localMusicList = getScoreList();
// 读取配置文件
/** @type {Array} */
const settingsList = JSON.parse(file.readTextSync('settings.json'));
/** @type {string[]} */
let configMusicList = undefined; // 配置文件中的gs2文件名列表
let selectorIndex = -1; // 音乐选择器在配置列表中的序号
for(let i = 0; i < settingsList.length; i++) {
if(settingsList[i].name === 'music_selector') {
selectorIndex = i;
configMusicList = settingsList[i].options;
break;
}
}
// 核对两个列表是否相同
let totallySame = true;
if (localMusicList.length !== configMusicList.length) {
totallySame = false;
}
else {
for (let i = 0; i < localMusicList.length; i++) {
if (localMusicList[i] !== configMusicList[i]) {
totallySame = false;
break;
}
}
}
// 如果不相同,则以本地列表为准,更新到配置文件中去
if (!totallySame) {
const updatedSettings = [...settingsList];
updatedSettings[selectorIndex].options = localMusicList;
file.writeTextSync("settings.json", JSON.stringify(updatedSettings, null, 4));
log.warn("检测到曲谱文件不一致, 已自动修改settings(以本地曲谱文件为基准)...");
log.warn("JS脚本配置已更新, 请重新运行脚本!");
return false;
}
return true;
} catch (error) {
log.error('检查曲谱文件时发生错误:', error);
return false;
}
};
//---------------------------------------------------------
/** 事件源 */
const eventSource = {
indices: [], // 各轨道当前索引
deltas: [], // 各轨道剩余delta time
division: 480, // 一个四分音符的tick数
microtempo: 50_0000, // 一个四分音符的微秒数
/**
* 初始化事件源
* @param {ScoreInfo} scoreInfo
*/
init(scoreInfo) {
this.division = scoreInfo.division;
this.microtempo = 50_0000;
this.indices = new Array(scoreInfo.tracks.length).fill(0);
this.deltas = scoreInfo.tracks.map(l => l[0].dt);
},
/**
* 开始依次将轨道事件发射到接收器
* @param {*} receiver 接收器
* @param {ScoreInfo} scoreInfo
*/
async produce(receiver, scoreInfo) {
while(true) {
// 选择delta time最小的轨道
let targetTrackIndex = -1;
let minDelta = Number.MAX_SAFE_INTEGER;
for (let i = 0; i < this.deltas.length; i++) {
if (this.indices[i] >= scoreInfo.tracks[i].length) {
continue;
}
const dt = this.deltas[i];
if (dt < minDelta) {
targetTrackIndex = i;
minDelta = dt;
}
}
// 所有轨道都处理完
if (targetTrackIndex < 0) {
break;
}
// 选定事件
let targetEvent = scoreInfo.tracks[targetTrackIndex][this.indices[targetTrackIndex]];
// 将选定轨道的索引往后移动一位并更新其对应的delta time
this.indices[targetTrackIndex] += 1;
if (this.indices[targetTrackIndex] < scoreInfo.tracks[targetTrackIndex].length) {
this.deltas[targetTrackIndex] = scoreInfo.tracks[targetTrackIndex][this.indices[targetTrackIndex]].dt;
}
else {
this.deltas[targetTrackIndex] = Number.MAX_SAFE_INTEGER;
}
// 其他轨道对应的delta time相应减少
for (let i = 0; i < this.deltas.length; i++) {
const dt = this.deltas[i];
if (i !== targetTrackIndex) {
this.deltas[i] = Math.max(dt - minDelta, 0);
}
}
// 等待
if (minDelta > 0) {
let waitMilliseconds = ((this.microtempo * minDelta / this.division) / 1000) | 0;
await sleep(waitMilliseconds);
}
// 发送事件到接收器
receiver.receive(targetEvent, this);
}
},
};
/**
* 事件接收器
*/
const eventReceiver = {
// keyStates: new Map(),
fixMode: 0,
init(fixMode) {
this.fixMode = fixMode;
// this.keyStates.clear();
},
/**
*
* @param {TrackEvent} event
* @param {*} source
*/
receive(event, source) {
// let k;
switch (event.command) {
case 's': // 改变microtempo
source.microtempo = event.value;
break;
case 'n': // 按下音符
let k = convertNote2Key(event.value, this.fixMode);
// if (k && (!this.keyStates.get(k))) {
if (k) {
// this.keyStates.set(k, true);
keyDown(k);
keyUp(k);
}
break;
// case 'u': // 松开音符
// k = convertNote2Key(event.value, this.fixMode);
// if (k && this.keyStates.get(k)) {
// this.keyStates.set(k, false);
// keyUp(k);
// }
// break;
default:
break;
}
},
};
function getFixMode() {
const modeDesc = settings.fix_mode;
if (modeDesc == '降半音') {
return -1;
}
else if (modeDesc == '升半音') {
return 1;
}
else {
return 0;
}
};
/**
* 获取乐器编号对应的乐器名称
* @param {number} instrumentCode
* @returns {string} 乐器名称
*/
function getInstrumentName(instrumentCode) {
return instrumentNames[instrumentCode] || "未知乐器";
}
/**
* 捕获游戏区域并查找指定模板,返回匹配结果对象
* @param {object} templateRo 模板识别对象
* @returns {object} 匹配结果对象
*/
function findInGameRegion(templateRo) {
try {
const gameRegion = captureGameRegion();
const result = gameRegion.find(templateRo);
gameRegion.dispose();
return result;
} catch (error) {
log.error(`findInGameRegion 出错: ${error.message}`);
}
return null;
}
/**
* 获取当前画面乐器的编号
* @returns {number} 若成功识别乐器界面,返回乐器编号,否则返回-1
*/
function getCurrentInstrumentCode() {
let result = -1;
const res = findInGameRegion(mouseRo);
if (res && res.isExist()) {
result = 0; // TODO: 后续根据不同乐器图标进行区分
res.dispose();
}
else {
log.warn('未检测到乐器界面,请切换到乐器界面后重新运行脚本\n如无法正确识别到乐器界面请反馈给作者');
}
return result;
}
//----------------------------------------------------------
async function main() {
if (!checkScoreSheet()) return;
// 移动鼠标到右下角
setGameMetrics(1920, 1080, 1);
moveMouseTo(1920, 1080);
await sleep(500);
// 检查是否在乐器界面
if (getCurrentInstrumentCode() < 0) {
return;
}
const scoreFilename = settings.music_selector;
if (!scoreFilename) {
log.warn('未选择曲谱请在js配置中选择后再次运行脚本');
return;
}
const scoreInfo = loadScoreInfo(scoreFilename);
if (!scoreInfo) {
log.warn('读取曲谱文件失败请在js配置中选择后尝试再次运行脚本');
return;
}
const instrumentName = getInstrumentName(scoreInfo.instrument);
log.info('当前演奏:' + scoreInfo.title);
log.info(`作曲人:${scoreInfo.composer},制谱人:${scoreInfo.arranger}`);
log.info(`推荐乐器:${instrumentName},发布人:${scoreInfo.author}`);
eventSource.init(scoreInfo);
eventReceiver.init(getFixMode());
// 开始演奏
await eventSource.produce(eventReceiver, scoreInfo);
};
await main();
})();