From 4bd1dea6bfb9d75fb2f71a437a41a6c2e361d3fe Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 17 Mar 2023 02:06:15 -0400 Subject: [PATCH 01/17] allnet: add info log to downloadorder --- core/allnet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/allnet.py b/core/allnet.py index 67b47b3..c028a57 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -181,7 +181,9 @@ class AllnetServlet: self.logger.error(e) return b"" + self.logger.info(f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}") resp = AllnetDownloadOrderResponse() + if not self.config.allnet.allow_online_updates: return self.dict_to_http_form_string([vars(resp)]) From 7ca4e6adb93e4fd3ef2e5ffa6665d3d8cd6aba4c Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 17 Mar 2023 02:11:49 -0400 Subject: [PATCH 02/17] fix IP address logging --- core/allnet.py | 4 ++-- core/frontend.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index c028a57..2afb8cf 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -192,7 +192,7 @@ class AllnetServlet: def handle_billing_request(self, request: Request, _: Dict): req_dict = self.billing_req_to_dict(request.content.getvalue()) - request_ip = request.getClientAddress() + request_ip = Utils.get_ip_addr(request) if req_dict is None: self.logger.error(f"Failed to parse request {request.content.getvalue()}") return b"" @@ -225,7 +225,7 @@ class AllnetServlet: return self.dict_to_http_form_string([vars(resp)]) msg = ( - f"Billing checkin from {request.getClientIP()}: game {kc_game} keychip {kc_serial} playcount " + f"Billing checkin from {request_ip}: game {kc_game} keychip {kc_serial} playcount " f"{kc_playcount} billing_type {kc_billigtype} nearfull {kc_nearfull} playlimit {kc_playlimit}" ) self.logger.info(msg) diff --git a/core/frontend.py b/core/frontend.py index e230681..127b174 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -30,7 +30,7 @@ class UserSession(object): class FrontendServlet(resource.Resource): def getChild(self, name: bytes, request: Request): - self.logger.debug(f"{request.getClientIP()} -> {name.decode()}") + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {name.decode()}") if name == b"": return self return resource.Resource.getChild(self, name, request) @@ -84,7 +84,7 @@ class FrontendServlet(resource.Resource): ) def render_GET(self, request): - self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}") + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.uri.decode()}") template = self.environment.get_template("core/frontend/index.jinja") return template.render( server_name=self.config.server.name, @@ -113,7 +113,7 @@ class FE_Base(resource.Resource): class FE_Gate(FE_Base): def render_GET(self, request: Request): - self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}") + self.logger.debug(f"{Utils.get_ip_addr(request)} -> {request.uri.decode()}") uri: str = request.uri.decode() sesh = request.getSession() From 6965132e5b40b5e1ff30121596c0e69c40a8aa53 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 17 Mar 2023 02:16:49 -0400 Subject: [PATCH 03/17] chuni: fix hard error caused by not having the db set up --- titles/chuni/base.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 13f423b..3668b29 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -32,6 +32,9 @@ class ChuniBase: def handle_get_game_charge_api_request(self, data: Dict) -> Dict: game_charge_list = self.data.static.get_enabled_charges(self.version) + + if game_charge_list is None or len(game_charge_list) == 0: + return {"length": 0, "gameChargeList": []} charges = [] for x in range(len(game_charge_list)): @@ -52,6 +55,14 @@ class ChuniBase: def handle_get_game_event_api_request(self, data: Dict) -> Dict: game_events = self.data.static.get_enabled_events(self.version) + if game_events is None or len(game_events) == 0: + self.logger.warn("No enabled events, did you run the reader?") + return { + "type": data["type"], + "length": 0, + "gameEventList": [], + } + event_list = [] for evt_row in game_events: tmp = {} From 5a388e2a24d680ee4d445c12417990ae619b0aa2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 17 Mar 2023 19:53:00 -0400 Subject: [PATCH 04/17] docs: fix casing in game_specific_info.md --- docs/game_specific_info.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 8af5541..f028c6c 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -8,13 +8,13 @@ using the megaime database. Clean installations always create the latest databas # Table of content -- [Supported Games](#Supported-Games) - - [Chunithm](#Chunithm) - - [crossbeats REV.](#crossbeats-REV) - - [maimai DX](#maimai-DX) - - [O.N.G.E.K.I.](#ONGEKI) - - [Card Maker](#Card-Maker) - - [WACCA](#WACCA) +- [Supported Games](#supported-games) + - [Chunithm](#chunithm) + - [crossbeats REV.](#crossbeats-rev) + - [maimai DX](#maimai-dx) + - [O.N.G.E.K.I.](#ongeki) + - [Card Maker](#card-maker) + - [WACCA](#wacca) # Supported Games From 401623f20bcea9e48d14023b2170394ad51c664e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 17 Mar 2023 20:03:07 -0400 Subject: [PATCH 05/17] docs: fix ongeki anchor in game_specific_info.md --- docs/game_specific_info.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index f028c6c..b09d61e 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -12,7 +12,7 @@ using the megaime database. Clean installations always create the latest databas - [Chunithm](#chunithm) - [crossbeats REV.](#crossbeats-rev) - [maimai DX](#maimai-dx) - - [O.N.G.E.K.I.](#ongeki) + - [O.N.G.E.K.I.](#o-n-g-e-k-i) - [Card Maker](#card-maker) - [WACCA](#wacca) From 6ff8c4d931a7450126427f4780bfaf3c4fc9b65a Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 18 Mar 2023 02:12:33 -0400 Subject: [PATCH 06/17] fix mai2 and diva db scripts --- core/data/schema/versions/SBZV_1_rollback.sql | 6 +++--- core/data/schema/versions/SDEZ_2_rollback.sql | 21 +++++++++++++++++++ core/data/schema/versions/SDEZ_3_upgrade.sql | 4 ++-- 3 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 core/data/schema/versions/SDEZ_2_rollback.sql diff --git a/core/data/schema/versions/SBZV_1_rollback.sql b/core/data/schema/versions/SBZV_1_rollback.sql index a7bccce..6389157 100644 --- a/core/data/schema/versions/SBZV_1_rollback.sql +++ b/core/data/schema/versions/SBZV_1_rollback.sql @@ -1,9 +1,9 @@ SET FOREIGN_KEY_CHECKS=0; -ALTER TABLE diva_score DROP COLUMN edition; -ALTER TABLE diva_playlog DROP COLUMN edition; - ALTER TABLE diva_score DROP FOREIGN KEY diva_score_ibfk_1; ALTER TABLE diva_score DROP CONSTRAINT diva_score_uk; ALTER TABLE diva_score ADD CONSTRAINT diva_score_uk UNIQUE (user, pv_id, difficulty); ALTER TABLE diva_score ADD CONSTRAINT diva_score_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE; + +ALTER TABLE diva_score DROP COLUMN edition; +ALTER TABLE diva_playlog DROP COLUMN edition; SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_2_rollback.sql b/core/data/schema/versions/SDEZ_2_rollback.sql new file mode 100644 index 0000000..b631711 --- /dev/null +++ b/core/data/schema/versions/SDEZ_2_rollback.sql @@ -0,0 +1,21 @@ +ALTER TABLE mai2_item_card +CHANGE COLUMN cardId card_id INT NOT NULL AFTER user, +CHANGE COLUMN cardTypeId card_kind INT NOT NULL, +CHANGE COLUMN charaId chara_id INT NOT NULL, +CHANGE COLUMN mapId map_id INT NOT NULL, +CHANGE COLUMN startDate start_date TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00', +CHANGE COLUMN endDate end_date TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00'; + +ALTER TABLE mai2_item_item +CHANGE COLUMN itemId item_id INT NOT NULL AFTER user, +CHANGE COLUMN itemKind item_kind INT NOT NULL, +CHANGE COLUMN isValid is_valid TINYINT(1) NOT NULL DEFAULT '1'; + +ALTER TABLE mai2_item_character +CHANGE COLUMN characterId character_id INT NOT NULL, +CHANGE COLUMN useCount use_count INT NOT NULL DEFAULT '0'; + +ALTER TABLE mai2_item_charge +CHANGE COLUMN chargeId charge_id INT NOT NULL, +CHANGE COLUMN purchaseDate purchase_date TIMESTAMP NOT NULL, +CHANGE COLUMN validDate valid_date TIMESTAMP NOT NULL; \ 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 index c13e1fe..bdb5906 100644 --- a/core/data/schema/versions/SDEZ_3_upgrade.sql +++ b/core/data/schema/versions/SDEZ_3_upgrade.sql @@ -3,8 +3,8 @@ 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'; +CHANGE COLUMN start_date startDate TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00', +CHANGE COLUMN end_date 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, From 188be2dfc16c11efb3b60595b8e260f1c36c7a32 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 18 Mar 2023 02:12:58 -0400 Subject: [PATCH 07/17] database: add autoupgrade command --- core/data/database.py | 74 +++++++++++++++++++++++++++++++--------- core/data/schema/base.py | 9 +++++ dbutils.py | 3 ++ 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/core/data/database.py b/core/data/database.py index 0ce039e..07fe79e 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -145,25 +145,49 @@ class Data: ) return - if not os.path.exists( - f"core/data/schema/versions/{game.upper()}_{version}_{action}.sql" - ): - self.logger.error( - f"Could not find {action} script {game.upper()}_{version}_{action}.sql in core/data/schema/versions folder" - ) - return + if action == "upgrade": + for x in range(old_ver, version): + if not os.path.exists( + f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql" + ): + self.logger.error( + f"Could not find {action} script {game.upper()}_{x + 1}_{action}.sql in core/data/schema/versions folder" + ) + return - with open( - f"core/data/schema/versions/{game.upper()}_{version}_{action}.sql", - "r", - encoding="utf-8", - ) as f: - sql = f.read() + with open( + f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql", + "r", + encoding="utf-8", + ) as f: + sql = f.read() - result = self.base.execute(sql) - if result is None: - self.logger.error("Error execuing sql script!") - return None + result = self.base.execute(sql) + if result is None: + self.logger.error("Error execuing sql script!") + return None + + else: + for x in range(old_ver, version, -1): + if not os.path.exists( + f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql" + ): + self.logger.error( + f"Could not find {action} script {game.upper()}_{x - 1}_{action}.sql in core/data/schema/versions folder" + ) + return + + with open( + f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql", + "r", + encoding="utf-8", + ) as f: + sql = f.read() + + result = self.base.execute(sql) + if result is None: + self.logger.error("Error execuing sql script!") + return None result = self.base.set_schema_ver(version, game) if result is None: @@ -237,3 +261,19 @@ class Data: if not cards: self.logger.info(f"Delete hanging user {user['id']}") self.user.delete_user(user["id"]) + + def autoupgrade(self) -> None: + all_games = self.base.get_all_schema_vers() + if all_games is None: + self.logger.warn("Failed to get schema versions") + + for x in all_games: + game = x["game"].upper() + update_ver = 1 + for y in range(2, 100): + if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"): + update_ver = y + else: + break + + self.migrate_database(game, update_ver, "upgrade") \ No newline at end of file diff --git a/core/data/schema/base.py b/core/data/schema/base.py index 9899f29..f77a9aa 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -2,6 +2,7 @@ import json import logging from random import randrange from typing import Any, Optional, Dict, List +from sqlalchemy.engine import Row from sqlalchemy.engine.cursor import CursorResult from sqlalchemy.engine.base import Connection from sqlalchemy.sql import text, func, select @@ -80,6 +81,14 @@ class BaseData: Generate a random 5-7 digit id """ return randrange(10000, 9999999) + + def get_all_schema_vers(self) -> Optional[List[Row]]: + sql = select(schema_ver) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() def get_schema_ver(self, game: str) -> Optional[int]: sql = select(schema_ver).where(schema_ver.c.game == game) diff --git a/dbutils.py b/dbutils.py index 65b1f8e..176c67e 100644 --- a/dbutils.py +++ b/dbutils.py @@ -64,6 +64,9 @@ if __name__ == "__main__": else: data.migrate_database(args.game, int(args.version), args.action) + elif args.action == "autoupgrade": + data.autoupgrade() + elif args.action == "create-owner": data.create_owner(args.email) From 62b62db5b58c8c63b93644e5518b8e89af67e983 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 18 Mar 2023 12:26:57 -0400 Subject: [PATCH 08/17] wacca: fix favorites, purchasing and unlocking songs, incorrectly displayed grades --- titles/wacca/base.py | 23 ++++++++--------------- titles/wacca/handlers/helpers.py | 16 +++++++++++++++- titles/wacca/handlers/user_info.py | 6 +++--- titles/wacca/index.py | 2 +- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index bc0c09a..275f380 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -635,14 +635,14 @@ class WaccaBase: new_tickets.append([ticket["id"], ticket["ticket_id"], 9999999999]) for item in req.itemsUsed: - if item.itemType == WaccaConstants.ITEM_TYPES["wp"]: + if item.itemType == WaccaConstants.ITEM_TYPES["wp"] and not self.game_config.mods.infinite_wp: if current_wp >= item.quantity: current_wp -= item.quantity self.data.profile.spend_wp(req.profileId, item.quantity) else: return BaseResponse().make() - elif item.itemType == WaccaConstants.ITEM_TYPES["ticket"]: + elif item.itemType == WaccaConstants.ITEM_TYPES["ticket"] and not self.game_config.mods.infinite_tickets: for x in range(len(new_tickets)): if new_tickets[x][1] == item.itemId: self.data.item.spend_ticket(new_tickets[x][0]) @@ -880,7 +880,7 @@ class WaccaBase: user_id = profile["user"] resp.currentWp = profile["wp"] - if req.purchaseType == PurchaseType.PurchaseTypeWP: + if req.purchaseType == PurchaseType.PurchaseTypeWP and not self.game_config.mods.infinite_wp: resp.currentWp -= req.cost self.data.profile.spend_wp(req.profileId, req.cost) @@ -1070,19 +1070,12 @@ class WaccaBase: ): if item.quantity > WaccaConstants.Difficulty.HARD.value: old_score = self.data.score.get_best_score( - user_id, item.itemId, item.quantity + user_id, item.itemId, item.quantity + ) + if not old_score: + self.data.score.put_best_score( + user_id, item.itemId, item.quantity, 0, [0] * 5, [0] * 13, 0, 0 ) - if not old_score: - self.data.score.put_best_score( - user_id, - item.itemId, - item.quantity, - 0, - [0] * 5, - [0] * 13, - 0, - 0, - ) if item.quantity == 0: item.quantity = WaccaConstants.Difficulty.HARD.value diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index 9e9847a..02b8bd7 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -577,7 +577,21 @@ class SongDetailGradeCountsV2(SongDetailGradeCountsV1): self.ssspCt = counts[12] def make(self) -> List: - return super().make() + [self.spCt, self.sspCt, self.ssspCt] + return [ + self.dCt, + self.cCt, + self.bCt, + self.aCt, + self.aaCt, + self.aaaCt, + self.sCt, + self.spCt, + self.ssCt, + self.sspCt, + self.sssCt, + self.ssspCt, + self.masterCt, + ] class BestScoreDetailV1: diff --git a/titles/wacca/handlers/user_info.py b/titles/wacca/handlers/user_info.py index 8c67db9..bf6b74b 100644 --- a/titles/wacca/handlers/user_info.py +++ b/titles/wacca/handlers/user_info.py @@ -11,9 +11,9 @@ class UserInfoUpdateRequest(BaseRequest): self.profileId = int(self.params[0]) self.optsUpdated: List[UserOption] = [] self.unknown2: List = self.params[2] - self.datesUpdated: List[DateUpdate] = [] - self.favoritesAdded: List[int] = self.params[4] - self.favoritesRemoved: List[int] = self.params[5] + self.datesUpdated: List[DateUpdate] = [] + self.favoritesRemoved: List[int] = self.params[4] + self.favoritesAdded: List[int] = self.params[5] for x in self.params[1]: self.optsUpdated.append(UserOption(x[0], x[1])) diff --git a/titles/wacca/index.py b/titles/wacca/index.py index 0b51849..a59cda1 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -102,7 +102,7 @@ class WaccaServlet: resp.message = "不正なリクエスト エラーです" return end(resp.make()) - if "/api/" in url_path: + if "api/" in url_path: func_to_find = ( "handle_" + url_path.partition("api/")[2].replace("/", "_") + "_request" ) From dfd38778892f5d2d36e0ea3b59ad7d49fb7375a3 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 18 Mar 2023 12:55:04 -0400 Subject: [PATCH 09/17] wacca: rename locked -> lock_state in MusicUpdateDetailV1 --- titles/wacca/handlers/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/wacca/handlers/helpers.py b/titles/wacca/handlers/helpers.py index 02b8bd7..f86be2f 100644 --- a/titles/wacca/handlers/helpers.py +++ b/titles/wacca/handlers/helpers.py @@ -942,7 +942,7 @@ class MusicUpdateDetailV1: self.score = 0 self.lowestMissCount = 0 self.maxSkillPts = 0 - self.locked = 0 + self.lock_state = 0 def make(self) -> List: return [ @@ -954,7 +954,7 @@ class MusicUpdateDetailV1: self.score, self.lowestMissCount, self.maxSkillPts, - self.locked, + self.lock_state, ] From 12fd663eb7c2e7a85269eeac665d7adbaab0bf4e Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 19 Mar 2023 23:29:57 -0400 Subject: [PATCH 10/17] wacca: fix songs locking after playing them after unlcoked them with an ex unlock ticket, fixes #12 --- titles/wacca/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 275f380..020c167 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -836,7 +836,7 @@ class WaccaBase: resp.songDetail.grades = SongDetailGradeCountsV2(counts=grades) else: resp.songDetail.grades = SongDetailGradeCountsV1(counts=grades) - + resp.songDetail.lock_state = 1 return resp.make() # TODO: Coop and vs data From ac8a660e1308b2604a6d754da9d6fc21c85877c4 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 19 Mar 2023 23:52:33 -0400 Subject: [PATCH 11/17] allnet: allow unknown games to auth in develop mode --- core/allnet.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/core/allnet.py b/core/allnet.py index 2afb8cf..0d4fbf7 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -95,14 +95,21 @@ class AllnetServlet: self.logger.debug(f"Allnet request: {vars(req)}") if req.game_id not in self.uri_registry: - msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}." - self.data.base.log_event( - "allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg - ) - self.logger.warn(msg) + if not self.config.server.is_develop: + msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}." + self.data.base.log_event( + "allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg + ) + self.logger.warn(msg) - resp.stat = 0 - return self.dict_to_http_form_string([vars(resp)]) + resp.stat = 0 + return self.dict_to_http_form_string([vars(resp)]) + + else: + self.logger.info(f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}") + resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/" + resp.host = f"{self.config.title.hostname}:{self.config.title.port}" + return self.dict_to_http_form_string([vars(resp)]) resp.uri, resp.host = self.uri_registry[req.game_id] From 2a290f2a3df154e39c29d0067f230532b705241c Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Fri, 24 Mar 2023 18:10:10 +0100 Subject: [PATCH 12/17] chuni: added teams and ticket saving, fixed last played song --- example_config/chuni.yaml | 11 +++++++++ titles/chuni/__init__.py | 2 +- titles/chuni/base.py | 43 ++++++++++++++++++++++++---------- titles/chuni/config.py | 28 ++++++++++++++++++++++ titles/chuni/new.py | 14 ++++------- titles/chuni/newplus.py | 10 ++++---- titles/chuni/schema/profile.py | 6 +++-- titles/chuni/schema/static.py | 17 ++++++-------- 8 files changed, 91 insertions(+), 40 deletions(-) diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index 3794f06..c70ccab 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -2,5 +2,16 @@ server: enable: True loglevel: "info" +team: + name: ARTEMiS + +version: + "11": + rom: 2.00.00 + data: 2.00.00 + "12": + rom: 2.05.00 + data: 2.05.00 + crypto: encrypted_only: False \ No newline at end of file diff --git a/titles/chuni/__init__.py b/titles/chuni/__init__.py index 7256b10..89cd4f5 100644 --- a/titles/chuni/__init__.py +++ b/titles/chuni/__init__.py @@ -7,4 +7,4 @@ index = ChuniServlet database = ChuniData reader = ChuniReader game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW] -current_schema_version = 1 +current_schema_version = 3 diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 3668b29..936e5ef 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -32,7 +32,7 @@ class ChuniBase: def handle_get_game_charge_api_request(self, data: Dict) -> Dict: game_charge_list = self.data.static.get_enabled_charges(self.version) - + if game_charge_list is None or len(game_charge_list) == 0: return {"length": 0, "gameChargeList": []} @@ -130,7 +130,7 @@ class ChuniBase: return { "userId": data["userId"], "length": len(activity_list), - "kind": data["kind"], + "kind": int(data["kind"]), "userActivityList": activity_list, } @@ -451,13 +451,13 @@ class ChuniBase: "playerLevel": profile["playerLevel"], "rating": profile["rating"], "headphone": profile["headphone"], - "chargeState": "1", + "chargeState": 1, "userNameEx": profile["userName"], } def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: - recet_rating_list = self.data.profile.get_profile_recent_rating(data["userId"]) - if recet_rating_list is None: + recent_rating_list = self.data.profile.get_profile_recent_rating(data["userId"]) + if recent_rating_list is None: return { "userId": data["userId"], "length": 0, @@ -466,8 +466,8 @@ class ChuniBase: return { "userId": data["userId"], - "length": len(recet_rating_list["recentRating"]), - "userRecentRatingList": recet_rating_list["recentRating"], + "length": len(recent_rating_list["recentRating"]), + "userRecentRatingList": recent_rating_list["recentRating"], } def handle_get_user_region_api_request(self, data: Dict) -> Dict: @@ -479,8 +479,24 @@ class ChuniBase: } def handle_get_user_team_api_request(self, data: Dict) -> Dict: - # TODO: Team - return {"userId": data["userId"], "teamId": 0} + # TODO: use the database "chuni_profile_team" with a GUI + team_name = self.game_cfg.team.team_name + if team_name == "": + return {"userId": data["userId"], "teamId": 0} + + return { + "userId": data["userId"], + "teamId": 1, + "teamRank": 1, + "teamName": team_name, + "userTeamPoint": { + "userId": data["userId"], + "teamId": 1, + "orderId": 1, + "teamPoint": 1, + "aggrDate": data["playDate"], + }, + } def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict: return { @@ -583,6 +599,9 @@ class ChuniBase: return {"returnCode": "1"} def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + # add tickets after they got bought, this makes sure the tickets are + # still valid after an unsuccessful logout + self.data.profile.put_profile_charge(data["userId"], data["userCharge"]) return {"returnCode": "1"} def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: @@ -603,7 +622,5 @@ class ChuniBase: 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 + "userNetBattleData": {"recentNBSelectMusicList": []}, + } diff --git a/titles/chuni/config.py b/titles/chuni/config.py index c4a351f..ac527af 100644 --- a/titles/chuni/config.py +++ b/titles/chuni/config.py @@ -21,6 +21,32 @@ class ChuniServerConfig: ) +class ChuniTeamConfig: + def __init__(self, parent_config: "ChuniConfig") -> None: + self.__config = parent_config + + @property + def team_name(self) -> str: + return CoreConfig.get_config_field( + self.__config, "chuni", "team", "name", default="" + ) + + +class ChuniVersionConfig: + def __init__(self, parent_config: "ChuniConfig") -> None: + self.__config = parent_config + + def version_rom(self, version: int) -> str: + return CoreConfig.get_config_field( + self.__config, "chuni", "version", f"{version}", "rom", default="2.00.00" + ) + + def version_data(self, version: int) -> str: + return CoreConfig.get_config_field( + self.__config, "chuni", "version", f"{version}", "data", default="2.00.00" + ) + + class ChuniCryptoConfig: def __init__(self, parent_config: "ChuniConfig") -> None: self.__config = parent_config @@ -46,4 +72,6 @@ class ChuniCryptoConfig: class ChuniConfig(dict): def __init__(self) -> None: self.server = ChuniServerConfig(self) + self.team = ChuniTeamConfig(self) + self.version = ChuniVersionConfig(self) self.crypto = ChuniCryptoConfig(self) diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 611c6d2..2c50c07 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -49,8 +49,8 @@ class ChuniNew(ChuniBase): "matchEndTime": match_end, "matchTimeLimit": 99, "matchErrorLimit": 9999, - "romVersion": "2.00.00", - "dataVersion": "2.00.00", + "romVersion": self.game_cfg.version.version_rom(self.version), + "dataVersion": self.game_cfg.version.version_data(self.version), "matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", @@ -454,9 +454,7 @@ class ChuniNew(ChuniBase): # 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 + user_id, id=upsert["orderId"], hasCompleted=True ) return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"} @@ -467,10 +465,6 @@ class ChuniNew(ChuniBase): # 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 - ) + 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 9dec9aa..83debb5 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -1,6 +1,4 @@ -from datetime import datetime, timedelta from typing import Dict, Any -import pytz from core.config import CoreConfig from titles.chuni.new import ChuniNew @@ -15,8 +13,12 @@ class ChuniNewPlus(ChuniNew): def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) - ret["gameSetting"]["romVersion"] = "2.05.00" - ret["gameSetting"]["dataVersion"] = "2.05.00" + ret["gameSetting"]["romVersion"] = self.game_cfg.version.version_rom( + self.version + ) + ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version_data( + self.version + ) ret["gameSetting"][ "matchingUri" ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index 9000b9b..e35769c 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -558,8 +558,10 @@ class ChuniProfileData(BaseData): return result.lastrowid def get_profile_activity(self, aime_id: int, kind: int) -> Optional[List[Row]]: - sql = select(activity).where( - and_(activity.c.user == aime_id, activity.c.kind == kind) + sql = ( + select(activity) + .where(and_(activity.c.user == aime_id, activity.c.kind == kind)) + .order_by(activity.c.sortNumber.desc()) # to get the last played track ) result = self.execute(sql) diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 0784872..4882046 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -390,20 +390,17 @@ class ChuniStaticData(BaseData): return None return result.fetchall() - def get_gacha_card_by_character(self, gacha_id: int, chara_id: int) -> Optional[Dict]: + 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() + 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 - )) + 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: From 6489e3ca21b2e3473ead2e029b80b19dc5c453e2 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sun, 26 Mar 2023 04:33:53 -0400 Subject: [PATCH 13/17] pokken: add skeleton LoadUser response --- titles/pokken/base.py | 92 +++++++++++++++++++++++++++++++++++++++++- titles/pokken/index.py | 1 - 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 6c2bf26..1a9d5da 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -1,8 +1,10 @@ from datetime import datetime, timedelta import json, logging from typing import Any, Dict +import random -from core.config import CoreConfig +from core.data import Data +from core import CoreConfig from titles.pokken.config import PokkenConfig from titles.pokken.proto import jackal_pb2 @@ -13,6 +15,7 @@ class PokkenBase: self.game_cfg = game_cfg self.version = 0 self.logger = logging.getLogger("pokken") + self.data = Data(core_cfg) def handle_noop(self, request: Any) -> bytes: res = jackal_pb2.Response() @@ -123,7 +126,94 @@ class PokkenBase: ranking.event_end = True ranking.modify_date = int(datetime.now().timestamp() / 1000) res.load_ranking.CopyFrom(ranking) + return res.SerializeToString() + def handle_load_user(self, request: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.LOAD_USER + access_code = request.load_user.access_code + user_id = self.data.card.get_user_id_from_card(access_code) + + if user_id is None: # TODO: Toggle auto-register + user_id = self.data.user.create_user() + card_id = self.data.card.create_card(user_id, access_code) + + self.logger.info(f"Register new card {access_code} (UserId {user_id}, CardId {card_id})") + + # TODO: Check for user data. For now just treat ever card-in as a new user + + load_usr = jackal_pb2.LoadUserResponseData() + load_usr.commidserv_result = 1 + load_usr.load_hash = 1 + load_usr.cardlock_status = False + load_usr.banapass_id = user_id + load_usr.access_code = access_code + load_usr.new_card_flag = True + load_usr.precedent_release_flag = 0xFFFFFFFF + load_usr.navi_newbie_flag = True + load_usr.navi_enable_flag = True + load_usr.pad_vibrate_flag = True + load_usr.home_region_code = 0 + load_usr.home_loc_name = "" + load_usr.pref_code = 0 + load_usr.trainer_name = "Newb" + str(random.randint(1111,999999)) + load_usr.trainer_rank_point = 0 + load_usr.wallet = 0 + load_usr.fight_money = 0 + load_usr.score_point = 0 + load_usr.grade_max_num = 0 + load_usr.total_play_days = 0 + load_usr.play_date_time = 0 + load_usr.lucky_box_fail_num = 0 + load_usr.event_reward_get_flag = 0 + load_usr.rank_pvp_all = 0 + load_usr.rank_pvp_loc = 0 + load_usr.rank_cpu_all = 0 + load_usr.rank_cpu_loc = 0 + load_usr.rank_event = 0 + load_usr.awake_num = 0 + load_usr.use_support_num = 0 + load_usr.rankmatch_flag = 0 + load_usr.title_text_id = 0 + load_usr.title_plate_id = 0 + load_usr.title_decoration_id = 0 + load_usr.navi_trainer = 0 + load_usr.navi_version_id = 0 + load_usr.aid_skill = 0 + load_usr.comment_text_id = 0 + load_usr.comment_word_id = 0 + load_usr.latest_use_pokemon = 0 + load_usr.ex_ko_num = 0 + load_usr.wko_num = 0 + load_usr.timeup_win_num = 0 + load_usr.cool_ko_num = 0 + load_usr.perfect_ko_num = 0 + load_usr.record_flag = 0 + load_usr.site_register_status = 0 + load_usr.continue_num = 0 + load_usr.event_state = 0 + load_usr.event_id = 0 + load_usr.sp_bonus_category_id_1 = 0 + load_usr.sp_bonus_key_value_1 = 0 + load_usr.sp_bonus_category_id_2 = 0 + load_usr.sp_bonus_key_value_2 = 0 + + res.load_user.CopyFrom(load_usr) + return res.SerializeToString() + + def handle_set_bnpassid_lock(self, data: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SET_BNPASSID_LOCK + return res.SerializeToString() + + def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes: + res = jackal_pb2.Response() + res.result = 1 + res.type = jackal_pb2.MessageType.SET_BNPASSID_LOCK + return res.SerializeToString() + def handle_matching_noop(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict: return {} diff --git a/titles/pokken/index.py b/titles/pokken/index.py index 42a5a0e..a7a328f 100644 --- a/titles/pokken/index.py +++ b/titles/pokken/index.py @@ -124,7 +124,6 @@ class PokkenServlet(resource.Resource): self.logger.debug(pokken_request) ret = handler(pokken_request) - self.logger.debug(f"Response: {ret}") return ret def handle_matching(self, request: Request) -> bytes: From 541fe76a7c5b02b2b06de99aaf14fa5ce565eebd Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 28 Mar 2023 01:09:16 +0200 Subject: [PATCH 14/17] cardmaker: fixed chuni endless loading --- titles/chuni/new.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 2c50c07..2963850 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -269,10 +269,10 @@ class ChuniNew(ChuniBase): tmp = user_print_list[x]._asdict() print_list.append(tmp["cardId"]) - if len(user_print_list) >= max_ct: + if len(print_list) >= max_ct: break - if len(user_print_list) >= max_ct: + if len(print_list) >= max_ct: next_idx = next_idx + max_ct else: next_idx = -1 From 1aa92458f447b36bfef4e52ddf0547728f136383 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 28 Mar 2023 18:28:57 +0200 Subject: [PATCH 15/17] chuni: added login bonus (+importer), fixed config strings --- example_config/chuni.yaml | 7 +- titles/chuni/base.py | 133 +++++++++++++++++++++++++++----- titles/chuni/config.py | 27 +++++-- titles/chuni/new.py | 4 +- titles/chuni/newplus.py | 8 +- titles/chuni/read.py | 74 ++++++++++++++++++ titles/chuni/schema/item.py | 75 ++++++++++++++++-- titles/chuni/schema/static.py | 140 ++++++++++++++++++++++++++++++++++ 8 files changed, 424 insertions(+), 44 deletions(-) diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index c70ccab..bbac976 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -5,11 +5,14 @@ server: team: name: ARTEMiS +mods: + use_login_bonus: True + version: - "11": + 11: rom: 2.00.00 data: 2.00.00 - "12": + 12: rom: 2.05.00 data: 2.05.00 diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 936e5ef..95c35e8 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -23,7 +23,92 @@ class ChuniBase: self.version = ChuniConstants.VER_CHUNITHM 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"]}) + """ + Handles the login bonus logic, required for the game because + getUserLoginBonus gets called after getUserItem and therefore the + items needs to be inserted in the database before they get requested. + + Adds a bonusCount after a user logged in after 24 hours, makes sure + loginBonus 30 gets looped, only show the login banner every 24 hours, + adds the bonus to items (itemKind 6) + """ + + # ignore the login bonus if disabled in config + if not self.game_cfg.mods.use_login_bonus: + return {"returnCode": 1} + + user_id = data["userId"] + login_bonus_presets = self.data.static.get_login_bonus_presets(self.version) + + for preset in login_bonus_presets: + # check if a user already has some pogress and if not add the + # login bonus entry + user_login_bonus = self.data.item.get_login_bonus( + user_id, self.version, preset["id"] + ) + if user_login_bonus is None: + self.data.item.put_login_bonus(user_id, self.version, preset["id"]) + # yeah i'm lazy + user_login_bonus = self.data.item.get_login_bonus( + user_id, self.version, preset["id"] + ) + + # skip the login bonus entirely if its already finished + if user_login_bonus["isFinished"]: + continue + + # make sure the last login is more than 24 hours ago + if user_login_bonus["lastUpdateDate"] < datetime.now() - timedelta( + hours=24 + ): + # increase the login day counter and update the last login date + bonus_count = user_login_bonus["bonusCount"] + 1 + last_update_date = datetime.now() + + all_login_boni = self.data.static.get_login_bonus( + self.version, preset["id"] + ) + + # assume its not None + max_needed_days = all_login_boni[0]["needLoginDayCount"] + + # make sure to not show login boni after all days got redeemed + is_finished = False + if bonus_count > max_needed_days: + # assume that all login preset ids under 3000 needs to be + # looped, like 30 and 40 are looped, 40 does not work? + if preset["id"] < 3000: + bonus_count = 1 + else: + is_finished = True + + # grab the item for the corresponding day + login_item = self.data.static.get_login_bonus_by_required_days( + self.version, preset["id"], bonus_count + ) + if login_item is not None: + # now add the present to the database so the + # handle_get_user_item_api_request can grab them + self.data.item.put_item( + user_id, + { + "itemId": login_item["presentId"], + "itemKind": 6, + "stock": login_item["itemNum"], + "isValid": True, + }, + ) + + self.data.item.put_login_bonus( + user_id, + self.version, + preset["id"], + bonusCount=bonus_count, + lastUpdateDate=last_update_date, + isWatched=False, + isFinished=is_finished, + ) + return {"returnCode": 1} def handle_game_logout_api_request(self, data: Dict) -> Dict: @@ -309,26 +394,28 @@ class ChuniBase: } def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: - """ - Unsure how to get this to trigger... - """ + user_id = data["userId"] + user_login_bonus = self.data.item.get_all_login_bonus(user_id, self.version) + if user_login_bonus is None: + return {"userId": user_id, "length": 0, "userLoginBonusList": []} + + user_login_list = [] + for bonus in user_login_bonus: + user_login_list.append( + { + "presetId": bonus["presetId"], + "bonusCount": bonus["bonusCount"], + "lastUpdateDate": datetime.strftime( + bonus["lastUpdateDate"], "%Y-%m-%d %H:%M:%S" + ), + "isWatched": bonus["isWatched"], + } + ) + return { - "userId": data["userId"], - "length": 2, - "userLoginBonusList": [ - { - "presetId": "10", - "bonusCount": "0", - "lastUpdateDate": "1970-01-01 09:00:00", - "isWatched": "true", - }, - { - "presetId": "20", - "bonusCount": "0", - "lastUpdateDate": "1970-01-01 09:00:00", - "isWatched": "true", - }, - ], + "userId": user_id, + "length": len(user_login_list), + "userLoginBonusList": user_login_list, } def handle_get_user_map_api_request(self, data: Dict) -> Dict: @@ -596,6 +683,12 @@ class ChuniBase: for emoney in upsert["userEmoneyList"]: self.data.profile.put_profile_emoney(user_id, emoney) + if "userLoginBonusList" in upsert: + for login in upsert["userLoginBonusList"]: + self.data.item.put_login_bonus( + user_id, self.version, login["presetId"], isWatched=True + ) + return {"returnCode": "1"} def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: diff --git a/titles/chuni/config.py b/titles/chuni/config.py index ac527af..48d70d2 100644 --- a/titles/chuni/config.py +++ b/titles/chuni/config.py @@ -32,19 +32,29 @@ class ChuniTeamConfig: ) +class ChuniModsConfig: + def __init__(self, parent_config: "ChuniConfig") -> None: + self.__config = parent_config + + @property + def use_login_bonus(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "chuni", "mods", "use_login_bonus", default=True + ) + + class ChuniVersionConfig: def __init__(self, parent_config: "ChuniConfig") -> None: self.__config = parent_config - def version_rom(self, version: int) -> str: + def version(self, version: int) -> Dict: + """ + in the form of: + 11: {"rom": 2.00.00, "data": 2.00.00} + """ return CoreConfig.get_config_field( - self.__config, "chuni", "version", f"{version}", "rom", default="2.00.00" - ) - - def version_data(self, version: int) -> str: - return CoreConfig.get_config_field( - self.__config, "chuni", "version", f"{version}", "data", default="2.00.00" - ) + self.__config, "chuni", "version", default={} + )[version] class ChuniCryptoConfig: @@ -73,5 +83,6 @@ class ChuniConfig(dict): def __init__(self) -> None: self.server = ChuniServerConfig(self) self.team = ChuniTeamConfig(self) + self.mods = ChuniModsConfig(self) self.version = ChuniVersionConfig(self) self.crypto = ChuniCryptoConfig(self) diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 2963850..67b6fcc 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -49,8 +49,8 @@ class ChuniNew(ChuniBase): "matchEndTime": match_end, "matchTimeLimit": 99, "matchErrorLimit": 9999, - "romVersion": self.game_cfg.version.version_rom(self.version), - "dataVersion": self.game_cfg.version.version_data(self.version), + "romVersion": self.game_cfg.version.version(self.version)["rom"], + "dataVersion": self.game_cfg.version.version(self.version)["data"], "matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index 83debb5..422d57a 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -13,12 +13,8 @@ class ChuniNewPlus(ChuniNew): def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) - ret["gameSetting"]["romVersion"] = self.game_cfg.version.version_rom( - self.version - ) - ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version_data( - self.version - ) + ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)["rom"] + ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"] ret["gameSetting"][ "matchingUri" ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/" diff --git a/titles/chuni/read.py b/titles/chuni/read.py index abf78e3..9ac13c4 100644 --- a/titles/chuni/read.py +++ b/titles/chuni/read.py @@ -42,6 +42,80 @@ class ChuniReader(BaseReader): self.read_music(f"{dir}/music") self.read_charges(f"{dir}/chargeItem") self.read_avatar(f"{dir}/avatarAccessory") + self.read_login_bonus(f"{dir}/") + + def read_login_bonus(self, root_dir: str) -> None: + for root, dirs, files in walk(f"{root_dir}loginBonusPreset"): + for dir in dirs: + if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"): + with open(f"{root}/{dir}/LoginBonusPreset.xml", "rb") as fp: + bytedata = fp.read() + strdata = bytedata.decode("UTF-8") + + xml_root = ET.fromstring(strdata) + for name in xml_root.findall("name"): + id = name.find("id").text + name = name.find("str").text + is_enabled = ( + True if xml_root.find("disableFlag").text == "false" else False + ) + + result = self.data.static.put_login_bonus_preset( + self.version, id, name, is_enabled + ) + + if result is not None: + self.logger.info(f"Inserted login bonus preset {id}") + else: + self.logger.warn(f"Failed to insert login bonus preset {id}") + + for bonus in xml_root.find("infos").findall("LoginBonusDataInfo"): + for name in bonus.findall("loginBonusName"): + bonus_id = name.find("id").text + bonus_name = name.find("str").text + + if path.exists( + f"{root_dir}/loginBonus/loginBonus{bonus_id}/LoginBonus.xml" + ): + with open( + f"{root_dir}/loginBonus/loginBonus{bonus_id}/LoginBonus.xml", + "rb", + ) as fp: + bytedata = fp.read() + strdata = bytedata.decode("UTF-8") + + bonus_root = ET.fromstring(strdata) + + for present in bonus_root.findall("present"): + present_id = present.find("id").text + present_name = present.find("str").text + + item_num = int(bonus_root.find("itemNum").text) + need_login_day_count = int( + bonus_root.find("needLoginDayCount").text + ) + login_bonus_category_type = int( + bonus_root.find("loginBonusCategoryType").text + ) + + result = self.data.static.put_login_bonus( + self.version, + id, + bonus_id, + bonus_name, + present_id, + present_name, + item_num, + need_login_day_count, + login_bonus_category_type, + ) + + if result is not None: + self.logger.info(f"Inserted login bonus {bonus_id}") + else: + self.logger.warn( + f"Failed to insert login bonus {bonus_id}" + ) def read_events(self, evt_dir: str) -> None: for root, dirs, files in walk(evt_dir): diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index 124d7df..4ffcf93 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -184,8 +184,73 @@ print_detail = Table( mysql_charset="utf8mb4", ) +login_bonus = Table( + "chuni_item_login_bonus", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("presetId", Integer, nullable=False), + Column("bonusCount", Integer, nullable=False, server_default="0"), + Column("lastUpdateDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), + Column("isWatched", Boolean, server_default="0"), + Column("isFinished", Boolean, server_default="0"), + UniqueConstraint("version", "user", "presetId", name="chuni_item_login_bonus_uk"), + mysql_charset="utf8mb4", +) + class ChuniItemData(BaseData): + def put_login_bonus( + self, user_id: int, version: int, preset_id: int, **login_bonus_data + ) -> Optional[int]: + sql = insert(login_bonus).values( + version=version, user=user_id, presetId=preset_id, **login_bonus_data + ) + + conflict = sql.on_duplicate_key_update(presetId=preset_id, **login_bonus_data) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def get_all_login_bonus( + self, user_id: int, version: int, is_finished: bool = False + ) -> Optional[List[Row]]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.user == user_id, + login_bonus.c.isFinished == is_finished, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_login_bonus( + self, user_id: int, version: int, preset_id: int + ) -> Optional[Row]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.user == user_id, + login_bonus.c.presetId == preset_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + def put_character(self, user_id: int, character_data: Dict) -> Optional[int]: character_data["user"] = user_id @@ -335,7 +400,7 @@ class ChuniItemData(BaseData): sql = print_state.select( and_( print_state.c.user == aime_id, - print_state.c.hasCompleted == has_completed + print_state.c.hasCompleted == has_completed, ) ) @@ -351,7 +416,7 @@ class ChuniItemData(BaseData): and_( print_state.c.user == aime_id, print_state.c.gachaId == gacha_id, - print_state.c.hasCompleted == has_completed + print_state.c.hasCompleted == has_completed, ) ) @@ -380,9 +445,7 @@ class ChuniItemData(BaseData): user=aime_id, serialId=serial_id, **user_print_data ) - conflict = sql.on_duplicate_key_update( - user=aime_id, **user_print_data - ) + conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data) result = self.execute(conflict) if result is None: @@ -390,4 +453,4 @@ class ChuniItemData(BaseData): f"put_user_print_detail: Failed to insert! aime_id: {aime_id}" ) return None - return result.lastrowid \ No newline at end of file + return result.lastrowid diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 4882046..bef58c0 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -122,8 +122,148 @@ gacha_cards = Table( mysql_charset="utf8mb4", ) +login_bonus_preset = Table( + "chuni_static_login_bonus_preset", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("presetName", String(255), nullable=False), + Column("isEnabled", Boolean, server_default="1"), + UniqueConstraint("version", "id", name="chuni_static_login_bonus_preset_uk"), + mysql_charset="utf8mb4", +) + +login_bonus = Table( + "chuni_static_login_bonus", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column( + "presetId", + ForeignKey( + "chuni_static_login_bonus_preset.id", + ondelete="cascade", + onupdate="cascade", + ), + nullable=False, + ), + Column("loginBonusId", Integer, nullable=False), + Column("loginBonusName", String(255), nullable=False), + Column("presentId", Integer, nullable=False), + Column("presentName", String(255), nullable=False), + Column("itemNum", Integer, nullable=False), + Column("needLoginDayCount", Integer, nullable=False), + Column("loginBonusCategoryType", Integer, nullable=False), + UniqueConstraint( + "version", "presetId", "loginBonusId", name="chuni_static_login_bonus_uk" + ), + mysql_charset="utf8mb4", +) + class ChuniStaticData(BaseData): + def put_login_bonus( + self, + version: int, + preset_id: int, + login_bonus_id: int, + login_bonus_name: str, + present_id: int, + present_ame: str, + item_num: int, + need_login_day_count: int, + login_bonus_category_type: int, + ) -> Optional[int]: + sql = insert(login_bonus).values( + version=version, + presetId=preset_id, + loginBonusId=login_bonus_id, + loginBonusName=login_bonus_name, + presentId=present_id, + presentName=present_ame, + itemNum=item_num, + needLoginDayCount=need_login_day_count, + loginBonusCategoryType=login_bonus_category_type, + ) + + conflict = sql.on_duplicate_key_update( + loginBonusName=login_bonus_name, + presentName=present_ame, + itemNum=item_num, + needLoginDayCount=need_login_day_count, + loginBonusCategoryType=login_bonus_category_type, + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def get_login_bonus( + self, version: int, preset_id: int, + ) -> Optional[List[Row]]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.presetId == preset_id, + ) + ).order_by(login_bonus.c.needLoginDayCount.desc()) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_login_bonus_by_required_days( + self, version: int, preset_id: int, need_login_day_count: int + ) -> Optional[Row]: + sql = login_bonus.select( + and_( + login_bonus.c.version == version, + login_bonus.c.presetId == preset_id, + login_bonus.c.needLoginDayCount == need_login_day_count, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_login_bonus_preset( + self, version: int, preset_id: int, preset_name: str, is_enabled: bool + ) -> Optional[int]: + sql = insert(login_bonus_preset).values( + id=preset_id, + version=version, + presetName=preset_name, + isEnabled=is_enabled, + ) + + conflict = sql.on_duplicate_key_update( + presetName=preset_name, isEnabled=is_enabled + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def get_login_bonus_presets( + self, version: int, is_enabled: bool = True + ) -> Optional[List[Row]]: + sql = login_bonus_preset.select( + and_( + login_bonus_preset.c.version == version, + login_bonus_preset.c.isEnabled == is_enabled, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + def put_event( self, version: int, event_id: int, type: int, name: str ) -> Optional[int]: From 571a691e0eebae59a5f46494a697fe58ac4a11f1 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 28 Mar 2023 18:54:27 +0200 Subject: [PATCH 16/17] chuni: added `use_login_bonus` check to UserLoginBonusApi --- titles/chuni/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 95c35e8..d41d415 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -396,7 +396,8 @@ class ChuniBase: def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: user_id = data["userId"] user_login_bonus = self.data.item.get_all_login_bonus(user_id, self.version) - if user_login_bonus is None: + # ignore the loginBonus request if its disabled in config + if user_login_bonus is None or not self.game_cfg.mods.use_login_bonus: return {"userId": user_id, "length": 0, "userLoginBonusList": []} user_login_list = [] From a60d52b7422b639ca982e63b07c0e8b5508bef2e Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Thu, 30 Mar 2023 22:58:45 +0200 Subject: [PATCH 17/17] chuni: fixed missing login boni IndexError --- titles/chuni/base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/titles/chuni/base.py b/titles/chuni/base.py index d41d415..0eaabff 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -69,7 +69,13 @@ class ChuniBase: self.version, preset["id"] ) - # assume its not None + # skip the current bonus preset if no boni were found + if all_login_boni is None or len(all_login_boni) < 1: + self.logger.warn( + f"No bonus entries found for bonus preset {preset['id']}" + ) + continue + max_needed_days = all_login_boni[0]["needLoginDayCount"] # make sure to not show login boni after all days got redeemed