diff --git a/core/data/schema/versions/SDED_1_upgrade.sql b/core/data/schema/versions/SDED_1_upgrade.sql deleted file mode 100644 index a4d666e..0000000 --- a/core/data/schema/versions/SDED_1_upgrade.sql +++ /dev/null @@ -1,99 +0,0 @@ -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 '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', - 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/core/data/schema/versions/SDEZ_3_upgrade.sql b/core/data/schema/versions/SDEZ_3_upgrade.sql new file mode 100644 index 0000000..c13e1fe --- /dev/null +++ b/core/data/schema/versions/SDEZ_3_upgrade.sql @@ -0,0 +1,21 @@ +ALTER TABLE mai2_item_card +CHANGE COLUMN card_id cardId INT NOT NULL AFTER user, +CHANGE COLUMN card_kind cardTypeId INT NOT NULL, +CHANGE COLUMN chara_id charaId INT NOT NULL, +CHANGE COLUMN map_id mapId INT NOT NULL, +CHANGE COLUMN startDate startDate TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00', +CHANGE COLUMN endDate endDate TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00'; + +ALTER TABLE mai2_item_item +CHANGE COLUMN item_id itemId INT NOT NULL AFTER user, +CHANGE COLUMN item_kind itemKind INT NOT NULL, +CHANGE COLUMN is_valid isValid TINYINT(1) NOT NULL DEFAULT '1'; + +ALTER TABLE mai2_item_character +CHANGE COLUMN character_id characterId INT NOT NULL, +CHANGE COLUMN use_count useCount INT NOT NULL DEFAULT '0'; + +ALTER TABLE mai2_item_charge +CHANGE COLUMN charge_id chargeId INT NOT NULL, +CHANGE COLUMN purchase_date purchaseDate TIMESTAMP NOT NULL, +CHANGE COLUMN valid_date validDate TIMESTAMP NOT NULL; diff --git a/read.py b/read.py index 538198a..a8400a5 100644 --- a/read.py +++ b/read.py @@ -4,7 +4,8 @@ import re import os import yaml from os import path -import logging, coloredlogs +import logging +import coloredlogs from logging.handlers import TimedRotatingFileHandler from typing import List, Optional @@ -135,7 +136,8 @@ if __name__ == "__main__": for dir, mod in titles.items(): if args.series in mod.game_codes: - handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra) + handler = mod.reader(config, args.version, + bin_arg, opt_arg, args.extra) handler.read() logger.info("Done") diff --git a/titles/chuni/base.py b/titles/chuni/base.py index f66eac8..13f423b 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -588,3 +588,11 @@ class ChuniBase: def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} + + def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict: + return { + "userId": data["userId"], + "userNetBattleData": { + "recentNBSelectMusicList": [] + } + } \ No newline at end of file diff --git a/titles/chuni/index.py b/titles/chuni/index.py index a8b581e..a103a7d 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -103,7 +103,7 @@ class ChuniServlet: return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "") def render_POST(self, request: Request, version: int, url_path: str) -> bytes: - if url_path.lower() == "/ping": + if url_path.lower() == "ping": return zlib.compress(b'{"returnCode": "1"}') req_raw = request.content.getvalue() diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 0d74ba6..611c6d2 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -1,6 +1,6 @@ import logging from datetime import datetime, timedelta - +from random import randint from typing import Dict from core.config import CoreConfig @@ -61,8 +61,8 @@ class ChuniNew(ChuniBase): } def handle_remove_token_api_request(self, data: Dict) -> Dict: - return { "returnCode": "1" } - + return {"returnCode": "1"} + def handle_delete_token_api_request(self, data: Dict) -> Dict: return {"returnCode": "1"} @@ -122,11 +122,355 @@ class ChuniNew(ChuniBase): "playerLevel": profile["playerLevel"], "rating": profile["rating"], "headphone": profile["headphone"], - "chargeState": 0, - "userNameEx": "0", + # Enables favorites and teams + "chargeState": 1, + "userNameEx": "", "banState": 0, "classEmblemMedal": profile["classEmblemMedal"], "classEmblemBase": profile["classEmblemBase"], "battleRankId": profile["battleRankId"], } return data1 + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + p = self.data.profile.get_profile_data(data["userId"], self.version) + if p is None: + return {} + + return { + "userName": p["userName"], + "level": p["level"], + "medal": p["medal"], + "lastDataVersion": "2.00.00", + "isLogin": False, + } + + def handle_printer_login_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + def handle_printer_logout_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + def handle_get_game_gacha_api_request(self, data: Dict) -> Dict: + """ + returns all current active banners (gachas) + """ + game_gachas = self.data.static.get_gachas(self.version) + + # 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" + ) + + game_gacha_list.append(tmp) + + return { + "length": len(game_gacha_list), + "gameGachaList": game_gacha_list, + # no clue + "registIdList": [], + } + + def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict: + """ + returns all valid cards for a given gachaId + """ + game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"]) + + 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), + # check isPickup from the chuni_static_gachas? + "isPickup": False, + "gameGachaCardList": game_gacha_card_list, + # again no clue + "emissionList": [], + "afterCalcList": [], + "ssrBookCalcList": [], + } + + def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + p = self.data.profile.get_profile_data(data["userId"], self.version) + if p is None: + return {} + + profile = p._asdict() + profile.pop("id") + profile.pop("user") + profile.pop("version") + + return { + "userId": data["userId"], + "userData": profile, + "userEmoney": [ + { + "type": 0, + "emoneyCredit": 100, + "emoneyBrand": 1, + "ext1": 0, + "ext2": 0, + "ext3": 0, + } + ], + } + + 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_get_user_printed_card_api_request(self, data: Dict) -> Dict: + user_print_list = self.data.item.get_user_print_states( + data["userId"], has_completed=True + ) + if user_print_list is None: + return { + "userId": data["userId"], + "length": 0, + "nextIndex": -1, + "userPrintedCardList": [], + } + + print_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(user_print_list)): + tmp = user_print_list[x]._asdict() + print_list.append(tmp["cardId"]) + + if len(user_print_list) >= max_ct: + break + + if len(user_print_list) >= max_ct: + next_idx = next_idx + max_ct + else: + next_idx = -1 + + return { + "userId": data["userId"], + "length": len(print_list), + "nextIndex": next_idx, + "userPrintedCardList": print_list, + } + + def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + + user_print_states = self.data.item.get_user_print_states( + user_id, has_completed=False + ) + + card_print_state_list = [] + for card in user_print_states: + tmp = card._asdict() + tmp["orderId"] = tmp["id"] + tmp.pop("user") + tmp["limitDate"] = datetime.strftime(tmp["limitDate"], "%Y-%m-%d") + + card_print_state_list.append(tmp) + + return { + "userId": user_id, + "length": len(card_print_state_list), + "userCardPrintStateList": card_print_state_list, + } + + def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + return super().handle_get_user_character_api_request(data) + + def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + return super().handle_get_user_item_api_request(data) + + def handle_roll_gacha_api_request(self, data: Dict) -> Dict: + """ + Handle a gacha roll API request, with: + gachaId: the gachaId where the cards should be pulled from + times: the number of gacha rolls + characterId: the character which the user wants + """ + gacha_id = data["gachaId"] + num_rolls = data["times"] + chara_id = data["characterId"] + + rolled_cards = [] + + # characterId is set after 10 rolls, where the user can select a card + # from all gameGachaCards, therefore the correct cardId for a given + # characterId should be returned + if chara_id != -1: + # get the + card = self.data.static.get_gacha_card_by_character(gacha_id, chara_id) + + tmp = card._asdict() + tmp.pop("id") + + rolled_cards.append(tmp) + else: + gacha_cards = self.data.static.get_gacha_cards(gacha_id) + + # get the card id for each roll + for _ in range(num_rolls): + # get the index from all possible cards + card_idx = randint(0, len(gacha_cards) - 1) + # remove the index from the cards so it wont get pulled again + card = gacha_cards.pop(card_idx) + + # remove the "id" fronm the card + tmp = card._asdict() + tmp.pop("id") + + rolled_cards.append(tmp) + + return {"length": len(rolled_cards), "gameGachaCardList": rolled_cards} + + def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict: + upsert = data["cmUpsertUserGacha"] + user_id = data["userId"] + place_id = data["placeId"] + + # save the user data + user_data = upsert["userData"] + user_data.pop("rankUpChallengeResults") + user_data.pop("userEmoney") + + self.data.profile.put_profile_data(user_id, self.version, user_data) + + # save the user gacha + user_gacha = upsert["userGacha"] + gacha_id = user_gacha["gachaId"] + user_gacha.pop("gachaId") + user_gacha.pop("dailyGachaDate") + + self.data.item.put_user_gacha(user_id, gacha_id, user_gacha) + + # save all user items + if "userItemList" in upsert: + for item in upsert["userItemList"]: + self.data.item.put_item(user_id, item) + + # add every gamegachaCard to database + for card in upsert["gameGachaCardList"]: + self.data.item.put_user_print_state( + user_id, + hasCompleted=False, + placeId=place_id, + cardId=card["cardId"], + gachaId=card["gachaId"], + ) + + # retrieve every game gacha card which has been added in order to get + # the orderId for the next request + user_print_states = self.data.item.get_user_print_states_by_gacha( + user_id, gacha_id, has_completed=False + ) + card_print_state_list = [] + for card in user_print_states: + tmp = card._asdict() + tmp["orderId"] = tmp["id"] + tmp.pop("user") + tmp["limitDate"] = datetime.strftime(tmp["limitDate"], "%Y-%m-%d") + + card_print_state_list.append(tmp) + + return { + "returnCode": "1", + "apiName": "CMUpsertUserGachaApi", + "userCardPrintStateList": card_print_state_list, + } + + 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"] + user_id = data["userId"] + + # 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(user_id, serial_id, user_print_detail) + + return { + "returnCode": 1, + "orderId": 0, + "serialId": serial_id, + "apiName": "CMUpsertUserPrintApi", + } + + def handle_cm_upsert_user_print_subtract_api_request(self, data: Dict) -> Dict: + upsert = data["userCardPrintState"] + user_id = data["userId"] + place_id = data["placeId"] + + # save all user items + if "userItemList" in data: + for item in data["userItemList"]: + self.data.item.put_item(user_id, item) + + # set the card print state to success and use the orderId as the key + self.data.item.put_user_print_state( + user_id, + id=upsert["orderId"], + hasCompleted=True + ) + + return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"} + + def handle_cm_upsert_user_print_cancel_api_request(self, data: Dict) -> Dict: + order_ids = data["orderIdList"] + user_id = data["userId"] + + # set the card print state to success and use the orderId as the key + for order_id in order_ids: + self.data.item.put_user_print_state( + user_id, + id=order_id, + hasCompleted=True + ) + + return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"} diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index 7e15985..f46bb82 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -30,3 +30,12 @@ class ChuniNewPlus(ChuniNew): "reflectorUri" ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" return ret + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = super().handle_cm_get_user_preview_api_request(data) + + # hardcode lastDataVersion for CardMaker 1.35 + # user_data["lastDataVersion"] = "2.05.00" + # hardcode lastDataVersion for CardMaker 1.36 + user_data["lastDataVersion"] = "2.10.00" + return user_data diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index cc519fa..124d7df 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -114,6 +114,76 @@ map_area = Table( mysql_charset="utf8mb4", ) +gacha = Table( + "chuni_item_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("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="chuni_item_gacha_uk"), + mysql_charset="utf8mb4", +) + +print_state = Table( + "chuni_item_print_state", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("hasCompleted", Boolean, nullable=False, server_default="0"), + Column( + "limitDate", TIMESTAMP, nullable=False, server_default="2038-01-01 00:00:00.0" + ), + Column("placeId", Integer), + Column("cardId", Integer), + Column("gachaId", Integer), + UniqueConstraint("id", "user", name="chuni_item_print_state_uk"), + mysql_charset="utf8mb4", +) + +print_detail = Table( + "chuni_item_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("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("printOption1", Boolean, server_default="0"), + Column("printOption2", Boolean, server_default="0"), + Column("printOption3", Boolean, server_default="0"), + Column("printOption4", Boolean, server_default="0"), + Column("printOption5", Boolean, server_default="0"), + Column("printOption6", Boolean, server_default="0"), + Column("printOption7", Boolean, server_default="0"), + Column("printOption8", Boolean, server_default="0"), + Column("printOption9", Boolean, server_default="0"), + Column("printOption10", Boolean, server_default="0"), + Column("created", String(255), server_default=""), + UniqueConstraint("serialId", name="chuni_item_print_detail_uk"), + mysql_charset="utf8mb4", +) + class ChuniItemData(BaseData): def put_character(self, user_id: int, character_data: Dict) -> Optional[int]: @@ -235,3 +305,89 @@ class ChuniItemData(BaseData): if result is None: return None return result.fetchall() + + 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 put_user_gacha( + self, aime_id: int, gacha_id: int, gacha_data: Dict + ) -> Optional[int]: + sql = insert(gacha).values(user=aime_id, gachaId=gacha_id, **gacha_data) + + conflict = sql.on_duplicate_key_update( + user=aime_id, gachaId=gacha_id, **gacha_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 get_user_print_states( + self, aime_id: int, has_completed: bool = False + ) -> Optional[List[Row]]: + sql = print_state.select( + and_( + print_state.c.user == aime_id, + print_state.c.hasCompleted == has_completed + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_user_print_states_by_gacha( + self, aime_id: int, gacha_id: int, has_completed: bool = False + ) -> Optional[List[Row]]: + sql = print_state.select( + and_( + print_state.c.user == aime_id, + print_state.c.gachaId == gacha_id, + print_state.c.hasCompleted == has_completed + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_user_print_state(self, aime_id: int, **print_data) -> Optional[int]: + sql = insert(print_state).values(user=aime_id, **print_data) + + conflict = sql.on_duplicate_key_update(user=aime_id, **print_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_user_print_state: 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, **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 \ No newline at end of file diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 0d58c45..0784872 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -68,6 +68,60 @@ avatar = Table( mysql_charset="utf8mb4", ) +gachas = Table( + "chuni_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("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"), + UniqueConstraint("version", "gachaId", "gachaName", name="chuni_static_gachas_uk"), + mysql_charset="utf8mb4", +) + +cards = Table( + "chuni_static_cards", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("cardId", Integer, nullable=False), + Column("charaName", String(255), nullable=False), + Column("charaId", Integer, nullable=False), + Column("presentName", String(255), nullable=False), + Column("rarity", Integer, server_default="2"), + Column("labelType", Integer, nullable=False), + Column("difType", Integer, nullable=False), + Column("miss", Integer, nullable=False), + Column("combo", Integer, nullable=False), + Column("chain", Integer, nullable=False), + Column("skillName", String(255), nullable=False), + UniqueConstraint("version", "cardId", name="chuni_static_cards_uk"), + mysql_charset="utf8mb4", +) + +gacha_cards = Table( + "chuni_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"), + UniqueConstraint("gachaId", "cardId", name="chuni_static_gacha_cards_uk"), + mysql_charset="utf8mb4", +) + class ChuniStaticData(BaseData): def put_event( @@ -265,3 +319,112 @@ class ChuniStaticData(BaseData): if result is None: return None return result.lastrowid + + def put_gacha( + self, + version: int, + gacha_id: int, + gacha_name: int, + **gacha_data, + ) -> Optional[int]: + sql = insert(gachas).values( + version=version, + gachaId=gacha_id, + gachaName=gacha_name, + **gacha_data, + ) + + conflict = sql.on_duplicate_key_update( + version=version, + gachaId=gacha_id, + gachaName=gacha_name, + **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_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 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 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 get_gacha_card_by_character(self, gacha_id: int, chara_id: int) -> Optional[Dict]: + sql_sub = ( + select(cards.c.cardId) + .filter( + cards.c.charaId == chara_id + ) + .scalar_subquery() + ) + + # Perform the main query, also rename the resulting column to ranking + sql = gacha_cards.select(and_( + gacha_cards.c.gachaId == gacha_id, + gacha_cards.c.cardId == sql_sub + )) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + 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() diff --git a/titles/cm/__init__.py b/titles/cm/__init__.py index ae4e9f0..1115f96 100644 --- a/titles/cm/__init__.py +++ b/titles/cm/__init__.py @@ -1,9 +1,11 @@ from titles.cm.index import CardMakerServlet from titles.cm.const import CardMakerConstants from titles.cm.read import CardMakerReader +from titles.cm.database import CardMakerData index = CardMakerServlet reader = CardMakerReader +database = CardMakerData game_codes = [CardMakerConstants.GAME_CODE] diff --git a/titles/cm/database.py b/titles/cm/database.py new file mode 100644 index 0000000..1d32109 --- /dev/null +++ b/titles/cm/database.py @@ -0,0 +1,8 @@ +from core.data import Data +from core.config import CoreConfig + + +class CardMakerData(Data): + def __init__(self, cfg: CoreConfig) -> None: + super().__init__(cfg) + # empty Card Maker database diff --git a/titles/cm/read.py b/titles/cm/read.py index 3a4635f..f27b40b 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -12,6 +12,10 @@ from titles.ongeki.database import OngekiData from titles.cm.const import CardMakerConstants from titles.ongeki.const import OngekiConstants from titles.ongeki.config import OngekiConfig +from titles.mai2.database import Mai2Data +from titles.mai2.const import Mai2Constants +from titles.chuni.database import ChuniData +from titles.chuni.const import ChuniConstants class CardMakerReader(BaseReader): @@ -25,6 +29,8 @@ class CardMakerReader(BaseReader): ) -> None: super().__init__(config, version, bin_dir, opt_dir, extra) self.ongeki_data = OngekiData(config) + self.mai2_data = Mai2Data(config) + self.chuni_data = ChuniData(config) try: self.logger.info( @@ -34,15 +40,29 @@ class CardMakerReader(BaseReader): self.logger.error(f"Invalid Card Maker version {version}") exit(1) + def _get_card_maker_directory(self, directory: str) -> str: + for root, dirs, files in os.walk(directory): + for dir in dirs: + if ( + os.path.exists(f"{root}/{dir}/MU3") + and os.path.exists(f"{root}/{dir}/MAI") + and os.path.exists(f"{root}/{dir}/CHU") + ): + return f"{root}/{dir}" + def read(self) -> None: static_datas = { "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: + data_dir = self._get_card_maker_directory(self.bin_dir) + + self.read_chuni_card(f"{data_dir}/CHU/Data/A000/card") + self.read_chuni_gacha(f"{data_dir}/CHU/Data/A000/gacha") + + self.read_mai2_card(f"{data_dir}/MAI/Data/A000/card") for file, func in static_datas.items(): if os.path.exists(f"{self.bin_dir}/MU3/{file}"): read_csv = getattr(CardMakerReader, func) @@ -53,13 +73,163 @@ class CardMakerReader(BaseReader): ) if self.opt_dir is not None: - data_dirs += 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 for dir in data_dirs: + self.read_chuni_card(f"{dir}/CHU/card") + self.read_chuni_gacha(f"{dir}/CHU/gacha") + self.read_ongeki_gacha(f"{dir}/MU3/gacha") + def read_chuni_card(self, base_dir: str) -> None: + self.logger.info(f"Reading cards from {base_dir}...") + + version_ids = { + "v2_00": ChuniConstants.VER_CHUNITHM_NEW, + "v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS, + # Chunithm SUN, ignore for now + "v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1 + } + + 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) + + chara_name = troot.find("chuniCharaName").find("str").text + chara_id = troot.find("chuniCharaName").find("id").text + version = version_ids[ + troot.find("netOpenName").find("str").text[:5] + ] + present_name = troot.find("chuniPresentName").find("str").text + rarity = int(troot.find("rareType").text) + label = int(troot.find("labelType").text) + dif = int(troot.find("difType").text) + miss = int(troot.find("miss").text) + combo = int(troot.find("combo").text) + chain = int(troot.find("chain").text) + skill_name = troot.find("skillName").text + + self.chuni_data.static.put_card( + version, + card_id, + charaName=chara_name, + charaId=chara_id, + presentName=present_name, + rarity=rarity, + labelType=label, + difType=dif, + miss=miss, + combo=combo, + chain=chain, + skillName=skill_name, + ) + + self.logger.info(f"Added chuni card {card_id}") + + def read_chuni_gacha(self, base_dir: str) -> None: + self.logger.info(f"Reading gachas from {base_dir}...") + + version_ids = { + "v2_00": ChuniConstants.VER_CHUNITHM_NEW, + "v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS, + # Chunithm SUN, ignore for now + "v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1, + } + + 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("gachaName").text + gacha_id = int(troot.find("name").find("id").text) + + version = version_ids[ + troot.find("netOpenName").find("str").text[:5] + ] + ceiling_cnt = int(troot.find("ceilingNum").text) + gacha_type = int(troot.find("gachaType").text) + is_ceiling = ( + True if troot.find("ceilingType").text == "1" else False + ) + + self.chuni_data.static.put_gacha( + version, + gacha_id, + name, + type=gacha_type, + isCeiling=is_ceiling, + ceilingCnt=ceiling_cnt, + ) + + self.logger.info(f"Added chuni gacha {gacha_id}") + + for gacha_card in troot.find("infos").iter("GachaCardDataInfo"): + # get the card ID from the id element + card_id = gacha_card.find("cardName").find("id").text + + # get the weight from the weight element + weight = int(gacha_card.find("weight").text) + + # get the pickup flag from the pickup element + is_pickup = ( + True if gacha_card.find("pickup").text == "1" else False + ) + + self.chuni_data.static.put_gacha_card( + gacha_id, + card_id, + weight=weight, + rarity=2, + isPickup=is_pickup, + ) + + self.logger.info( + f"Added chuni card {card_id} to gacha {gacha_id}" + ) + + def read_mai2_card(self, base_dir: str) -> None: + self.logger.info(f"Reading cards from {base_dir}...") + + version_ids = { + "1.00": Mai2Constants.VER_MAIMAI_DX, + "1.05": Mai2Constants.VER_MAIMAI_DX_PLUS, + "1.09": Mai2Constants.VER_MAIMAI_DX_PLUS, + "1.10": Mai2Constants.VER_MAIMAI_DX_SPLASH, + "1.15": Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS, + "1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE, + "1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS, + } + + 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()) + + name = troot.find("name").find("str").text + card_id = int(troot.find("name").find("id").text) + + version = version_ids[ + troot.find("enableVersion").find("str").text + ] + + enabled = ( + True if troot.find("disable").text == "false" else False + ) + + self.mai2_data.static.put_card( + version, card_id, name, enabled=enabled + ) + self.logger.info(f"Added mai2 card {card_id}") + def read_ongeki_gacha_csv(self, file_path: str) -> None: self.logger.info(f"Reading gachas from {file_path}...") @@ -76,7 +246,7 @@ class CardMakerReader(BaseReader): maxSelectPoint=row["maxSelectPoint"], ) - self.logger.info(f"Added gacha {row['gachaId']}") + self.logger.info(f"Added ongeki gacha {row['gachaId']}") def read_ongeki_gacha_card_csv(self, file_path: str) -> None: self.logger.info(f"Reading gacha cards from {file_path}...") @@ -93,7 +263,7 @@ class CardMakerReader(BaseReader): isSelect=True if row["isSelect"] == "1" else False, ) - self.logger.info(f"Added card {row['cardId']} to gacha") + self.logger.info(f"Added ongeki card {row['cardId']} to gacha") def read_ongeki_gacha(self, base_dir: str) -> None: self.logger.info(f"Reading gachas from {base_dir}...") @@ -152,4 +322,4 @@ class CardMakerReader(BaseReader): isCeiling=is_ceiling, maxSelectPoint=max_select_point, ) - self.logger.info(f"Added gacha {gacha_id}") + self.logger.info(f"Added ongeki gacha {gacha_id}") diff --git a/titles/cm/schema/__init__.py b/titles/cm/schema/__init__.py new file mode 100644 index 0000000..a9a2c5b --- /dev/null +++ b/titles/cm/schema/__init__.py @@ -0,0 +1 @@ +__all__ = [] diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 0d9ea89..27fba3a 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -7,4 +7,4 @@ index = Mai2Servlet database = Mai2Data reader = Mai2Reader game_codes = [Mai2Constants.GAME_CODE] -current_schema_version = 2 +current_schema_version = 3 diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 8a48d8b..741ccb6 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -202,6 +202,16 @@ class Mai2Base: 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"]: + self.data.item.put_charge( + user_id, + charge["chargeId"], + charge["stock"], + datetime.strptime(charge["purchaseDate"], "%Y-%m-%d %H:%M:%S"), + datetime.strptime(charge["validDate"], "%Y-%m-%d %H:%M:%S") + ) + if upsert["isNewCharacterList"] and int(upsert["isNewCharacterList"]) > 0: for char in upsert["userCharacterList"]: self.data.item.put_character( @@ -299,10 +309,67 @@ class Mai2Base: return {"userId": data["userId"], "userOption": options_dict} def handle_get_user_card_api_request(self, data: Dict) -> Dict: - return {"userId": data["userId"], "nextIndex": 0, "userCardList": []} + 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"], "%Y-%m-%d %H:%M:%S") + tmp["endDate"] = datetime.strftime( + tmp["endDate"], "%Y-%m-%d %H:%M:%S") + 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: - return {"userId": data["userId"], "length": 0, "userChargeList": []} + 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"], "%Y-%m-%d %H:%M:%S") + tmp["validDate"] = datetime.strftime( + tmp["validDate"], "%Y-%m-%d %H:%M:%S") + + 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) @@ -313,15 +380,13 @@ class Mai2Base: for x in range(next_idx, data["maxCount"]): try: - user_item_list.append( - { - "item_kind": user_items[x]["item_kind"], - "item_id": user_items[x]["item_id"], - "stock": user_items[x]["stock"], - "isValid": user_items[x]["is_valid"], - } - ) - except: + user_item_list.append({ + "itemKind": user_items[x]["itemKind"], + "itemId": user_items[x]["itemId"], + "stock": user_items[x]["stock"], + "isValid": user_items[x]["isValid"] + }) + except IndexError: break if len(user_item_list) == data["maxCount"]: @@ -332,21 +397,18 @@ class Mai2Base: "userId": data["userId"], "nextIndex": next_idx, "itemKind": kind, - "userItemList": user_item_list, + "userItemList": user_item_list } 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: - chara_list.append( - { - "characterId": chara["character_id"], - "level": chara["level"], - "awakening": chara["awakening"], - "useCount": chara["use_count"], - } - ) + tmp = chara._asdict() + tmp.pop("id") + tmp.pop("user") + chara_list.append(tmp) return {"userId": data["userId"], "userCharacterList": chara_list} @@ -417,10 +479,21 @@ class Mai2Base: tmp.pop("user") mlst.append(tmp) - return {"userActivity": {"playList": plst, "musicList": mlst}} + 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: @@ -429,7 +502,11 @@ class Mai2Base: tmp.pop("id") course_list.append(tmp) - return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list} + 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 @@ -540,18 +617,11 @@ class Mai2Base: if songs is not None: for song in songs: - music_detail_list.append( - { - "musicId": song["song_id"], - "level": song["chart_id"], - "playCount": song["play_count"], - "achievement": song["achievement"], - "comboStatus": song["combo_status"], - "syncStatus": song["sync_status"], - "deluxscoreMax": song["dx_score"], - "scoreRank": song["score_rank"], - } - ) + 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 @@ -559,5 +629,5 @@ class Mai2Base: return { "userId": data["userId"], "nextIndex": next_index, - "userMusicList": [{"userMusicDetailList": music_detail_list}], + "userMusicList": [{"userMusicDetailList": music_detail_list}] } diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 0679d1f..3cd1629 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -89,7 +89,7 @@ class Mai2Servlet: ) def render_POST(self, request: Request, version: int, url_path: str) -> bytes: - if url_path.lower() == "/ping": + if url_path.lower() == "ping": return zlib.compress(b'{"returnCode": "1"}') req_raw = request.content.getvalue() diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 072eb3e..d64d954 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -1,5 +1,6 @@ from core.data.schema import BaseData, metadata +from datetime import datetime from typing import Optional, Dict, List from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON @@ -17,11 +18,11 @@ character = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("character_id", Integer, nullable=False), + Column("characterId", Integer, nullable=False), Column("level", Integer, nullable=False, server_default="1"), Column("awakening", Integer, nullable=False, server_default="0"), - Column("use_count", Integer, nullable=False, server_default="0"), - UniqueConstraint("user", "character_id", name="mai2_item_character_uk"), + Column("useCount", Integer, nullable=False, server_default="0"), + UniqueConstraint("user", "characterId", name="mai2_item_character_uk"), mysql_charset="utf8mb4", ) @@ -34,13 +35,13 @@ card = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("card_kind", Integer, nullable=False), - Column("card_id", Integer, nullable=False), - Column("chara_id", Integer, nullable=False), - Column("map_id", Integer, nullable=False), - Column("start_date", String(255), nullable=False), - Column("end_date", String(255), nullable=False), - UniqueConstraint("user", "card_kind", "card_id", name="mai2_item_card_uk"), + Column("cardId", Integer, nullable=False), + Column("cardTypeId", Integer, nullable=False), + Column("charaId", Integer, nullable=False), + Column("mapId", Integer, nullable=False), + Column("startDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), + UniqueConstraint("user", "cardId", "cardTypeId", name="mai2_item_card_uk"), mysql_charset="utf8mb4", ) @@ -53,11 +54,11 @@ item = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("item_kind", Integer, nullable=False), - Column("item_id", Integer, nullable=False), + Column("itemId", Integer, nullable=False), + Column("itemKind", Integer, nullable=False), Column("stock", Integer, nullable=False, server_default="1"), - Column("is_valid", Boolean, nullable=False, server_default="1"), - UniqueConstraint("user", "item_kind", "item_id", name="mai2_item_item_uk"), + Column("isValid", Boolean, nullable=False, server_default="1"), + UniqueConstraint("user", "itemId", "itemKind", name="mai2_item_item_uk"), mysql_charset="utf8mb4", ) @@ -139,11 +140,44 @@ charge = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("charge_id", Integer, nullable=False), + Column("chargeId", Integer, nullable=False), Column("stock", Integer, nullable=False), - Column("purchase_date", String(255), nullable=False), - Column("valid_date", String(255), nullable=False), - UniqueConstraint("user", "charge_id", name="mai2_item_charge_uk"), + Column("purchaseDate", String(255), nullable=False), + Column("validDate", String(255), nullable=False), + UniqueConstraint("user", "chargeId", name="mai2_item_charge_uk"), + mysql_charset="utf8mb4", +) + +print_detail = Table( + "mai2_item_print_detail", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + 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("cardRomVersion", Integer), + Column("isHolograph", Boolean, server_default="1"), + Column("printOption1", Boolean, server_default="0"), + Column("printOption2", Boolean, server_default="0"), + Column("printOption3", Boolean, server_default="0"), + Column("printOption4", Boolean, server_default="0"), + Column("printOption5", Boolean, server_default="0"), + Column("printOption6", Boolean, server_default="0"), + Column("printOption7", Boolean, server_default="0"), + Column("printOption8", Boolean, server_default="0"), + Column("printOption9", Boolean, server_default="0"), + Column("printOption10", Boolean, server_default="0"), + Column("created", String(255), server_default=""), + UniqueConstraint("user", "serialId", name="mai2_item_print_detail_uk"), mysql_charset="utf8mb4", ) @@ -154,15 +188,15 @@ class Mai2ItemData(BaseData): ) -> None: sql = insert(item).values( user=user_id, - item_kind=item_kind, - item_id=item_id, + itemKind=item_kind, + itemId=item_id, stock=stock, - is_valid=is_valid, + isValid=is_valid, ) conflict = sql.on_duplicate_key_update( stock=stock, - is_valid=is_valid, + isValid=is_valid, ) result = self.execute(conflict) @@ -178,7 +212,7 @@ class Mai2ItemData(BaseData): sql = item.select(item.c.user == user_id) else: sql = item.select( - and_(item.c.user == user_id, item.c.item_kind == item_kind) + and_(item.c.user == user_id, item.c.itemKind == item_kind) ) result = self.execute(sql) @@ -190,8 +224,8 @@ class Mai2ItemData(BaseData): sql = item.select( and_( item.c.user == user_id, - item.c.item_kind == item_kind, - item.c.item_id == item_id, + item.c.itemKind == item_kind, + item.c.itemId == item_id, ) ) @@ -382,3 +416,93 @@ class Mai2ItemData(BaseData): if result is None: return None return result.fetchall() + + def put_card( + self, + user_id: int, + card_type_id: int, + card_kind: int, + chara_id: int, + map_id: int, + ) -> Optional[Row]: + sql = insert(card).values( + user=user_id, + cardId=card_type_id, + cardTypeId=card_kind, + charaId=chara_id, + mapId=map_id, + ) + + conflict = sql.on_duplicate_key_update(charaId=chara_id, mapId=map_id) + + result = self.execute(conflict) + if result is None: + self.logger.warn( + f"put_card: failed to insert card! user_id: {user_id}, kind: {kind}" + ) + return None + return result.lastrowid + + def get_cards(self, user_id: int, kind: int = None) -> Optional[Row]: + if kind is None: + sql = card.select(card.c.user == user_id) + else: + sql = card.select(and_(card.c.user == user_id, card.c.cardKind == kind)) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_charge( + self, + user_id: int, + charge_id: int, + stock: int, + purchase_date: datetime, + valid_date: datetime, + ) -> Optional[Row]: + sql = insert(charge).values( + user=user_id, + chargeId=charge_id, + stock=stock, + purchaseDate=purchase_date, + validDate=valid_date, + ) + + conflict = sql.on_duplicate_key_update( + stock=stock, purchaseDate=purchase_date, validDate=valid_date + ) + + result = self.execute(conflict) + if result is None: + self.logger.warn( + f"put_card: failed to insert charge! user_id: {user_id}, chargeId: {charge_id}" + ) + return None + return result.lastrowid + + def get_charges(self, user_id: int) -> Optional[Row]: + sql = charge.select(charge.c.user == user_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + 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_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/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 335a731..1ce8046 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -448,7 +448,9 @@ class Mai2ProfileData(BaseData): return None return result.lastrowid - def get_profile_activity(self, user_id: int, kind: int = None) -> Optional[Row]: + def get_profile_activity( + self, user_id: int, kind: int = None + ) -> Optional[List[Row]]: sql = activity.select( and_( activity.c.user == user_id, @@ -459,4 +461,4 @@ class Mai2ProfileData(BaseData): result = self.execute(sql) if result is None: return None - return result.fetchone() + return result.fetchall() diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 15bf519..4d3291d 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -242,7 +242,7 @@ class Mai2ScoreData(BaseData): return result.lastrowid def get_courses(self, user_id: int) -> Optional[List[Row]]: - sql = course.select(best_score.c.user == user_id) + sql = course.select(course.c.user == user_id) result = self.execute(sql) if result is None: diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index 2908a47..e40e37f 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -53,6 +53,22 @@ ticket = Table( mysql_charset="utf8mb4", ) +cards = Table( + "mai2_static_cards", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("cardId", Integer, nullable=False), + Column("cardName", String(255), nullable=False), + 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("enabled", Boolean, server_default="1"), + UniqueConstraint("version", "cardId", "cardName", name="mai2_static_cards_uk"), + mysql_charset="utf8mb4", +) + class Mai2StaticData(BaseData): def put_game_event( @@ -166,6 +182,8 @@ class Mai2StaticData(BaseData): conflict = sql.on_duplicate_key_update(price=ticket_price) + conflict = sql.on_duplicate_key_update(price=ticket_price) + result = self.execute(conflict) if result is None: self.logger.warn(f"Failed to insert charge {ticket_id} type {ticket_type}") @@ -208,3 +226,24 @@ class Mai2StaticData(BaseData): if result is None: return None return result.fetchone() + + def put_card(self, version: int, card_id: int, card_name: str, **card_data) -> int: + sql = insert(cards).values( + version=version, cardId=card_id, cardName=card_name, **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}") + return None + return result.lastrowid + + def get_enabled_cards(self, version: int) -> Optional[List[Row]]: + sql = cards.select(and_(cards.c.version == version, cards.c.enabled == True)) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() diff --git a/titles/mai2/universe.py b/titles/mai2/universe.py index 56c2d3f..56b3e8f 100644 --- a/titles/mai2/universe.py +++ b/titles/mai2/universe.py @@ -1,4 +1,5 @@ from typing import Any, List, Dict +from random import randint from datetime import datetime, timedelta import pytz import json @@ -13,3 +14,176 @@ class Mai2Universe(Mai2Base): 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 977fce9..4e6c410 100644 --- a/titles/mai2/universeplus.py +++ b/titles/mai2/universeplus.py @@ -4,12 +4,21 @@ import pytz import json from core.config import CoreConfig -from titles.mai2.base import Mai2Base +from titles.mai2.universe import Mai2Universe from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config -class Mai2UniversePlus(Mai2Base): +class Mai2UniversePlus(Mai2Universe): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = super().handle_cm_get_user_preview_api_request(data) + + # hardcode lastDataVersion for CardMaker 1.35 + # user_data["lastDataVersion"] = "1.25.00" + # hardcode lastDataVersion for CardMaker 1.36 + user_data["lastDataVersion"] = "1.30.00" + return user_data diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index 4f7619c..10bb1a8 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -452,7 +452,8 @@ class OngekiBase: tmp.pop("id") items.append(tmp) - xout = kind * 10000000000 + (data["nextIndex"] % 10000000000) + len(items) + xout = kind * 10000000000 + \ + (data["nextIndex"] % 10000000000) + len(items) if len(items) < data["maxCount"] or data["maxCount"] == 0: nextIndex = 0 @@ -851,7 +852,8 @@ class OngekiBase: ) if "userOption" in upsert and len(upsert["userOption"]) > 0: - self.data.profile.put_profile_options(user_id, upsert["userOption"][0]) + self.data.profile.put_profile_options( + user_id, upsert["userOption"][0]) if "userPlaylogList" in upsert: for playlog in upsert["userPlaylogList"]: diff --git a/titles/ongeki/bright.py b/titles/ongeki/bright.py index 4b2a06f..23eeb6c 100644 --- a/titles/ongeki/bright.py +++ b/titles/ongeki/bright.py @@ -93,7 +93,12 @@ class OngekiBright(OngekiBase): 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 {} + return { + "userId": data["userId"], + "length": 0, + "nextIndex": 0, + "userCharacterList": [] + } max_ct = data["maxCount"] next_idx = data["nextIndex"] @@ -543,7 +548,7 @@ class OngekiBright(OngekiBase): "returnCode": 1, "orderId": 0, "serialId": "11111111111111111111", - "apiName": "CMUpsertUserPrintPlaylogApi", + "apiName": "CMUpsertUserPrintPlaylogApi" } def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: @@ -551,7 +556,7 @@ class OngekiBright(OngekiBase): "returnCode": 1, "orderId": 0, "serialId": "11111111111111111111", - "apiName": "CMUpsertUserPrintlogApi", + "apiName": "CMUpsertUserPrintlogApi" } def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index 07c8ff2..7927d84 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -3,7 +3,8 @@ import json import inflection import yaml import string -import logging, coloredlogs +import logging +import coloredlogs import zlib from logging.handlers import TimedRotatingFileHandler from os import path @@ -93,7 +94,7 @@ class OngekiServlet: ) def render_POST(self, request: Request, version: int, url_path: str) -> bytes: - if url_path.lower() == "/ping": + if url_path.lower() == "ping": return zlib.compress(b'{"returnCode": 1}') req_raw = request.content.getvalue() diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index d406597..d826fba 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -706,7 +706,7 @@ class OngekiItemData(BaseData): ) conflict = sql.on_duplicate_key_update( - user=aime_id, serialId=serial_id, **user_print_data + user=aime_id, **user_print_data ) result = self.execute(conflict)