From be1dcd441561999a69563b510d09f02d9c11115b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=B3=E6=9D=8E=E5=9F=8E?= <61641591+shilic@users.noreply.github.com> Date: Sun, 22 Dec 2024 01:25:33 +0800 Subject: [PATCH] =?UTF-8?q?[=E5=8E=9F=E7=90=B4]=E5=8E=9F=E7=A5=9E=E7=90=B4?= =?UTF-8?q?=E8=B0=B1=E8=87=AA=E5=8A=A8=E5=BC=B9=E5=A5=8F=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原神琴谱自动弹奏脚本,通过读取本地琴谱文件,解析成乐谱对象,然后模拟键盘操作,实现自动弹奏。(需角色手动提前乐器) --- repo/js/YuanQin/assets/JOJO黄金之风.json | 7 + repo/js/YuanQin/assets/孤勇者.json | 7 + repo/js/YuanQin/main.js | 318 +++++++++++++++++++++++ repo/js/YuanQin/manifest.json | 13 + 4 files changed, 345 insertions(+) create mode 100644 repo/js/YuanQin/assets/JOJO黄金之风.json create mode 100644 repo/js/YuanQin/assets/孤勇者.json create mode 100644 repo/js/YuanQin/main.js create mode 100644 repo/js/YuanQin/manifest.json diff --git a/repo/js/YuanQin/assets/JOJO黄金之风.json b/repo/js/YuanQin/assets/JOJO黄金之风.json new file mode 100644 index 00000000..0ec09ce5 --- /dev/null +++ b/repo/js/YuanQin/assets/JOJO黄金之风.json @@ -0,0 +1,7 @@ +{ + "name":"JOJO黄金之风", + "pause":200, + "longPause":500, + "split":"括号", + "toneStr":"(EHN)(WFV)(WFV)QW(WM)(WV)(QM)(JC)QCW(EHN)(YQV)(YQV)HJ(QX)(WN)(QX)(JC)GCF(EHN)Z(WFV)(WFV)QW(WM)(WV)(QM)(JC)QCW(EHN)(YQV)(YQV)YU(QX)(RX)(EX)(WC)QCTNNNBNN(AZ)NCBNNNBNNXX(AZ)BNNNBNN(AZ)NCBNNNBN(AZ)(AZ)(SX)(SX)(AZ)BNNNBNN(AZ)NCBNNNBNNXX(AZ)BNNNBNN(AZ)NCBNNNBNNNNBNN(AZ)NCBNNNBNNXX(AZ)BNNNBNN(AZ)NCBNNNBN(AZ)(AZ)(SX)(SX)(AZ)BNNNBNN(AZ)NCBNNNBNNXX(AZ)BNNNBNN(AZ)NCBNNNBNC" +} \ No newline at end of file diff --git a/repo/js/YuanQin/assets/孤勇者.json b/repo/js/YuanQin/assets/孤勇者.json new file mode 100644 index 00000000..d316317a --- /dev/null +++ b/repo/js/YuanQin/assets/孤勇者.json @@ -0,0 +1,7 @@ +{ + "name":"孤勇者", + "pause":200, + "longPause":"450", + "split":"括号", + "toneStr":"E+QWQE+QWQWQH+QH+QH+QWQJ++E+QWQE+QWQWQH+QH+QH+QWQJ++HQY+YYTY+YTYTYTE+HQY+YYTYTU+UUYUYE++ETEWEWEW+ETETEW+EWEW+QWEHQEWEWQH++HJQWJQQ+QJQWJQQ+QWEWEWE+EWETE++HJQWJQQ+QJQWJQQ+QWEWEWE+EWETE++TE+TE+TETYET+TE+TE+TETYET++TTEWWQEWWQH+TTEWWQEWWQH" +} \ No newline at end of file diff --git a/repo/js/YuanQin/main.js b/repo/js/YuanQin/main.js new file mode 100644 index 00000000..53d2131e --- /dev/null +++ b/repo/js/YuanQin/main.js @@ -0,0 +1,318 @@ +/** + * @author 阿城同学 + */ +// 所有的代码必须由 async function 包裹 +(async function () { + log.info("[{YuanQin}]正在启动", "原琴"); + /* ------------------------------------------------------1. 数据结构区, 以下代码用于预定义数据结构-------------------------------------------------- */ + /** 乐谱文件,后期可以优化,将该文件放到 settings.json 文件中进行配置。 */ // assets/JOJO黄金之风.json + let scoreFilePath = "assets/孤勇者.json"; + /** 定义分割类型枚举 */ + const SplitTypeEnum = { + /** 括号分割 */ + Bracket : 0, + /** 逗号分割 */ + Comma : 1, + /** 默认分割类型 */ + Default : 2, + }; + /** 定义音符类型枚举 */ + const ToneTypeEnum = { + /** 该音符表示一个长停顿,例如某段歌曲中的间隔 (就是换气,谁TM 唱歌不换气啊) */ + LongPause : 0, + /** 该音符表示一个单音符(简简单单) */ + Single : 1, + /** 该音符表示一个多音符,即多个音符同时按下(原琴玩家表示办不到啊,只有用脚本了) */ + Multi : 2, + }; + /** 音符对象模型 */ + var ToneObject = { + toneType : ToneTypeEnum.Single , + tones : [] , + }; + /** 定义一个乐谱对象原型,用于描述乐谱。 */ + var MusicScoreObject = { + /** 乐谱名称 */ + name : "" , + /** 暂停间隔 (单位:毫秒)。当你按下一个音符时,需要暂停这么多毫秒的时间再按下一个音符(废话,难道你一直按啊)*/ + pauseMills : 200 , + /** 长间隔 (单位:毫秒)。表示一个长停顿,例如某段歌曲中的间隔 。(就是换气,谁TM 唱歌不换气啊)*/ + longPauseMills : 500 , + /** 分割类型 。用于适应不同的乐谱格式,例如:将同时按下的音符用括号括起来。 */ + split : SplitTypeEnum.Bracket , + /** 音符列表 这个字段用于以字符串形式表示音符列表 */ + toneStr : "", + /** 音符列表 用 列表的形式存储每个音符 toneList : ToneObject = [] */ + toneList : [] , + }; + /* ----------------------------------------------------2. 执行区域 ,以下代码用于正式执行操作------------------------------------------------------ */ + /** 乐谱对象 */ + let musicScore = await getMusicScoreFromFile(scoreFilePath); // 使用 await 来等待函数执行完毕。获取乐谱 + //await printMusicScore(musicScore); //打印一个乐谱的基本信息 + let ifParse = await parseScore(musicScore); // 解析乐谱 + if(ifParse){ + log.info("音符解析成功,尝试播放《{name}》",musicScore.name); + await printTones(musicScore); // 打印解析后的音符列表 + await sleep(3000); + await playMusic(musicScore); + } + else{ + log.error("音符解析失败"); + } + /* ---------------------------------------------------3. 方法区 , 以下代码用于定义方法------------------------------------------------------------*/ + /** + * 从文件中读取一个乐谱 + * @param {String} scorePath 文件地址,例如 assets/JOJO黄金之风.json + * @returns 返回一个乐谱对象 MusicScoreObject + */ + async function getMusicScoreFromFile(scorePath){ + let musicScore = Object.create(MusicScoreObject); // 通过原型创建一个乐谱对象 + //log.info(`尝试读取乐谱《${scorePath}》`); + let fileContent; + try { + fileContent = file.readTextSync(scorePath); // 同步阻塞式的将乐谱从文件中读取。 + //log.info("{fileContent}",fileContent); // 用于测试读取文件,测试成功 + log.info("[{y}]读取乐谱文件《{scorePath}》成功","原琴",scorePath); + } catch (error) { + log.error("[{y}]在读取文件 《{scorePath}》时,发生错误","原琴",scorePath); + return null ; + } + if(fileContent == null){ // 可能存在文件无法读出的情况,增加一步校验。 + log.error("[{y}]在读取文件 《{scorePath}》,文件为空","原琴",scorePath); + return null ; + } + // 一些正则表达式,用于解析你的乐谱文件 + const scoreNameRegex = /"\s*name\s*"\s*:\s*"\s*(?[^"\s]+)\s*"\s*,/; + const scorePauseRegex = /"\s*pause\s*"\s*:\s*"?\s*(?\d+)\s*"?\s*,/; + const scoreLongPauseRegex = /"\s*longPause\s*"\s*:\s*"?\s*(?\d+)\s*"?\s*,/; + const scoreSplitRegex = /"\s*split\s*"\s*:\s*"\s*(?[^"\s]+)\s*"\s*,/; + const scoreTonesRegex = /"\s*toneStr\s*"\s*:\s*"\s*(?[^"]+)\s*"\s*,?/; + // 执行正则表达式 匹配操作 + const scoreNameMatch = scoreNameRegex.exec(fileContent); + const scorePauseMatch = scorePauseRegex.exec(fileContent); + const scoreLongPauseMatch = scoreLongPauseRegex.exec(fileContent); + const scoreSplitMatch = scoreSplitRegex.exec(fileContent); + const scoreTonesMatch = scoreTonesRegex.exec(fileContent); + if(scoreNameMatch.groups.scoreName != null) { // 可能会出现无法捕获的情况,例如用户没有填这个字段。给代码增加容错性。 + musicScore.name = scoreNameMatch.groups.scoreName; + } + if(scorePauseMatch.groups.scorePause != null) { + let value = parseInt(scorePauseMatch.groups.scorePause); + musicScore.pauseMills = value; + } + if(scoreLongPauseMatch.groups.scoreLongPause != null){ + let str = scoreLongPauseMatch.groups.scoreLongPause; + //log.debug("长间隔 String = {m}",str); + let value = parseInt(str); + //log.debug("长间隔 int = {v}",value); + musicScore.longPauseMills = value; + } + if(scoreSplitMatch.groups.scoreSplit != null){ // 分割类型 ,如 括号 + //log.debug("开始识别 分割类型"); + let value = scoreSplitMatch.groups.scoreSplit; + if(value === "括号"){ + musicScore.split = SplitTypeEnum.Bracket ; + }else if(value === "逗号"){ + musicScore.split = SplitTypeEnum.Comma ; + }else{ + musicScore.split = SplitTypeEnum.Bracket ; + } + } + if(scoreTonesMatch.groups.scoreTones != null){ + //log.debug("开始识别 音符"); + musicScore.toneStr = scoreTonesMatch.groups.scoreTones; + } + return musicScore; + } //getMusicScoreFromFile 从文件中读取一个乐谱 + /** + * 解析一个乐谱对象 + * @param {MusicScoreObject} musicScore + * @returns 返回是否解析成功 + */ + async function parseScore(musicScore) { + if (musicScore == null){ + return false; + } + musicScore.toneList = await parseTones(musicScore.toneStr,musicScore.split); + if(musicScore.toneList == null){ + return false; + } + return true; + } // 解析一个乐谱对象 + /** + * 传入待解析的字符串,以及分割类型,解析得到最终的音符数组 + * @param {String} tonesString + * @param {SplitTypeEnum} splitType + * @returns 返回一个音符数组 + */ + async function parseTones(tonesString ,splitType) { + let toneList = [] ; //预定义返回值 + /** 括号分割模式下,用于识别音符的正则表达式 /g 表示全局匹配*/ + // /([(]\s*(?[A-Za-z]{2,})\s*[)])|(?[a-zA-Z])/g + const BracketRegex =/([(]\s*(?[A-Za-z]{2,})\s*[)])|(?[a-zA-Z])|(?[+])/g ; + let regex = null; + if(splitType == SplitTypeEnum.Bracket){ // 括号分割 + regex = BracketRegex; + } + else{ + log.error("未识别到音符分割模式"); + } + if(regex == null){ + return null; + } + //let match = regex.exec(tonesString); + log.info("正在执行音符匹配"); + let match; + //let toneArray = [] ; + while( match = regex.exec(tonesString) ) { + let tone = Object.create(ToneObject); // 通过原型创建一个音符对象 + tone.tones = [] ; + let value ; + if(match.groups.multi != null){ + //log.debug("{m}",match[1]); + tone.toneType = ToneTypeEnum.Multi ; + value = match.groups.multi; // 如 EHN + const charArray = Array.from(value); + for (const element of charArray) { + tone.tones.push(element); + } + } + if(match.groups.single != null){ + tone.toneType = ToneTypeEnum.Single ; + value = match.groups.single ; // 如 B + //tone.tones.push(value); + tone.tones = value ; + } + if(match.groups.LongPause != null){ + tone.toneType = ToneTypeEnum.LongPause ; + value = match.groups.LongPause ; // 如 + 号;以及多个加号,如 +++ 表示更长时间的停顿 + // const charArray = Array.from(value); + // for (const element of charArray) { + // tone.tones.push(element); + // } + tone.tones = value ; + } + toneList.push(tone); + // //log.debug("{m}",value); + //toneArray.push(value); + } + //log.debug("{m}",toneArray); + return toneList ; + } // 传入待解析的字符串,以及分割类型,解析得到最终的音符数组 + + /** + * 打印解析后的音符列表 + * @param {MusicScoreObject} musicScore + */ + async function printTones(musicScore) { + let toneList = musicScore.toneList; + let toneStr = "" ; + for(const toneObject of toneList) { // 遍历所有音符 + let type = toneObject.toneType; + let tones = toneObject.tones; + switch(type){ + case ToneTypeEnum.Multi: + toneStr = toneStr + "[" ; + toneStr = toneStr + tones; + toneStr = toneStr + "]" ; + toneStr = toneStr + " "; + break; + case ToneTypeEnum.Single: + toneStr = toneStr + tones; + toneStr = toneStr + " "; + break; + case ToneTypeEnum.LongPause: + toneStr = toneStr + tones; + toneStr = toneStr + " "; + break; + default: + break; + } + } // 遍历所有音符 + log.info("{m}",toneStr); + } // 打印解析后的音符列表 + /** + * 打印解析后的乐谱的基本信息 + * @param {MusicScoreObject} musicScore + */ + async function printMusicScore(musicScore) { + log.debug("22尝试输出捕获到的乐谱名称:{m}",musicScore.name); // 例如输出:[scoreName,黄金之风],或输出: 黄金之风 + log.debug("尝试输出捕获到的乐谱暂停间隔:{m}",musicScore.pauseMills); + log.debug("尝试输出捕获到的乐谱长间隔:{m}",musicScore.longPauseMills); + log.debug("尝试输出捕获到的分割类型:{m}",musicScore.split); + //log.debug("尝试输出捕获到的音符列表:{m}",musicScore.toneStr); + } //打印一个乐谱 + /** + * 执行播放操作 + * @param {MusicScoreObject} musicScore + */ + async function playMusic(musicScore) { + let mills = musicScore.pauseMills ; + let longPause = musicScore.longPauseMills ; + let toneList = musicScore.toneList; + for(const toneObject of toneList) { // 遍历所有音符 + let type = toneObject.toneType; + let tones = toneObject.tones; + // 1. 按下按键,并松开 + switch(type){ + case ToneTypeEnum.Multi: + playSingle(tones); + await sleep(mills); + break; + case ToneTypeEnum.Single: + playMulti(tones); + await sleep(mills); + break; + case ToneTypeEnum.LongPause: + await sleep(longPause); + break; + default: + break; + } + // 2. 间隔一定时间 + + } // 遍历所有音符 + } //执行播放操作 + /** + * 模拟单个按键按下 + * @param {String} singlekey + */ + async function playSingle(singlekey) { + //按下按键后马上松开 + keyDown(singlekey); + keyUp(singlekey); + } + /** + * 模拟多个按键同时按下 + * @param {String[]} multiKeyArray + */ + async function playMulti(multiKeyArray) { + // 多个按键同时按下 + for(const key of multiKeyArray){ + keyDown(key); + } + // 多个按键同时松开 + for(const key of multiKeyArray){ + keyUp(key); + } + } // 模拟多个按键同时按下 + + /** 这段代码用于测试正则表达捕获组的输出 + */ + async function testRegex() { + let str = '{systemName=系统1, systemId=1001} , {systemName=系统2,systemId=1002} , {systemName=系统3, systemId=1003}'; + let reg = /systemName=(.*?),/g; + // str .match(reg); + // reg.exec(str ) + let result = ""; + while(result = reg.exec(str)) { + log.info(result[1]); + } + } + /** 这段代码用于测试播放效果 */ + async function playTest() { + await sleep(1000); + let array = ["E","H","N"]; + playMulti(array); + } +})();// 所有的代码必须由 async function 包裹 \ No newline at end of file diff --git a/repo/js/YuanQin/manifest.json b/repo/js/YuanQin/manifest.json new file mode 100644 index 00000000..41b6da01 --- /dev/null +++ b/repo/js/YuanQin/manifest.json @@ -0,0 +1,13 @@ +{ + "manifest_version": 1, + "name": "[原琴]原神琴谱自动弹奏脚本", + "version": "1.0", + "description": "原神琴谱自动弹奏脚本,通过读取本地琴谱文件,解析成乐谱对象,然后模拟键盘操作,实现自动弹奏。(需角色手动提前乐器)", + "authors": [ + { + "name": "阿城同学", + "link": "https://github.com/shilic" + } + ], + "main": "main.js" +} \ No newline at end of file