邦邦技能机制的简单验证
声明:本项目为个人学习与技术研究用途,请勿用于商业目的,不得将本项目中任何内容用于违反国家/地区/组织等的法律法规或相关规定的其他用途。
环境:ガルパ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#2 | 89 | 28.86 | 1731.89 -> 1732 -> [1733,2033] | 3463.78 -> 3464 -> [3465,4065] |
| note#112 | 104.5 | 33.89 | 2033.51 -> 2034 -> 无技能加成 | 4067.03 -> 4068 -> 无技能加成 |
| skill#3 | 131.5 | 42.65 | 2558.92 -> 2559 -> [2560,2860] | 5117.84 -> 5118 -> [5119,5719] |
| note#177&178 | 147 | 47.68 | 2860.54 -> 2861 -> 无技能加成 | 5721.08 -> 5722 -> 无技能加成 |
| skill#4 | 178 | 57.73 | 3463.78 -> 3464 -> [3465,3765] | 6927.57 -> 6928 -> [6929,7529] |
| note#232&233 | 193.5 | 62.76 | 3765.41 -> 3766 -> 无技能加成 | 7530.81 -> 7531 -> 无技能加成 |
| skill#5 | 288 | 93.41 | 5604.32 -> 5605 -> [5606,5906] | 11208.65 -> 11209 -> [11210,11810] |
| note#385 | 303.5 | 98.43 | 5905.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-01 | 5605 | 5906 | 5906 | 3910 | 1038870 |
| 60FPS-02 | 5605 | 5906 | 5906 | 3910 | 1038870 |
| 60FPS-03 | 5605 | 5906 | 5906 | 3910 | 1038870 |
| 60FPS-04 | 5605 | 5906 | 5906 | 3910 | 1038870 |
| 60FPS-05 | 5606 (+1) | 5907 (+1) | 5907 (+1) | 3910 | 1038870 |
| 60FPS-06 | 5605 | 5906 | 5906 | 3910 | 1038870 |
| 60FPS-07 | 5606 (+1) | 5907 (+1) | 5907 (+1) | 3910 | 1038870 |
| 120FPS-01 | 11210 (+1) | 11811 (+1) | 11813 (+1) | 1700 | 1036660 |
| 120FPS-02 | 11209 | 11810 | 11812 | 1700 | 1036660 |
| 120FPS-03 | 11210 (+1) | 11811 (+1) | 11813 (+1) | 1700 | 1036660 |
| 120FPS-04 | 11210 (+1) | 11811 (+1) | 11813 (+1) | 1700 | 1036660 |
可以看出,在实验数据范围内,理论和实际完美吻合。唯一的差异是帧号的偏移,但帧号的相对位置没有问题。
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.0556. 结论
上一篇文章关于技能区间长度为(duration*fps+1)帧的说法基本正确。不过文章用的是日邦测试,不知道国服是否有一样的结果。国服需要用更隐蔽的hook方案,这里就不介绍了。此外,设备性能也会通过掉帧等影响判定的效果,在歌曲一开始,理论和实际应该是对齐的(第一个键 beat10,理论与实际只差只有+0.048ms)。