From 3acc2dc197a73e96d33d16de744199d2d8cae4b9 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Fri, 3 Mar 2023 23:46:29 +0100 Subject: [PATCH 1/4] Initial Card Maker ONGEKI support --- core/data/schema/versions/SDDT_3_rollback.sql | 1 + core/data/schema/versions/SDDT_4_upgrade.sql | 1 + core/data/schema/versions/SDED_1_upgrade.sql | 99 +++ example_config/cardmaker.yaml | 3 + example_config/ongeki.yaml | 23 + titles/chuni/base.py | 5 +- titles/cm/__init__.py | 15 + titles/cm/base.py | 84 +++ titles/cm/cm_data/MU3/static_cards.csv | Bin 0 -> 333451 bytes titles/cm/cm_data/MU3/static_gacha_cards.csv | Bin 0 -> 6759 bytes titles/cm/cm_data/MU3/static_gachas.csv | Bin 0 -> 5458 bytes titles/cm/config.py | 19 + titles/cm/const.py | 10 + titles/cm/index.py | 91 +++ titles/cm/read.py | 199 ++++++ titles/mai2/base.py | 9 +- titles/ongeki/bright.py | 609 +++++++++++++++++- titles/ongeki/config.py | 18 +- titles/ongeki/const.py | 18 +- titles/ongeki/schema/item.py | 129 +++- titles/ongeki/schema/profile.py | 3 +- titles/ongeki/schema/static.py | 203 ++++++ 22 files changed, 1526 insertions(+), 13 deletions(-) create mode 100644 core/data/schema/versions/SDDT_3_rollback.sql create mode 100644 core/data/schema/versions/SDDT_4_upgrade.sql create mode 100644 core/data/schema/versions/SDED_1_upgrade.sql create mode 100644 example_config/cardmaker.yaml create mode 100644 titles/cm/__init__.py create mode 100644 titles/cm/base.py create mode 100644 titles/cm/cm_data/MU3/static_cards.csv create mode 100644 titles/cm/cm_data/MU3/static_gacha_cards.csv create mode 100644 titles/cm/cm_data/MU3/static_gachas.csv create mode 100644 titles/cm/config.py create mode 100644 titles/cm/const.py create mode 100644 titles/cm/index.py create mode 100644 titles/cm/read.py diff --git a/core/data/schema/versions/SDDT_3_rollback.sql b/core/data/schema/versions/SDDT_3_rollback.sql new file mode 100644 index 0000000..a248ce3 --- /dev/null +++ b/core/data/schema/versions/SDDT_3_rollback.sql @@ -0,0 +1 @@ +ALTER TABLE ongeki_profile_data DROP COLUMN lastEmoneyCredit; \ No newline at end of file diff --git a/core/data/schema/versions/SDDT_4_upgrade.sql b/core/data/schema/versions/SDDT_4_upgrade.sql new file mode 100644 index 0000000..45510c2 --- /dev/null +++ b/core/data/schema/versions/SDDT_4_upgrade.sql @@ -0,0 +1 @@ +ALTER TABLE ongeki_profile_data ADD COLUMN lastEmoneyCredit INTEGER DEFAULT 0; diff --git a/core/data/schema/versions/SDED_1_upgrade.sql b/core/data/schema/versions/SDED_1_upgrade.sql new file mode 100644 index 0000000..c601745 --- /dev/null +++ b/core/data/schema/versions/SDED_1_upgrade.sql @@ -0,0 +1,99 @@ +CREATE TABLE ongeki_user_gacha ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + user INT NOT NULL, + gachaId INT NOT NULL, + totalGachaCnt INT DEFAULT 0, + ceilingGachaCnt INT DEFAULT 0, + selectPoint INT DEFAULT 0, + useSelectPoint INT DEFAULT 0, + dailyGachaCnt INT DEFAULT 0, + fiveGachaCnt INT DEFAULT 0, + elevenGachaCnt INT DEFAULT 0, + dailyGachaDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT ongeki_user_gacha_uk UNIQUE (user, gachaId), + CONSTRAINT ongeki_user_gacha_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user (id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE ongeki_user_gacha_supply ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + user INT NOT NULL, + cardId INT NOT NULL, + CONSTRAINT ongeki_user_gacha_supply_uk UNIQUE (user, cardId), + CONSTRAINT ongeki_user_gacha_supply_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user (id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE ongeki_static_gachas ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + version INT NOT NULL, + gachaId INT NOT NULL, + gachaName VARCHAR(255) NOT NULL, + kind INT NOT NULL, + type INT DEFAULT 0, + isCeiling BOOLEAN DEFAULT 0, + maxSelectPoint INT DEFAULT 0, + ceilingCnt INT DEFAULT 10, + changeRateCnt1 INT DEFAULT 0, + changeRateCnt2 INT DEFAULT 0, + startDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0', + endDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0', + noticeStartDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0', + noticeEndDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0', + convertEndDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0', + CONSTRAINT ongeki_static_gachas_uk UNIQUE (version, gachaId, gachaName) +); + +CREATE TABLE ongeki_static_gacha_cards ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + gachaId INT NOT NULL, + cardId INT NOT NULL, + rarity INT NOT NULL, + weight INT DEFAULT 1, + isPickup BOOLEAN DEFAULT 0, + isSelect BOOLEAN DEFAULT 1, + CONSTRAINT ongeki_static_gacha_cards_uk UNIQUE (gachaId, cardId) +); + + +CREATE TABLE ongeki_static_cards ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + version INT NOT NULL, + cardId INT NOT NULL, + name VARCHAR(255) NOT NULL, + charaId INT NOT NULL, + nickName VARCHAR(255), + school VARCHAR(255) NOT NULL, + attribute VARCHAR(5) NOT NULL, + gakunen VARCHAR(255) NOT NULL, + rarity INT NOT NULL, + levelParam VARCHAR(255) NOT NULL, + skillId INT NOT NULL, + choKaikaSkillId INT NOT NULL, + cardNumber VARCHAR(255), + CONSTRAINT ongeki_static_cards_uk UNIQUE (version, cardId) +) CHARACTER SET utf8mb4; + +CREATE TABLE ongeki_user_print_detail ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + user INT NOT NULL, + cardId INT NOT NULL, + cardType INT DEFAULT 0, + printDate TIMESTAMP NOT NULL, + serialId VARCHAR(20) NOT NULL, + placeId INT NOT NULL, + clientId VARCHAR(11) NOT NULL, + printerSerialId VARCHAR(20) NOT NULL, + isHolograph BOOLEAN DEFAULT 0, + isAutographed BOOLEAN DEFAULT 0, + printOption1 BOOLEAN DEFAULT 1, + printOption2 BOOLEAN DEFAULT 1, + printOption3 BOOLEAN DEFAULT 1, + printOption4 BOOLEAN DEFAULT 1, + printOption5 BOOLEAN DEFAULT 1, + printOption6 BOOLEAN DEFAULT 1, + printOption7 BOOLEAN DEFAULT 1, + printOption8 BOOLEAN DEFAULT 1, + printOption9 BOOLEAN DEFAULT 1, + printOption10 BOOLEAN DEFAULT 0, + FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT ongeki_user_print_detail_uk UNIQUE (serialId) +) CHARACTER SET utf8mb4; \ No newline at end of file diff --git a/example_config/cardmaker.yaml b/example_config/cardmaker.yaml new file mode 100644 index 0000000..a04dda5 --- /dev/null +++ b/example_config/cardmaker.yaml @@ -0,0 +1,3 @@ +server: + enable: True + loglevel: "info" diff --git a/example_config/ongeki.yaml b/example_config/ongeki.yaml index a04dda5..6ebd18a 100644 --- a/example_config/ongeki.yaml +++ b/example_config/ongeki.yaml @@ -1,3 +1,26 @@ server: enable: True loglevel: "info" + +gachas: + enabled_gachas: + - 1011 + - 1012 + - 1043 + - 1067 + - 1068 + - 1069 + - 1070 + - 1071 + - 1072 + - 1073 + - 1074 + - 1075 + - 1076 + - 1077 + - 1081 + - 1085 + - 1089 + - 1104 + - 1111 + - 1135 \ No newline at end of file diff --git a/titles/chuni/base.py b/titles/chuni/base.py index bf32f63..d557583 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -20,7 +20,10 @@ class ChuniBase(): self.logger = logging.getLogger("chuni") self.game = ChuniConstants.GAME_CODE self.version = ChuniConstants.VER_CHUNITHM - + + def handle_ping_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + def handle_game_login_api_request(self, data: Dict) -> Dict: #self.data.base.log_event("chuni", "login", logging.INFO, {"version": self.version, "user": data["userId"]}) return { "returnCode": 1 } diff --git a/titles/cm/__init__.py b/titles/cm/__init__.py new file mode 100644 index 0000000..040bec7 --- /dev/null +++ b/titles/cm/__init__.py @@ -0,0 +1,15 @@ +from titles.cm.index import CardMakerServlet +from titles.cm.const import CardMakerConstants +from titles.cm.read import CardMakerReader + +index = CardMakerServlet +reader = CardMakerReader + +use_default_title = True +include_protocol = True +title_secure = False +game_codes = [CardMakerConstants.GAME_CODE] +trailing_slash = True +use_default_host = True + +current_schema_version = 1 diff --git a/titles/cm/base.py b/titles/cm/base.py new file mode 100644 index 0000000..c22fa41 --- /dev/null +++ b/titles/cm/base.py @@ -0,0 +1,84 @@ +from datetime import date, datetime, timedelta +from typing import Any, Dict, List +import json +import logging +from enum import Enum + +from core.config import CoreConfig +from core.data.cache import cached +from titles.cm.const import CardMakerConstants +from titles.cm.config import CardMakerConfig + + +class CardMakerBase(): + def __init__(self, core_cfg: CoreConfig, game_cfg: CardMakerConfig) -> None: + self.core_cfg = core_cfg + self.game_cfg = game_cfg + self.date_time_format = "%Y-%m-%d %H:%M:%S" + self.date_time_format_ext = "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5] + self.date_time_format_short = "%Y-%m-%d" + self.logger = logging.getLogger("cardmaker") + self.game = CardMakerConstants.GAME_CODE + self.version = CardMakerConstants.VER_CARD_MAKER + + def handle_get_game_connect_api_request(self, data: Dict) -> Dict: + uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}" + + # CHUNITHM = 0, maimai = 1, ONGEKI = 2 + return { + "length": 3, + "gameConnectList": [ + { + "modelKind": 0, + "type": 1, + "titleUri": f"{uri}/SDHD/200/" + }, + { + "modelKind": 1, + "type": 1, + "titleUri": f"{uri}/SDEZ/120/" + }, + { + "modelKind": 2, + "type": 1, + "titleUri": f"{uri}/SDDT/130/" + } + ] + } + + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + reboot_start = date.strftime(datetime.now() + timedelta(hours=3), self.date_time_format) + reboot_end = date.strftime(datetime.now() + timedelta(hours=4), self.date_time_format) + + return { + "gameSetting": { + "dataVersion": "1.30.00", + "ongekiCmVersion": "1.30.01", + "chuniCmVersion": "2.00.00", + "maimaiCmVersion": "1.20.00", + "requestInterval": 10, + "rebootStartTime": reboot_start, + "rebootEndTime": reboot_end, + "maxCountCharacter": 100, + "maxCountItem": 100, + "maxCountCard": 100, + "watermark": False, + "isMaintenance": False, + "isBackgroundDistribute": False + }, + "isDumpUpload": False, + "isAou": False + } + + def handle_get_client_bookkeeping_api_request(self, data: Dict) -> Dict: + return { + "placeId": data["placeId"], + "length": 0, + "clientBookkeepingList": [] + } + + def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} + + def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1, "apiName": "UpsertClientBookkeepingApi"} diff --git a/titles/cm/cm_data/MU3/static_cards.csv b/titles/cm/cm_data/MU3/static_cards.csv new file mode 100644 index 0000000000000000000000000000000000000000..53e6e566e85af902acb7fa78d5213b9fef257331 GIT binary patch literal 333451 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 z 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 literal 0 HcmV?d00001 diff --git a/titles/cm/cm_data/MU3/static_gacha_cards.csv b/titles/cm/cm_data/MU3/static_gacha_cards.csv new file mode 100644 index 0000000000000000000000000000000000000000..303e71a710e45c9db10e70441483bcd779422c8f GIT binary patch literal 6759 zcmZvgOO9nH42I`fMvd=)*MW1EkXfXxpj6%JE+vDg(@~V&$6y|0&{zv5Bub|yzYGbinZYP(rmn*S3xpsLQp=n%}jntysUrCo^?b7&7-R_B#OEVu+E;I@c=V-E+ z9uuo)#HBvy$6)d9nd~_lx`XB7)jXnJ6Nl?W6zC@HbxOmR2wT+%u2@iF>7~lav@-?u^bf}_$D?ee#>8B6=T4xM9jIa zhjCU`AkNHU^KvP9mNVv(b?bvQPRZ$&Ml}v`87#HxSgR&3qKkRc^-QdIy$kD^sy5}kdB-(amq z#{$^}-()@G8?Sd^-(b6`dfk#uOl3XovBM8xCRT$*(N_V!T7~-w#ub< zMYqfBG3RKocwN|=SS}HxO-YXsMs_{x{%&$q75ARt|Ed=WWw1 zJ$gGk_(rkX<#P6W&eWT{+-WafHx;j&inp7tL;S9T?zp3@Z#Db9)O@mWh}GqF)8*q= z4{@o7oYzCF8Xe<3ewFS^&-`BM=sBCe#pkk8-*D?4YPTM0qtYGab$zVrq5i7P<>+f? zW_%MlV}qI9i^!Sy2{Y$In2CWf6Dwvmros;UvpHfpTUWxo?v`_rGjSGX>Ohz|3xjPn zDa_kVG!sHDMb6tz_H2pV!R{fy+nHuvnqzmn*`CWGxAhWXUN_m^BsP35ldWfwJH)`& zF3Z^-%j~e{WY4t79rkSdf$YWCWU}Xy-cA=jSxq!6uXCboh6#hc9!WZ?5sqSUwQv(F2}o@*?4#Nm?G0$0=soSos6QT zJsX+&3vA4P#s_V1sQnM2Ce)2r4($V>aI;z-E4d&AJCh z=bUQ;*}=xFE0CLK3mL{#_S_EZ(Y)d8&Nn-(_vkxnu~pB=^u3Ah?ueHV$=fzs+&Z*WI($Tjvs)kIQqYq38DgKC|!QdOnwJ z_Jb)0-=&bbdL}s+17a@c#9ZuSrn_v5U%ciwvSs(8?;uWYem6rozrV}wZ+wGVvFh(% z3LMP`A>dm`8O%tL1W3+&ubWKm(t49=zweCS$)SFJl>|pocuj!ql zYkH?L?46Urd7F%Vqsw^uRx>luGnw=%NB<5iU2(JKf za7=hZ0&x=z3IC;IjpP-7p?8KyB$1kok3p6#&Gk3)-S2*P^ z@!ABgQv~M%7WO zjmQUwtXs21wpSo5|H9aQCXWCHR6b^`)k$Jal;npuf|zsU74RhOxX8b`gOx`(Ou`ro->056L&-xWujSY0aZ zR?wEst{BfIh%v4k#hiRlpzWDBn4hzivV788!CbcVuDZbMuSm5NRkQM8fWX$)fwiAv zGjEN9_a5_2&MKgkZL>$!`$8dm>9>Nh)l|V)(ug%yN93TZnR~_F?-~355Vn|Muj(KL zh*>#UVudBbR;J`94pth&ThPDi_H(vB?V;L%AcLTU_C_c_lzyG!Tzk#ifp*Mqu&>M| zUR+039n~VM_}7tCN3lo)@PuB}iu7nMMJ_#)#z1x*awZMHCFg=&(xZ9Lyx@i8XeGd# zrHh)<7|@&B?QJ)WcQ09Lm1-NpOW^1vkGtC|0fULh)h83&S?uJE#WkwVqJuUxT!8|n@sc~M zE~qeyZAR2vk?JW?eps^qwIj|(tOP(h<9Lqua1G7{oqU6;X_``5dI2_4q;LhU3qF#n zYgC8Xfzd-Zz@1a#Ze>t_=bEg;f)T()OsfO~U?m{||n0=k~pB0w2rLd24Hld%2pCa8K@l z2@O6VZSi=F?-U|~k6oi(@~II!gAizj?jL$yOZ zAfWn)JP69St^00!xTCvMdTq@SNaA None: + self.__config = parent_config + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field(self.__config, 'cardmaker', 'server', 'enable', default=True) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'cardmaker', 'server', 'loglevel', default="info")) + + +class CardMakerConfig(dict): + def __init__(self) -> None: + self.server = CardMakerServerConfig(self) diff --git a/titles/cm/const.py b/titles/cm/const.py new file mode 100644 index 0000000..f59af5d --- /dev/null +++ b/titles/cm/const.py @@ -0,0 +1,10 @@ +class CardMakerConstants(): + GAME_CODE = "SDED" + + VER_CARD_MAKER = 0 + + VERSION_NAMES = ["Card Maker 1.34"] + + @classmethod + def game_ver_to_string(cls, ver: int): + return cls.VERSION_NAMES[ver] diff --git a/titles/cm/index.py b/titles/cm/index.py new file mode 100644 index 0000000..6ba7382 --- /dev/null +++ b/titles/cm/index.py @@ -0,0 +1,91 @@ +from twisted.web.http import Request +import json +import inflection +import yaml +import string +import logging +import coloredlogs +import zlib +from logging.handlers import TimedRotatingFileHandler + +from core.config import CoreConfig +from titles.cm.config import CardMakerConfig +from titles.cm.const import CardMakerConstants +from titles.cm.base import CardMakerBase + + +class CardMakerServlet(): + def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: + self.core_cfg = core_cfg + self.game_cfg = CardMakerConfig() + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/cardmaker.yaml"))) + + self.versions = [ + CardMakerBase(core_cfg, self.game_cfg) + ] + + self.logger = logging.getLogger("cardmaker") + log_fmt_str = "[%(asctime)s] Card Maker | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cardmaker"), encoding='utf8', + when="d", backupCount=10) + + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + 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) + + def render_POST(self, request: Request, version: int, url_path: str) -> bytes: + req_raw = request.content.getvalue() + url_split = url_path.split("/") + internal_ver = 0 + endpoint = url_split[len(url_split) - 1] + + print(f"version: {version}") + + if version >= 130 and version < 135: # Card Maker + internal_ver = CardMakerConstants.VER_CARD_MAKER + + 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 + # technically not 0 + self.logger.error("Encryption not supported at this time") + + try: + unzip = zlib.decompress(req_raw) + + except zlib.error as e: + self.logger.error(f"Failed to decompress v{version} {endpoint} request -> {e}") + return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + + req_data = json.loads(unzip) + + self.logger.info(f"v{version} {endpoint} request - {req_data}") + + func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + + try: + handler = getattr(self.versions[internal_ver], func_to_find) + resp = handler(req_data) + + except AttributeError as e: + self.logger.warning(f"Unhandled v{version} request {endpoint} - {e}") + return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + + except Exception as e: + self.logger.error(f"Error handling v{version} method {endpoint} - {e}") + return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) + + if resp is None: + resp = {'returnCode': 1} + + self.logger.info(f"Response {resp}") + + return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) diff --git a/titles/cm/read.py b/titles/cm/read.py new file mode 100644 index 0000000..b9cb674 --- /dev/null +++ b/titles/cm/read.py @@ -0,0 +1,199 @@ +from decimal import Decimal +import logging +import os +import re +import csv +import xml.etree.ElementTree as ET +from typing import Any, Dict, List, Optional + +from read import BaseReader +from core.config import CoreConfig +from titles.ongeki.database import OngekiData +from titles.cm.const import CardMakerConstants +from titles.ongeki.const import OngekiConstants +from titles.ongeki.config import OngekiConfig + + +class CardMakerReader(BaseReader): + def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], + opt_dir: Optional[str], extra: Optional[str]) -> None: + super().__init__(config, version, bin_dir, opt_dir, extra) + self.ongeki_data = OngekiData(config) + + try: + self.logger.info( + f"Start importer for {CardMakerConstants.game_ver_to_string(version)}") + except IndexError: + self.logger.error(f"Invalid ongeki version {version}") + exit(1) + + def read(self) -> None: + static_datas = { + "static_cards.csv": "read_ongeki_card_csv", + "static_gachas.csv": "read_ongeki_gacha_csv", + "static_gacha_cards.csv": "read_ongeki_gacha_card_csv" + } + + if self.bin_dir is not None: + for file, func in static_datas.items(): + if os.path.exists(f"{self.bin_dir}/MU3/{file}"): + read_csv = getattr(CardMakerReader, func) + read_csv(self, f"{self.bin_dir}/MU3/{file}") + else: + self.logger.warn(f"Couldn't find {file} file in {self.bin_dir}, skipping") + + if self.opt_dir is not None: + dir = self.get_data_directories(self.opt_dir) + + # ONGEKI (MU3) cnnot easily access the bin data(A000.pac) + # so only opt_dir will work for now + self.read_gacha(f"{dir}/MU3/gacha") + self.read_card(f"{dir}/MU3/card") + + def read_ongeki_card_csv(self, file_path: str) -> None: + self.logger.info(f"Reading cards from {file_path}...") + + with open(file_path, encoding="utf-8") as f: + reader = csv.DictReader(f) + for row in reader: + self.ongeki_data.static.put_card( + row["version"], + row["cardId"], + name=row["name"], + charaId=row["charaId"], + nickName=row["nickName"] if row["nickName"] != "" else None, + school=row["school"], + attribute=row["attribute"], + gakunen=row["gakunen"], + rarity=row["rarity"], + levelParam=row["levelParam"], + skillId=row["skillId"], + choKaikaSkillId=row["choKaikaSkillId"], + cardNumber=row["cardNumber"] if row["cardNumber"] != "" else None + ) + + self.logger.info(f"Added card {row['cardId']}") + + def read_ongeki_gacha_csv(self, file_path: str) -> None: + self.logger.info(f"Reading gachas from {file_path}...") + + with open(file_path, encoding="utf-8") as f: + reader = csv.DictReader(f) + for row in reader: + self.ongeki_data.static.put_gacha( + row["version"], + row["gachaId"], + row["gachaName"], + row["kind"], + type=row["type"], + isCeiling=True if row["isCeiling"] == "1" else False, + maxSelectPoint=row["maxSelectPoint"], + ceilingCnt=row["ceilingCnt"], + changeRateCnt1=row["changeRateCnt1"], + changeRateCnt2=row["changeRateCnt2"] + ) + + self.logger.info(f"Added gacha {row['gachaId']}") + + def read_ongeki_gacha_card_csv(self, file_path: str) -> None: + self.logger.info(f"Reading gacha cards from {file_path}...") + + with open(file_path, encoding="utf-8") as f: + reader = csv.DictReader(f) + for row in reader: + self.ongeki_data.static.put_gacha_card( + row["gachaId"], + row["cardId"], + rarity=row["rarity"], + weight=row["weight"], + isPickup=True if row["isPickup"] == "1" else False, + isSelect=True if row["isSelect"] == "1" else False + ) + + self.logger.info(f"Added card {row['cardId']} to gacha") + + def read_ongeki_card(self, base_dir: str) -> None: + self.logger.info(f"Reading cards from {base_dir}...") + + version_ids = { + '1000': OngekiConstants.VER_ONGEKI, + '1005': OngekiConstants.VER_ONGEKI_PLUS, + '1010': OngekiConstants.VER_ONGEKI_SUMMER, + '1015': OngekiConstants.VER_ONGEKI_SUMMER_PLUS, + '1020': OngekiConstants.VER_ONGEKI_RED, + '1025': OngekiConstants.VER_ONGEKI_RED_PLUS, + '1030': OngekiConstants.VER_ONGEKI_BRIGHT, + '1035': OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY + } + + for root, dirs, files in os.walk(base_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Card.xml"): + with open(f"{root}/{dir}/Card.xml", "r", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + card_id = int(troot.find('Name').find('id').text) + name = troot.find('Name').find('str').text + chara_id = int(troot.find('CharaID').find('id').text) + nick_name = troot.find('NickName').text + school = troot.find('School').find('str').text + attribute = troot.find('Attribute').text + gakunen = troot.find('Gakunen').find('str').text + rarity = OngekiConstants.RARITY_TYPES[ + troot.find('Rarity').text].value + + level_param = [] + for lvl in troot.find('LevelParam').findall('int'): + level_param.append(lvl.text) + + skill_id = int(troot.find('SkillID').find('id').text) + cho_kai_ka_skill_id = int(troot.find('ChoKaikaSkillID').find('id').text) + + version = version_ids[ + troot.find('VersionID').find('id').text] + card_number = troot.find('CardNumberString').text + + self.ongeki_data.static.put_card( + version, + card_id, + name=name, + charaId=chara_id, + nickName=nick_name, + school=school, + attribute=attribute, + gakunen=gakunen, + rarity=rarity, + levelParam=','.join(level_param), + skillId=skill_id, + choKaikaSkillId=cho_kai_ka_skill_id, + cardNumber=card_number + ) + self.logger.info(f"Added card {card_id}") + + def read_ongeki_gacha(self, base_dir: str) -> None: + self.logger.info(f"Reading gachas from {base_dir}...") + + # assuming some GachaKinds based on the GachaType + type_to_kind = { + "Normal": "Normal", + "Pickup": "Pickup", + "RecoverFiveShotFlag": "BonusRestored", + "Free": "Free", + "FreeSR": "Free" + } + + for root, dirs, files in os.walk(base_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Gacha.xml"): + with open(f"{root}/{dir}/Gacha.xml", "r", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + name = troot.find('Name').find('str').text + id = int(troot.find('Name').find('id').text) + + gacha_kind = OngekiConstants.CM_GACHA_KINDS[ + type_to_kind[troot.find('Type').text]].value + + self.ongeki_data.static.put_gacha( + self.version, id, name, gacha_kind) + self.logger.info(f"Added gacha {id}") diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 4b0b81c..3817890 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -16,6 +16,9 @@ class Mai2Base(): self.data = Mai2Data(cfg) self.logger = logging.getLogger("mai2") + def handle_ping_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + def handle_get_game_setting_api_request(self, data: Dict): reboot_start = date.strftime(datetime.now() + timedelta(hours=3), Mai2Constants.DATE_TIME_FORMAT) reboot_end = date.strftime(datetime.now() + timedelta(hours=4), Mai2Constants.DATE_TIME_FORMAT) @@ -202,9 +205,9 @@ class Mai2Base(): for fav in upsert["userFavoriteList"]: self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) - if "isNewFriendSeasonRankingList" in upsert and int(upsert["isNewFriendSeasonRankingList"]) > 0: - for fsr in upsert["userFriendSeasonRankingList"]: - pass + # if "isNewFriendSeasonRankingList" in upsert and int(upsert["isNewFriendSeasonRankingList"]) > 0: + # for fsr in upsert["userFriendSeasonRankingList"]: + # pass def handle_user_logout_api_request(self, data: Dict) -> Dict: pass diff --git a/titles/ongeki/bright.py b/titles/ongeki/bright.py index d92ed48..57660b5 100644 --- a/titles/ongeki/bright.py +++ b/titles/ongeki/bright.py @@ -1,5 +1,6 @@ from datetime import date, datetime, timedelta from typing import Any, Dict +from random import randint import pytz import json @@ -8,8 +9,8 @@ from titles.ongeki.base import OngekiBase from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig -class OngekiBright(OngekiBase): +class OngekiBright(OngekiBase): def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_BRIGHT @@ -19,3 +20,609 @@ class OngekiBright(OngekiBase): ret["gameSetting"]["dataVersion"] = "1.30.00" ret["gameSetting"]["onlineDataVersion"] = "1.30.00" return ret + + def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + # first check for a bright memory profile after that check for a + # bright profile + p = (self.data.profile.get_profile_data(data["userId"], self.version+1) + or self.data.profile.get_profile_data(data["userId"], self.version)) + if p is None: + return {} + + 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") + + # TODO: replace datetime objects with strings + + # add access code that we don't store + user_data["accessCode"] = cards[0]["access_code"] + + # hardcode Card Maker version for now + user_data["compatibleCmVersion"] = "1.30.01" + + return {"userId": data["userId"], "userData": user_data} + + def handle_printer_login_api_request(self, data: Dict): + return {"returnCode": 1} + + def handle_printer_logout_api_request(self, data: Dict): + return {"returnCode": 1} + + 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 {} + + 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 = -1 + + card_list = [] + for card in user_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("user") + card_list.append(tmp) + + return { + "userId": data["userId"], + "length": len(card_list[start_idx:end_idx]), + "nextIndex": next_idx, + "userCardList": card_list[start_idx:end_idx] + } + + def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + user_characters = self.data.item.get_characters(data["userId"]) + if user_characters is None: + return {} + + max_ct = data["maxCount"] + next_idx = data["nextIndex"] + start_idx = next_idx + end_idx = max_ct + start_idx + + if len(user_characters[start_idx:]) > max_ct: + next_idx += max_ct + else: + next_idx = -1 + + character_list = [] + for character in user_characters: + tmp = character._asdict() + tmp.pop("id") + tmp.pop("user") + character_list.append(tmp) + + return { + "userId": data["userId"], + "length": len(character_list[start_idx:end_idx]), + "nextIndex": next_idx, + "userCharacterList": character_list[start_idx:end_idx] + } + + def handle_get_user_gacha_api_request(self, data: Dict) -> Dict: + user_gachas = self.data.item.get_user_gachas(data["userId"]) + if user_gachas is None: + return { + "userId": data["userId"], + "length": 0, + "userGachaList": [] + } + + user_gacha_list = [] + for gacha in user_gachas: + tmp = gacha._asdict() + tmp.pop("id") + tmp.pop("user") + tmp["dailyGachaDate"] = datetime.strftime( + tmp["dailyGachaDate"], "%Y-%m-%d") + user_gacha_list.append(tmp) + + return { + "userId": data["userId"], + "length": len(user_gacha_list), + "userGachaList": user_gacha_list + } + + def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + return self.handle_get_user_item_api_request(data) + + def handle_cm_get_user_gacha_supply_api_request(self, data: Dict) -> Dict: + # not used for now? not sure what it even does + user_gacha_supplies = self.data.item.get_user_gacha_supplies( + data["userId"]) + if user_gacha_supplies is None: + return { + "supplyId": 1, + "length": 0, + "supplyCardList": [] + } + + supply_list = [gacha["cardId"] for gacha in user_gacha_supplies] + + return { + "supplyId": 1, + "length": len(supply_list), + "supplyCardList": supply_list + } + + def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: + """ + returns all current active banners (gachas) + "Select Gacha" requires maxSelectPoint set and isCeiling set to 1 + """ + game_gachas = [] + # for every gacha_id in the OngekiConfig, grab the banner from the db + for gacha_id in self.game_cfg.gachas.enabled_gachas: + game_gacha = self.data.static.get_gacha(self.version, gacha_id) + if game_gacha: + game_gachas.append(game_gacha) + + # clean the database rows + game_gacha_list = [] + for gacha in game_gachas: + tmp = gacha._asdict() + tmp.pop("id") + tmp.pop("version") + 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") + tmp["convertEndDate"] = datetime.strftime( + tmp["convertEndDate"], "%Y-%m-%d %H:%M:%S") + + # make sure to only show gachas for the current version + # so only up to bright, 1140 is the first bright memory gacha + if tmp["gachaId"] < 1140: + game_gacha_list.append(tmp) + + return { + "length": len(game_gacha_list), + "gameGachaList": game_gacha_list, + # no clue + "registIdList": [] + } + + def handle_roll_gacha_api_request(self, data: Dict) -> Dict: + """ + Handle a gacha roll API request + """ + gacha_id = data["gachaId"] + num_rolls = data["times"] + # change_rate is the 5 gacha rool SR gurantee once a week + change_rate = data["changeRate"] + # SSR book which guarantees a SSR card, itemKind=15, itemId=1 + book_used = data["bookUseCount"] + if num_rolls not in {1, 5, 11}: + return {} + + # https://gamerch.com/ongeki/entry/462978 + + # 77% chance of gett ing a R card + # 20% chance of getting a SR card + # 3% chance of getting a SSR card + rarity = [1 for _ in range(77)] + rarity += [2 for _ in range(20)] + rarity += [3 for _ in range(3)] + + # gachaId 1011 is "無料ガチャ" (free gacha), which requires GatchaTickets + # itemKind=11, itemId=1 and appearenty sucks + # 94% chance of getting a R card + # 5% chance of getting a SR card + # 1% chance of getting a SSR card + if gacha_id == 1011: + rarity = [1 for _ in range(94)] + rarity += [2 for _ in range(5)] + rarity += [3 for _ in range(1)] + + # gachaId 1012 is "無料ガチャ(SR確定)" (SR confirmed! free gacha), which + # requires GatchaTickets itemKind=11, itemId=4 and always guarantees + # a SR card or higher + # 92% chance of getting a SR card + # 8% chance of getting a SSR card + elif gacha_id == 1012: + rarity = [2 for _ in range(92)] + rarity += [3 for _ in range(8)] + + assert len(rarity) == 100 + + # uniform distribution to get the rarity of the card + rolls = [rarity[randint(0, len(rarity)-1)] for _ in range(num_rolls)] + + # if SSR book used, make sure you always get one SSR + if book_used == 1: + if rolls.count(3) == 0: + # if there is no SSR, re-roll + return self.handle_roll_gacha_api_request(data) + # make sure that 11 rolls always have at least 1 SR or SSR + elif (num_rolls == 5 and change_rate is True) or num_rolls == 11: + if rolls.count(2) == 0 and rolls.count(3) == 0: + # if there is no SR or SSR, re-roll + return self.handle_roll_gacha_api_request(data) + + # get a list of cards for each rarity + cards_r = self.data.static.get_cards_by_rarity(self.version, 1) + cards_sr, cards_ssr = [], [] + + # free gachas are only allowed to get their specific cards! (R irrelevant) + if gacha_id in {1011, 1012}: + gacha_cards = self.data.static.get_gacha_cards(gacha_id) + for card in gacha_cards: + if card["rarity"] == 3: + cards_sr.append({ + "cardId": card["cardId"], + "rarity": 2 + }) + elif card["rarity"] == 4: + cards_ssr.append({ + "cardId": card["cardId"], + "rarity": 3 + }) + else: + cards_sr = self.data.static.get_cards_by_rarity(self.version, 2) + cards_ssr = self.data.static.get_cards_by_rarity(self.version, 3) + + # get the promoted cards for that gacha and add them multiple + # times to increase chances by factor chances + chances = 10 + + gacha_cards = self.data.static.get_gacha_cards(gacha_id) + for card in gacha_cards: + # make sure to add the cards to the corresponding rarity + if card["rarity"] == 2: + cards_r += [{ + "cardId": card["cardId"], + "rarity": 1 + }] * chances + if card["rarity"] == 3: + cards_sr += [{ + "cardId": card["cardId"], + "rarity": 2 + }] * chances + elif card["rarity"] == 4: + cards_ssr += [{ + "cardId": card["cardId"], + "rarity": 3 + }] * chances + + # get the card id for each roll + rolled_cards = [] + for i in range(len(rolls)): + if rolls[i] == 1: + rolled_cards.append(cards_r[randint(0, len(cards_r)-1)]) + elif rolls[i] == 2: + rolled_cards.append(cards_sr[randint(0, len(cards_sr)-1)]) + elif rolls[i] == 3: + rolled_cards.append(cards_ssr[randint(0, len(cards_ssr)-1)]) + + game_gacha_card_list = [] + for card in rolled_cards: + game_gacha_card_list.append({ + "gachaId": data["gachaId"], + "cardId": card["cardId"], + # +1 because Card Maker is weird + "rarity": card["rarity"] + 1, + "weight": 1, + "isPickup": False, + "isSelect": False + }) + + return { + "length": len(game_gacha_card_list), + "gameGachaCardList": game_gacha_card_list + } + + def handle_cm_upsert_user_gacha_api_request(self, data: Dict): + upsert = data["cmUpsertUserGacha"] + user_id = data["userId"] + + gacha_id = data["gachaId"] + gacha_count = data["gachaCnt"] + play_date = datetime.strptime(data["playDate"][:10], '%Y-%m-%d') + select_point = data["selectPoint"] + + total_gacha_count, ceiling_gacha_count = 0, 0 + daily_gacha_cnt, five_gacha_cnt, eleven_gacha_cnt = 0, 0, 0 + daily_gacha_date = datetime.strptime('2000-01-01', '%Y-%m-%d') + + # check if the user previously rolled the exact same gacha + user_gacha = self.data.item.get_user_gacha(user_id, gacha_id) + if user_gacha: + total_gacha_count = user_gacha["totalGachaCnt"] + ceiling_gacha_count = user_gacha["ceilingGachaCnt"] + daily_gacha_cnt = user_gacha["dailyGachaCnt"] + five_gacha_cnt = user_gacha["fiveGachaCnt"] + eleven_gacha_cnt = user_gacha["elevenGachaCnt"] + # parse just the year, month and date + daily_gacha_date = user_gacha["dailyGachaDate"] + + # if the saved dailyGachaDate is different from the roll, + # reset dailyGachaCnt and change the date + if daily_gacha_date != play_date: + daily_gacha_date = play_date + daily_gacha_cnt = 0 + + self.data.item.put_user_gacha( + user_id, + gacha_id, + totalGachaCnt=total_gacha_count + gacha_count, + ceilingGachaCnt=ceiling_gacha_count + gacha_count, + selectPoint=select_point, + useSelectPoint=0, + dailyGachaCnt=daily_gacha_cnt + gacha_count, + fiveGachaCnt=five_gacha_cnt+1 if gacha_count == 5 else five_gacha_cnt, + elevenGachaCnt=eleven_gacha_cnt+1 if gacha_count == 11 else eleven_gacha_cnt, + dailyGachaDate=daily_gacha_date + ) + + if "userData" in upsert and len(upsert["userData"]) > 0: + # check if the profile is a bright memory profile + p = self.data.profile.get_profile_data(data["userId"], self.version+1) + if p is not None: + # save the bright memory profile + self.data.profile.put_profile_data( + user_id, self.version+1, upsert["userData"][0]) + else: + # save the bright profile + self.data.profile.put_profile_data( + user_id, self.version, upsert["userData"][0]) + + if "userCharacterList" in upsert: + for x in upsert["userCharacterList"]: + self.data.item.put_character(user_id, x) + + if "userItemList" in upsert: + for x in upsert["userItemList"]: + self.data.item.put_item(user_id, x) + + if "userCardList" in upsert: + for x in upsert["userCardList"]: + self.data.item.put_card(user_id, x) + + # TODO? + # if "gameGachaCardList" in upsert: + # for x in upsert["gameGachaCardList"]: + + return {'returnCode': 1, 'apiName': 'CMUpsertUserGachaApi'} + + def handle_cm_upsert_user_select_gacha_api_request(self, data: Dict) -> Dict: + upsert = data["cmUpsertUserSelectGacha"] + user_id = data["userId"] + + if "userData" in upsert and len(upsert["userData"]) > 0: + # check if the profile is a bright memory profile + p = self.data.profile.get_profile_data(data["userId"], self.version+1) + if p is not None: + # save the bright memory profile + self.data.profile.put_profile_data( + user_id, self.version+1, upsert["userData"][0]) + else: + # save the bright profile + self.data.profile.put_profile_data( + user_id, self.version, upsert["userData"][0]) + + if "userCharacterList" in upsert: + for x in upsert["userCharacterList"]: + self.data.item.put_character(user_id, x) + + if "userCardList" in upsert: + for x in upsert["userCardList"]: + self.data.item.put_card(user_id, x) + + if "selectGachaLogList" in data: + for x in data["selectGachaLogList"]: + self.data.item.put_user_gacha( + user_id, + x["gachaId"], + selectPoint=0, + useSelectPoint=x["useSelectPoint"] + ) + + return {'returnCode': 1, 'apiName': 'cmUpsertUserSelectGacha'} + + def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: + game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"]) + if game_gacha_cards == []: + # fallback to be at least able to select that gacha + return { + "gachaId": data["gachaId"], + "length": 6, + "isPickup": False, + "gameGachaCardList": [ + { + "gachaId": data["gachaId"], + "cardId": 100984, + "rarity": 4, + "weight": 1, + "isPickup": False, + "isSelect": True + }, + { + "gachaId": data["gachaId"], + "cardId": 100997, + "rarity": 3, + "weight": 2, + "isPickup": False, + "isSelect": True + }, + { + "gachaId": data["gachaId"], + "cardId": 100998, + "rarity": 3, + "weight": 2, + "isPickup": False, + "isSelect": True + }, + { + "gachaId": data["gachaId"], + "cardId": 101020, + "rarity": 2, + "weight": 3, + "isPickup": False, + "isSelect": True + }, + { + "gachaId": data["gachaId"], + "cardId": 101021, + "rarity": 2, + "weight": 3, + "isPickup": False, + "isSelect": True + }, + { + "gachaId": data["gachaId"], + "cardId": 101022, + "rarity": 2, + "weight": 3, + "isPickup": False, + "isSelect": True + } + ], + "emissionList": [], + "afterCalcList": [], + "ssrBookCalcList": [] + } + + game_gacha_card_list = [] + for gacha_card in game_gacha_cards: + tmp = gacha_card._asdict() + tmp.pop("id") + game_gacha_card_list.append(tmp) + + return { + "gachaId": data["gachaId"], + "length": len(game_gacha_card_list), + "isPickup": False, + "gameGachaCardList": game_gacha_card_list, + # again no clue + "emissionList": [], + "afterCalcList": [], + "ssrBookCalcList": [] + } + + def handle_get_game_theater_api_request(self, data: Dict) -> Dict: + """ + shows a banner after every print, not sure what its used for + """ + + """ + return { + "length": 1, + "gameTheaterList": [{ + "theaterId": 1, + "theaterName": "theaterName", + "startDate": "2018-01-01 00:00:00.0", + "endDate": "2038-01-01 00:00:00.0", + "gameSubTheaterList": [{ + "theaterId": 1, + "id": 2, + "no": 4 + }] + } + ], + "registIdList": [] + } + """ + + return { + "length": 0, + "gameTheaterList": [], + "registIdList": [] + } + + def handle_cm_upsert_user_print_playlog_api_request(self, data: Dict) -> Dict: + return { + "returnCode": 1, + "orderId": 0, + "serialId": "11111111111111111111", + "apiName": "CMUpsertUserPrintPlaylogApi" + } + + def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + return { + "returnCode": 1, + "orderId": 0, + "serialId": "11111111111111111111", + "apiName": "CMUpsertUserPrintlogApi" + } + + def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + user_print_detail = data["userPrintDetail"] + + # generate random serial id + serial_id = ''.join([str(randint(0, 9)) for _ in range(20)]) + + # not needed because are either zero or unset + user_print_detail.pop("orderId") + user_print_detail.pop("printNumber") + user_print_detail.pop("serialId") + user_print_detail["printDate"] = datetime.strptime( + user_print_detail["printDate"], '%Y-%m-%d' + ) + + # add the entry to the user print table with the random serialId + self.data.item.put_user_print_detail( + data["userId"], + serial_id, + user_print_detail + ) + + return { + "returnCode": 1, + "serialId": serial_id, + "apiName": "CMUpsertUserPrintApi" + } + + def handle_cm_upsert_user_all_api_request(self, data: Dict) -> Dict: + upsert = data["cmUpsertUserAll"] + user_id = data["userId"] + + if "userData" in upsert and len(upsert["userData"]) > 0: + # check if the profile is a bright memory profile + p = self.data.profile.get_profile_data(data["userId"], self.version+1) + if p is not None: + # save the bright memory profile + self.data.profile.put_profile_data( + user_id, self.version+1, upsert["userData"][0]) + else: + # save the bright profile + self.data.profile.put_profile_data( + user_id, self.version, upsert["userData"][0]) + + if "userActivityList" in upsert: + for act in upsert["userActivityList"]: + self.data.profile.put_profile_activity( + user_id, act["kind"], act["id"], act["sortNumber"], + act["param1"], act["param2"], act["param3"], act["param4"]) + + if "userItemList" in upsert: + for x in upsert["userItemList"]: + self.data.item.put_item(user_id, x) + + if "userCardList" in upsert: + for x in upsert["userCardList"]: + self.data.item.put_card(user_id, x) + + return {'returnCode': 1, 'apiName': 'cmUpsertUserAll'} diff --git a/titles/ongeki/config.py b/titles/ongeki/config.py index 722677c..3a89f29 100644 --- a/titles/ongeki/config.py +++ b/titles/ongeki/config.py @@ -1,17 +1,31 @@ +from typing import List + from core.config import CoreConfig + class OngekiServerConfig(): def __init__(self, parent_config: "OngekiConfig") -> None: self.__config = parent_config - + @property def enable(self) -> bool: return CoreConfig.get_config_field(self.__config, 'ongeki', 'server', 'enable', default=True) - + @property def loglevel(self) -> int: return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'ongeki', 'server', 'loglevel', default="info")) + +class OngekiGachaConfig(): + def __init__(self, parent_config: "OngekiConfig") -> None: + self.__config = parent_config + + @property + def enabled_gachas(self) -> List[int]: + return CoreConfig.get_config_field(self.__config, 'ongeki', 'gachas', 'enabled_gachas', default=[]) + + class OngekiConfig(dict): def __init__(self) -> None: self.server = OngekiServerConfig(self) + self.gachas = OngekiGachaConfig(self) diff --git a/titles/ongeki/const.py b/titles/ongeki/const.py index dd6c0f5..518459a 100644 --- a/titles/ongeki/const.py +++ b/titles/ongeki/const.py @@ -1,5 +1,7 @@ from typing import Final, Dict from enum import Enum + + class OngekiConstants(): GAME_CODE = "SDDT" @@ -35,6 +37,20 @@ class OngekiConstants(): 'SilverJewelEvent', ]) + class CM_GACHA_KINDS(Enum): + Normal = 0 + Pickup = 1 + BonusRestored = 2 + Free = 3 + PickupBonusRestored = 4 + + class RARITY_TYPES(Enum): + N = 0 + R = 1 + SR = 2 + SSR = 3 + SRPlus = 12 + # The game expects the server to give Lunatic an ID of 10, while the game uses 4 internally... except in Music.xml class DIFF_NAME(Enum): Basic = 0 @@ -48,4 +64,4 @@ class OngekiConstants(): @classmethod def game_ver_to_string(cls, ver: int): - return cls.VERSION_NAMES[ver] \ No newline at end of file + return cls.VERSION_NAMES[ver] diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index c3f8c7e..001388a 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -2,6 +2,7 @@ from typing import Dict, Optional, List from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON from sqlalchemy.schema import ForeignKey +from sqlalchemy.engine import Row from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert @@ -242,6 +243,63 @@ tech_event = Table( mysql_charset='utf8mb4' ) +gacha = Table( + "ongeki_user_gacha", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("gachaId", Integer, nullable=False), + Column("totalGachaCnt", Integer, server_default="0"), + Column("ceilingGachaCnt", Integer, server_default="0"), + Column("selectPoint", Integer, server_default="0"), + Column("useSelectPoint", Integer, server_default="0"), + Column("dailyGachaCnt", Integer, server_default="0"), + Column("fiveGachaCnt", Integer, server_default="0"), + Column("elevenGachaCnt", Integer, server_default="0"), + Column("dailyGachaDate", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "gachaId", name="ongeki_user_gacha_uk"), + mysql_charset='utf8mb4' +) + +gacha_supply = Table( + "ongeki_user_gacha_supply", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("cardId", Integer, nullable=False), + UniqueConstraint("user", "cardId", name="ongeki_user_gacha_supply_uk"), + mysql_charset='utf8mb4' +) + + +print_detail = Table( + "ongeki_user_print_detail", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("cardId", Integer, nullable=False), + Column("cardType", Integer, server_default="0"), + Column("printDate", TIMESTAMP, nullable=False), + Column("serialId", String(20), nullable=False), + Column("placeId", Integer, nullable=False), + Column("clientId", String(11), nullable=False), + Column("printerSerialId", String(20), nullable=False), + Column("isHolograph", Boolean, server_default="0"), + Column("isAutographed", Boolean, server_default="0"), + Column("printOption1", Boolean, server_default="1"), + Column("printOption2", Boolean, server_default="1"), + Column("printOption3", Boolean, server_default="1"), + Column("printOption4", Boolean, server_default="1"), + Column("printOption5", Boolean, server_default="1"), + Column("printOption6", Boolean, server_default="1"), + Column("printOption7", Boolean, server_default="1"), + Column("printOption8", Boolean, server_default="1"), + Column("printOption9", Boolean, server_default="1"), + Column("printOption10", Boolean, server_default="0"), + UniqueConstraint("serialId", name="ongeki_user_print_detail_uk"), + mysql_charset='utf8mb4' +) + class OngekiItemData(BaseData): def put_card(self, aime_id: int, card_data: Dict) -> Optional[int]: @@ -545,7 +603,7 @@ class OngekiItemData(BaseData): if result is None: return None return result.fetchall() - + def put_memorychapter(self, aime_id: int, memorychapter_data: Dict) -> Optional[int]: memorychapter_data["user"] = aime_id @@ -557,10 +615,73 @@ class OngekiItemData(BaseData): self.logger.warn(f"put_memorychapter: Failed to update! aime_id: {aime_id}") return None return result.lastrowid - + def get_memorychapters(self, aime_id: int) -> Optional[List[Dict]]: sql = select(memorychapter).where(memorychapter.c.user == aime_id) result = self.execute(sql) - if result is None: return None - return result.fetchall() \ No newline at end of file + if result is None: + return None + return result.fetchall() + + def get_user_gacha(self, aime_id: int, gacha_id: int) -> Optional[Row]: + sql = gacha.select(and_( + gacha.c.user == aime_id, + gacha.c.gachaId == gacha_id + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]: + sql = gacha.select(gacha.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_user_gacha_supplies(self, aime_id: int) -> Optional[List[Row]]: + sql = gacha_supply.select(gacha_supply.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_user_gacha(self, aime_id: int, gacha_id: int, **data) -> Optional[int]: + sql = insert(gacha).values( + user=aime_id, + gachaId=gacha_id, + **data) + + conflict = sql.on_duplicate_key_update( + user=aime_id, + gachaId=gacha_id, + **data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_user_gacha: Failed to insert! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_user_print_detail(self, aime_id: int, serial_id: str, + user_print_data: Dict) -> Optional[int]: + sql = insert(print_detail).values( + user=aime_id, + serialId=serial_id, + **user_print_data) + + conflict = sql.on_duplicate_key_update( + user=aime_id, + serialId=serial_id, + **user_print_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_user_print_detail: Failed to insert! aime_id: {aime_id}") + return None + return result.lastrowid diff --git a/titles/ongeki/schema/profile.py b/titles/ongeki/schema/profile.py index 26c79b5..d7514c6 100644 --- a/titles/ongeki/schema/profile.py +++ b/titles/ongeki/schema/profile.py @@ -78,7 +78,8 @@ profile = Table( Column("overDamageBattlePoint", Integer, server_default="0"), Column("bestBattlePoint", Integer, server_default="0"), Column("lastEmoneyBrand", Integer, server_default="0"), - Column("isDialogWatchedSuggestMemory", Boolean), + Column("lastEmoneyCredit", Integer, server_default="0"), + Column("isDialogWatchedSuggestMemory", Boolean, server_default="0"), UniqueConstraint("user", "version", name="ongeki_profile_profile_uk"), mysql_charset='utf8mb4' ) diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index e98ec58..2e1bc51 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -37,7 +37,210 @@ music = Table( mysql_charset='utf8mb4' ) +gachas = Table( + "ongeki_static_gachas", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("gachaId", Integer, nullable=False), + Column("gachaName", String(255), nullable=False), + Column("type", Integer, nullable=False, server_default="0"), + Column("kind", Integer, nullable=False, server_default="0"), + Column("isCeiling", Boolean, server_default="0"), + Column("maxSelectPoint", Integer, server_default="0"), + Column("ceilingCnt", Integer, server_default="10"), + Column("changeRateCnt1", Integer, server_default="0"), + Column("changeRateCnt2", Integer, server_default="0"), + Column("startDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + Column("convertEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + UniqueConstraint("version", "gachaId", "gachaName", name="ongeki_static_gachas_uk"), + mysql_charset='utf8mb4' +) + +gacha_cards = Table( + "ongeki_static_gacha_cards", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("gachaId", Integer, nullable=False), + Column("cardId", Integer, nullable=False), + Column("rarity", Integer, nullable=False), + Column("weight", Integer, server_default="1"), + Column("isPickup", Boolean, server_default="0"), + Column("isSelect", Boolean, server_default="0"), + UniqueConstraint("gachaId", "cardId", name="ongeki_static_gacha_cards_uk"), + mysql_charset='utf8mb4' +) + +cards = Table( + "ongeki_static_cards", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("cardId", Integer, nullable=False), + Column("name", String(255), nullable=False), + Column("charaId", Integer, nullable=False), + Column("nickName", String(255)), + Column("school", String(255), nullable=False), + Column("attribute", String(5), nullable=False), + Column("gakunen", String(255), nullable=False), + Column("rarity", Integer, nullable=False), + Column("levelParam", String(255), nullable=False), + Column("skillId", Integer, nullable=False), + Column("choKaikaSkillId", Integer, nullable=False), + Column("cardNumber", String(255)), + UniqueConstraint("version", "cardId", name="ongeki_static_cards_uk"), + mysql_charset='utf8mb4' +) + + class OngekiStaticData(BaseData): + def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: + sql = insert(cards).values( + version=version, + cardId=card_id, + **card_data) + + conflict = sql.on_duplicate_key_update(**card_data) + + result = self.execute(conflict) + if result is None: + self.logger.warn(f"Failed to insert card! card_id {card_id}") + return None + return result.lastrowid + + def get_card(self, version: int, card_id: int) -> Optional[Dict]: + sql = cards.select(and_( + cards.c.version <= version, + cards.c.cardId == card_id + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_card_by_card_number(self, version: int, card_number: str) -> Optional[Dict]: + if not card_number.startswith("[O.N.G.E.K.I.]"): + card_number = f"[O.N.G.E.K.I.]{card_number}" + + sql = cards.select(and_( + cards.c.version <= version, + cards.c.cardNumber == card_number + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_card_by_name(self, version: int, name: str) -> Optional[Dict]: + sql = cards.select(and_( + cards.c.version <= version, + cards.c.name == name + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_cards(self, version: int) -> Optional[List[Dict]]: + sql = cards.select(cards.c.version <= version) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_cards_by_rarity(self, version: int, rarity: int) -> Optional[List[Dict]]: + sql = cards.select(and_( + cards.c.version <= version, + cards.c.rarity == rarity + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_gacha(self, version: int, gacha_id: int, gacha_name: int, + gacha_kind: int, **gacha_data) -> Optional[int]: + sql = insert(gachas).values( + version=version, + gachaId=gacha_id, + gachaName=gacha_name, + kind=gacha_kind, + **gacha_data + ) + + conflict = sql.on_duplicate_key_update( + version=version, + gachaId=gacha_id, + gachaName=gacha_name, + kind=gacha_kind, + **gacha_data + ) + + result = self.execute(conflict) + if result is None: + self.logger.warn(f"Failed to insert gacha! gacha_id {gacha_id}") + return None + return result.lastrowid + + def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]: + sql = gachas.select(and_( + gachas.c.version == version, + gachas.c.gachaId == gacha_id + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_gachas(self, version: int) -> Optional[List[Dict]]: + sql = gachas.select( + gachas.c.version == version).order_by( + gachas.c.gachaId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_gacha_card(self, gacha_id: int, card_id: int, **gacha_card) -> Optional[int]: + sql = insert(gacha_cards).values( + gachaId=gacha_id, + cardId=card_id, + **gacha_card + ) + + conflict = sql.on_duplicate_key_update( + gachaId=gacha_id, + cardId=card_id, + **gacha_card + ) + + result = self.execute(conflict) + if result is None: + self.logger.warn(f"Failed to insert gacha card! gacha_id {gacha_id}") + return None + return result.lastrowid + + def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]: + sql = gacha_cards.select( + gacha_cards.c.gachaId == gacha_id + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + def put_event(self, version: int, event_id: int, event_type: int, event_name: str) -> Optional[int]: sql = insert(events).values( version = version, From fe8f40c62718cd0d49aef362b96a2d469cd4a7de Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sat, 4 Mar 2023 11:09:03 +0100 Subject: [PATCH 2/4] Fixed upgrade script endDate --- core/data/schema/versions/SDED_1_upgrade.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/data/schema/versions/SDED_1_upgrade.sql b/core/data/schema/versions/SDED_1_upgrade.sql index c601745..a4d666e 100644 --- a/core/data/schema/versions/SDED_1_upgrade.sql +++ b/core/data/schema/versions/SDED_1_upgrade.sql @@ -35,7 +35,7 @@ CREATE TABLE ongeki_static_gachas ( changeRateCnt1 INT DEFAULT 0, changeRateCnt2 INT DEFAULT 0, startDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0', - endDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0', + endDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0', noticeStartDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0', noticeEndDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0', convertEndDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0', From 74f3ab7c3f4e9ea63fa4ca0cb097c2e3a8e49b33 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sun, 5 Mar 2023 23:54:13 +0100 Subject: [PATCH 3/4] cm: added support for 1.36, fixed importer - Added support for Card Maker 1.36.xx - Added cards importer to ONGEKI importer - Added 4 new 1.36 gachas (requires importing them from opt files) - Fixed version for Card Maker opt importer --- example_config/ongeki.yaml | 7 +- readme.md | 4 + titles/cm/__init__.py | 5 +- titles/cm/cm136.py | 50 ++++++++ titles/cm/cm_data/MU3/static_cards.csv | Bin 333451 -> 0 bytes titles/cm/cm_data/MU3/static_gacha_cards.csv | Bin 6759 -> 10059 bytes titles/cm/cm_data/MU3/static_gachas.csv | Bin 5458 -> 2786 bytes titles/cm/const.py | 3 +- titles/cm/index.py | 26 ++-- titles/cm/read.py | 127 +++++-------------- titles/ongeki/__init__.py | 2 +- titles/ongeki/bright.py | 24 ++-- titles/ongeki/brightmemory.py | 26 +++- titles/ongeki/read.py | 104 ++++++++++++--- titles/ongeki/schema/profile.py | 9 +- titles/ongeki/schema/static.py | 2 +- 16 files changed, 242 insertions(+), 147 deletions(-) create mode 100644 titles/cm/cm136.py delete mode 100644 titles/cm/cm_data/MU3/static_cards.csv diff --git a/example_config/ongeki.yaml b/example_config/ongeki.yaml index 6ebd18a..3db7098 100644 --- a/example_config/ongeki.yaml +++ b/example_config/ongeki.yaml @@ -23,4 +23,9 @@ gachas: - 1089 - 1104 - 1111 - - 1135 \ No newline at end of file + - 1135 + # can be used for Card Maker 1.35 and up, else will be ignored + - 1149 + - 1156 + - 1163 + - 1164 diff --git a/readme.md b/readme.md index 24440fc..d64cede 100644 --- a/readme.md +++ b/readme.md @@ -15,6 +15,10 @@ Games listed below have been tested and confirmed working. Only game versions ol + Hatsune Miku Arcade + All versions ++ Card Maker + + 1.34.xx + + 1.36.xx + + Ongeki + All versions up to Bright Memory diff --git a/titles/cm/__init__.py b/titles/cm/__init__.py index 040bec7..7c03605 100644 --- a/titles/cm/__init__.py +++ b/titles/cm/__init__.py @@ -10,6 +10,7 @@ include_protocol = True title_secure = False game_codes = [CardMakerConstants.GAME_CODE] trailing_slash = True -use_default_host = True +use_default_host = False +host = "" -current_schema_version = 1 +current_schema_version = 1 \ No newline at end of file diff --git a/titles/cm/cm136.py b/titles/cm/cm136.py new file mode 100644 index 0000000..2859c24 --- /dev/null +++ b/titles/cm/cm136.py @@ -0,0 +1,50 @@ +from datetime import date, datetime, timedelta +from typing import Any, Dict, List +import json +import logging +from enum import Enum + +from core.config import CoreConfig +from core.data.cache import cached +from titles.cm.base import CardMakerBase +from titles.cm.const import CardMakerConstants +from titles.cm.config import CardMakerConfig + + +class CardMaker136(CardMakerBase): + def __init__(self, core_cfg: CoreConfig, game_cfg: CardMakerConfig) -> None: + super().__init__(core_cfg, game_cfg) + self.version = CardMakerConstants.VER_CARD_MAKER_136 + + def handle_get_game_connect_api_request(self, data: Dict) -> Dict: + uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}" + + # CHUNITHM = 0, maimai = 1, ONGEKI = 2 + return { + "length": 3, + "gameConnectList": [ + { + "modelKind": 0, + "type": 1, + "titleUri": f"{uri}/SDHD/205/" + }, + { + "modelKind": 1, + "type": 1, + "titleUri": f"{uri}/SDEZ/125/" + }, + { + "modelKind": 2, + "type": 1, + "titleUri": f"{uri}/SDDT/135/" + } + ] + } + + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = super().handle_get_game_setting_api_request(data) + ret["gameSetting"]["dataVersion"] = "1.35.00" + ret["gameSetting"]["ongekiCmVersion"] = "1.35.04" + ret["gameSetting"]["chuniCmVersion"] = "2.05.00" + ret["gameSetting"]["maimaiCmVersion"] = "1.25.00" + return ret diff --git a/titles/cm/cm_data/MU3/static_cards.csv b/titles/cm/cm_data/MU3/static_cards.csv deleted file mode 100644 index 53e6e566e85af902acb7fa78d5213b9fef257331..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 333451 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 z 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 diff --git a/titles/cm/cm_data/MU3/static_gacha_cards.csv b/titles/cm/cm_data/MU3/static_gacha_cards.csv index 303e71a710e45c9db10e70441483bcd779422c8f..9588c6f8e4c907554dca0fccc022d5a0b3c3308d 100644 GIT binary patch literal 10059 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 delta 7 OcmX@@_uOPdx)cBpvjb)T diff --git a/titles/cm/cm_data/MU3/static_gachas.csv b/titles/cm/cm_data/MU3/static_gachas.csv index 20615740c04d64f035cc024dddafabe6e12657fd..5554cdbc12828cf315aab69b66d66811b0ea47e9 100644 GIT binary patch delta 183 zcmcbl^+>T z$zjZPlV>wqPrl0>GTDN~dU6#I?_sf+{F}vRvLCDU{gSP1Mv%X^T}!)wv)3stR}DKFrWMqh}Ai5CuegSPF@Vew}Dug%V4q> fm;K}kTpp9}b0tsqe literal 5458 zcmbVQOIO=Q7~T6<3|^!gA0Nx+l_gC{!)Yj~NfzDJAujj?>>luGnw=%NB<5iU2(JKf za7=hZ0&x=z3IC;IjpP-7p?8KyB$1kok3p6#&Gk3)-S2*P^ z@!ABgQv~M%7WO zjmQUwtXs21wpSo5|H9aQCXWCHR6b^`)k$Jal;npuf|zsU74RhOxX8b`gOx`(Ou`ro->056L&-xWujSY0aZ zR?wEst{BfIh%v4k#hiRlpzWDBn4hzivV788!CbcVuDZbMuSm5NRkQM8fWX$)fwiAv zGjEN9_a5_2&MKgkZL>$!`$8dm>9>Nh)l|V)(ug%yN93TZnR~_F?-~355Vn|Muj(KL zh*>#UVudBbR;J`94pth&ThPDi_H(vB?V;L%AcLTU_C_c_lzyG!Tzk#ifp*Mqu&>M| zUR+039n~VM_}7tCN3lo)@PuB}iu7nMMJ_#)#z1x*awZMHCFg=&(xZ9Lyx@i8XeGd# zrHh)<7|@&B?QJ)WcQ09Lm1-NpOW^1vkGtC|0fULh)h83&S?uJE#WkwVqJuUxT!8|n@sc~M zE~qeyZAR2vk?JW?eps^qwIj|(tOP(h<9Lqua1G7{oqU6;X_``5dI2_4q;LhU3qF#n zYgC8Xfzd-Zz@1a#Ze>t_=bEg;f)T()OsfO~U?m{||n0=k~pB0w2rLd24Hld%2pCa8K@l z2@O6VZSi=F?-U|~k6oi(@~II!gAizj?jL$yOZ zAfWn)JP69St^00!xTCvMdTq@SNaA bytes: req_raw = request.content.getvalue() @@ -51,18 +54,21 @@ class CardMakerServlet(): if version >= 130 and version < 135: # Card Maker internal_ver = CardMakerConstants.VER_CARD_MAKER + elif version >= 135 and version < 140: # Card Maker + internal_ver = CardMakerConstants.VER_CARD_MAKER_136 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 + # 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 # technically not 0 self.logger.error("Encryption not supported at this time") - try: + try: unzip = zlib.decompress(req_raw) except zlib.error as e: - self.logger.error(f"Failed to decompress v{version} {endpoint} request -> {e}") + self.logger.error( + f"Failed to decompress v{version} {endpoint} request -> {e}") return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) req_data = json.loads(unzip) @@ -76,11 +82,13 @@ class CardMakerServlet(): resp = handler(req_data) except AttributeError as e: - self.logger.warning(f"Unhandled v{version} request {endpoint} - {e}") + self.logger.warning( + f"Unhandled v{version} request {endpoint} - {e}") return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) except Exception as e: - self.logger.error(f"Error handling v{version} method {endpoint} - {e}") + self.logger.error( + f"Error handling v{version} method {endpoint} - {e}") return zlib.compress("{\"stat\": \"0\"}".encode("utf-8")) if resp is None: diff --git a/titles/cm/read.py b/titles/cm/read.py index b9cb674..57d9279 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -24,16 +24,17 @@ class CardMakerReader(BaseReader): self.logger.info( f"Start importer for {CardMakerConstants.game_ver_to_string(version)}") except IndexError: - self.logger.error(f"Invalid ongeki version {version}") + self.logger.error(f"Invalid Card Maker version {version}") exit(1) def read(self) -> None: static_datas = { - "static_cards.csv": "read_ongeki_card_csv", "static_gachas.csv": "read_ongeki_gacha_csv", "static_gacha_cards.csv": "read_ongeki_gacha_card_csv" } + data_dirs = [] + if self.bin_dir is not None: for file, func in static_datas.items(): if os.path.exists(f"{self.bin_dir}/MU3/{file}"): @@ -43,36 +44,12 @@ class CardMakerReader(BaseReader): self.logger.warn(f"Couldn't find {file} file in {self.bin_dir}, skipping") if self.opt_dir is not None: - dir = self.get_data_directories(self.opt_dir) + data_dirs += self.get_data_directories(self.opt_dir) # ONGEKI (MU3) cnnot easily access the bin data(A000.pac) # so only opt_dir will work for now - self.read_gacha(f"{dir}/MU3/gacha") - self.read_card(f"{dir}/MU3/card") - - def read_ongeki_card_csv(self, file_path: str) -> None: - self.logger.info(f"Reading cards from {file_path}...") - - with open(file_path, encoding="utf-8") as f: - reader = csv.DictReader(f) - for row in reader: - self.ongeki_data.static.put_card( - row["version"], - row["cardId"], - name=row["name"], - charaId=row["charaId"], - nickName=row["nickName"] if row["nickName"] != "" else None, - school=row["school"], - attribute=row["attribute"], - gakunen=row["gakunen"], - rarity=row["rarity"], - levelParam=row["levelParam"], - skillId=row["skillId"], - choKaikaSkillId=row["choKaikaSkillId"], - cardNumber=row["cardNumber"] if row["cardNumber"] != "" else None - ) - - self.logger.info(f"Added card {row['cardId']}") + for dir in data_dirs: + self.read_ongeki_gacha(f"{dir}/MU3/gacha") def read_ongeki_gacha_csv(self, file_path: str) -> None: self.logger.info(f"Reading gachas from {file_path}...") @@ -87,10 +64,7 @@ class CardMakerReader(BaseReader): row["kind"], type=row["type"], isCeiling=True if row["isCeiling"] == "1" else False, - maxSelectPoint=row["maxSelectPoint"], - ceilingCnt=row["ceilingCnt"], - changeRateCnt1=row["changeRateCnt1"], - changeRateCnt2=row["changeRateCnt2"] + maxSelectPoint=row["maxSelectPoint"] ) self.logger.info(f"Added gacha {row['gachaId']}") @@ -112,64 +86,6 @@ class CardMakerReader(BaseReader): self.logger.info(f"Added card {row['cardId']} to gacha") - def read_ongeki_card(self, base_dir: str) -> None: - self.logger.info(f"Reading cards from {base_dir}...") - - version_ids = { - '1000': OngekiConstants.VER_ONGEKI, - '1005': OngekiConstants.VER_ONGEKI_PLUS, - '1010': OngekiConstants.VER_ONGEKI_SUMMER, - '1015': OngekiConstants.VER_ONGEKI_SUMMER_PLUS, - '1020': OngekiConstants.VER_ONGEKI_RED, - '1025': OngekiConstants.VER_ONGEKI_RED_PLUS, - '1030': OngekiConstants.VER_ONGEKI_BRIGHT, - '1035': OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY - } - - for root, dirs, files in os.walk(base_dir): - for dir in dirs: - if os.path.exists(f"{root}/{dir}/Card.xml"): - with open(f"{root}/{dir}/Card.xml", "r", encoding="utf-8") as f: - troot = ET.fromstring(f.read()) - - card_id = int(troot.find('Name').find('id').text) - name = troot.find('Name').find('str').text - chara_id = int(troot.find('CharaID').find('id').text) - nick_name = troot.find('NickName').text - school = troot.find('School').find('str').text - attribute = troot.find('Attribute').text - gakunen = troot.find('Gakunen').find('str').text - rarity = OngekiConstants.RARITY_TYPES[ - troot.find('Rarity').text].value - - level_param = [] - for lvl in troot.find('LevelParam').findall('int'): - level_param.append(lvl.text) - - skill_id = int(troot.find('SkillID').find('id').text) - cho_kai_ka_skill_id = int(troot.find('ChoKaikaSkillID').find('id').text) - - version = version_ids[ - troot.find('VersionID').find('id').text] - card_number = troot.find('CardNumberString').text - - self.ongeki_data.static.put_card( - version, - card_id, - name=name, - charaId=chara_id, - nickName=nick_name, - school=school, - attribute=attribute, - gakunen=gakunen, - rarity=rarity, - levelParam=','.join(level_param), - skillId=skill_id, - choKaikaSkillId=cho_kai_ka_skill_id, - cardNumber=card_number - ) - self.logger.info(f"Added card {card_id}") - def read_ongeki_gacha(self, base_dir: str) -> None: self.logger.info(f"Reading gachas from {base_dir}...") @@ -189,11 +105,34 @@ class CardMakerReader(BaseReader): troot = ET.fromstring(f.read()) name = troot.find('Name').find('str').text - id = int(troot.find('Name').find('id').text) + gacha_id = int(troot.find('Name').find('id').text) + + # skip already existing gachas + if self.ongeki_data.static.get_gacha( + OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, gacha_id) is not None: + self.logger.info(f"Gacha {gacha_id} already added, skipping") + continue + + # 1140 is the first bright memory gacha + if gacha_id < 1140: + version = OngekiConstants.VER_ONGEKI_BRIGHT + else: + version = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY gacha_kind = OngekiConstants.CM_GACHA_KINDS[ type_to_kind[troot.find('Type').text]].value + # hardcode which gachas get "Select Gacha" with 33 points + is_ceiling, max_select_point = 0, 0 + if gacha_id in {1163, 1164, 1165, 1166, 1167, 1168}: + is_ceiling = 1 + max_select_point = 33 + self.ongeki_data.static.put_gacha( - self.version, id, name, gacha_kind) - self.logger.info(f"Added gacha {id}") + version, + gacha_id, + name, + gacha_kind, + isCeiling=is_ceiling, + maxSelectPoint=max_select_point) + self.logger.info(f"Added gacha {gacha_id}") diff --git a/titles/ongeki/__init__.py b/titles/ongeki/__init__.py index 26e107c..8ca6862 100644 --- a/titles/ongeki/__init__.py +++ b/titles/ongeki/__init__.py @@ -15,4 +15,4 @@ trailing_slash = True use_default_host = False host = "" -current_schema_version = 2 \ No newline at end of file +current_schema_version = 2 diff --git a/titles/ongeki/bright.py b/titles/ongeki/bright.py index 57660b5..8f66f93 100644 --- a/titles/ongeki/bright.py +++ b/titles/ongeki/bright.py @@ -22,10 +22,8 @@ class OngekiBright(OngekiBase): return ret def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: - # first check for a bright memory profile after that check for a - # bright profile - p = (self.data.profile.get_profile_data(data["userId"], self.version+1) - or self.data.profile.get_profile_data(data["userId"], self.version)) + # check for a bright profile + p = self.data.profile.get_profile_data(data["userId"], self.version) if p is None: return {} @@ -50,6 +48,8 @@ class OngekiBright(OngekiBase): user_data["accessCode"] = cards[0]["access_code"] # hardcode Card Maker version for now + # Card Maker 1.34.00 = 1.30.01 + # Card Maker 1.36.00 = 1.35.04 user_data["compatibleCmVersion"] = "1.30.01" return {"userId": data["userId"], "userData": user_data} @@ -195,7 +195,9 @@ class OngekiBright(OngekiBase): # make sure to only show gachas for the current version # so only up to bright, 1140 is the first bright memory gacha - if tmp["gachaId"] < 1140: + if self.version == OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY: + game_gacha_list.append(tmp) + elif self.version == OngekiConstants.VER_ONGEKI_BRIGHT and tmp["gachaId"] < 1140: game_gacha_list.append(tmp) return { @@ -379,11 +381,11 @@ class OngekiBright(OngekiBase): if "userData" in upsert and len(upsert["userData"]) > 0: # check if the profile is a bright memory profile - p = self.data.profile.get_profile_data(data["userId"], self.version+1) + p = self.data.profile.get_profile_data(data["userId"], self.version) if p is not None: # save the bright memory profile self.data.profile.put_profile_data( - user_id, self.version+1, upsert["userData"][0]) + user_id, self.version, upsert["userData"][0]) else: # save the bright profile self.data.profile.put_profile_data( @@ -413,11 +415,11 @@ class OngekiBright(OngekiBase): if "userData" in upsert and len(upsert["userData"]) > 0: # check if the profile is a bright memory profile - p = self.data.profile.get_profile_data(data["userId"], self.version+1) + p = self.data.profile.get_profile_data(data["userId"], self.version) if p is not None: # save the bright memory profile self.data.profile.put_profile_data( - user_id, self.version+1, upsert["userData"][0]) + user_id, self.version, upsert["userData"][0]) else: # save the bright profile self.data.profile.put_profile_data( @@ -601,11 +603,11 @@ class OngekiBright(OngekiBase): if "userData" in upsert and len(upsert["userData"]) > 0: # check if the profile is a bright memory profile - p = self.data.profile.get_profile_data(data["userId"], self.version+1) + p = self.data.profile.get_profile_data(data["userId"], self.version) if p is not None: # save the bright memory profile self.data.profile.put_profile_data( - user_id, self.version+1, upsert["userData"][0]) + user_id, self.version, upsert["userData"][0]) else: # save the bright profile self.data.profile.put_profile_data( diff --git a/titles/ongeki/brightmemory.py b/titles/ongeki/brightmemory.py index a99f806..c3e4ef1 100644 --- a/titles/ongeki/brightmemory.py +++ b/titles/ongeki/brightmemory.py @@ -5,11 +5,12 @@ import json from core.config import CoreConfig from titles.ongeki.base import OngekiBase +from titles.ongeki.bright import OngekiBright from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig -class OngekiBrightMemory(OngekiBase): +class OngekiBrightMemory(OngekiBright): def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None: super().__init__(core_cfg, game_cfg) self.version = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY @@ -28,7 +29,7 @@ class OngekiBrightMemory(OngekiBase): def handle_get_user_memory_chapter_api_request(self, data: Dict) -> Dict: memories = self.data.item.get_memorychapters(data["userId"]) - if not memories: + if not memories: return {"userId": data["userId"], "length":6, "userMemoryChapterList":[ {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70001, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70002, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, @@ -37,17 +38,17 @@ class OngekiBrightMemory(OngekiBase): {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70005, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}, {"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70099, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0} ]} - + memory_chp = [] for chp in memories: tmp = chp._asdict() tmp.pop("id") tmp.pop("user") memory_chp.append(tmp) - + return { - "userId": data["userId"], - "length": len(memory_chp), + "userId": data["userId"], + "length": len(memory_chp), "userMemoryChapterList": memory_chp } @@ -55,4 +56,15 @@ class OngekiBrightMemory(OngekiBase): return { "techScore": 0, "cardNum": 0 - } \ No newline at end of file + } + + def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + # check for a bright memory profile + user_data = super().handle_cm_get_user_data_api_request(data) + + # hardcode Card Maker version for now + # Card Maker 1.34.00 = 1.30.01 + # Card Maker 1.36.00 = 1.35.04 + user_data["userData"]["compatibleCmVersion"] = "1.35.04" + + return user_data diff --git a/titles/ongeki/read.py b/titles/ongeki/read.py index 8b1be3d..397fa8c 100644 --- a/titles/ongeki/read.py +++ b/titles/ongeki/read.py @@ -11,29 +11,99 @@ from titles.ongeki.database import OngekiData from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig + class OngekiReader(BaseReader): - def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None: + def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], + opt_dir: Optional[str], extra: Optional[str]) -> None: super().__init__(config, version, bin_dir, opt_dir, extra) self.data = OngekiData(config) try: - self.logger.info(f"Start importer for {OngekiConstants.game_ver_to_string(version)}") + self.logger.info( + f"Start importer for {OngekiConstants.game_ver_to_string(version)}") except IndexError: self.logger.error(f"Invalid ongeki version {version}") exit(1) - + def read(self) -> None: data_dirs = [] 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) - + for dir in data_dirs: self.read_events(f"{dir}/event") self.read_music(f"{dir}/music") - + self.read_card(f"{dir}/card") + + def read_card(self, base_dir: str) -> None: + self.logger.info(f"Reading cards from {base_dir}...") + + version_ids = { + '1000': OngekiConstants.VER_ONGEKI, + '1005': OngekiConstants.VER_ONGEKI_PLUS, + '1010': OngekiConstants.VER_ONGEKI_SUMMER, + '1015': OngekiConstants.VER_ONGEKI_SUMMER_PLUS, + '1020': OngekiConstants.VER_ONGEKI_RED, + '1025': OngekiConstants.VER_ONGEKI_RED_PLUS, + '1030': OngekiConstants.VER_ONGEKI_BRIGHT, + '1035': OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY + } + + for root, dirs, files in os.walk(base_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Card.xml"): + with open(f"{root}/{dir}/Card.xml", "r", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + card_id = int(troot.find('Name').find('id').text) + + # skip already existing cards + if self.data.static.get_card( + OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, card_id) is not None: + + self.logger.info(f"Card {card_id} already added, skipping") + continue + + name = troot.find('Name').find('str').text + chara_id = int(troot.find('CharaID').find('id').text) + nick_name = troot.find('NickName').text + school = troot.find('School').find('str').text + attribute = troot.find('Attribute').text + gakunen = troot.find('Gakunen').find('str').text + rarity = OngekiConstants.RARITY_TYPES[ + troot.find('Rarity').text].value + + level_param = [] + for lvl in troot.find('LevelParam').findall('int'): + level_param.append(lvl.text) + + skill_id = int(troot.find('SkillID').find('id').text) + cho_kai_ka_skill_id = int(troot.find('ChoKaikaSkillID').find('id').text) + + version = version_ids[ + troot.find('VersionID').find('id').text] + card_number = troot.find('CardNumberString').text + + self.data.static.put_card( + version, + card_id, + name=name, + charaId=chara_id, + nickName=nick_name, + school=school, + attribute=attribute, + gakunen=gakunen, + rarity=rarity, + levelParam=','.join(level_param), + skillId=skill_id, + choKaikaSkillId=cho_kai_ka_skill_id, + cardNumber=card_number + ) + self.logger.info(f"Added card {card_id}") + def read_events(self, base_dir: str) -> None: self.logger.info(f"Reading events from {base_dir}...") @@ -45,12 +115,13 @@ class OngekiReader(BaseReader): name = troot.find('Name').find('str').text id = int(troot.find('Name').find('id').text) - event_type = OngekiConstants.EVT_TYPES[troot.find('EventType').text].value - + event_type = OngekiConstants.EVT_TYPES[troot.find( + 'EventType').text].value - self.data.static.put_event(self.version, id, event_type, name) + self.data.static.put_event( + self.version, id, event_type, name) self.logger.info(f"Added event {id}") - + def read_music(self, base_dir: str) -> None: self.logger.info(f"Reading music from {base_dir}...") @@ -72,9 +143,9 @@ class OngekiReader(BaseReader): title = name.find('str').text artist = troot.find('ArtistName').find('str').text genre = troot.find('Genre').find('str').text - + fumens = troot.find("FumenData") - for fumens_data in fumens.findall('FumenData'): + for fumens_data in fumens.findall('FumenData'): path = fumens_data.find('FumenFile').find('path').text if path is None or not os.path.exists(f"{root}/{dir}/{path}"): continue @@ -82,8 +153,9 @@ class OngekiReader(BaseReader): chart_id = int(path.split(".")[0].split("_")[1]) level = float( f"{fumens_data.find('FumenConstIntegerPart').text}.{fumens_data.find('FumenConstFractionalPart').text}" - ) - - self.data.static.put_chart(self.version, song_id, chart_id, title, artist, genre, level) - self.logger.info(f"Added song {song_id} chart {chart_id}") + ) + self.data.static.put_chart( + self.version, song_id, chart_id, title, artist, genre, level) + self.logger.info( + f"Added song {song_id} chart {chart_id}") diff --git a/titles/ongeki/schema/profile.py b/titles/ongeki/schema/profile.py index d7514c6..ce07490 100644 --- a/titles/ongeki/schema/profile.py +++ b/titles/ongeki/schema/profile.py @@ -180,7 +180,7 @@ region = Table( mysql_charset='utf8mb4' ) -training_room = Table ( +training_room = Table( "ongeki_profile_training_room", metadata, Column("id", Integer, primary_key=True, nullable=False), @@ -193,7 +193,7 @@ training_room = Table ( mysql_charset='utf8mb4' ) -kop = Table ( +kop = Table( "ongeki_profile_kop", metadata, Column("id", Integer, primary_key=True, nullable=False), @@ -219,6 +219,7 @@ rival = Table( mysql_charset='utf8mb4' ) + class OngekiProfileData(BaseData): def __init__(self, cfg: CoreConfig, conn: Connection) -> None: super().__init__(cfg, conn) @@ -287,14 +288,14 @@ class OngekiProfileData(BaseData): result = self.execute(sql) if result is None: return None return result.fetchall() - + def get_kop(self, aime_id: int) -> Optional[List[Row]]: sql = select(kop).where(kop.c.user == aime_id) result = self.execute(sql) if result is None: return None return result.fetchall() - + def get_rivals(self, aime_id: int) -> Optional[List[Row]]: sql = select(rival.c.rivalUserId).where(rival.c.user == aime_id) diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index 2e1bc51..7c5ed68 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -192,7 +192,7 @@ class OngekiStaticData(BaseData): def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]: sql = gachas.select(and_( - gachas.c.version == version, + gachas.c.version <= version, gachas.c.gachaId == gacha_id )) From 6609732546f96cc6745a137ccfbebd519021c043 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 6 Mar 2023 16:06:17 +0100 Subject: [PATCH 4/4] cm: added get_allnet_info --- titles/cm/__init__.py | 8 +------- titles/cm/const.py | 2 ++ titles/cm/index.py | 22 ++++++++++++++++++++-- titles/ongeki/__init__.py | 2 +- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/titles/cm/__init__.py b/titles/cm/__init__.py index 7c03605..ae4e9f0 100644 --- a/titles/cm/__init__.py +++ b/titles/cm/__init__.py @@ -5,12 +5,6 @@ from titles.cm.read import CardMakerReader index = CardMakerServlet reader = CardMakerReader -use_default_title = True -include_protocol = True -title_secure = False game_codes = [CardMakerConstants.GAME_CODE] -trailing_slash = True -use_default_host = False -host = "" -current_schema_version = 1 \ No newline at end of file +current_schema_version = 1 diff --git a/titles/cm/const.py b/titles/cm/const.py index 54cc009..3dec4fe 100644 --- a/titles/cm/const.py +++ b/titles/cm/const.py @@ -1,6 +1,8 @@ class CardMakerConstants(): GAME_CODE = "SDED" + CONFIG_NAME = "cardmaker.yaml" + VER_CARD_MAKER = 0 VER_CARD_MAKER_136 = 1 diff --git a/titles/cm/index.py b/titles/cm/index.py index 34fafb0..b2751b0 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -1,4 +1,3 @@ -from twisted.web.http import Request import json import inflection import yaml @@ -6,6 +5,10 @@ import string import logging import coloredlogs import zlib + +from os import path +from typing import Tuple +from twisted.web.http import Request from logging.handlers import TimedRotatingFileHandler from core.config import CoreConfig @@ -19,7 +22,8 @@ class CardMakerServlet(): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: self.core_cfg = core_cfg self.game_cfg = CardMakerConfig() - self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/cardmaker.yaml"))) + if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"): + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"))) self.versions = [ CardMakerBase(core_cfg, self.game_cfg), @@ -44,6 +48,20 @@ class CardMakerServlet(): coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str) + @classmethod + def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]: + game_cfg = CardMakerConfig() + if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"): + game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"))) + + if not game_cfg.server.enable: + return (False, "", "") + + if core_cfg.server.is_develop: + return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", "") + + return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") + def render_POST(self, request: Request, version: int, url_path: str) -> bytes: req_raw = request.content.getvalue() url_split = url_path.split("/") diff --git a/titles/ongeki/__init__.py b/titles/ongeki/__init__.py index 8ca6862..26e107c 100644 --- a/titles/ongeki/__init__.py +++ b/titles/ongeki/__init__.py @@ -15,4 +15,4 @@ trailing_slash = True use_default_host = False host = "" -current_schema_version = 2 +current_schema_version = 2 \ No newline at end of file