From addec3e88db891babe1179b9c1c1f51f6f324d1e Mon Sep 17 00:00:00 2001 From: drmext <71258889+drmext@users.noreply.github.com> Date: Sun, 11 Jun 2023 15:29:42 +0000 Subject: [PATCH] IIDX score import --- README.md | 12 +- utils/db/README.md | 20 ++- utils/db/import_iidx_spice_automap.py | 199 ++++++++++++++++++++++++++ 3 files changed, 224 insertions(+), 7 deletions(-) create mode 100644 utils/db/import_iidx_spice_automap.py diff --git a/README.md b/README.md index 196b7ff..ae743d1 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,16 @@ for experimental local testing and playing - DRS requires `music-info-base.xml` copied to the server folder +## Score Import + +Scores can be [imported](utils/db) from any network + +- DDR + +- IIDX + ## Web Interface -- Extract [BounceTrippy](https://github.com/drmext/BounceTrippy/releases) webui to the server folder (DDR only) +Extract [BounceTrippy](https://github.com/drmext/BounceTrippy/releases) webui to the server folder + +- DDR diff --git a/utils/db/README.md b/utils/db/README.md index 23eb1a7..ceda409 100644 --- a/utils/db/README.md +++ b/utils/db/README.md @@ -1,6 +1,6 @@ # Database Utilities -**Backup db.json before shrinking or importing** +**Backup db.json before using these scripts** ## Shrink DB @@ -13,8 +13,6 @@ Example: ## Score Import -### [import_ddr_spice_automap.py](import_ddr_spice_automap.py) - Instructions: 1. Enable `EA Automap` and `EA Netdump` in spicecfg @@ -27,10 +25,20 @@ Instructions: 1. Disable `EA Automap` and `EA Netdump` in spicecfg -1. Run the import script +1. Run the corresponding import script + +### [import_ddr_spice_automap.py](import_ddr_spice_automap.py) Example: `python utils\db\import_ddr_spice_automap.py --automap_xml automap_0.xml --version 19 --monkey_db db.json --ddr_id 12345678` -- `--version` {19 for A20P or 20 for A3} +- `--version` 19 for A20P or 20 for A3 -- `--ddr_id` {destination profile in db.json} +- `--ddr_id` destination profile in db.json + +### [import_iidx_spice_automap.py](import_iidx_spice_automap.py) + +Example: `python utils\db\import_iidx_spice_automap.py --automap_xml automap_0.xml --version 30 --monkey_db db.json --iidx_id 12345678` + +- `--version` must match the source export version (27+ supported) + +- `--iidx_id` destination profile in db.json \ No newline at end of file diff --git a/utils/db/import_iidx_spice_automap.py b/utils/db/import_iidx_spice_automap.py new file mode 100644 index 0000000..d327bc3 --- /dev/null +++ b/utils/db/import_iidx_spice_automap.py @@ -0,0 +1,199 @@ +import argparse +import xml.etree.ElementTree as ET +from enum import IntEnum + +from tinydb import TinyDB, where +from tinydb.middlewares import CachingMiddleware +from tinydb.storages import JSONStorage + + +def main(automap_xml, version, monkey_db, iidx_id): + class ClearFlags(IntEnum): + NO_PLAY = 0 + FAILED = 1 + ASSIST_CLEAR = 2 + EASY_CLEAR = 3 + CLEAR = 4 + HARD_CLEAR = 5 + EX_HARD_CLEAR = 6 + FULL_COMBO = 7 + + storage = CachingMiddleware(JSONStorage) + storage.WRITE_CACHE_SIZE = 5000 + + db = TinyDB( + monkey_db, + indent=2, + encoding="utf-8", + ensure_ascii=False, + storage=storage, + ) + + iidx_id = int(iidx_id.replace("-", "")) + + profile = db.table("iidx_profile").get(where("iidx_id") == iidx_id) + if profile == None: + raise SystemExit(f"ERROR: IIDX profile {iidx_id} not in {monkey_db}") + + game_version = 30 + + scores = [] + + with open(automap_xml, "rb") as fp: + automap_0 = fp.read().split(b"\n\n") + + scores_xml = False + for xml in automap_0: + try: + tree = ET.ElementTree(ET.fromstring(xml.decode(encoding="shift-jis"))) + root = tree.getroot() + except: + continue + if scores_xml: + sp_dp = int(root.find(f"IIDX{version}music/style").get("type")) + print(sp_dp) + for m in root.findall(f"IIDX{version}music/m"): + score = [int(x) for x in m.text.split()] + if score[0] != -1: + # skip rivals + continue + music_id = score[1] + for difficulty in range(5): + d = difficulty + 2 + if score[d] != -1: + clear_flg = score[d] + ex_score = score[d + 5] + miss_count = score[d + 10] + scores.append( + [ + sp_dp, + music_id, + difficulty, + clear_flg, + ex_score, + miss_count, + ] + ) + scores_xml = False + else: + try: + if root.find(f"IIDX{version}music").get("method") == "getrank": + scores_xml = True + except AttributeError: + continue + + total_count = len(scores) + + if total_count == 0: + raise SystemExit("ERROR: No scores to import") + + for s in scores: + play_style = s[0] + music_id = s[1] + difficulty = s[2] + clear_flg = s[3] + ex_score = s[4] + miss_count = s[5] + + print( + f"music_id: {music_id}, sp_dp: {play_style}, difficulty: {difficulty}, clear_flg: {clear_flg}, ex_score: {ex_score}, miss_count: {miss_count}" + ) + + best_score = db.table("iidx_scores_best").get( + (where("iidx_id") == iidx_id) + & (where("play_style") == play_style) + & (where("music_id") == music_id) + & (where("chart_id") == difficulty) + ) + best_score = {} if best_score is None else best_score + + if clear_flg < ClearFlags.EASY_CLEAR: + miss_count = -1 + best_miss_count = best_score.get("miss_count", miss_count) + if best_miss_count == -1: + miss_count = max(miss_count, best_miss_count) + elif clear_flg > ClearFlags.ASSIST_CLEAR: + miss_count = min(miss_count, best_miss_count) + else: + miss_count = best_miss_count + best_ex_score = best_score.get("ex_score", ex_score) + best_score_data = { + "game_version": game_version, + "iidx_id": iidx_id, + "pid": 13, + "play_style": play_style, + "music_id": music_id, + "chart_id": difficulty, + "miss_count": miss_count, + "ex_score": max(ex_score, best_ex_score), + "ghost": best_score.get( + "ghost", + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), + "ghost_gauge": best_score.get( + "ghost_gauge", + "4c0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), + "clear_flg": max(clear_flg, best_score.get("clear_flg", clear_flg)), + "gauge_type": best_score.get("gauge_type", 4), + } + + db.table("iidx_scores_best").upsert( + best_score_data, + (where("iidx_id") == iidx_id) + & (where("play_style") == play_style) + & (where("music_id") == music_id) + & (where("chart_id") == difficulty), + ) + + score_stats = db.table("iidx_score_stats").get( + (where("music_id") == music_id) + & (where("play_style") == play_style) + & (where("chart_id") == difficulty) + ) + score_stats = {} if score_stats is None else score_stats + + score_stats["game_version"] = game_version + score_stats["play_style"] = play_style + score_stats["music_id"] = music_id + score_stats["chart_id"] = difficulty + score_stats["play_count"] = score_stats.get("play_count", 0) + 1 + score_stats["fc_count"] = score_stats.get("fc_count", 0) + ( + 1 if clear_flg == ClearFlags.FULL_COMBO else 0 + ) + score_stats["clear_count"] = score_stats.get("clear_count", 0) + ( + 1 if clear_flg >= ClearFlags.EASY_CLEAR else 0 + ) + score_stats["fc_rate"] = int( + (score_stats["fc_count"] / score_stats["play_count"]) * 1000 + ) + score_stats["clear_rate"] = int( + (score_stats["clear_count"] / score_stats["play_count"]) * 1000 + ) + + db.table("iidx_score_stats").upsert( + score_stats, + (where("music_id") == music_id) + & (where("play_style") == play_style) + & (where("chart_id") == difficulty), + ) + + db.close() + print() + print(f"{total_count} scores imported to IIDX profile {iidx_id} in {monkey_db}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--automap_xml", help="Input xml file", required=True) + parser.add_argument( + "--version", + help="", + default=30, + type=int, + ) + parser.add_argument("--monkey_db", help="Output json file", required=True) + parser.add_argument("--iidx_id", help="12345678", required=True) + args = parser.parse_args() + + main(args.automap_xml, args.version, args.monkey_db, args.iidx_id)