In Falsus 谱面字段详解

免责声明

本工具仅供个人学习研究使用,请勿用于商业用途或未经授权的传播。

文档中描述的游戏资源(包括但不限于谱面文件、容器格式与密钥派生算法),其所有权与著作权均归属于游戏原始开发者/发行方,请遵守相关法律法规与用户协议。

因使用本文档所述方法、代码或结论所引发的任何法律后果、数据丢失,或对游戏文件造成的损坏,作者概不负责

在读取、备份或修改任何游戏文件前,请确认您有权这样做!

本文档描述 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)的
  • 一条记录给出起点位置、终点位置、起点宽度、终点宽度四个量,每个量各带自己的细分格数

公式

Python
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 的高字节决定
高字节类型说明
1tap点击,startMs == endMs
2hold长按,有持续时长
4flick滑击,startMs == endMs
5field空域,有持续时长
  • track:由标志 A 的低字节决定
低字节轨道组
1ground(地面 4 轨)
2side-left(左侧轨)
3side-right(右侧轨)
4air(空域)
  • 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 − 1
  • startMs / endMs:段的起止毫秒
  • durationMs:endMs - startMs。tap/flick 为 0
  • beat / 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
    位掩码公式:
Python
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–3
  • timeMs:毫秒。全部 5,109 条事件该字段完全单调,可确认是时间
  • 载荷按类型解释(0x10 为载荷 1,0x18 为载荷 2):
类型载荷 1载荷 2对应事件
0double,如 0.0 / 0.5 / 1.0 / 2.0 / ±20.00track:滚动速度倍率(可为负 = 反向)
1double,取值 65–9560 = BPMfloat = 滚动速度倍率(0.01–999,默认 4.0)bpm:BPM 变更(附带速度变更)
2double,逐拍递增的滚动位置(如 242→956)float,倍率(4.0 / 10.0 / 99.0)与 track 同类,多带一个滚动位置
3整数 = 轨道号 | (启用 << 8)(实测 0/1/2/4 与 0/1)0lane:轨道开关

导出 JSON 里的键名(载荷 1 按类型走三个键之一,载荷 2 只在非 0 时出现):

键出现条件含义
id恒有事件序号(+0x00)
type恒有0–3
timeMs恒有毫秒(+0x08)
beat恒有按 BPM 时间轴折算的拍号
bpmtype == 1载荷 1 按 double 解释(实测 65–9560)
valuetype == 0 / type == 2载荷 1 按 double 解释(滚动速度倍率 / 滚动位置)
flagstype == 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

评论

No comments yet. Why don’t you start the discussion?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注