Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
53d1532
feat: use real maimai game art on the dan settlement screen
Swan416ya Oct 9, 2026
720e7f1
feat: assemble the dan settlement screen from official maimai parts
Swan416ya Oct 9, 2026
817fdd1
fix: score-coloured achievement digits and a drawn track frame
Swan416ya Oct 9, 2026
618e6e9
Merge remote-tracking branch 'upstream/main' into feat/dan-challenge
Swan416ya Oct 9, 2026
5472309
fix: rebalance the dan settlement row and bottom band
Swan416ya Oct 9, 2026
064276c
fix: put the DX score inside the official label frame
Swan416ya Oct 9, 2026
9977c4b
fix: fill the punched-out slots in the dan result base art
Swan416ya Oct 10, 2026
d475c7d
fix: use the official dan result layout, labels and rank art
Swan416ya Oct 10, 2026
b62f928
fix: tighten the dan digits, narrow the score box, lift the DX row
Swan416ya Oct 10, 2026
f73265a
fix: stop the DX decimal from flying, align the DX label, lift the to…
Swan416ya Oct 10, 2026
a082b8b
refactor: one glyph per cell — drop the per-character decimal hack
Swan416ya Oct 10, 2026
d3a99d4
feat: rebuild the dan card the way the original is built
Swan416ya Oct 10, 2026
e2d32d4
fix: put the decimal on the baseline, flatten the card colour
Swan416ya Oct 10, 2026
c46557e
fix: thin decimal on the baseline, right-aligned achievement, drop th…
Swan416ya Oct 10, 2026
ee09742
fix: stop the pushed-down decimal from bleeding, official score colou…
Swan416ya Oct 10, 2026
f8d55df
feat: redraw the track card on the original KopMBase layout
Swan416ya Oct 10, 2026
5d55588
fix: square off the bottom corners of the achievement box
Swan416ya Oct 10, 2026
cdd299b
fix: drop the decimal cell instead of the atlas tile, and put the ach…
Swan416ya Oct 10, 2026
774890e
chore: ignore repo-local AGENTS.md
Swan416ya Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ __pycache__/
Thumbs.db
results/local-*.json
prompt0.md

# Agent instructions belong in the user-level DSH home, not in this repo.
AGENTS.md
69 changes: 58 additions & 11 deletions docs/dan.md

Large diffs are not rendered by default.

Binary file added docs/images/dan-card-vs-original.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/dan-card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/dan-card2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/dan-dialog-mixed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/dan-page-mixed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/dan-page-perfect.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/dan-settlement-mixed.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/dan-settlement.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/maimai/gam-rank-a.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/maimai/gam-rank-aa.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/maimai/gam-rank-aaa.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/maimai/gam-rank-b.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/maimai/gam-rank-bb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/maimai/gam-rank-bbb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/maimai/gam-rank-c.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/maimai/gam-rank-d.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/maimai/gam-rank-s.png
Binary file added public/assets/maimai/gam-rank-sp.png
Binary file added public/assets/maimai/gam-rank-ss.png
Binary file added public/assets/maimai/gam-rank-ssp.png
Binary file added public/assets/maimai/gam-rank-sss.png
Binary file added public/assets/maimai/gam-rank-sssp.png
Binary file added public/assets/maimai/life-base-green.png
Binary file added public/assets/maimai/life-base-red.png
Binary file added public/assets/maimai/num-26p-outline.png
Binary file added public/assets/maimai/num-26p.png
Binary file added public/assets/maimai/num-90p-outline.png
Binary file added public/assets/maimai/num-90p.png
Binary file added public/assets/maimai/num-score-blue.png
Binary file added public/assets/maimai/num-score-gold.png
Binary file added public/assets/maimai/num-score-red.png
Binary file added public/assets/maimai/result-dani.png
Binary file added public/assets/maimai/result-random.png
Binary file added public/assets/maimai/score-per-blue.png
Binary file added public/assets/maimai/score-per-gold.png
Binary file added public/assets/maimai/score-per-red.png
Binary file added public/assets/maimai/stamp-clear.png
Binary file added public/assets/maimai/stamp-fail.png
Binary file added public/assets/maimai/track-plate.png
386 changes: 247 additions & 139 deletions public/dan.css

Large diffs are not rendered by default.

429 changes: 354 additions & 75 deletions public/dan.js

Large diffs are not rendered by default.

588 changes: 588 additions & 0 deletions scripts/dan-settlement-e2e.mjs

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions scripts/export-original-card.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""把**原版**的乐曲卡片素材原样导出来,供对照。

用途:现在卡片的边框是照着官方贴图的版式用 CSS 画的(`.dan-track-frame`),用户要直接看
原版素材本身。这里把逐题行那一圈的原图导到 `docs/images/original-card/`,并顺手把当前渲染
出来的第 1 张卡裁成一张独立小图 `docs/images/dan-card.png`。

用法:
python scripts/export-original-card.py "<游戏目录>" # 含 resources.assets 的那一层
"""
from __future__ import annotations

import sys
from pathlib import Path

import UnityPy

# 逐题行那一圈的原版贴图。`UI_CMN_RSL_KopMBase_*` 就是官方乐曲卡片的边框(640×140,
# 五种难度各一张,颜色与难度名 BASiC/ADVANCED/EXPERT/MASTER/Re:MASTER 都烘在图里)。
WANTED = [
"UI_CMN_RSL_KopMBase_BSC",
"UI_CMN_RSL_KopMBase_ADV",
"UI_CMN_RSL_KopMBase_EXP",
"UI_CMN_RSL_KopMBase_MST",
"UI_CMN_RSL_KopMBase_MST_Re",
"UI_CMN_RSL_JacketImage_S",
"UI_CMN_RSL_MusicJacket_Base",
"UI_CMN_RSL_JaketTrack",
"UI_CMN_RSL_JaketTrackNo_0",
"UI_CMN_RSL_AllScorekBase",
"UI_DNM_Result_musicBase_01",
"UI_RSL_AchivementCounter",
"UI_DNM_AchivementCounter",
]
PREFIXES = ("UI_CMN_RSL_", "UI_DNM_Result_music", "UI_RSL_Achivement", "UI_DNM_Achivement")


def main() -> int:
if len(sys.argv) < 2:
print(__doc__)
return 2
game = Path(sys.argv[1])
out = Path(__file__).resolve().parent.parent / "docs" / "images" / "original-card"
out.mkdir(parents=True, exist_ok=True)
env = UnityPy.load(str(game / "resources.assets"))
saved, extra = [], []
for obj in env.objects:
if obj.type.name not in ("Texture2D", "Sprite"):
continue
try:
data = obj.read()
name = getattr(data, "m_Name", "") or ""
if not name:
continue
if name in WANTED:
image = data.image
image.save(out / f"{name}.png")
saved.append((name, image.size))
elif any(name.startswith(p) for p in PREFIXES):
extra.append(name)
except Exception as exc: # 有的贴图数据在别的 bundle 里,读不到就跳过
if getattr(obj, "read", None):
try:
name = obj.read().m_Name
except Exception:
name = "?"
if name in WANTED:
print(f" 跳过 {name}:{type(exc).__name__}")
for name, size in sorted(saved):
print(f" 导出 {name} {size[0]}x{size[1]}")
if extra:
print("同前缀但未导出的(想看哪个再说):")
for name in sorted(extra):
print(f" {name}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
114 changes: 114 additions & 0 deletions scripts/extract-maimai-assets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""从 maimai DX 客户端资源里提取段位認定(Dani Mode)结算页用到的贴图。

这是**开发工具**,不是应用依赖:仓库里已经放好了提取结果
(public/assets/maimai/*.png),只有要重新提一遍时才跑它。

用法:
pip install UnityPy texture2ddecoder Pillow
python scripts/extract-maimai-assets.py "<游戏目录>" [输出目录]

`<游戏目录>` 是客户端里含 `resources.assets` 的那一层,例如
E:/Downloads/156Sinmai_Data/156Sinmai_Data/156Sinmai_Data

为什么要这么提:段位認定结算那整屏是**成品图**(页头文字、格纹页眉、两侧水引、
下半部色带与左下角段位名牌白框全部烘焙在底图里),运行时只是把逐题行、达成率数字、
段位名与总达成率盖上去。所以页面直接用这张底图 + 几件小素材,比自己用 CSS 画近似得多。
注意:底图下半部那两三块**挖白的空面板**是原作留给运行时文字的位置,不要再拿别的脚本
把它们续画填掉(第 12 轮干过一次,第 13 轮撤了)。
"""
from __future__ import annotations

import sys
from pathlib import Path

import UnityPy

# 仓库里用的文件名 ← 游戏里的贴图名。只搬这几件,不是整个 UI_DNM 家族(159 张)。
#
# 故意**不**搬的一件:逐题行的乐曲边框 `UI_CMN_RSL_KopMBase_{BSC,ADV,EXP,MST,MST_Re}`
# (640×140,五种难度各一张)。它是逐题行那一圈底色,但颜色与难度名(BASIC/ADVANCED/
# EXPERT/MASTER/Re:MASTER)都烘死在图里,套不上我们自己的 CF 难度分档。所以边框按它的
# 版式用 CSS 画一层(见 dan.css 的 .dan-track-frame):外圈浅色环 + 主色场 + 底部浅色带
# + 左侧白曲绘槽 + 深蓝标题条 + 白色达成率框,尺寸全按这张贴图量出来的像素。
WANTED: dict[str, str] = {
"UI_DNM_Result_Base_01": "result-dani.png", # 「段位認定」整屏底图 980×928
"UI_DNM_Result_Base_03": "result-random.png", # 「ランダム段位認定」整屏底图
"UI_DNM_Result_musicBase_01": "track-plate.png", # 逐题行底板 576×124
"UI_DNM_Icon_Result_01": "stamp-clear.png", # 逐题「可」印 108×108
"UI_DNM_Icon_Result_02": "stamp-fail.png", # 逐题「不可」印
# 数字图集:规整 4×4 网格,逐格由同名 Sprite 定 rect(见 dan.js 的 NUM_FONTS)。
# 图集里全是**纯白剪影**(游戏运行时才染色),所以描边要另外提一层 _Outline。
"UI_CMN_Num_26p": "num-26p.png", # 小号数字 136×160,格 34×40
"UI_CMN_Num_26p_Outline": "num-26p-outline.png", # 同布局的描边剪影
"UI_CMN_Num_90p": "num-90p.png", # 没有成绩时的占位「—」用,356×420,格 89×105
"UI_CMN_Num_90p_Outline": "num-90p-outline.png",
# 分数数字:同布局的三套(blue / gold / red)296×392,格 74×98,自带颜色、
# 不用描边层 —— 原作就是按达成率高低换这一套的颜色。
# 同名的 `UI_NUM_Score_0001111_Base` 故意不搬:那是一张**空图集**(16 格里一个字形都没有,
# 整张只有几列杂散像素),照它渲染数字会得到一片看不见的灰影。
"UI_NUM_Score_0001111_Blue": "num-score-blue.png",
"UI_NUM_Score_0001111_Gold": "num-score-gold.png",
"UI_NUM_Score_0001111_Red": "num-score-red.png",
# 达成率末尾那个大「%」字形:同样按分数分色(没有 base 那版,低档用图集里的 % 格)
"UI_RSL_Score_Per_Gold": "score-per-gold.png", # 80×80
"UI_RSL_Score_Per_Blue": "score-per-blue.png",
"UI_RSL_Score_Per_Red": "score-per-red.png",
# 逐题评级徽章:`UI_GAM_Rank_*` 是**游戏内**那套,单级各一张(源图紧裁,36–110 × 42–44)。
# 故意不用 `UI_CMN_TabTitle_Rank_*`:那是**页签标题**上的区间图 —— AAA 那张画的是
# 「A~AAA」、BBB 是「~BBB」,而且它没有 D/C/B/BB/A/AA,低分全挤在同一张牌上。
"UI_GAM_Rank_D": "gam-rank-d.png",
"UI_GAM_Rank_C": "gam-rank-c.png",
"UI_GAM_Rank_B": "gam-rank-b.png",
"UI_GAM_Rank_BB": "gam-rank-bb.png",
"UI_GAM_Rank_BBB": "gam-rank-bbb.png",
"UI_GAM_Rank_A": "gam-rank-a.png",
"UI_GAM_Rank_AA": "gam-rank-aa.png",
"UI_GAM_Rank_AAA": "gam-rank-aaa.png",
"UI_GAM_Rank_S": "gam-rank-s.png",
"UI_GAM_Rank_Sp": "gam-rank-sp.png",
"UI_GAM_Rank_SS": "gam-rank-ss.png",
"UI_GAM_Rank_SSp": "gam-rank-ssp.png",
"UI_GAM_Rank_SSS": "gam-rank-sss.png",
"UI_GAM_Rank_SSSp": "gam-rank-sssp.png",
# 右下角那个组件:剩余生命底盘(绿 = 通关 / 红 = 未通关)。
# `UI_RSL_DXScore_Base`(「でらっくスコア」标签)**故意不搬**:官方那格是单曲按打击
# 精度算的 DX 分数,我们显示的是四道单题 rating 之和,口径不同,套那个框等于撒谎。
"UI_DNM_Base_Life_01": "life-base-green.png",
"UI_DNM_Base_Life_03": "life-base-red.png",
}


def main(argv: list[str]) -> int:
if len(argv) < 2:
print(__doc__)
return 2
game_dir = Path(argv[1])
out_dir = Path(argv[2]) if len(argv) > 2 else Path("public/assets/maimai")
assets = game_dir / "resources.assets"
if not assets.exists():
print(f"找不到 {assets}")
return 1

out_dir.mkdir(parents=True, exist_ok=True)
env = UnityPy.load(str(assets))
written: dict[str, str] = {}
for obj in env.objects:
if obj.type.name != "Texture2D":
continue
data = obj.read()
name = getattr(data, "m_Name", None) or getattr(data, "name", "") or ""
target = WANTED.get(name)
if target is None or target in written:
continue
image = data.image
image.save(out_dir / target)
written[target] = f"{name} ({image.width}x{image.height})"

for target in WANTED.values():
print(f" {written.get(target, '缺失!'):<34} → {target}")
return 0 if len(written) == len(WANTED) else 1


if __name__ == "__main__":
raise SystemExit(main(sys.argv))
Loading