diff --git a/tools/chart_achievement/README.md b/tools/chart_achievement/README.md index 3bfb1b8..9a87e04 100644 --- a/tools/chart_achievement/README.md +++ b/tools/chart_achievement/README.md @@ -20,13 +20,31 @@ python chart_achievement.py # 不给参数 = 交互向导 | `--chart <路径>` | 直接给 `.ma2` 文件,如 `.../music/music000008/000008_03.ma2` | | `--game <目录> --music-id ` | 给游戏目录(`Sinmai_Data`、`StreamingAssets`、`A000`、`music` 都能认),按 `musicid` 自动找谱面 | -`--difficulty basic|advanced|expert|master|remaster`(也接受 0~4),默认 **master**。 -`--level` 只用于显示(如 `12+`),不参与计算。 +### 难度(--level) + +难度用**游戏里规定的数字**表示,也就是谱面文件名后缀 `_00` ~ `_04` 的那个数字 +(对应游戏主数据 `musicDifficulty/MusicDifficulty00000X` 的 ``): + +| 数字 | 难度 | 谱面文件名 | +| --- | --- | --- | +| 0 | BASIC | `_00.ma2` | +| 1 | ADVANCED | `_01.ma2` | +| 2 | EXPERT | `_02.ma2` | +| 3 | MASTER | `_03.ma2` | +| 4 | RE:MASTER | `_04.ma2` | + +`--level 0` ~ `--level 4` 或 `--level BASIC` / `ADVANCED` / `EXPERT` / `MASTER` / `RE:MASTER` +(大小写随意;`--difficulty` 是同一个参数的别名)都行,**默认 3(MASTER)**; +不给时会在 stderr 提示用了默认值。 + +注意:这里的「等级」指**谱面难度**。谱面的数值等级(如 `12+`)是另一回事, +由 `Music.xml` 自动读出,只用于显示。 ## 正向:已知判定数 → 达成率 + DX 分 ```powershell -python chart_achievement.py --chart "<...>\000008_03.ma2" --level 12+ --cp 290 --perfect 8 --great 3 --good 1 +python chart_achievement.py --chart "<...>\000008_03.ma2" --cp 290 --perfect 8 --great 3 --good 1 +python chart_achievement.py --game "<...>\Sinmai_Data" --music-id 8 --level 3 --cp 290 --perfect 8 --great 3 --good 1 # 3 = MASTER ``` - `--cp` / `--perfect` / `--great` / `--good` 都给个数,`--miss` 留空时自动补足 @@ -40,8 +58,8 @@ python chart_achievement.py --chart "<...>\000008_03.ma2" --level 12+ --cp 290 - ## 反向:已知达成率 → 判定组合 + DX 分 ```powershell -python chart_achievement.py --game "D:\SDGB2026\Package\Sinmai_Data" --music-id 8 --achievement 100.75 -python chart_achievement.py --game "D:\SDGB2026\Package\Sinmai_Data" --music-id 8 --achievement 99 --mode range +python chart_achievement.py --game "D:\SDGB2026\Package\Sinmai_Data" --music-id 8 --level 3 --achievement 100.75 # 3 = MASTER +python chart_achievement.py --game "D:\SDGB2026\Package\Sinmai_Data" --music-id 8 --level 2 --achievement 99 --mode range # 2 = EXPERT ``` 两种模式(`--mode`): diff --git a/tools/chart_achievement/chart_achievement.py b/tools/chart_achievement/chart_achievement.py index 1fb4119..76b3c6a 100644 --- a/tools/chart_achievement/chart_achievement.py +++ b/tools/chart_achievement/chart_achievement.py @@ -101,7 +101,7 @@ def load_from_game(game_path: str, music_id: int, difficulty: int) -> Chart: # ------------------------------------------------------------------ 展示 -def print_chart(chart: Chart, th: Theory, level: Optional[str]) -> None: +def print_chart(chart: Chart, th: Theory) -> None: print(LINE) print("谱面") print(" 文件 %s" % chart.path) @@ -110,10 +110,10 @@ def print_chart(chart: Chart, th: Theory, level: Optional[str]) -> None: if chart.music_id is not None: print(" 编号 %d" % chart.music_id) if chart.difficulty_name: - print(" 难度 %s (%02d)" % (chart.difficulty_name, chart.difficulty_index or 0)) - shown_level = level or chart.level_text - if shown_level: - print(" 等级 %s" % shown_level) + print(" 难度 %s (%d)" % (chart.difficulty_name.upper(), + chart.difficulty_index if chart.difficulty_index is not None else 0)) + if chart.level_text: + print(" 等级 %s" % chart.level_text) parts = [] for g in GROUP_ORDER: n = th.counts.get(g, 0) @@ -210,7 +210,7 @@ def print_reverse(chart: Chart, outcome) -> None: # ------------------------------------------------------------------ JSON -def to_json(chart: Chart, th: Theory, level: Optional[str], payload: Dict) -> str: +def to_json(chart: Chart, th: Theory, payload: Dict) -> str: data = { "chart": { "path": chart.path, @@ -218,7 +218,7 @@ def to_json(chart: Chart, th: Theory, level: Optional[str], payload: Dict) -> st "song_name": chart.song_name, "difficulty": chart.difficulty_name, "difficulty_index": chart.difficulty_index, - "level": level or chart.level_text, + "level_text": chart.level_text, "notes": th.counts, "total_notes": th.total_notes, "connect_slide": chart.connect_count, @@ -267,7 +267,7 @@ def interactive(args) -> None: mid = ask_int("musicid: ") if mid is None: continue - diff = ask("难度 basic/advanced/expert/master/remaster [master]: ", "master") + diff = ask("难度 0=BASIC 1=ADVANCED 2=EXPERT 3=MASTER 4=RE:MASTER [3]: ", "3") try: chart = load_from_game(game, mid, parse_difficulty(diff)) except SystemExit: @@ -281,8 +281,7 @@ def interactive(args) -> None: except SystemExit: chart = None th = theory_from_chart(chart) - level = ask("等级(仅用于显示,可留空): ") or None - print_chart(chart, th, level) + print_chart(chart, th) mode = ask("计算模式:1) 已知判定数算达成率 2) 已知达成率倒算判定数 [1]: ", "1") if mode.startswith("2"): @@ -297,7 +296,7 @@ def interactive(args) -> None: print(" 请输入数字") mode = ask("反算方式:1) 精确(只列真能打出该达成率的组合) 2) 区间(判定数只是估算时用) [1]: ", "1") args.mode = "range" if mode.startswith("2") else "exact" - run_reverse(chart, th, level, target, args) + run_reverse(chart, th, target, args) else: values = {} for j in JUDGMENTS: @@ -305,7 +304,7 @@ def interactive(args) -> None: continue values[j] = ask_int(" %s 数 [0]: " % JUDGE_LABEL[j], 0) or 0 values["miss"] = ask_int(" Miss 数(留空 = 总数减去以上判定): ") - run_forward(chart, th, level, values, args) + run_forward(chart, th, values, args) # ------------------------------------------------------------------ 执行 @@ -322,12 +321,12 @@ def normalize_judges(th: Theory, raw: Dict[str, Optional[int]]) -> Dict[str, int return given -def run_forward(chart: Chart, th: Theory, level: Optional[str], +def run_forward(chart: Chart, th: Theory, raw: Dict[str, Optional[int]], args) -> None: judges = normalize_judges(th, raw) result = solve_forward(th, judges) if args.json: - print(to_json(chart, th, level, { + print(to_json(chart, th, { "mode": "forward", "judges": judges, "achievement_min": float(result.achievement_min), @@ -336,15 +335,15 @@ def run_forward(chart: Chart, th: Theory, level: Optional[str], "dx_score": result.dx, })) return - print_chart(chart, th, level) + print_chart(chart, th) print_forward(chart, result) -def run_reverse(chart: Chart, th: Theory, level: Optional[str], +def run_reverse(chart: Chart, th: Theory, target: Fraction, args) -> None: outcome = solve_reverse(th, target, limit=args.limit, mode=args.mode) if args.json: - print(to_json(chart, th, level, { + print(to_json(chart, th, { "mode": "reverse", "search_mode": outcome["mode"], "target": float(target), @@ -363,7 +362,7 @@ def run_reverse(chart: Chart, th: Theory, level: Optional[str], } for r in outcome["rows"]], })) return - print_chart(chart, th, level) + print_chart(chart, th) print_reverse(chart, outcome) @@ -374,24 +373,24 @@ def build_parser() -> argparse.ArgumentParser: formatter_class=argparse.RawDescriptionHelpFormatter, epilog="""示例: # 直接给谱面文件,正向计算 - python chart_achievement.py --chart "D:\\SDGB2026\\Package\\Sinmai_Data\\StreamingAssets\\A000\\music\\music000008\\000008_03.ma2" --level 12+ --cp 290 --perfect 10 --great 2 + python chart_achievement.py --chart "D:\\SDGB2026\\Package\\Sinmai_Data\\StreamingAssets\\A000\\music\\music000008\\000008_03.ma2" --cp 290 --perfect 10 --great 2 - # 给游戏路径 + musicid(默认 Master 难度) - python chart_achievement.py --game "D:\\SDGB2026\\Package\\Sinmai_Data" --music-id 8 --difficulty master --cp 300 --perfect 2 + # 给游戏路径 + musicid + 难度(难度用游戏里的数字:0=BASIC 1=ADVANCED 2=EXPERT 3=MASTER 4=RE:MASTER) + python chart_achievement.py --game "D:\\SDGB2026\\Package\\Sinmai_Data" --music-id 8 --level 3 --cp 300 --perfect 2 # 由达成率倒算判定组合(精确模式:只列真能打出该值的组合) - python chart_achievement.py --game "D:\\SDGB2026\\Package\\Sinmai_Data" --music-id 8 --achievement 99.5 + python chart_achievement.py --game "D:\\SDGB2026\\Package\\Sinmai_Data" --music-id 8 --level 3 --achievement 99.5 # 判定数只是估算时,用区间模式 - python chart_achievement.py --game "D:\\SDGB2026\\Package\\Sinmai_Data" --music-id 8 --achievement 99.5 --mode range + python chart_achievement.py --game "D:\\SDGB2026\\Package\\Sinmai_Data" --music-id 8 --level 3 --achievement 99.5 --mode range """) src = p.add_argument_group("谱面来源(二选一)") src.add_argument("--chart", metavar="PATH", help=".ma2 谱面文件路径") src.add_argument("--game", metavar="PATH", help="游戏目录(Sinmai_Data / StreamingAssets / A000 / music 均可)") src.add_argument("--music-id", type=int, metavar="ID", help="曲目编号(配合 --game 使用)") - src.add_argument("--difficulty", default="master", metavar="D", - help="难度 basic/advanced/expert/master/remaster 或 0~4(默认 master)") - src.add_argument("--level", metavar="TEXT", help="等级,仅用于显示(如 12+ / 13.4)") + src.add_argument("--level", "--difficulty", dest="level", default=None, metavar="D", + help="谱面难度,用游戏里的数字:0=BASIC 1=ADVANCED 2=EXPERT " + "3=MASTER 4=RE:MASTER(也可直接写名字;默认 3=MASTER)") fw = p.add_argument_group("正向计算:已知判定数") fw.add_argument("--cp", type=int, metavar="N", help="Critical Perfect 个数") @@ -422,10 +421,26 @@ def main(argv: Optional[List[str]] = None) -> int: return 0 if args.chart: chart = load_from_file(args.chart) + if args.level is not None: + try: + want = parse_difficulty(args.level) + except ValueError as exc: + die(str(exc)) + if chart.difficulty_index is not None and want != chart.difficulty_index: + print("[注意] --level 给的是 %s,但谱面文件名里是 %s,按文件名算" + % (DIFFICULTY_NAMES[want], DIFFICULTY_NAMES[chart.difficulty_index]), + file=sys.stderr) else: if args.music_id is None: die("使用 --game 时必须同时给 --music-id") - chart = load_from_game(args.game, args.music_id, parse_difficulty(args.difficulty)) + if args.level is None: + print("[提示] 没有给 --level,按默认难度 3(MASTER)计算" + "(0=BASIC 1=ADVANCED 2=EXPERT 3=MASTER 4=RE:MASTER)", file=sys.stderr) + try: + difficulty = parse_difficulty(args.level if args.level is not None else "master") + except ValueError as exc: + die(str(exc)) + chart = load_from_game(args.game, args.music_id, difficulty) th = theory_from_chart(chart) if th.total_notes == 0: @@ -440,7 +455,7 @@ def main(argv: Optional[List[str]] = None) -> int: die("目标达成率 %s 超过该谱面上限 %s" % (pct(target), pct(th.max_achievement))) if target < 0: die("目标达成率不能为负") - run_reverse(chart, th, args.level, target, args) + run_reverse(chart, th, target, args) return 0 raw = {"cp": args.cp, "perfect": args.perfect, "great": args.great, @@ -448,7 +463,7 @@ def main(argv: Optional[List[str]] = None) -> int: if all(v is None for v in raw.values()): die("请给判定数(--cp/--perfect/--great/--good),或用 --achievement 做反向计算;" "两个都不给则进入交互模式(需去掉其它参数)") - run_forward(chart, th, args.level, raw, args) + run_forward(chart, th, raw, args) return 0 diff --git a/tools/chart_achievement/ma2.py b/tools/chart_achievement/ma2.py index 16b0639..37301ef 100644 --- a/tools/chart_achievement/ma2.py +++ b/tools/chart_achievement/ma2.py @@ -274,7 +274,8 @@ def parse_difficulty(text) -> int: key = str(text).strip().lower() if key in _DIFFICULTY_ALIASES: return _DIFFICULTY_ALIASES[key] - raise ValueError("无法识别的难度:%s(可用 basic/advanced/expert/master/remaster 或 0~4)" % text) + raise ValueError("无法识别的难度:%s(可用 0=BASIC 1=ADVANCED 2=EXPERT 3=MASTER 4=RE:MASTER," + "或直接写名字)" % text) def find_music_dir(game_path: str) -> str: