From 032cf5cd405c60c863579ac60f727f71fa30c338 Mon Sep 17 00:00:00 2001 From: drmext <71258889+drmext@users.noreply.github.com> Date: Sun, 7 May 2023 06:43:54 +0000 Subject: [PATCH] Implement DRS --- .gitignore | 2 +- README.md | 14 +- core_common.py | 3 + modules/core/cardmng.py | 1 + modules/drs/__init__.py | 0 modules/drs/eventlog.py | 25 +++ modules/drs/game.py | 482 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 521 insertions(+), 6 deletions(-) create mode 100644 modules/drs/__init__.py create mode 100644 modules/drs/eventlog.py create mode 100644 modules/drs/game.py diff --git a/.gitignore b/.gitignore index b1e2bab..3be6dc3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ *.pyc -mdb*.xml +*.xml /db*.json /*.db .venv/ diff --git a/README.md b/README.md index b2b4b58..196b7ff 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ for experimental local testing and playing ## Instructions -1. Install [python](https://www.python.org/ftp/python/3.11.2/python-3.11.2-amd64.exe) with "Add python.exe to PATH" checked +1. Install [python](https://www.python.org/ftp/python/3.11.3/python-3.11.3-amd64.exe) with "Add python.exe to PATH" checked -1. Run `start.bat` +1. Run [start.bat (Windows)](start.bat) or [start.sh (Linux)](start.sh) 1. Edit prop/ea3-config.xml services *url* and url_slash *1* @@ -19,6 +19,8 @@ for experimental local testing and playing - DDR A20 PLUS - DDR A3 +- DRS + - GITADORA 5 Matixx - GITADORA 6 EXCHAIN - GITADORA 7 NEX+AGE @@ -39,10 +41,12 @@ for experimental local testing and playing - **URL Slash 1 (On)** must be enabled in tools or ea3-config -- GITADORA requires `mdb_*.xml` copied or symlinked to the server folder +- GITADORA requires `mdb_*.xml` copied to the server folder -- NOSTALGIA requires `music_list.xml` copied or symlinked to the server folder +- NOSTALGIA requires `music_list.xml` copied to the server folder + +- DRS requires `music-info-base.xml` copied to the server folder ## Web Interface -- Extract [BounceTrippy](https://github.com/drmext/BounceTrippy/releases) webui to the server folder +- Extract [BounceTrippy](https://github.com/drmext/BounceTrippy/releases) webui to the server folder (DDR only) diff --git a/core_common.py b/core_common.py index 8817cc3..7c581cc 100644 --- a/core_common.py +++ b/core_common.py @@ -106,6 +106,9 @@ async def core_get_game_version_from_software_version(software_version): if ext >= 2020090402: # ??? return 6 + elif model == "REC": + return 1 + else: return 0 diff --git a/modules/core/cardmng.py b/modules/core/cardmng.py index 1d523fa..23533d1 100644 --- a/modules/core/cardmng.py +++ b/modules/core/cardmng.py @@ -14,6 +14,7 @@ def get_target_table(game_id): "KFC": "sdvx_profile", "M32": "gitadora_profile", "PAN": "nostalgia_profile", + "REC": "dancerush_profile", "JDZ": "iidx_profile", "KDZ": "iidx_profile", } diff --git a/modules/drs/__init__.py b/modules/drs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/drs/eventlog.py b/modules/drs/eventlog.py new file mode 100644 index 0000000..9da0d4a --- /dev/null +++ b/modules/drs/eventlog.py @@ -0,0 +1,25 @@ +import config + +from fastapi import APIRouter, Request, Response + +from core_common import core_process_request, core_prepare_response, E + +router = APIRouter(prefix="/local", tags=["local"]) +router.model_whitelist = ["REC"] + + +@router.post("/{gameinfo}/eventlog/write") +async def drs_eventlog_write(request: Request): + request_info = await core_process_request(request) + + response = E.response( + E.eventlog( + E.gamesession(9999999, __type="s64"), + E.logsendflg(0, __type="s32"), + E.logerrlevel(0, __type="s32"), + E.evtidnosendflg(0, __type="s32"), + ) + ) + + response_body, response_headers = await core_prepare_response(request, response) + return Response(content=response_body, headers=response_headers) diff --git a/modules/drs/game.py b/modules/drs/game.py new file mode 100644 index 0000000..ee72a44 --- /dev/null +++ b/modules/drs/game.py @@ -0,0 +1,482 @@ +import xml.etree.ElementTree as ET +from os import path + +from tinydb import Query, where + +import config +import random +import time + +from fastapi import APIRouter, Request, Response + +from core_common import core_process_request, core_prepare_response, E +from core_database import get_db + +router = APIRouter(prefix="/local", tags=["local"]) +router.model_whitelist = ["REC"] + + +def get_profile(cid): + return get_db().table("dancerush_profile").get(where("card") == cid) + + +def get_game_profile(cid, game_version): + profile = get_profile(cid) + + return profile["version"].get(str(game_version), None) + + +def get_id_from_profile(cid): + profile = get_db().table("dancerush_profile").get(where("card") == cid) + + djid = "%08d" % profile["drs_id"] + djid_split = "-".join([djid[:4], djid[4:]]) + + return profile["drs_id"], djid_split + + +@router.post("/{gameinfo}/game/get_common") +async def drs_game_get_common(request: Request): + request_info = await core_process_request(request) + + songs = {} + + # TODO: server side song unlock is incomplete, use hex edits for now + for f in ( + path.join("modules", "drs", "music-info-base.xml"), + path.join("music-info-base.xml"), + ): + if path.exists(f): + with open(f, "r", encoding="utf-8") as fp: + tree = ET.parse(fp, ET.XMLParser()) + root = tree.getroot() + + for entry in root: + mid = entry.get("id") + songs[mid] = {} + for atr in ( + "title_name", + "title_yomigana", + "artist_name", + "artist_yomigana", + "bpm_max", + "bpm_min", + # "distribution_date", + "volume", + "bg_no", + "region", + # "limitation_type", + # "price", + "genre", + "play_video_flags", + "is_fixed", + "version", + "demo_pri", + "license", + "color1", + "color2", + "color3", + ): + songs[mid][atr] = entry.find(f"info/{atr}").text + if songs[mid][atr] == None: + songs[mid][atr] = "" + for atr in ( + "1b", + "1a", + "2b", + "2a", + ): + songs[mid][f"{atr}_difnum"] = entry.find( + f"difficulty/fumen_{atr}/difnum" + ).text + # songs[mid][f"{atr}_playable"] = entry.find(f"difficulty/fumen_{atr}/playable").text + break + + response = E.response( + E.game( + E.mdb( + *[ + E.music( + E.info( + E.title_name(songs[s]["title_name"], __type="str"), + E.title_yomigana(songs[s]["title_yomigana"], __type="str"), + E.artist_name(songs[s]["artist_name"], __type="str"), + E.artist_yomigana( + songs[s]["artist_yomigana"], __type="str" + ), + E.bpm_max(songs[s]["bpm_max"], __type="u32"), + E.bpm_min(songs[s]["bpm_min"], __type="u32"), + E.distribution_date(20180427, __type="u32"), + E.volume(songs[s]["volume"], __type="u16"), + E.bg_no(songs[s]["bg_no"], __type="u16"), + E.region("JUAKYC", __type="str"), + E.limitation_type(3, __type="u8"), + E.price(0, __type="s32"), + E.genre(songs[s]["genre"], __type="u32"), + E.play_video_flags( + songs[s]["play_video_flags"], __type="u32" + ), + E.is_fixed(songs[s]["is_fixed"], __type="u8"), + E.version(songs[s]["version"], __type="u8"), + E.demo_pri(songs[s]["demo_pri"], __type="u8"), + E.license(songs[s]["license"], __type="str"), + E.color1(int(songs[s]["color1"], 16), __type="u32"), + E.color2(int(songs[s]["color2"], 16), __type="u32"), + E.color3(int(songs[s]["color3"], 16), __type="u32"), + ), + E.difficulty( + E.fumen_1b( + E.difnum(songs[s]["1b_difnum"], __type="u8"), + E.playable(1, __type="u8"), + ), + E.fumen_1a( + E.difnum(songs[s]["1a_difnum"], __type="u8"), + E.playable(1, __type="u8"), + ), + E.fumen_2b( + E.difnum(songs[s]["2b_difnum"], __type="u8"), + E.playable(1, __type="u8"), + ), + E.fumen_2a( + E.difnum(songs[s]["2a_difnum"], __type="u8"), + E.playable(1, __type="u8"), + ), + ), + id=s, + ) + for s in songs + ], + ), + E.extra(*[E.info(E.music_id(i, __type="s32")) for i in songs]), + E.contest( + *[ + E.info( + E.contest_id(i, __type="s32"), + E.start_date(1683422123358, __type="u64"), + E.end_date(1693422123358, __type="u64"), + E.title("", __type="str"), + E.regulation(i, __type="s32"), + E.target_music( + E.music( + E.music_id(1, __type="s32"), + E.music_type("1b", __type="str"), + ) + ), + ) + for i in range(1, 3) + ] + ), + E.event( + *[ + E.info( + E.event_id(e, __type="s32"), + E.start_date(1683422123358, __type="u64"), + E.end_date(1693422123358, __type="u64"), + E.param("", __type="str"), + ) + for e in range(1, 14) + ] + ), + # E.kac2020( + # E.reward( + # E.data( + # E.music_id(1, __type="s32"), + # E.is_available(1, __type="bool"), + # ) + # ) + # ), + # E.silhouette(E.info(E.silhouette_id(i, __type="s32"))), + # E.music_condition(*[E.music(E.conditions(), id=s) for s in songs]), + ) + ) + + response_body, response_headers = await core_prepare_response(request, response) + return Response(content=response_body, headers=response_headers) + + +@router.post("/{gameinfo}/game/get_playdata_{player}") +async def drs_game_get_playdata(player: str, request: Request): + request_info = await core_process_request(request) + game_version = request_info["game_version"] + + dataid = request_info["root"][0].find("userid/refid").text + profile = get_game_profile(dataid, game_version) + + if profile: + djid, djid_split = get_id_from_profile(dataid) + + response = E.response( + E.game( + E.result(0, __type="s32"), + E.userid(E.code(djid, __type="s32")), + E.profile(E.name(profile["name"], __type="str")), + E.playinfo( + E.softcode("", __type="str"), + E.start_date(1683422123358, __type="u64"), + E.end_date(1683422123358, __type="u64"), + E.mode_id(profile["mode_id"], __type="s32"), + E.music_id(profile["music_id"], __type="s32"), + E.music_type(profile["music_type"], __type="str"), + E.pcbid("0", __type="str"), + E.locid("EA000001", __type="str"), + ), + E.paramdata( + *[ + E.data( + E.data_type(p[0], __type="s32"), + E.data_id(p[1], __type="s32"), + E.param_list(p[2], __type="s32"), + ) + for p in profile["params"] + ] + ), + E.dance_dance_rush(E.data()), + E.summer_dance_damp(E.data()), + E.kac2020(), + E.hidden_param(0, __type="s32"), + E.play_count(1001, __type="u32"), + E.daily_count(301, __type="u32"), + E.play_chain(31, __type="u32"), + ) + ) + + else: + response = E.response( + E.game( + E.result(1, __type="s32"), + ) + ) + + response_body, response_headers = await core_prepare_response(request, response) + return Response(content=response_body, headers=response_headers) + + +@router.post("/{gameinfo}/game/lock_multi_login_{player}") +async def drs_game_lock_multi_login(player: str, request: Request): + request_info = await core_process_request(request) + + response = E.response(E.game()) + + response_body, response_headers = await core_prepare_response(request, response) + return Response(content=response_body, headers=response_headers) + + +@router.post("/{gameinfo}/game/sign_up_{player}") +async def drs_game_sign_up(player: str, request: Request): + request_info = await core_process_request(request) + game_version = request_info["game_version"] + + root = request_info["root"][0] + + dataid = root.find("userid/dataid").text + cardno = root.find("userid/cardno").text + name = root.find("profile/name").text + + db = get_db().table("dancerush_profile") + all_profiles_for_card = db.get(Query().card == dataid) + + if all_profiles_for_card is None: + all_profiles_for_card = {"card": dataid, "version": {}} + + if "drs_id" not in all_profiles_for_card: + drs_id = random.randint(10000000, 99999999) + all_profiles_for_card["drs_id"] = drs_id + + all_profiles_for_card["version"][str(game_version)] = { + "game_version": game_version, + "name": name, + "mode_id": 0, + "music_id": 1, + "music_type": "1a", + "params": [], + } + + db.upsert(all_profiles_for_card, where("card") == dataid) + + response = E.response(E.game()) + + response_body, response_headers = await core_prepare_response(request, response) + return Response(content=response_body, headers=response_headers) + + +@router.post("/{gameinfo}/game/get_musicscore_{player}") +async def drs_get_musicscore(player: str, request: Request): + request_info = await core_process_request(request) + game_version = request_info["game_version"] + + scores = [] + db = get_db() + for record in db.table("drs_scores_best").search( + (where("game_version") == game_version) + ): + scores.append( + [ + record["music_id"], + record["music_type"], + record["score"], + record["rank"], + record["combo"], + record["param"], + ] + ) + + response = E.response( + E.game( + E.scoredata( + *[ + E.music( + E.music_id(s[0], __type="s32"), + E.music_type(s[1], __type="str"), + E.play_cnt(1, __type="s32"), + E.score(s[2], __type="s32"), + E.rank(s[3], __type="s32"), + E.combo(s[4], __type="s32"), + E.param(s[5], __type="s32"), + E.bestscore_date(1683422123358, __type="u64"), + E.lastplay_date(1683422123358, __type="u64"), + ) + for s in scores + ], + ), + ), + ) + + response_body, response_headers = await core_prepare_response(request, response) + return Response(content=response_body, headers=response_headers) + + +@router.post("/{gameinfo}/game/save_musicscore") +async def drs_save_musicscore(request: Request): + request_info = await core_process_request(request) + game_version = request_info["game_version"] + + timestamp = time.time() + + root = request_info["root"][0][0] + + dataid = root.find("userid/refid").text + profile = get_game_profile(dataid, game_version) + djid, djid_split = get_id_from_profile(dataid) + + music_id = int(root.find("music_id").text) + music_type = root.find("music_type").text + mode = int(root.find("mode").text) + score = int(root.find("score").text) + rank = int(root.find("rank").text) + combo = int(root.find("combo").text) + param = int(root.find("param").text) + perfect = int(root.find("member/perfect").text) + great = int(root.find("member/great").text) + good = int(root.find("member/good").text) + bad = int(root.find("member/bad").text) + + db = get_db() + db.table("drs_scores").insert( + { + "timestamp": timestamp, + "game_version": game_version, + "drs_id": djid, + "music_id": music_id, + "music_type": music_type, + "mode": mode, + "score": score, + "rank": rank, + "combo": combo, + "param": param, + "perfect": perfect, + "great": great, + "good": good, + "bad": bad, + }, + ) + + best = db.table("drs_scores_best").get( + (where("drs_id") == djid) + & (where("game_version") == game_version) + & (where("music_id") == music_id) + & (where("music_type") == music_type) + ) + best = {} if best is None else best + + best_score_data = { + "game_version": game_version, + "drs_id": djid, + "name": profile["name"], + "music_id": music_id, + "music_type": music_type, + "score": max(score, best.get("score", score)), + "rank": max(rank, best.get("rank", rank)), + "combo": max(combo, best.get("combo", combo)), + "param": param, + } + + db.table("drs_scores_best").upsert( + best_score_data, + (where("drs_id") == djid) + & (where("game_version") == game_version) + & (where("music_id") == music_id) + & (where("music_type") == music_type), + ) + + response = E.response(E.game()) + + response_body, response_headers = await core_prepare_response(request, response) + return Response(content=response_body, headers=response_headers) + + +@router.post("/{gameinfo}/game/save_playdata") +async def drs_save_musicscore(request: Request): + request_info = await core_process_request(request) + game_version = request_info["game_version"] + + root = request_info["root"][0][0] + + dataid = root.find("userid/refid").text + + profile = get_profile(dataid) + game_profile = profile["version"].get(str(game_version), {}) + + game_profile["mode_id"] = int(root.find("playinfo/mode_id").text) + game_profile["music_id"] = int(root.find("playinfo/music_id").text) + game_profile["music_type"] = root.find("playinfo/music_type").text + + old_params = game_profile["params"] + params = {} + + for old in old_params: + t = str(old[0]) + i = str(old[1]) + p = old[2] + if t not in params: + params[t] = {} + if i not in params[t]: + params[t][i] = {} + params[t][i] = p + + for info in root.find("paramdata"): + t = info.find("data_type").text + i = info.find("data_id").text + p = info.find("param_list") + + if t not in params: + params[t] = {} + if i not in params[t]: + params[t][i] = {} + params[t][i] = [int(x) for x in p.text.split(" ")] + + params_list = [] + + for t in params: + for i in params[t]: + params_list.append([int(t), int(i), params[t][i]]) + + game_profile["params"] = params_list + + profile["version"][str(game_version)] = game_profile + + get_db().table("dancerush_profile").upsert(profile, where("card") == dataid) + + response = E.response(E.game()) + + response_body, response_headers = await core_prepare_response(request, response) + return Response(content=response_body, headers=response_headers)