#!/usr/bin/env python3 # -*- coding: utf-8 -*- """舞萌DX 谱面达成率 / DX 分计算器。 正向:给 谱面 + 各判定数量 -> 算达成率与 DX 分 反向:给 谱面 + 目标达成率 -> 倒算所有可行的判定组合与 DX 分 只依赖 Python 标准库。 """ from __future__ import annotations import argparse import json import os import re import sys from fractions import Fraction from typing import Dict, List, Optional sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from ma2 import (DIFFICULTY_NAMES, GROUP_LABEL, GROUP_ORDER, Chart, chart_path, find_music_dir, level_display, parse_difficulty, parse_ma2, parse_music_xml) from score import (CLASS_LABEL, CLASS_ORDER, CLASS_WEIGHT, DX_PER, JUDGE_LABEL, JUDGMENTS, Theory, class_capacity, dx_score, reference_achievement, solve_exact, solve_forward, solve_reverse, theory_from_chart) try: # 中文输出到管道时避免 UnicodeEncodeError sys.stdout.reconfigure(errors="replace") sys.stderr.reconfigure(errors="replace") except Exception: # noqa: BLE001 pass LINE = "-" * 60 def pct(value) -> str: return "%.4f%%" % float(value) def die(msg: str, code: int = 2) -> "None": print("[错误] " + msg, file=sys.stderr) raise SystemExit(code) # --------------------------------------------------------------- 载入谱面 def _enrich(chart: Chart, path: str) -> Chart: """从同目录的 Music.xml 补全曲名 / 难度 / 等级(拿不到就跳过)。""" base = os.path.basename(path) stem = os.path.splitext(base)[0] if "_" in stem: left, right = stem.split("_", 1) try: chart.music_id = int(left) chart.difficulty_index = int(right) chart.difficulty_name = DIFFICULTY_NAMES[chart.difficulty_index] except (ValueError, IndexError): pass xml_path = os.path.join(os.path.dirname(path), "Music.xml") if os.path.isfile(xml_path): info = parse_music_xml(xml_path) chart.song_name = info.get("name") or chart.song_name notes = info.get("notes") or [] idx = chart.difficulty_index if idx is not None and 0 <= idx < len(notes): entry = notes[idx] chart.level_text = level_display(entry.get("level"), entry.get("levelDecimal")) try: chart.max_notes_xml = int(entry.get("maxNotes") or 0) except (TypeError, ValueError): chart.max_notes_xml = None return chart def load_from_file(path: str) -> Chart: path = os.path.abspath(os.path.expanduser(path)) if not os.path.isfile(path): die("谱面文件不存在:%s" % path) return _enrich(parse_ma2(path), path) def load_from_game(game_path: str, music_id: int, difficulty: int) -> Chart: music_dir = find_music_dir(game_path) path = chart_path(music_dir, music_id, difficulty) if not os.path.isfile(path): alt = os.path.join(music_dir, "music%06d" % music_id) hint = "" if os.path.isdir(alt): have = sorted(f for f in os.listdir(alt) if f.lower().endswith(".ma2")) hint = ";该曲目现有谱面:" + (", ".join(have) if have else "无") die("找不到谱面:%s%s" % (path, hint)) chart = parse_ma2(path) chart.music_id = music_id chart.difficulty_index = difficulty chart.difficulty_name = DIFFICULTY_NAMES[difficulty] if 0 <= difficulty <= 4 else "?" chart = _enrich(chart, path) chart.difficulty_index = difficulty chart.difficulty_name = DIFFICULTY_NAMES[difficulty] if 0 <= difficulty <= 4 else "?" return chart # ------------------------------------------------------------------ 展示 def print_chart(chart: Chart, th: Theory) -> None: print(LINE) print("谱面") print(" 文件 %s" % chart.path) if chart.song_name: print(" 曲目 %s" % chart.song_name) if chart.music_id is not None: print(" 编号 %d" % chart.music_id) if chart.difficulty_name: 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) if n: parts.append("%s %d" % (GROUP_LABEL[g], n)) print(" 音符 %s" % " · ".join(parts)) print(" 合计 %d 个计分音符" % th.total_notes) if chart.connect_count: print(" (另有 %d 个接续滑条节点,不计分)" % chart.connect_count) bonus = " + 绝赞奖励 %d" % th.break_bonus_score if th.break_bonus_score else "" print(" 理论分 %d%s" % (th.all_perfect_score, bonus)) print(" 理论DX %d" % th.max_dx) print(" 满分 %s" % pct(th.max_achievement)) for w in chart.warnings: print(" [警告] %s" % w) declared = chart.totals.get("TTM_SCR_SS") if declared is not None and declared != th.all_perfect_score: print(" [警告] 谱面自带 TTM_SCR_SS=%d 与计算出的理论分 %d 不一致" % (declared, th.all_perfect_score)) def print_forward(chart: Chart, result) -> None: th = result.theory print(LINE) print("判定输入(汇总)") for j in JUDGMENTS: print(" %-18s %d" % (JUDGE_LABEL[j], result.judges.get(j, 0))) print(" %-18s %d / %d" % ("合计", sum(result.judges.values()), th.total_notes)) print(LINE) print("结果") exact = result.achievement_min == result.achievement_max if exact: print(" 达成率 %s (确切值)" % pct(result.achievement_min)) else: print(" 达成率 %s (按音符类别比例分摊的估计值)" % pct(result.achievement_ref)) print(" 区间 %s ~ %s" % (pct(result.achievement_min), pct(result.achievement_max))) print(" DX 分 %d / %d (%.2f%%)" % (result.dx, th.max_dx, 100.0 * result.dx / th.max_dx if th.max_dx else 0.0)) if not exact: print() print(" 说明:只给汇总判定数时,达成率取决于 Great/Good/Miss 落在哪类音符上——") print(" 区间下界 = 扣分都落在权重最高的音符上,上界 = 都落在权重最低的音符上。") print(" 想要一个确切数值:用 --tap / --hold / --slide / --break 按类别给判定数。") print(LINE) _EXACT_KEYS = ("cp", "perfect", "perfect50", "great", "great40", "great50", "good", "miss") _EXACT_KEY_LABEL = { "cp": "CP", "perfect": "Perfect", "perfect50": "Perfect(50)", "great": "Great", "great40": "Great(40%)", "great50": "Great(50%)", "good": "Good", "miss": "Miss", } # 打印时用纯 ASCII,避免中文宽字符把表格撑歪 _EXACT_CLASS_TITLE = { "tap": "Tap/Star/Touch/EX", "hold": "Hold", "slide": "Slide", "break": "Break", } def print_exact(chart: Chart, result) -> None: th = result.theory print(LINE) print("判定输入(按音符类别;Break = 绝赞;没给的部分按 CP 算)") for cls in CLASS_ORDER: cap = class_capacity(th, cls) if not cap: continue parts = [] for key in _EXACT_KEYS: n = result.spec.get(cls, {}).get(key, 0) if n: parts.append("%s %d" % (_EXACT_KEY_LABEL[key], n)) print(" %-24s %4d 个:%s" % ("%s (%d)" % (_EXACT_CLASS_TITLE[cls], CLASS_WEIGHT[cls]), cap, " · ".join(parts) if parts else "-")) print(LINE) print("结果") print(" 达成率 %s (确切值)" % pct(result.achievement)) print(" DX 分 %d / %d (%.2f%%)" % (result.dx, th.max_dx, 100.0 * result.dx / th.max_dx if th.max_dx else 0.0)) if th.all_perfect_score: print(" 失分 %d / %d (%.4f%%)" % (result.loss_total, th.all_perfect_score, 100.0 * result.loss_total / th.all_perfect_score)) if th.break_bonus_score: print(" 绝赞奖励 %d / %d (%.4f%%)" % (result.bonus_total, th.break_bonus_score, 100.0 * result.bonus_total / th.break_bonus_score)) print(LINE) def print_reverse(chart: Chart, outcome) -> None: th = outcome["theory"] rows = outcome["rows"] mode = outcome.get("mode", "exact") print(LINE) print("目标达成率 %s [%s]" % (pct(outcome["target"]), "精确模式" if mode == "exact" else "区间模式")) note = outcome.get("note") if note: print(" [说明] %s" % note) if not rows: if mode == "exact": cap = outcome.get("exact_cap") or 0 print(" (在严格验证范围内没有找到解:坏音符 ≤ %d 的组合里没有能精确打出该值的)" % cap) if outcome.get("more_unverified"): print(" [注意] 坏音符超过 %d 个的候选组合没有验证(不保证那里没有解)" % cap) else: print(" (无解:该达成率在这个谱面上打不出来)") print(LINE) return if mode == "exact": print(" Miss Good Great CP+P CP 范围 DX 分范围") else: print(" Miss Good Great CP+P CP 范围 达成率范围 DX 分范围") for r in rows: cp_range = "%d~%d" % (r.cp_min, r.cp_max) if r.cp_min != r.cp_max else str(r.cp_min) dx_range = "%d~%d" % (r.dx_min, r.dx_max) if r.dx_min != r.dx_max else str(r.dx_min) if mode == "exact": print(" %4d %4d %5d %4d %-11s %s" % (r.miss, r.good, r.great, r.cp_plus_perfect, cp_range, dx_range)) else: ach = "%s ~ %s" % (pct(r.achievement_min), pct(r.achievement_max)) print(" %4d %4d %5d %4d %-11s %-24s %s" % (r.miss, r.good, r.great, r.cp_plus_perfect, cp_range, ach, dx_range)) print() if mode == "exact": print(" 共 %d 组能精确打出 %s 的判定组合(CP+P = Critical Perfect 与 Perfect 之和)" % (len(rows), pct(outcome["target"]))) cap = outcome.get("exact_cap") or 0 print(" 已逐个严格验证 Miss+Good+Great ≤ %d 的组合;每个组合都真的能打出该达成率" % cap) if outcome.get("unverified"): print(" [注意] 另有 %d 组候选(坏音符超过 %d 个)未做精确验证,未列入结果" % (outcome["unverified"], cap)) if outcome.get("more_unverified"): print(" [注意] 坏音符超过 %d 个的候选组合没有验证(不保证那里没有解)" % cap) else: print(" 共 %d 组可行组合(CP+P = Critical Perfect 与 Perfect 之和)" % len(rows)) print(" 区间模式:只保证达成率落在这个范围内,不保证能恰好打出目标值") if outcome["truncated"]: print(" [注意] 结果被 --limit / 扫描上限截断,可能还有更多组合") print(" DX 分 = 3*CP + 2*Perfect + 1*Great;CP 越靠范围上端,DX 分越高") print(LINE) # ------------------------------------------------------------------ JSON def to_json(chart: Chart, th: Theory, payload: Dict) -> str: data = { "chart": { "path": chart.path, "music_id": chart.music_id, "song_name": chart.song_name, "difficulty": chart.difficulty_name, "difficulty_index": chart.difficulty_index, "level_text": chart.level_text, "notes": th.counts, "total_notes": th.total_notes, "connect_slide": chart.connect_count, "all_perfect_score": th.all_perfect_score, "break_bonus_score": th.break_bonus_score, "max_dx": th.max_dx, "max_achievement": float(th.max_achievement), "warnings": chart.warnings, }, } data.update(payload) return json.dumps(data, ensure_ascii=False, indent=2) # ------------------------------------------------------------------ 交互 def ask(prompt: str, default: str = "") -> str: try: text = input(prompt).strip() except EOFError: text = "" return text or default def ask_int(prompt: str, default: Optional[int] = None) -> Optional[int]: while True: raw = ask(prompt, "" if default is None else str(default)) if raw == "" and default is None: return None try: return int(raw) except ValueError: print(" 请输入整数") def interactive(args) -> None: print("=" * 60) print("舞萌DX 谱面达成率 / DX 分 计算器") print("=" * 60) chart = None while chart is None: mode = ask("谱面来源:1) .ma2 文件路径 2) 游戏路径 + musicid [1]: ", "1") if mode.startswith("2"): game = ask("游戏路径(Sinmai_Data 目录): ") if not game: continue mid = ask_int("musicid: ") if mid is None: continue 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: chart = None else: path = ask("谱面文件路径: ") if not path: continue try: chart = load_from_file(path) except SystemExit: chart = None th = theory_from_chart(chart) print_chart(chart, th) mode = ask("计算模式:1) 汇总判定数(估计值 + 区间) 2) 按音符类别给判定数(确切值)" " 3) 由达成率倒算判定数 [1]: ", "1") if mode.startswith("3"): while True: raw = ask("目标达成率(如 99.5 或 100.5): ") if not raw: return try: target = Fraction(raw.rstrip("%")) break except (ValueError, ZeroDivisionError): print(" 请输入数字") mode = ask("反算方式:1) 精确(只列真能打出该达成率的组合) 2) 区间(判定数只是估算时用) [1]: ", "1") args.mode = "range" if mode.startswith("2") else "exact" run_reverse(chart, th, target, args) elif mode.startswith("2"): spec = {} for cls in CLASS_ORDER: cap = class_capacity(th, cls) if not cap: continue raw = ask(" %s(%d 个)CP/Perfect/Great/Good/Miss [%d/0/0/0/0]: " % (CLASS_LABEL[cls], cap, cap)) if not raw: continue try: spec[cls] = parse_class_counts(raw, cls) except ValueError as exc: print(" [错误] %s" % exc) run_exact(chart, th, spec, args) else: values = {} for j in JUDGMENTS: if j == "miss": continue values[j] = ask_int(" %s 数 [0]: " % JUDGE_LABEL[j], 0) or 0 values["miss"] = ask_int(" Miss 数(留空 = 总数减去以上判定): ") run_forward(chart, th, values, args) # ------------------------------------------------------------------ 执行 def normalize_judges(th: Theory, raw: Dict[str, Optional[int]]) -> Dict[str, int]: given = {j: int(raw.get(j) or 0) for j in JUDGMENTS} if raw.get("miss") is None: given["miss"] = th.total_notes - sum(given[j] for j in JUDGMENTS if j != "miss") if given["miss"] < 0: die("CP+Perfect+Great+Good 合计 %d 超过了音符总数 %d" % (sum(given[j] for j in JUDGMENTS if j != "miss"), th.total_notes)) total = sum(given.values()) if total != th.total_notes: die("判定数之和 %d 与音符总数 %d 不一致(Miss 请留空自动补足)" % (total, th.total_notes)) return given _CLASS_KEYS = ("cp", "perfect", "great", "good", "miss") def parse_class_counts(text: str, cls: str) -> Dict[str, int]: """把 "CP/Perfect/Great/Good/Miss"(斜杠、逗号或空格分隔)解析成判定数字典。""" parts = [p for p in re.split(r"[\/,\s]+", str(text).strip()) if p] if len(parts) > len(_CLASS_KEYS): raise ValueError("%s 最多 %d 个数(CP/Perfect/Great/Good/Miss):%s" % (CLASS_LABEL[cls], len(_CLASS_KEYS), text)) out: Dict[str, int] = {} for key, raw in zip(_CLASS_KEYS, parts): try: n = int(raw) except ValueError: raise ValueError("%s 里的「%s」不是整数:%s" % (CLASS_LABEL[cls], raw, text)) if n < 0: raise ValueError("%s 里不能有负数:%s" % (CLASS_LABEL[cls], text)) if n: out[key] = n return out def build_spec(args) -> Dict[str, Dict[str, int]]: """把 --tap/--hold/--slide/--break 以及绝赞档位参数拼成 solve_exact 的输入。""" spec: Dict[str, Dict[str, int]] = {} for cls in CLASS_ORDER: text = getattr(args, cls, None) if text: spec[cls] = parse_class_counts(text, cls) brk = dict(spec.get("break") or {}) perfect_total = brk.get("perfect", 0) great_total = brk.get("great", 0) p50 = int(args.break_perfect50 or 0) g40 = int(args.break_great40 or 0) g50 = int(args.break_great50 or 0) if p50 or g40 or g50: if p50 > perfect_total: raise ValueError("--break-perfect50 %d 超过绝赞 Perfect 总数 %d" "(先在 --break 里写 CP/Perfect/Great/Good/Miss)" % (p50, perfect_total)) if g40 + g50 > great_total: raise ValueError("--break-great40 + --break-great50 = %d 超过绝赞 Great 总数 %d" % (g40 + g50, great_total)) if p50: brk["perfect"] = perfect_total - p50 brk["perfect50"] = p50 if g40: brk["great40"] = g40 if g50: brk["great50"] = g50 if great_total - g40 - g50: brk["great"] = great_total - g40 - g50 else: brk.pop("great", None) brk = {k: v for k, v in brk.items() if v} spec["break"] = brk return spec def run_exact(chart: Chart, th: Theory, spec: Dict[str, Dict[str, int]], args) -> None: try: result = solve_exact(th, spec) except ValueError as exc: die(str(exc)) if args.json: print(to_json(chart, th, { "mode": "forward_exact", "judges_by_class": result.spec, "achievement": float(result.achievement), "dx_score": result.dx, "loss_total": result.loss_total, "bonus_total": result.bonus_total, })) return print_chart(chart, th) print_exact(chart, result) 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, { "mode": "forward", "judges": judges, "achievement_min": float(result.achievement_min), "achievement_max": float(result.achievement_max), "achievement_reference": float(result.achievement_ref), "dx_score": result.dx, })) return print_chart(chart, th) print_forward(chart, result) 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, { "mode": "reverse", "search_mode": outcome["mode"], "target": float(target), "truncated": outcome["truncated"], "exact_cap": outcome.get("exact_cap"), "unverified": outcome.get("unverified", 0), "more_unverified": outcome.get("more_unverified", False), "note": outcome.get("note"), "solutions": [{ "miss": r.miss, "good": r.good, "great": r.great, "critical_perfect_plus_perfect": r.cp_plus_perfect, "critical_perfect_min": r.cp_min, "critical_perfect_max": r.cp_max, "achievement_min": float(r.achievement_min), "achievement_max": float(r.achievement_max), "dx_min": r.dx_min, "dx_max": r.dx_max, } for r in outcome["rows"]], })) return print_chart(chart, th) print_reverse(chart, outcome) def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( prog="chart_achievement.py", description="舞萌DX 谱面达成率 / DX 分计算器(正向计算 + 由达成率倒算判定数)", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="""示例: # 直接给谱面文件,正向计算(汇总判定数 -> 估计值 + 区间) python chart_achievement.py --chart "D:\\SDGB2026\\Package\\Sinmai_Data\\StreamingAssets\\A000\\music\\music000008\\000008_03.ma2" --cp 290 --perfect 10 --great 2 # 按音符类别给判定数 -> 确切的达成率(CP/Perfect/Great/Good/Miss,可省略后面的) python chart_achievement.py --chart "D:\\SDGB2026\\Package\\Sinmai_Data\\StreamingAssets\\A000\\music\\music000008\\000008_03.ma2" --tap 260/0/3 --break 6 # 给游戏路径 + 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 --level 3 --achievement 99.5 # 判定数只是估算时,用区间模式 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("--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 个数") fw.add_argument("--perfect", type=int, metavar="N", help="Perfect 个数") fw.add_argument("--great", type=int, metavar="N", help="Great 个数") fw.add_argument("--good", type=int, metavar="N", help="Good 个数") fw.add_argument("--miss", type=int, metavar="N", help="Miss 个数(留空 = 总数减去其它判定)") ex = p.add_argument_group( "正向计算(精确):按音符类别给判定数,得到确切的达成率", "每个类别写 CP/Perfect/Great/Good/Miss 五个数(斜杠或空格分隔,后面可以省略)," "没给满的部分按 CP 算。") ex.add_argument("--tap", metavar="CP/P/G/GD/M", help="Tap/Star/Touch/EX 类(权重 500)") ex.add_argument("--hold", metavar="CP/P/G/GD/M", help="Hold 系(权重 1000)") ex.add_argument("--slide", metavar="CP/P/G/GD/M", help="Slide 系(权重 1500)") ex.add_argument("--break", dest="break", metavar="CP/P/G/GD/M", help="绝赞(权重 2500;Great/Perfect 档位见下面三个参数)") ex.add_argument("--break-perfect50", type=int, metavar="N", help="绝赞 Perfect 里奖励分只有 50 的个数(Perfect2nd)") ex.add_argument("--break-great40", type=int, metavar="N", help="绝赞 Great 里失分 40%% 的个数(Great2nd)") ex.add_argument("--break-great50", type=int, metavar="N", help="绝赞 Great 里失分 50%% 的个数(Great3rd)") rv = p.add_argument_group("反向计算:已知达成率") rv.add_argument("--achievement", metavar="A", help="目标达成率(如 99.5 / 100.5)") rv.add_argument("--mode", choices=("exact", "range"), default="exact", help="exact(默认):只列真的能打出该达成率的组合;" "range:列出达成率区间覆盖该值的组合(更快,输入不精确时用)") rv.add_argument("--limit", type=int, default=200, metavar="N", help="反向计算最多输出多少组解(默认 200)") p.add_argument("--json", action="store_true", help="以 JSON 输出结果") return p def main(argv: Optional[List[str]] = None) -> int: args = build_parser().parse_args(argv) if args.chart and args.game: die("--chart 与 --game 只能给一个") if not args.chart and not args.game: interactive(args) 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") 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: die("谱面里没有解析到任何计分音符:%s" % chart.path) if args.achievement: try: target = Fraction(str(args.achievement).rstrip("%")) except (ValueError, ZeroDivisionError): die("--achievement 需要是数字,如 99.5") if target > th.max_achievement: die("目标达成率 %s 超过该谱面上限 %s" % (pct(target), pct(th.max_achievement))) if target < 0: die("目标达成率不能为负") run_reverse(chart, th, target, args) return 0 class_flags = {"tap": args.tap, "hold": args.hold, "slide": args.slide, "break": getattr(args, "break", None), "perfect50": args.break_perfect50, "great40": args.break_great40, "great50": args.break_great50} if any(v for v in class_flags.values()): if any(v is not None for v in (args.cp, args.perfect, args.great, args.good, args.miss)): die("汇总判定数(--cp/--perfect/--great/--good/--miss)" "和按类别判定数(--tap/--hold/--slide/--break)只能给一种") try: spec = build_spec(args) except ValueError as exc: die(str(exc)) run_exact(chart, th, spec, args) return 0 raw = {"cp": args.cp, "perfect": args.perfect, "great": args.great, "good": args.good, "miss": args.miss} if all(v is None for v in raw.values()): die("请给判定数:汇总用 --cp/--perfect/--great/--good," "精确用 --tap/--hold/--slide/--break;或用 --achievement 做反向计算") run_forward(chart, th, raw, args) return 0 if __name__ == "__main__": raise SystemExit(main())