邦邦技能机制的简单验证


声明:本项目为个人学习与技术研究用途,请勿用于商业目的,不得将本项目中任何内容用于违反国家/地区/组织等的法律法规或相关规定的其他用途。

环境:ガルパ10.1.3
分析工具:Frida-server, IDA Pro, Python
日期:2026-07-11

之前通过静态分析,得出了邦邦技能键持续时间为(duration*fps+1)帧的结论,今天用日服邦邦来验证一下。

1. 理论计算

由于有所有谱面数据,简单用脚本找了下60fps和120fps能显示明显区别的歌,最终选的是《Yes! BanG_Dream!》的EX难度,在5.0s技能时长下,在技能上会出现差异。

理论计算如下

歌曲信息:BPM185, 技能#2~#5在5.029秒处有边界note

beat时间/s(60fps)帧 -> 判定帧 -> 技能区间帧/判定(120fps) 帧-> 判定帧 -> 技能区间帧
skill#28928.861731.89 -> 1732 -> [1733,2033]3463.78 -> 3464 -> [3465,4065]
note#112104.533.892033.51 -> 2034 -> 无技能加成4067.03 -> 4068 -> 无技能加成
skill#3131.542.652558.92 -> 2559 -> [2560,2860]5117.84 -> 5118 -> [5119,5719]
note#177&17814747.682860.54 -> 2861 -> 无技能加成5721.08 -> 5722 -> 无技能加成
skill#417857.733463.78 -> 3464 -> [3465,3765]6927.57 -> 6928 -> [6929,7529]
note#232&233193.562.763765.41 -> 3766 -> 无技能加成7530.81 -> 7531 -> 无技能加成
skill#528893.415604.32 -> 5605 -> [5606,5906]11208.65 -> 11209 -> [11210,11810]
note#385303.598.435905.95 -> 5906 -> 有技能加成11811.89 -> 11812 -> 无技能加成

可以看到60fps中#2、#3、#4都是差1帧进技能区间,距离分别为(8.5ms,9ms,6.8ms),#5直接处在技能区间;120fps则差的较远,距离技能区间都差2~3帧,距离分别为(16.9ms,17.3ms,15.1ms,12.8ms)。

按照5.0s技能区间推算:60fps下,该歌曲有117个技能区间的键和342个非技能区间的键;120fps下,该歌曲有116个技能区间的键和343个非技能区间的键。事前auto测试可以得知,歌曲基础note分数为1700,受130%技能加成的note分数为3910,那么对歌曲总分的计算如下:

60fps: 117*3910+342*1700=1038870

120fps: 116*3910+343*1700=1036660

2. HOOK测试打印逐帧信息

这里用的是日服邦邦
hook.js放在结尾了,这里只看输出的csv结果
总结表格如下

测试skill#5判定帧
(理论5605/11209)
#5技能结束帧
(理论5906/11810)
note#385判定帧
(理论5906/11812)
note#385分数游戏总分
60FPS-0156055906590639101038870
60FPS-0256055906590639101038870
60FPS-0356055906590639101038870
60FPS-0456055906590639101038870
60FPS-055606 (+1)5907 (+1)5907 (+1)39101038870
60FPS-0656055906590639101038870
60FPS-075606 (+1)5907 (+1)5907 (+1)39101038870
120FPS-0111210 (+1)11811 (+1)11813 (+1)17001036660
120FPS-0211209118101181217001036660
120FPS-0311210 (+1)11811 (+1)11813 (+1)17001036660
120FPS-0411210 (+1)11811 (+1)11813 (+1)17001036660

可以看出,在实验数据范围内,理论和实际完美吻合。唯一的差异是帧号的偏移,但帧号的相对位置没有问题。

3. 原始csv

篇幅原因,原始输出只放1个技能的,.csv文件见frame_test.zip

60FPS skill#5

frame,state,hasSkill,timer,finTimer,MusicPos,dt_s,dt_ms,score,events
...
5603,0,0,0.000000,-0.000011,13820.9404,0.0166668799,16.666880,805630,
5604,0,0,0.000000,-0.000011,13823.4072,0.0166669078,16.666908,805630,
5605,1,0,0.000000,-0.000011,13825.8740,0.0166668799,16.666880,809030,TRIG
5606,2,1,5.000000,-0.000011,13828.3408,0.0166669339,16.666934,809030,
5607,2,1,4.983333,-0.000011,13830.8076,0.0166668594,16.666859,809030,
...
5904,2,1,0.033266,-0.000011,14563.4180,0.0166668817,16.666882,871590,
5905,2,1,0.016599,-0.000011,14565.8848,0.0166669376,16.666938,871590,
5906,2,1,-0.000068,-0.000011,14568.3516,0.0166669134,16.666913,875500,FIN
5907,3,0,0.000000,0.750000,14570.8184,0.0166669041,16.666904,875500,
5908,3,0,0.000000,0.733333,14573.2852,0.0166669413,16.666941,875500,

120FPS skill#5

frame,state,hasSkill,timer,finTimer,MusicPos,dt_s,dt_ms,score,events
...
11208,0,0,0.000000,-0.000011,13822.1699,0.0083333924,8.333392,805630,
11209,0,0,0.000000,-0.000011,13823.4033,0.0083334940,8.333494,805630,
11210,1,0,0.000000,-0.000011,13824.6367,0.0083334474,8.333447,809030,TRIG
11211,2,1,5.000000,-0.000011,13825.8701,0.0083334530,8.333453,809030,
11212,2,1,4.991666,-0.000011,13827.1035,0.0083334632,8.333463,809030,
...
11809,2,1,0.016591,-0.000011,14563.4141,0.0083335042,8.333504,871590,
11810,2,1,0.008258,-0.000011,14564.6475,0.0083334548,8.333455,871590,
11811,2,1,-0.000076,-0.000011,14565.8809,0.0083333831,8.333383,871590,FIN
11812,3,0,0.000000,0.750000,14567.1143,0.0083335228,8.333523,871590,
11813,3,0,0.000000,0.741666,14568.3477,0.0083334018,8.333402,873290,

4. hook.js

使用的是ガルパ_10.1.3,未来地址可能改变

// Full game data + currentScore
var frame=0,recording=false,lastScore=-1;
var fState=-1,fHasSkill=0,fTimer=-1,fFinTimer=-1,fDt=-1,fMusicPos=-1,pendingEvents=[];

setTimeout(function() {
    var m=Process.getModuleByName("libil2cpp.so"),base=m.base;
    
    // Score tracker - Score$$UpdateTotalScore (0x322AFA0)
    var scoreThis = null;
    Interceptor.attach(base.add(0x322AFA0), {
        onEnter: function(args) { scoreThis = args[0]; },
        onLeave: function(args) {
            if (scoreThis) {
                try { lastScore = scoreThis.add(0x88).readU32(); } catch(e) {}
            }
        }
    });
    
    // Frame anchor: InGameManager$$updatePlayState (0x32F9BB0)
    Interceptor.attach(base.add(0x32F9BB0), {
        onEnter: function(args) { try { fDt=this.context.s0; } catch(e) {} },
        onLeave: function() {
            frame++;
            if (fDt>0 && !recording) { recording=true;
                console.log("frame,state,hasSkill,timer,finTimer,MusicPos,dt_s,dt_ms,score,events"); }
            if (recording) {
                var st=fState>=0?fState:"?", mp=(typeof fMusicPos==='number'&&!isNaN(fMusicPos))?fMusicPos.toFixed(4):"?";
                var tm=(typeof fTimer==='number'&&!isNaN(fTimer))?fTimer.toFixed(6):"?";
                var ft=(typeof fFinTimer==='number'&&!isNaN(fFinTimer))?fFinTimer.toFixed(6):"?";
                var ds=(typeof fDt==='number'&&!isNaN(fDt)&&fDt>0)?fDt.toFixed(10):"?";
                var sc=lastScore>=0?lastScore:"?";
                console.log(frame+","+st+","+fHasSkill+","+tm+","+ft+","+mp+","+ds+","+(ds!='?'?(fDt*1000).toFixed(6):'?')+","+sc+","+pendingEvents.join("|"));
                pendingEvents=[];fDt=-1;fState=-1;fMusicPos=-1;
            }
        }
    });

    // Skill state
    Interceptor.attach(base.add(0x33227C0), {
        onEnter: function(args) {
            try{fState=args[0].add(0x90).readS32()}catch(e){}
            try{fTimer=args[0].add(0x88).readFloat()}catch(e){}
            try{fFinTimer=args[0].add(0x8C).readFloat()}catch(e){}
            try{fHasSkill=args[0].add(0x80).readPointer().isNull()?0:1}catch(e){}
        }
    });
    Interceptor.attach(base.add(0x3323378),{onEnter:function(){pendingEvents.push("TRIG");}});
    Interceptor.attach(base.add(0x33236B0),{onEnter:function(){pendingEvents.push("FIN");}});
    Interceptor.attach(base.add(0x33044D4),{onLeave:function(){try{fMusicPos=this.context.s0}catch(e){}}});
},2000);

5. 扩展测试

之前的研究中提到了一个判定函数,伪代码如下

void MoveState(NoteSingleBase* this, float deltaTime) {
    UpdateBase(this, deltaTime);
    float MusicPos = GetAdjustMusicPos(noteManager);
    float notePos = (float)noteInfo->absolutePos;

    if (MusicPos >= notePos) {
        this->accumulatedTime += deltaTime;

        if (IsAutoPlay())
            forcePerfect();                    // vtable[89]
        else if (gameState == 14)
            forcePerfect();                    // vtable[89]
        else if (this->accumulatedTime > TIMEOUT)
            ChangeState(Wait/Stop);            // vtable[87]
    } else {
        this->accumulatedTime = 0;
    }
}

尝试hook了一下这个,可以得到理论和实际的偏移情况(测试环境 60fps)
推测在60拍和183拍附近掉帧了
不过这个数据设备间差异应该挺大,大概只能说明掉帧等是会影响判定的,期待有大佬进一步分析

拍 (距离判定帧)理论距离  实际距离     时间差     理论帧   实际帧    帧差
   beat |   thr(ms)   act(ms)   dev(ms) |  thr(f)  act(f)  dev(f)
-----------------------------------------------------------------
   10.0 |     6.757     6.805    +0.048 |   0.405   0.408  +0.003
   11.0 |    15.766    15.819    +0.053 |   0.946   0.949  +0.003
   12.0 |     8.108     8.166    +0.058 |   0.486   0.490  +0.003
   14.0 |     9.459     9.527    +0.068 |   0.568   0.572  +0.004
   ...
   57.5 |     1.351     1.630    +0.279 |   0.081   0.098  +0.017
   59.0 |    14.865    15.150    +0.285 |   0.892   0.909  +0.017
   61.0 |    16.216     8.189    -8.027 |   0.973   0.491  -0.482
   62.0 |     8.559     0.534    -8.025 |   0.514   0.032  -0.481
   64.0 |     9.910     1.897    -8.013 |   0.595   0.114  -0.481
   65.0 |     2.252    10.910    +8.658 |   0.135   0.655  +0.519
   ...
  180.5 |     9.459     2.013    -7.446 |   0.568   0.121  -0.447
  181.5 |     1.802    11.026    +9.224 |   0.108   0.662  +0.553
  182.0 |     6.306    15.533    +9.227 |   0.378   0.932  +0.554
  184.0 |     7.658     8.571    +0.913 |   0.459   0.514  +0.055
  185.5 |     4.505     5.424    +0.919 |   0.270   0.325  +0.055
  186.0 |     9.009     9.931    +0.922 |   0.541   0.596  +0.055

6. 结论

上一篇文章关于技能区间长度为(duration*fps+1)帧的说法基本正确。不过文章用的是日邦测试,不知道国服是否有一样的结果。国服需要用更隐蔽的hook方案,这里就不介绍了。此外,设备性能也会通过掉帧等影响判定的效果,在歌曲一开始,理论和实际应该是对齐的(第一个键 beat10,理论与实际只差只有+0.048ms)。

最后修改:2026 年 07 月 11 日
如果觉得我的文章对你有用,请随意赞赏