From 2af7751504819d96bf8b6c5e04ac2faee2e0c936 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Wed, 15 Mar 2023 20:03:22 +0000 Subject: [PATCH] Added support for maimai and Chunithm in Card Maker 1.34/1.35 (#14) Co-authored-by: Dniel97 Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/14 Co-authored-by: Dniel97 Co-committed-by: Dniel97 --- core/data/schema/versions/SDED_1_upgrade.sql | 99 ------ core/data/schema/versions/SDEZ_3_upgrade.sql | 21 ++ docs/game_specific_info.md | 351 ++++++++++++++++++ read.py | 6 +- readme.md | 5 +- titles/chuni/base.py | 8 + titles/chuni/index.py | 2 +- titles/chuni/new.py | 354 ++++++++++++++++++- titles/chuni/newplus.py | 7 + titles/chuni/schema/item.py | 156 ++++++++ titles/chuni/schema/static.py | 163 +++++++++ titles/cm/__init__.py | 2 + titles/cm/{cm136.py => cm135.py} | 8 +- titles/cm/const.py | 4 +- titles/cm/database.py | 8 + titles/cm/index.py | 8 +- titles/cm/read.py | 182 +++++++++- titles/cm/schema/__init__.py | 1 + titles/mai2/__init__.py | 2 +- titles/mai2/base.py | 140 ++++++-- titles/mai2/index.py | 2 +- titles/mai2/schema/item.py | 174 +++++++-- titles/mai2/schema/profile.py | 6 +- titles/mai2/schema/score.py | 2 +- titles/mai2/schema/static.py | 39 ++ titles/mai2/universe.py | 174 +++++++++ titles/mai2/universeplus.py | 11 +- titles/ongeki/base.py | 6 +- titles/ongeki/bright.py | 11 +- titles/ongeki/brightmemory.py | 6 +- titles/ongeki/index.py | 5 +- titles/ongeki/schema/item.py | 2 +- 32 files changed, 1763 insertions(+), 202 deletions(-) delete mode 100644 core/data/schema/versions/SDED_1_upgrade.sql create mode 100644 core/data/schema/versions/SDEZ_3_upgrade.sql create mode 100644 docs/game_specific_info.md rename titles/cm/{cm136.py => cm135.py} (89%) create mode 100644 titles/cm/database.py create mode 100644 titles/cm/schema/__init__.py 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/docs/game_specific_info.md b/docs/game_specific_info.md new file mode 100644 index 0000000..8af5541 --- /dev/null +++ b/docs/game_specific_info.md @@ -0,0 +1,351 @@ +# ARTEMiS Games Documentation + +Below are all supported games with supported version ids in order to use +the corresponding importer and database upgrades. + +**Important: The described database upgrades are only required if you are using an old database schema, f.e. still +using the megaime database. Clean installations always create the latest database structure!** + +# 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 + +Games listed below have been tested and confirmed working. + +## Chunithm + +### SDBT + +| Version ID | Version Name | +|------------|--------------------| +| 0 | Chunithm | +| 1 | Chunithm+ | +| 2 | Chunithm Air | +| 3 | Chunithm Air + | +| 4 | Chunithm Star | +| 5 | Chunithm Star + | +| 6 | Chunithm Amazon | +| 7 | Chunithm Amazon + | +| 8 | Chunithm Crystal | +| 9 | Chunithm Crystal + | +| 10 | Chunithm Paradise | + +### SDHD/SDBT + +| Version ID | Version Name | +|------------|-----------------| +| 11 | Chunithm New!! | +| 12 | Chunithm New!!+ | + + +### Importer + +In order to use the importer locate your game installation folder and execute: + +```shell +python read.py --series SDBT --version --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder +``` + +The importer for Chunithm will import: Events, Music, Charge Items and Avatar Accesories. + +### Database upgrade + +Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see +which version is the latest, f.e. `SDBT_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to +perform all previous updates as well: + +```shell +python dbutils.py --game SDBT --version 2 upgrade +python dbutils.py --game SDBT --version 3 upgrade +``` + +## crossbeats REV. + +### SDCA + +| Version ID | Version Name | +|------------|------------------------------------| +| 0 | crossbeats REV. | +| 1 | crossbeats REV. SUNRISE | +| 2 | crossbeats REV. SUNRISE S2 | +| 3 | crossbeats REV. SUNRISE S2 Omnimix | + +### Importer + +In order to use the importer you need to use the provided `Export.csv` file: + +```shell +python read.py --series SDCA --version --binfolder titles/cxb/data +``` + +The importer for crossbeats REV. will import Music. + +### Config + +Config file is located in `config/cxb.yaml`. + +| Option | Info | +|------------------------|------------------------------------------------------------| +| `hostname` | Requires a proper `hostname` (not localhost!) to run | +| `ssl_enable` | Enables/Disables the use of the `ssl_cert` and `ssl_key` | +| `port` | Set your unsecure port number | +| `port_secure` | Set your secure/SSL port number | +| `ssl_cert`, `ssl_key` | Enter your SSL certificate (requires not self signed cert) | + + +## maimai DX + +### SDEZ + +| Version ID | Version Name | +|------------|-------------------------| +| 0 | maimai DX | +| 1 | maimai DX PLUS | +| 2 | maimai DX Splash | +| 3 | maimai DX Splash PLUS | +| 4 | maimai DX Universe | +| 5 | maimai DX Universe PLUS | + +### Importer + +In order to use the importer locate your game installation folder and execute: + +```shell +python read.py --series SDEZ --version --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder +``` + +The importer for maimai DX will import Events, Music and Tickets. + +**NOTE: It is required to use the importer because the game will +crash without it!** + +### Database upgrade + +Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDEZ_2_upgrade.sql`. In order to upgrade to version 2 in this case you need to perform all previous updates as well: + +```shell +python dbutils.py --game SDEZ --version 2 upgrade +``` + +## Hatsune Miku Project Diva + +### SBZV + +| Version ID | Version Name | +|------------|---------------------------------| +| 0 | Project Diva Arcade | +| 1 | Project Diva Arcade Future Tone | + + +### Importer + +In order to use the importer locate your game installation folder and execute: + +```shell +python read.py --series SBZV --version --binfolder /path/to/game/data/diva --optfolder /path/to/game/data/diva/mdata +``` + +The importer for Project Diva Arcade will all required data in order to use +the Shop, Modules and Customizations. + +### Config + +Config file is located in `config/diva.yaml`. + +| Option | Info | +|----------------------|-------------------------------------------------------------------------------------------------| +| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased | +| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased | + + +### Database upgrade + +Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see +which version is the latest, f.e. `SBZV_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to +perform all previous updates as well: + +```shell +python dbutils.py --game SBZV --version 2 upgrade +python dbutils.py --game SBZV --version 3 upgrade +python dbutils.py --game SBZV --version 4 upgrade +``` + +## O.N.G.E.K.I. + +### SDDT + +| Version ID | Version Name | +|------------|----------------------------| +| 0 | O.N.G.E.K.I. | +| 1 | O.N.G.E.K.I. + | +| 2 | O.N.G.E.K.I. Summer | +| 3 | O.N.G.E.K.I. Summer + | +| 4 | O.N.G.E.K.I. Red | +| 5 | O.N.G.E.K.I. Red + | +| 6 | O.N.G.E.K.I. Bright | +| 7 | O.N.G.E.K.I. Bright Memory | + + +### Importer + +In order to use the importer locate your game installation folder and execute: + +```shell +python read.py --series SDDT --version --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder +``` + +The importer for O.N.G.E.K.I. will all all Cards, Music and Events. + +**NOTE: The Importer is required for Card Maker.** + +### Config + +Config file is located in `config/ongeki.yaml`. + +| Option | Info | +|------------------|----------------------------------------------------------------------------------------------------------------| +| `enabled_gachas` | Enter all gacha IDs for Card Maker to work, other than default may not work due to missing cards added to them | + +Note: 1149 and higher are only for Card Maker 1.35 and higher and will be ignored on lower versions. + +### Database upgrade + +Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see +which version is the latest, f.e. `SDDT_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to +perform all previous updates as well: + +```shell +python dbutils.py --game SDDT --version 2 upgrade +python dbutils.py --game SDDT --version 3 upgrade +python dbutils.py --game SDDT --version 4 upgrade +``` + +## Card Maker + +### SDED + +| Version ID | Version Name | +|------------|-----------------| +| 0 | Card Maker 1.34 | +| 1 | Card Maker 1.35 | + + +### Support status + +* Card Maker 1.34: + * Chunithm New!!: Yes + * maimai DX Universe: Yes + * O.N.G.E.K.I. Bright: Yes + +* Card Maker 1.35: + * Chunithm New!!+: Yes + * maimai DX Universe PLUS: Yes + * O.N.G.E.K.I. Bright Memory: Yes + + +### Importer + +In order to use the importer you need to use the provided `.csv` files (which are required for O.N.G.E.K.I.) and the +option folders: + +```shell +python read.py --series SDED --version --binfolder titles/cm/cm_data --optfolder /path/to/cardmaker/option/folder +``` + +**If you haven't already executed the O.N.G.E.K.I. importer, make sure you import all cards!** + +```shell +python read.py --series SDDT --version --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder +``` + +Also make sure to import all maimai and Chunithm data as well: + +```shell +python read.py --series SDED --version --binfolder /path/to/cardmaker/CardMaker_Data +``` + +The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai/Chunithm) and the hardcoded +Cards for each Gacha (O.N.G.E.K.I. only). + +**NOTE: Without executing the importer Card Maker WILL NOT work!** + + +### O.N.G.E.K.I. Gachas + +Gacha "無料ガチャ" can only pull from the free cards with the following probabilities: 94%: R, 5% SR and 1% chance of +getting an SSR card + +Gacha "無料ガチャ(SR確定)" can only pull from free SR cards with prob: 92% SR and 8% chance of getting an SSR card + +Gacha "レギュラーガチャ" can pull from every card added to ongeki_static_cards with the following prob: 77% R, 20% SR +and 3% chance of getting an SSR card + +All other (limited) gachas can pull from every card added to ongeki_static_cards but with the promoted cards +(click on the green button under the banner) having a 10 times higher chance to get pulled + +### Chunithm Gachas + +All cards in Chunithm (basically just the characters) have the same rarity to it just pulls randomly from all cards +from a given gacha but made sure you cannot pull the same card twice in the same 5 times gacha roll. + +### Notes + +Card Maker 1.34 will only load an O.N.G.E.K.I. Bright profile (1.30). Card Maker 1.35 will only load an O.N.G.E.K.I. +Bright Memory profile (1.35). +The gachas inside the `ongeki.yaml` will make sure only the right gacha ids for the right CM version will be loaded. +Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded for CM 1.35. + +**NOTE: There is currently no way to load/use the (printed) maimai DX cards!** + +## WACCA + +### SDFE + +| Version ID | Version Name | +|------------|---------------| +| 0 | WACCA | +| 1 | WACCA S | +| 2 | WACCA Lily | +| 3 | WACCA Lily R | +| 4 | WACCA Reverse | + + +### Importer + +In order to use the importer locate your game installation folder and execute: + +```shell +python read.py --series SDFE --version --binfolder /path/to/game/WindowsNoEditor/Mercury/Content +``` + +The importer for WACCA will import all Music data. + +### Config + +Config file is located in `config/wacca.yaml`. + +| Option | Info | +|--------------------|-----------------------------------------------------------------------------| +| `always_vip` | Enables/Disables VIP, if disabled it needs to be purchased manually in game | +| `infinite_tickets` | Always set the "unlock expert" tickets to 5 | +| `infinite_wp` | Sets the user WP to `999999` | +| `enabled_gates` | Enter all gate IDs which should be enabled in game | + + +### Database upgrade + +Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDFE_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to perform all previous updates as well: + +```shell +python dbutils.py --game SDFE --version 2 upgrade +python dbutils.py --game SDFE --version 3 upgrade +``` diff --git a/read.py b/read.py index 341c502..a1bd0ab 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 @@ -134,7 +135,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/readme.md b/readme.md index d64cede..4afc225 100644 --- a/readme.md +++ b/readme.md @@ -17,7 +17,7 @@ Games listed below have been tested and confirmed working. Only game versions ol + Card Maker + 1.34.xx - + 1.36.xx + + 1.35.xx + Ongeki + All versions up to Bright Memory @@ -36,5 +36,8 @@ Games listed below have been tested and confirmed working. Only game versions ol ## Setup guides Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md) and [ubuntu](docs/INSTALL_UBUNTU.md) to setup and run the server. +## Game specific information +Read [Games specific info](docs/game_specific_info.md) for all supported games, importer settings, configuration option and database upgrades. + ## Production guide See the [production guide](docs/prod.md) for running a production server. 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 de01346..b5648fb 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..9dec9aa 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -30,3 +30,10 @@ 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" + 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/cm136.py b/titles/cm/cm135.py similarity index 89% rename from titles/cm/cm136.py rename to titles/cm/cm135.py index fb5b6b5..782f07a 100644 --- a/titles/cm/cm136.py +++ b/titles/cm/cm135.py @@ -11,10 +11,10 @@ from titles.cm.const import CardMakerConstants from titles.cm.config import CardMakerConfig -class CardMaker136(CardMakerBase): +class CardMaker135(CardMakerBase): def __init__(self, core_cfg: CoreConfig, game_cfg: CardMakerConfig) -> None: super().__init__(core_cfg, game_cfg) - self.version = CardMakerConstants.VER_CARD_MAKER_136 + self.version = CardMakerConstants.VER_CARD_MAKER_135 def handle_get_game_connect_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_connect_api_request(data) @@ -26,13 +26,13 @@ class CardMaker136(CardMakerBase): ret["gameConnectList"][0]["titleUri"] = f"{uri}/SDHD/205/" ret["gameConnectList"][1]["titleUri"] = f"{uri}/SDEZ/125/" ret["gameConnectList"][2]["titleUri"] = f"{uri}/SDDT/135/" - + return ret def handle_get_game_setting_api_request(self, data: Dict) -> Dict: ret = super().handle_get_game_setting_api_request(data) ret["gameSetting"]["dataVersion"] = "1.35.00" - ret["gameSetting"]["ongekiCmVersion"] = "1.35.04" + ret["gameSetting"]["ongekiCmVersion"] = "1.35.03" ret["gameSetting"]["chuniCmVersion"] = "2.05.00" ret["gameSetting"]["maimaiCmVersion"] = "1.25.00" return ret diff --git a/titles/cm/const.py b/titles/cm/const.py index c5627ee..09f289e 100644 --- a/titles/cm/const.py +++ b/titles/cm/const.py @@ -4,9 +4,9 @@ class CardMakerConstants: CONFIG_NAME = "cardmaker.yaml" VER_CARD_MAKER = 0 - VER_CARD_MAKER_136 = 1 + VER_CARD_MAKER_135 = 1 - VERSION_NAMES = ("Card Maker 1.34", "Card Maker 1.36") + VERSION_NAMES = ("Card Maker 1.34", "Card Maker 1.35") @classmethod def game_ver_to_string(cls, ver: int): 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/index.py b/titles/cm/index.py index d082aad..d544e59 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -15,7 +15,7 @@ from core.config import CoreConfig from titles.cm.config import CardMakerConfig from titles.cm.const import CardMakerConstants from titles.cm.base import CardMakerBase -from titles.cm.cm136 import CardMaker136 +from titles.cm.cm135 import CardMaker135 class CardMakerServlet: @@ -29,7 +29,7 @@ class CardMakerServlet: self.versions = [ CardMakerBase(core_cfg, self.game_cfg), - CardMaker136(core_cfg, self.game_cfg), + CardMaker135(core_cfg, self.game_cfg), ] self.logger = logging.getLogger("cardmaker") @@ -87,8 +87,8 @@ class CardMakerServlet: if version >= 130 and version < 135: # Card Maker internal_ver = CardMakerConstants.VER_CARD_MAKER - elif version >= 135 and version < 140: # Card Maker - internal_ver = CardMakerConstants.VER_CARD_MAKER_136 + elif version >= 135 and version < 136: # Card Maker 1.35 + internal_ver = CardMakerConstants.VER_CARD_MAKER_135 if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: # If we get a 32 character long hex string, it's a hash and we're 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..54fe896 100644 --- a/titles/mai2/universeplus.py +++ b/titles/mai2/universeplus.py @@ -4,12 +4,19 @@ 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" + 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/brightmemory.py b/titles/ongeki/brightmemory.py index 954d0e5..6e2548b 100644 --- a/titles/ongeki/brightmemory.py +++ b/titles/ongeki/brightmemory.py @@ -142,8 +142,8 @@ class OngekiBrightMemory(OngekiBright): user_data = super().handle_cm_get_user_data_api_request(data) # hardcode Card Maker version for now - # Card Maker 1.34.00 = 1.30.01 - # Card Maker 1.36.00 = 1.35.04 - user_data["userData"]["compatibleCmVersion"] = "1.35.04" + # Card Maker 1.34 = 1.30.01 + # Card Maker 1.35 = 1.35.03 + user_data["userData"]["compatibleCmVersion"] = "1.35.03" return user_data 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)