You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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:布尔值true
  • b:开始嵌套对象,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库读取:

  1. 安装依赖:

    • 安装Haxe(根据系统选择对应安装包)
    • 安装hxnodejs库:
      haxelib install hxnodejs
      
  2. 编写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("读取存档失败");
        }
      }
    }
    
  3. 编译为Node.js脚本:

    haxe -lib hxnodejs -js sol_parser.js -main SolParser
    
  4. 在Electron中调用:
    在Electron的主进程或渲染进程中执行sol_parser.js,然后读取生成的week_scores.json文件即可。


内容的提问来源于stack exchange,提问作者ToniPortal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 21:00:52