免责声明
本工具仅供个人学习研究使用,请勿用于商业用途或未经授权的传播。
文档中描述的游戏资源(包括但不限于谱面文件、容器格式与密钥派生算法),其所有权与著作权均归属于游戏原始开发者/发行方,请遵守相关法律法规与用户协议。
因使用本文档所述方法、代码或结论所引发的任何法律后果、数据丢失,或对游戏文件造成的损坏,作者概不负责
在读取、备份或修改任何游戏文件前,请确认您有权这样做!
本文档描述 In Falsus 谱面文件里每个字段代表什么:概念、坐标系、音符与事件的语义
所有结论来自对游戏本体的反汇编 + 全量数据验证,凡属推测而非实测的结论都会明确标注
〇、速查编号表
先给编号,后文再用名称展开
type 1 = tap 点击,startMs == endMs
2 = hold 长按,startMs != endMs
4 = flick 滑击,startMs == endMs
5 = field 空域,startMs != endMs
track 1 = ground 地面 4 轨
2 = side-left 左侧轨
3 = side-right 右侧轨
4 = air 空域
lane 0–5(= x * 4);仅六轨有意义,空域为 null
0 = 左侧轨,1–4 = 地面四轨,5 = 右侧轨
flick 4 = right (文件存 1024 = 4 << 8)
16 = left (文件存 4096 = 16 << 8)
ease 0 = Linear
1 = SineOut
2 = SineIn (仅 field 有,左右各一)一、概览
游戏场地分两部分:
- 六条轨道:地面 4 条 + 两侧各 1 条。这 6 轨只有 tap与 hold
- 空域:包含 Flick与 Field
谱面文件把这些内容存成两类记录:音符记录(每 80 字节一条)与事件记录(每 32 字节一条,用于 BPM 变更、滚动速度、轨道开关等),头部 28 字节给出基准 BPM 与拍号
二、概念与单位
段(segment)
一条音符记录就是一个「段」,是谱面的最小单位。段不是「一个音符」,而是一段轨迹:
- 有起点与终点,各自带时间、横向位置、宽度
startMs == endMs表示瞬时,否则表示持续一段时间的动作- 段内位置/宽度如何从起点过渡到终点,由 Field 的左右缓动决定(见第三节)
其余类型按线性插值
每段在游戏里恰好对应一个音符对象
所以游戏的”音符总数”是段数,不是线数
线(line)
「线」不是判定线,而是「对象 ID」:同一条线的若干段,在玩家眼里是同一个对象
- 线内的所有段类型与轨道必然相同
- 线内相邻段 99.9% 满足「上一段
endMs== 下一段startMs」,即时间上首尾相接 - 多段线只出现在 Field 上
对 tap/hold/flick,line 等价于「这个音符的编号」
对 Field, 一条线是一个会移动/变形的完整区域,由若干段拼接描述其轨迹
坐标与细分格
- 位置和宽度都以有理数存储:(位置, 细分格数)
例如(21, 24)表示「24 格里的第 21 格」,归一化位置 = 21/24
文件里还会把两者同乘一个系数(如170/680其实就是1/4),游戏读入时先按最大公约数约分,所以第二元就是细分格数 - 六条轨道固定用 4 格:位置
0/4, 1/4, 2/4, 3/4, 4/4, 5/4,所以x × 4就是轨道号 0–5 - 空域的细分格数逐 note 不同,实测是 2–200 之间的各种偶数(共 46 种取值,最常见的三个是 12、24、32)
width:宽度,与x同一坐标系
六轨多为1/4,跨轨音符为 1/2、3/4、1
空域则是区域的大小- 六轨的「一个音符占多条轨道」就是靠宽度表达的,位置始终是左边缘:
x × 4= 最左侧那一轨的轨号,width × 4= 占几轨,所以它覆盖轨号 x×4 … (x+width)×4 − 1
例如占地面 2–3 轨是x = 2/4, width = 2/4,占满地面四轨是x = 1/4, width = 4/4(原始值[[1,4],[1,4],[4,4],[4,4]])
游戏内部就是按这个换算出_fA._He/_ie(LaneFirst/LaneLast)的 - 一条记录给出起点位置、终点位置、起点宽度、终点宽度四个量,每个量各带自己的细分格数
公式
x = 起点位置分子 / 起点位置分母
endX = 终点位置分子 / 终点位置分母
width = 起点宽度分子 / 起点宽度分母
endWidth = 终点宽度分子 / 终点宽度分母
lane = x * 4 # 仅六轨,结果 0–5;空域为 null
laneLast = lane + width * 4 - 1 # 覆盖到的最右轨;空域为 null时间与拍号
timeMs:毫秒beat:拍号
以头部bpm为起始,遇到 type 1 事件(BPM 变更)就在该时刻切换 BPM,再分段累加
三、字段详解
音符记录
index:音符序号,恒等于该记录在文件中的下标line:线 ID ,见第二节type:由标志 A 的高字节决定
| 高字节 | 类型 | 说明 |
|---|---|---|
| 1 | tap | 点击,startMs == endMs |
| 2 | hold | 长按,有持续时长 |
| 4 | flick | 滑击,startMs == endMs |
| 5 | field | 空域,有持续时长 |
track:由标志 A 的低字节决定
| 低字节 | 轨道组 |
|---|---|
| 1 | ground(地面 4 轨) |
| 2 | side-left(左侧轨) |
| 3 | side-right(右侧轨) |
| 4 | air(空域) |
lane:六轨编号 0–5,等于x × 4;空域为null
跨轨宽音符这里给的是最左侧那一轨,它实际覆盖lane … lane + width×4 − 1
(例如x = 2/4, width = 2/4的lane = 2,覆盖 2–3 轨)laneFirst/laneLast:该音符覆盖的轨道区间(单轨音符两者相等,空域为null)laneFirst就等于lane,laneLast = laneFirst + width×4 − 1startMs/endMs:段的起止毫秒durationMs:endMs - startMs。tap/flick 为 0beat/endBeat:按 BPM 时间轴折算的拍号,见第二节x/endX:起点/终点横向位置 = 分子 ÷ 细分格数width/endWidth:起点/终点宽度,同一坐标系
六轨中起点与终点位置恒相等,只有 Field 会移动fractions:四个量的原始值[[分子, 细分格数] × 4],顺序为[起点x, 终点x, 起点宽, 终点宽]。上面那些归一化浮点就是它算出来的,保留原始对是为了不丢精度(例如5/7转浮点会截断)与便于写回文件flick:仅 flick 有。由标志 B 决定方向(Right = 4、Left = 16,在文件中左移 8 位存放):
| 标志 B | 方向 |
|---|---|
| 1024(= 4 << 8) | right |
| 4096(= 16 << 8) | left |
leftEase/rightEase:仅 field 有。标志 B 是两个 one-hot 三位组:
位 2–4 是左边界的缓动、位 5–7 是右边界的缓动,取值 0 / 1 / 2 依次对应 Linear / SineOut / SineIn
位掩码公式:
leftMask = flagB & 0x1C # 位 2-4
rightMask = flagB & 0xE0 # 位 5-7
leftMask: 0x04 -> 0 Linear
0x08 -> 1 SineOut
0x10 -> 2 SineIn
rightMask: 0x20 -> 0 Linear
0x40 -> 1 SineOut
0x80 -> 2 SineIn判定点数量(游戏内物量)
一条 hold 或 field 在游戏内不是一个判定点,而是沿时长被拆成多个判定点
| 类型 | 判定点数 |
|---|---|
| tap (1) / flick (4) | 恒 1 |
| hold (2) | 1 + ⌈拍长 × 2⌉ |
| field (5) | ⌈拍长 × 2⌉ |
⌈⌉ 要带 0.01 的半拍容差:beat / endBeat 只存 3–4 位有效数字,2 × 拍长 会落在整数附近而不是整数上(例如 endBeat - beat = 0.999 其实是 1 拍)
事件记录
type:0–3timeMs:毫秒。全部 5,109 条事件该字段完全单调,可确认是时间- 载荷按类型解释(
0x10为载荷 1,0x18为载荷 2):
| 类型 | 载荷 1 | 载荷 2 | 对应事件 |
|---|---|---|---|
| 0 | double,如 0.0 / 0.5 / 1.0 / 2.0 / ±20.0 | 0 | track:滚动速度倍率(可为负 = 反向) |
| 1 | double,取值 65–9560 = BPM | float = 滚动速度倍率(0.01–999,默认 4.0) | bpm:BPM 变更(附带速度变更) |
| 2 | double,逐拍递增的滚动位置(如 242→956) | float,倍率(4.0 / 10.0 / 99.0) | 与 track 同类,多带一个滚动位置 |
| 3 | 整数 = 轨道号 | (启用 << 8)(实测 0/1/2/4 与 0/1) | 0 | lane:轨道开关 |
导出 JSON 里的键名(载荷 1 按类型走三个键之一,载荷 2 只在非 0 时出现):
| 键 | 出现条件 | 含义 |
|---|---|---|
id | 恒有 | 事件序号(+0x00) |
type | 恒有 | 0–3 |
timeMs | 恒有 | 毫秒(+0x08) |
beat | 恒有 | 按 BPM 时间轴折算的拍号 |
bpm | type == 1 | 载荷 1 按 double 解释(实测 65–9560) |
value | type == 0 / type == 2 | 载荷 1 按 double 解释(滚动速度倍率 / 滚动位置) |
flags | type == 3 | 载荷 1 原样整数(轨道号 | (启用 << 8)) |
speed | 载荷 2 非 0 时才有 | 载荷 2 按 float 解释(滚动速度倍率) |
四、未确定
- 事件 type 2 的确切语义(载荷形状已确定:一个滚动位置 + 一个倍率)
- 线内相邻段有约 31% 在位置上不连续(31.5%)、约 31% 在宽度上不连续(31.9%),两者至少一个不连续的是 39.5%
附:字段速查
index 音符序号(= 记录下标)
line 线 ID / Group ID(一个玩家可见对象 = 一条线)
type 音符类型编号 1 tap / 2 hold / 4 flick / 5 field
track 轨道组编号 1 地面 / 2 左侧轨 / 3 右侧轨 / 4 空域
lane 0-5(= x*4),空域为 null;跨轨宽音符给的是最左轨
laneFirst 同上(= lane)
laneLast 覆盖到的最右轨 = laneFirst + width×4 − 1;空域为 null
timeMs 起始毫秒
endMs 结束毫秒
durationMs endMs - timeMs
beat 起始拍号(BPM 时间轴折算)
endBeat 结束拍号
x / endX 起止横向位置(归一化 = 分子 / 细分格数)
width 起始宽度
endWidth 结束宽度
fractions 四个量的原始 (分子, 细分格数)
flick flick 专用:方向编号 4 右 / 16 左
leftEase field 专用:左边界缓动编号 0 Linear / 1 SineOut / 2 SineIn
rightEase field 专用:右边界缓动编号 0 Linear / 1 SineOut / 2 SineIn