如何在Electron环境用JavaScript读取HaxeFlixel的.sol文件
解析Friday Night Funkin的.sol存档文件获取周分数(Electron开发)
问题概述
正在用Electron开发工具,需要提取Friday Night Funkin生成的.sol存档文件中的各周分数并在网页展示。该文件是HaxeFlixel引擎的专有序列化格式,手动编写的JavaScript解析代码逻辑杂乱,无法完整解析所有数据,希望找到可靠的读取方法。
现有尝试代码
// namesearch: 目标存档所在文件夹名称 async function writescoreweek(namesearch) { try { var current = []; const search = ["week9", "week8", "week7", "week6", "week5", "week4", "week3", "week2", "week1", "week0"]; for (var i = 0; i < search.length; i++) { var searchfct = searchscoreweek(`${namesearch}`, search[i]); if (searchfct != "") { let student = { name: search[i], score: searchfct }; (current).push(student); } } } catch (err) { console.log(err) } finally { await fs.writeFileSync(`./data/${namesearch}.json`, JSON.stringify(current, null, 4)); var weekjson = await require(`./data/${namesearch}.json`); return weekjson } } function searchscoreweek(game, search) { try { const helloPath = path.resolve(__dirname, `${process.env.APPDATA}/ShadowMario/${game}/ninjamuffin99/`, 'funkin.sol'); const helloSource = fs.readFileSync(helloPath, 'utf8'); var debut = helloSource.substring( helloSource.indexOf("i") + 1, helloSource.lastIndexOf("y") ); debut = debut.split(":") var after = []; for (var i = 0; i < debut.length; i++) { if (debut[i].search(`${search}`) != -1) { after.push(debut[i]) } } } catch (err) { } finally { var fin = []; for (var i = 0; i < after.length; i++) { fin.push(after[i].substring( after[i].indexOf("i") + 1, after[i].lastIndexOf("y") )); } var apresfin = []; for (var i = 0; i < fin.length; i++) { if (fin[i].search(`${search}`) == -1) { apresfin.push(fin[i]) } } return apresfin; } }
.sol文件格式示例
oy11:timeBarTypey11:Time%20Lefty11:comboOffsetazzzzhy8:arrowHSVaazzzhazzzhazzzhazzzhhy12:middleScrollfy9:scoreZoomty10:noteOffsetzy12:ghostTappingty14:healthBarAlphai1y12:noteSplashesty16:gameplaySettingsby12:opponentplayfy9:instakillfy11:scrollspeedi1y9:songspeedi1y7:botplayfy8:practicefy10:healthgaini1y10:scrolltypey14:multiplicativey10:healthlossi1hy10:lowQualityfy7:showFPSty10:goodWindowi90y9:frameratei144y7:noResetfy7:shadersfy10:sickWindowi45y10:downScrollfy12:ratingOffsetzy8:camZoomsty10:songRatingby8:foreplayd0.887971698113208y11:malfunctiond0.8996138996139y11:donnie-softd0.834459459459459y14:encounter-nullzy9:encounterd0.955947136563877y14:encounter-hardzy3:sexzy10:drift-easyd0.87639405204461y16:malfunction-hardzy16:donnie-soft-hardzy5:driftzy6:insaned0.854901960784314y8:aye-papid0.91304347826087y17:kagayaku-kassettod0.875y4:uhohd0.822265625y9:uhoh-hardzy22:kagayaku-kassetto-hardzy8:collapsezy11:insane-hardzy16:malfunction-easyzy10:drift-hardzhy8:flashingty10:safeFramesi10y13:henchmenDeathzy10:weekScoresby10:week9-easyzy6:week10zy10:week8-easyzy10:week9-hardzy5:week7zy11:week10-easyzy10:week8-hardzy10:week7-easyi278330y5:week8i157270y11:week10-hardzy10:week7-hardzy5:week9zhy7:hideHudfy9:badWindowi135y14:hitsoundVolumezy10:songScoresbR32i61930R33i77360R34i40520R35zR36i73500R37zR38zR39i77510R40zR41zR42zR43i71110R44i34550R45i43510R46i67800R47zR48zR49zR50zR51zR52zhy13:weekCompletedbR65tR61thy18:globalAntialiasingty15:achievementsMapby17:friday_night_playthy10:fullscreenfy13:imagesPersistfy14:controllerModefg
解决方案
方法1:纯JavaScript解析(适配Electron现有栈)
该.sol文件是HaxeFlixel的FlxSave序列化格式,每个数据项由类型前缀+内容组成,核心规则如下:
yN:XXX:字符串类型,N为字符串长度,XXX为URL编码的内容iN:整数类型,N为数值dN:浮点数类型,N为数值f:布尔值false,t:布尔值trueb:开始嵌套对象,z:结束嵌套对象/数组h:空值
针对周分数提取,可编写结构化解析器,直接将.sol内容转为可操作的JavaScript对象:
const fs = require('fs'); const path = require('path'); /** * 解析.sol文件为JavaScript对象 * @param {string} filePath .sol文件路径 * @returns {object} 解析后的存档数据 */ function parseSolFile(filePath) { const content = fs.readFileSync(filePath, 'utf8'); const root = {}; let index = 0; // 解析嵌套对象的辅助函数 const parseNested = (startIdx) => { const obj = {}; let idx = startIdx; while (idx < content.length && content[idx] !== 'z') { const type = content[idx]; idx++; if (type === 'y') { // 读取字符串键 const colonPos = content.indexOf(':', idx); const strLen = parseInt(content.slice(idx, colonPos), 10); idx = colonPos + 1; const key = decodeURIComponent(content.slice(idx, idx + strLen)); idx += strLen; // 读取对应值 const valueType = content[idx]; idx++; let value; switch (valueType) { case 'i': // 读取整数,直到遇到下一个类型前缀 let numEnd = idx; while (!['y','i','d','f','t','b','z','h'].includes(content[numEnd])) numEnd++; value = parseInt(content.slice(idx, numEnd), 10); idx = numEnd; break; case 'd': // 读取浮点数 numEnd = idx; while (!['y','i','d','f','t','b','z','h'].includes(content[numEnd])) numEnd++; value = parseFloat(content.slice(idx, numEnd)); idx = numEnd; break; case 'f': value = false; break; case 't': value = true; break; case 'b': // 递归解析嵌套对象 const nestedResult = parseNested(idx); value = nestedResult.obj; idx = nestedResult.nextIdx; break; case 'z': value = null; break; case 'h': value = null; break; } obj[key] = value; } } // 跳过结束符z return { obj, nextIdx: idx + 1 }; }; // 解析根层级数据 while (index < content.length) { const type = content[index]; index++; if (type === 'y') { const colonPos = content.indexOf(':', index); const strLen = parseInt(content.slice(index, colonPos), 10); index = colonPos + 1; const key = decodeURIComponent(content.slice(index, index + strLen)); index += strLen; const valueType = content[index]; index++; let value; switch (valueType) { case 'i': let numEnd = index; while (!['y','i','d','f','t','b','z','h'].includes(content[numEnd])) numEnd++; value = parseInt(content.slice(index, numEnd), 10); index = numEnd; break; case 'd': numEnd = index; while (!['y','i','d','f','t','b','z','h'].includes(content[numEnd])) numEnd++; value = parseFloat(content.slice(index, numEnd)); index = numEnd; break; case 'f': value = false; break; case 't': value = true; break; case 'b': const nestedResult = parseNested(index); value = nestedResult.obj; index = nestedResult.nextIdx; break; case 'z': value = null; break; case 'h': value = null; break; } root[key] = value; } else if (type === 'z') { break; } } return root; } // 使用示例 const gameFolder = "你的游戏文件夹名称"; // 替换为实际的游戏文件夹名 const solPath = path.resolve(process.env.APPDATA, `ShadowMario/${gameFolder}/ninjamuffin99/funkin.sol`); const saveData = parseSolFile(solPath); // 提取周分数,直接像操作JSON一样使用 console.log("各周分数数据:", saveData.weekScores); // 示例输出:{ "week7-easy": 278330, "week8": 157270, ... }
解析完成后,saveData.weekScores就是包含所有周分数的对象,可直接用于网页展示。
方法2:使用hxnodejs(官方序列化逻辑)
如果希望完全匹配HaxeFlixel的存档解析逻辑,可通过Haxe语言结合hxnodejs库读取:
安装依赖:
- 安装Haxe(根据系统选择对应安装包)
- 安装hxnodejs库:
haxelib install hxnodejs
编写Haxe解析代码(
SolParser.hx):import flixel.system.FlxSave; import js.node.Fs; import js.node.Path; class SolParser { static function main() { // 替换为实际的游戏文件夹名 var gameFolder = "你的游戏文件夹名称"; var savePath = Path.resolve(process.env.APPDATA, "ShadowMario/" + gameFolder + "/ninjamuffin99/funkin.sol"); var save = new FlxSave(); if (save.load(savePath)) { // 获取周分数数据 var weekScores = save.data.weekScores; trace("周分数:" + weekScores); // 转为JSON保存,供Electron的JavaScript读取 var jsonStr = haxe.Json.stringify(weekScores); Fs.writeFileSync("./week_scores.json", jsonStr); } else { trace("读取存档失败"); } } }编译为Node.js脚本:
haxe -lib hxnodejs -js sol_parser.js -main SolParser在Electron中调用:
在Electron的主进程或渲染进程中执行sol_parser.js,然后读取生成的week_scores.json文件即可。
内容的提问来源于stack exchange,提问作者ToniPortal
相关产品推荐
相关产品推荐

