From deeac1d8dbe163e3abe090826d16b50af0809cb2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 30 Apr 2023 22:19:31 -0400 Subject: [PATCH 01/40] add finale handler, pre-dx game codes --- titles/mai2/__init__.py | 11 +++++++- titles/mai2/base.py | 3 +-- titles/mai2/const.py | 41 +++++++++++++++++++++++++++++- titles/mai2/dx.py | 15 +++++++++++ titles/mai2/{plus.py => dxplus.py} | 2 +- titles/mai2/finale.py | 15 +++++++++++ titles/mai2/index.py | 24 ++++++++++++++--- 7 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 titles/mai2/dx.py rename titles/mai2/{plus.py => dxplus.py} (93%) create mode 100644 titles/mai2/finale.py diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 810eac9..0a76de8 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -6,5 +6,14 @@ from titles.mai2.read import Mai2Reader index = Mai2Servlet database = Mai2Data reader = Mai2Reader -game_codes = [Mai2Constants.GAME_CODE] +game_codes = [ + Mai2Constants.GAME_CODE_DX, + Mai2Constants.GAME_CODE_FINALE, + Mai2Constants.GAME_CODE_MILK, + Mai2Constants.GAME_CODE_MURASAKI, + Mai2Constants.GAME_CODE_PINK, + Mai2Constants.GAME_CODE_ORANGE, + Mai2Constants.GAME_CODE_GREEN, + Mai2Constants.GAME_CODE, +] current_schema_version = 4 diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 171378c..efa30a0 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -12,8 +12,7 @@ class Mai2Base: def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: self.core_config = cfg self.game_config = game_cfg - self.game = Mai2Constants.GAME_CODE - self.version = Mai2Constants.VER_MAIMAI_DX + self.version = Mai2Constants.VER_MAIMAI self.data = Mai2Data(cfg) self.logger = logging.getLogger("mai2") diff --git a/titles/mai2/const.py b/titles/mai2/const.py index dcc7e29..19cb867 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -20,10 +20,31 @@ class Mai2Constants: DATE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S" - GAME_CODE = "SDEZ" + GAME_CODE = "SBXL" + GAME_CODE_GREEN = "SBZF" + GAME_CODE_ORANGE = "SDBM" + GAME_CODE_PINK = "SDCQ" + GAME_CODE_MURASAKI = "SDDK" + GAME_CODE_MILK = "SDDZ" + GAME_CODE_FINALE = "SDEY" + GAME_CODE_DX = "SDEZ" CONFIG_NAME = "mai2.yaml" + VER_MAIMAI = 1000 + VER_MAIMAI_PLUS = 1001 + VER_MAIMAI_GREEN = 1002 + VER_MAIMAI_GREEN_PLUS = 1003 + VER_MAIMAI_ORANGE = 1004 + VER_MAIMAI_ORANGE_PLUS = 1005 + VER_MAIMAI_PINK = 1006 + VER_MAIMAI_PINK_PLUS = 1007 + VER_MAIMAI_MURASAKI = 1008 + VER_MAIMAI_MURASAKI_PLUS = 1009 + VER_MAIMAI_MILK = 1010 + VER_MAIMAI_MILK_PLUS = 1011 + VER_MAIMAI_FINALE = 1012 + VER_MAIMAI_DX = 0 VER_MAIMAI_DX_PLUS = 1 VER_MAIMAI_DX_SPLASH = 2 @@ -42,6 +63,24 @@ class Mai2Constants: "maimai DX Festival", ) + VERSION_STRING_OLD = ( + "maimai", + "maimai PLUS", + "maimai GreeN", + "maimai GreeN PLUS", + "maimai ORANGE", + "maimai ORANGE PLUS", + "maimai PiNK", + "maimai PiNK PLUS", + "maimai MURASAKi", + "maimai MURASAKi PLUS", + "maimai MiLK", + "maimai MiLK PLUS", + "maimai FiNALE", + ) + @classmethod def game_ver_to_string(cls, ver: int): + if ver >= 1000: + return cls.VERSION_STRING_OLD[ver / 1000] return cls.VERSION_STRING[ver] diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py new file mode 100644 index 0000000..9a9cae7 --- /dev/null +++ b/titles/mai2/dx.py @@ -0,0 +1,15 @@ +from typing import Any, List, Dict +from datetime import datetime, timedelta +import pytz +import json + +from core.config import CoreConfig +from titles.mai2.base import Mai2Base +from titles.mai2.config import Mai2Config +from titles.mai2.const import Mai2Constants + + +class Mai2DX(Mai2Base): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_DX diff --git a/titles/mai2/plus.py b/titles/mai2/dxplus.py similarity index 93% rename from titles/mai2/plus.py rename to titles/mai2/dxplus.py index a3c9288..64c9297 100644 --- a/titles/mai2/plus.py +++ b/titles/mai2/dxplus.py @@ -9,7 +9,7 @@ from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -class Mai2Plus(Mai2Base): +class Mai2DXPlus(Mai2Base): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_PLUS diff --git a/titles/mai2/finale.py b/titles/mai2/finale.py new file mode 100644 index 0000000..bb4b67e --- /dev/null +++ b/titles/mai2/finale.py @@ -0,0 +1,15 @@ +from typing import Any, List, Dict +from datetime import datetime, timedelta +import pytz +import json + +from core.config import CoreConfig +from titles.mai2.base import Mai2Base +from titles.mai2.config import Mai2Config +from titles.mai2.const import Mai2Constants + + +class Mai2Finale(Mai2Base): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_FINALE diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 1b92842..9eb3b52 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -14,7 +14,9 @@ from core.utils import Utils from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants from titles.mai2.base import Mai2Base -from titles.mai2.plus import Mai2Plus +from titles.mai2.finale import Mai2Finale +from titles.mai2.dx import Mai2DX +from titles.mai2.dxplus import Mai2DXPlus from titles.mai2.splash import Mai2Splash from titles.mai2.splashplus import Mai2SplashPlus from titles.mai2.universe import Mai2Universe @@ -32,8 +34,8 @@ class Mai2Servlet: ) self.versions = [ - Mai2Base, - Mai2Plus, + Mai2DX, + Mai2DXPlus, Mai2Splash, Mai2SplashPlus, Mai2Universe, @@ -41,6 +43,22 @@ class Mai2Servlet: Mai2Festival, ] + self.versions_old = [ + Mai2Base, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Mai2Finale, + ] + self.logger = logging.getLogger("mai2") log_fmt_str = "[%(asctime)s] Mai2 | %(levelname)s | %(message)s" log_fmt = logging.Formatter(log_fmt_str) From 8d94d25893b41db9c5f1f4293eba86225f651169 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 3 May 2023 03:25:29 -0400 Subject: [PATCH 02/40] mai2: add version seperators --- titles/mai2/index.py | 104 +++++++++++++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 34 deletions(-) diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 9eb3b52..60a618f 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -60,27 +60,29 @@ class Mai2Servlet: ] self.logger = logging.getLogger("mai2") - log_fmt_str = "[%(asctime)s] Mai2 | %(levelname)s | %(message)s" - log_fmt = logging.Formatter(log_fmt_str) - fileHandler = TimedRotatingFileHandler( - "{0}/{1}.log".format(self.core_cfg.server.log_dir, "mai2"), - encoding="utf8", - when="d", - backupCount=10, - ) + if not hasattr(self.logger, "initted"): + log_fmt_str = "[%(asctime)s] Mai2 | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "mai2"), + encoding="utf8", + when="d", + backupCount=10, + ) - fileHandler.setFormatter(log_fmt) + fileHandler.setFormatter(log_fmt) - consoleHandler = logging.StreamHandler() - consoleHandler.setFormatter(log_fmt) + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) - self.logger.addHandler(fileHandler) - self.logger.addHandler(consoleHandler) + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) - self.logger.setLevel(self.game_cfg.server.loglevel) - coloredlogs.install( - level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str - ) + self.logger.setLevel(self.game_cfg.server.loglevel) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + self.logger.initted = True @classmethod def get_allnet_info( @@ -100,13 +102,13 @@ class Mai2Servlet: return ( True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", - f"{core_cfg.title.hostname}:{core_cfg.title.port}/", + f"{core_cfg.title.hostname}", ) return ( True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", - f"{core_cfg.title.hostname}/", + f"{core_cfg.title.hostname}", ) def render_POST(self, request: Request, version: int, url_path: str) -> bytes: @@ -120,21 +122,50 @@ class Mai2Servlet: endpoint = url_split[len(url_split) - 1] client_ip = Utils.get_ip_addr(request) - if version < 105: # 1.0 - internal_ver = Mai2Constants.VER_MAIMAI_DX - elif version >= 105 and version < 110: # Plus - internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS - elif version >= 110 and version < 115: # Splash - internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH - elif version >= 115 and version < 120: # Splash Plus - internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS - elif version >= 120 and version < 125: # Universe - internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE - elif version >= 125 and version < 130: # Universe Plus - internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS - elif version >= 130: # Festival - internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL + if request.uri.startswith(b"/SDEY"): + if version < 105: # 1.0 + internal_ver = Mai2Constants.VER_MAIMAI_DX + elif version >= 105 and version < 110: # Plus + internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS + elif version >= 110 and version < 115: # Splash + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH + elif version >= 115 and version < 120: # Splash Plus + internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS + elif version >= 120 and version < 125: # Universe + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE + elif version >= 125 and version < 130: # Universe Plus + internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS + elif version >= 130: # Festival + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL + else: + if version < 110: # 1.0 + internal_ver = Mai2Constants.VER_MAIMAI + elif version >= 110 and version < 120: # Plus + internal_ver = Mai2Constants.VER_MAIMAI_PLUS + elif version >= 120 and version < 130: # Green + internal_ver = Mai2Constants.VER_MAIMAI_GREEN + elif version >= 130 and version < 140: # Plus + internal_ver = Mai2Constants.VER_MAIMAI_GREEN_PLUS + elif version >= 140 and version < 150: # Orange + internal_ver = Mai2Constants.VER_MAIMAI_ORANGE + elif version >= 150 and version < 160: # Plus + internal_ver = Mai2Constants.VER_MAIMAI_ORANGE_PLUS + elif version >= 160 and version < 170: # Pink + internal_ver = Mai2Constants.VER_MAIMAI_PINK + elif version >= 170 and version < 180: # Plus + internal_ver = Mai2Constants.VER_MAIMAI_PINK_PLUS + elif version >= 180 and version < 185: # Murasaki + internal_ver = Mai2Constants.VER_MAIMAI_MURASAKI + elif version >= 185 and version < 190: # Plus + internal_ver = Mai2Constants.VER_MAIMAI_MURASAKI_PLUS + elif version >= 190 and version < 195: # Milk + internal_ver = Mai2Constants.VER_MAIMAI_MILK + elif version >= 195 and version < 197: # Plus + internal_ver = Mai2Constants.VER_MAIMAI_MILK_PLUS + elif version >= 197: # Finale + internal_ver = Mai2Constants.VER_MAIMAI_FINALE + if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: # If we get a 32 character long hex string, it's a hash and we're # doing encrypted. The likelyhood of false positives is low but @@ -156,7 +187,12 @@ class Mai2Servlet: self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" - handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) + + if internal_ver >= Mai2Constants.VER_MAIMAI: + handler_cls = self.versions_old[internal_ver](self.core_cfg, self.game_cfg) + + else: + handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) if not hasattr(handler_cls, func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") From 7bb8c2c80c4ade483efc7cd02ce21d98d76dfc11 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 3 May 2023 03:25:55 -0400 Subject: [PATCH 03/40] billing: handle malformed requests --- core/allnet.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 119f0ae..ab435e7 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -249,14 +249,18 @@ class AllnetServlet: signer = PKCS1_v1_5.new(rsa) digest = SHA.new() - kc_playlimit = int(req_dict[0]["playlimit"]) - kc_nearfull = int(req_dict[0]["nearfull"]) - kc_billigtype = int(req_dict[0]["billingtype"]) - kc_playcount = int(req_dict[0]["playcnt"]) - kc_serial: str = req_dict[0]["keychipid"] - kc_game: str = req_dict[0]["gameid"] - kc_date = strptime(req_dict[0]["date"], "%Y%m%d%H%M%S") - kc_serial_bytes = kc_serial.encode() + try: + kc_playlimit = int(req_dict[0]["playlimit"]) + kc_nearfull = int(req_dict[0]["nearfull"]) + kc_billigtype = int(req_dict[0]["billingtype"]) + kc_playcount = int(req_dict[0]["playcnt"]) + kc_serial: str = req_dict[0]["keychipid"] + kc_game: str = req_dict[0]["gameid"] + kc_date = strptime(req_dict[0]["date"], "%Y%m%d%H%M%S") + kc_serial_bytes = kc_serial.encode() + + except KeyError as e: + return f"result=5&linelimit=&message={e} field is missing".encode() machine = self.data.arcade.get_machine(kc_serial) if machine is None and not self.config.server.allow_unregistered_serials: From e3b1addce62a1b6763547f379be500932b4ae83f Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 4 May 2023 20:12:31 -0400 Subject: [PATCH 04/40] mai2: fix up version comments --- titles/mai2/index.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 249b3af..4d6a177 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -145,23 +145,23 @@ class Mai2Servlet: internal_ver = Mai2Constants.VER_MAIMAI_PLUS elif version >= 120 and version < 130: # Green internal_ver = Mai2Constants.VER_MAIMAI_GREEN - elif version >= 130 and version < 140: # Plus + elif version >= 130 and version < 140: # Green Plus internal_ver = Mai2Constants.VER_MAIMAI_GREEN_PLUS elif version >= 140 and version < 150: # Orange internal_ver = Mai2Constants.VER_MAIMAI_ORANGE - elif version >= 150 and version < 160: # Plus + elif version >= 150 and version < 160: # Orange Plus internal_ver = Mai2Constants.VER_MAIMAI_ORANGE_PLUS elif version >= 160 and version < 170: # Pink internal_ver = Mai2Constants.VER_MAIMAI_PINK - elif version >= 170 and version < 180: # Plus + elif version >= 170 and version < 180: # Pink Plus internal_ver = Mai2Constants.VER_MAIMAI_PINK_PLUS elif version >= 180 and version < 185: # Murasaki internal_ver = Mai2Constants.VER_MAIMAI_MURASAKI - elif version >= 185 and version < 190: # Plus + elif version >= 185 and version < 190: # Murasaki Plus internal_ver = Mai2Constants.VER_MAIMAI_MURASAKI_PLUS elif version >= 190 and version < 195: # Milk internal_ver = Mai2Constants.VER_MAIMAI_MILK - elif version >= 195 and version < 197: # Plus + elif version >= 195 and version < 197: # Milk Plus internal_ver = Mai2Constants.VER_MAIMAI_MILK_PLUS elif version >= 197: # Finale internal_ver = Mai2Constants.VER_MAIMAI_FINALE From dcff8adbab364d325c18ed0afd9db6d3ac3c69ac Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 4 May 2023 20:22:41 -0400 Subject: [PATCH 05/40] mai2: update documentation --- docs/game_specific_info.md | 49 +++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index f1b334e..3260118 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -105,28 +105,48 @@ Config file is located in `config/cxb.yaml`. ### SDEZ -| Version ID | Version Name | -|------------|-------------------------| -| 0 | maimai DX | -| 1 | maimai DX PLUS | -| 2 | maimai DX Splash | -| 3 | maimai DX Splash PLUS | -| 4 | maimai DX Universe | -| 5 | maimai DX Universe PLUS | -| 6 | maimai DX Festival | +| Game Code | Version ID | Version Name | +|-----------|------------|-------------------------| +| SDEZ | 0 | maimai DX | +| SDEZ | 1 | maimai DX PLUS | +| SDEZ | 2 | maimai DX Splash | +| SDEZ | 3 | maimai DX Splash PLUS | +| SDEZ | 4 | maimai DX Universe | +| SDEZ | 5 | maimai DX Universe PLUS | +| SDEZ | 6 | maimai DX Festival | + +For versions pre-dx +| Game Code | Version ID | Version Name | +|-----------|------------|----------------------| +| SBXL | 1000 | maimai | +| SBXL | 1001 | maimai PLUS | +| SBZF | 1002 | maimai GreeN | +| SBZF | 1003 | maimai GreeN PLUS | +| SDBM | 1004 | maimai ORANGE | +| SDBM | 1005 | maimai ORANGE PLUS | +| SDCQ | 1006 | maimai PiNK | +| SDCQ | 1007 | maimai PiNK PLUS | +| SDDK | 1008 | maimai MURASAKI | +| SDDK | 1009 | maimai MURASAKI PLUS | +| SDDZ | 1010 | maimai MILK | +| SDDZ | 1011 | maimai MILK PLUS | +| SDEY | 1012 | maimai FiNALE | ### Importer In order to use the importer locate your game installation folder and execute: - +DX: ```shell -python read.py --series SDEZ --version --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder +python read.py --series --version --binfolder /path/to/StreamingAssets --optfolder /path/to/game/option/folder +``` +Pre-DX: +```shell +python read.py --series --version --binfolder /path/to/data --optfolder /path/to/patch/data ``` - The importer for maimai DX will import Events, Music and Tickets. +The importer for maimai Pre-DX will import Events and Music. Not all games will have patch data. -**NOTE: It is required to use the importer because the game will -crash without Events!** +**NOTE: It is required to use the importer because some games will not function properly or even crash without Events!** ### Database upgrade @@ -135,6 +155,7 @@ Always make sure your database (tables) are up-to-date, to do so go to the `core ```shell python dbutils.py --game SDEZ upgrade ``` +Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code! ## Hatsune Miku Project Diva From 989c08065749aa6cb01fc819f784ebd219164997 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 4 May 2023 20:25:14 -0400 Subject: [PATCH 06/40] mai2: further documentation clarification --- docs/game_specific_info.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 3260118..167dc8b 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -144,9 +144,10 @@ Pre-DX: python read.py --series --version --binfolder /path/to/data --optfolder /path/to/patch/data ``` The importer for maimai DX will import Events, Music and Tickets. -The importer for maimai Pre-DX will import Events and Music. Not all games will have patch data. -**NOTE: It is required to use the importer because some games will not function properly or even crash without Events!** +The importer for maimai Pre-DX will import Events and Music. Not all games will have patch data. Milk - Finale have file encryption, and need an AES key. That key is not provided by the developers. + +**Important: It is required to use the importer because some games may not function properly or even crash without Events!** ### Database upgrade From 8b9771b5af3cc2a80643be53f2e69ae201b2df80 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 5 May 2023 00:24:47 -0400 Subject: [PATCH 07/40] mai2: implement event reader for pre-dx games --- docs/game_specific_info.md | 2 +- titles/mai2/const.py | 2 +- titles/mai2/read.py | 157 ++++++++++++++++++++++++++++++++++--- 3 files changed, 149 insertions(+), 12 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 167dc8b..a2a916a 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -145,7 +145,7 @@ python read.py --series --version --binfolder /path/to/ ``` The importer for maimai DX will import Events, Music and Tickets. -The importer for maimai Pre-DX will import Events and Music. Not all games will have patch data. Milk - Finale have file encryption, and need an AES key. That key is not provided by the developers. +The importer for maimai Pre-DX will import Events and Music. Not all games will have patch data. Milk - Finale have file encryption, and need an AES key. That key is not provided by the developers. For games that do use encryption, provide the key, as a hex string, with the `--extra` flag. Ex `--extra 00112233445566778899AABBCCDDEEFF` **Important: It is required to use the importer because some games may not function properly or even crash without Events!** diff --git a/titles/mai2/const.py b/titles/mai2/const.py index 19cb867..6d638b9 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -82,5 +82,5 @@ class Mai2Constants: @classmethod def game_ver_to_string(cls, ver: int): if ver >= 1000: - return cls.VERSION_STRING_OLD[ver / 1000] + return cls.VERSION_STRING_OLD[ver - 1000] return cls.VERSION_STRING[ver] diff --git a/titles/mai2/read.py b/titles/mai2/read.py index 5809464..daa908f 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -4,6 +4,9 @@ import os import re import xml.etree.ElementTree as ET from typing import Any, Dict, List, Optional +from Crypto.Cipher import AES +import zlib +import codecs from core.config import CoreConfig from core.data import Data @@ -34,18 +37,139 @@ class Mai2Reader(BaseReader): def read(self) -> None: data_dirs = [] - if self.bin_dir is not None: - data_dirs += self.get_data_directories(self.bin_dir) + if self.version < Mai2Constants.VER_MAIMAI: + if self.bin_dir is not None: + data_dirs += self.get_data_directories(self.bin_dir) - if self.opt_dir is not None: - data_dirs += self.get_data_directories(self.opt_dir) + if self.opt_dir is not None: + data_dirs += self.get_data_directories(self.opt_dir) - for dir in data_dirs: - self.logger.info(f"Read from {dir}") - self.get_events(f"{dir}/event") - self.disable_events(f"{dir}/information", f"{dir}/scoreRanking") - self.read_music(f"{dir}/music") - self.read_tickets(f"{dir}/ticket") + for dir in data_dirs: + self.logger.info(f"Read from {dir}") + self.get_events(f"{dir}/event") + self.disable_events(f"{dir}/information", f"{dir}/scoreRanking") + self.read_music(f"{dir}/music") + self.read_tickets(f"{dir}/ticket") + + else: + self.logger.warn("Pre-DX Readers are not yet implemented!") + if not os.path.exists(f"{self.bin_dir}/tables"): + self.logger.error(f"tables directory not found in {self.bin_dir}") + return + + if self.version >= Mai2Constants.VER_MAIMAI_MILK: + if self.extra is None: + self.logger.error("Milk - Finale requre an AES key via a hex string send as the --extra flag") + return + + key = bytes.fromhex(self.extra) + + else: + key = None + + evt_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmEvent.bin", key) + txt_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmtextout_jp.bin", key) + score_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmScore.bin", key) + + self.read_old_events(evt_table) + + return + + def load_table_raw(self, dir: str, file: str, key: Optional[bytes]) -> Optional[List[Dict[str, str]]]: + if not os.path.exists(f"{dir}/{file}"): + self.logger.warn(f"file {file} does not exist in directory {dir}, skipping") + return + + self.logger.info(f"Load table {file} from {dir}") + if key is not None: + cipher = AES.new(key, AES.MODE_CBC) + with open(f"{dir}/{file}", "rb") as f: + f_encrypted = f.read() + f_data = cipher.decrypt(f_encrypted)[0x10:] + + else: + with open(f"{dir}/{file}", "rb") as f: + f_data = f.read()[0x10:] + + if f_data is None or not f_data: + self.logger.warn(f"file {dir} could not be read, skipping") + return + + f_data_deflate = zlib.decompress(f_data, wbits = zlib.MAX_WBITS | 16) + f_decoded = codecs.utf_16_le_decode(f_data_deflate)[0] + f_split = f_decoded.splitlines() + + has_struct_def = "struct " in f_decoded + is_struct = False + struct_def = [] + tbl_content = [] + + if has_struct_def: + for x in f_split: + if x.startswith("struct "): + is_struct = True + struct_name = x[7:-1] + continue + + if x.startswith("};"): + is_struct = False + break + + if is_struct: + try: + struct_def.append(x[x.rindex(" ") + 2: -1]) + except ValueError: + self.logger.warn(f"rindex failed on line {x}") + + if is_struct: + self.logger.warn("Struct not formatted properly") + + if not struct_def: + self.logger.warn("Struct def not found") + + name = file[:file.index(".")] + if "_" in name: + name = name[:file.index("_")] + + for x in f_split: + if not x.startswith(name.upper()): + continue + + line_match = re.match(r"(\w+)\((.*?)\)([ ]+\/{3}<[ ]+(.*))?", x) + if line_match is None: + continue + + if not line_match.group(1) == name.upper(): + self.logger.warn(f"Strange regex match for line {x} -> {line_match}") + continue + + vals = line_match.group(2) + comment = line_match.group(4) + line_dict = {} + + vals_split = vals.split(",") + for y in range(len(vals_split)): + stripped = vals_split[y].strip().lstrip("L\"").lstrip("\"").rstrip("\"") + if not stripped or stripped is None: + continue + + if has_struct_def and len(struct_def) > y: + line_dict[struct_def[y]] = stripped + + else: + line_dict[f'item_{y}'] = stripped + + if comment: + line_dict['comment'] = comment + + tbl_content.append(line_dict) + + if tbl_content: + return tbl_content + + else: + self.logger.warning("Failed load table content, skipping") + return def get_events(self, base_dir: str) -> None: self.logger.info(f"Reading events from {base_dir}...") @@ -188,3 +312,16 @@ class Mai2Reader(BaseReader): self.version, id, ticket_type, price, name ) self.logger.info(f"Added ticket {id}...") + + def read_old_events(self, events: List[Dict[str, str]]) -> None: + for event in events: + evt_id = int(event.get('イベントID', '0')) + evt_expire_time = float(event.get('オフ時強制時期', '0.0')) + is_exp = bool(int(event.get('海外許可', '0'))) + is_aou = bool(int(event.get('AOU許可', '0'))) + name = event.get('comment', f'evt_{evt_id}') + + self.data.static.put_game_event(self.version, 0, evt_id, name) + + if not (is_exp or is_aou): + self.data.static.toggle_game_event(self.version, evt_id, False) \ No newline at end of file From cad523dfceb4c2833d7a79361da5aa41b9f1f3f9 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 5 May 2023 00:36:07 -0400 Subject: [PATCH 08/40] mai2: add patch reader --- titles/mai2/read.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/titles/mai2/read.py b/titles/mai2/read.py index daa908f..f4b17cd 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -72,6 +72,13 @@ class Mai2Reader(BaseReader): score_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmScore.bin", key) self.read_old_events(evt_table) + + if self.opt_dir is not None: + evt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmEvent.bin", key) + txt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmtextout_jp.bin", key) + score_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmScore.bin", key) + + self.read_old_events(evt_table) return @@ -95,7 +102,7 @@ class Mai2Reader(BaseReader): self.logger.warn(f"file {dir} could not be read, skipping") return - f_data_deflate = zlib.decompress(f_data, wbits = zlib.MAX_WBITS | 16) + f_data_deflate = zlib.decompress(f_data, wbits = zlib.MAX_WBITS | 16)[0x12:] # lop off the junk at the beginning f_decoded = codecs.utf_16_le_decode(f_data_deflate)[0] f_split = f_decoded.splitlines() @@ -313,7 +320,10 @@ class Mai2Reader(BaseReader): ) self.logger.info(f"Added ticket {id}...") - def read_old_events(self, events: List[Dict[str, str]]) -> None: + def read_old_events(self, events: Optional[List[Dict[str, str]]]) -> None: + if events is None: + return + for event in events: evt_id = int(event.get('イベントID', '0')) evt_expire_time = float(event.get('オフ時強制時期', '0.0')) @@ -324,4 +334,9 @@ class Mai2Reader(BaseReader): self.data.static.put_game_event(self.version, 0, evt_id, name) if not (is_exp or is_aou): - self.data.static.toggle_game_event(self.version, evt_id, False) \ No newline at end of file + self.data.static.toggle_game_event(self.version, evt_id, False) + + def read_old_music(self, scores: Optional[List[Dict[str, str]]], text: Optional[List[Dict[str, str]]]) -> None: + if scores is None or text is None: + return + # TODO From 8149f09a408b79aa575ebaece6578b363831c95a Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 5 May 2023 00:37:05 -0400 Subject: [PATCH 09/40] mai2: stub music reader --- titles/mai2/read.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/titles/mai2/read.py b/titles/mai2/read.py index f4b17cd..a7e10de 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -72,6 +72,7 @@ class Mai2Reader(BaseReader): score_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmScore.bin", key) self.read_old_events(evt_table) + self.read_old_music(score_table, txt_table) if self.opt_dir is not None: evt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmEvent.bin", key) @@ -79,6 +80,7 @@ class Mai2Reader(BaseReader): score_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmScore.bin", key) self.read_old_events(evt_table) + self.read_old_music(score_table, txt_table) return From b34b441ba8b97cd9c3e49e0f404c846c3807ee78 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 6 May 2023 19:04:10 -0400 Subject: [PATCH 10/40] mai2: reimplement pre-dx versions --- core/data/schema/versions/SDEZ_4_rollback.sql | 26 + core/data/schema/versions/SDEZ_5_upgrade.sql | 17 + titles/mai2/base.py | 40 +- titles/mai2/const.py | 61 +- titles/mai2/dx.py | 725 ++++++++++++++++++ titles/mai2/dxplus.py | 4 +- titles/mai2/festival.py | 4 +- titles/mai2/index.py | 34 +- titles/mai2/splash.py | 4 +- titles/mai2/splashplus.py | 4 +- titles/mai2/universe.py | 177 +---- titles/mai2/universeplus.py | 4 +- 12 files changed, 840 insertions(+), 260 deletions(-) create mode 100644 core/data/schema/versions/SDEZ_4_rollback.sql create mode 100644 core/data/schema/versions/SDEZ_5_upgrade.sql diff --git a/core/data/schema/versions/SDEZ_4_rollback.sql b/core/data/schema/versions/SDEZ_4_rollback.sql new file mode 100644 index 0000000..290fa85 --- /dev/null +++ b/core/data/schema/versions/SDEZ_4_rollback.sql @@ -0,0 +1,26 @@ +DELETE FROM mai2_static_event WHERE version < 13; +UPDATE mai2_static_event SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_static_music WHERE version < 13; +UPDATE mai2_static_music SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_static_ticket WHERE version < 13; +UPDATE mai2_static_ticket SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_static_cards WHERE version < 13; +UPDATE mai2_static_cards SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_detail WHERE version < 13; +UPDATE mai2_profile_detail SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_extend WHERE version < 13; +UPDATE mai2_profile_extend SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_option WHERE version < 13; +UPDATE mai2_profile_option SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_ghost WHERE version < 13; +UPDATE mai2_profile_ghost SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_rating WHERE version < 13; +UPDATE mai2_profile_rating SET version = version - 13 WHERE version >= 13; diff --git a/core/data/schema/versions/SDEZ_5_upgrade.sql b/core/data/schema/versions/SDEZ_5_upgrade.sql new file mode 100644 index 0000000..18ba4ac --- /dev/null +++ b/core/data/schema/versions/SDEZ_5_upgrade.sql @@ -0,0 +1,17 @@ +UPDATE mai2_static_event SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_static_music SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_static_ticket SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_static_cards SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_detail SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_extend SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_option SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_ghost SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_rating SET version = version + 13 WHERE version < 1000; \ No newline at end of file diff --git a/titles/mai2/base.py b/titles/mai2/base.py index dcb3bcc..7cfdb93 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -17,18 +17,18 @@ class Mai2Base: self.logger = logging.getLogger("mai2") if self.core_config.server.is_develop and self.core_config.title.port > 0: - self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/100/" + self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/197/" else: - self.old_server = f"http://{self.core_config.title.hostname}/SDEY/100/" + self.old_server = f"http://{self.core_config.title.hostname}/SDEY/197/" def handle_get_game_setting_api_request(self, data: Dict): # TODO: See if making this epoch 0 breaks things reboot_start = date.strftime( - datetime.now() + timedelta(hours=3), Mai2Constants.DATE_TIME_FORMAT + datetime.fromtimestamp(0.0), Mai2Constants.DATE_TIME_FORMAT ) reboot_end = date.strftime( - datetime.now() + timedelta(hours=4), Mai2Constants.DATE_TIME_FORMAT + datetime.fromtimestamp(0.0) + timedelta(hours=1), Mai2Constants.DATE_TIME_FORMAT ) return { "gameSetting": { @@ -39,9 +39,9 @@ class Mai2Base: "movieUploadLimit": 10000, "movieStatus": 0, "movieServerUri": "", - "deliverServerUri": "", - "oldServerUri": self.old_server, - "usbDlServerUri": "", + "deliverServerUri": self.old_server + "deliver", + "oldServerUri": self.old_server + "old", + "usbDlServerUri": self.old_server + "usbdl", "rebootInterval": 0, }, "isAouAccession": "true", @@ -56,8 +56,9 @@ class Mai2Base: def handle_get_game_event_api_request(self, data: Dict) -> Dict: events = self.data.static.get_enabled_events(self.version) + print(self.version) events_lst = [] - if events is None: + if events is None or not events: self.logger.warn("No enabled events, did you run the reader?") return {"type": data["type"], "length": 0, "gameEventList": []} @@ -127,28 +128,20 @@ class Mai2Base: "userId": data["userId"], "userName": profile["userName"], "isLogin": False, - "lastGameId": profile["lastGameId"], "lastDataVersion": profile["lastDataVersion"], - "lastRomVersion": profile["lastRomVersion"], "lastLoginDate": profile["lastLoginDate"], "lastPlayDate": profile["lastPlayDate"], "playerRating": profile["playerRating"], - "nameplateId": 0, # Unused + "nameplateId": 0, # Unused + "frameId": profile["frameId"], "iconId": profile["iconId"], "trophyId": 0, # Unused "partnerId": profile["partnerId"], - "frameId": profile["frameId"], - "dispRate": option[ - "dispRate" - ], # 0: all/begin, 1: disprate, 2: dispDan, 3: hide, 4: end - "totalAwake": profile["totalAwake"], - "isNetMember": profile["isNetMember"], - "dailyBonusDate": profile["dailyBonusDate"], - "headPhoneVolume": option["headPhoneVolume"], - "isInherit": False, # Not sure what this is or does?? - "banState": profile["banState"] - if profile["banState"] is not None - else 0, # New with uni+ + "dispRate": option["dispRate"], # 0: all, 1: dispRate, 2: dispDan, 3: hide + "dispRank": 0, # TODO + "dispHomeRanker": 0, # TODO + "dispTotalLv": 0, # TODO + "totalLv": 0, # TODO } def handle_user_login_api_request(self, data: Dict) -> Dict: @@ -169,7 +162,6 @@ class Mai2Base: "lastLoginDate": lastLoginDate, "loginCount": loginCt, "consecutiveLoginCount": 0, # We don't really have a way to track this... - "loginId": loginCt, # Used with the playlog! } def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: diff --git a/titles/mai2/const.py b/titles/mai2/const.py index 6d638b9..d8f5941 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -31,39 +31,29 @@ class Mai2Constants: CONFIG_NAME = "mai2.yaml" - VER_MAIMAI = 1000 - VER_MAIMAI_PLUS = 1001 - VER_MAIMAI_GREEN = 1002 - VER_MAIMAI_GREEN_PLUS = 1003 - VER_MAIMAI_ORANGE = 1004 - VER_MAIMAI_ORANGE_PLUS = 1005 - VER_MAIMAI_PINK = 1006 - VER_MAIMAI_PINK_PLUS = 1007 - VER_MAIMAI_MURASAKI = 1008 - VER_MAIMAI_MURASAKI_PLUS = 1009 - VER_MAIMAI_MILK = 1010 - VER_MAIMAI_MILK_PLUS = 1011 - VER_MAIMAI_FINALE = 1012 + VER_MAIMAI = 0 + VER_MAIMAI_PLUS = 1 + VER_MAIMAI_GREEN = 2 + VER_MAIMAI_GREEN_PLUS = 3 + VER_MAIMAI_ORANGE = 4 + VER_MAIMAI_ORANGE_PLUS = 5 + VER_MAIMAI_PINK = 6 + VER_MAIMAI_PINK_PLUS = 7 + VER_MAIMAI_MURASAKI = 8 + VER_MAIMAI_MURASAKI_PLUS = 9 + VER_MAIMAI_MILK = 10 + VER_MAIMAI_MILK_PLUS = 11 + VER_MAIMAI_FINALE = 12 - VER_MAIMAI_DX = 0 - VER_MAIMAI_DX_PLUS = 1 - VER_MAIMAI_DX_SPLASH = 2 - VER_MAIMAI_DX_SPLASH_PLUS = 3 - VER_MAIMAI_DX_UNIVERSE = 4 - VER_MAIMAI_DX_UNIVERSE_PLUS = 5 - VER_MAIMAI_DX_FESTIVAL = 6 + VER_MAIMAI_DX = 13 + VER_MAIMAI_DX_PLUS = 14 + VER_MAIMAI_DX_SPLASH = 15 + VER_MAIMAI_DX_SPLASH_PLUS = 16 + VER_MAIMAI_DX_UNIVERSE = 17 + VER_MAIMAI_DX_UNIVERSE_PLUS = 18 + VER_MAIMAI_DX_FESTIVAL = 19 VERSION_STRING = ( - "maimai DX", - "maimai DX PLUS", - "maimai DX Splash", - "maimai DX Splash PLUS", - "maimai DX Universe", - "maimai DX Universe PLUS", - "maimai DX Festival", - ) - - VERSION_STRING_OLD = ( "maimai", "maimai PLUS", "maimai GreeN", @@ -76,11 +66,16 @@ class Mai2Constants: "maimai MURASAKi PLUS", "maimai MiLK", "maimai MiLK PLUS", - "maimai FiNALE", + "maimai FiNALE", + "maimai DX", + "maimai DX PLUS", + "maimai DX Splash", + "maimai DX Splash PLUS", + "maimai DX Universe", + "maimai DX Universe PLUS", + "maimai DX Festival", ) @classmethod def game_ver_to_string(cls, ver: int): - if ver >= 1000: - return cls.VERSION_STRING_OLD[ver - 1000] return cls.VERSION_STRING[ver] diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 9a9cae7..9d07d49 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -2,6 +2,7 @@ from typing import Any, List, Dict from datetime import datetime, timedelta import pytz import json +from random import randint from core.config import CoreConfig from titles.mai2.base import Mai2Base @@ -13,3 +14,727 @@ class Mai2DX(Mai2Base): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX + + def handle_get_user_preview_api_request(self, data: Dict) -> Dict: + p = self.data.profile.get_profile_detail(data["userId"], self.version) + o = self.data.profile.get_profile_option(data["userId"], self.version) + if p is None or o is None: + return {} # Register + profile = p._asdict() + option = o._asdict() + + return { + "userId": data["userId"], + "userName": profile["userName"], + "isLogin": False, + "lastGameId": profile["lastGameId"], + "lastDataVersion": profile["lastDataVersion"], + "lastRomVersion": profile["lastRomVersion"], + "lastLoginDate": profile["lastLoginDate"], + "lastPlayDate": profile["lastPlayDate"], + "playerRating": profile["playerRating"], + "nameplateId": 0, # Unused + "iconId": profile["iconId"], + "trophyId": 0, # Unused + "partnerId": profile["partnerId"], + "frameId": profile["frameId"], + "dispRate": option[ + "dispRate" + ], # 0: all/begin, 1: disprate, 2: dispDan, 3: hide, 4: end + "totalAwake": profile["totalAwake"], + "isNetMember": profile["isNetMember"], + "dailyBonusDate": profile["dailyBonusDate"], + "headPhoneVolume": option["headPhoneVolume"], + "isInherit": False, # Not sure what this is or does?? + "banState": profile["banState"] + if profile["banState"] is not None + else 0, # New with uni+ + } + + def handle_user_login_api_request(self, data: Dict) -> Dict: + profile = self.data.profile.get_profile_detail(data["userId"], self.version) + + if profile is not None: + lastLoginDate = profile["lastLoginDate"] + loginCt = profile["playCount"] + + if "regionId" in data: + self.data.profile.put_profile_region(data["userId"], data["regionId"]) + else: + loginCt = 0 + lastLoginDate = "2017-12-05 07:00:00.0" + + return { + "returnCode": 1, + "lastLoginDate": lastLoginDate, + "loginCount": loginCt, + "consecutiveLoginCount": 0, # We don't really have a way to track this... + "loginId": loginCt, # Used with the playlog! + } + + def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + playlog = data["userPlaylog"] + + self.data.score.put_playlog(user_id, playlog) + + return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} + + def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + charge = data["userCharge"] + + # remove the ".0" from the date string, festival only? + charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") + self.data.item.put_charge( + user_id, + charge["chargeId"], + charge["stock"], + datetime.strptime(charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT), + datetime.strptime(charge["validDate"], Mai2Constants.DATE_TIME_FORMAT), + ) + + return {"returnCode": 1, "apiName": "UpsertUserChargelogApi"} + + def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + upsert = data["upsertUserAll"] + + if "userData" in upsert and len(upsert["userData"]) > 0: + upsert["userData"][0]["isNetMember"] = 1 + upsert["userData"][0].pop("accessCode") + self.data.profile.put_profile_detail( + user_id, self.version, upsert["userData"][0] + ) + + if "userExtend" in upsert and len(upsert["userExtend"]) > 0: + self.data.profile.put_profile_extend( + user_id, self.version, upsert["userExtend"][0] + ) + + if "userGhost" in upsert: + for ghost in upsert["userGhost"]: + self.data.profile.put_profile_extend(user_id, self.version, ghost) + + if "userOption" in upsert and len(upsert["userOption"]) > 0: + self.data.profile.put_profile_option( + user_id, self.version, upsert["userOption"][0] + ) + + if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0: + self.data.profile.put_profile_rating( + user_id, self.version, upsert["userRatingList"][0] + ) + + if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0: + for k, v in upsert["userActivityList"][0].items(): + for act in v: + self.data.profile.put_profile_activity(user_id, act) + + if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0: + for charge in upsert["userChargeList"]: + # remove the ".0" from the date string, festival only? + charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") + self.data.item.put_charge( + user_id, + charge["chargeId"], + charge["stock"], + datetime.strptime( + charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT + ), + datetime.strptime( + charge["validDate"], Mai2Constants.DATE_TIME_FORMAT + ), + ) + + if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0: + for char in upsert["userCharacterList"]: + self.data.item.put_character( + user_id, + char["characterId"], + char["level"], + char["awakening"], + char["useCount"], + ) + + if "userItemList" in upsert and len(upsert["userItemList"]) > 0: + for item in upsert["userItemList"]: + self.data.item.put_item( + user_id, + int(item["itemKind"]), + item["itemId"], + item["stock"], + item["isValid"], + ) + + if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: + for login_bonus in upsert["userLoginBonusList"]: + self.data.item.put_login_bonus( + user_id, + login_bonus["bonusId"], + login_bonus["point"], + login_bonus["isCurrent"], + login_bonus["isComplete"], + ) + + if "userMapList" in upsert and len(upsert["userMapList"]) > 0: + for map in upsert["userMapList"]: + self.data.item.put_map( + user_id, + map["mapId"], + map["distance"], + map["isLock"], + map["isClear"], + map["isComplete"], + ) + + if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0: + for music in upsert["userMusicDetailList"]: + self.data.score.put_best_score(user_id, music) + + if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0: + for course in upsert["userCourseList"]: + self.data.score.put_course(user_id, course) + + if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0: + for fav in upsert["userFavoriteList"]: + self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) + + if ( + "userFriendSeasonRankingList" in upsert + and len(upsert["userFriendSeasonRankingList"]) > 0 + ): + for fsr in upsert["userFriendSeasonRankingList"]: + fsr["recordDate"] = ( + datetime.strptime( + fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ), + ) + self.data.item.put_friend_season_ranking(user_id, fsr) + + return {"returnCode": 1, "apiName": "UpsertUserAllApi"} + + def handle_user_logout_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + def handle_get_user_data_api_request(self, data: Dict) -> Dict: + profile = self.data.profile.get_profile_detail(data["userId"], self.version) + if profile is None: + return + + profile_dict = profile._asdict() + profile_dict.pop("id") + profile_dict.pop("user") + profile_dict.pop("version") + + return {"userId": data["userId"], "userData": profile_dict} + + def handle_get_user_extend_api_request(self, data: Dict) -> Dict: + extend = self.data.profile.get_profile_extend(data["userId"], self.version) + if extend is None: + return + + extend_dict = extend._asdict() + extend_dict.pop("id") + extend_dict.pop("user") + extend_dict.pop("version") + + return {"userId": data["userId"], "userExtend": extend_dict} + + def handle_get_user_option_api_request(self, data: Dict) -> Dict: + options = self.data.profile.get_profile_option(data["userId"], self.version) + if options is None: + return + + options_dict = options._asdict() + options_dict.pop("id") + options_dict.pop("user") + options_dict.pop("version") + + return {"userId": data["userId"], "userOption": options_dict} + + def handle_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = self.data.item.get_cards(data["userId"]) + if user_cards is None: + return {"userId": data["userId"], "nextIndex": 0, "userCardList": []} + + max_ct = data["maxCount"] + next_idx = data["nextIndex"] + start_idx = next_idx + end_idx = max_ct + start_idx + + if len(user_cards[start_idx:]) > max_ct: + next_idx += max_ct + else: + next_idx = 0 + + card_list = [] + for card in user_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("user") + tmp["startDate"] = datetime.strftime( + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["endDate"] = datetime.strftime( + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) + card_list.append(tmp) + + return { + "userId": data["userId"], + "nextIndex": next_idx, + "userCardList": card_list[start_idx:end_idx], + } + + def handle_get_user_charge_api_request(self, data: Dict) -> Dict: + user_charges = self.data.item.get_charges(data["userId"]) + if user_charges is None: + return {"userId": data["userId"], "length": 0, "userChargeList": []} + + user_charge_list = [] + for charge in user_charges: + tmp = charge._asdict() + tmp.pop("id") + tmp.pop("user") + tmp["purchaseDate"] = datetime.strftime( + tmp["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["validDate"] = datetime.strftime( + tmp["validDate"], Mai2Constants.DATE_TIME_FORMAT + ) + + user_charge_list.append(tmp) + + return { + "userId": data["userId"], + "length": len(user_charge_list), + "userChargeList": user_charge_list, + } + + def handle_get_user_item_api_request(self, data: Dict) -> Dict: + kind = int(data["nextIndex"] / 10000000000) + next_idx = int(data["nextIndex"] % 10000000000) + user_item_list = self.data.item.get_items(data["userId"], kind) + + items: list[Dict[str, Any]] = [] + for i in range(next_idx, len(user_item_list)): + tmp = user_item_list[i]._asdict() + tmp.pop("user") + tmp.pop("id") + items.append(tmp) + if len(items) >= int(data["maxCount"]): + break + + xout = kind * 10000000000 + next_idx + len(items) + + if len(items) < int(data["maxCount"]): + next_idx = 0 + else: + next_idx = xout + + return { + "userId": data["userId"], + "nextIndex": next_idx, + "itemKind": kind, + "userItemList": items, + } + + def handle_get_user_character_api_request(self, data: Dict) -> Dict: + characters = self.data.item.get_characters(data["userId"]) + + chara_list = [] + for chara in characters: + tmp = chara._asdict() + tmp.pop("id") + tmp.pop("user") + chara_list.append(tmp) + + return {"userId": data["userId"], "userCharacterList": chara_list} + + def handle_get_user_favorite_api_request(self, data: Dict) -> Dict: + favorites = self.data.item.get_favorites(data["userId"], data["itemKind"]) + if favorites is None: + return + + userFavs = [] + for fav in favorites: + userFavs.append( + { + "userId": data["userId"], + "itemKind": fav["itemKind"], + "itemIdList": fav["itemIdList"], + } + ) + + return {"userId": data["userId"], "userFavoriteData": userFavs} + + def handle_get_user_ghost_api_request(self, data: Dict) -> Dict: + ghost = self.data.profile.get_profile_ghost(data["userId"], self.version) + if ghost is None: + return + + ghost_dict = ghost._asdict() + ghost_dict.pop("user") + ghost_dict.pop("id") + ghost_dict.pop("version_int") + + return {"userId": data["userId"], "userGhost": ghost_dict} + + def handle_get_user_rating_api_request(self, data: Dict) -> Dict: + rating = self.data.profile.get_profile_rating(data["userId"], self.version) + if rating is None: + return + + rating_dict = rating._asdict() + rating_dict.pop("user") + rating_dict.pop("id") + rating_dict.pop("version") + + return {"userId": data["userId"], "userRating": rating_dict} + + def handle_get_user_activity_api_request(self, data: Dict) -> Dict: + """ + kind 1 is playlist, kind 2 is music list + """ + playlist = self.data.profile.get_profile_activity(data["userId"], 1) + musiclist = self.data.profile.get_profile_activity(data["userId"], 2) + if playlist is None or musiclist is None: + return + + plst = [] + mlst = [] + + for play in playlist: + tmp = play._asdict() + tmp["id"] = tmp["activityId"] + tmp.pop("activityId") + tmp.pop("user") + plst.append(tmp) + + for music in musiclist: + tmp = music._asdict() + tmp["id"] = tmp["activityId"] + tmp.pop("activityId") + tmp.pop("user") + mlst.append(tmp) + + return {"userActivity": {"playList": plst, "musicList": mlst}} + + def handle_get_user_course_api_request(self, data: Dict) -> Dict: + user_courses = self.data.score.get_courses(data["userId"]) + if user_courses is None: + return {"userId": data["userId"], "nextIndex": 0, "userCourseList": []} + + course_list = [] + for course in user_courses: + tmp = course._asdict() + tmp.pop("user") + tmp.pop("id") + course_list.append(tmp) + + return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list} + + def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: + # No support for custom pfps + return {"length": 0, "userPortraitList": []} + + def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict: + friend_season_ranking = self.data.item.get_friend_season_ranking(data["userId"]) + if friend_season_ranking is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userFriendSeasonRankingList": [], + } + + friend_season_ranking_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(friend_season_ranking)): + tmp = friend_season_ranking[x]._asdict() + tmp.pop("user") + tmp.pop("id") + tmp["recordDate"] = datetime.strftime( + tmp["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ) + friend_season_ranking_list.append(tmp) + + if len(friend_season_ranking_list) >= max_ct: + break + + if len(friend_season_ranking) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 + + return { + "userId": data["userId"], + "nextIndex": next_idx, + "userFriendSeasonRankingList": friend_season_ranking_list, + } + + def handle_get_user_map_api_request(self, data: Dict) -> Dict: + maps = self.data.item.get_maps(data["userId"]) + if maps is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userMapList": [], + } + + map_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(maps)): + tmp = maps[x]._asdict() + tmp.pop("user") + tmp.pop("id") + map_list.append(tmp) + + if len(map_list) >= max_ct: + break + + if len(maps) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 + + return { + "userId": data["userId"], + "nextIndex": next_idx, + "userMapList": map_list, + } + + def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: + login_bonuses = self.data.item.get_login_bonuses(data["userId"]) + if login_bonuses is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userLoginBonusList": [], + } + + login_bonus_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(login_bonuses)): + tmp = login_bonuses[x]._asdict() + tmp.pop("user") + tmp.pop("id") + login_bonus_list.append(tmp) + + if len(login_bonus_list) >= max_ct: + break + + if len(login_bonuses) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 + + return { + "userId": data["userId"], + "nextIndex": next_idx, + "userLoginBonusList": login_bonus_list, + } + + def handle_get_user_region_api_request(self, data: Dict) -> Dict: + return {"userId": data["userId"], "length": 0, "userRegionList": []} + + def handle_get_user_music_api_request(self, data: Dict) -> Dict: + songs = self.data.score.get_best_scores(data["userId"]) + music_detail_list = [] + next_index = 0 + + if songs is not None: + for song in songs: + tmp = song._asdict() + tmp.pop("id") + tmp.pop("user") + music_detail_list.append(tmp) + + if len(music_detail_list) == data["maxCount"]: + next_index = data["maxCount"] + data["nextIndex"] + break + + return { + "userId": data["userId"], + "nextIndex": next_index, + "userMusicList": [{"userMusicDetailList": music_detail_list}], + } + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + p = self.data.profile.get_profile_detail(data["userId"], self.version) + if p is None: + return {} + + return { + "userName": p["userName"], + "rating": p["playerRating"], + # hardcode lastDataVersion for CardMaker 1.34 + "lastDataVersion": "1.20.00", + "isLogin": False, + "isExistSellingCard": False, + } + + def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + # user already exists, because the preview checks that already + p = self.data.profile.get_profile_detail(data["userId"], self.version) + + cards = self.data.card.get_user_cards(data["userId"]) + if cards is None or len(cards) == 0: + # This should never happen + self.logger.error( + f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}" + ) + return {} + + # get the dict representation of the row so we can modify values + user_data = p._asdict() + + # remove the values the game doesn't want + user_data.pop("id") + user_data.pop("user") + user_data.pop("version") + + return {"userId": data["userId"], "userData": user_data} + + def handle_cm_login_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + def handle_cm_logout_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: + selling_cards = self.data.static.get_enabled_cards(self.version) + if selling_cards is None: + return {"length": 0, "sellingCardList": []} + + selling_card_list = [] + for card in selling_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("version") + tmp.pop("cardName") + tmp.pop("enabled") + + tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S") + tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S") + tmp["noticeStartDate"] = datetime.strftime( + tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S" + ) + tmp["noticeEndDate"] = datetime.strftime( + tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S" + ) + + selling_card_list.append(tmp) + + return {"length": len(selling_card_list), "sellingCardList": selling_card_list} + + def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = self.data.item.get_cards(data["userId"]) + if user_cards is None: + return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []} + + max_ct = data["maxCount"] + next_idx = data["nextIndex"] + start_idx = next_idx + end_idx = max_ct + start_idx + + if len(user_cards[start_idx:]) > max_ct: + next_idx += max_ct + else: + next_idx = 0 + + card_list = [] + for card in user_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("user") + + tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S") + tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S") + card_list.append(tmp) + + return { + "returnCode": 1, + "length": len(card_list[start_idx:end_idx]), + "nextIndex": next_idx, + "userCardList": card_list[start_idx:end_idx], + } + + def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + super().handle_get_user_item_api_request(data) + + def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + characters = self.data.item.get_characters(data["userId"]) + + chara_list = [] + for chara in characters: + chara_list.append( + { + "characterId": chara["characterId"], + # no clue why those values are even needed + "point": 0, + "count": 0, + "level": chara["level"], + "nextAwake": 0, + "nextAwakePercent": 0, + "favorite": False, + "awakening": chara["awakening"], + "useCount": chara["useCount"], + } + ) + + return { + "returnCode": 1, + "length": len(chara_list), + "userCharacterList": chara_list, + } + + def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: + return {"length": 0, "userPrintDetailList": []} + + def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + upsert = data["userPrintDetail"] + + # set a random card serial number + serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) + + user_card = upsert["userCard"] + self.data.item.put_card( + user_id, + user_card["cardId"], + user_card["cardTypeId"], + user_card["charaId"], + user_card["mapId"], + ) + + # properly format userPrintDetail for the database + upsert.pop("userCard") + upsert.pop("serialId") + upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d") + + self.data.item.put_user_print_detail(user_id, serial_id, upsert) + + return { + "returnCode": 1, + "orderId": 0, + "serialId": serial_id, + "startDate": "2018-01-01 00:00:00", + "endDate": "2038-01-01 00:00:00", + } + + def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + return { + "returnCode": 1, + "orderId": 0, + "serialId": data["userPrintlog"]["serialId"], + } + + def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} diff --git a/titles/mai2/dxplus.py b/titles/mai2/dxplus.py index 64c9297..9062ff5 100644 --- a/titles/mai2/dxplus.py +++ b/titles/mai2/dxplus.py @@ -4,12 +4,12 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.base import Mai2Base +from titles.mai2.dx import Mai2DX from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -class Mai2DXPlus(Mai2Base): +class Mai2DXPlus(Mai2DX): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_PLUS diff --git a/titles/mai2/festival.py b/titles/mai2/festival.py index 4e51619..85b3df2 100644 --- a/titles/mai2/festival.py +++ b/titles/mai2/festival.py @@ -1,12 +1,12 @@ from typing import Dict from core.config import CoreConfig -from titles.mai2.universeplus import Mai2UniversePlus +from titles.mai2.dx import Mai2DX from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config -class Mai2Festival(Mai2UniversePlus): +class Mai2Festival(Mai2DX): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 4d6a177..e6818de 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -34,16 +34,6 @@ class Mai2Servlet: ) self.versions = [ - Mai2DX, - Mai2DXPlus, - Mai2Splash, - Mai2SplashPlus, - Mai2Universe, - Mai2UniversePlus, - Mai2Festival, - ] - - self.versions_old = [ Mai2Base, None, None, @@ -56,7 +46,14 @@ class Mai2Servlet: None, None, None, - Mai2Finale, + Mai2Finale, + Mai2DX, + Mai2DXPlus, + Mai2Splash, + Mai2SplashPlus, + Mai2Universe, + Mai2UniversePlus, + Mai2Festival, ] self.logger = logging.getLogger("mai2") @@ -122,7 +119,7 @@ class Mai2Servlet: endpoint = url_split[len(url_split) - 1] client_ip = Utils.get_ip_addr(request) - if request.uri.startswith(b"/SDEY"): + if request.uri.startswith(b"/SDEZ"): if version < 105: # 1.0 internal_ver = Mai2Constants.VER_MAIMAI_DX elif version >= 105 and version < 110: # Plus @@ -187,12 +184,7 @@ class Mai2Servlet: self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" - - if internal_ver >= Mai2Constants.VER_MAIMAI: - handler_cls = self.versions_old[internal_ver](self.core_cfg, self.game_cfg) - - else: - handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) + handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) if not hasattr(handler_cls, func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") @@ -213,3 +205,9 @@ class Mai2Servlet: self.logger.debug(f"Response {resp}") return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) + + def render_GET(self, request: Request, version: int, url_path: str) -> bytes: + if url_path.endswith("ping"): + return zlib.compress(b"ok") + else: + return zlib.compress(b"{}") \ No newline at end of file diff --git a/titles/mai2/splash.py b/titles/mai2/splash.py index ad31695..0c9f827 100644 --- a/titles/mai2/splash.py +++ b/titles/mai2/splash.py @@ -4,12 +4,12 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.base import Mai2Base +from titles.mai2.dx import Mai2DX from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -class Mai2Splash(Mai2Base): +class Mai2Splash(Mai2DX): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH diff --git a/titles/mai2/splashplus.py b/titles/mai2/splashplus.py index 54431c9..e26b267 100644 --- a/titles/mai2/splashplus.py +++ b/titles/mai2/splashplus.py @@ -4,12 +4,12 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.base import Mai2Base +from titles.mai2.dx import Mai2DX from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants -class Mai2SplashPlus(Mai2Base): +class Mai2SplashPlus(Mai2DX): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS diff --git a/titles/mai2/universe.py b/titles/mai2/universe.py index 56b3e8f..adcb205 100644 --- a/titles/mai2/universe.py +++ b/titles/mai2/universe.py @@ -5,185 +5,12 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.base import Mai2Base +from titles.mai2.dx import Mai2DX from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config -class Mai2Universe(Mai2Base): +class Mai2Universe(Mai2DX): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE - - def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_detail(data["userId"], self.version) - if p is None: - return {} - - return { - "userName": p["userName"], - "rating": p["playerRating"], - # hardcode lastDataVersion for CardMaker 1.34 - "lastDataVersion": "1.20.00", - "isLogin": False, - "isExistSellingCard": False, - } - - def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: - # user already exists, because the preview checks that already - p = self.data.profile.get_profile_detail(data["userId"], self.version) - - cards = self.data.card.get_user_cards(data["userId"]) - if cards is None or len(cards) == 0: - # This should never happen - self.logger.error( - f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}" - ) - return {} - - # get the dict representation of the row so we can modify values - user_data = p._asdict() - - # remove the values the game doesn't want - user_data.pop("id") - user_data.pop("user") - user_data.pop("version") - - return {"userId": data["userId"], "userData": user_data} - - def handle_cm_login_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} - - def handle_cm_logout_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} - - def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: - selling_cards = self.data.static.get_enabled_cards(self.version) - if selling_cards is None: - return {"length": 0, "sellingCardList": []} - - selling_card_list = [] - for card in selling_cards: - tmp = card._asdict() - tmp.pop("id") - tmp.pop("version") - tmp.pop("cardName") - tmp.pop("enabled") - - tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S") - tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S") - tmp["noticeStartDate"] = datetime.strftime( - tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S" - ) - tmp["noticeEndDate"] = datetime.strftime( - tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S" - ) - - selling_card_list.append(tmp) - - return {"length": len(selling_card_list), "sellingCardList": selling_card_list} - - def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: - user_cards = self.data.item.get_cards(data["userId"]) - if user_cards is None: - return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []} - - max_ct = data["maxCount"] - next_idx = data["nextIndex"] - start_idx = next_idx - end_idx = max_ct + start_idx - - if len(user_cards[start_idx:]) > max_ct: - next_idx += max_ct - else: - next_idx = 0 - - card_list = [] - for card in user_cards: - tmp = card._asdict() - tmp.pop("id") - tmp.pop("user") - - tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S") - tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S") - card_list.append(tmp) - - return { - "returnCode": 1, - "length": len(card_list[start_idx:end_idx]), - "nextIndex": next_idx, - "userCardList": card_list[start_idx:end_idx], - } - - def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: - super().handle_get_user_item_api_request(data) - - def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: - characters = self.data.item.get_characters(data["userId"]) - - chara_list = [] - for chara in characters: - chara_list.append( - { - "characterId": chara["characterId"], - # no clue why those values are even needed - "point": 0, - "count": 0, - "level": chara["level"], - "nextAwake": 0, - "nextAwakePercent": 0, - "favorite": False, - "awakening": chara["awakening"], - "useCount": chara["useCount"], - } - ) - - return { - "returnCode": 1, - "length": len(chara_list), - "userCharacterList": chara_list, - } - - def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: - return {"length": 0, "userPrintDetailList": []} - - def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: - user_id = data["userId"] - upsert = data["userPrintDetail"] - - # set a random card serial number - serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) - - user_card = upsert["userCard"] - self.data.item.put_card( - user_id, - user_card["cardId"], - user_card["cardTypeId"], - user_card["charaId"], - user_card["mapId"], - ) - - # properly format userPrintDetail for the database - upsert.pop("userCard") - upsert.pop("serialId") - upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d") - - self.data.item.put_user_print_detail(user_id, serial_id, upsert) - - return { - "returnCode": 1, - "orderId": 0, - "serialId": serial_id, - "startDate": "2018-01-01 00:00:00", - "endDate": "2038-01-01 00:00:00", - } - - def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: - return { - "returnCode": 1, - "orderId": 0, - "serialId": data["userPrintlog"]["serialId"], - } - - def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} diff --git a/titles/mai2/universeplus.py b/titles/mai2/universeplus.py index e45c719..e9f03f4 100644 --- a/titles/mai2/universeplus.py +++ b/titles/mai2/universeplus.py @@ -1,12 +1,12 @@ from typing import Dict from core.config import CoreConfig -from titles.mai2.universe import Mai2Universe +from titles.mai2.dx import Mai2DX from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config -class Mai2UniversePlus(Mai2Universe): +class Mai2UniversePlus(Mai2DX): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS From 9766e3ab78cec0e2c930d747a116cee7fab813a8 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 7 May 2023 02:16:50 -0400 Subject: [PATCH 11/40] mai2: hardcode reboot time --- titles/mai2/base.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 7cfdb93..46143ec 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -23,19 +23,12 @@ class Mai2Base: self.old_server = f"http://{self.core_config.title.hostname}/SDEY/197/" def handle_get_game_setting_api_request(self, data: Dict): - # TODO: See if making this epoch 0 breaks things - reboot_start = date.strftime( - datetime.fromtimestamp(0.0), Mai2Constants.DATE_TIME_FORMAT - ) - reboot_end = date.strftime( - datetime.fromtimestamp(0.0) + timedelta(hours=1), Mai2Constants.DATE_TIME_FORMAT - ) return { "gameSetting": { "isMaintenance": "false", "requestInterval": 10, - "rebootStartTime": reboot_start, - "rebootEndTime": reboot_end, + "rebootStartTime": "2020-01-01 07:00:00.0", + "rebootEndTime": "2020-01-01 07:59:59.0", "movieUploadLimit": 10000, "movieStatus": 0, "movieServerUri": "", From d172e5582b3c88d8b526704443c0c091172eedc8 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 9 May 2023 03:53:31 -0400 Subject: [PATCH 12/40] fixup allnet response for res class 2 --- core/allnet.py | 17 +++++++++-------- titles/mai2/index.py | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index ab435e7..31065d2 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -194,7 +194,7 @@ class AllnetServlet: self.logger.info( f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}" ) - resp = AllnetDownloadOrderResponse() + resp = AllnetDownloadOrderResponse(serial=req.serial) if ( not self.config.allnet.allow_online_updates @@ -430,6 +430,7 @@ class AllnetPowerOnResponse3: class AllnetPowerOnResponse2: def __init__(self) -> None: + time = datetime.now(tz=pytz.timezone("Asia/Tokyo")) self.stat = 1 self.uri = "" self.host = "" @@ -442,14 +443,14 @@ class AllnetPowerOnResponse2: self.region_name2 = "Y" self.region_name3 = "Z" self.country = "JPN" - self.year = datetime.now().year - self.month = datetime.now().month - self.day = datetime.now().day - self.hour = datetime.now().hour - self.minute = datetime.now().minute - self.second = datetime.now().second + self.year = time.year + self.month = time.month + self.day = time.day + self.hour = time.hour + self.minute = time.minute + self.second = time.second self.setting = "1" - self.timezone = "+0900" + self.timezone = "+09:00" self.res_class = "PowerOnResponseV2" diff --git a/titles/mai2/index.py b/titles/mai2/index.py index e6818de..be73c36 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -99,7 +99,7 @@ class Mai2Servlet: return ( True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", - f"{core_cfg.title.hostname}:{core_cfg.title.port}", + f"{core_cfg.title.hostname}", ) return ( From 42ed222095e7087f0ae65de9a6f98c9892372629 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Wed, 10 May 2023 02:31:30 -0400 Subject: [PATCH 13/40] mai2: add gamesetting urls --- titles/mai2/base.py | 8 +++++--- titles/mai2/config.py | 29 +++++++++++++++++++++++++++++ titles/mai2/dx.py | 6 ++++++ titles/mai2/finale.py | 8 ++++++++ titles/mai2/index.py | 30 +++++++++++++++++++++++++++--- 5 files changed, 75 insertions(+), 6 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 46143ec..ff2a0c5 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -15,6 +15,9 @@ class Mai2Base: self.version = Mai2Constants.VER_MAIMAI self.data = Mai2Data(cfg) self.logger = logging.getLogger("mai2") + self.can_deliver = False + self.can_usbdl = False + self.old_server = "" if self.core_config.server.is_develop and self.core_config.title.port > 0: self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/197/" @@ -32,9 +35,9 @@ class Mai2Base: "movieUploadLimit": 10000, "movieStatus": 0, "movieServerUri": "", - "deliverServerUri": self.old_server + "deliver", + "deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "", "oldServerUri": self.old_server + "old", - "usbDlServerUri": self.old_server + "usbdl", + "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", "rebootInterval": 0, }, "isAouAccession": "true", @@ -49,7 +52,6 @@ class Mai2Base: def handle_get_game_event_api_request(self, data: Dict) -> Dict: events = self.data.static.get_enabled_events(self.version) - print(self.version) events_lst = [] if events is None or not events: self.logger.warn("No enabled events, did you run the reader?") diff --git a/titles/mai2/config.py b/titles/mai2/config.py index 3a20065..91cdd87 100644 --- a/titles/mai2/config.py +++ b/titles/mai2/config.py @@ -19,7 +19,36 @@ class Mai2ServerConfig: ) ) +class Mai2DeliverConfig: + def __init__(self, parent: "Mai2Config") -> None: + self.__config = parent + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "deliver", "enable", default=False + ) + + @property + def udbdl_enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "deliver", "udbdl_enable", default=False + ) + + @property + def list_folder(self) -> int: + return CoreConfig.get_config_field( + self.__config, "mai2", "server", "list_folder", default="" + ) + + @property + def list_folder(self) -> int: + return CoreConfig.get_config_field( + self.__config, "mai2", "server", "content_folder", default="" + ) + class Mai2Config(dict): def __init__(self) -> None: self.server = Mai2ServerConfig(self) + self.deliver = Mai2DeliverConfig(self) diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 9d07d49..0ada84b 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -14,6 +14,12 @@ class Mai2DX(Mai2Base): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX + + if self.core_config.server.is_develop and self.core_config.title.port > 0: + self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEZ/100/" + + else: + self.old_server = f"http://{self.core_config.title.hostname}/SDEZ/100/" def handle_get_user_preview_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_detail(data["userId"], self.version) diff --git a/titles/mai2/finale.py b/titles/mai2/finale.py index bb4b67e..e29196f 100644 --- a/titles/mai2/finale.py +++ b/titles/mai2/finale.py @@ -13,3 +13,11 @@ class Mai2Finale(Mai2Base): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_FINALE + self.can_deliver = True + self.can_usbdl = True + + if self.core_config.server.is_develop and self.core_config.title.port > 0: + self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/197/" + + else: + self.old_server = f"http://{self.core_config.title.hostname}/SDEY/197/" diff --git a/titles/mai2/index.py b/titles/mai2/index.py index be73c36..0bb8b44 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -207,7 +207,31 @@ class Mai2Servlet: return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) def render_GET(self, request: Request, version: int, url_path: str) -> bytes: - if url_path.endswith("ping"): - return zlib.compress(b"ok") + self.logger.info(f"v{version} GET {url_path}") + url_split = url_path.split("/") + + if url_split[0] == "old": + if url_split[1] == "ping": + self.logger.info(f"v{version} old server ping") + return zlib.compress(b"ok") + + elif url_split[1].startswith("userdata"): + self.logger.info(f"v{version} old server userdata inquire") + return zlib.compress(b"{}") + + elif url_split[1].startswith("friend"): + self.logger.info(f"v{version} old server friend inquire") + return zlib.compress(b"{}") + + elif url_split[0] == "usbdl": + if url_split[1] == "CONNECTIONTEST": + self.logger.info(f"v{version} usbdl server test") + return zlib.compress(b"ok") + + elif url_split[0] == "deliver": + if url_split[len(url_split) - 1] == "maimai_deliver.list": + self.logger.info(f"v{version} maimai_deliver.list inquire") + return zlib.compress(b"") + else: - return zlib.compress(b"{}") \ No newline at end of file + return zlib.compress(b"{}") From 49166c1a7b8cbfd01e7d96dc61e28c771f8b5dd8 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Thu, 11 May 2023 09:52:18 -0400 Subject: [PATCH 14/40] mai2: fix handle_get_game_setting_api_request --- titles/mai2/base.py | 16 ++++++++-------- titles/mai2/dx.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index ff2a0c5..dd82fd7 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -26,21 +26,21 @@ class Mai2Base: self.old_server = f"http://{self.core_config.title.hostname}/SDEY/197/" def handle_get_game_setting_api_request(self, data: Dict): - return { + return { + "isDevelop": False, + "isAouAccession": False, "gameSetting": { - "isMaintenance": "false", - "requestInterval": 10, + "isMaintenance": False, + "requestInterval": 1800, "rebootStartTime": "2020-01-01 07:00:00.0", "rebootEndTime": "2020-01-01 07:59:59.0", - "movieUploadLimit": 10000, - "movieStatus": 0, - "movieServerUri": "", + "movieUploadLimit": 100, + "movieStatus": 1, + "movieServerUri": self.old_server + "movie/", "deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "", "oldServerUri": self.old_server + "old", "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", - "rebootInterval": 0, }, - "isAouAccession": "true", } def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 0ada84b..9ac7067 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -20,6 +20,24 @@ class Mai2DX(Mai2Base): else: self.old_server = f"http://{self.core_config.title.hostname}/SDEZ/100/" + + def handle_get_game_setting_api_request(self, data: Dict): + return { + "gameSetting": { + "isMaintenance": False, + "requestInterval": 1800, + "rebootStartTime": "2020-01-01 07:00:00.0", + "rebootEndTime": "2020-01-01 07:59:59.0", + "movieUploadLimit": 100, + "movieStatus": 1, + "movieServerUri": self.old_server + "movie/", + "deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "", + "oldServerUri": self.old_server + "old", + "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", + "rebootInterval": 0, + }, + "isAouAccession": False, + } def handle_get_user_preview_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_detail(data["userId"], self.version) From 8ae0aba89cc80baed3e07e444104118d51fb2a28 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 12 May 2023 22:05:05 -0400 Subject: [PATCH 15/40] mai2: update default config --- example_config/mai2.yaml | 5 +++++ titles/mai2/config.py | 8 +------- titles/mai2/index.py | 7 +++++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/example_config/mai2.yaml b/example_config/mai2.yaml index a04dda5..d89f5d7 100644 --- a/example_config/mai2.yaml +++ b/example_config/mai2.yaml @@ -1,3 +1,8 @@ server: enable: True loglevel: "info" + +deliver: + enable: False + udbdl_enable: False + content_folder: "" \ No newline at end of file diff --git a/titles/mai2/config.py b/titles/mai2/config.py index 91cdd87..d5ed41f 100644 --- a/titles/mai2/config.py +++ b/titles/mai2/config.py @@ -36,13 +36,7 @@ class Mai2DeliverConfig: ) @property - def list_folder(self) -> int: - return CoreConfig.get_config_field( - self.__config, "mai2", "server", "list_folder", default="" - ) - - @property - def list_folder(self) -> int: + def content_folder(self) -> int: return CoreConfig.get_config_field( self.__config, "mai2", "server", "content_folder", default="" ) diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 0bb8b44..c4d6874 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -1,4 +1,5 @@ from twisted.web.http import Request +from twisted.web.server import NOT_DONE_YET import json import inflection import yaml @@ -229,8 +230,10 @@ class Mai2Servlet: return zlib.compress(b"ok") elif url_split[0] == "deliver": - if url_split[len(url_split) - 1] == "maimai_deliver.list": - self.logger.info(f"v{version} maimai_deliver.list inquire") + file = url_split[len(url_split) - 1] + self.logger.info(f"v{version} {file} deliver inquire") + + if not self.game_cfg.deliver.enable or not path.exists(f"{self.game_cfg.deliver.content_folder}/{file}"): return zlib.compress(b"") else: From 61e3a2c9306963eef9b18a831fe3bdc6d8b43631 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 12 May 2023 22:06:19 -0400 Subject: [PATCH 16/40] index: remove hanging debug log call --- index.py | 1 - 1 file changed, 1 deletion(-) diff --git a/index.py b/index.py index 7199dbe..6992ebb 100644 --- a/index.py +++ b/index.py @@ -111,7 +111,6 @@ class HttpDispatcher(resource.Resource): ) def render_GET(self, request: Request) -> bytes: - self.logger.debug(request.uri) test = self.map_get.match(request.uri.decode()) client_ip = Utils.get_ip_addr(request) From 02078080a841d981eb30169e0a2fc8694b4169a8 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 12 May 2023 22:12:03 -0400 Subject: [PATCH 17/40] index: additional logging for malformed return data --- index.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/index.py b/index.py index 6992ebb..c75486d 100644 --- a/index.py +++ b/index.py @@ -160,9 +160,16 @@ class HttpDispatcher(resource.Resource): if type(ret) == str: return ret.encode() - elif type(ret) == bytes: + + elif type(ret) == bytes or type(ret) == tuple: # allow for bytes or tuple (data, response code) responses return ret + + elif ret is None: + self.logger.warn(f"None returned by controller for {request.uri.decode()} endpoint") + return b"" + else: + self.logger.warn(f"Unknown data type returned by controller for {request.uri.decode()} endpoint") return b"" From 0c6d9a36cefa644ad00923f5d281f24f0ea312f8 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 25 Jun 2023 18:43:00 -0400 Subject: [PATCH 18/40] mai2: add movie server endpoints --- titles/mai2/base.py | 2 +- titles/mai2/index.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 5a8b0c6..e77b2e1 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -36,7 +36,7 @@ class Mai2Base: "rebootEndTime": "2020-01-01 07:59:59.0", "movieUploadLimit": 100, "movieStatus": 1, - "movieServerUri": self.old_server + "movie/", + "movieServerUri": self.old_server + "movie", "deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "", "oldServerUri": self.old_server + "old", "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", diff --git a/titles/mai2/index.py b/titles/mai2/index.py index c4d6874..1d9b8bf 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -112,6 +112,10 @@ class Mai2Servlet: def render_POST(self, request: Request, version: int, url_path: str) -> bytes: if url_path.lower() == "ping": return zlib.compress(b'{"returnCode": "1"}') + + elif url_path.startswith("movie/"): + self.logger.info(f"Movie data: {url_path} - {request.content.getvalue()}") + return b"" req_raw = request.content.getvalue() url = request.uri.decode() @@ -211,6 +215,10 @@ class Mai2Servlet: self.logger.info(f"v{version} GET {url_path}") url_split = url_path.split("/") + if url_split[0] == "movie": + if url_split[1] == "moviestart": + return json.dumps({"moviestart":{"status":"OK"}}).encode() + if url_split[0] == "old": if url_split[1] == "ping": self.logger.info(f"v{version} old server ping") From e3d38dacde7d7d6e82e625496803ee4e860e5f9c Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 25 Jun 2023 19:10:34 -0400 Subject: [PATCH 19/40] mai2: fix movies --- titles/mai2/base.py | 2 +- titles/mai2/config.py | 29 +++++++++++++++++++++++++++++ titles/mai2/index.py | 6 +++--- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index e77b2e1..ef15abe 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -36,7 +36,7 @@ class Mai2Base: "rebootEndTime": "2020-01-01 07:59:59.0", "movieUploadLimit": 100, "movieStatus": 1, - "movieServerUri": self.old_server + "movie", + "movieServerUri": self.old_server + "api/movie" if self.game_config.uploads.movies else "movie", "deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "", "oldServerUri": self.old_server + "old", "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "", diff --git a/titles/mai2/config.py b/titles/mai2/config.py index d5ed41f..d63c0b2 100644 --- a/titles/mai2/config.py +++ b/titles/mai2/config.py @@ -41,8 +41,37 @@ class Mai2DeliverConfig: self.__config, "mai2", "server", "content_folder", default="" ) +class Mai2UploadsConfig: + def __init__(self, parent: "Mai2Config") -> None: + self.__config = parent + + @property + def photos(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "uploads", "photos", default=False + ) + + @property + def photos_dir(self) -> str: + return CoreConfig.get_config_field( + self.__config, "mai2", "uploads", "photos_dir", default="" + ) + + @property + def movies(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "mai2", "uploads", "movies", default=False + ) + + @property + def movies_dir(self) -> str: + return CoreConfig.get_config_field( + self.__config, "mai2", "uploads", "movies_dir", default="" + ) + class Mai2Config(dict): def __init__(self) -> None: self.server = Mai2ServerConfig(self) self.deliver = Mai2DeliverConfig(self) + self.uploads = Mai2UploadsConfig(self) diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 1d9b8bf..c250894 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -113,7 +113,7 @@ class Mai2Servlet: if url_path.lower() == "ping": return zlib.compress(b'{"returnCode": "1"}') - elif url_path.startswith("movie/"): + elif url_path.startswith("api/movie/"): self.logger.info(f"Movie data: {url_path} - {request.content.getvalue()}") return b"" @@ -215,8 +215,8 @@ class Mai2Servlet: self.logger.info(f"v{version} GET {url_path}") url_split = url_path.split("/") - if url_split[0] == "movie": - if url_split[1] == "moviestart": + if (url_split[0] == "api" and url_split[1] == "movie") or url_split[0] == "movie": + if url_split[2] == "moviestart": return json.dumps({"moviestart":{"status":"OK"}}).encode() if url_split[0] == "old": From 127e6f8aa8632f9cae05dee1de9559a7da36c5c4 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 27 Jun 2023 00:32:35 -0400 Subject: [PATCH 20/40] mai2: add finale databases --- core/data/schema/versions/SDEZ_5_rollback.sql | 78 +++++++++ core/data/schema/versions/SDEZ_6_upgrade.sql | 62 +++++++ titles/mai2/__init__.py | 2 +- titles/mai2/schema/item.py | 79 ++++----- titles/mai2/schema/profile.py | 155 ++++++++++++++++++ titles/mai2/schema/score.py | 96 +++++++++++ 6 files changed, 432 insertions(+), 40 deletions(-) create mode 100644 core/data/schema/versions/SDEZ_5_rollback.sql create mode 100644 core/data/schema/versions/SDEZ_6_upgrade.sql diff --git a/core/data/schema/versions/SDEZ_5_rollback.sql b/core/data/schema/versions/SDEZ_5_rollback.sql new file mode 100644 index 0000000..1507faa --- /dev/null +++ b/core/data/schema/versions/SDEZ_5_rollback.sql @@ -0,0 +1,78 @@ +DELETE FROM mai2_static_event WHERE version < 13; +UPDATE mai2_static_event SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_static_music WHERE version < 13; +UPDATE mai2_static_music SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_static_ticket WHERE version < 13; +UPDATE mai2_static_ticket SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_static_cards WHERE version < 13; +UPDATE mai2_static_cards SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_detail WHERE version < 13; +UPDATE mai2_profile_detail SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_extend WHERE version < 13; +UPDATE mai2_profile_extend SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_option WHERE version < 13; +UPDATE mai2_profile_option SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_ghost WHERE version < 13; +UPDATE mai2_profile_ghost SET version = version - 13 WHERE version >= 13; + +DELETE FROM mai2_profile_rating WHERE version < 13; +UPDATE mai2_profile_rating SET version = version - 13 WHERE version >= 13; + +DROP TABLE maimai_score_best; +DROP TABLE maimai_playlog; +DROP TABLE maimai_profile_detail; +DROP TABLE maimai_profile_option; +DROP TABLE maimai_profile_web_option; +DROP TABLE maimai_profile_grade_status; + +ALTER TABLE mai2_item_character DROP COLUMN point; + +ALTER TABLE mai2_item_card MODIFY COLUMN cardId int(11) NOT NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN cardTypeId int(11) NOT NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN charaId int(11) NOT NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN mapId int(11) NOT NULL; + +ALTER TABLE mai2_item_character MODIFY COLUMN characterId int(11) NOT NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN level int(11) NOT NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN awakening int(11) NOT NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN useCount int(11) NOT NULL; + +ALTER TABLE mai2_item_charge MODIFY COLUMN chargeId int(11) NOT NULL; +ALTER TABLE mai2_item_charge MODIFY COLUMN stock int(11) NOT NULL; + +ALTER TABLE mai2_item_favorite MODIFY COLUMN itemKind int(11) NOT NULL; + +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN seasonId int(11) NOT NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN point int(11) NOT NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rank int(11) NOT NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rewardGet tinyint(1) NOT NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN userName varchar(8) NOT NULL; + +ALTER TABLE mai2_item_item MODIFY COLUMN itemId int(11) NOT NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN itemKind int(11) NOT NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN stock int(11) NOT NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN isValid tinyint(1) NOT NULL; + +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN bonusId int(11) NOT NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN point int(11) NOT NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isCurrent tinyint(1) NOT NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isComplete tinyint(1) NOT NULL; + +ALTER TABLE mai2_item_map MODIFY COLUMN mapId int(11) NOT NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN distance int(11) NOT NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isLock tinyint(1) NOT NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isClear tinyint(1) NOT NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isComplete tinyint(1) NOT NULL; + +ALTER TABLE mai2_item_print_detail MODIFY COLUMN printDate timestamp DEFAULT current_timestamp() NOT NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN serialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN placeId int(11) NOT NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN clientId varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN printerSerialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_6_upgrade.sql b/core/data/schema/versions/SDEZ_6_upgrade.sql new file mode 100644 index 0000000..06b2d45 --- /dev/null +++ b/core/data/schema/versions/SDEZ_6_upgrade.sql @@ -0,0 +1,62 @@ +UPDATE mai2_static_event SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_static_music SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_static_ticket SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_static_cards SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_detail SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_extend SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_option SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_ghost SET version = version + 13 WHERE version < 1000; + +UPDATE mai2_profile_rating SET version = version + 13 WHERE version < 1000; + +ALTER TABLE mai2_item_character ADD point int(11) NULL; + +ALTER TABLE mai2_item_card MODIFY COLUMN cardId int(11) NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN cardTypeId int(11) NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN charaId int(11) NULL; +ALTER TABLE mai2_item_card MODIFY COLUMN mapId int(11) NULL; + +ALTER TABLE mai2_item_character MODIFY COLUMN characterId int(11) NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN level int(11) NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN awakening int(11) NULL; +ALTER TABLE mai2_item_character MODIFY COLUMN useCount int(11) NULL; + +ALTER TABLE mai2_item_charge MODIFY COLUMN chargeId int(11) NULL; +ALTER TABLE mai2_item_charge MODIFY COLUMN stock int(11) NULL; + +ALTER TABLE mai2_item_favorite MODIFY COLUMN itemKind int(11) NULL; + +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN seasonId int(11) NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN point int(11) NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rank int(11) NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rewardGet tinyint(1) NULL; +ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN userName varchar(8) NULL; + +ALTER TABLE mai2_item_item MODIFY COLUMN itemId int(11) NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN itemKind int(11) NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN stock int(11) NULL; +ALTER TABLE mai2_item_item MODIFY COLUMN isValid tinyint(1) NULL; + +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN bonusId int(11) NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN point int(11) NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isCurrent tinyint(1) NULL; +ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isComplete tinyint(1) NULL; + +ALTER TABLE mai2_item_map MODIFY COLUMN mapId int(11) NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN distance int(11) NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isLock tinyint(1) NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isClear tinyint(1) NULL; +ALTER TABLE mai2_item_map MODIFY COLUMN isComplete tinyint(1) NULL; + +ALTER TABLE mai2_item_print_detail MODIFY COLUMN printDate timestamp DEFAULT current_timestamp() NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN serialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN placeId int(11) NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN clientId varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL; +ALTER TABLE mai2_item_print_detail MODIFY COLUMN printerSerialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL; \ No newline at end of file diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index dc8fc84..7063ee6 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -16,4 +16,4 @@ game_codes = [ Mai2Constants.GAME_CODE_GREEN, Mai2Constants.GAME_CODE, ] -current_schema_version = 5 +current_schema_version = 6 diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 6b70ed1..4e20383 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -18,10 +18,11 @@ character = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("characterId", Integer, nullable=False), - Column("level", Integer, nullable=False, server_default="1"), - Column("awakening", Integer, nullable=False, server_default="0"), - Column("useCount", Integer, nullable=False, server_default="0"), + Column("characterId", Integer), + Column("level", Integer), + Column("awakening", Integer), + Column("useCount", Integer), + Column("point", Integer), UniqueConstraint("user", "characterId", name="mai2_item_character_uk"), mysql_charset="utf8mb4", ) @@ -35,12 +36,12 @@ card = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("cardId", Integer, nullable=False), - Column("cardTypeId", Integer, nullable=False), - Column("charaId", Integer, nullable=False), - Column("mapId", Integer, nullable=False), - Column("startDate", TIMESTAMP, nullable=False, server_default=func.now()), - Column("endDate", TIMESTAMP, nullable=False), + Column("cardId", Integer), + Column("cardTypeId", Integer), + Column("charaId", Integer), + Column("mapId", Integer), + Column("startDate", TIMESTAMP, server_default=func.now()), + Column("endDate", TIMESTAMP), UniqueConstraint("user", "cardId", "cardTypeId", name="mai2_item_card_uk"), mysql_charset="utf8mb4", ) @@ -54,10 +55,10 @@ item = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("itemId", Integer, nullable=False), - Column("itemKind", Integer, nullable=False), - Column("stock", Integer, nullable=False, server_default="1"), - Column("isValid", Boolean, nullable=False, server_default="1"), + Column("itemId", Integer), + Column("itemKind", Integer), + Column("stock", Integer), + Column("isValid", Boolean), UniqueConstraint("user", "itemId", "itemKind", name="mai2_item_item_uk"), mysql_charset="utf8mb4", ) @@ -71,11 +72,11 @@ map = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("mapId", Integer, nullable=False), - Column("distance", Integer, nullable=False), - Column("isLock", Boolean, nullable=False, server_default="0"), - Column("isClear", Boolean, nullable=False, server_default="0"), - Column("isComplete", Boolean, nullable=False, server_default="0"), + Column("mapId", Integer), + Column("distance", Integer), + Column("isLock", Boolean), + Column("isClear", Boolean), + Column("isComplete", Boolean), UniqueConstraint("user", "mapId", name="mai2_item_map_uk"), mysql_charset="utf8mb4", ) @@ -89,10 +90,10 @@ login_bonus = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("bonusId", Integer, nullable=False), - Column("point", Integer, nullable=False), - Column("isCurrent", Boolean, nullable=False, server_default="0"), - Column("isComplete", Boolean, nullable=False, server_default="0"), + Column("bonusId", Integer), + Column("point", Integer), + Column("isCurrent", Boolean), + Column("isComplete", Boolean), UniqueConstraint("user", "bonusId", name="mai2_item_login_bonus_uk"), mysql_charset="utf8mb4", ) @@ -106,12 +107,12 @@ friend_season_ranking = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("seasonId", Integer, nullable=False), - Column("point", Integer, nullable=False), - Column("rank", Integer, nullable=False), - Column("rewardGet", Boolean, nullable=False), - Column("userName", String(8), nullable=False), - Column("recordDate", TIMESTAMP, nullable=False), + Column("seasonId", Integer), + Column("point", Integer), + Column("rank", Integer), + Column("rewardGet", Boolean), + Column("userName", String(8)), + Column("recordDate", TIMESTAMP), UniqueConstraint( "user", "seasonId", "userName", name="mai2_item_friend_season_ranking_uk" ), @@ -127,7 +128,7 @@ favorite = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("itemKind", Integer, nullable=False), + Column("itemKind", Integer), Column("itemIdList", JSON), UniqueConstraint("user", "itemKind", name="mai2_item_favorite_uk"), mysql_charset="utf8mb4", @@ -142,10 +143,10 @@ charge = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("chargeId", Integer, nullable=False), - Column("stock", Integer, nullable=False), - Column("purchaseDate", String(255), nullable=False), - Column("validDate", String(255), nullable=False), + Column("chargeId", Integer), + Column("stock", Integer), + Column("purchaseDate", String(255)), + Column("validDate", String(255)), UniqueConstraint("user", "chargeId", name="mai2_item_charge_uk"), mysql_charset="utf8mb4", ) @@ -161,11 +162,11 @@ print_detail = Table( ), Column("orderId", Integer), Column("printNumber", Integer), - Column("printDate", TIMESTAMP, nullable=False, server_default=func.now()), - Column("serialId", String(20), nullable=False), - Column("placeId", Integer, nullable=False), - Column("clientId", String(11), nullable=False), - Column("printerSerialId", String(20), nullable=False), + Column("printDate", TIMESTAMP, server_default=func.now()), + Column("serialId", String(20)), + Column("placeId", Integer), + Column("clientId", String(11)), + Column("printerSerialId", String(20)), Column("cardRomVersion", Integer), Column("isHolograph", Boolean, server_default="1"), Column("printOption1", Boolean, server_default="0"), diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 3cb42d1..eb0da73 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -99,6 +99,68 @@ detail = Table( mysql_charset="utf8mb4", ) +detail_old = Table( + "maimai_profile_detail", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("lastDataVersion", Integer), + Column("userName", String(8)), + Column("point", Integer), + Column("totalPoint", Integer), + Column("iconId", Integer), + Column("nameplateId", Integer), + Column("frameId", Integer), + Column("trophyId", Integer), + Column("playCount", Integer), + Column("playVsCount", Integer), + Column("playSyncCount", Integer), + Column("winCount", Integer), + Column("helpCount", Integer), + Column("comboCount", Integer), + Column("feverCount", Integer), + Column("totalHiScore", Integer), + Column("totalEasyHighScore", Integer), + Column("totalBasicHighScore", Integer), + Column("totalAdvancedHighScore", Integer), + Column("totalExpertHighScore", Integer), + Column("totalMasterHighScore", Integer), + Column("totalReMasterHighScore", Integer), + Column("totalHighSync", Integer), + Column("totalEasySync", Integer), + Column("totalBasicSync", Integer), + Column("totalAdvancedSync", Integer), + Column("totalExpertSync", Integer), + Column("totalMasterSync", Integer), + Column("totalReMasterSync", Integer), + Column("playerRating", Integer), + Column("highestRating", Integer), + Column("rankAuthTailId", Integer), + Column("eventWatchedDate", String(255)), + Column("webLimitDate", String(255)), + Column("challengeTrackPhase", Integer), + Column("firstPlayBits", Integer), + Column("lastPlayDate", String(255)), + Column("lastPlaceId", Integer), + Column("lastPlaceName", String(255)), + Column("lastRegionId", Integer), + Column("lastRegionName", String(255)), + Column("lastClientId", String(255)), + Column("lastCountryCode", String(255)), + Column("eventPoint", Integer), + Column("totalLv", Integer), + Column("lastLoginBonusDay", Integer), + Column("lastSurvivalBonusDay", Integer), + Column("loginBonusLv", Integer), + UniqueConstraint("user", "version", name="maimai_profile_detail_uk"), + mysql_charset="utf8mb4", +) + ghost = Table( "mai2_profile_ghost", metadata, @@ -223,6 +285,99 @@ option = Table( mysql_charset="utf8mb4", ) +option_old = Table( + "maimai_profile_option", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("soudEffect", Integer), + Column("mirrorMode", Integer), + Column("guideSpeed", Integer), + Column("bgInfo", Integer), + Column("brightness", Integer), + Column("isStarRot", Integer), + Column("breakSe", Integer), + Column("slideSe", Integer), + Column("hardJudge", Integer), + Column("isTagJump", Integer), + Column("breakSeVol", Integer), + Column("slideSeVol", Integer), + Column("isUpperDisp", Integer), + Column("trackSkip", Integer), + Column("optionMode", Integer), + Column("simpleOptionParam", Integer), + Column("adjustTiming", Integer), + Column("dispTiming", Integer), + Column("timingPos", Integer), + Column("ansVol", Integer), + Column("noteVol", Integer), + Column("dmgVol", Integer), + Column("appealFlame", Integer), + Column("isFeverDisp", Integer), + Column("dispJudge", Integer), + Column("judgePos", Integer), + Column("ratingGuard", Integer), + Column("selectChara", Integer), + Column("sortType", Integer), + Column("filterGenre", Integer), + Column("filterLevel", Integer), + Column("filterRank", Integer), + Column("filterVersion", Integer), + Column("filterRec", Integer), + Column("filterFullCombo", Integer), + Column("filterAllPerfect", Integer), + Column("filterDifficulty", Integer), + Column("filterFullSync", Integer), + Column("filterReMaster", Integer), + Column("filterMaxFever", Integer), + Column("finalSelectId", Integer), + Column("finalSelectCategory", Integer), + UniqueConstraint("user", "version", name="maimai_profile_option_uk"), + mysql_charset="utf8mb4", +) + +web_opt = Table( + "maimai_profile_web_option", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("isNetMember", Boolean), + Column("dispRate", Integer), + Column("dispJudgeStyle", Integer), + Column("dispRank", Integer), + Column("dispHomeRanker", Integer), + Column("dispTotalLv", Integer), + UniqueConstraint("user", "version", name="maimai_profile_web_option_uk"), + mysql_charset="utf8mb4", +) + +grade_status = Table( + "maimai_profile_grade_status", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("gradeVersion", Integer), + Column("gradeLevel", Integer), + Column("gradeSubLevel", Integer), + Column("gradeMaxId", Integer), + UniqueConstraint("user", "gradeVersion", name="maimai_profile_grade_status_uk"), + mysql_charset="utf8mb4", +) + rating = Table( "mai2_profile_rating", metadata, diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 85dff16..b754eb6 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -175,6 +175,102 @@ course = Table( mysql_charset="utf8mb4", ) +playlog_old = Table( + "maimai_playlog", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer), + # Pop access code + Column("orderId", Integer), + Column("sortNumber", Integer), + Column("placeId", Integer), + Column("placeName", String(255)), + Column("country", String(255)), + Column("regionId", Integer), + Column("playDate", String(255)), + Column("userPlayDate", String(255)), + Column("musicId", Integer), + Column("level", Integer), + Column("gameMode", Integer), + Column("rivalNum", Integer), + Column("track", Integer), + Column("eventId", Integer), + Column("isFreeToPlay", Boolean), + Column("playerRating", Integer), + Column("playedUserId1", Integer), + Column("playedUserId2", Integer), + Column("playedUserId3", Integer), + Column("playedUserName1", String(255)), + Column("playedUserName2", String(255)), + Column("playedUserName3", String(255)), + Column("playedMusicLevel1", Integer), + Column("playedMusicLevel2", Integer), + Column("playedMusicLevel3", Integer), + Column("achievement", Integer), + Column("score", Integer), + Column("tapScore", Integer), + Column("holdScore", Integer), + Column("slideScore", Integer), + Column("breakScore", Integer), + Column("syncRate", Integer), + Column("vsWin", Integer), + Column("isAllPerfect", Boolean), + Column("fullCombo", Integer), + Column("maxFever", Integer), + Column("maxCombo", Integer), + Column("tapPerfect", Integer), + Column("tapGreat", Integer), + Column("tapGood", Integer), + Column("tapBad", Integer), + Column("holdPerfect", Integer), + Column("holdGreat", Integer), + Column("holdGood", Integer), + Column("holdBad", Integer), + Column("slidePerfect", Integer), + Column("slideGreat", Integer), + Column("slideGood", Integer), + Column("slideBad", Integer), + Column("breakPerfect", Integer), + Column("breakGreat", Integer), + Column("breakGood", Integer), + Column("breakBad", Integer), + Column("judgeStyle", Integer), + Column("isTrackSkip", Boolean), + Column("isHighScore", Boolean), + Column("isChallengeTrack", Boolean), + Column("challengeLife", Integer), + Column("challengeRemain", Integer), + Column("isAllPerfectPlus", Integer), + mysql_charset="utf8mb4", +) + +best_score_old = Table( + "maimai_score_best", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("musicId", Integer), + Column("level", Integer), + Column("playCount", Integer), + Column("achievement", Integer), + Column("scoreMax", Integer), + Column("syncRateMax", Integer), + Column("isAllPerfect", Boolean), + Column("isAllPerfectPlus", Integer), + Column("fullCombo", Integer), + Column("maxFever", Integer), + UniqueConstraint("user", "musicId", "level", name="maimai_score_best_uk"), + mysql_charset="utf8mb4", +) class Mai2ScoreData(BaseData): def put_best_score(self, user_id: int, score_data: Dict) -> Optional[int]: From 4ea83f60256a21c5c42b690f80fd27b496b10a67 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 30 Jun 2023 00:26:07 -0400 Subject: [PATCH 21/40] allnet: add handler for LoaderStateRecorder --- core/allnet.py | 18 +++++++++++++++++- index.py | 7 +++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/core/allnet.py b/core/allnet.py index 8af120b..00303f5 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Any, Optional, Tuple +from typing import Dict, List, Any, Optional, Tuple, Union import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler from twisted.web.http import Request @@ -241,6 +241,22 @@ class AllnetServlet: ) return b"" + def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes: + req_data = request.content.getvalue() + req_dict = self.kvp_to_dict([req_data.decode()])[0] + + serial: Union[str, None] = req_dict.get("serial", None) + num_files_to_dl: Union[str, None] = req_dict.get("nb_ftd", None) + num_files_dld: Union[str, None] = req_dict.get("nb_dld", None) + dl_state: Union[str, None] = req_dict.get("dld_st", None) + ip = Utils.get_ip_addr(request) + + if serial is None or num_files_dld is None or num_files_to_dl is None or dl_state is None: + return "NG".encode() + + self.logger.info(f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})") + return "OK".encode() + def handle_billing_request(self, request: Request, _: Dict): req_dict = self.billing_req_to_dict(request.content.getvalue()) request_ip = Utils.get_ip_addr(request) diff --git a/index.py b/index.py index 7199dbe..265bded 100644 --- a/index.py +++ b/index.py @@ -63,6 +63,13 @@ class HttpDispatcher(resource.Resource): action="handle_dlorder", conditions=dict(method=["POST"]), ) + self.map_post.connect( + "allnet_loaderstaterecorder", + "/sys/servlet/LoaderStateRecorder", + controller="allnet", + action="handle_loaderstaterecorder", + conditions=dict(method=["POST"]), + ) self.map_post.connect( "allnet_billing", "/request", From 610ef70bad3de6b83c4ca09013c26e91ef6578e6 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 30 Jun 2023 00:32:52 -0400 Subject: [PATCH 22/40] allnet: add Alive get and post handlers --- core/allnet.py | 3 +++ index.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/core/allnet.py b/core/allnet.py index 00303f5..edb704c 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -256,6 +256,9 @@ class AllnetServlet: self.logger.info(f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})") return "OK".encode() + + def handle_alive(self, request: Request, match: Dict) -> bytes: + return "OK".encode() def handle_billing_request(self, request: Request, _: Dict): req_dict = self.billing_req_to_dict(request.content.getvalue()) diff --git a/index.py b/index.py index 265bded..6996f0e 100644 --- a/index.py +++ b/index.py @@ -70,6 +70,20 @@ class HttpDispatcher(resource.Resource): action="handle_loaderstaterecorder", conditions=dict(method=["POST"]), ) + self.map_post.connect( + "allnet_alive", + "/sys/servlet/Alive", + controller="allnet", + action="handle_alive", + conditions=dict(method=["POST"]), + ) + self.map_get.connect( + "allnet_alive", + "/sys/servlet/Alive", + controller="allnet", + action="handle_alive", + conditions=dict(method=["GET"]), + ) self.map_post.connect( "allnet_billing", "/request", From 8b43d554fc35cf26ff1b9a68f631d9f79dad682a Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 30 Jun 2023 01:19:17 -0400 Subject: [PATCH 23/40] allnet: make use of urllib.parse where applicable --- core/allnet.py | 137 +++++++++++++++++++++++++++---------------------- 1 file changed, 76 insertions(+), 61 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index edb704c..bc5eedf 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -11,6 +11,7 @@ from Crypto.Hash import SHA from Crypto.Signature import PKCS1_v1_5 from time import strptime from os import path +import urllib.parse from core.config import CoreConfig from core.utils import Utils @@ -79,7 +80,7 @@ class AllnetServlet: req = AllnetPowerOnRequest(req_dict[0]) # Validate the request. Currently we only validate the fields we plan on using - if not req.game_id or not req.ver or not req.serial or not req.ip: + if not req.game_id or not req.ver or not req.serial or not req.ip or not req.firm_ver or not req.boot_ver: raise AllnetRequestException( f"Bad auth request params from {request_ip} - {vars(req)}" ) @@ -89,12 +90,14 @@ class AllnetServlet: self.logger.error(e) return b"" - if req.format_ver == "3": + if req.format_ver == 3: resp = AllnetPowerOnResponse3(req.token) - else: + elif req.format_ver == 2: resp = AllnetPowerOnResponse2() + else: + resp = AllnetPowerOnResponse() - self.logger.debug(f"Allnet request: {vars(req)}") + self.logger.debug(f"Allnet request: {vars(req)}") if req.game_id not in self.uri_registry: if not self.config.server.is_develop: msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}." @@ -103,8 +106,9 @@ class AllnetServlet: ) self.logger.warn(msg) - resp.stat = 0 - return self.dict_to_http_form_string([vars(resp)]) + resp.stat = -1 + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") else: self.logger.info( @@ -113,12 +117,15 @@ class AllnetServlet: resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/" resp.host = f"{self.config.title.hostname}:{self.config.title.port}" - self.logger.debug(f"Allnet response: {vars(resp)}") - return self.dict_to_http_form_string([vars(resp)]) + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + + self.logger.debug(f"Allnet response: {resp_str}") + return (resp_str + "\n").encode("utf-8") resp.uri, resp.host = self.uri_registry[req.game_id] - machine = self.data.arcade.get_machine(req.serial) + machine = self.data.arcade.get_machine(req.serial) if machine is None and not self.config.server.allow_unregistered_serials: msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}." self.data.base.log_event( @@ -126,8 +133,9 @@ class AllnetServlet: ) self.logger.warn(msg) - resp.stat = 0 - return self.dict_to_http_form_string([vars(resp)]) + resp.stat = -2 + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") if machine is not None: arcade = self.data.arcade.get_arcade(machine["arcade"]) @@ -169,9 +177,13 @@ class AllnetServlet: msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}" self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg) self.logger.info(msg) - self.logger.debug(f"Allnet response: {vars(resp)}") - return self.dict_to_http_form_string([vars(resp)]).encode("utf-8") + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + self.logger.debug(f"Allnet response: {resp_dict}") + resp_str += "\n" + + return resp_str.encode("utf-8") def handle_dlorder(self, request: Request, _: Dict): request_ip = Utils.get_ip_addr(request) @@ -202,7 +214,7 @@ class AllnetServlet: not self.config.allnet.allow_online_updates or not self.config.allnet.update_cfg_folder ): - return self.dict_to_http_form_string([vars(resp)]) + return urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n" else: # TODO: Keychip check if path.exists( @@ -217,7 +229,8 @@ class AllnetServlet: self.logger.debug(f"Sending download uri {resp.uri}") self.data.base.log_event("allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}") - return self.dict_to_http_form_string([vars(resp)]) + + return urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n" def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes: if "file" not in match: @@ -323,7 +336,7 @@ class AllnetServlet: resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig) - resp_str = self.dict_to_http_form_string([vars(resp)], True) + resp_str = self.dict_to_http_form_string([vars(resp)]) if resp_str is None: self.logger.error(f"Failed to parse response {vars(resp)}") @@ -382,7 +395,7 @@ class AllnetServlet: def dict_to_http_form_string( self, data: List[Dict[str, Any]], - crlf: bool = False, + crlf: bool = True, trailing_newline: bool = True, ) -> Optional[str]: """ @@ -392,21 +405,19 @@ class AllnetServlet: urlencode = "" for item in data: for k, v in item.items(): + if k is None or v is None: + continue urlencode += f"{k}={v}&" - if crlf: urlencode = urlencode[:-1] + "\r\n" else: urlencode = urlencode[:-1] + "\n" - if not trailing_newline: if crlf: urlencode = urlencode[:-2] else: urlencode = urlencode[:-1] - return urlencode - except Exception as e: self.logger.error(f"dict_to_http_form_string: {e} while parsing {data}") return None @@ -416,20 +427,19 @@ class AllnetPowerOnRequest: def __init__(self, req: Dict) -> None: if req is None: raise AllnetRequestException("Request processing failed") - self.game_id: str = req.get("game_id", "") - self.ver: str = req.get("ver", "") - self.serial: str = req.get("serial", "") - self.ip: str = req.get("ip", "") - self.firm_ver: str = req.get("firm_ver", "") - self.boot_ver: str = req.get("boot_ver", "") - self.encode: str = req.get("encode", "") - self.hops = int(req.get("hops", "0")) - self.format_ver = req.get("format_ver", "2") - self.token = int(req.get("token", "0")) + self.game_id: str = req.get("game_id", None) + self.ver: str = req.get("ver", None) + self.serial: str = req.get("serial", None) + self.ip: str = req.get("ip", None) + self.firm_ver: str = req.get("firm_ver", None) + self.boot_ver: str = req.get("boot_ver", None) + self.encode: str = req.get("encode", "EUC-JP") + self.hops = int(req.get("hops", "-1")) + self.format_ver = float(req.get("format_ver", "1.00")) + self.token: str = req.get("token", "0") - -class AllnetPowerOnResponse3: - def __init__(self, token) -> None: +class AllnetPowerOnResponse: + def __init__(self) -> None: self.stat = 1 self.uri = "" self.host = "" @@ -440,40 +450,45 @@ class AllnetPowerOnResponse3: self.region_name0 = "W" self.region_name1 = "" self.region_name2 = "" - self.region_name3 = "" - self.country = "JPN" - self.allnet_id = "123" - self.client_timezone = "+0900" - self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime( - "%Y-%m-%dT%H:%M:%SZ" - ) + self.region_name3 = "" self.setting = "1" - self.res_ver = "3" - self.token = str(token) - - -class AllnetPowerOnResponse2: - def __init__(self) -> None: - self.stat = 1 - self.uri = "" - self.host = "" - self.place_id = "123" - self.name = "ARTEMiS" - self.nickname = "ARTEMiS" - self.region0 = "1" - self.region_name0 = "W" - self.region_name1 = "X" - self.region_name2 = "Y" - self.region_name3 = "Z" - self.country = "JPN" self.year = datetime.now().year self.month = datetime.now().month self.day = datetime.now().day self.hour = datetime.now().hour self.minute = datetime.now().minute self.second = datetime.now().second - self.setting = "1" - self.timezone = "+0900" + +class AllnetPowerOnResponse3(AllnetPowerOnResponse): + def __init__(self, token) -> None: + super().__init__() + + # Added in v3 + self.country = "JPN" + self.allnet_id = "123" + self.client_timezone = "+0900" + self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + self.res_ver = "3" + self.token = token + + # Removed in v3 + self.year = None + self.month = None + self.day = None + self.hour = None + self.minute = None + self.second = None + + +class AllnetPowerOnResponse2(AllnetPowerOnResponse): + def __init__(self) -> None: + super().__init__() + + # Added in v2 + self.country = "JPN" + self.timezone = "+09:00" self.res_class = "PowerOnResponseV2" From 9d33091bb85cc43749a800a862b86b0a69a1a682 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 30 Jun 2023 01:34:46 -0400 Subject: [PATCH 24/40] allnet: use parse_qsl --- core/allnet.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index bc5eedf..d440b0f 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -256,7 +256,9 @@ class AllnetServlet: def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes: req_data = request.content.getvalue() - req_dict = self.kvp_to_dict([req_data.decode()])[0] + sections = req_data.decode("utf-8").split("\r\n") + + req_dict = dict(urllib.parse.parse_qsl(sections[0])) serial: Union[str, None] = req_dict.get("serial", None) num_files_to_dl: Union[str, None] = req_dict.get("nb_ftd", None) @@ -347,21 +349,6 @@ class AllnetServlet: self.logger.info(f"Ping from {Utils.get_ip_addr(request)}") return b"naomi ok" - def kvp_to_dict(self, kvp: List[str]) -> List[Dict[str, Any]]: - ret: List[Dict[str, Any]] = [] - for x in kvp: - items = x.split("&") - tmp = {} - - for item in items: - kvp = item.split("=") - if len(kvp) == 2: - tmp[kvp[0]] = kvp[1] - - ret.append(tmp) - - return ret - def billing_req_to_dict(self, data: bytes): """ Parses an billing request string into a python dictionary @@ -371,7 +358,10 @@ class AllnetServlet: unzipped = decomp.decompress(data) sections = unzipped.decode("ascii").split("\r\n") - return self.kvp_to_dict(sections) + ret = [] + for x in sections: + ret.append(dict(urllib.parse.parse_qsl(x))) + return ret except Exception as e: self.logger.error(f"billing_req_to_dict: {e} while parsing {data}") @@ -386,7 +376,10 @@ class AllnetServlet: unzipped = zlib.decompress(zipped) sections = unzipped.decode("utf-8").split("\r\n") - return self.kvp_to_dict(sections) + ret = [] + for x in sections: + ret.append(dict(urllib.parse.parse_qsl(x))) + return ret except Exception as e: self.logger.error(f"allnet_req_to_dict: {e} while parsing {data}") From 318b73dd57b71dfb87fabef9c21ce99f1c2b222e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 01:08:54 -0400 Subject: [PATCH 25/40] finale: finish porting request data from aqua --- titles/mai2/base.py | 92 +++++++++++++---- titles/mai2/dx.py | 21 ---- titles/mai2/schema/profile.py | 182 +++++++++++++++++++++++++++++----- titles/mai2/schema/score.py | 39 +++++--- 4 files changed, 258 insertions(+), 76 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index ef15abe..ae12e81 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -112,12 +112,12 @@ class Mai2Base: return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_detail(data["userId"], self.version) - o = self.data.profile.get_profile_option(data["userId"], self.version) - if p is None or o is None: + p = self.data.profile.get_profile_detail(data["userId"], self.version, True) + w = self.data.profile.get_web_option(data["userId"], self.version) + if p is None or w is None: return {} # Register profile = p._asdict() - option = o._asdict() + web_opt = w._asdict() return { "userId": data["userId"], @@ -127,16 +127,15 @@ class Mai2Base: "lastLoginDate": profile["lastLoginDate"], "lastPlayDate": profile["lastPlayDate"], "playerRating": profile["playerRating"], - "nameplateId": 0, # Unused + "nameplateId": profile["nameplateId"], "frameId": profile["frameId"], "iconId": profile["iconId"], - "trophyId": 0, # Unused - "partnerId": profile["partnerId"], - "dispRate": option["dispRate"], # 0: all, 1: dispRate, 2: dispDan, 3: hide - "dispRank": 0, # TODO - "dispHomeRanker": 0, # TODO - "dispTotalLv": 0, # TODO - "totalLv": 0, # TODO + "trophyId": profile["trophyId"], + "dispRate": web_opt["dispRate"], # 0: all, 1: dispRate, 2: dispDan, 3: hide + "dispRank": web_opt["dispRank"], + "dispHomeRanker": web_opt["dispHomeRanker"], + "dispTotalLv": web_opt["dispTotalLv"], + "totalLv": profile["totalLv"], } def handle_user_login_api_request(self, data: Dict) -> Dict: @@ -188,11 +187,34 @@ class Mai2Base: upsert = data["upsertUserAll"] if "userData" in upsert and len(upsert["userData"]) > 0: - upsert["userData"][0]["isNetMember"] = 1 upsert["userData"][0].pop("accessCode") + upsert["userData"][0].pop("userId") + self.data.profile.put_profile_detail( - user_id, self.version, upsert["userData"][0] + user_id, self.version, upsert["userData"][0], False ) + + if "UserWebOption" in upsert and len(upsert["UserWebOption"]) > 0: + upsert["UserWebOption"][0]["isNetMember"] = True + self.data.profile.put_web_option( + user_id, self.version, upsert["UserWebOption"][0] + ) + + if "userGradeStatusList" in upsert and len(upsert["userGradeStatusList"]) > 0: + self.data.profile.put_web_option( + user_id, self.version, upsert["userGradeStatusList"][0] + ) + + if "userBossList" in upsert and len(upsert["userBossList"]) > 0: + self.data.profile.put_boss_list( + user_id, self.version, upsert["userBossList"][0] + ) + + if "userPlaylogList" in upsert and len(upsert["userPlaylogList"]) > 0: + for playlog in upsert["userPlaylogList"]: + self.data.score.put_playlog( + user_id, self.version, playlog + ) if "userExtend" in upsert and len(upsert["userExtend"]) > 0: self.data.profile.put_profile_extend( @@ -201,11 +223,14 @@ class Mai2Base: if "userGhost" in upsert: for ghost in upsert["userGhost"]: - self.data.profile.put_profile_extend(user_id, self.version, ghost) + self.data.profile.put_profile_ghost(user_id, self.version, ghost) + + if "userRecentRatingList" in upsert: + self.data.profile.put_recent_rating(user_id, self.version, upsert["userRecentRatingList"]) if "userOption" in upsert and len(upsert["userOption"]) > 0: self.data.profile.put_profile_option( - user_id, self.version, upsert["userOption"][0] + user_id, self.version, upsert["userOption"][0], False ) if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0: @@ -305,7 +330,7 @@ class Mai2Base: return {"returnCode": 1} def handle_get_user_data_api_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile_detail(data["userId"], self.version) + profile = self.data.profile.get_profile_detail(data["userId"], self.version, False) if profile is None: return @@ -329,7 +354,7 @@ class Mai2Base: return {"userId": data["userId"], "userExtend": extend_dict} def handle_get_user_option_api_request(self, data: Dict) -> Dict: - options = self.data.profile.get_profile_option(data["userId"], self.version) + options = self.data.profile.get_profile_option(data["userId"], self.version, False) if options is None: return @@ -399,6 +424,25 @@ class Mai2Base: "userChargeList": user_charge_list, } + def handle_get_user_present_api_request(self, data: Dict) -> Dict: + return { "userId": data.get("userId", 0), "length": 0, "userPresentList": []} + + def handle_get_transfer_friend_api_request(self, data: Dict) -> Dict: + return {} + + def handle_get_user_present_event_api_request(self, data: Dict) -> Dict: + return { "userId": data.get("userId", 0), "length": 0, "userPresentEventList": []} + + def handle_get_user_boss_api_request(self, data: Dict) -> Dict: + b = self.data.profile.get_boss_list(data["userId"]) + if b is None: + return { "userId": data.get("userId", 0), "userBossData": {}} + boss_lst = b._asdict() + boss_lst.pop("id") + boss_lst.pop("user") + + return { "userId": data.get("userId", 0), "userBossData": boss_lst} + def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = int(data["nextIndex"] / 10000000000) next_idx = int(data["nextIndex"] % 10000000000) @@ -435,6 +479,8 @@ class Mai2Base: tmp = chara._asdict() tmp.pop("id") tmp.pop("user") + tmp.pop("awakening") + tmp.pop("useCount") chara_list.append(tmp) return {"userId": data["userId"], "userCharacterList": chara_list} @@ -468,6 +514,16 @@ class Mai2Base: return {"userId": data["userId"], "userGhost": ghost_dict} + def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: + rating = self.data.profile.get_recent_rating(data["userId"]) + if rating is None: + return + + r = rating._asdict() + lst = r.get("userRecentRatingList", []) + + return {"userId": data["userId"], "length": len(lst), "userRecentRatingList": lst} + def handle_get_user_rating_api_request(self, data: Dict) -> Dict: rating = self.data.profile.get_profile_rating(data["userId"], self.version) if rating is None: diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 9ac7067..266332e 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -75,27 +75,6 @@ class Mai2DX(Mai2Base): else 0, # New with uni+ } - def handle_user_login_api_request(self, data: Dict) -> Dict: - profile = self.data.profile.get_profile_detail(data["userId"], self.version) - - if profile is not None: - lastLoginDate = profile["lastLoginDate"] - loginCt = profile["playCount"] - - if "regionId" in data: - self.data.profile.put_profile_region(data["userId"], data["regionId"]) - else: - loginCt = 0 - lastLoginDate = "2017-12-05 07:00:00.0" - - return { - "returnCode": 1, - "lastLoginDate": lastLoginDate, - "loginCount": loginCt, - "consecutiveLoginCount": 0, # We don't really have a way to track this... - "loginId": loginCt, # Used with the playlog! - } - def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] playlog = data["userPlaylog"] diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index eb0da73..950fd99 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -423,42 +423,80 @@ activity = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("kind", Integer, nullable=False), - Column("activityId", Integer, nullable=False), - Column("param1", Integer, nullable=False), - Column("param2", Integer, nullable=False), - Column("param3", Integer, nullable=False), - Column("param4", Integer, nullable=False), - Column("sortNumber", Integer, nullable=False), + Column("kind", Integer), + Column("activityId", Integer), + Column("param1", Integer), + Column("param2", Integer), + Column("param3", Integer), + Column("param4", Integer), + Column("sortNumber", Integer), UniqueConstraint("user", "kind", "activityId", name="mai2_profile_activity_uk"), mysql_charset="utf8mb4", ) +boss_list = Table( + "mai2_profile_boss_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("pandoraFlagList0", Integer), + Column("pandoraFlagList1", Integer), + Column("pandoraFlagList2", Integer), + Column("pandoraFlagList3", Integer), + Column("pandoraFlagList4", Integer), + Column("pandoraFlagList5", Integer), + Column("pandoraFlagList6", Integer), + Column("emblemFlagList", Integer), + UniqueConstraint("user", name="mai2_profile_boss_list_uk"), + mysql_charset="utf8mb4", +) + +recent_rating = Table( + "mai2_profile_recent_rating", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("userRecentRatingList", Integer), + UniqueConstraint("user", name="mai2_profile_recent_rating_uk"), + mysql_charset="utf8mb4", +) class Mai2ProfileData(BaseData): def put_profile_detail( - self, user_id: int, version: int, detail_data: Dict + self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True ) -> Optional[Row]: detail_data["user"] = user_id detail_data["version"] = version - sql = insert(detail).values(**detail_data) + + if is_dx: + sql = insert(detail).values(**detail_data) + else: + sql = insert(detail_old).values(**detail_data) conflict = sql.on_duplicate_key_update(**detail_data) result = self.execute(conflict) if result is None: self.logger.warn( - f"put_profile: Failed to create profile! user_id {user_id}" + f"put_profile: Failed to create profile! user_id {user_id} is_dx {is_dx}" ) return None return result.lastrowid - def get_profile_detail(self, user_id: int, version: int) -> Optional[Row]: - sql = ( - select(detail) - .where(and_(detail.c.user == user_id, detail.c.version <= version)) - .order_by(detail.c.version.desc()) - ) + def get_profile_detail(self, user_id: int, version: int, is_dx: bool = True) -> Optional[Row]: + if is_dx: + sql = ( + select(detail) + .where(and_(detail.c.user == user_id, detail.c.version <= version)) + .order_by(detail.c.version.desc()) + ) + + else: + sql = ( + select(detail_old) + .where(and_(detail_old.c.user == user_id, detail_old.c.version <= version)) + .order_by(detail_old.c.version.desc()) + ) result = self.execute(sql) if result is None: @@ -520,26 +558,36 @@ class Mai2ProfileData(BaseData): return result.fetchone() def put_profile_option( - self, user_id: int, version: int, option_data: Dict + self, user_id: int, version: int, option_data: Dict, is_dx: bool = True ) -> Optional[int]: option_data["user"] = user_id option_data["version"] = version - sql = insert(option).values(**option_data) + if is_dx: + sql = insert(option).values(**option_data) + else: + sql = insert(option_old).values(**option_data) conflict = sql.on_duplicate_key_update(**option_data) result = self.execute(conflict) if result is None: - self.logger.warn(f"put_profile_option: failed to update! {user_id}") + self.logger.warn(f"put_profile_option: failed to update! {user_id} is_dx {is_dx}") return None return result.lastrowid - def get_profile_option(self, user_id: int, version: int) -> Optional[Row]: - sql = ( - select(option) - .where(and_(option.c.user == user_id, option.c.version <= version)) - .order_by(option.c.version.desc()) - ) + def get_profile_option(self, user_id: int, version: int, is_dx: bool = True) -> Optional[Row]: + if is_dx: + sql = ( + select(option) + .where(and_(option.c.user == user_id, option.c.version <= version)) + .order_by(option.c.version.desc()) + ) + else: + sql = ( + select(option_old) + .where(and_(option_old.c.user == user_id, option_old.c.version <= version)) + .order_by(option_old.c.version.desc()) + ) result = self.execute(sql) if result is None: @@ -629,3 +677,87 @@ class Mai2ProfileData(BaseData): if result is None: return None return result.fetchall() + + def put_web_option(self, user_id: int, web_opts: Dict) -> Optional[int]: + sql = insert(web_opt).values(**web_opts) + + conflict = sql.on_duplicate_key_update(**web_opts) + + result = self.execute(conflict) + if result is None: + self.logger.warn( + f"put_web_option: failed to update! user_id: {user_id}" + ) + return None + return result.lastrowid + + def get_web_option(self, user_id: int) -> Optional[Row]: + sql = web_opt.select(web_opt.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_grade_status(self, user_id: int, grade_stat: Dict) -> Optional[int]: + sql = insert(grade_status).values(**grade_stat) + + conflict = sql.on_duplicate_key_update(**grade_stat) + + result = self.execute(conflict) + if result is None: + self.logger.warn( + f"put_grade_status: failed to update! user_id: {user_id}" + ) + return None + return result.lastrowid + + def get_grade_status(self, user_id: int) -> Optional[Row]: + sql = grade_status.select(grade_status.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_boss_list(self, user_id: int, boss_stat: Dict) -> Optional[int]: + sql = insert(boss_list).values(**boss_stat) + + conflict = sql.on_duplicate_key_update(**boss_stat) + + result = self.execute(conflict) + if result is None: + self.logger.warn( + f"put_boss_list: failed to update! user_id: {user_id}" + ) + return None + return result.lastrowid + + def get_boss_list(self, user_id: int) -> Optional[Row]: + sql = boss_list.select(boss_list.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]: + sql = insert(recent_rating).values(**rr) + + conflict = sql.on_duplicate_key_update(**rr) + + result = self.execute(conflict) + if result is None: + self.logger.warn( + f"put_recent_rating: failed to update! user_id: {user_id}" + ) + return None + return result.lastrowid + + def get_recent_rating(self, user_id: int) -> Optional[Row]: + sql = recent_rating.select(recent_rating.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() \ No newline at end of file diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index b754eb6..6b25d14 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -273,28 +273,39 @@ best_score_old = Table( ) class Mai2ScoreData(BaseData): - def put_best_score(self, user_id: int, score_data: Dict) -> Optional[int]: + def put_best_score(self, user_id: int, score_data: Dict, is_dx: bool = True) -> Optional[int]: score_data["user"] = user_id - sql = insert(best_score).values(**score_data) + if is_dx: + sql = insert(best_score).values(**score_data) + else: + sql = insert(best_score_old).values(**score_data) conflict = sql.on_duplicate_key_update(**score_data) result = self.execute(conflict) if result is None: self.logger.error( - f"put_best_score: Failed to insert best score! user_id {user_id}" + f"put_best_score: Failed to insert best score! user_id {user_id} is_dx {is_dx}" ) return None return result.lastrowid @cached(2) - def get_best_scores(self, user_id: int, song_id: int = None) -> Optional[List[Row]]: - sql = best_score.select( - and_( - best_score.c.user == user_id, - (best_score.c.song_id == song_id) if song_id is not None else True, + def get_best_scores(self, user_id: int, song_id: int = None, is_dx: bool = True) -> Optional[List[Row]]: + if is_dx: + sql = best_score.select( + and_( + best_score.c.user == user_id, + (best_score.c.song_id == song_id) if song_id is not None else True, + ) + ) + else: + sql = best_score_old.select( + and_( + best_score_old.c.user == user_id, + (best_score_old.c.song_id == song_id) if song_id is not None else True, + ) ) - ) result = self.execute(sql) if result is None: @@ -317,15 +328,19 @@ class Mai2ScoreData(BaseData): return None return result.fetchone() - def put_playlog(self, user_id: int, playlog_data: Dict) -> Optional[int]: + def put_playlog(self, user_id: int, playlog_data: Dict, is_dx: bool = True) -> Optional[int]: playlog_data["user"] = user_id - sql = insert(playlog).values(**playlog_data) + + if is_dx: + sql = insert(playlog).values(**playlog_data) + else: + sql = insert(playlog_old).values(**playlog_data) conflict = sql.on_duplicate_key_update(**playlog_data) result = self.execute(conflict) if result is None: - self.logger.error(f"put_playlog: Failed to insert! user_id {user_id}") + self.logger.error(f"put_playlog: Failed to insert! user_id {user_id} is_dx {is_dx}") return None return result.lastrowid From 2c6902a546672187b07b075d2504ccaaa0bfc584 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 01:12:15 -0400 Subject: [PATCH 26/40] mai2: fix typos --- titles/mai2/schema/profile.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 950fd99..f423b34 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -434,8 +434,8 @@ activity = Table( mysql_charset="utf8mb4", ) -boss_list = Table( - "mai2_profile_boss_list", +boss = Table( + "maimai_profile_boss", metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), @@ -447,12 +447,12 @@ boss_list = Table( Column("pandoraFlagList5", Integer), Column("pandoraFlagList6", Integer), Column("emblemFlagList", Integer), - UniqueConstraint("user", name="mai2_profile_boss_list_uk"), + UniqueConstraint("user", name="mai2_profile_boss_uk"), mysql_charset="utf8mb4", ) recent_rating = Table( - "mai2_profile_recent_rating", + "maimai_profile_recent_rating", metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), @@ -721,7 +721,7 @@ class Mai2ProfileData(BaseData): return result.fetchone() def put_boss_list(self, user_id: int, boss_stat: Dict) -> Optional[int]: - sql = insert(boss_list).values(**boss_stat) + sql = insert(boss).values(**boss_stat) conflict = sql.on_duplicate_key_update(**boss_stat) @@ -734,7 +734,7 @@ class Mai2ProfileData(BaseData): return result.lastrowid def get_boss_list(self, user_id: int) -> Optional[Row]: - sql = boss_list.select(boss_list.c.user == user_id) + sql = boss.select(boss.c.user == user_id) result = self.execute(sql) if result is None: From 3e461f4d71edfe13b010d2d8b831c86f0a145bc6 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 01:41:34 -0400 Subject: [PATCH 27/40] mai2: finale fixes --- titles/mai2/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index ae12e81..32f7922 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -113,7 +113,7 @@ class Mai2Base: def handle_get_user_preview_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_detail(data["userId"], self.version, True) - w = self.data.profile.get_web_option(data["userId"], self.version) + w = self.data.profile.get_web_option(data["userId"]) if p is None or w is None: return {} # Register profile = p._asdict() @@ -201,19 +201,19 @@ class Mai2Base: ) if "userGradeStatusList" in upsert and len(upsert["userGradeStatusList"]) > 0: - self.data.profile.put_web_option( - user_id, self.version, upsert["userGradeStatusList"][0] + self.data.profile.put_grade_status( + user_id, upsert["userGradeStatusList"][0] ) if "userBossList" in upsert and len(upsert["userBossList"]) > 0: self.data.profile.put_boss_list( - user_id, self.version, upsert["userBossList"][0] + user_id, upsert["userBossList"][0] ) if "userPlaylogList" in upsert and len(upsert["userPlaylogList"]) > 0: for playlog in upsert["userPlaylogList"]: self.data.score.put_playlog( - user_id, self.version, playlog + user_id, playlog ) if "userExtend" in upsert and len(upsert["userExtend"]) > 0: From dc8c27046ef8eca9f6bfbd8f2f8369ce5f3e3b44 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 01:42:38 -0400 Subject: [PATCH 28/40] mai2: more finale fixes --- titles/mai2/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 32f7922..6f68487 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -226,7 +226,7 @@ class Mai2Base: self.data.profile.put_profile_ghost(user_id, self.version, ghost) if "userRecentRatingList" in upsert: - self.data.profile.put_recent_rating(user_id, self.version, upsert["userRecentRatingList"]) + self.data.profile.put_recent_rating(user_id, upsert["userRecentRatingList"]) if "userOption" in upsert and len(upsert["userOption"]) > 0: self.data.profile.put_profile_option( From d89eb61e6276dc9f625cc7caf1a8c3fec5e2a1d5 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 01:56:52 -0400 Subject: [PATCH 29/40] mai2: fixes round 3 --- titles/mai2/base.py | 14 ++++++-------- titles/mai2/schema/profile.py | 10 +++++++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 6f68487..e3fce20 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -113,7 +113,7 @@ class Mai2Base: def handle_get_user_preview_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_detail(data["userId"], self.version, True) - w = self.data.profile.get_web_option(data["userId"]) + w = self.data.profile.get_web_option(data["userId"], self.version) if p is None or w is None: return {} # Register profile = p._asdict() @@ -229,6 +229,7 @@ class Mai2Base: self.data.profile.put_recent_rating(user_id, upsert["userRecentRatingList"]) if "userOption" in upsert and len(upsert["userOption"]) > 0: + upsert["userOption"][0].pop("userId") self.data.profile.put_profile_option( user_id, self.version, upsert["userOption"][0], False ) @@ -239,9 +240,8 @@ class Mai2Base: ) if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0: - for k, v in upsert["userActivityList"][0].items(): - for act in v: - self.data.profile.put_profile_activity(user_id, act) + for act in upsert["userActivityList"]: + self.data.profile.put_profile_activity(user_id, act) if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0: for charge in upsert["userChargeList"]: @@ -265,8 +265,7 @@ class Mai2Base: user_id, char["characterId"], char["level"], - char["awakening"], - char["useCount"], + char["point"], ) if "userItemList" in upsert and len(upsert["userItemList"]) > 0: @@ -276,7 +275,6 @@ class Mai2Base: int(item["itemKind"]), item["itemId"], item["stock"], - item["isValid"], ) if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: @@ -302,7 +300,7 @@ class Mai2Base: if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0: for music in upsert["userMusicDetailList"]: - self.data.score.put_best_score(user_id, music) + self.data.score.put_best_score(user_id, music, False) if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0: for course in upsert["userCourseList"]: diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index f423b34..9fd11a3 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -678,7 +678,9 @@ class Mai2ProfileData(BaseData): return None return result.fetchall() - def put_web_option(self, user_id: int, web_opts: Dict) -> Optional[int]: + def put_web_option(self, user_id: int, version: int, web_opts: Dict) -> Optional[int]: + web_opts["user"] = user_id + web_opts["version"] = version sql = insert(web_opt).values(**web_opts) conflict = sql.on_duplicate_key_update(**web_opts) @@ -691,8 +693,8 @@ class Mai2ProfileData(BaseData): return None return result.lastrowid - def get_web_option(self, user_id: int) -> Optional[Row]: - sql = web_opt.select(web_opt.c.user == user_id) + def get_web_option(self, user_id: int, version: int) -> Optional[Row]: + sql = web_opt.select(and_(web_opt.c.user == user_id, web_opt.c.version == version)) result = self.execute(sql) if result is None: @@ -700,6 +702,7 @@ class Mai2ProfileData(BaseData): return result.fetchone() def put_grade_status(self, user_id: int, grade_stat: Dict) -> Optional[int]: + grade_stat["user"] = user_id sql = insert(grade_status).values(**grade_stat) conflict = sql.on_duplicate_key_update(**grade_stat) @@ -721,6 +724,7 @@ class Mai2ProfileData(BaseData): return result.fetchone() def put_boss_list(self, user_id: int, boss_stat: Dict) -> Optional[int]: + boss_stat["user"] = user_id sql = insert(boss).values(**boss_stat) conflict = sql.on_duplicate_key_update(**boss_stat) From 9859ab4fdb7ccf08f2079f8cb707f2d9b0ebd296 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 01:59:19 -0400 Subject: [PATCH 30/40] mai2: fix playlog saving --- titles/mai2/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index e3fce20..5811735 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -213,7 +213,7 @@ class Mai2Base: if "userPlaylogList" in upsert and len(upsert["userPlaylogList"]) > 0: for playlog in upsert["userPlaylogList"]: self.data.score.put_playlog( - user_id, playlog + user_id, playlog, False ) if "userExtend" in upsert and len(upsert["userExtend"]) > 0: From d9a92f58650282fb0188e8619d9e648588048ab8 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 02:04:30 -0400 Subject: [PATCH 31/40] mai2: 4th round of fixes --- titles/mai2/base.py | 7 +++---- titles/mai2/schema/item.py | 13 +++++++++++++ titles/mai2/schema/profile.py | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 5811735..8fa3d79 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -261,11 +261,9 @@ class Mai2Base: if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0: for char in upsert["userCharacterList"]: - self.data.item.put_character( + self.data.item.put_character_( user_id, - char["characterId"], - char["level"], - char["point"], + char ) if "userItemList" in upsert and len(upsert["userItemList"]) > 0: @@ -275,6 +273,7 @@ class Mai2Base: int(item["itemKind"]), item["itemId"], item["stock"], + True ) if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 4e20383..284365f 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -333,6 +333,19 @@ class Mai2ItemData(BaseData): if result is None: return None return result.fetchone() + + def put_character_(self, user_id: int, char_data: Dict) -> Optional[int]: + char_data["user"] = user_id + sql = insert(character).values(**char_data) + + conflict = sql.on_duplicate_key_update(**char_data) + result = self.execute(conflict) + if result is None: + self.logger.warn( + f"put_character_: failed to insert item! user_id: {user_id}" + ) + return None + return result.lastrowid def put_character( self, diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 9fd11a3..462e238 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -456,7 +456,7 @@ recent_rating = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), - Column("userRecentRatingList", Integer), + Column("userRecentRatingList", JSON), UniqueConstraint("user", name="mai2_profile_recent_rating_uk"), mysql_charset="utf8mb4", ) From b29cb0fbaa29d8b097fcd002e5ae8990a1bcd7b2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 02:06:00 -0400 Subject: [PATCH 32/40] mai2: fix put_recent_rating --- titles/mai2/schema/profile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 462e238..48a15ed 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -746,9 +746,9 @@ class Mai2ProfileData(BaseData): return result.fetchone() def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]: - sql = insert(recent_rating).values(**rr) + sql = insert(recent_rating).values(rr) - conflict = sql.on_duplicate_key_update(**rr) + conflict = sql.on_duplicate_key_update(rr) result = self.execute(conflict) if result is None: From 8f9584c3d2485f3d87f55fb032cd7fa1de574ab0 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 02:07:19 -0400 Subject: [PATCH 33/40] mai2: hotfix put_recent_rating --- titles/mai2/schema/profile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 48a15ed..f32b877 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -746,9 +746,9 @@ class Mai2ProfileData(BaseData): return result.fetchone() def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]: - sql = insert(recent_rating).values(rr) + sql = insert(recent_rating).values(user=user_id, userRecentRatingList=rr) - conflict = sql.on_duplicate_key_update(rr) + conflict = sql.on_duplicate_key_update(userRecentRatingList=rr) result = self.execute(conflict) if result is None: From 3e9cec3a20c8abaa9f7c0f8796a8cab98fccfb07 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 02:11:37 -0400 Subject: [PATCH 34/40] mai2: put_recent_rating final fix --- titles/mai2/schema/profile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index f32b877..54bda21 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -748,7 +748,7 @@ class Mai2ProfileData(BaseData): def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]: sql = insert(recent_rating).values(user=user_id, userRecentRatingList=rr) - conflict = sql.on_duplicate_key_update(userRecentRatingList=rr) + conflict = sql.on_duplicate_key_update({'userRecentRatingList': rr}) result = self.execute(conflict) if result is None: From c4c0566cd5d437161f3d8da258f6ddee596dccdd Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 02:19:19 -0400 Subject: [PATCH 35/40] mai2: fix userWebOption --- titles/mai2/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 8fa3d79..8445ab5 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -194,10 +194,10 @@ class Mai2Base: user_id, self.version, upsert["userData"][0], False ) - if "UserWebOption" in upsert and len(upsert["UserWebOption"]) > 0: - upsert["UserWebOption"][0]["isNetMember"] = True + if "userWebOption" in upsert and len(upsert["userWebOption"]) > 0: + upsert["userWebOption"][0]["isNetMember"] = True self.data.profile.put_web_option( - user_id, self.version, upsert["UserWebOption"][0] + user_id, self.version, upsert["userWebOption"][0] ) if "userGradeStatusList" in upsert and len(upsert["userGradeStatusList"]) > 0: From 042440c76ed149a0480d9cfd72c3e4ae380ceeb9 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 02:27:26 -0400 Subject: [PATCH 36/40] mai2: fix handle_get_user_preview_api_request --- titles/mai2/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 8445ab5..5894282 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -112,7 +112,7 @@ class Mai2Base: return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} def handle_get_user_preview_api_request(self, data: Dict) -> Dict: - p = self.data.profile.get_profile_detail(data["userId"], self.version, True) + p = self.data.profile.get_profile_detail(data["userId"], self.version, False) w = self.data.profile.get_web_option(data["userId"], self.version) if p is None or w is None: return {} # Register @@ -124,7 +124,7 @@ class Mai2Base: "userName": profile["userName"], "isLogin": False, "lastDataVersion": profile["lastDataVersion"], - "lastLoginDate": profile["lastLoginDate"], + "lastLoginDate": profile["lastPlayDate"], "lastPlayDate": profile["lastPlayDate"], "playerRating": profile["playerRating"], "nameplateId": profile["nameplateId"], From d204954447127c9a196503dd25f5fc41009c1ff1 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 02:40:07 -0400 Subject: [PATCH 37/40] mai2: add missing finale endpoints --- titles/mai2/base.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 5894282..a53e416 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -683,6 +683,31 @@ class Mai2Base: def handle_get_user_region_api_request(self, data: Dict) -> Dict: return {"userId": data["userId"], "length": 0, "userRegionList": []} + + def handle_get_user_web_option_api_request(self, data: Dict) -> Dict: + w = self.data.profile.get_web_option(data["userId"], self.version) + if w is None: + return {"userId": data["userId"], "userWebOption": {}} + + web_opt = w._asdict() + web_opt.pop("id") + web_opt.pop("user") + web_opt.pop("version") + + return {"userId": data["userId"], "userWebOption": web_opt} + + def handle_get_user_survival_api_request(self, data: Dict) -> Dict: + return {"userId": data["userId"], "length": 0, "userSurvivalList": []} + + def handle_get_user_grade_api_request(self, data: Dict) -> Dict: + g = self.data.profile.get_grade_status(data["userId"]) + if g is None: + return {"userId": data["userId"], "userGradeStatus": {}, "length": 0, "userGradeList": []} + grade_stat = g._asdict() + grade_stat.pop("id") + grade_stat.pop("user") + + return {"userId": data["userId"], "userGradeStatus": grade_stat, "length": 0, "userGradeList": []} def handle_get_user_music_api_request(self, data: Dict) -> Dict: user_id = data.get("userId", 0) @@ -695,7 +720,7 @@ class Mai2Base: self.logger.warn("handle_get_user_music_api_request: Could not find userid in data, or userId is 0") return {} - songs = self.data.score.get_best_scores(user_id) + songs = self.data.score.get_best_scores(user_id, is_dx=False) if songs is None: self.logger.debug("handle_get_user_music_api_request: get_best_scores returned None!") return { From f279adb89472031be53e53b0a71c9d1d539df676 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 1 Jul 2023 21:51:18 -0400 Subject: [PATCH 38/40] mai2: add consecutive day login count, update db to v7, fix reader, courses, and docs --- core/data/schema/versions/SDEZ_6_rollback.sql | 1 + core/data/schema/versions/SDEZ_7_upgrade.sql | 9 ++++ docs/game_specific_info.md | 45 ++++++++-------- titles/mai2/__init__.py | 2 +- titles/mai2/base.py | 29 ++++++++++- titles/mai2/read.py | 3 +- titles/mai2/schema/profile.py | 52 ++++++++++++++++++- titles/mai2/schema/score.py | 2 +- 8 files changed, 115 insertions(+), 28 deletions(-) create mode 100644 core/data/schema/versions/SDEZ_6_rollback.sql create mode 100644 core/data/schema/versions/SDEZ_7_upgrade.sql diff --git a/core/data/schema/versions/SDEZ_6_rollback.sql b/core/data/schema/versions/SDEZ_6_rollback.sql new file mode 100644 index 0000000..0ca7036 --- /dev/null +++ b/core/data/schema/versions/SDEZ_6_rollback.sql @@ -0,0 +1 @@ +DROP TABLE aime.mai2_profile_consec_logins; diff --git a/core/data/schema/versions/SDEZ_7_upgrade.sql b/core/data/schema/versions/SDEZ_7_upgrade.sql new file mode 100644 index 0000000..20f3c70 --- /dev/null +++ b/core/data/schema/versions/SDEZ_7_upgrade.sql @@ -0,0 +1,9 @@ +CREATE TABLE `mai2_profile_consec_logins` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user` int(11) NOT NULL, + `version` int(11) NOT NULL, + `logins` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `mai2_profile_consec_logins_uk` (`user`,`version`), + CONSTRAINT `mai2_profile_consec_logins_ibfk_1` FOREIGN KEY (`user`) REFERENCES `aime_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; \ No newline at end of file diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index f4fec94..0b522aa 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -129,30 +129,31 @@ Config file is located in `config/cxb.yaml`. | Game Code | Version ID | Version Name | |-----------|------------|-------------------------| -| SDEZ | 0 | maimai DX | -| SDEZ | 1 | maimai DX PLUS | -| SDEZ | 2 | maimai DX Splash | -| SDEZ | 3 | maimai DX Splash PLUS | -| SDEZ | 4 | maimai DX Universe | -| SDEZ | 5 | maimai DX Universe PLUS | -| SDEZ | 6 | maimai DX Festival | + For versions pre-dx -| Game Code | Version ID | Version Name | -|-----------|------------|----------------------| -| SBXL | 1000 | maimai | -| SBXL | 1001 | maimai PLUS | -| SBZF | 1002 | maimai GreeN | -| SBZF | 1003 | maimai GreeN PLUS | -| SDBM | 1004 | maimai ORANGE | -| SDBM | 1005 | maimai ORANGE PLUS | -| SDCQ | 1006 | maimai PiNK | -| SDCQ | 1007 | maimai PiNK PLUS | -| SDDK | 1008 | maimai MURASAKI | -| SDDK | 1009 | maimai MURASAKI PLUS | -| SDDZ | 1010 | maimai MILK | -| SDDZ | 1011 | maimai MILK PLUS | -| SDEY | 1012 | maimai FiNALE | +| Game Code | Version ID | Version Name | +|-----------|------------|-------------------------| +| SBXL | 0 | maimai | +| SBXL | 1 | maimai PLUS | +| SBZF | 2 | maimai GreeN | +| SBZF | 3 | maimai GreeN PLUS | +| SDBM | 4 | maimai ORANGE | +| SDBM | 5 | maimai ORANGE PLUS | +| SDCQ | 6 | maimai PiNK | +| SDCQ | 7 | maimai PiNK PLUS | +| SDDK | 8 | maimai MURASAKI | +| SDDK | 9 | maimai MURASAKI PLUS | +| SDDZ | 10 | maimai MILK | +| SDDZ | 11 | maimai MILK PLUS | +| SDEY | 12 | maimai FiNALE | +| SDEZ | 13 | maimai DX | +| SDEZ | 14 | maimai DX PLUS | +| SDEZ | 15 | maimai DX Splash | +| SDEZ | 16 | maimai DX Splash PLUS | +| SDEZ | 17 | maimai DX Universe | +| SDEZ | 18 | maimai DX Universe PLUS | +| SDEZ | 19 | maimai DX Festival | ### Importer diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 7063ee6..2fa3874 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -16,4 +16,4 @@ game_codes = [ Mai2Constants.GAME_CODE_GREEN, Mai2Constants.GAME_CODE, ] -current_schema_version = 6 +current_schema_version = 7 diff --git a/titles/mai2/base.py b/titles/mai2/base.py index a53e416..9f1ffaf 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -140,6 +140,7 @@ class Mai2Base: def handle_user_login_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_detail(data["userId"], self.version) + consec = self.data.profile.get_consec_login(data["userId"], self.version) if profile is not None: lastLoginDate = profile["lastLoginDate"] @@ -150,12 +151,32 @@ class Mai2Base: else: loginCt = 0 lastLoginDate = "2017-12-05 07:00:00.0" + + if consec is None or not consec: + consec_ct = 1 + + else: + lastlogindate_ = datetime.strptime(profile["lastLoginDate"], "%Y-%m-%d %H:%M:%S.%f").timestamp() + today_midnight = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp() + yesterday_midnight = today_midnight - 86400 + + if lastlogindate_ < today_midnight: + consec_ct = consec['logins'] + 1 + self.data.profile.add_consec_login(data["userId"], self.version) + + elif lastlogindate_ < yesterday_midnight: + consec_ct = 1 + self.data.profile.reset_consec_login(data["userId"], self.version) + + else: + consec_ct = consec['logins'] + return { "returnCode": 1, "lastLoginDate": lastLoginDate, "loginCount": loginCt, - "consecutiveLoginCount": 0, # We don't really have a way to track this... + "consecutiveLoginCount": consec_ct, # Number of consecutive days we've logged in. } def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: @@ -747,3 +768,9 @@ class Mai2Base: "nextIndex": next_index, "userMusicList": [{"userMusicDetailList": music_detail_list}], } + + def handle_upload_user_portrait_api_request(self, data: Dict) -> Dict: + self.logger.debug(data) + + def handle_upload_user_photo_api_request(self, data: Dict) -> Dict: + self.logger.debug(data) \ No newline at end of file diff --git a/titles/mai2/read.py b/titles/mai2/read.py index a7e10de..4ff401d 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -37,7 +37,7 @@ class Mai2Reader(BaseReader): def read(self) -> None: data_dirs = [] - if self.version < Mai2Constants.VER_MAIMAI: + if self.version >= Mai2Constants.VER_MAIMAI_DX: if self.bin_dir is not None: data_dirs += self.get_data_directories(self.bin_dir) @@ -52,7 +52,6 @@ class Mai2Reader(BaseReader): self.read_tickets(f"{dir}/ticket") else: - self.logger.warn("Pre-DX Readers are not yet implemented!") if not os.path.exists(f"{self.bin_dir}/tables"): self.logger.error(f"tables directory not found in {self.bin_dir}") return diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 54bda21..916be20 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -461,6 +461,17 @@ recent_rating = Table( mysql_charset="utf8mb4", ) +consec_logins = Table( + "mai2_profile_consec_logins", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("version", Integer, nullable=False), + Column("logins", Integer), + UniqueConstraint("user", "version", name="mai2_profile_consec_logins_uk"), + mysql_charset="utf8mb4", +) + class Mai2ProfileData(BaseData): def put_profile_detail( self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True @@ -764,4 +775,43 @@ class Mai2ProfileData(BaseData): result = self.execute(sql) if result is None: return None - return result.fetchone() \ No newline at end of file + return result.fetchone() + + def add_consec_login(self, user_id: int, version: int) -> None: + sql = insert(consec_logins).values( + user=user_id, + version=version, + logins=1 + ) + + conflict = sql.on_duplicate_key_update( + logins=consec_logins.c.logins + 1 + ) + + result = self.execute(conflict) + if result is None: + self.logger.error(f"Failed to update consecutive login count for user {user_id} version {version}") + + def get_consec_login(self, user_id: int, version: int) -> Optional[Row]: + sql = select(consec_logins).where(and_( + consec_logins.c.user==user_id, + consec_logins.c.version==version, + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def reset_consec_login(self, user_id: int, version: int) -> Optional[Row]: + sql = consec_logins.update(and_( + consec_logins.c.user==user_id, + consec_logins.c.version==version, + )).values( + logins=1 + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 6b25d14..181a895 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -362,4 +362,4 @@ class Mai2ScoreData(BaseData): result = self.execute(sql) if result is None: return None - return result.fetchone() + return result.fetchall() From a89247cdd60d0d6add2bd2b773f027b34eee0bd9 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 2 Jul 2023 02:33:45 -0400 Subject: [PATCH 39/40] wacca: add note about VIP rewards --- docs/game_specific_info.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 0b522aa..d5d1eff 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -421,6 +421,41 @@ Always make sure your database (tables) are up-to-date, to do so go to the `core python dbutils.py --game SDFE upgrade ``` +### VIP Rewards +Below is a list of VIP rewards. Currently, VIP is not implemented, and thus these are not obtainable. These 23 rewards were distributed once per month for VIP users on the real network. + + Plates: + 211004 リッチ + 211018 特盛えりざべす + 211025 イースター + 211026 特盛りりぃ + 311004 ファンシー + 311005 インカンテーション + 311014 夜明け + 311015 ネイビー + 311016 特盛るーん + + Ring Colors: + 203002 Gold Rushイエロー + 203009 トロピカル + 303005 ネイチャー + + Icons: + 202020 どらみんぐ + 202063 ユニコーン + 202086 ゴリラ + 302014 ローズ + 302015 ファラオ + 302045 肉球 + 302046 WACCA + 302047 WACCA Lily + 302048 WACCA Reverse + + Note Sound Effect: + 205002 テニス + 205008 シャワー + 305003 タンバリンMk-Ⅱ + ## SAO ### SDEW From 432177957a282bc92fc1a7c1ef407a680162ed6f Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 2 Jul 2023 02:42:49 -0400 Subject: [PATCH 40/40] pokken: save most profile data --- titles/pokken/base.py | 49 +++++++++++++++++++++++++--- titles/pokken/schema/profile.py | 57 +++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 7 deletions(-) diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 50bc760..0b849b3 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -8,6 +8,7 @@ from core import CoreConfig from .config import PokkenConfig from .proto import jackal_pb2 from .database import PokkenData +from .const import PokkenConstants class PokkenBase: @@ -301,11 +302,11 @@ class PokkenBase: battle = req.battle_data mon = req.pokemon_data - self.data.profile.update_support_team(user_id, 1, req.support_set_1[0], req.support_set_1[1]) - self.data.profile.update_support_team(user_id, 2, req.support_set_2[0], req.support_set_2[1]) - self.data.profile.update_support_team(user_id, 3, req.support_set_3[0], req.support_set_3[1]) + p = self.data.profile.touch_profile(user_id) + if p is None or not p: + self.data.profile.create_profile(user_id) - if req.trainer_name_pending: # we're saving for the first time + if req.trainer_name_pending is not None and req.trainer_name_pending: # we're saving for the first time self.data.profile.set_profile_name(user_id, req.trainer_name_pending, req.avatar_gender if req.avatar_gender else None) for tut_flg in req.tutorial_progress_flag: @@ -328,6 +329,46 @@ class PokkenBase: for reward in req.reward_data: self.data.item.add_reward(user_id, reward.get_category_id, reward.get_content_id, reward.get_type_id) + + self.data.profile.add_profile_points(user_id, get_rank_pts, get_money, get_score_pts, grade_max) + + self.data.profile.update_support_team(user_id, 1, req.support_set_1[0], req.support_set_1[1]) + self.data.profile.update_support_team(user_id, 2, req.support_set_2[0], req.support_set_2[1]) + self.data.profile.update_support_team(user_id, 3, req.support_set_3[0], req.support_set_3[1]) + + self.data.profile.put_pokemon(user_id, mon.char_id, mon.illustration_book_no, mon.bp_point_atk, mon.bp_point_res, mon.bp_point_def, mon.bp_point_sp) + self.data.profile.add_pokemon_xp(user_id, mon.char_id, mon.get_pokemon_exp) + + for x in range(len(battle.play_mode)): + self.data.profile.put_pokemon_battle_result( + user_id, + mon.char_id, + PokkenConstants.BATTLE_TYPE(battle.play_mode[x]), + PokkenConstants.BATTLE_RESULT(battle.result[x]) + ) + + self.data.profile.put_stats( + user_id, + battle.ex_ko_num, + battle.wko_num, + battle.timeup_win_num, + battle.cool_ko_num, + battle.perfect_ko_num, + num_continues + ) + + self.data.profile.put_extra( + user_id, + extra_counter, + evt_reward_get_flg, + total_play_days, + awake_num, + use_support_ct, + beat_num, + aid_skill, + last_evt + ) + return res.SerializeToString() diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index 94e15e8..812964d 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -137,6 +137,14 @@ pokemon_data = Table( class PokkenProfileData(BaseData): + def touch_profile(self, user_id: int) -> Optional[int]: + sql = select([profile.c.id]).where(profile.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone()['id'] + def create_profile(self, user_id: int) -> Optional[int]: sql = insert(profile).values(user=user_id) conflict = sql.on_duplicate_key_update(user=user_id) @@ -158,6 +166,33 @@ class PokkenProfileData(BaseData): f"Failed to update pokken profile name for user {user_id}!" ) + def put_extra( + self, + user_id: int, + extra_counter: int, + evt_reward_get_flg: int, + total_play_days: int, + awake_num: int, + use_support_ct: int, + beat_num: int, + aid_skill: int, + last_evt: int + ) -> None: + sql = update(profile).where(profile.c.user == user_id).values( + extra_counter=extra_counter, + event_reward_get_flag=evt_reward_get_flg, + total_play_days=total_play_days, + awake_num=awake_num, + use_support_num=use_support_ct, + beat_num=beat_num, + aid_skill=aid_skill, + last_play_event_id=last_evt + ) + + result = self.execute(sql) + if result is None: + self.logger.error(f"Failed to put extra data for user {user_id}") + def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: List) -> None: sql = update(profile).where(profile.c.user == user_id).values( tutorial_progress_flag=tutorial_flags, @@ -192,9 +227,14 @@ class PokkenProfileData(BaseData): ) def add_profile_points( - self, user_id: int, rank_pts: int, money: int, score_pts: int + self, user_id: int, rank_pts: int, money: int, score_pts: int, grade_max: int ) -> None: - pass + sql = update(profile).where(profile.c.user == user_id).values( + trainer_rank_point = profile.c.trainer_rank_point + rank_pts, + fight_money = profile.c.fight_money + money, + score_point = profile.c.score_point + score_pts, + grade_max_num = grade_max + ) def get_profile(self, user_id: int) -> Optional[Row]: sql = profile.select(profile.c.user == user_id) @@ -294,7 +334,18 @@ class PokkenProfileData(BaseData): """ Records profile stats """ - pass + sql = update(profile).where(profile.c.user==user_id).values( + ex_ko_num=profile.c.ex_ko_num + exkos, + wko_num=profile.c.wko_num + wkos, + timeup_win_num=profile.c.timeup_win_num + timeout_wins, + cool_ko_num=profile.c.cool_ko_num + cool_kos, + perfect_ko_num=profile.c.perfect_ko_num + perfects, + continue_num=continues, + ) + + result = self.execute(sql) + if result is None: + self.logger.warn(f"Failed to update stats for user {user_id}") def update_support_team(self, user_id: int, support_id: int, support1: int = 4294967295, support2: int = 4294967295) -> None: sql = update(profile).where(profile.c.user==user_id).values(