API 目前处于 Early Preview 阶段, 可能存在诸多不完善与错误之处,欢迎加群交流反馈:加入官方交流群(1073434398)
Milthm
milthm 的 Profile 与 Score 格式:milthmUserId/region 身份、谱面粒度的单键成绩(chartId → songId)、reality 的双版本公式与 Best 20 桶,以及「分母 20 vs 除以条数」的口径差异。
本页是 milthm 在 RankHub 上的字段级格式说明。milthm 是 10 个游戏里少有的
官方公开了完整评分公式的游戏(Reality V3 有官方数学定义);RankHub 侧按该公式
服务端重算 best20 与档案 reality。
机器可读版本
GET /v1/games/milthm/format 返回与本页一致的格式数据(同一份字段表)。
文档可能滞后,以端点返回为准。
一条成绩 = 一张谱面
milthm 的成绩粒度是谱面:同一首歌的不同难度是不同行,所以 songId 会重复。
(存储键是 chartId,wire 上由统一字段 songId 承载它。)用 songId 当唯一键会把多难度并成一条。
Profile 格式
身份字段(建档用)
| 字段 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
milthmUserId | string | 是 | 玩家 id(游戏侧账号 id) | "10000001" |
region | string | 否 | 区服,取值 cn / global,缺省 cn | "cn" |
- 建档走
POST /v1/games/milthm/profiles:只落库。 - 同一
(region, milthmUserId)已有未解绑档案 ⇒409(Milthm account is already linked)。 region传其他值不会报错:非global一律按cn处理(这是服务端的既有归一)。- 字段式建档后
syncCapable: false;取值以字段为准。 name也是建档必填项,但它是统一字段(player.name)而不是身份键,所以不在上表;10 个游戏一致。
统一字段(player)
| 字段 | 类型 | 可空 | 单位 | 说明 |
|---|---|---|---|---|
name | string | 可空 | — | 显示名(优先 nickname,回落 username) |
rating | number | 可空 | Reality(小数) | 取档案 summary.calculated_reality:服务端按 Best 20 重算的值 |
player.rating是 RankHub 重算的 Reality(calculated_reality),不是官方云存档里的reality_value(那个值保留在原生对象里)。两者在定数改版或成绩不足 20 条时会不同。
原生字段(player.native)
| 原生字段(wire 键) | 类型 | 单位 / 取值 | 语义 | 可空 |
|---|---|---|---|---|
userId | string | — | 玩家 id(原生 user_id) | 可空 |
region | string | cn / global | 区服 | 可空 |
topCount | number | ≥ 0 | 参与 Best 20 计算的成绩条数(min(20, M) 口径下的实际条数) | 可空 |
档案里还有 username / nickname(已进统一字段 name)与云存档元数据(cloud_meta_json),
不在 player.native 里。
Score 格式
成绩键与比较值
| 项 | 值 | 说明 |
|---|---|---|
keyFields | songId | 格式端点声明的键是统一字段 songId(wire 上没有 chartId 顶层字段)。它的值取自 song_id,回落 chart_id——真实存储键是 chartId(谱面级),请注意同一曲目会有多行 |
compareField | score | 「只升不降」比较的是统一 score(原生字段 best_score,0~1,010,000) |
ratingField | rating | 单曲评分字段是统一 rating(原生字段 reality)。值由调用方给或服务端按公式算 |
- 推成绩时
scores[].chartId缺省会回落到scores[].songId(milthm 的谱面主键通常就是chartId); wire 上两者被读成同一个值,但 storage 层按chartId去重。 scores[].realityFormula可选:v2/v3,缺省v2。它决定这条成绩用哪套单曲公式, 但不出现在 wire 响应里(只在库内与推导时使用)。- 写路径的"只升不降"比较值是分数(原生
best_score→ 统一score),不是 Reality:分数更高但 Reality 更低的情况是存在的 (两套公式的换算不同)。
本页
keyFields/compareField/ratingField里的名字都是 v1 wire 字段名(GameScoreDto的顶层字段);游戏原生列名写在说明列。
统一字段
milthm 会填的统一字段:
| 字段 | 类型 | 单位 / 量纲 | 可空 | 说明 |
|---|---|---|---|---|
scoreId | string | — | 否 | 成绩行 id |
songId | string | — | 否 | 曲目 id(song_id,回落 chart_id) |
songName | string | — | 可空 | 曲名(来自曲库) |
difficulty | string | 难度名(Drizzle / Sprinkle / Cloudburst / Special / Clear) | 可空 | 难度(来自曲库) |
achievements | number | 百分数 0~100 | 可空 | 最高准确率 bestAccuracy(不是 0~1) |
score | number | 0~1,010,000 | 可空 | 最高分 bestScore(比较值) |
rating | number | 小数,Reality | 可空 | 单曲 Reality |
playTime | string | — | 恒 null | 只存「每谱面最佳」,没有逐次游玩时间 |
恒为 null 的字段:type、levelIndex(milthm 没有难度下标)、level、rank
(官方 grade 未映射)、fc / fs、dxScore / dxStar。
原生字段(score.native)
milthm 的 score.native 是固定的 6 个键:
| 原生字段(wire 键) | 类型 | 单位 / 取值 | 语义 | 可空 |
|---|---|---|---|---|
chartId | string | — | 谱面 id(成绩的真正主键) | 可空 |
artist | string | — | 曲师 | 可空 |
bestLevel | number | 0~100 | 达成等级(0 为最低);语义未在官方文档定义 | 可空 |
constantV2 | number | ≥ 0 | 谱面定数(V2 口径) | 可空 |
constantV3 | number | ≥ 0 | 谱面定数(V3 口径);两套定数可能不同 | 可空 |
resourceMissing | boolean | — | 曲库缺失标记:true 表示这条成绩算不出可靠 Reality | 可空 |
原生成绩里还有 BestScore / BestAccuracy / AchievedStatus 等,分别落到 score /
achievements /(不映射,只在库内);achieved_status 数组与 reality_formula 不进 native。
评分语义与量纲
单曲 Reality 由「分数 q + 谱面定数 d」决定,Reality V3 是官方公开口径:
R_entry(q, d) =
d + 1.5 , q >= 1,000,000
d + (q - 850,000) / 100,000 , 850,000 <= q < 1,000,000
max(d × (0.5 + (q - 700,000) / 300,000) + (q - 850,000)/100,000, 0) , 700,000 <= q < 850,000
max((d - 3) × (q - 600,000) / 200,000, 0) , 600,000 <= q < 700,000
0 , q < 600,000| 量 | 量纲 | 说明 |
|---|---|---|
q(bestScore) | 整数 0~1,010,000 | ScoreV3:准度分 100 万 + 连击分 0.5 万 + AP 附加分 0.5 万 |
d(定数) | 小数,≥ 0 | 有两套:constantV2 / constantV3 |
| 单曲 Reality | 小数,≥ 0 | 上限 d + 1.5(q ≥ 1,000,000 起不再增长);分段在 60 万 / 70 万 / 85 万 / 100 万四点连续 |
player.rating(总分) | 小数 | 单曲 Reality 降序前 20 的平均 |
- 双版本:4.0 起同时存在 V2 与 V3 公式。官方规则是每张谱面独立取
max(V2, V3); RankHub 用启发式(记录来源 / 达成等级 / 分数阈值 / 达成状态)判断该条用哪套,两者不等价。 - V2 无官方文档:社区口径的分段(
1,005,000起1 + d、995,000/980,000/700,000分段) 与实现一致,但只能算community证据。 - 总评分母:官方规范写死「分母恒 20,不足 20 条按 0 补齐」;RankHub 的实现是 除以实际条数。所以成绩不足 20 条时,RankHub 算出的 Reality 会偏高(1 条成绩时差 20 倍)。
- 舍入:官方未规定;wire 上统一
round3(3 位小数)。
最佳成绩桶
GET /v1/games/milthm/profiles/{profileId}/bests 返回一个桶:
| key | label | 成员规则 | totals 键 |
|---|---|---|---|
best20 | Best 20 | 每张谱面(chartId)取最佳成绩,按单曲 reality 降序取前 20 条 | reality / topCount |
totals.reality= 档案的 Reality(round3),即 Bucket 20 的重算结果。totals.topCount= 参与计算的条数(min(20, M))。这个数不是常量:新档案可能小于 20。buckets[].total= 桶内rating之和,不是 Reality(不要当总分展示)。- 去重粒度是谱面不是曲目:同一首歌的两个难度可以同时进 Best 20。
写入(第三方)
写入只有两种形状:RankHub 格式(上文两张统一字段表里的字段)与游戏原生格式(native)。
共享字段、批量上限与失败语义见写入格式;本节只列 milthm 真正会读的字段。
建档(POST /v1/games/milthm/profiles)
player 用上文的身份字段(milthmUserId 必填,region 缺省 cn)+ name。可以同时带 scores[],
返回的 player.rating 以落库后的汇总(calculated reality)为准。身份重复 ⇒ 409。
推成绩(POST .../profiles/{profileId}/scores)
| 字段 | 类型 | 必给 | 说明 |
|---|---|---|---|
songId | string | 支持 | 曲目 id |
chartId | string | 强烈建议 | 谱面 id(如 "1001-3"),缺省回落 songId;同曲多难度不分开会互相覆盖 |
difficulty | string | 可选 | 难度标识,归一成字符串 |
score | number | 支持 | 0~1,010,000;比较值,缺省 0 |
accuracy | number | 支持 | 百分数 0~100(缺省回落 achievements,再缺省 0) |
rating | number | 支持 | 单曲 Reality,调用方给;缺省落 0,服务端不代算(milthm 不属于服务端算分族) |
realityFormula | string | 可选 | v3 = Reality V3 公式;其它值按 v2;决定 constant 写进哪一列 |
constant | number | 可选 | 谱面定数(v2 → constant_v2,v3 → constant_v3) |
artist | string | 可选 | 曲师 |
curl -sS -X POST https://api.rankhub.cc/v1/games/milthm/profiles/$PROFILE_ID/scores \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"scores": [
{
"songId": "1001",
"chartId": "1001-3",
"difficulty": "Cloudburst",
"score": 1005000,
"accuracy": 99.85,
"rating": 14.2,
"realityFormula": "v3",
"constant": 12.8
}
]
}'以上为示意数据:字段真实,数值虚构。响应
applied[].songId回的是chartId。
原生数据(native)
player.native / scores[].native 是游戏原生格式:与顶层同名字段同时出现时 native 优先(浅合并)。
只给 native 时,顶层的 name / milthmUserId 可以省略。
建档(player.native)
native 键 | 类型 | 必填 | 换算 |
|---|---|---|---|
UserID | string(非空) | 支持 | → milthmUserId |
Nickname / Username | string | 可选 | → name(优先 Nickname,再 Username,最后回落 UserID) |
推成绩(scores[].native)
native 键 | 类型 | 必填 | 换算 |
|---|---|---|---|
BeatmapID | string(非空) | 支持 | → chartId 和 songId(milthm 的谱面 id 同时当曲目 id) |
BestScore | integer 0~1,010,000 | 支持 | → score(比较值) |
BestAccuracy | number 0~100(百分数) | 支持 | → accuracy 和 achievements |
可写字段 / 不可改
| 位置 | 可写 |
|---|---|
player.native(profile) | name、top_count(PATCH .../profiles/{profileId} 的 native 同样生效) |
scores[].native(score,除唯一键) | BestScore、BestAccuracy |
reality需要曲库定数,native 里没有它的输入 ⇒ 单曲 Reality 仍用顶层rating。- 不可改:身份字段
UserID/region(UserID与现值不同 ⇒400 common.invalidArgument;region无法通过native改); 成绩唯一键BeatmapID(native里必须给,换了就是另一条成绩)。
本游戏的写入规则
- 必给:
songId+score+accuracy+rating;chartId强烈建议给(缺省会拿songId当谱面键,同曲多难度互相覆盖)。 - 比较值是
score(原生best_score):新值不高于现值 ⇒ 不覆盖,回improved: false+ignored(notImproved)。 - 谁算分:单曲 Reality 由调用方给(
rating,服务端不代算);总分(Best 20 口径)由服务端重算。 realityFormula决定定数落哪一列:v3才用 V3 公式(constant→constant_v3),其它一律按 v2。
消费者注意事项
achievements是bestAccuracy,量纲0~100(百分数)。官方在线 API 的score_accuracy是0~1——跨源合并时必须换算,别混用。- 一条成绩是一张谱面:wire 的
keyFields是songId,但它取自song_id、回落chart_id(= 谱面级存储键), 所以同一曲目的多个难度会得到多行、songId重复。用songId合并会把多难度成绩覆盖掉; 要按谱面区分请读native.chartId。 player.rating是服务端重算值(Best 20 口径),不是官方云存档的reality_value; 两者在定数改版、成绩不足 20 条、双版本选择不同时会不一致。- 分母口径差异:官方「分母恒 20」,RankHub「除以实际条数」⇒ 不足 20 条时偏高。
totals.topCount < 20就是提示。 - 两套定数并存:
native.constantV2与native.constantV3可能不同;判断某条成绩用了哪套, 要看内部的realityFormula(wire 上不返回)。 resourceMissing: true表示这条成绩不可靠:曲库缺该谱面时算不出可信 Reality, 展示层应标记而不是当作正常值。totals.reality与buckets[].total不是一个数:前者是 Reality,后者是桶内合计(和)。playTime恒null、rank恒null:milthm 只存「每谱面最佳」,官方grade未映射。