API 目前处于 Early Preview 阶段, 可能存在诸多不完善与错误之处,欢迎加群交流反馈:加入官方交流群(1073434398)
Orzmic
orzmic 的 Rating 口径、Best 30 桶,以及本地文档存储下 profile / score 的统一字段与上游原样 native 字段。
orzmic 属于本地文档存储族:整份档案是一个 JSON 文档,player.native 与 score.native 装的都是
上游原样对象,服务端不计算任何 rating。官方算法从未公开,社区与逆向实现之间存在冲突。
机器可读版本
本页字段表与服务端 GET /v1/games/orzmic/format 一一对应(profile.identityFields /
profile.fields / profile.nativeFields、score.keyFields / score.compareField /
score.ratingField / score.fields / score.nativeFields / score.buckets)。文档可能滞后,以端点返回为准。
Profile 格式
一张 orzmic 档案 = 一个 RankHub 用户在 orzmic 上的一张卡,整份文档存在一行 JSON 里。对外主键是 profileId(uuid)。
身份字段(建档用)
| 字段 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
externalId | string ≤128 | 是 | 外部标识。orzmic 没有官方账号体系,这个值由调用方与用户约定 | "orz-sample-0001" |
- 缺失 ⇒
400;服务端只落库;syncCapable恒为false; player.name必填(展示名,可来自原生name/userName/playerName/nickname)。
统一字段(player)
| 字段 | 类型 | 说明 |
|---|---|---|
name | string | null | 显示名。依次尝试原生 name → userName → playerName → friend_code |
rating | number | null | 个人 Rating。取档案汇总 summary.rating,退回 player.rating —— 写入方给的值,服务端不代算 |
rating 由写入方给,服务端不算
本平台对 orzmic 的评分语义是 provided:服务端不做任何评分计算,桶和头部数字都由写入方给,
所以 player.rating 的可信度等于「谁写的这条数据」。
原生存档里个人 Rating 是字符串 Rat(末位还带评级字母),解析成数字时要把末位字符剥掉,否则会得到 null。
原生字段(player.native)
本地游戏的 player.native 是三层嵌套,不是扁平字段:
| 字段 | 类型 | 说明 |
|---|---|---|
externalId | string | 档案身份列(与身份字段同源) |
player | object | null | 上游玩家块原样:Ver / Name / Coin / CharID / Rat / SPRat / B30Scores … |
summary | object | null | 上游汇总块原样(rating 等由写入方填) |
上游玩家块里值得注意的原生键:
| 原生键 | 类型 | 说明 |
|---|---|---|
Ver | number | 存档格式 / 游戏版本号 |
Name | string | 玩家名 |
Rat | string | 个人 Rating,形如 "12.34X":末位字符是评级字母,直接 Number() 会失败 |
SPRat | string | 第二个 rating,语义未确认 |
B30Scores | string[] | 最佳成绩列表,每项是逗号分隔的 5 字段字符串(不是对象数组) |
Score 格式
成绩键与比较值
| 项 | 值 |
|---|---|
成绩键 keyFields | songId + difficulty |
比较字段 compareField | score(本次得分) |
单曲评分字段 ratingField | rating(原生 native.rating,写入方给;服务端不代算) |
| tie-break | 无(单个比较值) |
曲目键名不统一,服务端是「猜」出来的
本地游戏的成绩是 Record<string, unknown>,曲目键在上游有多个候选名:
song_id / songId / id / music_id,服务端按这个顺序逐个尝试;difficulty 候选是 difficulty / difficulty_name。
所以同一个游戏的数据里可能混着不同键名(导入路径写 music_id,推成绩路径补 song_id)。
写数据时请明确用 song_id + difficulty,别再引入新拼法。
统一字段
| 字段 | 类型 | 说明 |
|---|---|---|
scoreId | string | 成绩行 id |
songId | string | 曲目 id(按上面候选键名解析) |
songName | string | null | 曲名。依次尝试 title → song_name |
difficulty | string | null | 难度(原样字符串化) |
achievements | number | null | 与 score 同源 —— 本地游戏没有独立的「达成率」字段 |
score | number | null | 本次得分 |
rating | number | null | 该谱面的评分值,写入方给(本地导入的存档通常为 null) |
rank | string | null | 评级(通常缺失) |
native | object | null | 整份原生成绩对象,见下表 |
恒为 null(原生没有或无映射):type、levelIndex、level、dxScore、dxStar、fc、fs、playTime。
achievements 与 score 是同一个数
本地游戏族没有「达成率」这个归一字段,所以 achievements 直接复用 score。
客户端不要把它当百分比用(orzmic 的满分是 1,000,000 + 物量,不是 1,000,000)。
原生字段(score.native)
端点声明 nativeFields 为空数组
score.native 就是整份上游成绩对象(原样透传),键名由数据源/写入方决定,
所以格式端点对这个游戏不声明固定的 nativeFields(空数组)。
空数组 ≠ native 为空:数据在,只是没有可承诺的固定键集合。下面列出常见键供参考。
| 键 | 说明 |
|---|---|
| 曲目键 | song_id / id / music_id / songId 之一(导入路径常见 music_id,推成绩路径补 song_id) |
difficulty | 难度。存档编码是 1=Special / 2=Hard / 3=Normal / 4=Easy —— 数值不是顺序,1 才是最难的 |
score | 本次得分,区间 0 ~ 1,000,000 + 物量 |
rating | 单曲 Rating(只有推成绩时由调用方给出才会有;本地导入的存档不带) |
play_status | 进度状态:>=1 通关 / >=2 ID / >=3 PD(+ 变体被合并) |
ex_score | 金/银标记:0=金、1=银、其他=无 |
除上面的常见键外,score.native 里可能还有调用方自己塞进来的任意键(原样存)。
本页只承诺上面几个稳定键的语义;其余字段的增减不视为破坏性变更。
评分语义与量纲
单曲 Rating(社区 / 逆向口径,无官方文档):单曲 Rating = 定数 + 分数补偿(mod) + 金/银加成
定数(Constant):社区值域约1.0 ~ 16.x,精度 0.1;mod是分段函数,随Score变化。两个来源在分段边界上互相冲突,仅两处在Score = 1,000,000收敛:
Score 区间 | 社区口径(mod) | 逆向口径(mod) |
|---|---|---|
= 1,000,000 + 物量 | +2.1 | +2.2 |
∈ (1,000,000, 1,000,000 + 物量) | +2.0 | +2.1 |
= 1,000,000 | 需由截断段推出 | +2.0 |
∈ [980,000, 1,000,000) | +1.4(980,000 处) | +1.0(980,000 处),向 1,000,000 线性升到 +2.0 |
∈ [950,000, 980,000) | +0.7 → +1.4 线性 | +0.4 → +1.0 线性 |
< 900,000 等低段 | 未取到完整片段 | 逐段递减,< 700,000 直接 0 |
- 金/银加成:
ex_score = 0(金)为+0.05(Score ≥ 1,000,000时+0.10);ex_score = 1(银)为+0.02(Score ≥ 1,000,000时社区说+0.05、逆向实现算+0.04); - 单曲聚合:一首歌只贡献一个分数 —— 取它所有难度里最高的那个,四舍五入保留 3 位小数;
- 个人 Rating:社区现行口径是
floor2( Σ 单曲 Rating 前 30 / 30 )(向下取整保留 2 位小数); 历史上长期是 b20,改成 b30 的确切版本未知。原生存档字段名叫B30Scores,是「30 条」的旁证。
服务端不实现这套公式
上面这些只用于解释数字从哪来。本平台服务端不做 orzmic 的 rating 计算:单曲值来自写入方,
totals.total 是桶内评分之和(不是前 30 的平均)。所以别指望用端点返回值反推出官方 Rating。
最佳成绩桶
GET /v1/games/orzmic/profiles/{profileId}/bests 的桶来自上游快照分组,不是服务端重新选桶:
key | label | 成员规则 | totalsKeys |
|---|---|---|---|
best30 | BEST30 | 游戏自己算好的前 30 条(成员关系由游戏决定,服务端不重选);推成绩时 bucket 的固定词汇只有 best30 | total |
label由桶键大写生成(best30→BEST30);- 上游快照没有分组时会退化成单桶(
key为best,label取快照给的标签),保证buckets非空; totals.total:桶内各条rating(缺失则score)之和。它不是原生个人 Rating (那是前 N 名的平均)—— 两者量纲完全不同(和 vs 均值)。
写入(第三方)
请求体形状、idempotencyKey、批量上限、428/403 等通用规则见写入格式;
这里只写 orzmic 真正特殊的部分。
两种写形状
- RankHub 格式:直接给统一字段(
songId/difficulty/rating/score/bucket…); - 游戏原生格式(
native):只用于成绩 ——scores[].native里放整份上游成绩对象, 服务端挑出必要字段后把整份对象原样存下来,与顶层同名字段同时出现时native优先; 不合法 ⇒400 common.invalidArgument(message指明键)。
本游戏的写入规则
- 必给:
songId+difficulty+rating+score+native。 服务端不做任何评分计算(rating一律由调用方给 —— maimai / arcaea 之外的游戏都是这样,只有那两者服务端算),rating不给就是null(桶内排序会退化成按score)。 - 比较值是
score;同一(songId, difficulty)只有分数更高才会覆盖。 difficulty原样字符串化:1=Special /2=Hard /3=Normal /4=Easy,数值不是顺序,1 最难。bucket是固定词汇:只接受best30,拼错直接400 orzmic best bucket must be one of best30(不是默默归桶)。- 不会重算玩家头部:推送只补成绩,不改
player/summary(player.rating需要你自己维护或重新建档)。 - 409:本地游戏一个用户只能有一份未解绑档案 ⇒ 重复建档返回
orzmic profile already exists for this identity(重复调用请改用推成绩)。 - 通用上界(写入守卫开启时生效,默认
off):score越界400,rating越界或playTime晚于现在 1 小时422。
原生数据(native)
对 orzmic 来说,native 就是那条上游成绩 JSON 本身:服务端按既有推送契约挑出必要字段,
并把整份对象原样存进成绩的 native(读侧 score.native 就是它)。
身份字段 externalId 必须给
orzmic 没有官方账号体系,身份是调用方约定的 externalId:建档时仍要给它(顶层 player.externalId,
或 native 里的 externalId / external_id)。player.native 只贡献展示名与其余玩家字段。
native 键 | 类型 | 换算 |
|---|---|---|
song_id / songId / id / music_id | string / number,必填其一 | songId(按这个顺序取第一个非空) |
difficulty | string,必填 | difficulty(原样字符串化) |
score / achievement / achievements | number,必填其一 | score(比较值) |
rating | number ≥0 | rating(可选;不给不代算) |
bucket | string | bucket(可选,缺省 best30;目前只接受 best30) |
| 其余键 | — | 原样落库,读侧 score.native 可完整取回 |
- 可写字段(
native,除身份与唯一键外全部):profile 侧name/userName/playerName/nickname(→name)+ 其余任意键(建档或PATCH /v1/games/orzmic/profiles/{profileId}的native, 其余键原样进playerJson,读回player.native.player.<键>);score 侧除song_id+difficulty外的全部键。 - 不可改:身份
externalId——PATCH的native里给了不同的externalId/external_id⇒400,message=native.externalId cannot be changed (identity field);成绩唯一键song_id+difficulty顶层与 native 都给时必须一致,不一致以 native 为准(会写到另一张谱面上)。
示例(示意数据) —— 一条推成绩请求(原生格式):
curl -sS -X POST https://api.rankhub.cc/v1/games/orzmic/profiles/$PROFILE_ID/scores \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"scores": [
{
"native": {
"music_id": 10023,
"difficulty": "2",
"score": 1004820,
"rating": 15.83,
"play_status": 2,
"ex_score": 0
}
}
]
}'消费者注意事项
rating不是服务端算的:来自写入方/上游,服务端只排序、只落库。要拿「官方 Rating」得自己按社区公式算,或让写入方带上。totals.total是桶内求和,不是原生个人 Rating:原生是前 N 名的平均,这里是和,差 N 倍。achievements与score同源,且 orzmic 满分是1,000,000 + 物量,score会大于 1,000,000 —— 当百分比用会错。- 难度编码不是顺序:
1 = Special、2 = Hard、3 = Normal、4 = Easy,1是最难的,直接当数组下标会全错。 - 曲目键名有 4 个候选(
song_id/id/music_id/songId),服务端按顺序猜;同一档案里可能混着两种拼法。 bucket是固定词汇:只接受best30;拼错会400(不是默默丢进best30)。playTime恒为null,rank通常也是null。- 桶的成员关系由游戏决定:社区公式不重新选桶,本平台也不重选(只在推成绩时按评分降序截断到 30)。
- 没有同步能力:
syncCapable恒false,数据只能靠建档后的推成绩或重新导入维护。 - 推成绩时
native原样存:服务端把它与song_id/difficulty/score/rating合并写进成绩存储, 读回的native是「你的native+ 这几个补齐键」。