diff --git a/Dockerfile b/Dockerfile index 81252b1..f946002 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,7 @@ COPY dbutils.py dbutils.py COPY read.py read.py ADD core core ADD titles titles +ADD config config ADD logs logs ADD cert cert diff --git a/changelog.md b/changelog.md index 75ca217..9ad97c5 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,18 @@ Documenting updates to ARTEMiS, to be updated every time the master branch is pu ### Card Maker + Added support for maimai DX FESTiVAL PLUS +## 20231001 +### Initial D THE ARCADE ++ Added support for Initial D THE ARCADE S2 + + Story mode progress added + + Bunta Challenge/Touhou Project modes added + + Time Trials added + + Leaderboards added, but doesn't refresh sometimes + + Theory of Street mode added (with CPUs) + + Play Stamp/Timetrial events added + + Frontend to download profile added + + Importer to import profiles added + ## 20230716 ### General + Docker files added (#19) diff --git a/core/data/schema/versions/SDDT_5_rollback.sql b/core/data/schema/versions/SDDT_5_rollback.sql new file mode 100644 index 0000000..007716c --- /dev/null +++ b/core/data/schema/versions/SDDT_5_rollback.sql @@ -0,0 +1,22 @@ +SET FOREIGN_KEY_CHECKS=0; + +ALTER TABLE ongeki_user_event_point DROP COLUMN version; +ALTER TABLE ongeki_user_event_point DROP COLUMN rank; +ALTER TABLE ongeki_user_event_point DROP COLUMN type; +ALTER TABLE ongeki_user_event_point DROP COLUMN date; + +ALTER TABLE ongeki_user_tech_event DROP COLUMN version; + +ALTER TABLE ongeki_user_mission_point DROP COLUMN version; + +ALTER TABLE ongeki_static_events DROP COLUMN endDate; + +DROP TABLE ongeki_tech_event_ranking; +DROP TABLE ongeki_static_music_ranking_list; +DROP TABLE ongeki_static_rewards; +DROP TABLE ongeki_static_present_list; +DROP TABLE ongeki_static_tech_music; +DROP TABLE ongeki_static_client_testmode; +DROP TABLE ongeki_static_game_point; + +SET FOREIGN_KEY_CHECKS=1; diff --git a/core/data/schema/versions/SDDT_6_upgrade.sql b/core/data/schema/versions/SDDT_6_upgrade.sql new file mode 100644 index 0000000..82d5336 --- /dev/null +++ b/core/data/schema/versions/SDDT_6_upgrade.sql @@ -0,0 +1,98 @@ +SET FOREIGN_KEY_CHECKS=0; + +ALTER TABLE ongeki_user_event_point ADD COLUMN version INTEGER NOT NULL; +ALTER TABLE ongeki_user_event_point ADD COLUMN rank INTEGER; +ALTER TABLE ongeki_user_event_point ADD COLUMN type INTEGER NOT NULL; +ALTER TABLE ongeki_user_event_point ADD COLUMN date VARCHAR(25); + +ALTER TABLE ongeki_user_tech_event ADD COLUMN version INTEGER NOT NULL; + +ALTER TABLE ongeki_user_mission_point ADD COLUMN version INTEGER NOT NULL; + +ALTER TABLE ongeki_static_events ADD COLUMN endDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; + +CREATE TABLE ongeki_tech_event_ranking ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + user INT NOT NULL, + version INT NOT NULL, + date VARCHAR(25), + eventId INT NOT NULL, + rank INT, + totalPlatinumScore INT NOT NULL, + totalTechScore INT NOT NULL, + UNIQUE KEY ongeki_tech_event_ranking_uk (user, eventId), + CONSTRAINT ongeki_tech_event_ranking_ibfk1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE ongeki_static_music_ranking_list ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + version INT NOT NULL, + musicId INT NOT NULL, + point INT NOT NULL, + userName VARCHAR(255), + UNIQUE KEY ongeki_static_music_ranking_list_uk (version, musicId) +); + +CREATE TABLE ongeki_static_rewards ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + version INT NOT NULL, + rewardId INT NOT NULL, + rewardName VARCHAR(255) NOT NULL, + itemKind INT NOT NULL, + itemId INT NOT NULL, + UNIQUE KEY ongeki_tech_event_ranking_uk (version, rewardId) +); + +CREATE TABLE ongeki_static_present_list ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + version INT NOT NULL, + presentId INT NOT NULL, + presentName VARCHAR(255) NOT NULL, + rewardId INT NOT NULL, + stock INT NOT NULL, + message VARCHAR(255), + startDate VARCHAR(25) NOT NULL, + endDate VARCHAR(25) NOT NULL, + UNIQUE KEY ongeki_static_present_list_uk (version, presentId, rewardId) +); + +CREATE TABLE ongeki_static_tech_music ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + version INT NOT NULL, + eventId INT NOT NULL, + musicId INT NOT NULL, + level INT NOT NULL, + UNIQUE KEY ongeki_static_tech_music_uk (version, musicId, eventId) +); + +CREATE TABLE ongeki_static_client_testmode ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + regionId INT NOT NULL, + placeId INT NOT NULL, + clientId VARCHAR(11) NOT NULL, + updateDate TIMESTAMP NOT NULL, + isDelivery BOOLEAN NOT NULL, + groupId INT NOT NULL, + groupRole INT NOT NULL, + continueMode INT NOT NULL, + selectMusicTime INT NOT NULL, + advertiseVolume INT NOT NULL, + eventMode INT NOT NULL, + eventMusicNum INT NOT NULL, + patternGp INT NOT NULL, + limitGp INT NOT NULL, + maxLeverMovable INT NOT NULL, + minLeverMovable INT NOT NULL, + UNIQUE KEY ongeki_static_client_testmode_uk (clientId) +); + +CREATE TABLE ongeki_static_game_point ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, + type INT NOT NULL, + cost INT NOT NULL, + startDate VARCHAR(25) NOT NULL DEFAULT "2000-01-01 05:00:00.0", + endDate VARCHAR(25) NOT NULL DEFAULT "2099-01-01 05:00:00.0", + UNIQUE KEY ongeki_static_game_point_uk (type) +); + +SET FOREIGN_KEY_CHECKS=1; diff --git a/docs/INSTALL_WINDOWS.md b/docs/INSTALL_WINDOWS.md index e88def3..b976b26 100644 --- a/docs/INSTALL_WINDOWS.md +++ b/docs/INSTALL_WINDOWS.md @@ -10,11 +10,11 @@ This step-by-step guide assumes that you are using a fresh install of Windows 10 3. Make sure that you enable "Create shortcuts for installed applications" and "Add Python to environment variables" and hit Install ## Install MySQL 8.0 -1. Download MySQL 8.0 Server : [Link](https://cdn.mysql.com//Downloads/MySQLInstaller/mysql-installer-web-community-8.0.31.0.msi) -2. Install mysql-installer-web-community-8.0.31.0.msi +1. Download MySQL 8.0 Server : [Link](https://dev.mysql.com/get/Downloads/MySQLInstaller/mysql-installer-community-8.0.34.0.msi) +2. Install mysql-installer-web-community-8.0.34.0.msi 1. Click on "Add ..." on the side 2. Click on the "+" next to MySQL Servers - 3. Make sure MySQL Server 8.0.29 - X64 is under the products to be installed. + 3. Make sure MySQL Server 8.0.34 - X64 is under the products to be installed. 4. Hit Next and Next once installed 5. Select the configuration type "Development Computer" 6. Hit Next @@ -23,9 +23,10 @@ This step-by-step guide assumes that you are using a fresh install of Windows 10 9. Leave everything under Windows Service as default and hit Next > 10. Click on Execute and for it to finish and hit Next> and then Finish 3. Open MySQL 8.0 Command Line Client and login as your root user -4. Type those commands to create your user and the database -``` -CREATE USER 'aime'@'localhost' IDENTIFIED BY 'MyStrongPass.'; +4. Change `` to a new password for the user aime, type those commands to create your user and the database + +```sql +CREATE USER 'aime'@'localhost' IDENTIFIED BY ''; CREATE DATABASE aime; GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; FLUSH PRIVILEGES; @@ -34,33 +35,50 @@ exit; ## Install Python modules 1. Change your work path to the artemis-master folder using 'cd' and install the requirements: -> pip install -r requirements.txt -## Copy/Rename the folder example_config to config +```shell +pip install -r requirements.txt +``` -## Adjust /config/core.yaml +## Copy/Rename the folder `example_config` to `config` -1. Make sure to change the server listen_address to be set to your local machine IP (ex.: 192.168.1.xxx) +## Adjust `config/core.yaml` + +1. Make sure to change the server `hostname` to be set to your local machine IP (ex.: 192.168.xxx.xxx) - In case you want to run this only locally, set the following values: -``` + +```yaml server: listen_address: 0.0.0.0 title: - hostname: localhost + hostname: 192.168.xxx.xxx +``` + +1. Adjust the proper MySQL information you created earlier +```yaml +database: + host: "localhost" + username: "aime" + password: "" + name: "aime" ``` -2. Adjust the proper MySQL information you created earlier 3. Add the AimeDB key at the bottom of the file 4. If the webui is needed, change the flag from False to True ## Create the database tables for ARTEMiS -> python dbutils.py create + +```shell +python dbutils.py create +``` ## Firewall Adjustements Make sure the following ports are open both on your router and local Windows firewall in case you want to use this for public use (NOT recommended): > Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8080 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha ## Running the ARTEMiS instance -> python index.py +```shell +python index.py +``` # Troubleshooting @@ -78,6 +96,7 @@ Make sure the following ports are open both on your router and local Windows fir ## AttributeError: module 'collections' has no attribute 'Hashable' 1. This means the pyYAML module is obsolete, simply rerun pip with the -U (force update) flag, as shown below. - Change your work path to the artemis-master (or artemis-develop) folder using 'cd' and run the following commands: -``` + +```shell pip install -r requirements.txt -U ``` diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 9f39a7e..fa2a250 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -6,6 +6,12 @@ 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!** +To upgrade the core database and the database for every game, execute: + +```shell +python dbutils.py autoupgrade +``` + # Table of content - [Supported Games](#supported-games) @@ -16,6 +22,7 @@ using the megaime database. Clean installations always create the latest databas - [Card Maker](#card-maker) - [WACCA](#wacca) - [Sword Art Online Arcade](#sao) + - [Initial D THE ARCADE](#initial-d-the-arcade) # Supported Games @@ -27,7 +34,7 @@ Games listed below have been tested and confirmed working. ### SDBT | Version ID | Version Name | -|------------|-----------------------| +| ---------- | --------------------- | | 0 | CHUNITHM | | 1 | CHUNITHM PLUS | | 2 | CHUNITHM AIR | @@ -43,7 +50,7 @@ Games listed below have been tested and confirmed working. ### SDHD/SDBT | Version ID | Version Name | -|------------|---------------------| +| ---------- | ------------------- | | 11 | CHUNITHM NEW!! | | 12 | CHUNITHM NEW PLUS!! | | 13 | CHUNITHM SUN | @@ -83,9 +90,7 @@ crypto: ### 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_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to -perform all previous updates as well: +Always make sure your database (tables) are up-to-date: ```shell python dbutils.py --game SDBT upgrade @@ -146,7 +151,7 @@ The songId is based on the actual ID within your version of Chunithm. ### SDCA | Version ID | Version Name | -|------------|------------------------------------| +| ---------- | ---------------------------------- | | 0 | crossbeats REV. | | 1 | crossbeats REV. SUNRISE | | 2 | crossbeats REV. SUNRISE S2 | @@ -166,26 +171,26 @@ The importer for crossbeats REV. will import Music. 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) | +| 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 -| Game Code | Version ID | Version Name | -|-----------|------------|-------------------------| +| Game Code | Version ID | Version Name | +| --------- | ---------- | ------------ | For versions pre-dx | Game Code | Version ID | Version Name | -|-----------|------------|-------------------------| +| --------- | ---------- | ----------------------- | | SBXL | 0 | maimai | | SBXL | 1 | maimai PLUS | | SBZF | 2 | maimai GreeN | @@ -227,11 +232,12 @@ The importer for maimai Pre-DX will import Events and Music. Not all games will ### 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: +Always make sure your database (tables) are up-to-date: ```shell python dbutils.py --game SDEZ upgrade ``` + Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code! ## Hatsune Miku Project Diva @@ -239,7 +245,7 @@ Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code! ### SBZV | Version ID | Version Name | -|------------|---------------------------------| +| ---------- | ------------------------------- | | 0 | Project Diva Arcade | | 1 | Project Diva Arcade Future Tone | @@ -260,7 +266,7 @@ the Shop, Modules and Customizations. 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 | @@ -270,9 +276,7 @@ In order to use custom PV Lists, simply drop in your .dat files inside of /title ### 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: +Always make sure your database (tables) are up-to-date: ```shell python dbutils.py --game SBZV upgrade @@ -283,7 +287,7 @@ python dbutils.py --game SBZV upgrade ### 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 | @@ -311,7 +315,7 @@ The importer for O.N.G.E.K.I. will all all Cards, Music and Events. 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 | | `crypto` | This option is used to enable the TLS Encryption | @@ -328,20 +332,80 @@ crypto: ### 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: +Always make sure your database (tables) are up-to-date: ```shell python dbutils.py --game SDDT upgrade ``` +### Controlling Events (Ranking Event, Technical Challenge Event, Mission Event) + +Events are controlled by 2 types of enabled events: +- RankingEvent (type 6), TechChallengeEvent (type 17) +- AcceptRankingEvent (type 7), AcceptTechChallengeEvent (type 18) + +Both Ranking and Accept must be enabled for event to function properly + +Event will run for the time specified in startDate and endDate + +AcceptRankingEvent and AcceptTechChallengeEvent are reward period for events, which specify from what startDate until endDate you can collect the rewards for attending the event, so the reward period must start in the future, e.g. : + +- RankingEvent startDate 2023-12-01 - endDate 2023-12-30 - period in which whole event is running +- AcceptRankingEvent startDate 2023-12-23 - endDate 2023-12-30 - period in which you can collect rewards for the event + +If player misses the AcceptRankingEvent period - ranking will be invalidated and receive lowest reward from the event (typically 500x money) + +Technical Challenge Song List: + +Songs that are used for Technical Challenge are not stored anywhere in data files, so you need to fill the database table by yourself, you can gather all songs that should be in Technical Challenges from ONGEKI japanese wikis, or, you can create your own sets: + +Database table : `ongeki_static_tech_music` + +``` +id: Id in table, just increment for each entry +version: version of the game you want the tech challenge to be in (from RED and up) +eventId: Id of the event in ongeki_static_events, insert the Id of the TechChallengeEvent (type 17) you want the song be assigned to +musicId: Id of the song you want to add, use songId from ongeki_static_music table +level: Difficulty of the song you want to track during the event, from 0(basic) to 3(master) + +``` + +Current implementation of Ranking and Technical Challenge Events are updated on every profile save to the Network, and Ranked on each player login, in official specification, calculation for current rank on the network should be done in the maintenance window + +Mission Event (type 13) is a monthly type of event, which is used when another event doesn't have it's own Ranking or Technical Challenge Event running, only one Mission Event should be running at a time, so enable only the specific Mission you want to run currently on the Network + +If you're often trying fresh cards, registering new profiles etc., you can also consider disabling all Announcement Events (type 1), as it will disable all the banners that pop up on login (they show up only once though, so if you click through them once they won't show again) + +Event type 2 in Database are Advertisement Movies, enable only 1 you want to currently play, and disable others + + +Present and Reward List - populate reward list using read.py + +Create present for players by adding an entry in `ongeki_static_present_list` +``` +id: unique for each entry +version: game version you want the present be in +presentId: id of the present - starts with 1001 and go up from that, must be unique for each reward(don't set multiple rewardIds with same presentId) +presentName: present name which will be shown on the bottom when received +rewardId: ID of item from ongeki_static_rewards +stock: how many you want to give (like 5 copies of same card, or 10000 money, etc.) +message: no idea, can be left empty +startDate: date when to start giving out +endDate: date when ends +``` + +After inserting present to the table, add the presentId into players `ongeki_static_item`, where itemKind is 9, itemId is the presentId, and stock set 1 and isValid to 1 + +After that, on next login the present should be received (or whenever it supposed to happen) + + + ## Card Maker ### SDED | Version ID | Version Name | -|------------|-----------------| +| ---------- | --------------- | | 0 | Card Maker 1.30 | | 1 | Card Maker 1.35 | @@ -463,7 +527,7 @@ Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded fo ### SDFE | Version ID | Version Name | -|------------|---------------| +| ---------- | ------------- | | 0 | WACCA | | 1 | WACCA S | | 2 | WACCA Lily | @@ -486,7 +550,7 @@ The importer for WACCA will import all Music data. 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` | @@ -495,7 +559,7 @@ Config file is located in `config/wacca.yaml`. ### 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: +Always make sure your database (tables) are up-to-date: ```shell python dbutils.py --game SDFE upgrade @@ -540,9 +604,9 @@ Below is a list of VIP rewards. Currently, VIP is not implemented, and thus thes ### SDEW -| Version ID | Version Name | -|------------|---------------| -| 0 | SAO | +| Version ID | Version Name | +| ---------- | ------------ | +| 0 | SAO | ### Importer @@ -559,16 +623,16 @@ The importer for SAO will import all items, heroes, support skills and titles da Config file is located in `config/sao.yaml`. -| Option | Info | -|--------------------|-----------------------------------------------------------------------------| -| `hostname` | Changes the server listening address for Mucha | -| `port` | Changes the listing port | -| `auto_register` | Allows the game to handle the automatic registration of new cards | +| Option | Info | +| --------------- | ----------------------------------------------------------------- | +| `hostname` | Changes the server listening address for Mucha | +| `port` | Changes the listing port | +| `auto_register` | Allows the game to handle the automatic registration of new cards | ### 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. `SDEW_1_upgrade.sql`. In order to upgrade to version 3 in this case you need to perform all previous updates as well: +Always make sure your database (tables) are up-to-date: ```shell python dbutils.py --game SDEW upgrade @@ -587,4 +651,135 @@ python dbutils.py --game SDEW upgrade - Midorica - Limited Network Support - Dniel97 - Helping with network base -- tungnotpunk - Source \ No newline at end of file +- tungnotpunk - Source + +## Initial D THE ARCADE + +### SDGT + +| Version ID | Version Name | +| ---------- | ----------------------------- | +| 0 | Initial D THE ARCADE Season 1 | +| 1 | Initial D THE ARCADE Season 2 | + +**Important: Only version 1.50.00 (Season 2) is currently working and actively supported!** + +### Profile Importer + +In order to use the profile importer download the `idac_profile.json` file from the frontend +and either directly use the folder path with `idac_profile.json` in it or specify the complete +path to the `.json` file + +```shell +python read.py --game SDGT --version --optfolder /path/to/game/download/folder +``` + +The importer for SDGT will import the complete profile data with personal high scores as well. + +### Config + +Config file is located in `config/idac.yaml`. + +| Option | Info | +| ----------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `ssl` | Enables/Disables the use of the `ssl_cert` and `ssl_key` (currently unsuported) | +| `matching_host` | IPv4 address of your PC for the Online Battle (currently unsupported) | +| `port_matching` | Port number for the Online Battle Matching | +| `port_echo1/2` | Port numbers for Echos | +| `port_matching_p2p` | Port number for Online Battle (currently unsupported) | +| `stamp.enable` | Enables/Disabled the play stamp events | +| `stamp.enabled_stamps` | Define up to 3 play stamp events (without `.json` extension, which are placed in `titles/idac/data/stamps`) | +| `timetrial.enable` | Enables/Disables the time trial event | +| `timetrial.enabled_timetrial` | Define one! trial event (without `.json` extension, which are placed in `titles/idac/data/timetrial`) | + + +### Database upgrade + +Always make sure your database (tables) are up-to-date: + +```shell +python dbutils.py --game SDGT upgrade +``` + +### Notes +- Online Battle is not supported +- Online Battle Matching is not supported + +### Item categories + +| Category ID | Category Name | +| ----------- | ------------------------ | +| 1 | D Coin | +| 3 | Car Dressup Token | +| 5 | Avatar Dressup Token | +| 6 | Tachometer | +| 7 | Aura | +| 8 | Aura Color | +| 9 | Avatar Face | +| 10 | Avatar Eye | +| 11 | Avatar Mouth | +| 12 | Avatar Hair | +| 13 | Avatar Glasses | +| 14 | Avatar Face accessories | +| 15 | Avatar Body | +| 18 | Avatar Background | +| 21 | Chat Stamp | +| 22 | Keychain | +| 24 | Title | +| 25 | FullTune Ticket | +| 26 | Paper Cup | +| 27 | BGM | +| 28 | Drifting Text | +| 31 | Start Menu BG | +| 32 | Car Color/Paint | +| 33 | Aura Level | +| 34 | FullTune Ticket Fragment | +| 35 | Underneon Lights | + +### TimeRelease Chapter: + +1. Story: 1, 2, 3, 4, 5, 6, 7, 8, 9, 19 (Chapter 10), (29 Chapter 11?) +2. MF Ghost: 10, 11, 12, 13, 14, 15 +3. Bunta: 15, 16, 17, 18, 19, 20, (21, 21, 22?) +4. Special Event: 23, 24, 25, 26, 27, 28 (Touhou Project) + +### TimeRelease Courses: + + +| Course ID | Course Name | Direction | +| --------- | ------------------------- | ------------------------ | +| 0 | Akina Lake(秋名湖) | CounterClockwise(左周り) | +| 2 | Akina Lake(秋名湖) | Clockwise(右周り) | +| 52 | Hakone(箱根) | Downhill(下り) | +| 54 | Hakone(箱根) | Hillclimb(上り) | +| 36 | Usui(碓氷) | CounterClockwise(左周り) | +| 38 | Usui(碓氷) | Clockwise(右周り) | +| 4 | Myogi(妙義) | Downhill(下り) | +| 6 | Myogi(妙義) | Hillclimb(上り) | +| 8 | Akagi(赤城) | Downhill(下り) | +| 10 | Akagi(赤城) | Hillclimb(上り) | +| 12 | Akina(秋名) | Downhill(下り) | +| 14 | Akina(秋名) | Hillclimb(上り) | +| 16 | Irohazaka(いろは坂) | Downhill(下り) | +| 18 | Irohazaka(いろは坂) | Reverse(逆走) | +| 56 | Momiji Line(もみじライン) | Downhill(下り) | +| 58 | Momiji Line(もみじライン) | Hillclimb(上り) | +| 20 | Tsukuba(筑波) | Outbound(往路) | +| 22 | Tsukuba(筑波) | Inbound(復路) | +| 24 | Happogahara(八方ヶ原) | Outbound(往路) | +| 26 | Happogahara(八方ヶ原) | Inbound(復路) | +| 40 | Sadamine(定峰) | Downhill(下り) | +| 42 | Sadamine(定峰) | Hillclimb(上り) | +| 44 | Tsuchisaka(土坂) | Outbound(往路) | +| 46 | Tsuchisaka(土坂) | Inbound(復路) | +| 48 | Akina Snow(秋名雪) | Downhill(下り) | +| 50 | Akina Snow(秋名雪) | Hillclimb(上り) | +| 68 | Odawara(小田原) | Forward(順走) | +| 70 | Odawara(小田原) | Reverse(逆走) | + +### Credits +- Bottersnike: For the HUGE Reverse Engineering help +- Kinako: For helping with the timeRelease unlocking of courses and special mode + +A huge thanks to all people who helped shaping this project to what it is now and don't want to be mentioned here. + diff --git a/example_config/cxb.yaml b/example_config/cxb.yaml index 08948bd..7723ff4 100644 --- a/example_config/cxb.yaml +++ b/example_config/cxb.yaml @@ -1,9 +1,3 @@ server: enable: True - loglevel: "info" - hostname: "localhost" - ssl_enable: False - port: 8082 - port_secure: 443 - ssl_cert: "cert/title.crt" - ssl_key: "cert/title.key" + loglevel: "info" \ No newline at end of file diff --git a/example_config/idac.yaml b/example_config/idac.yaml new file mode 100644 index 0000000..626565e --- /dev/null +++ b/example_config/idac.yaml @@ -0,0 +1,22 @@ +server: + enable: True + loglevel: "info" + ssl: False + ssl_key: "cert/idac.key" + ssl_cert: "cert/idac.crt" + matching_host: "127.0.0.1" + port_matching: 20000 + port_echo1: 20001 + port_echo2: 20002 + port_matching_p2p: 20003 + +stamp: + enable: True + enabled_stamps: # max 3 play stamps + - "touhou_remilia_scarlet" + - "touhou_flandre_scarlet" + - "touhou_sakuya_izayoi" + +timetrial: + enable: True + enabled_timetrial: "touhou_remilia_scarlet" diff --git a/example_config/ongeki.yaml b/example_config/ongeki.yaml index e4088c0..9af5efe 100644 --- a/example_config/ongeki.yaml +++ b/example_config/ongeki.yaml @@ -1,6 +1,7 @@ server: enable: True loglevel: "info" + use_https: False gachas: enabled_gachas: diff --git a/example_config/sao.yaml b/example_config/sao.yaml index 7e3ecf2..0209ffe 100644 --- a/example_config/sao.yaml +++ b/example_config/sao.yaml @@ -1,6 +1,13 @@ server: - hostname: "localhost" enable: True loglevel: "info" - port: 9000 - auto_register: True \ No newline at end of file + auto_register: True + +crypt: + enable: False + key: "" + iv: "" + +hash: + verify_hash: False + hash_base: "" \ No newline at end of file diff --git a/readme.md b/readme.md index 4b94304..c527090 100644 --- a/readme.md +++ b/readme.md @@ -33,6 +33,9 @@ Games listed below have been tested and confirmed working. Only game versions ol + Sword Art Online Arcade (partial support) + Final ++ Initial D THE ARCADE + + Season 2 + ## Requirements - python 3 (tested working with 3.9 and 3.10, other versions YMMV) - pip diff --git a/titles/cxb/base.py b/titles/cxb/base.py index 172628d..fe583e6 100644 --- a/titles/cxb/base.py +++ b/titles/cxb/base.py @@ -198,14 +198,6 @@ class CxbBase: ).decode("utf-8") ) - def task_generateIndexData(self, versionindex: List[str], uid: int): - try: - v_profile = self.data.profile.get_profile_index(0, uid, self.version) - v_profile_data = v_profile["data"] - versionindex.append(int(v_profile_data["appVersion"])) - except Exception: - versionindex.append("10400") - def handle_action_loadrange_request(self, data: Dict) -> Dict: range_start = data["loadrange"]["range"][0] range_end = data["loadrange"]["range"][1] @@ -279,9 +271,14 @@ class CxbBase: thread_ScoreData = Thread(target=CxbBase.task_generateScoreData(song, index, data1)) thread_ScoreData.start() - for v in index: - thread_IndexData = Thread(target=CxbBase.task_generateIndexData(self, versionindex, uid)) - thread_IndexData.start() + v_profile = self.data.profile.get_profile_index(0, uid, self.version) + v_profile_data = v_profile["data"] + + for _, data in enumerate(profile): + if v_profile_data: + versionindex.append(int(v_profile_data["appVersion"])) + else: + versionindex.append("10400") return {"index": index, "data": data1, "version": versionindex} diff --git a/titles/cxb/config.py b/titles/cxb/config.py index 00b7290..fa5a6a3 100644 --- a/titles/cxb/config.py +++ b/titles/cxb/config.py @@ -19,42 +19,6 @@ class CxbServerConfig: ) ) - @property - def hostname(self) -> str: - return CoreConfig.get_config_field( - self.__config, "cxb", "server", "hostname", default="localhost" - ) - - @property - def ssl_enable(self) -> bool: - return CoreConfig.get_config_field( - self.__config, "cxb", "server", "ssl_enable", default=False - ) - - @property - def port(self) -> int: - return CoreConfig.get_config_field( - self.__config, "cxb", "server", "port", default=8082 - ) - - @property - def port_secure(self) -> int: - return CoreConfig.get_config_field( - self.__config, "cxb", "server", "port_secure", default=443 - ) - - @property - def ssl_cert(self) -> str: - return CoreConfig.get_config_field( - self.__config, "cxb", "server", "ssl_cert", default="cert/title.crt" - ) - - @property - def ssl_key(self) -> str: - return CoreConfig.get_config_field( - self.__config, "cxb", "server", "ssl_key", default="cert/title.key" - ) - class CxbConfig(dict): def __init__(self) -> None: diff --git a/titles/cxb/index.py b/titles/cxb/index.py index 7e6cd7c..4a1489c 100644 --- a/titles/cxb/index.py +++ b/titles/cxb/index.py @@ -1,7 +1,6 @@ from twisted.web.http import Request import traceback -from twisted.web import resource, server -from twisted.internet import reactor, endpoints +import sys import yaml import json import re @@ -77,7 +76,7 @@ class CxbServlet(BaseServlet): return True def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: - if not self.core_cfg.server.is_using_proxy and Utils.get_title_port_ssl(self.core_cfg) != 443: + if not self.core_cfg.server.is_using_proxy and Utils.get_title_port_ssl(self.core_cfg): return ( f"https://{self.core_cfg.title.hostname}:{self.core_cfg.title.port_ssl}", "", @@ -124,35 +123,42 @@ class CxbServlet(BaseServlet): func_to_find = "handle_data_" version_string = "Base" internal_ver = 0 + version = 0 if req_json == {}: self.logger.warning(f"Empty json request to /data") return b"" + + subcmd = list(req_json.keys())[0] + if subcmd == "dldate": - if ( - not type(req_json["dldate"]) is dict - or "filetype" not in req_json["dldate"] - ): - self.logger.warning(f"Malformed dldate request: {req_json}") - return b"" + if ( + not type(req_json["dldate"]) is dict + or "filetype" not in req_json["dldate"] + ): + self.logger.warning(f"Malformed dldate request: {req_json}") + return b"" - filetype = req_json["dldate"]["filetype"] - filetype_split = filetype.split("/") + filetype = req_json["dldate"]["filetype"] + filetype_split = filetype.split("/") - if len(filetype_split) < 2 or not filetype_split[0].isnumeric(): - self.logger.warning(f"Malformed dldate request: {req_json}") - return b"" + if len(filetype_split) < 2 or not filetype_split[0].isnumeric(): + self.logger.warning(f"Malformed dldate request: {req_json}") + return b"" - version = int(filetype_split[0]) - filename = filetype_split[len(filetype_split) - 1] + version = int(filetype_split[0]) + filename = filetype_split[len(filetype_split) - 1] - match = re.match( - "^([A-Za-z]*)(\d\d\d\d)$", filetype_split[len(filetype_split) - 1] - ) - if match: - func_to_find += f"{inflection.underscore(match.group(1))}xxxx" + match = re.match( + "^([A-Za-z]*)(\d\d\d\d)$", filetype_split[len(filetype_split) - 1] + ) + if match: + func_to_find += f"{inflection.underscore(match.group(1))}xxxx" + else: + func_to_find += f"{inflection.underscore(filename)}" else: - func_to_find += f"{inflection.underscore(filename)}" + filetype = subcmd + func_to_find += filetype func_to_find += "_request" @@ -183,9 +189,10 @@ class CxbServlet(BaseServlet): except Exception as e: self.logger.error(f"Error handling request for file {filetype} - {e}") if self.logger.level == logging.DEBUG: - traceback.print_exception(e, limit=1) + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb, limit=1) with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a") as f: - traceback.print_exception(e, limit=1, file=f) + traceback.print_exception(tp, val, tb, limit=1, file=f) return "" self.logger.debug(f"{version_string} Response {resp}") @@ -211,9 +218,10 @@ class CxbServlet(BaseServlet): except Exception as e: self.logger.error(f"Error handling action {subcmd} request - {e}") if self.logger.level == logging.DEBUG: - traceback.print_exception(e, limit=1) + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb, limit=1) with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a") as f: - traceback.print_exception(e, limit=1, file=f) + traceback.print_exception(tp, val, tb, limit=1, file=f) return "" self.logger.debug(f"Response {resp}") @@ -239,9 +247,10 @@ class CxbServlet(BaseServlet): except Exception as e: self.logger.error(f"Error handling auth {subcmd} request - {e}") if self.logger.level == logging.DEBUG: - traceback.print_exception(e, limit=1) + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb, limit=1) with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), "a") as f: - traceback.print_exception(e, limit=1, file=f) + traceback.print_exception(tp, val, tb, limit=1, file=f) return "" self.logger.debug(f"Response {resp}") diff --git a/titles/idac/__init__.py b/titles/idac/__init__.py new file mode 100644 index 0000000..0c632bd --- /dev/null +++ b/titles/idac/__init__.py @@ -0,0 +1,12 @@ +from titles.idac.index import IDACServlet +from titles.idac.const import IDACConstants +from titles.idac.database import IDACData +from titles.idac.read import IDACReader +from titles.idac.frontend import IDACFrontend + +index = IDACServlet +database = IDACData +reader = IDACReader +frontend = IDACFrontend +game_codes = [IDACConstants.GAME_CODE] +current_schema_version = 1 diff --git a/titles/idac/base.py b/titles/idac/base.py new file mode 100644 index 0000000..eb75b93 --- /dev/null +++ b/titles/idac/base.py @@ -0,0 +1,16 @@ +import logging + +from core.config import CoreConfig +from titles.idac.config import IDACConfig +from titles.idac.const import IDACConstants +from titles.idac.database import IDACData + + +class IDACBase: + def __init__(self, core_cfg: CoreConfig, game_cfg: IDACConfig) -> None: + self.core_cfg = core_cfg + self.game_config = game_cfg + self.game = IDACConstants.GAME_CODE + self.version = IDACConstants.VER_IDAC_SEASON_1 + self.data = IDACData(core_cfg) + self.logger = logging.getLogger("idac") diff --git a/titles/idac/config.py b/titles/idac/config.py new file mode 100644 index 0000000..2f97219 --- /dev/null +++ b/titles/idac/config.py @@ -0,0 +1,121 @@ +from core.config import CoreConfig + + +class IDACServerConfig: + def __init__(self, parent: "IDACConfig") -> None: + self.__config = parent + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "enable", default=True + ) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "idac", "server", "loglevel", default="info" + ) + ) + + @property + def ssl(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "ssl", default=False + ) + + @property + def ssl_cert(self) -> str: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "ssl_cert", default="cert/title.crt" + ) + + @property + def ssl_key(self) -> str: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "ssl_key", default="cert/title.key" + ) + + @property + def matching_host(self) -> str: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "matching_host", default="127.0.0.1" + ) + + @property + def matching(self) -> int: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "port_matching", default=20000 + ) + + @property + def echo1(self) -> int: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "port_echo1", default=20001 + ) + + @property + def echo2(self) -> int: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "port_echo2", default=20002 + ) + + @property + def matching_p2p(self) -> int: + return CoreConfig.get_config_field( + self.__config, "idac", "server", "port_matching_p2p", default=20003 + ) + + +class IDACStampConfig: + def __init__(self, parent: "IDACConfig") -> None: + self.__config = parent + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "idac", "stamp", "enable", default=True + ) + + @property + def enabled_stamps(self) -> list: + return CoreConfig.get_config_field( + self.__config, + "idac", + "stamp", + "enabled_stamps", + default=[ + "touhou_remilia_scarlet", + "touhou_flandre_scarlet", + "touhou_sakuya_izayoi", + ], + ) + + +class IDACTimetrialConfig: + def __init__(self, parent: "IDACConfig") -> None: + self.__config = parent + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "idac", "timetrial", "enable", default=True + ) + + @property + def enabled_timetrial(self) -> str: + return CoreConfig.get_config_field( + self.__config, + "idac", + "timetrial", + "enabled_timetrial", + default="touhou_remilia_scarlet", + ) + + +class IDACConfig(dict): + def __init__(self) -> None: + self.server = IDACServerConfig(self) + self.stamp = IDACStampConfig(self) + self.timetrial = IDACTimetrialConfig(self) diff --git a/titles/idac/const.py b/titles/idac/const.py new file mode 100644 index 0000000..cfae20e --- /dev/null +++ b/titles/idac/const.py @@ -0,0 +1,16 @@ +class IDACConstants(): + GAME_CODE = "SDGT" + + CONFIG_NAME = "idac.yaml" + + VER_IDAC_SEASON_1 = 0 + VER_IDAC_SEASON_2 = 1 + + VERSION_STRING = ( + "Initial D THE ARCADE Season 1", + "Initial D THE ARCADE Season 2", + ) + + @classmethod + def game_ver_to_string(cls, ver: int): + return cls.VERSION_STRING[ver] diff --git a/titles/idac/data/avatarGacha.json b/titles/idac/data/avatarGacha.json new file mode 100644 index 0000000..bcfba2f Binary files /dev/null and b/titles/idac/data/avatarGacha.json differ diff --git a/titles/idac/data/create_delivery_images.py b/titles/idac/data/create_delivery_images.py new file mode 100644 index 0000000..ac37925 --- /dev/null +++ b/titles/idac/data/create_delivery_images.py @@ -0,0 +1,38 @@ +import os +import hashlib + + +def prepare_images(image_folder="titles/idac/data/images"): + print(f"Preparing image delivery files in {image_folder}...") + + for file in os.listdir(image_folder): + if file.endswith(".png") or file.endswith(".jpg"): + # dpg_name = "adv-" + file[:-4].upper() + dpg_name = file[:-4] + if file.endswith(".png"): + dpg_name += ".dpg" + else: + dpg_name += ".djg" + + if os.path.exists(os.path.join(image_folder, dpg_name)): + continue + else: + with open( + os.path.join(image_folder, file), "rb" + ) as original_image_file: + original_image = original_image_file.read() + image_hash = hashlib.md5(original_image).hexdigest() + print( + f"DPG for {file} not found, creating with hash {image_hash.upper()} ..." + ) + md5_buf = bytes.fromhex(image_hash) + dpg_buf = md5_buf + original_image + dpg_name = "adv-" + image_hash.upper() + dpg_name[:-4] + with open(os.path.join(image_folder, dpg_name), "wb") as dpg_file: + dpg_file.write(dpg_buf) + + print(f"Created {dpg_name}.") + + +# Call the function to execute it +prepare_images() diff --git a/titles/idac/data/stamps/touhou_flandre_scarlet.json b/titles/idac/data/stamps/touhou_flandre_scarlet.json new file mode 100644 index 0000000..a286a87 Binary files /dev/null and b/titles/idac/data/stamps/touhou_flandre_scarlet.json differ diff --git a/titles/idac/data/stamps/touhou_remilia_scarlet.json b/titles/idac/data/stamps/touhou_remilia_scarlet.json new file mode 100644 index 0000000..d3a1b42 Binary files /dev/null and b/titles/idac/data/stamps/touhou_remilia_scarlet.json differ diff --git a/titles/idac/data/stamps/touhou_sakuya_izayoi.json b/titles/idac/data/stamps/touhou_sakuya_izayoi.json new file mode 100644 index 0000000..3749eee Binary files /dev/null and b/titles/idac/data/stamps/touhou_sakuya_izayoi.json differ diff --git a/titles/idac/data/timeRelease_v0100.json b/titles/idac/data/timeRelease_v0100.json new file mode 100644 index 0000000..acb5209 Binary files /dev/null and b/titles/idac/data/timeRelease_v0100.json differ diff --git a/titles/idac/data/timeRelease_v0131.json b/titles/idac/data/timeRelease_v0131.json new file mode 100644 index 0000000..4798c30 Binary files /dev/null and b/titles/idac/data/timeRelease_v0131.json differ diff --git a/titles/idac/data/timeRelease_v0141.json b/titles/idac/data/timeRelease_v0141.json new file mode 100644 index 0000000..c7805db Binary files /dev/null and b/titles/idac/data/timeRelease_v0141.json differ diff --git a/titles/idac/data/timeRelease_v0150.json b/titles/idac/data/timeRelease_v0150.json new file mode 100644 index 0000000..539c138 Binary files /dev/null and b/titles/idac/data/timeRelease_v0150.json differ diff --git a/titles/idac/data/timetrial/touhou_flandre_scarlet.json b/titles/idac/data/timetrial/touhou_flandre_scarlet.json new file mode 100644 index 0000000..0db5ed7 Binary files /dev/null and b/titles/idac/data/timetrial/touhou_flandre_scarlet.json differ diff --git a/titles/idac/data/timetrial/touhou_remilia_scarlet.json b/titles/idac/data/timetrial/touhou_remilia_scarlet.json new file mode 100644 index 0000000..6b50a2b Binary files /dev/null and b/titles/idac/data/timetrial/touhou_remilia_scarlet.json differ diff --git a/titles/idac/data/timetrial/touhou_sakuya_izayoi.json b/titles/idac/data/timetrial/touhou_sakuya_izayoi.json new file mode 100644 index 0000000..c31d391 Binary files /dev/null and b/titles/idac/data/timetrial/touhou_sakuya_izayoi.json differ diff --git a/titles/idac/database.py b/titles/idac/database.py new file mode 100644 index 0000000..dac4556 --- /dev/null +++ b/titles/idac/database.py @@ -0,0 +1,12 @@ +from core.data import Data +from core.config import CoreConfig +from titles.idac.schema.profile import IDACProfileData +from titles.idac.schema.item import IDACItemData + + +class IDACData(Data): + def __init__(self, cfg: CoreConfig) -> None: + super().__init__(cfg) + + self.profile = IDACProfileData(cfg, self.session) + self.item = IDACItemData(cfg, self.session) diff --git a/titles/idac/echo.py b/titles/idac/echo.py new file mode 100644 index 0000000..08e5526 --- /dev/null +++ b/titles/idac/echo.py @@ -0,0 +1,63 @@ +import logging +import socket + +from twisted.internet.protocol import DatagramProtocol +from socketserver import BaseRequestHandler, TCPServer +from typing import Tuple + +from core.config import CoreConfig +from titles.idac.config import IDACConfig +from titles.idac.database import IDACData + + +class IDACEchoUDP(DatagramProtocol): + def __init__(self, cfg: CoreConfig, game_cfg: IDACConfig, port: int) -> None: + super().__init__() + self.port = port + self.core_config = cfg + self.game_config = game_cfg + self.logger = logging.getLogger("idac") + + def datagramReceived(self, data, addr): + self.logger.info( + f"UDP Ping from from {addr[0]}:{addr[1]} -> {self.port} - {data.hex()}" + ) + self.transport.write(data, addr) + + +class IDACEchoTCP(BaseRequestHandler): + def __init__( + self, request, client_address, server, cfg: CoreConfig, game_cfg: IDACConfig + ) -> None: + self.core_config = cfg + self.game_config = game_cfg + self.logger = logging.getLogger("idac") + self.data = IDACData(cfg) + super().__init__(request, client_address, server) + + def handle(self): + data = self.request.recv(1024).strip() + self.logger.debug( + f"TCP Ping from {self.client_address[0]}:{self.client_address[1]} -> {self.server.server_address[1]}: {data.hex()}" + ) + self.request.sendall(data) + self.request.shutdown(socket.SHUT_WR) + + +class IDACEchoTCPFactory(TCPServer): + def __init__( + self, + server_address: Tuple[str, int], + RequestHandlerClass, + cfg: CoreConfig, + game_cfg: IDACConfig, + bind_and_activate: bool = ..., + ) -> None: + super().__init__(server_address, RequestHandlerClass, bind_and_activate) + self.core_config = cfg + self.game_config = game_cfg + + def finish_request(self, request, client_address): + self.RequestHandlerClass( + request, client_address, self, self.core_config, self.game_config + ) diff --git a/titles/idac/frontend.py b/titles/idac/frontend.py new file mode 100644 index 0000000..78abae8 --- /dev/null +++ b/titles/idac/frontend.py @@ -0,0 +1,142 @@ +import json +import yaml +import jinja2 +from os import path +from twisted.web.util import redirectTo +from twisted.web.http import Request +from twisted.web.server import Session + +from core.frontend import FE_Base, IUserSession +from core.config import CoreConfig +from titles.idac.database import IDACData +from titles.idac.schema.profile import * +from titles.idac.schema.item import * +from titles.idac.config import IDACConfig +from titles.idac.const import IDACConstants + + +class IDACFrontend(FE_Base): + def __init__( + self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str + ) -> None: + super().__init__(cfg, environment) + self.data = IDACData(cfg) + self.game_cfg = IDACConfig() + if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}")) + ) + self.nav_name = "頭文字D THE ARCADE" + # TODO: Add version list + self.version = IDACConstants.VER_IDAC_SEASON_2 + + self.ticket_names = { + 3: "car_dressup_points", + 5: "avatar_points", + 25: "full_tune_tickets", + 34: "full_tune_fragments", + } + + def generate_all_tables_json(self, user_id: int): + json_export = {} + + idac_tables = { + profile, + config, + avatar, + rank, + stock, + theory, + car, + ticket, + story, + episode, + difficulty, + course, + trial, + challenge, + theory_course, + theory_partner, + theory_running, + vs_info, + stamp, + timetrial_event + } + + for table in idac_tables: + sql = select(table).where( + table.c.user == user_id, + ) + + # check if the table has a version column + if "version" in table.c: + sql = sql.where(table.c.version == self.version) + + # lol use the profile connection for items, dirty hack + result = self.data.profile.execute(sql) + data_list = result.fetchall() + + # add the list to the json export with the correct table name + json_export[table.name] = [] + for data in data_list: + tmp = data._asdict() + tmp.pop("id") + tmp.pop("user") + json_export[table.name].append(tmp) + + return json.dumps(json_export, indent=4, default=str, ensure_ascii=False) + + def render_GET(self, request: Request) -> bytes: + uri: str = request.uri.decode() + + template = self.environment.get_template( + "titles/idac/frontend/idac_index.jinja" + ) + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + user_id = usr_sesh.userId + # user_id = usr_sesh.user_id + + # profile export + if uri.startswith("/game/idac/export"): + if user_id == 0: + return redirectTo(b"/game/idac", request) + + # set the file name, content type and size to download the json + content = self.generate_all_tables_json(user_id).encode("utf-8") + request.responseHeaders.addRawHeader( + b"content-type", b"application/octet-stream" + ) + request.responseHeaders.addRawHeader( + b"content-disposition", b"attachment; filename=idac_profile.json" + ) + request.responseHeaders.addRawHeader( + b"content-length", str(len(content)).encode("utf-8") + ) + + self.logger.info(f"User {user_id} exported their IDAC data") + return content + + profile_data, tickets, rank = None, None, None + if user_id > 0: + profile_data = self.data.profile.get_profile(user_id, self.version) + ticket_data = self.data.item.get_tickets(user_id) + rank = self.data.profile.get_profile_rank(user_id, self.version) + + tickets = { + self.ticket_names[ticket["ticket_id"]]: ticket["ticket_cnt"] + for ticket in ticket_data + } + + return template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + profile=profile_data, + tickets=tickets, + rank=rank, + sesh=vars(usr_sesh), + active_page="idac", + ).encode("utf-16") + + def render_POST(self, request: Request) -> bytes: + pass diff --git a/titles/idac/frontend/idac_index.jinja b/titles/idac/frontend/idac_index.jinja new file mode 100644 index 0000000..eeecc65 --- /dev/null +++ b/titles/idac/frontend/idac_index.jinja @@ -0,0 +1,134 @@ +{% extends "core/frontend/index.jinja" %} +{% block content %} +

頭文字D THE ARCADE

+ +{% if sesh is defined and sesh["userId"] > 0 %} +
+
+
+
+

{{ sesh["username"] }}'s Profile

+
+
+ + +
+
+
+
+ + {% if profile is defined and profile is not none %} +
+
+
+
+
Information
+
+
Username
+

{{ profile.username }}

+
Cash
+

{{ profile.cash }} D

+
Grade
+

+ {% set grade = rank.grade %} + {% if grade >= 1 and grade <= 72 %} + {% set grade_number = (grade - 1) // 9 %} + {% set grade_letters = ['E', 'D', 'C', 'B', 'A', 'S', 'SS', 'X'] %} + {{ grade_letters[grade_number] }}{{ 9 - ((grade-1) % 9) }} + {% else %} + Unknown + {% endif %} +

+
+
+
+
+
+ +
+
Statistics
+
+
+
+
Total Plays
+

{{ profile.total_play }}

+
+
+
Last Played
+

{{ profile.last_play_date }}

+
+
+
Mileage
+

{{ profile.mileage / 1000}} km

+
+
+ {% if tickets is defined and tickets|length > 0 %} +
Tokens/Tickets
+
+
+
+
Avatar Tokens
+

{{ tickets.avatar_points }}/30

+
+
+
Car Dressup Tokens
+

{{ tickets.car_dressup_points }}/30

+
+
+
FullTune Tickets
+

{{ tickets.full_tune_tickets }}/99

+
+
+
FullTune Fragments
+

{{ tickets.full_tune_fragments }}/10

+
+
+ {% endif %} +
+
+
+
+ {% else %} + + {% endif %} + +
+
+{% else %} + +{% endif %} + + + + + +{% endblock content %} \ No newline at end of file diff --git a/titles/idac/frontend/js/idac_scripts.js b/titles/idac/frontend/js/idac_scripts.js new file mode 100644 index 0000000..111fea6 --- /dev/null +++ b/titles/idac/frontend/js/idac_scripts.js @@ -0,0 +1,10 @@ +$(document).ready(function () { + $('#exportBtn').click(function () { + window.location = "/game/idac/export"; + + // appendAlert('Successfully exported the profile', 'success'); + + // Close the modal on success + $('#export').modal('hide'); + }); +}); \ No newline at end of file diff --git a/titles/idac/index.py b/titles/idac/index.py new file mode 100644 index 0000000..7daedae --- /dev/null +++ b/titles/idac/index.py @@ -0,0 +1,167 @@ +import json +import traceback +import inflection +import yaml +import logging +import coloredlogs + +from os import path +from typing import Dict, List, Tuple +from logging.handlers import TimedRotatingFileHandler +from twisted.web import server +from twisted.web.http import Request +from twisted.internet import reactor, endpoints + +from core.config import CoreConfig +from core.utils import Utils +from titles.idac.base import IDACBase +from titles.idac.season2 import IDACSeason2 +from titles.idac.config import IDACConfig +from titles.idac.const import IDACConstants +from titles.idac.echo import IDACEchoUDP +from titles.idac.matching import IDACMatching + + +class IDACServlet: + def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: + self.core_cfg = core_cfg + self.game_cfg = IDACConfig() + if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}")) + ) + + self.versions = [ + IDACBase(core_cfg, self.game_cfg), + IDACSeason2(core_cfg, self.game_cfg) + ] + + self.logger = logging.getLogger("idac") + log_fmt_str = "[%(asctime)s] IDAC | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "idac"), + encoding="utf8", + when="d", + backupCount=10, + ) + + fileHandler.setFormatter(log_fmt) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(log_fmt) + + self.logger.addHandler(fileHandler) + self.logger.addHandler(consoleHandler) + + self.logger.setLevel(self.game_cfg.server.loglevel) + coloredlogs.install( + level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str + ) + + @classmethod + def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool: + game_cfg = IDACConfig() + + if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return False + + return True + + def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: + return ( + [], + [("render_POST", "/SDGT/{version}/initiald/{category}/{endpoint}", {})] + ) + + def get_allnet_info( + self, game_code: str, game_ver: int, keychip: str + ) -> Tuple[bool, str, str]: + title_port_int = Utils.get_title_port(self.core_cfg) + t_port = f":{title_port_int}" if title_port_int and not self.core_cfg.server.is_using_proxy else "" + + return ( + f"", + # requires http or else it defaults to https + f"http://{self.core_cfg.title.hostname}{t_port}/{game_code}/{game_ver}/", + ) + + def render_POST(self, request: Request, game_code: int, matchers: Dict) -> bytes: + req_raw = request.content.getvalue() + internal_ver = 0 + version = int(matchers['version']) + category = matchers['category'] + endpoint = matchers['endpoint'] + client_ip = Utils.get_ip_addr(request) + + if version >= 100 and version < 140: # IDAC Season 1 + internal_ver = IDACConstants.VER_IDAC_SEASON_1 + elif version >= 140 and version < 171: # IDAC Season 2 + internal_ver = IDACConstants.VER_IDAC_SEASON_2 + + header_application = self.decode_header(request.getAllHeaders()) + + req_data = json.loads(req_raw) + + self.logger.info(f"v{version} {endpoint} request from {client_ip}") + self.logger.debug(f"Headers: {header_application}") + self.logger.debug(req_data) + + # func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + func_to_find = "handle_" + func_to_find += f"{category.lower()}_" if not category == "" else "" + func_to_find += f"{endpoint.lower()}_request" + + if not hasattr(self.versions[internal_ver], func_to_find): + self.logger.warning(f"Unhandled v{version} request {endpoint}") + return '{"status_code": "0"}'.encode("utf-8") + + resp = None + try: + handler = getattr(self.versions[internal_ver], func_to_find) + resp = handler(req_data, header_application) + + except Exception as e: + traceback.print_exc() + self.logger.error(f"Error handling v{version} method {endpoint} - {e}") + return '{"status_code": "0"}'.encode("utf-8") + + if resp is None: + resp = {"status_code": "0"} + + self.logger.debug(f"Response {resp}") + return json.dumps(resp, ensure_ascii=False).encode("utf-8") + + + def decode_header(self, data: Dict) -> Dict: + app: str = data[b"application"].decode() + ret = {} + + for x in app.split(", "): + y = x.split("=") + ret[y[0]] = y[1].replace('"', "") + + return ret + + def setup(self): + if self.game_cfg.server.enable: + endpoints.serverFromString( + reactor, + f"tcp:{self.game_cfg.server.matching}:interface={self.core_cfg.server.listen_address}", + ).listen(server.Site(IDACMatching(self.core_cfg, self.game_cfg))) + + reactor.listenUDP( + self.game_cfg.server.echo1, + IDACEchoUDP(self.core_cfg, self.game_cfg, self.game_cfg.server.echo1), + ) + reactor.listenUDP( + self.game_cfg.server.echo2, + IDACEchoUDP(self.core_cfg, self.game_cfg, self.game_cfg.server.echo2), + ) + + self.logger.info(f"Matching listening on {self.game_cfg.server.matching} with echos on {self.game_cfg.server.echo1} and {self.game_cfg.server.echo2}") diff --git a/titles/idac/matching.py b/titles/idac/matching.py new file mode 100644 index 0000000..396eec8 --- /dev/null +++ b/titles/idac/matching.py @@ -0,0 +1,72 @@ +import json +import logging + +from typing import Dict +from twisted.web import resource + +from core import CoreConfig +from titles.idac.season2 import IDACBase +from titles.idac.config import IDACConfig + + +class IDACMatching(resource.Resource): + isLeaf = True + + def __init__(self, cfg: CoreConfig, game_cfg: IDACConfig) -> None: + self.core_config = cfg + self.game_config = game_cfg + self.base = IDACBase(cfg, game_cfg) + self.logger = logging.getLogger("idac") + + self.queue = 0 + + def get_matching_state(self): + if self.queue >= 1: + self.queue -= 1 + return 0 + else: + return 1 + + def render_POST(self, req) -> bytes: + url = req.uri.decode() + req_data = json.loads(req.content.getvalue().decode()) + header_application = self.decode_header(req.getAllHeaders()) + user_id = int(header_application["session"]) + + # self.getMatchingStatus(user_id) + + self.logger.info( + f"IDAC Matching request from {req.getClientIP()}: {url} - {req_data}" + ) + + resp = {"status_code": "0"} + if url == "/regist": + self.queue = self.queue + 1 + elif url == "/status": + if req_data.get("cancel_flag"): + self.queue = self.queue - 1 + self.logger.info( + f"IDAC Matching endpoint {req.getClientIP()} had quited" + ) + + resp = { + "status_code": "0", + # Only IPv4 is supported + "host": self.game_config.server.matching_host, + "port": self.game_config.server.matching_p2p, + "room_name": "INDTA", + "state": 1, + } + + self.logger.debug(f"Response {resp}") + return json.dumps(resp, ensure_ascii=False).encode("utf-8") + + def decode_header(self, data: Dict) -> Dict: + app: str = data[b"application"].decode() + ret = {} + + for x in app.split(", "): + y = x.split("=") + ret[y[0]] = y[1].replace('"', "") + + return ret diff --git a/titles/idac/read.py b/titles/idac/read.py new file mode 100644 index 0000000..8798e9b --- /dev/null +++ b/titles/idac/read.py @@ -0,0 +1,161 @@ +import json +import logging +import os +from typing import Any, Dict, List, Optional + +from read import BaseReader +from core.data import Data +from core.config import CoreConfig +from titles.idac.const import IDACConstants +from titles.idac.database import IDACData +from titles.idac.schema.profile import * +from titles.idac.schema.item import * + + +class IDACReader(BaseReader): + def __init__( + self, + config: CoreConfig, + version: int, + bin_dir: Optional[str], + opt_dir: Optional[str], + extra: Optional[str], + ) -> None: + super().__init__(config, version, bin_dir, opt_dir, extra) + self.card_data = Data(config).card + self.data = IDACData(config) + + try: + self.logger.info( + f"Start importer for {IDACConstants.game_ver_to_string(version)}" + ) + except IndexError: + self.logger.error(f"Invalid Initial D THE ARCADE version {version}") + exit(1) + + def read(self) -> None: + if self.bin_dir is None and self.opt_dir is None: + self.logger.error( + ( + "To import your profile specify the '--optfolder'", + " path to your idac_profile.json file, exiting", + ) + ) + exit(1) + + if self.opt_dir is not None: + if not os.path.exists(self.opt_dir): + self.logger.error( + f"Path to idac_profile.json does not exist: {self.opt_dir}" + ) + exit(1) + + if os.path.isdir(self.opt_dir): + self.opt_dir = os.path.join(self.opt_dir, "idac_profile.json") + + if not os.path.isfile(self.opt_dir) or self.opt_dir[-5:] != ".json": + self.logger.error( + f"Path to idac_profile.json does not exist: {self.opt_dir}" + ) + exit(1) + + self.read_idac_profile(self.opt_dir) + + def read_idac_profile(self, file_path: str) -> None: + self.logger.info(f"Reading profile from {file_path}...") + + # read it as binary to avoid encoding issues + profile_data: Dict[str, Any] = {} + with open(file_path, "rb") as f: + profile_data = json.loads(f.read().decode("utf-8")) + + if not profile_data: + self.logger.error("Profile could not be parsed, exiting") + exit(1) + + access_code = None + while access_code is None: + access_code = input("Enter your 20 digits access code: ") + if len(access_code) != 20 or not access_code.isdigit(): + access_code = None + self.logger.warning("Invalid access code, please try again.") + + # check if access code already exists, if not create a new profile + user_id = self.card_data.get_user_id_from_card(access_code) + if user_id is None: + choice = input("Access code does not exist, do you want to create a new profile? (Y/n): ") + if choice.lower() == "n": + self.logger.info("Exiting...") + exit(0) + + user_id = self.data.user.create_user() + + if user_id is None: + self.logger.error("Failed to register user!") + user_id = -1 + + else: + card_id = self.data.card.create_card(user_id, access_code) + + if card_id is None: + self.logger.error("Failed to register card!") + user_id = -1 + + if user_id == -1: + self.logger.error("Failed to create profile, exiting") + exit(1) + + # table mapping to insert the data properly + tables = { + "idac_profile": profile, + "idac_profile_config": config, + "idac_profile_avatar": avatar, + "idac_profile_rank": rank, + "idac_profile_stock": stock, + "idac_profile_theory": theory, + "idac_user_car": car, + "idac_user_ticket": ticket, + "idac_user_story": story, + "idac_user_story_episode": episode, + "idac_user_story_episode_difficulty": difficulty, + "idac_user_course": course, + "idac_user_time_trial": trial, + "idac_user_challenge": challenge, + "idac_user_theory_course": theory_course, + "idac_user_theory_partner": theory_partner, + "idac_user_theory_running": theory_running, + "idac_user_vs_info": vs_info, + "idac_user_stamp": stamp, + "idac_user_timetrial_event": timetrial_event, + } + + for name, data_list in profile_data.items(): + # get the SQLAlchemy table object from the name + table = tables.get(name) + if table is None: + self.logger.warning(f"Unknown table {name}, skipping") + continue + + for data in data_list: + # add user to the data + data["user"] = user_id + + # check if the table has a version column + if "version" in table.c: + data["version"] = self.version + + sql = insert(table).values( + **data + ) + + # lol use the profile connection for items, dirty hack + conflict = sql.on_duplicate_key_update(**data) + result = self.data.profile.execute(conflict) + + if result is None: + self.logger.error(f"Failed to insert data into table {name}") + exit(1) + + self.logger.info(f"Inserted data into table {name}") + + self.logger.info("Profile import complete!") diff --git a/titles/idac/schema/item.py b/titles/idac/schema/item.py new file mode 100644 index 0000000..80ee7ba --- /dev/null +++ b/titles/idac/schema/item.py @@ -0,0 +1,983 @@ +from typing import Dict, Optional, List +from sqlalchemy import ( + Table, + Column, + UniqueConstraint, + PrimaryKeyConstraint, + and_, + update, +) +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.schema import ForeignKey +from sqlalchemy.engine import Row +from sqlalchemy.sql import func, select +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +car = Table( + "idac_user_car", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("version", Integer, nullable=False), + Column("car_id", Integer), + Column("style_car_id", Integer), + Column("color", Integer), + Column("bureau", Integer), + Column("kana", Integer), + Column("s_no", Integer), + Column("l_no", Integer), + Column("car_flag", Integer), + Column("tune_point", Integer), + Column("tune_level", Integer, server_default="1"), + Column("tune_parts", Integer), + Column("infinity_tune", Integer, server_default="0"), + Column("online_vs_win", Integer, server_default="0"), + Column( + "pickup_seq", Integer, server_default="1" + ), # the order in which the car was picked up + Column( + "purchase_seq", Integer, server_default="1" + ), # the order in which the car was purchased + Column("color_stock_list", String(32)), + Column("color_stock_new_list", String(32)), + Column("parts_stock_list", String(48)), + Column("parts_stock_new_list", String(48)), + Column("parts_set_equip_list", String(48)), + Column("parts_list", JSON), + Column("equip_parts_count", Integer, server_default="0"), + Column("total_car_parts_count", Integer, server_default="0"), + Column("use_count", Integer, server_default="0"), + Column("story_use_count", Integer, server_default="0"), + Column("timetrial_use_count", Integer, server_default="0"), + Column("vs_use_count", Integer, server_default="0"), + Column("net_vs_use_count", Integer, server_default="0"), + Column("theory_use_count", Integer, server_default="0"), + Column("car_mileage", Integer, server_default="0"), + UniqueConstraint("user", "version", "style_car_id", name="idac_user_car_uk"), + mysql_charset="utf8mb4", +) + +ticket = Table( + "idac_user_ticket", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("ticket_id", Integer), + Column("ticket_cnt", Integer), + UniqueConstraint("user", "ticket_id", name="idac_user_ticket_uk"), + mysql_charset="utf8mb4", +) + +story = Table( + "idac_user_story", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("story_type", Integer), + Column("chapter", Integer), + Column("loop_count", Integer, server_default="1"), + UniqueConstraint("user", "chapter", name="idac_user_story_uk"), + mysql_charset="utf8mb4", +) + +episode = Table( + "idac_user_story_episode", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("chapter", Integer), + Column("episode", Integer), + Column("play_status", Integer), + UniqueConstraint("user", "chapter", "episode", name="idac_user_story_episode_uk"), + mysql_charset="utf8mb4", +) + +difficulty = Table( + "idac_user_story_episode_difficulty", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("episode", Integer), + Column("difficulty", Integer), + Column("play_count", Integer), + Column("clear_count", Integer), + Column("play_status", Integer), + Column("play_score", Integer), + UniqueConstraint( + "user", "episode", "difficulty", name="idac_user_story_episode_difficulty_uk" + ), + mysql_charset="utf8mb4", +) + +course = Table( + "idac_user_course", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("course_id", Integer), + Column("run_counts", Integer, server_default="1"), + Column("skill_level_exp", Integer, server_default="0"), + UniqueConstraint("user", "course_id", name="idac_user_course_uk"), + mysql_charset="utf8mb4", +) + +trial = Table( + "idac_user_time_trial", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("version", Integer, nullable=False), + Column("style_car_id", Integer), + Column("course_id", Integer), + Column("eval_id", Integer, server_default="0"), + Column("goal_time", Integer), + Column("section_time_1", Integer), + Column("section_time_2", Integer), + Column("section_time_3", Integer), + Column("section_time_4", Integer), + Column("mission", Integer), + Column("play_dt", TIMESTAMP, server_default=func.now()), + UniqueConstraint( + "user", "version", "course_id", "style_car_id", name="idac_user_time_trial_uk" + ), + mysql_charset="utf8mb4", +) + +challenge = Table( + "idac_user_challenge", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("vs_type", Integer), + Column("play_difficulty", Integer), + Column("cleared_difficulty", Integer), + Column("story_type", Integer), + Column("play_count", Integer, server_default="1"), + Column("weak_difficulty", Integer, server_default="0"), + Column("eval_id", Integer), + Column("advantage", Integer), + Column("sec1_advantage_avg", Integer), + Column("sec2_advantage_avg", Integer), + Column("sec3_advantage_avg", Integer), + Column("sec4_advantage_avg", Integer), + Column("nearby_advantage_rate", Integer), + Column("win_flag", Integer), + Column("result", Integer), + Column("record", Integer), + Column("course_id", Integer), + Column("last_play_course_id", Integer), + Column("style_car_id", Integer), + Column("course_day", Integer), + UniqueConstraint( + "user", "vs_type", "play_difficulty", name="idac_user_challenge_uk" + ), + mysql_charset="utf8mb4", +) + +theory_course = Table( + "idac_user_theory_course", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("course_id", Integer), + Column("max_victory_grade", Integer, server_default="0"), + Column("run_count", Integer, server_default="1"), + Column("powerhouse_lv", Integer), + Column("powerhouse_exp", Integer), + Column("played_powerhouse_lv", Integer), + Column("update_dt", TIMESTAMP, server_default=func.now()), + UniqueConstraint("user", "course_id", name="idac_user_theory_course_uk"), + mysql_charset="utf8mb4", +) + +theory_partner = Table( + "idac_user_theory_partner", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("partner_id", Integer), + Column("fellowship_lv", Integer), + Column("fellowship_exp", Integer), + UniqueConstraint("user", "partner_id", name="idac_user_theory_partner_uk"), + mysql_charset="utf8mb4", +) + +theory_running = Table( + "idac_user_theory_running", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("course_id", Integer), + Column("attack", Integer), + Column("defense", Integer), + Column("safety", Integer), + Column("runaway", Integer), + Column("trick_flag", Integer), + UniqueConstraint("user", "course_id", name="idac_user_theory_running_uk"), + mysql_charset="utf8mb4", +) + +vs_info = Table( + "idac_user_vs_info", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("group_key", String(25)), + Column("win_flg", Integer), + Column("style_car_id", Integer), + Column("course_id", Integer), + Column("course_day", Integer), + Column("players_num", Integer), + Column("winning", Integer), + Column("advantage_1", Integer), + Column("advantage_2", Integer), + Column("advantage_3", Integer), + Column("advantage_4", Integer), + Column("select_course_id", Integer), + Column("select_course_day", Integer), + Column("select_course_random", Integer), + Column("matching_success_sec", Integer), + Column("boost_flag", Integer), + Column("vs_history", Integer), + Column("break_count", Integer), + Column("break_penalty_flag", Integer), + UniqueConstraint("user", "group_key", name="idac_user_vs_info_uk"), + mysql_charset="utf8mb4", +) + +stamp = Table( + "idac_user_stamp", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("m_stamp_event_id", Integer), + Column("select_flag", Integer), + Column("stamp_masu", Integer), + Column("daily_bonus", Integer), + Column("weekly_bonus", Integer), + Column("weekday_bonus", Integer), + Column("weekend_bonus", Integer), + Column("total_bonus", Integer), + Column("day_total_bonus", Integer), + Column("store_battle_bonus", Integer), + Column("story_bonus", Integer), + Column("online_battle_bonus", Integer), + Column("timetrial_bonus", Integer), + Column("fasteststreetlegaltheory_bonus", Integer), + Column("collaboration_bonus", Integer), + Column("add_bonus_daily_flag_1", Integer), + Column("add_bonus_daily_flag_2", Integer), + Column("add_bonus_daily_flag_3", Integer), + Column("create_date_daily", TIMESTAMP, server_default=func.now()), + Column("create_date_weekly", TIMESTAMP, server_default=func.now()), + UniqueConstraint("user", "m_stamp_event_id", name="idac_user_stamp_uk"), + mysql_charset="utf8mb4", +) + +timetrial_event = Table( + "idac_user_timetrial_event", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("timetrial_event_id", Integer), + Column("point", Integer), + UniqueConstraint("user", "timetrial_event_id", name="idac_user_timetrial_event_uk"), + mysql_charset="utf8mb4", +) + + +class IDACItemData(BaseData): + def get_random_user_car(self, aime_id: int, version: int) -> Optional[List[Row]]: + sql = ( + select(car) + .where(and_(car.c.user == aime_id, car.c.version == version)) + .order_by(func.rand()) + .limit(1) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_random_car(self, version: int) -> Optional[List[Row]]: + sql = select(car).where(car.c.version == version).order_by(func.rand()).limit(1) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_car( + self, aime_id: int, version: int, style_car_id: int + ) -> Optional[List[Row]]: + sql = select(car).where( + and_( + car.c.user == aime_id, + car.c.version == version, + car.c.style_car_id == style_car_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_cars( + self, version: int, aime_id: int, only_pickup: bool = False + ) -> Optional[List[Row]]: + if only_pickup: + sql = select(car).where( + and_( + car.c.user == aime_id, + car.c.version == version, + car.c.pickup_seq != 0, + ) + ) + else: + sql = select(car).where( + and_(car.c.user == aime_id, car.c.version == version) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_ticket(self, aime_id: int, ticket_id: int) -> Optional[Row]: + sql = select(ticket).where( + ticket.c.user == aime_id, ticket.c.ticket_id == ticket_id + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_tickets(self, aime_id: int) -> Optional[List[Row]]: + sql = select(ticket).where(ticket.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_story(self, aime_id: int, chapter_id: int) -> Optional[Row]: + sql = select(story).where( + and_(story.c.user == aime_id, story.c.chapter == chapter_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_stories(self, aime_id: int) -> Optional[List[Row]]: + sql = select(story).where(story.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_story_episodes(self, aime_id: int, chapter_id: int) -> Optional[List[Row]]: + sql = select(episode).where( + and_(episode.c.user == aime_id, episode.c.chapter == chapter_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_story_episode(self, aime_id: int, episode_id: int) -> Optional[Row]: + sql = select(episode).where( + and_(episode.c.user == aime_id, episode.c.episode == episode_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_story_episode_difficulties( + self, aime_id: int, episode_id: int + ) -> Optional[List[Row]]: + sql = select(difficulty).where( + and_(difficulty.c.user == aime_id, difficulty.c.episode == episode_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_courses(self, aime_id: int) -> Optional[List[Row]]: + sql = select(course).where(course.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_course(self, aime_id: int, course_id: int) -> Optional[Row]: + sql = select(course).where( + and_(course.c.user == aime_id, course.c.course_id == course_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_time_trial_courses(self, version: int) -> Optional[List[Row]]: + sql = select(trial.c.course_id).where(trial.c.version == version).distinct() + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_time_trial_user_best_time_by_course_car( + self, version: int, aime_id: int, course_id: int, style_car_id: int + ) -> Optional[Row]: + sql = select(trial).where( + and_( + trial.c.user == aime_id, + trial.c.version == version, + trial.c.course_id == course_id, + trial.c.style_car_id == style_car_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_time_trial_user_best_courses( + self, version: int, aime_id: int + ) -> Optional[List[Row]]: + # get for a given aime_id the best time for each course + subquery = ( + select( + trial.c.version, + func.min(trial.c.goal_time).label("min_goal_time"), + trial.c.course_id, + ) + .where(and_(trial.c.version == version, trial.c.user == aime_id)) + .group_by(trial.c.course_id) + .subquery() + ) + + # now get the full row for each best time + sql = select(trial).where( + and_( + trial.c.version == subquery.c.version, + trial.c.goal_time == subquery.c.min_goal_time, + trial.c.course_id == subquery.c.course_id, + trial.c.user == aime_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_time_trial_best_cars_by_course( + self, version: int, course_id: int, aime_id: Optional[int] = None + ) -> Optional[List[Row]]: + subquery = ( + select( + trial.c.version, + func.min(trial.c.goal_time).label("min_goal_time"), + trial.c.style_car_id, + ) + .where( + and_( + trial.c.version == version, + trial.c.course_id == course_id, + ) + ) + ) + + if aime_id is not None: + subquery = subquery.where(trial.c.user == aime_id) + + subquery = subquery.group_by(trial.c.style_car_id).subquery() + + sql = select(trial).where( + and_( + trial.c.version == subquery.c.version, + trial.c.goal_time == subquery.c.min_goal_time, + trial.c.style_car_id == subquery.c.style_car_id, + trial.c.course_id == course_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_time_trial_ranking_by_course( + self, + version: int, + course_id: int, + style_car_id: Optional[int] = None, + limit: Optional[int] = 10, + ) -> Optional[List[Row]]: + # get the top 10 ranking by goal_time for a given course which is grouped by user + subquery = select( + trial.c.version, + trial.c.user, + func.min(trial.c.goal_time).label("min_goal_time"), + ).where(and_(trial.c.version == version, trial.c.course_id == course_id)) + + # if wantd filter only by style_car_id + if style_car_id is not None: + subquery = subquery.where(trial.c.style_car_id == style_car_id) + + subquery = subquery.group_by(trial.c.user).subquery() + + sql = ( + select(trial) + .where( + and_( + trial.c.version == subquery.c.version, + trial.c.user == subquery.c.user, + trial.c.goal_time == subquery.c.min_goal_time, + ), + ) + .order_by(trial.c.goal_time) + ) + + # limit the result if needed + if limit is not None: + sql = sql.limit(limit) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_time_trial_best_ranking_by_course( + self, version: int, aime_id: int, course_id: int + ) -> Optional[Row]: + sql = ( + select(trial) + .where( + and_( + trial.c.version == version, + trial.c.user == aime_id, + trial.c.course_id == course_id, + ), + ) + .order_by(trial.c.goal_time) + .limit(1) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_challenge( + self, aime_id: int, vs_type: int, play_difficulty: int + ) -> Optional[Row]: + sql = select(challenge).where( + and_( + challenge.c.user == aime_id, + challenge.c.vs_type == vs_type, + challenge.c.play_difficulty == play_difficulty, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_challenges(self, aime_id: int) -> Optional[List[Row]]: + sql = select(challenge).where(challenge.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_best_challenges_by_vs_type( + self, aime_id: int, story_type: int = 4 + ) -> Optional[List[Row]]: + subquery = ( + select( + challenge.c.story_type, + challenge.c.user, + challenge.c.vs_type, + func.max(challenge.c.play_difficulty).label("last_play_lv"), + ) + .where( + and_(challenge.c.user == aime_id, challenge.c.story_type == story_type) + ) + .group_by(challenge.c.vs_type) + ) + + sql = ( + select( + challenge.c.story_type, + challenge.c.vs_type, + challenge.c.cleared_difficulty.label("max_clear_lv"), + challenge.c.play_difficulty.label("last_play_lv"), + challenge.c.course_id, + challenge.c.play_count, + ) + .where( + and_( + challenge.c.user == subquery.c.user, + challenge.c.vs_type == subquery.c.vs_type, + challenge.c.play_difficulty == subquery.c.last_play_lv, + ), + ) + .order_by(challenge.c.vs_type) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_theory_courses(self, aime_id: int) -> Optional[List[Row]]: + sql = select(theory_course).where(theory_course.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_theory_course_by_powerhouse_lv( + self, aime_id: int, course_id: int, powerhouse_lv: int, count: int = 3 + ) -> Optional[List[Row]]: + sql = ( + select(theory_course) + .where( + and_( + theory_course.c.user != aime_id, + theory_course.c.course_id == course_id, + theory_course.c.powerhouse_lv == powerhouse_lv, + ) + ) + .order_by(func.rand()) + .limit(count) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_theory_course(self, aime_id: int, course_id: int) -> Optional[List[Row]]: + sql = select(theory_course).where( + and_( + theory_course.c.user == aime_id, theory_course.c.course_id == course_id + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_theory_partners(self, aime_id: int) -> Optional[List[Row]]: + sql = select(theory_partner).where(theory_partner.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_theory_running(self, aime_id: int) -> Optional[List[Row]]: + sql = select(theory_running).where(theory_running.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_theory_running_by_course( + self, aime_id: int, course_id: int + ) -> Optional[Row]: + sql = select(theory_running).where( + and_( + theory_running.c.user == aime_id, + theory_running.c.course_id == course_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_vs_infos(self, aime_id: int) -> Optional[List[Row]]: + sql = select(vs_info).where(vs_info.c.user == aime_id) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_stamps(self, aime_id: int) -> Optional[List[Row]]: + sql = select(stamp).where( + and_( + stamp.c.user == aime_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_timetrial_event(self, aime_id: int, timetrial_event_id: int) -> Optional[Row]: + sql = select(timetrial_event).where( + and_( + timetrial_event.c.user == aime_id, + timetrial_event.c.timetrial_event_id == timetrial_event_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_car(self, aime_id: int, version: int, car_data: Dict) -> Optional[int]: + car_data["user"] = aime_id + car_data["version"] = version + + sql = insert(car).values(**car_data) + conflict = sql.on_duplicate_key_update(**car_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_car: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_ticket(self, aime_id: int, ticket_data: Dict) -> Optional[int]: + ticket_data["user"] = aime_id + + sql = insert(ticket).values(**ticket_data) + conflict = sql.on_duplicate_key_update(**ticket_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_ticket: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_story(self, aime_id: int, story_data: Dict) -> Optional[int]: + story_data["user"] = aime_id + + sql = insert(story).values(**story_data) + conflict = sql.on_duplicate_key_update(**story_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_story: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_story_episode_play_status( + self, aime_id: int, chapter_id: int, play_status: int = 1 + ) -> Optional[int]: + sql = ( + update(episode) + .where(and_(episode.c.user == aime_id, episode.c.chapter == chapter_id)) + .values(play_status=play_status) + ) + + result = self.execute(sql) + if result is None: + self.logger.warn( + f"put_story_episode_play_status: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_story_episode( + self, aime_id: int, chapter_id: int, episode_data: Dict + ) -> Optional[int]: + episode_data["user"] = aime_id + episode_data["chapter"] = chapter_id + + sql = insert(episode).values(**episode_data) + conflict = sql.on_duplicate_key_update(**episode_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_story_episode: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_story_episode_difficulty( + self, aime_id: int, episode_id: int, difficulty_data: Dict + ) -> Optional[int]: + difficulty_data["user"] = aime_id + difficulty_data["episode"] = episode_id + + sql = insert(difficulty).values(**difficulty_data) + conflict = sql.on_duplicate_key_update(**difficulty_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_story_episode_difficulty: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]: + course_data["user"] = aime_id + + sql = insert(course).values(**course_data) + conflict = sql.on_duplicate_key_update(**course_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_course: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_time_trial( + self, version: int, aime_id: int, time_trial_data: Dict + ) -> Optional[int]: + time_trial_data["user"] = aime_id + time_trial_data["version"] = version + + sql = insert(trial).values(**time_trial_data) + conflict = sql.on_duplicate_key_update(**time_trial_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_time_trial: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_challenge(self, aime_id: int, challenge_data: Dict) -> Optional[int]: + challenge_data["user"] = aime_id + + sql = insert(challenge).values(**challenge_data) + conflict = sql.on_duplicate_key_update(**challenge_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_challenge: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_theory_course( + self, aime_id: int, theory_course_data: Dict + ) -> Optional[int]: + theory_course_data["user"] = aime_id + + sql = insert(theory_course).values(**theory_course_data) + conflict = sql.on_duplicate_key_update(**theory_course_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_theory_course: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_theory_partner( + self, aime_id: int, theory_partner_data: Dict + ) -> Optional[int]: + theory_partner_data["user"] = aime_id + + sql = insert(theory_partner).values(**theory_partner_data) + conflict = sql.on_duplicate_key_update(**theory_partner_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_theory_partner: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_theory_running( + self, aime_id: int, theory_running_data: Dict + ) -> Optional[int]: + theory_running_data["user"] = aime_id + + sql = insert(theory_running).values(**theory_running_data) + conflict = sql.on_duplicate_key_update(**theory_running_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_theory_running: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_vs_info(self, aime_id: int, vs_info_data: Dict) -> Optional[int]: + vs_info_data["user"] = aime_id + + sql = insert(vs_info).values(**vs_info_data) + conflict = sql.on_duplicate_key_update(**vs_info_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_vs_info: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_stamp( + self, aime_id: int, stamp_data: Dict + ) -> Optional[int]: + stamp_data["user"] = aime_id + + sql = insert(stamp).values(**stamp_data) + conflict = sql.on_duplicate_key_update(**stamp_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"putstamp: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_timetrial_event( + self, aime_id: int, time_trial_event_id: int, point: int + ) -> Optional[int]: + timetrial_event_data = { + "user": aime_id, + "timetrial_event_id": time_trial_event_id, + "point": point, + } + + sql = insert(timetrial_event).values(**timetrial_event_data) + conflict = sql.on_duplicate_key_update(**timetrial_event_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_timetrial_event: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid diff --git a/titles/idac/schema/profile.py b/titles/idac/schema/profile.py new file mode 100644 index 0000000..5e363ca --- /dev/null +++ b/titles/idac/schema/profile.py @@ -0,0 +1,440 @@ +from typing import Dict, List, Optional +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger +from sqlalchemy.engine.base import Connection +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select +from sqlalchemy.engine import Row +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata +from core.config import CoreConfig + +profile = Table( + "idac_profile", + 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("username", String(8)), + Column("country", Integer), + Column("store", Integer), + Column("team_id", Integer, server_default="0"), + Column("total_play", Integer, server_default="0"), + Column("daily_play", Integer, server_default="0"), + Column("day_play", Integer, server_default="0"), + Column("mileage", Integer, server_default="0"), + Column("asset_version", Integer, server_default="1"), + Column("last_play_date", TIMESTAMP, server_default=func.now()), + Column("mytitle_id", Integer, server_default="0"), + Column("mytitle_efffect_id", Integer, server_default="0"), + Column("sticker_id", Integer, server_default="0"), + Column("sticker_effect_id", Integer, server_default="0"), + Column("papercup_id", Integer, server_default="0"), + Column("tachometer_id", Integer, server_default="0"), + Column("aura_id", Integer, server_default="0"), + Column("aura_color_id", Integer, server_default="0"), + Column("aura_line_id", Integer, server_default="0"), + Column("bgm_id", Integer, server_default="0"), + Column("keyholder_id", Integer, server_default="0"), + Column("start_menu_bg_id", Integer, server_default="0"), + Column("use_car_id", Integer, server_default="1"), + Column("use_style_car_id", Integer, server_default="1"), + Column("bothwin_count", Integer, server_default="0"), + Column("bothwin_score", Integer, server_default="0"), + Column("subcard_count", Integer, server_default="0"), + Column("vs_history", Integer, server_default="0"), + Column("stamp_key_assign_0", Integer), + Column("stamp_key_assign_1", Integer), + Column("stamp_key_assign_2", Integer), + Column("stamp_key_assign_3", Integer), + Column("name_change_category", Integer, server_default="0"), + Column("factory_disp", Integer, server_default="0"), + Column("create_date", TIMESTAMP, server_default=func.now()), + Column("cash", Integer, server_default="0"), + Column("dressup_point", Integer, server_default="0"), + Column("avatar_point", Integer, server_default="0"), + Column("total_cash", Integer, server_default="0"), + UniqueConstraint("user", "version", name="idac_profile_uk"), + mysql_charset="utf8mb4", +) + +# No point setting defaults since the game sends everything on profile creation anyway +config = Table( + "idac_profile_config", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("config_id", Integer), + Column("steering_intensity", Integer), + Column("transmission_type", Integer), + Column("default_viewpoint", Integer), + Column("favorite_bgm", Integer), + Column("bgm_volume", Integer), + Column("se_volume", Integer), + Column("master_volume", Integer), + Column("store_battle_policy", Integer), + Column("battle_onomatope_display", Integer), + Column("cornering_guide", Integer), + Column("minimap", Integer), + Column("line_guide", Integer), + Column("ghost", Integer), + Column("race_exit", Integer), + Column("result_skip", Integer), + Column("stamp_select_skip", Integer), + UniqueConstraint("user", name="idac_profile_config_uk"), + mysql_charset="utf8mb4", +) + +# No point setting defaults since the game sends everything on profile creation anyway +avatar = Table( + "idac_profile_avatar", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("sex", Integer), + Column("face", Integer), + Column("eye", Integer), + Column("mouth", Integer), + Column("hair", Integer), + Column("glasses", Integer), + Column("face_accessory", Integer), + Column("body", Integer), + Column("body_accessory", Integer), + Column("behind", Integer), + Column("bg", Integer), + Column("effect", Integer), + Column("special", Integer), + UniqueConstraint("user", name="idac_profile_avatar_uk"), + mysql_charset="utf8mb4", +) + +rank = Table( + "idac_profile_rank", + 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("story_rank_exp", Integer, server_default="0"), + Column("story_rank", Integer, server_default="1"), + Column("time_trial_rank_exp", Integer, server_default="0"), + Column("time_trial_rank", Integer, server_default="1"), + Column("online_battle_rank_exp", Integer, server_default="0"), + Column("online_battle_rank", Integer, server_default="1"), + Column("store_battle_rank_exp", Integer, server_default="0"), + Column("store_battle_rank", Integer, server_default="1"), + Column("theory_exp", Integer, server_default="0"), + Column("theory_rank", Integer, server_default="1"), + Column("pride_group_id", Integer, server_default="0"), + Column("pride_point", Integer, server_default="0"), + Column("grade_exp", Integer, server_default="0"), + Column("grade", Integer, server_default="1"), + Column("grade_reward_dist", Integer, server_default="0"), + Column("story_rank_reward_dist", Integer, server_default="0"), + Column("time_trial_rank_reward_dist", Integer, server_default="0"), + Column("online_battle_rank_reward_dist", Integer, server_default="0"), + Column("store_battle_rank_reward_dist", Integer, server_default="0"), + Column("theory_rank_reward_dist", Integer, server_default="0"), + Column("max_attained_online_battle_rank", Integer, server_default="1"), + Column("max_attained_pride_point", Integer, server_default="0"), + Column("is_last_max", Integer, server_default="0"), + UniqueConstraint("user", "version", name="idac_profile_rank_uk"), + mysql_charset="utf8mb4", +) + +stock = Table( + "idac_profile_stock", + 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("mytitle_list", String(1024), server_default=""), + Column("mytitle_new_list", String(1024), server_default=""), + Column("avatar_face_list", String(255), server_default=""), + Column("avatar_face_new_list", String(255), server_default=""), + Column("avatar_eye_list", String(255), server_default=""), + Column("avatar_eye_new_list", String(255), server_default=""), + Column("avatar_hair_list", String(255), server_default=""), + Column("avatar_hair_new_list", String(255), server_default=""), + Column("avatar_body_list", String(255), server_default=""), + Column("avatar_body_new_list", String(255), server_default=""), + Column("avatar_mouth_list", String(255), server_default=""), + Column("avatar_mouth_new_list", String(255), server_default=""), + Column("avatar_glasses_list", String(255), server_default=""), + Column("avatar_glasses_new_list", String(255), server_default=""), + Column("avatar_face_accessory_list", String(255), server_default=""), + Column("avatar_face_accessory_new_list", String(255), server_default=""), + Column("avatar_body_accessory_list", String(255), server_default=""), + Column("avatar_body_accessory_new_list", String(255), server_default=""), + Column("avatar_behind_list", String(255), server_default=""), + Column("avatar_behind_new_list", String(255), server_default=""), + Column("avatar_bg_list", String(255), server_default=""), + Column("avatar_bg_new_list", String(255), server_default=""), + Column("avatar_effect_list", String(255), server_default=""), + Column("avatar_effect_new_list", String(255), server_default=""), + Column("avatar_special_list", String(255), server_default=""), + Column("avatar_special_new_list", String(255), server_default=""), + Column("stamp_list", String(255), server_default=""), + Column("stamp_new_list", String(255), server_default=""), + Column("keyholder_list", String(256), server_default=""), + Column("keyholder_new_list", String(256), server_default=""), + Column("papercup_list", String(255), server_default=""), + Column("papercup_new_list", String(255), server_default=""), + Column("tachometer_list", String(255), server_default=""), + Column("tachometer_new_list", String(255), server_default=""), + Column("aura_list", String(255), server_default=""), + Column("aura_new_list", String(255), server_default=""), + Column("aura_color_list", String(255), server_default=""), + Column("aura_color_new_list", String(255), server_default=""), + Column("aura_line_list", String(255), server_default=""), + Column("aura_line_new_list", String(255), server_default=""), + Column("bgm_list", String(255), server_default=""), + Column("bgm_new_list", String(255), server_default=""), + Column("dx_color_list", String(255), server_default=""), + Column("dx_color_new_list", String(255), server_default=""), + Column("start_menu_bg_list", String(255), server_default=""), + Column("start_menu_bg_new_list", String(255), server_default=""), + Column("under_neon_list", String(255), server_default=""), + UniqueConstraint("user", "version", name="idac_profile_stock_uk"), + mysql_charset="utf8mb4", +) + +theory = Table( + "idac_profile_theory", + 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("play_count", Integer, server_default="0"), + Column("play_count_multi", Integer, server_default="0"), + Column("partner_id", Integer), + Column("partner_progress", Integer), + Column("partner_progress_score", Integer), + Column("practice_start_rank", Integer, server_default="0"), + Column("general_flag", Integer, server_default="0"), + Column("vs_history", Integer, server_default="0"), + Column("vs_history_multi", Integer, server_default="0"), + Column("win_count", Integer, server_default="0"), + Column("win_count_multi", Integer, server_default="0"), + UniqueConstraint("user", "version", name="idac_profile_theory_uk"), + mysql_charset="utf8mb4", +) + + +class IDACProfileData(BaseData): + def __init__(self, cfg: CoreConfig, conn: Connection) -> None: + super().__init__(cfg, conn) + self.date_time_format_ext = ( + "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5] + ) + self.date_time_format_short = "%Y-%m-%d" + + def get_profile(self, aime_id: int, version: int) -> Optional[Row]: + sql = select(profile).where( + and_( + profile.c.user == aime_id, + profile.c.version == version, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_different_random_profiles( + self, aime_id: int, version: int, count: int = 9 + ) -> Optional[Row]: + sql = ( + select(profile) + .where( + and_( + profile.c.user != aime_id, + profile.c.version == version, + ) + ) + .order_by(func.rand()) + .limit(count) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_profile_config(self, aime_id: int) -> Optional[Row]: + sql = select(config).where( + and_( + config.c.user == aime_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_profile_avatar(self, aime_id: int) -> Optional[Row]: + sql = select(avatar).where( + and_( + avatar.c.user == aime_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_profile_rank(self, aime_id: int, version: int) -> Optional[Row]: + sql = select(rank).where( + and_( + rank.c.user == aime_id, + rank.c.version == version, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_profile_stock(self, aime_id: int, version: int) -> Optional[Row]: + sql = select(stock).where( + and_( + stock.c.user == aime_id, + stock.c.version == version, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_profile_theory(self, aime_id: int, version: int) -> Optional[Row]: + sql = select(theory).where( + and_( + theory.c.user == aime_id, + theory.c.version == version, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_profile( + self, aime_id: int, version: int, profile_data: Dict + ) -> Optional[int]: + profile_data["user"] = aime_id + profile_data["version"] = version + + sql = insert(profile).values(**profile_data) + conflict = sql.on_duplicate_key_update(**profile_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_profile: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_profile_config(self, aime_id: int, config_data: Dict) -> Optional[int]: + config_data["user"] = aime_id + + sql = insert(config).values(**config_data) + conflict = sql.on_duplicate_key_update(**config_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_profile_config: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_profile_avatar(self, aime_id: int, avatar_data: Dict) -> Optional[int]: + avatar_data["user"] = aime_id + + sql = insert(avatar).values(**avatar_data) + conflict = sql.on_duplicate_key_update(**avatar_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_profile_avatar: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid + + def put_profile_rank( + self, aime_id: int, version: int, rank_data: Dict + ) -> Optional[int]: + rank_data["user"] = aime_id + rank_data["version"] = version + + sql = insert(rank).values(**rank_data) + conflict = sql.on_duplicate_key_update(**rank_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_profile_rank: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_profile_stock( + self, aime_id: int, version: int, stock_data: Dict + ) -> Optional[int]: + stock_data["user"] = aime_id + stock_data["version"] = version + + sql = insert(stock).values(**stock_data) + conflict = sql.on_duplicate_key_update(**stock_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn(f"put_profile_stock: Failed to update! aime_id: {aime_id}") + return None + return result.lastrowid + + def put_profile_theory( + self, aime_id: int, version: int, theory_data: Dict + ) -> Optional[int]: + theory_data["user"] = aime_id + theory_data["version"] = version + + sql = insert(theory).values(**theory_data) + conflict = sql.on_duplicate_key_update(**theory_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_profile_theory: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid diff --git a/titles/idac/season2.py b/titles/idac/season2.py new file mode 100644 index 0000000..ca57392 --- /dev/null +++ b/titles/idac/season2.py @@ -0,0 +1,2505 @@ +from datetime import datetime, timedelta +import os +from random import choice +from typing import Any, Dict, List +import json +import logging + +from core.config import CoreConfig +from titles.idac.const import IDACConstants +from titles.idac.config import IDACConfig +from titles.idac.base import IDACBase + + +class IDACSeason2(IDACBase): + def __init__(self, core_cfg: CoreConfig, game_cfg: IDACConfig) -> None: + super().__init__(core_cfg, game_cfg) + self.version = IDACConstants.VER_IDAC_SEASON_2 + + # load the play stamps and timetrial events into memory + self.stamp_info = [] + if self.game_config.stamp.enable: + for stamp in self.game_config.stamp.enabled_stamps: + if not os.path.exists(f"./titles/idac/data/stamps/{stamp}.json"): + self.logger.warning(f"Stamp {stamp} is enabled but does not exist!") + continue + + with open( + f"./titles/idac/data/stamps/{stamp}.json", encoding="UTF-8" + ) as f: + self.logger.debug(f"Loading stamp {stamp}") + self.stamp_info.append(self._fix_dates(json.load(f))) + + self.timetrial_event = {} + self.timetrial_event_id = None + if self.game_config.timetrial.enable: + timetrial = self.game_config.timetrial.enabled_timetrial + if timetrial is not None: + if not os.path.exists(f"./titles/idac/data/timetrial/{timetrial}.json"): + self.logger.warning( + f"Timetrial {timetrial} is enabled but does not exist!" + ) + else: + self.logger.debug(f"Loading timetrial {timetrial}") + with open( + f"./titles/idac/data/timetrial/{timetrial}.json", + encoding="UTF-8", + ) as f: + self.timetrial_event = self._fix_dates(json.load(f)) + + # required for saving + self.timetrial_event_id = self.timetrial_event.get( + "timetrial_event_id" + ) + + def handle_alive_get_request(self, data: Dict, headers: Dict): + return { + "status_code": "0", + # 1 = success, 0 = failed + "server_status": 1, + "force_reboot_time": int(datetime.now().timestamp()) - 86400, + } + + def _fix_dates(self, input: dict): + """ + Fix "start_dt" and "end_dt" dates in a JSON file. + """ + output = {} + + self.logger.debug(f"Fixing dates in {type(input)}") + for key, value in input.items(): + if key in {"start_dt", "end_dt"}: + if isinstance(value, str): + value = int(datetime.strptime(value, "%Y-%m-%d").timestamp()) + + output[key] = value + return output + + def handle_boot_getconfigdata_request(self, data: Dict, headers: Dict): + """ + category: + 1 = D Coin + 3 = Car Dressup Token + 5 = Avatar Dressup Token + 6 = Tachometer + 7 = Aura + 8 = Aura Color + 9 = Avatar Face + 10 = Avatar Eye + 11 = Avatar Mouth + 12 = Avatar Hair + 13 = Avatar Glasses + 14 = Avatar Face accessories + 15 = Avatar Body + 18 = Avatar Background + 21 = Chat Stamp + 22 = Keychain + 24 = Title + 25 = FullTune Ticket + 26 = Paper Cup + 27 = BGM + 28 = Drifting Text + 31 = Start Menu BG + 32 = Car Color/Paint + 33 = Aura Level + 34 = FullTune Ticket Fragment + 35 = Underneon Lights + """ + version = headers["device_version"] + ver_str = version.replace(".", "")[:3] + + if self.core_cfg.server.is_develop: + domain_api_game = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDGT/{ver_str}/" + else: + domain_api_game = f"http://{self.core_cfg.title.hostname}/SDGT/{ver_str}/" + + return { + "status_code": "0", + "free_continue_enable": 1, + "free_continue_new": 1, + "free_continue_play": 1, + "difference_time_to_jp": 0, + # has to match the game asset version to show theory of street + "asset_version": "1", + # option version? MV01? + "optional_version": "1", + "disconnect_offset": 0, + "boost_balance_version": "0", + "time_release_number": "0", + "play_stamp_enable": 1, + "play_stamp_bonus_coin": 1, + "gacha_chara_needs": 1, + "both_win_system_control": 1, + "subcard_system_congrol": 1, + "server_maintenance_start_hour": 0, + "server_maintenance_start_minutes": 0, + "server_maintenance_end_hour": 0, + "server_maintenance_end_minutes": 0, + "domain_api_game": domain_api_game, + "domain_matching": f"http://{self.core_cfg.title.hostname}:{self.game_config.server.matching}", + "domain_echo1": f"{self.core_cfg.title.hostname}:{self.game_config.server.echo1}", + "domain_echo2": f"{self.core_cfg.title.hostname}:{self.game_config.server.echo2}", + "domain_ping": f"{self.core_cfg.title.hostname}", + "battle_gift_event_master": [], + "round_event": [ + { + "round_event_id": 30, + "round_event_nm": f"{self.core_cfg.server.name} Event", + "start_dt": int( + datetime.strptime("2023-01-01", "%Y-%m-%d").timestamp() + ), + "end_dt": int( + datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp() + ), + "round_start_rank": 0, + "save_filename": "0", + # https://info-initialdac.sega.jp/1898/ + "vscount": [ + { + "reward_upper_limit": 10, + "reward_lower_limit": 10, + "reward": [{"reward_category": 21, "reward_type": 483}], + }, + { + "reward_upper_limit": 40, + "reward_lower_limit": 40, + "reward": [{"reward_category": 21, "reward_type": 484}], + }, + { + "reward_upper_limit": 80, + "reward_lower_limit": 80, + "reward": [{"reward_category": 22, "reward_type": 516}], + }, + { + "reward_upper_limit": 120, + "reward_lower_limit": 120, + "reward": [{"reward_category": 21, "reward_type": 461}], + }, + { + "reward_upper_limit": 180, + "reward_lower_limit": 180, + "reward": [{"reward_category": 21, "reward_type": 462}], + } + ], + "rank": [], + "point": [], + "playable_course_list": [ + {"course_id": 4, "course_day": 0}, + {"course_id": 4, "course_day": 1}, + {"course_id": 6, "course_day": 0}, + {"course_id": 6, "course_day": 1}, + {"course_id": 8, "course_day": 0}, + {"course_id": 8, "course_day": 1}, + {"course_id": 10, "course_day": 0}, + {"course_id": 10, "course_day": 1}, + {"course_id": 12, "course_day": 0}, + {"course_id": 12, "course_day": 1}, + {"course_id": 14, "course_day": 0}, + {"course_id": 14, "course_day": 1}, + {"course_id": 16, "course_day": 0}, + {"course_id": 16, "course_day": 1}, + {"course_id": 18, "course_day": 0}, + {"course_id": 18, "course_day": 1}, + {"course_id": 20, "course_day": 0}, + {"course_id": 20, "course_day": 1}, + {"course_id": 22, "course_day": 0}, + {"course_id": 22, "course_day": 1}, + {"course_id": 24, "course_day": 0}, + {"course_id": 24, "course_day": 1}, + {"course_id": 26, "course_day": 0}, + {"course_id": 26, "course_day": 1}, + {"course_id": 36, "course_day": 0}, + {"course_id": 36, "course_day": 1}, + {"course_id": 38, "course_day": 0}, + {"course_id": 38, "course_day": 1}, + {"course_id": 40, "course_day": 0}, + {"course_id": 40, "course_day": 1}, + {"course_id": 42, "course_day": 0}, + {"course_id": 42, "course_day": 1}, + {"course_id": 44, "course_day": 0}, + {"course_id": 44, "course_day": 1}, + {"course_id": 46, "course_day": 0}, + {"course_id": 46, "course_day": 1}, + {"course_id": 48, "course_day": 0}, + {"course_id": 48, "course_day": 1}, + {"course_id": 50, "course_day": 0}, + {"course_id": 50, "course_day": 1}, + {"course_id": 52, "course_day": 0}, + {"course_id": 52, "course_day": 1}, + {"course_id": 54, "course_day": 0}, + {"course_id": 54, "course_day": 1}, + {"course_id": 56, "course_day": 0}, + {"course_id": 56, "course_day": 1}, + {"course_id": 58, "course_day": 0}, + {"course_id": 58, "course_day": 1}, + {"course_id": 68, "course_day": 0}, + {"course_id": 68, "course_day": 1}, + {"course_id": 70, "course_day": 0}, + {"course_id": 70, "course_day": 1}, + ], + } + ], + "last_round_event": [], + "last_round_event_ranking": [], + "round_event_exp": [], + "stamp_info": self.stamp_info, + # 0 = use default data, 1+ = server version of timereleasedata response + "timerelease_no": 3, + # 0 = use default data, 1+ = server version of gachadata response + "timerelease_avatar_gacha_no": 3, + "takeover_reward": [], + "subcard_judge": [ + { + "condition_id": 1, + "lower_rank": 0, + "higher_rank": 10, + "condition_start": 2, + "condition_end": 3, + } + ], + "special_promote": [{"counter": 1, "online_rank_id": 1}], + "matching_id": 1, + "matching_group": [ + { + "group_id": 1, + "group_percent": 1, + } + ], + "timetrial_disp_date": int( + datetime.strptime("2023-10-01", "%Y-%m-%d").timestamp() + ), + # price for every car + "buy_car_need_cash": 5000, + # number of buyable shop/customization time limits + "time_extension_limit": 1, + "collabo_id": 0, + "driver_debut_end_date": int( + datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp() + ), + "online_battle_param1": 1, + "online_battle_param2": 1, + "online_battle_param3": 1, + "online_battle_param4": 1, + "online_battle_param5": 1, + "online_battle_param6": 1, + "online_battle_param7": 1, + "online_battle_param8": 1, + "theory_open_version": "1.30", + "theory_close_version": "1.50", + "special_mode_data": { + "start_dt": int( + datetime.strptime("2023-01-01", "%Y-%m-%d").timestamp() + ), + "end_dt": int(datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp()), + "story_type": 4, # touhou special event + }, + "timetrial_event_data": self.timetrial_event, + } + + def handle_boot_bookkeep_request(self, data: Dict, headers: Dict): + pass + + def handle_boot_getgachadata_request(self, data: Dict, headers: Dict): + """ + Reward category types: + 9: Face + 10: Eye + 11: Mouth + 12: Hair + 13: Glasses + 14: Face accessories + 15: Body + 18: Background + """ + + with open("./titles/idac/data/avatarGacha.json", encoding="UTF-8") as f: + avatar_gacha_data = json.load(f) + + # avatar_gacha_data = { + # "status_code": "0", + # "avatar_gacha_data": [ + # { + # "avatar_gacha_id": 0, + # "avatar_gacha_nm": "Standard", + # "gacha_type": 0, + # "save_filename": "0", + # "use_ticket_cnt": 1, + # "start_dt": int( + # datetime.strptime("2019-01-01", "%Y-%m-%d").timestamp() + # ), + # "end_dt": int( + # datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp() + # ), + # "gacha_reward": [ + # { + # "reward_id": 117, + # "reward_type": 118, + # "reward_category": 18, + # "rate": 1000, + # "pickup_flag": 0, + # }, + # ], + # } + # ], + # } + + self.logger.debug( + f'Available avatar gacha items: {len(avatar_gacha_data["avatar_gacha_data"][0]["gacha_reward"])}' + ) + + return avatar_gacha_data + + def handle_boot_gettimereleasedata_request(self, data: Dict, headers: Dict): + """ + timerelease chapter: + 1 = Story: 1, 2, 3, 4, 5, 6, 7, 8, 9, 19 (Chapter 10), (29 Chapter 11 lol?) + 2 = MF Ghost: 10, 11, 12, 13, 14, 15 + 3 = Bunta: 15, 16, 17, 18, 19, 20, (21, 21, 22?) + 4 = Special Event: 23, 24, 25, 26, 27, 28 (Touhou Project) + """ + path = "./titles/idac/data/" + + # 1.00.00 is default + device_version_data = headers.get("device_version", "1.00.00") + device_version = int(device_version_data.replace(".", "")[:-2]) + + timerelease_filename = f"timeRelease_v{device_version:04d}" + timerelease_path = f"{path}{timerelease_filename}.json" + + # if the file doesn't exist, try to find the next lowest version + if not os.path.exists(timerelease_path): + while device_version > 100: + device_version -= 1 + timerelease_filename = f"timeRelease_v{device_version:04d}" + timerelease_path = f"{path}{timerelease_filename}.json" + + # if the file exists, break out of the loop + if os.path.exists(timerelease_path): + break + + self.logger.debug(f"Using time release file: {timerelease_filename}") + # load the time release data + with open(f"{path}{timerelease_filename}.json") as f: + time_release_data = json.load(f) + + return time_release_data + + def handle_advertise_getrankingdata_request(self, data: Dict, headers: Dict): + best_data = [] + for last_update in data.get("last_update_date"): + course_id = last_update.get("course_id") + + ranking = self.data.item.get_time_trial_ranking_by_course( + self.version, course_id + ) + ranking_data = [] + for i, rank in enumerate(ranking): + user_id = rank["user"] + + # get the username, country and store from the profile + profile = self.data.profile.get_profile(user_id, self.version) + arcade = self.data.arcade.get_arcade(profile["store"]) + + if arcade is None: + arcade = {} + arcade["name"] = self.core_cfg.server.name + + # should never happen + if profile is None: + continue + + ranking_data.append( + { + "course_id": course_id, + "rank": i + 1, + "username": profile["username"], + "value": rank["goal_time"], + # gat the store name from the profile + "store": arcade["name"], + # get the country id from the profile, 9 is JPN + "country": profile["country"], + "style_car_id": rank["style_car_id"], + # convert the datetime to a timestamp + "play_dt": int(rank["play_dt"].timestamp()), + "section_time_1": rank["section_time_1"], + "section_time_2": rank["section_time_2"], + "section_time_3": rank["section_time_3"], + "section_time_4": rank["section_time_4"], + "mission": rank["mission"], + } + ) + + best_data.append( + { + "course_id": course_id, + "ranking_data": ranking_data, + } + ) + + return { + "status_code": "0", + "national_best_data": best_data, + "shop_best_data": best_data, + "rank_management_flag": 0, + } + + def handle_login_checklock_request(self, data: Dict, headers: Dict): + user_id = data["id"] + access_code = data["accesscode"] + is_new_player = 0 + + # check that the user_id from access_code matches the user_id + if user_id == self.data.card.get_user_id_from_card(access_code): + lock_result = 1 + + # check if an IDAC profile already exists + p = self.data.profile.get_profile(user_id, self.version) + is_new_player = 1 if p is None else 0 + else: + lock_result = 0 + user_id = "" + + # other: in use + return { + "status_code": "0", + # 0 = already in use, 1 = good, 2 = too new + "lock_result": lock_result, + "lock_date": int(datetime.now().timestamp()), + "daily_play": 1, + "session": f"{user_id}", + "shared_security_key": "a", + "session_procseq": "a", + "new_player": is_new_player, + "server_status": 1, + } + + def handle_login_unlock_request(self, data: Dict, headers: Dict): + return { + "status_code": "0", + "lock_result": 1, + } + + def handle_login_relock_request(self, data: Dict, headers: Dict): + return { + "status_code": "0", + "lock_result": 1, + "lock_date": int(datetime.now().timestamp()), + } + + def handle_login_guestplay_request(self, data: Dict, headers: Dict): + # TODO + pass + + def _generate_story_data(self, user_id: int) -> Dict: + stories = self.data.item.get_stories(user_id) + + story_data = [] + for s in stories: + chapter_id = s["chapter"] + episodes = self.data.item.get_story_episodes(user_id, chapter_id) + + episode_data = [] + for e in episodes: + episode_id = e["episode"] + difficulties = self.data.item.get_story_episode_difficulties( + user_id, episode_id + ) + + difficulty_data = [] + for d in difficulties: + difficulty_data.append( + { + "difficulty": d["difficulty"], + "play_count": d["play_count"], + "clear_count": d["clear_count"], + "play_status": d["play_status"], + "play_score": d["play_score"], + } + ) + + episode_data.append( + { + "episode": e["episode"], + "play_status": e["play_status"], + "difficulty_data": difficulty_data, + } + ) + + story_data.append( + { + "story_type": s["story_type"], + "chapter": s["chapter"], + "loop_count": s["loop_count"], + "episode_data": episode_data, + } + ) + + return story_data + + def _generate_special_data(self, user_id: int) -> Dict: + # 4 = special mode + specials = self.data.item.get_best_challenges_by_vs_type(user_id, story_type=4) + + special_data = [] + for s in specials: + special_data.append( + { + "story_type": s["story_type"], + "vs_type": s["vs_type"], + "max_clear_lv": s["max_clear_lv"], + "last_play_lv": s["last_play_lv"], + # change to last_play_course_id? + "last_play_course_id": s["course_id"], + } + ) + + return special_data + + def _generate_challenge_data(self, user_id: int) -> Dict: + # challenge mode (Bunta challenge only right now) + challenges = self.data.item.get_best_challenges_by_vs_type( + user_id, story_type=3 + ) + + challenge_data = [] + for c in challenges: + challenge_data.append( + { + "story_type": c["story_type"], + "vs_type": c["vs_type"], + "max_clear_lv": c["max_clear_lv"], + "last_play_lv": c["last_play_lv"], + # change to last_play_course_id? + "last_play_course_id": c["course_id"], + "play_count": c["play_count"], + } + ) + + return challenge_data + + def _save_stock_data(self, user_id: int, stock_data: Dict): + updated_stock_data = {} + for k, v in stock_data.items(): + if v != "": + updated_stock_data[k] = v + + if updated_stock_data: + self.data.profile.put_profile_stock( + user_id, self.version, updated_stock_data + ) + + def handle_user_getdata_request(self, data: Dict, headers: Dict): + user_id = int(headers["session"]) + + # get the user's profile, can never be None + p = self.data.profile.get_profile(user_id, self.version) + user_data = p._asdict() + arcade = self.data.arcade.get_arcade(user_data["store"]) + + del user_data["id"] + del user_data["user"] + del user_data["version"] + user_data["id"] = user_id + user_data["store_name"] = ( + self.core_cfg.server.name if arcade is None else arcade["name"] + ) + user_data["last_play_date"] = int(user_data["last_play_date"].timestamp()) + user_data["create_date"] = int(user_data["create_date"].timestamp()) + + # get the user's rank + r = self.data.profile.get_profile_rank(user_id, self.version) + rank_data = r._asdict() + del rank_data["id"] + del rank_data["user"] + del rank_data["version"] + + # add the mode_rank_data to the user_data + user_data["mode_rank_data"] = rank_data + + # get the user's avatar + a = self.data.profile.get_profile_avatar(user_id) + avatar_data = a._asdict() + del avatar_data["id"] + del avatar_data["user"] + + # get the user's stock + s = self.data.profile.get_profile_stock(user_id, self.version) + stock_data = s._asdict() + del stock_data["id"] + del stock_data["user"] + del stock_data["version"] + + # get the user's config + c = self.data.profile.get_profile_config(user_id) + config_data = c._asdict() + del config_data["id"] + del config_data["user"] + config_data["id"] = config_data.pop("config_id") + + # get the user's ticket + tickets: list = self.data.item.get_tickets(user_id) + + """ + ticket_id: + 3 = Car Dressup Points + 5 = Avatar Dressup Points + 25 = Full Tune Tickets + 34 = Full Tune Fragments + """ + + ticket_data = [] + for ticket in tickets: + ticket_data.append( + { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"], + } + ) + + # get the user's course, required for the "course proeficiency" + courses = self.data.item.get_courses(user_id) + course_data = [] + for course in courses: + course_data.append( + { + "id": 0, # no clue, always 0? + "course_id": course["course_id"], + "run_counts": course["run_counts"], + # "course proeficiency" in exp points + "skill_level_exp": course["skill_level_exp"], + } + ) + + # get the profile theory data + theory_data = {} + theory = self.data.profile.get_profile_theory(user_id, self.version) + if theory is not None: + theory_data = theory._asdict() + del theory_data["id"] + del theory_data["user"] + del theory_data["version"] + + # get the users theory course data + theory_course_data = [] + theory_courses = self.data.item.get_theory_courses(user_id) + for course in theory_courses: + tmp = course._asdict() + del tmp["id"] + del tmp["user"] + tmp["update_dt"] = int(tmp["update_dt"].timestamp()) + + theory_course_data.append(tmp) + + # get the users theory partner data + theory_partner_data = [] + theory_partners = self.data.item.get_theory_partners(user_id) + for partner in theory_partners: + tmp = partner._asdict() + del tmp["id"] + del tmp["user"] + + theory_partner_data.append(tmp) + + # get the users theory running pram data + theory_running_pram_data = [] + theory_running = self.data.item.get_theory_running(user_id) + for running in theory_running: + tmp = running._asdict() + del tmp["id"] + del tmp["user"] + + theory_running_pram_data.append(tmp) + + # get the users vs info data + vs_info_data = [] + vs_info = self.data.item.get_vs_infos(user_id) + for vs in vs_info: + vs_info_data.append( + { + "battle_mode": 1, + "vs_cnt": 1, + "vs_win": vs["win_flg"], + "invalid": 0, + "str": 0, + "str_now": 0, + "lose_now": 0, + "vs_history": vs["vs_history"], + "course_select_priority": 0, + "vsinfo_course_data": [ + { + "course_id": vs["course_id"], + "vs_cnt": 1, + "vs_win": vs["win_flg"], + } + ], + } + ) + + # get the user's car + cars = self.data.item.get_cars(self.version, user_id, only_pickup=True) + fulltune_count = 0 + total_car_parts_count = 0 + car_data = [] + for car in cars: + tmp = car._asdict() + del tmp["id"] + del tmp["user"] + del tmp["version"] + + car_data.append(tmp) + # tune_level of 16 means fully tuned, so add 1 to fulltune_count + if car["tune_level"] >= 16: + fulltune_count += 1 + + # add the number of car parts to total_car_parts_count? + # total_car_parts_count += tmp["total_car_parts_count"] + + car_data.append(tmp) + + # update user profile car count + user_data["have_car_cnt"] = len(car_data) + + # get the user's play stamps + stamps = self.data.item.get_stamps(user_id) + stamp_event_data = [] + for stamp in stamps: + tmp = stamp._asdict() + del tmp["id"] + del tmp["user"] + + now = datetime.now() + + # create timestamp for today at 1am + this_day = now.replace(hour=1, minute=0, second=0, microsecond=0) + + # check if this_day is greater than or equal to create_date_daily + if this_day >= tmp["create_date_daily"]: + # reset the daily stamp + tmp["create_date_daily"] = now + tmp["daily_bonus"] = 0 + + # create a timestamp for this monday at 1am + this_monday = now - timedelta(days=now.weekday()) + this_monday = this_monday.replace(hour=1, minute=0, second=0, microsecond=0) + + # check if this_monday is greater than or equal to create_date_weekly + if this_monday >= tmp["create_date_weekly"]: + # reset the weekly stamp + tmp["create_date_weekly"] = now + tmp["weekly_bonus"] = 0 + + # update the play stamp in the database + self.data.item.put_stamp(user_id, tmp) + + del tmp["create_date_daily"] + del tmp["create_date_weekly"] + stamp_event_data.append(tmp) + + # get the user's timetrial event data + timetrial_event_data = {} + timetrial = self.data.item.get_timetrial_event(user_id, self.timetrial_event_id) + if timetrial is not None: + timetrial_event_data = { + "timetrial_event_id": timetrial["timetrial_event_id"], + "point": timetrial["point"], + } + + return { + "status_code": "0", + "user_base_data": user_data, + "avatar_data": avatar_data, + "pick_up_car_data": car_data, + "story_data": self._generate_story_data(user_id), + "vsinfo_data": vs_info_data, + "stock_data": stock_data, + "mission_data": { + "id": 0, + "achieve_flag": 0, + "received_flag": 0, + "update_dt": int(datetime.now().timestamp() - 86400), + }, + "weekly_mission_data": [], + "course_data": course_data, + "toppatu_event_data": { + "id": 0, + "event_id": 0, + "count1": 0, + "count2": 0, + "count3": 0, + "accept_flag": 0, + }, + "event_data": { + "id": 0, + "active_event_id": 0, + "dialog_show_date": int(datetime.now().timestamp() - 86400), + "show_start_dialog_flag": 1, + "show_progress_dialog_flag": 1, + "show_end_dialog_flag": 1, + "end_event_id": 0, + }, + "rewards_data": {}, + "login_bonus_data": { + "gacha_id": 0, + "gacha_item_id": 0, + "category": 0, + "type": 0, + }, + "frozen_data": {"frozen_status": 2}, + "penalty_data": {"penalty_flag": 0, "penalty_2_level": 0}, + "config_data": config_data, + "battle_gift_data": [], + "ticket_data": ticket_data, + "round_event": [], + "last_round_event": [], + "past_round_event": [], + "total_round_point": 0, + "stamp_event_data": stamp_event_data, + "avatar_gacha_lottery_data": {"avatar_gacha_id": 0}, + "fulltune_count": fulltune_count, + "total_car_parts_count": total_car_parts_count, + "car_layout_count": [], + "car_style_count": [], + "car_use_count": [], + "maker_use_count": [], + "story_course": [{"course_id": 0, "count": 1}], + # TODO! + # "driver_debut": { + # "play_count": 137, + # "daily_play": 5, + # "last_play_dt": 0, + # "use_start_date": 0, + # "use_end_date": 0, + # "use_dt": 0, + # "ticket_cnt": 0, + # "ticket_get_bit": 0, + # }, + "theory_data": theory_data, + "theory_course_data": theory_course_data, + "theory_partner_data": theory_partner_data, + "theory_running_pram_data": theory_running_pram_data, + "special_mode_data": self._generate_special_data(user_id), + "challenge_mode_data": self._generate_challenge_data(user_id), + "season_rewards_data": [], + "timetrial_event_data": timetrial_event_data, + "special_mode_hint_data": {"story_type": 0, "hint_display_flag": 0}, + } + + def handle_timetrial_getbestrecordpreta_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + for car_id in data["car_ids"]: + pass + + course_mybest_data = [] + courses = self.data.item.get_time_trial_user_best_courses(self.version, user_id) + for course in courses: + course_mybest_data.append( + { + "course_id": course["course_id"], + # local rank, store rank, worldwide rank? + "rank": 1, + # no clue + "member": 10000, + # goal_time in ms + "value": course["goal_time"], + # total number of entries per course? + "total": 10, + "store": self.core_cfg.server.name, + # use car_id from request? + "car_id": 0, + "style_car_id": course["style_car_id"], + "play_dt": course["play_dt"].timestamp(), + "section_time_1": course["section_time_1"], + "section_time_2": course["section_time_2"], + "section_time_3": course["section_time_3"], + "section_time_4": course["section_time_4"], + # no clue + "mission": course["mission"], + } + ) + + course_pickup_car_best_data = [] + courses = self.data.item.get_time_trial_courses(self.version) + for course in courses: + car_list = [] + best_cars = self.data.item.get_time_trial_best_cars_by_course( + self.version, course["course_id"], user_id + ) + + for i, car in enumerate(best_cars): + car_list.append( + { + "rank": i + 1, + # no clue + "member": user_id, + "value": car["goal_time"], + "store": self.core_cfg.server.name, + # use car_id from request? + "car_id": 0, + "style_car_id": car["style_car_id"], + "play_dt": car["play_dt"].timestamp(), + "section_time_1": car["section_time_1"], + "section_time_2": car["section_time_2"], + "section_time_3": car["section_time_3"], + "section_time_4": car["section_time_4"], + "mission": car["mission"], + } + ) + + course_pickup_car_best_data.append( + { + "course_id": course["course_id"], + "car_list": car_list, + } + ) + + return { + "status_code": "0", + "course_mybest_data": course_mybest_data, + "course_pickup_car_best_data": course_pickup_car_best_data, + } + + def handle_timetrial_getbestrecordprerace_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + course_id = data["course_id"] + for car in data["car_ids"]: + # TODO: get the best record for this car + style_car_id = car["style_car_id"] + + # Not sure if this is actually correct + ranking = self.data.item.get_time_trial_ranking_by_course( + self.version, course_id + ) + course_best_data = [] + for i, rank in enumerate(ranking): + car_user_id = rank["user"] + + # get the username, country and store from the profile + profile = self.data.profile.get_profile(car_user_id, self.version) + arcade = self.data.arcade.get_arcade(profile["store"]) + + if arcade is None: + arcade = {} + arcade["name"] = self.core_cfg.server.name + + # should never happen + if profile is None: + continue + + course_best_data.append( + { + "course_id": course_id, + "rank": i + 1, + "member": car_user_id, + "value": rank["goal_time"], + "store": arcade["name"], + # use car_id from request? + "car_id": 0, + "style_car_id": rank["style_car_id"], + "play_dt": rank["play_dt"].timestamp(), + "section_time_1": rank["section_time_1"], + "section_time_2": rank["section_time_2"], + "section_time_3": rank["section_time_3"], + "section_time_4": rank["section_time_4"], + "mission": rank["mission"], + } + ) + + best_cars = self.data.item.get_time_trial_best_cars_by_course( + self.version, course_id + ) + + car_list = [] + for i, rank in enumerate(best_cars): + car_user_id = rank["user"] + # get the username, country and store from the profile + profile = self.data.profile.get_profile(car_user_id, self.version) + arcade = self.data.arcade.get_arcade(profile["store"]) + + if arcade is None: + arcade = {} + arcade["name"] = self.core_cfg.server.name + + # should never happen + if profile is None: + continue + + car_list.append( + { + "rank": i + 1, + # no clue + "member": car_user_id, + "value": rank["goal_time"], + "store": arcade["name"], + # use car_id from request? + "car_id": 0, + "style_car_id": rank["style_car_id"], + "play_dt": rank["play_dt"].timestamp(), + "section_time_1": rank["section_time_1"], + "section_time_2": rank["section_time_2"], + "section_time_3": rank["section_time_3"], + "section_time_4": rank["section_time_4"], + "mission": rank["mission"], + } + ) + + return { + "status_code": "0", + "course_car_best_data": [{"course_id": course_id, "car_list": car_list}], + "course_best_data": course_best_data, + } + + def handle_user_createaccount_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + car_data: Dict = data.pop("car_obj") + parts_data: List = car_data.pop("parts_list") + avatar_data: Dict = data.pop("avatar_obj") + config_data: Dict = data.pop("config_obj") + + rank_data: Dict = data.pop("mode_rank_data") + stock_data: Dict = data.pop("takeover_stock_obj") + takeover_ticket_list: List = data.pop("takeover_ticket") + + # not required? + use_ticket = data.pop("use_ticket") + + # save profile in database + data["store"] = headers.get("a_store", 0) + data["country"] = headers.get("a_country", 0) + data["asset_version"] = headers.get("asset_version", 1) + self.data.profile.put_profile(user_id, self.version, data) + + # save rank data in database + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in takeover_ticket_list: + self.data.item.put_ticket(user_id, ticket) + + config_data["config_id"] = config_data.pop("id") + self.data.profile.put_profile_config(user_id, config_data) + self.data.profile.put_profile_avatar(user_id, avatar_data) + + # save car data and car parts in database + car_data["parts_list"] = parts_data + self.data.item.put_car(user_id, self.version, car_data) + + return {"status_code": "0"} + + def handle_user_updatelogin_request(self, data: Dict, headers: Dict): + pass + + def handle_timetrial_getcarbest_request(self, data: Dict, headers: Dict): + pass + + def handle_factory_avatargacharesult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + use_ticket_cnt = data["use_ticket_cnt"] + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # get the user's ticket + tickets: list = self.data.item.get_tickets(user_id) + ticket_list = [] + for ticket in tickets: + # avatar tickets + if ticket["ticket_id"] == 5: + ticket_data = { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"] - use_ticket_cnt, + } + + # update the ticket in the database + self.data.item.put_ticket(user_id, ticket_data) + ticket_list.append(ticket_data) + + continue + + ticket_list.append( + { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"], + } + ) + + return {"status_code": "0", "ticket_data": ticket_list} + + def handle_factory_savefavoritecar_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + # save favorite cars in database + for car in data["pickup_on_car_ids"]: + self.data.item.put_car(user_id, self.version, car) + + for car in data["pickup_off_car_ids"]: + self.data.item.put_car( + user_id, + self.version, + {"style_car_id": car["style_car_id"], "pickup_seq": 0}, + ) + + return {"status_code": "0"} + + def handle_factory_updatemultiplecustomizeresult_request( + self, data: Dict, headers: Dict + ): + user_id = headers["session"] + + car_list = data.pop("car_list") + ticket_data: List = data.pop("ticket_data") + + # unused + total_car_parts_count = data.pop("total_car_parts_count") + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + for car in car_list: + # save car data and car parts in database + self.data.item.put_car(user_id, self.version, car) + + return {"status_code": "0"} + + def handle_factory_updatecustomizeresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + parts_data: List = data.pop("parts_list") + ticket_data: List = data.pop("ticket_data") + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save car data in database + data["parts_list"] = parts_data + self.data.item.put_car(user_id, self.version, data) + + return {"status_code": "0"} + + def handle_factory_getcardata_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + cars = self.data.item.get_cars(self.version, user_id) + car_data = [] + for car in cars: + tmp = car._asdict() + del tmp["id"] + del tmp["user"] + del tmp["version"] + + car_data.append(tmp) + + return { + "status_code": "0", + "car_data": car_data, + } + + def handle_factory_renamebefore_request(self, data: Dict, headers: Dict): + pass + + def handle_factory_buycarresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + parts_data: List = data.pop("parts_list") + pickup_on_list: List = data.pop("pickup_on_car_ids") + pickup_off_list: List = data.pop("pickup_off_car_ids") + + style_car_id = data.get("style_car_id") + + # get the pickup_seq for the new car + pickup_seq = 0 + # save favorite cars in database + for car in pickup_on_list: + # if the new car is a favorite get the new pickup_seqn for later + if car["style_car_id"] == style_car_id: + pickup_seq = car["pickup_seq"] + else: + self.data.item.put_car(user_id, self.version, car) + + data["pickup_seq"] = pickup_seq + + cash = data.pop("cash") + total_cash = data.pop("total_cash") + + # save the new cash in database + self.data.profile.put_profile( + user_id, self.version, {"total_cash": total_cash, "cash": cash} + ) + + # full tune ticket + use_ticket = data.pop("use_ticket") + if use_ticket: + # get the user's tickets, full tune ticket id is 25 + ticket = self.data.item.get_ticket(user_id, ticket_id=25) + + # update the ticket in the database + self.data.item.put_ticket( + user_id, + { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"] - 1, + }, + ) + + # also set the tune_level to 16 (fully tuned) + data["tune_level"] = 16 + + # save car data and car parts in database + data["parts_list"] = parts_data + self.data.item.put_car(user_id, self.version, data) + + for car in pickup_off_list: + self.data.item.put_car( + user_id, + self.version, + {"style_car_id": car["style_car_id"], "pickup_seq": 0}, + ) + + # get the user's car + cars = self.data.item.get_cars(self.version, user_id) + fulltune_count = 0 + total_car_parts_count = 0 + for car in cars: + # tune_level of 16 means fully tuned, so add 1 to fulltune_count + if car["tune_level"] >= 16: + fulltune_count += 1 + + # add the number of car parts to total_car_parts_count + # total_car_parts_count += car["total_car_parts_count"] + + # get the user's ticket + tickets = self.data.item.get_tickets(user_id) + ticket_data = [] + for ticket in tickets: + ticket_data.append( + { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"], + } + ) + + return { + "status_code": "0", + "ticket_data": ticket_data, + "fulltune_count": fulltune_count, + "total_car_parts_count": total_car_parts_count, + "car_layout_count": [], + "car_style_count": [], + } + + def handle_factory_renameresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + new_username = data.get("username") + + # save new username in database + if new_username: + self.data.profile.put_profile(user_id, self.version, data) + + return {"status_code": "0"} + + def handle_factory_updatecustomizeavatar_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + avatar_data: Dict = data.pop("avatar_obj") + stock_data: Dict = data.pop("stock_obj") + + # update the stock data in database + self._save_stock_data(user_id, stock_data) + + # save avatar data and avatar parts in database + self.data.profile.put_profile_avatar(user_id, avatar_data) + + return {"status_code": "0"} + + def handle_factory_updatecustomizeuser_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + + # update the stock data in database + self._save_stock_data(user_id, stock_data) + + # update profile data and config in database + self.data.profile.put_profile(user_id, self.version, data) + + return {"status_code": "0"} + + def handle_user_updatestampinfo_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stamp_event_data = data.pop("stamp_event_data") + for stamp in stamp_event_data: + self.data.item.put_stamp(user_id, stamp) + + return {"status_code": "0"} + + def handle_user_updatetimetrialresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + driver_debut_data = data.pop("driver_debut_obj") + rank_data: Dict = data.pop("mode_rank_obj") + + # time trial event points + event_point = data.pop("event_point") + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save mode rank data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # update profile + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + "mileage": data.pop("mileage"), + }, + ) + + # get the use_count and story_use_count of the used car + style_car_id = data.get("style_car_id") + car_mileage = data.pop("car_mileage") + used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + + # increase the use_count and story_use_count of the used car + used_car["use_count"] += 1 + used_car["timetrial_use_count"] += 1 + used_car["car_mileage"] = car_mileage + + # save the used car in database + self.data.item.put_car(user_id, self.version, used_car) + + # skill_level_exp is the "course proeficiency" and is saved + # in the course table + course_id = data.get("course_id") + run_counts = 1 + skill_level_exp = data.pop("skill_level_exp") + + # get the course data + course = self.data.item.get_course(user_id, course_id) + if course: + # update run_counts + run_counts = course["run_counts"] + 1 + + self.data.item.put_course( + user_id, + { + "course_id": course_id, + "run_counts": run_counts, + "skill_level_exp": skill_level_exp, + }, + ) + + goal_time = data.get("goal_time") + # grab the ranking data and count the numbers of rows with a faster time + # than the current goal_time + course_rank = self.data.item.get_time_trial_ranking_by_course( + self.version, course_id, limit=None + ) + course_rank = len([r for r in course_rank if r["goal_time"] < goal_time]) + 1 + + car_course_rank = self.data.item.get_time_trial_ranking_by_course( + self.version, course_id, style_car_id, limit=None + ) + car_course_rank = ( + len([r for r in car_course_rank if r["goal_time"] < goal_time]) + 1 + ) + + # only update the time if its better than the best time and also not 0 + if data.get("goal_time") > 0: + # get the current best goal time + best_time_trial = ( + self.data.item.get_time_trial_user_best_time_by_course_car( + self.version, user_id, course_id, style_car_id + ) + ) + + if ( + best_time_trial is None + or data.get("goal_time") < best_time_trial["goal_time"] + ): + # now finally save the time trial with updated timestamp + data["play_dt"] = datetime.now() + self.data.item.put_time_trial(self.version, user_id, data) + + # update the timetrial event points + self.data.item.put_timetrial_event( + user_id, self.timetrial_event_id, event_point + ) + + return { + "status_code": "0", + "course_rank": course_rank, + "course_car_rank": car_course_rank, + "location_course_store_rank": course_rank, + "car_use_count": [], + "maker_use_count": [], + "timetrial_event_data": { + "timetrial_event_id": self.timetrial_event_id, + "point": event_point, + }, + } + + def handle_user_updatestoryresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + driver_debut_data = data.pop("driver_debut_obj") + rank_data: Dict = data.pop("mode_rank_obj") + # stamp_event_data = data.pop("stamp_event_data") + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save mode rank data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # save the current story progress in database + max_loop = data.get("chapter_loop_max") + chapter_id = data.get("chapter") + + episode_id = data.get("episode") + difficulty = data.get("difficulty") + play_status = data.get("play_status") + + # get the current loop from the database + story_data = self.data.item.get_story(user_id, chapter_id) + # 1 = active, 2+ = cleared? + loop_count = 1 + if story_data: + loop_count = story_data["loop_count"] + + # if the played difficulty is smaller than loop_count you cannot clear + # (play_status = 2) the episode otherwise the following difficulties + # won't earn any EXP? + if difficulty < loop_count: + play_status = 1 + + # if the episode has already been cleared, set the play_status to 2 + # so it won't be set to unplayed (play_status = 1) + episode_data = self.data.item.get_story_episode(user_id, episode_id) + if episode_data: + if play_status < episode_data["play_status"]: + play_status = 2 + + # save the current episode progress in database + self.data.item.put_story_episode( + user_id, + chapter_id, + { + "episode": episode_id, + "play_status": play_status, + }, + ) + + if loop_count < max_loop and data.get("chapter_clear") == 1: + # increase the loop count + loop_count += 1 + + # for the current chapter set all episode play_status back to 1 + self.data.item.put_story_episode_play_status(user_id, chapter_id, 1) + + self.data.item.put_story( + user_id, + { + "story_type": data.get("story_type"), + "chapter": chapter_id, + "loop_count": loop_count, + }, + ) + + # save the current episode difficulty progress in database + self.data.item.put_story_episode_difficulty( + user_id, + episode_id, + { + "difficulty": difficulty, + "play_count": 1, # no idea where this comes from + "clear_count": 1, # no idea where this comes from + "play_status": data.get("play_status"), + "play_score": data.get("play_score"), + }, + ) + + # get the use_count and story_use_count of the used car + style_car_id = data.get("style_car_id") + car_mileage = data.get("car_mileage") + used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + + # increase the use_count and story_use_count of the used car + used_car["use_count"] += 1 + used_car["story_use_count"] += 1 + used_car["car_mileage"] = car_mileage + + # save the used car in database + self.data.item.put_car(user_id, self.version, used_car) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save user profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.pop("mileage"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + }, + ) + + return { + "status_code": "0", + "story_data": self._generate_story_data(user_id), + "car_use_count": [], + "maker_use_count": [], + } + + def handle_user_updatespecialmoderesult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + driver_debut_data = data.pop("driver_debut_obj") + rank_data: Dict = data.pop("mode_rank_obj") + + # unused + hint_display_flag: int = data.pop("hint_display_flag") + + # get the vs use count from database and update it + style_car_id = data.pop("style_car_id") + car_data = self.data.item.get_car(user_id, self.version, style_car_id) + story_use_count = car_data["story_use_count"] + 1 + + # save car data in database + self.data.item.put_car( + user_id, + self.version, + { + "style_car_id": style_car_id, + "car_mileage": data.pop("car_mileage"), + "story_use_count": story_use_count, + }, + ) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save user profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.pop("mileage"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + }, + ) + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save ticket data in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save mode_rank and reward_dist data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # finally save the special mode with story_type=4 in database + self.data.item.put_challenge(user_id, data) + + return { + "status_code": "0", + "special_mode_data": self._generate_special_data(user_id), + "car_use_count": [], + "maker_use_count": [], + } + + def handle_user_updatechallengemoderesult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + driver_debut_data = data.pop("driver_debut_obj") + rank_data: Dict = data.pop("mode_rank_obj") + + # get the vs use count from database and update it + style_car_id = data.get("style_car_id") + car_data = self.data.item.get_car(user_id, self.version, style_car_id) + story_use_count = car_data["story_use_count"] + 1 + + # save car data in database + self.data.item.put_car( + user_id, + self.version, + { + "style_car_id": style_car_id, + "car_mileage": data.pop("car_mileage"), + "story_use_count": story_use_count, + }, + ) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save user profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.pop("mileage"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + }, + ) + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save ticket data in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save mode_rank and reward_dist data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # get the challenge mode data from database + challenge_data = self.data.item.get_challenge( + user_id, data.get("vs_type"), data.get("play_difficulty") + ) + + if challenge_data: + # update play count + play_count = challenge_data["play_count"] + 1 + data["play_count"] = play_count + + # finally save the challenge mode with story_type=3 in database + self.data.item.put_challenge(user_id, data) + + return { + "status_code": "0", + "challenge_mode_data": self._generate_challenge_data(user_id), + "car_use_count": [], + "maker_use_count": [], + } + + def _generate_time_trial_data(self, season_id: int, user_id: int) -> List[Dict]: + # get the season time trial data from database + timetrial_data = [] + + courses = self.data.item.get_courses(user_id) + if courses is None or len(courses) == 0: + return {"status_code": "0", "timetrial_data": timetrial_data} + + for course in courses: + # grab the course id and course proeficiency + course_id = course["course_id"] + skill_level_exp = course["skill_level_exp"] + + # get the best time for the current course for the current user + best_trial = self.data.item.get_time_trial_best_ranking_by_course( + season_id, user_id, course_id + ) + if not best_trial: + continue + + goal_time = best_trial["goal_time"] + # get the rank for the current course + course_rank = self.data.item.get_time_trial_ranking_by_course( + season_id, course_id, limit=None + ) + course_rank = ( + len([r for r in course_rank if r["goal_time"] < goal_time]) + 1 + ) + + timetrial_data.append( + { + "style_car_id": best_trial["style_car_id"], + "course_id": course_id, + "skill_level_exp": skill_level_exp, + "goal_time": goal_time, + "rank": course_rank, + "rank_dt": int(best_trial["play_dt"].timestamp()), + } + ) + + return timetrial_data + + def handle_user_getpastseasontadata_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + season_id = data.get("season_id") + + # so to get the season 1 data just subtract 1 from the season id + past_timetrial_data = self._generate_time_trial_data(season_id - 1, user_id) + + # TODO: get the current season timetrial data somehow, because after requesting + # GetPastSeasonTAData the game will NOT request GetTAData?! + return { + "status_code": "0", + "season_id": season_id, + "past_season_timetrial_data": past_timetrial_data, + } + + def handle_user_gettadata_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + timetrial_data = self._generate_time_trial_data(self.version, user_id) + + # TODO: get the past season timetrial data somehow, because after requesting + # GetTAData the game will NOT request GetPastSeasonTAData?! + return { + "status_code": "0", + "timetrial_data": timetrial_data, + # "past_season_timetrial_data": timetrial_data, + } + + def handle_user_updatecartune_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + # full tune ticket + use_ticket = data.pop("use_ticket") + if use_ticket: + # get the user's tickets, full tune ticket id is 25 + ticket = self.data.item.get_ticket(user_id, ticket_id=25) + + # update the ticket in the database + self.data.item.put_ticket( + user_id, + { + "ticket_id": ticket["ticket_id"], + "ticket_cnt": ticket["ticket_cnt"] - 1, + }, + ) + + # also set the tune_level to 16 (fully tuned) + data["tune_level"] = 16 + + self.data.item.put_car(user_id, self.version, data) + + return { + "status_code": "0", + "story_data": self._generate_story_data(user_id), + "car_use_count": [], + "maker_use_count": [], + } + + def handle_log_saveplaylog_request(self, data: Dict, headers: Dict): + pass + + def handle_log_saveendlog_request(self, data: Dict, headers: Dict): + pass + + def handle_user_updatemoderesult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + config_data: Dict = data.pop("config_obj") + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + + # not required? + mode_id = data.pop("mode_id") + standby_play_flag = data.pop("standby_play_flag") + tips_list = data.pop("tips_list") + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save rank dist data in database + self.data.profile.put_profile_rank(user_id, self.version, reward_dist_data) + + # update profile data and config in database + self.data.profile.put_profile(user_id, self.version, data) + config_data["config_id"] = config_data.pop("id") + self.data.profile.put_profile_config(user_id, config_data) + + return {"status_code": "0", "server_status": 1} + + def _generate_theory_rival_data( + self, user_list: list, course_id: int, req_user_id: int + ) -> list: + rival_data = [] + for user_id in user_list: + # if not enough players are available just use the data from the req_user + if user_id == -1: + profile = self.data.profile.get_profile(req_user_id, self.version) + profile = profile._asdict() + # set the name to CPU + profile["username"] = f"CPU" + # also reset stamps to default + profile["country"] = 9 + profile["store"] = 0 + profile["stamp_key_assign_0"] = 0 + profile["stamp_key_assign_1"] = 1 + profile["stamp_key_assign_2"] = 2 + profile["stamp_key_assign_3"] = 3 + profile["mytitle_id"] = 0 + else: + profile = self.data.profile.get_profile(user_id, self.version) + + rank = self.data.profile.get_profile_rank(profile["user"], self.version) + + avatars = [ + { + "sex": 0, + "face": 1, + "eye": 1, + "mouth": 1, + "hair": 1, + "glasses": 0, + "face_accessory": 0, + "body": 1, + "body_accessory": 0, + "behind": 0, + "bg": 1, + "effect": 0, + "special": 0, + }, + { + "sex": 0, + "face": 1, + "eye": 1, + "mouth": 1, + "hair": 19, + "glasses": 0, + "face_accessory": 0, + "body": 2, + "body_accessory": 0, + "behind": 0, + "bg": 1, + "effect": 0, + "special": 0, + }, + { + "sex": 1, + "face": 91, + "eye": 265, + "mouth": 13, + "hair": 369, + "glasses": 0, + "face_accessory": 0, + "body": 113, + "body_accessory": 0, + "behind": 0, + "bg": 1, + "effect": 0, + "special": 0, + }, + { + "sex": 1, + "face": 91, + "eye": 265, + "mouth": 13, + "hair": 387, + "glasses": 0, + "face_accessory": 0, + "body": 114, + "body_accessory": 0, + "behind": 0, + "bg": 1, + "effect": 0, + "special": 0, + }, + ] + + if user_id == -1: + # get a random avatar from the list and some random car from all users + avatar = choice(avatars) + car = self.data.item.get_random_car(self.version) + else: + avatar = self.data.profile.get_profile_avatar(profile["user"]) + car = self.data.item.get_random_user_car(profile["user"], self.version) + + parts_list = [] + for part in car["parts_list"]: + parts_list.append(part["parts"]) + + course = self.data.item.get_theory_course(profile["user"], course_id) + powerhose_lv = 0 + if course: + powerhose_lv = course["powerhouse_lv"] + + theory_running = self.data.item.get_theory_running_by_course( + profile["user"], course_id + ) + + # normally it's 127 after the first play so we set it to 128 + attack = 128 + defense = 128 + safety = 128 + runaway = 128 + trick_flag = 0 + if theory_running and user_id != -1: + attack = theory_running["attack"] + defense = theory_running["defense"] + safety = theory_running["safety"] + runaway = theory_running["runaway"] + trick_flag = theory_running["trick_flag"] + + # get the time trial ranking medal + eval_id = 0 + time_trial = self.data.item.get_time_trial_best_ranking_by_course( + self.version, profile["user"], course_id + ) + if time_trial: + eval_id = time_trial["eval_id"] + + arcade = self.data.arcade.get_arcade(profile["store"]) + if arcade is None: + arcade = {} + arcade["name"] = self.core_cfg.server.name + + rival_data.append( + { + "id": profile["user"], + "name": profile["username"], + "grade": rank["grade"], + # only needed for power match + "powerhouseLv": powerhose_lv, + "mytitleId": profile["mytitle_id"], + "country": profile["country"], + "auraId": profile["aura_id"], + "auraColor": profile["aura_color_id"], + "auraLine": profile["aura_line_id"], + # not sure? + "roundRanking": 0, + "storeName": arcade["name"], + "sex": avatar["sex"], + "face": avatar["face"], + "eye": avatar["eye"], + "mouth": avatar["mouth"], + "hair": avatar["hair"], + "glasses": avatar["glasses"], + "faceAccessory": avatar["face_accessory"], + "body": avatar["body"], + "bodyAccessory": avatar["body_accessory"], + "behind": avatar["behind"], + "bg": avatar["bg"], + "effect": avatar["effect"], + "special": avatar["special"], + "styleCarId": car["style_car_id"], + "color": car["color"], + "bureau": car["bureau"], + "kana": car["kana"], + "sNo": car["s_no"], + "lNo": car["l_no"], + "tuneLv": car["tune_level"], + "carFlag": car["car_flag"], + "tunePoint": car["tune_point"], + "infinityTune": car["infinity_tune"], + "tuneParts": car["tune_parts"], + "partsList": parts_list, + "partsCount": car["equip_parts_count"], + "stamp0": profile["stamp_key_assign_0"], + "stamp1": profile["stamp_key_assign_1"], + "stamp2": profile["stamp_key_assign_2"], + "stamp3": profile["stamp_key_assign_3"], + "attack": attack, + "defense": defense, + "safety": safety, + "runaway": runaway, + "trickFlg": trick_flag, + # time trial ranking medal + "taEval": eval_id, + } + ) + + return rival_data + + def handle_theory_matching_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + course_id = data.pop("course_id") + # no idea why thats needed? + grade = data.pop("grade") + + # number of auto_matches and power_matches, official values are: + count_auto_match = 9 + count_power_match = 3 + + # required for the power_match list? + powerhose_lv = data.pop("powerhouse_lv") + + # get random profiles for auto match + profiles = self.data.profile.get_different_random_profiles( + user_id, self.version, count=count_auto_match + ) + + user_list = [profile["user"] for profile in profiles] + # if user_list is not count_auto_match long, fill it up with -1 + while len(user_list) < count_auto_match: + user_list.append(-1) + + auto_match = self._generate_theory_rival_data(user_list, course_id, user_id) + + # get profiles with the same powerhouse_lv for power match + theory_courses = self.data.item.get_theory_course_by_powerhouse_lv( + user_id, course_id, powerhose_lv, count=count_power_match + ) + user_list = [course["user"] for course in theory_courses] + + # if user_list is not count_power_match long, fill it up with -1 + while len(user_list) < count_power_match: + user_list.append(-1) + + power_match = self._generate_theory_rival_data(user_list, course_id, user_id) + + return { + "status_code": "0", + "server_status": 1, + "rival_data": { + "auto_match": auto_match, + "power_match": power_match, + }, + } + + def handle_user_updatetheoryresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + rank_data: Dict = data.pop("mode_rank_obj") + driver_debut_data: Dict = data.pop("driver_debut_obj") + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save rank dist data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # save the profile theory data in database + play_count = 1 + play_count_multi = 1 + win_count = 0 + win_count_multi = 0 + + theory_data = self.data.profile.get_profile_theory(user_id, self.version) + if theory_data: + play_count = theory_data["play_count"] + 1 + play_count_multi = theory_data["play_count_multi"] + 1 + win_count = theory_data["win_count"] + win_count_multi = theory_data["win_count_multi"] + + # check all advantages and see if one of them is larger than 0 + # if so, we won + if ( + data.get("advantage_1") > 0 + or data.get("advantage_2") > 0 + or data.get("advantage_3") > 0 + or data.get("advantage_4") > 0 + ): + win_count += 1 + win_count_multi += 1 + + self.data.profile.put_profile_theory( + user_id, + self.version, + { + "play_count": play_count, + "play_count_multi": play_count_multi, + "partner_id": data.get("partner_id"), + "partner_progress": data.get("partner_progress"), + "partner_progress_score": data.get("partner_progress_score"), + "practice_start_rank": data.get("practice_start_rank"), + "general_flag": data.get("general_flag"), + "vs_history": data.get("vs_history"), + # no idea? + "vs_history_multi": data.get("vs_history"), + "win_count": win_count, + "win_count_multi": win_count_multi, + }, + ) + + # save theory course in database + self.data.item.put_theory_course( + user_id, + { + "course_id": data.get("course_id"), + "max_victory_grade": data.get("max_victory_grade"), + # always add 1? + "run_count": 1, + "powerhouse_lv": data.get("powerhouse_lv"), + "powerhouse_exp": data.get("powerhouse_exp"), + # not sure if the played_powerhouse_lv is the same as powerhouse_lv + "played_powerhouse_lv": data.get("powerhouse_lv"), + }, + ) + + # save the theory partner in database + self.data.item.put_theory_partner( + user_id, + { + "partner_id": data.get("partner_id"), + "fellowship_lv": data.get("fellowship_lv"), + "fellowship_exp": data.get("fellowship_exp"), + }, + ) + + # save the theory running in database? + self.data.item.put_theory_running( + user_id, + { + "course_id": data.get("course_id"), + "attack": data.get("attack"), + "defense": data.get("defense"), + "safety": data.get("safety"), + "runaway": data.get("runaway"), + "trick_flag": data.get("trick_flag"), + }, + ) + + # get the use_count and theory_use_count of the used car + style_car_id = data.get("style_car_id") + car_mileage = data.get("car_mileage") + used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + + # increase the use_count and theory_use_count of the used car + used_car["use_count"] += 1 + used_car["theory_use_count"] += 1 + used_car["car_mileage"] = car_mileage + + # save the used car in database + self.data.item.put_car(user_id, self.version, used_car) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save the profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.get("mileage"), + "aura_id": data.get("aura_id"), + "aura_color_id": data.get("aura_color_id"), + "aura_line_id": data.get("aura_line_id"), + "cash": data.get("cash"), + "total_cash": data.get("total_cash"), + "dressup_point": data.get("dressup_point"), + "avatar_point": data.get("avatar_point"), + }, + ) + + return { + "status_code": "0", + "played_powerhouse_lv": data.get("powerhouse_lv"), + "car_use_count": [], + "maker_use_count": [], + "play_count": play_count, + "play_count_multi": play_count_multi, + "win_count": win_count, + "win_count_multi": win_count_multi, + } + + def handle_timetrial_getbestrecordprebattle_request( + self, data: Dict, headers: Dict + ): + user_id = headers["session"] + + course_pickup_car_best_data = [] + courses = self.data.item.get_time_trial_courses(self.version) + for course in courses: + car_list = [] + best_cars = self.data.item.get_time_trial_best_cars_by_course( + self.version, course["course_id"], user_id + ) + + for i, car in enumerate(best_cars): + car_list.append( + { + "rank": i + 1, + # no clue + "member": user_id, + "value": car["goal_time"], + "store": self.core_cfg.server.name, + # use car_id from request? + "car_id": 0, + "style_car_id": car["style_car_id"], + "play_dt": car["play_dt"].timestamp(), + "section_time_1": car["section_time_1"], + "section_time_2": car["section_time_2"], + "section_time_3": car["section_time_3"], + "section_time_4": car["section_time_4"], + "mission": car["mission"], + } + ) + + course_pickup_car_best_data.append( + { + "course_id": course["course_id"], + "car_list": car_list, + } + ) + + return { + "status_code": "0", + "course_pickup_car_best_data": course_pickup_car_best_data, + } + + def handle_user_updateonlinebattle_request(self, data: Dict, headers: Dict): + return { + "status_code": "0", + "bothwin_penalty": 1, + } + + def handle_user_updateonlinebattleresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + # save stock data in database + self._save_stock_data(user_id, stock_data) + + ticket_data: List = data.pop("ticket_data") + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + reward_dist_data: Dict = data.pop("reward_dist_obj") + rank_data: Dict = data.pop("mode_rank_obj") + + # save rank dist data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + driver_debut_data = data.pop("driver_debut_obj") + + # get the use_count and net_vs_use_count of the used car + style_car_id = data.get("style_car_id") + car_mileage = data.pop("car_mileage") + used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + + # increase the use_count and net_vs_use_count of the used car + used_car["use_count"] += 1 + used_car["net_vs_use_count"] += 1 + used_car["car_mileage"] = car_mileage + + # save the used car in database + self.data.item.put_car(user_id, self.version, used_car) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save the profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.pop("mileage"), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + }, + ) + + self.data.item.put_vs_info(user_id, data) + + vs_info = { + "battle_mode": 0, + "vs_cnt": 1, + "vs_win": data.get("win_flg"), + "invalid": 0, + "str": 0, + "str_now": 0, + "lose_now": 0, + "vs_history": data.get("vs_history"), + "course_select_priority": data.get("course_select_priority"), + "vsinfo_course_data": [ + { + "course_id": data.get("course_id"), + "vs_cnt": 1, + "vs_win": data.get("win_flg"), + } + ], + } + + return { + "status_code": "0", + "vsinfo_data": vs_info, + "round_event": [ + { + "count": 1, + "win": 1, + "rank": 1, + "point": 1, + "total_round_point": 1, + } + ], + "car_use_count": [], + "maker_use_count": [], + } + + def handle_user_updatestorebattleresult_request(self, data: Dict, headers: Dict): + user_id = headers["session"] + + stock_data: Dict = data.pop("stock_obj") + ticket_data: List = data.pop("ticket_data") + reward_dist_data: Dict = data.pop("reward_dist_obj") + rank_data: Dict = data.pop("mode_rank_obj") + driver_debut_data: Dict = data.pop("driver_debut_obj") + + # no idea? + result = data.pop("result") + battle_gift_event_id = data.pop("battle_gift_event_id") + gift_id = data.pop("gift_id") + + # save stock data in database + self._save_stock_data(user_id, stock_data) + + # save tickets in database + for ticket in ticket_data: + self.data.item.put_ticket(user_id, ticket) + + # save rank dist data in database + rank_data.update(reward_dist_data) + self.data.profile.put_profile_rank(user_id, self.version, rank_data) + + # get the use_count and net_vs_use_count of the used car + style_car_id = data.get("style_car_id") + car_mileage = data.pop("car_mileage") + used_car = self.data.item.get_car(user_id, self.version, style_car_id)._asdict() + + # increase the use_count and net_vs_use_count of the used car + used_car["use_count"] += 1 + used_car["vs_use_count"] += 1 + used_car["car_mileage"] = car_mileage + + # save the used car in database + self.data.item.put_car(user_id, self.version, used_car) + + # get the profile data, update total_play and daily_play, and save it + profile = self.data.profile.get_profile(user_id, self.version) + total_play = profile["total_play"] + 1 + + # save the profile in database + self.data.profile.put_profile( + user_id, + self.version, + { + "total_play": total_play, + "last_play_date": datetime.now(), + "mileage": data.pop("mileage"), + "aura_id": data.pop("aura_id"), + "aura_color_id": data.pop("aura_color_id"), + "aura_line_id": data.pop("aura_line_id"), + "cash": data.pop("cash"), + "total_cash": data.pop("total_cash"), + "dressup_point": data.pop("dressup_point"), + "avatar_point": data.pop("avatar_point"), + }, + ) + + # save vs_info in database + self.data.item.put_vs_info(user_id, data) + + vs_info = { + "battle_mode": 0, + "vs_cnt": 1, + "vs_win": data.get("win_flg"), + "invalid": 0, + "str": 0, + "str_now": 0, + "lose_now": 0, + "vs_history": data.get("vs_history"), + "course_select_priority": 0, + "vsinfo_course_data": [ + { + "course_id": data.get("course_id"), + "vs_cnt": 1, + "vs_win": data.get("win_flg"), + } + ], + } + + return { + "status_code": "0", + "vsinfo_data": vs_info, + "car_use_count": [], + "maker_use_count": [], + } diff --git a/titles/idz/handlers/load_server_info.py b/titles/idz/handlers/load_server_info.py index ef6e81c..4c60dd7 100644 --- a/titles/idz/handlers/load_server_info.py +++ b/titles/idz/handlers/load_server_info.py @@ -1,5 +1,6 @@ import struct +from core.utils import Utils from .base import IDZHandlerBase from core.config import CoreConfig from ..config import IDZConfig @@ -20,9 +21,11 @@ class IDZHandlerLoadServerInfo(IDZHandlerBase): offset = 0 if self.version >= IDZConstants.VER_IDZ_210: offset = 2 + + t_port = Utils.get_title_port(self.core_config) - news_str = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDDF/230/news/news80**.txt" - err_str = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDDF/230/error" + news_str = f"http://{self.core_config.title.hostname}:{t_port}/idz/news/news80**.txt" + err_str = f"http://{self.core_config.title.hostname}:{t_port}/idz/error" len_hostname = len(self.core_config.title.hostname) len_news = len(news_str) diff --git a/titles/idz/index.py b/titles/idz/index.py index 39d21c2..62473f9 100644 --- a/titles/idz/index.py +++ b/titles/idz/index.py @@ -85,7 +85,8 @@ class IDZServlet(BaseServlet): def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: return[ - [("render_GET", "/{game}/{version}/{endpoint:.*?}", {'game': R'S...'})], # TODO: Slim this down to only the news stuff + [("render_GET", "/idz/news/{endpoint:.*?}", {}), + ("render_GET", "/idz/error", {})], [] ] diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index c72c086..2f87b19 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -157,54 +157,44 @@ class OngekiBase: return {"type": data["type"], "length": 0, "gameIdlistList": []} def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: - return {"length": 0, "gameRankingList": []} + game_ranking_list = self.data.static.get_ranking_list(self.version) + + ranking_list = [] + for music in game_ranking_list: + tmp = music._asdict() + ranking_list.append(tmp) - def handle_get_game_point_api_request(self, data: Dict) -> Dict: - """ - Sets the GP amount for A and B sets for 1 - 3 credits - """ + if ranking_list is None: + return {"length": 0, "gameRankingList": []} return { - "length": 6, - "gamePointList": [ - { - "type": 0, - "cost": 100, - "startDate": "2000-01-01 05:00:00.0", - "endDate": "2099-01-01 05:00:00.0", - }, - { - "type": 1, - "cost": 230, - "startDate": "2000-01-01 05:00:00.0", - "endDate": "2099-01-01 05:00:00.0", - }, - { - "type": 2, - "cost": 370, - "startDate": "2000-01-01 05:00:00.0", - "endDate": "2099-01-01 05:00:00.0", - }, - { - "type": 3, - "cost": 120, - "startDate": "2000-01-01 05:00:00.0", - "endDate": "2099-01-01 05:00:00.0", - }, - { - "type": 4, - "cost": 240, - "startDate": "2000-01-01 05:00:00.0", - "endDate": "2099-01-01 05:00:00.0", - }, - { - "type": 5, - "cost": 360, - "startDate": "2000-01-01 05:00:00.0", - "endDate": "2099-01-01 05:00:00.0", - }, - ], + "type": data["type"], + "gameRankingList": ranking_list, } + + def handle_get_game_point_api_request(self, data: Dict) -> Dict: + get_game_point = self.data.static.get_static_game_point() + game_point = [] + + if not get_game_point: + self.logger.info(f"GP table is empty, inserting defaults") + self.data.static.put_static_game_point_defaults() + get_game_point = self.data.static.get_static_game_point() + for gp in get_game_point: + tmp = gp._asdict() + game_point.append(tmp) + return { + "length": len(game_point), + "gamePointList": game_point, + } + for gp in get_game_point: + tmp = gp._asdict() + game_point.append(tmp) + return { + "length": len(game_point), + "gamePointList": game_point, + } + def handle_game_login_api_request(self, data: Dict) -> Dict: return {"returnCode": 1, "apiName": "gameLogin"} @@ -215,11 +205,39 @@ class OngekiBase: return {"returnCode": 1, "apiName": "ExtendLockTimeApi"} def handle_get_game_reward_api_request(self, data: Dict) -> Dict: - # TODO: reward list - return {"length": 0, "gameRewardList": []} + get_game_rewards = self.data.static.get_reward_list(self.version) + + reward_list = [] + for reward in get_game_rewards: + tmp = reward._asdict() + tmp.pop("id") + tmp.pop("version") + tmp.pop("rewardname") + reward_list.append(tmp) + + if reward_list is None: + return {"length": 0, "gameRewardList": []} + return { + "length": len(reward_list), + "gameRewardList": reward_list, + } def handle_get_game_present_api_request(self, data: Dict) -> Dict: - return {"length": 0, "gamePresentList": []} + get_present = self.data.static.get_present_list(self.version) + + present_list = [] + for present in get_present: + tmp = present._asdict() + tmp.pop("id") + tmp.pop("version") + present_list.append(tmp) + + if present_list is None: + return {"length": 0, "gamePresentList": []} + return { + "length": len(present_list), + "gamePresentList": present_list, + } def handle_get_game_message_api_request(self, data: Dict) -> Dict: return {"length": 0, "gameMessageList": []} @@ -228,12 +246,39 @@ class OngekiBase: return {"length": 0, "gameSaleList": []} def handle_get_game_tech_music_api_request(self, data: Dict) -> Dict: - return {"length": 0, "gameTechMusicList": []} + music_list = self.data.static.get_tech_music(self.version) + + prep_music_list = [] + for music in music_list: + tmp = music._asdict() + tmp.pop("id") + tmp.pop("version") + prep_music_list.append(tmp) + + if prep_music_list is None: + return {"length": 0, "gameTechMusicList": []} + + return { + "length": len(prep_music_list), + "gameTechMusicList": prep_music_list, + } def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: + if self.core_cfg.server.is_develop: + return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} + + client_id = data["clientId"] + client_setting_data = data["clientSetting"] + self.data.static.put_client_setting_data(client_id, client_setting_data) return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: + if self.core_cfg.server.is_develop: + return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} + + region_id = data["regionId"] + client_testmode_data = data["clientTestmode"] + self.data.static.put_client_testmode_data(region_id, client_testmode_data) return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: @@ -277,13 +322,12 @@ class OngekiBase: "id": event["eventId"], # actually use the startDate from the import so it # properly shows all the events when new ones are imported - "startDate": datetime.strftime( - event["startDate"], "%Y-%m-%d %H:%M:%S.0" - ), - "endDate": "2099-12-31 00:00:00.0", + "startDate": datetime.strftime(event["startDate"], "%Y-%m-%d %H:%M:%S.0"), + #"endDate": "2099-12-31 00:00:00.0", + "endDate": datetime.strftime(event["endDate"], "%Y-%m-%d %H:%M:%S.0"), } ) - + return { "type": data["type"], "length": len(evt_list), @@ -385,7 +429,7 @@ class OngekiBase: } def handle_get_user_tech_event_api_request(self, data: Dict) -> Dict: - user_tech_event_list = self.data.item.get_tech_event(data["userId"]) + user_tech_event_list = self.data.item.get_tech_event(self.version, data["userId"]) if user_tech_event_list is None: return {} @@ -394,6 +438,7 @@ class OngekiBase: tmp = evt._asdict() tmp.pop("id") tmp.pop("user") + tmp.pop("version") tech_evt.append(tmp) return { @@ -403,15 +448,24 @@ class OngekiBase: } def handle_get_user_tech_event_ranking_api_request(self, data: Dict) -> Dict: - # user_event_ranking_list = self.data.item.get_tech_event_ranking(data["userId"]) - # if user_event_ranking_list is None: return {} + user_tech_event_ranks = self.data.item.get_tech_event_ranking(self.version, data["userId"]) + if user_tech_event_ranks is None: + return { + "userId": data["userId"], + "length": 0, + "userTechEventRankingList": [], + } + # collect the whole table and clear other players, to preserve proper ranking evt_ranking = [] - # for evt in user_event_ranking_list: - # tmp = evt._asdict() - # tmp.pop("id") - # tmp.pop("user") - # evt_ranking.append(tmp) + for evt in user_tech_event_ranks: + tmp = evt._asdict() + if tmp["user"] != data["userId"]: + tmp.clear() + else: + tmp.pop("id") + tmp.pop("user") + evt_ranking.append(tmp) return { "userId": data["userId"], @@ -533,20 +587,26 @@ class OngekiBase: return {"userId": data["userId"], "userData": user_data} def handle_get_user_event_ranking_api_request(self, data: Dict) -> Dict: - # user_event_ranking_list = self.data.item.get_event_ranking(data["userId"]) - # if user_event_ranking_list is None: return {} + user_event_ranking_list = self.data.item.get_ranking_event_ranks(self.version, data["userId"]) + if user_event_ranking_list is None: + return {} - evt_ranking = [] - # for evt in user_event_ranking_list: - # tmp = evt._asdict() - # tmp.pop("id") - # tmp.pop("user") - # evt_ranking.append(tmp) + # We collect the whole ranking table, and clear out any not needed data, this way we preserve the proper ranking + # In official spec this should be done server side, in maintenance period + prep_event_ranking = [] + for evt in user_event_ranking_list: + tmp = evt._asdict() + if tmp["user"] != data["userId"]: + tmp.clear() + else: + tmp.pop("id") + tmp.pop("user") + prep_event_ranking.append(tmp) return { "userId": data["userId"], - "length": len(evt_ranking), - "userEventRankingList": evt_ranking, + "length": len(prep_event_ranking), + "userEventRankingList": prep_event_ranking, } def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: @@ -777,7 +837,7 @@ class OngekiBase: } def handle_get_user_mission_point_api_request(self, data: Dict) -> Dict: - user_mission_point_list = self.data.item.get_mission_points(data["userId"]) + user_mission_point_list = self.data.item.get_mission_points(self.version, data["userId"]) if user_mission_point_list is None: return {} @@ -786,8 +846,10 @@ class OngekiBase: tmp = evt_music._asdict() tmp.pop("id") tmp.pop("user") + tmp.pop("version") mission_point_list.append(tmp) + return { "userId": data["userId"], "length": len(mission_point_list), @@ -804,6 +866,10 @@ class OngekiBase: tmp = evt_music._asdict() tmp.pop("id") tmp.pop("user") + # pop other stuff event_point doesn't want + tmp.pop("rank") + tmp.pop("type") + tmp.pop("date") event_point_list.append(tmp) return { @@ -951,11 +1017,11 @@ class OngekiBase: if "userEventPointList" in upsert: for x in upsert["userEventPointList"]: - self.data.item.put_event_point(user_id, x) + self.data.item.put_event_point(user_id, self.version, x) if "userMissionPointList" in upsert: for x in upsert["userMissionPointList"]: - self.data.item.put_mission_point(user_id, x) + self.data.item.put_mission_point(user_id, self.version, x) if "userRatinglogList" in upsert: for x in upsert["userRatinglogList"]: @@ -985,7 +1051,10 @@ class OngekiBase: if "userTechEventList" in upsert: for x in upsert["userTechEventList"]: - self.data.item.put_tech_event(user_id, x) + self.data.item.put_tech_event(user_id, self.version, x) + + # This should be updated once a day in maintenance window, but for time being we will push the update on each upsert + self.data.item.put_tech_event_ranking(user_id, self.version, x) if "userKopList" in upsert: for x in upsert["userKopList"]: diff --git a/titles/ongeki/config.py b/titles/ongeki/config.py index 2321af6..b952b2d 100644 --- a/titles/ongeki/config.py +++ b/titles/ongeki/config.py @@ -21,6 +21,12 @@ class OngekiServerConfig: self.__config, "ongeki", "server", "loglevel", default="info" ) ) + + @property + def use_https(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "ongeki", "server", "use_https", default=False + ) class OngekiGachaConfig: diff --git a/titles/ongeki/const.py b/titles/ongeki/const.py index 8d4c4ab..a7658f7 100644 --- a/titles/ongeki/const.py +++ b/titles/ongeki/const.py @@ -19,7 +19,6 @@ class OngekiConstants: EVT_TYPES: Enum = Enum( "EVT_TYPES", [ - "None", "Announcement", "Movie", "AddMyList", @@ -39,6 +38,36 @@ class OngekiConstants: "TechChallengeEvent", "AcceptTechChallengeEvent", "SilverJewelEvent", + "Max", + "None", + ], + ) + + REWARD_TYPES: Enum = Enum( + "REWARD_TYPES", + [ + "Card", + "NamePlate", + "Trophy", + "LimitBreakItem", + "AlmightyJewel", + "Money", + "Music", + "ProfileVoice", + "Present", + "ChapterJewel", + "GachaTicket", + "KaikaItem", + "ExpUpItem", + "IntimateUpItem", + "BookItem", + "SystemVoice", + "Costume", + "Medal", + "Attachment", + "UnlockItem", + "Max", + "None", ], ) diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index 2e928f6..baf1ee3 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -127,15 +127,19 @@ class OngekiServlet(BaseServlet): ) def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: - if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80: - return ( - f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", - f"{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/", - ) + title_port_int = Utils.get_title_port(self.core_cfg) + title_port_ssl_int = Utils.get_title_port_ssl(self.core_cfg) + proto = "https" if self.game_cfg.server.use_https and game_ver >= 120 else "http" + + if proto == "https": + t_port = f":{title_port_ssl_int}" if title_port_ssl_int and not self.core_cfg.server.is_using_proxy else "" + + else: + t_port = f":{title_port_int}" if title_port_int and not self.core_cfg.server.is_using_proxy else "" return ( - f"http://{self.core_cfg.title.hostname}/{game_code}/{game_ver}/", - f"{self.core_cfg.title.hostname}/", + f"{proto}://{self.core_cfg.title.hostname}{t_port}/{game_code}/{game_ver}/", + f"{self.core_cfg.title.hostname}{t_port}/", ) def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: @@ -204,6 +208,7 @@ class OngekiServlet(BaseServlet): if ( not encrtped and self.game_cfg.crypto.encrypted_only + and version >= 120 ): self.logger.error( f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}" @@ -247,7 +252,7 @@ class OngekiServlet(BaseServlet): zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) - if not encrtped: + if not encrtped or version < 120: return zipped padded = pad(zipped, 16) diff --git a/titles/ongeki/read.py b/titles/ongeki/read.py index b64194f..6e94094 100644 --- a/titles/ongeki/read.py +++ b/titles/ongeki/read.py @@ -44,6 +44,7 @@ class OngekiReader(BaseReader): self.read_events(f"{dir}/event") self.read_music(f"{dir}/music") self.read_card(f"{dir}/card") + self.read_reward(f"{dir}/reward") def read_card(self, base_dir: str) -> None: self.logger.info(f"Reading cards from {base_dir}...") @@ -171,3 +172,28 @@ class OngekiReader(BaseReader): self.version, song_id, chart_id, title, artist, genre, level ) self.logger.info(f"Added song {song_id} chart {chart_id}") + + def read_reward(self, base_dir: str) -> None: + self.logger.info(f"Reading rewards from {base_dir}...") + + for root, dirs, files in os.walk(base_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Reward.xml"): + strdata = "" + + with open(f"{root}/{dir}/Reward.xml", "r", encoding="utf-8") as f: + strdata = f.read() + + troot = ET.fromstring(strdata) + + if root is None: + continue + + name = troot.find("Name") + rewardId = name.find("id").text + rewardname = name.find("str").text + itemKind = OngekiConstants.REWARD_TYPES[troot.find("ItemType").text].value + itemId = troot.find("RewardItem").find("ItemName").find("id").text + + self.data.static.put_reward(self.version, rewardId, rewardname, itemKind, itemId) + self.logger.info(f"Added reward {rewardId}") diff --git a/titles/ongeki/schema/item.py b/titles/ongeki/schema/item.py index dcaf286..1214dac 100644 --- a/titles/ongeki/schema/item.py +++ b/titles/ongeki/schema/item.py @@ -1,3 +1,4 @@ +from datetime import date, datetime, timedelta from typing import Dict, Optional, List from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON @@ -170,8 +171,12 @@ event_point = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), - Column("eventId", Integer), - Column("point", Integer), + Column("version", Integer, nullable=False), + Column("eventId", Integer, nullable=False), + Column("point", Integer, nullable=False), + Column("rank", Integer), + Column("type", Integer, nullable=False), + Column("date", String(25)), Column("isRankingRewarded", Boolean), UniqueConstraint("user", "eventId", name="ongeki_user_event_point_uk"), mysql_charset="utf8mb4", @@ -182,6 +187,7 @@ mission_point = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("version", Integer), Column("eventId", Integer), Column("point", Integer), UniqueConstraint("user", "eventId", name="ongeki_user_mission_point_uk"), @@ -237,9 +243,10 @@ tech_event = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), - Column("eventId", Integer), - Column("totalTechScore", Integer), - Column("totalPlatinumScore", Integer), + Column("version", Integer, nullable=False), + Column("eventId", Integer, nullable=False), + Column("totalTechScore", Integer, nullable=False), + Column("totalPlatinumScore", Integer, nullable=False), Column("techRecordDate", String(25)), Column("isRankingRewarded", Boolean), Column("isTotalTechNewRecord", Boolean), @@ -247,6 +254,21 @@ tech_event = Table( mysql_charset="utf8mb4", ) +tech_ranking = Table( + "ongeki_tech_event_ranking", + 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("date", String(25)), + Column("eventId", Integer, nullable=False), + Column("rank", Integer), + Column("totalPlatinumScore", Integer, nullable=False), + Column("totalTechScore", Integer, nullable=False), + UniqueConstraint("user", "eventId", name="ongeki_tech_event_ranking_uk"), + mysql_charset="utf8mb4", +) + gacha = Table( "ongeki_user_gacha", metadata, @@ -316,7 +338,6 @@ print_detail = Table( mysql_charset="utf8mb4", ) - class OngekiItemData(BaseData): def put_card(self, aime_id: int, card_data: Dict) -> Optional[int]: card_data["user"] = aime_id @@ -511,9 +532,8 @@ class OngekiItemData(BaseData): return None return result.fetchall() - def put_mission_point( - self, aime_id: int, mission_point_data: Dict - ) -> Optional[int]: + def put_mission_point(self, aime_id: int, version: int, mission_point_data: Dict) -> Optional[int]: + mission_point_data["version"] = version mission_point_data["user"] = aime_id sql = insert(mission_point).values(**mission_point_data) @@ -525,16 +545,22 @@ class OngekiItemData(BaseData): return None return result.lastrowid - def get_mission_points(self, aime_id: int) -> Optional[List[Dict]]: - sql = select(mission_point).where(mission_point.c.user == aime_id) + def get_mission_points(self, version: int, aime_id: int) -> Optional[List[Dict]]: + sql = select(mission_point).where(and_(mission_point.c.user == aime_id, mission_point.c.version == version)) result = self.execute(sql) if result is None: return None return result.fetchall() - def put_event_point(self, aime_id: int, event_point_data: Dict) -> Optional[int]: + def put_event_point(self, aime_id: int, version: int, event_point_data: Dict) -> Optional[int]: + # We update only the newest (type: 1) entry, in official spec game watches for both latest(type:1) and previous (type:2) entries to give an additional info how many ranks has player moved up or down + # This fully featured is on TODO list, at the moment we just update the tables as data comes and give out rank as request comes event_point_data["user"] = aime_id + event_point_data["version"] = version + event_point_data["type"] = 1 + event_point_time = datetime.now() + event_point_data["date"] = datetime.strftime(event_point_time, "%Y-%m-%d %H:%M") sql = insert(event_point).values(**event_point_data) conflict = sql.on_duplicate_key_update(**event_point_data) @@ -625,8 +651,25 @@ class OngekiItemData(BaseData): return None return result.lastrowid - def get_tech_event(self, aime_id: int) -> Optional[List[Dict]]: - sql = select(tech_event).where(tech_event.c.user == aime_id) + def put_tech_event_ranking(self, version: int, aime_id: int, tech_event_data: Dict) -> Optional[int]: + tech_event_data["user"] = aime_id + tech_event_data["version"] = version + tech_event_data.pop("isRankingRewarded") + tech_event_data.pop("isTotalTechNewRecord") + tech_event_data["date"] = tech_event_data.pop("techRecordDate") + tech_event_data["rank"] = 0 + + sql = insert(tech_ranking).values(**tech_event_data) + conflict = sql.on_duplicate_key_update(**tech_event_data) + result = self.execute(conflict) + + if result is None: + self.logger.warning(f"put_tech_event_ranking: Failed to update ranking! aime_id {aime_id}") + return None + return result.lastrowid + + def get_tech_event(self, version: int, aime_id: int) -> Optional[List[Dict]]: + sql = select(tech_event).where(and_(tech_event.c.user == aime_id, tech_event.c.version == version)) result = self.execute(sql) if result is None: @@ -714,3 +757,21 @@ class OngekiItemData(BaseData): ) return None return result.lastrowid + + + def get_ranking_event_ranks(self, version: int, aime_id: int) -> Optional[List[Dict]]: + # Calculates player rank on GameRequest from server, and sends it back, official spec would rank players in maintenance period, on TODO list + sql = select(event_point.c.id, event_point.c.user, event_point.c.eventId, event_point.c.type, func.row_number().over(partition_by=event_point.c.eventId, order_by=event_point.c.point.desc()).label('rank'), event_point.c.date, event_point.c.point).where(event_point.c.version == version) + result = self.execute(sql) + if result is None: + self.logger.error(f"failed to rank aime_id: {aime_id} ranking event positions") + return None + return result.fetchall() + + def get_tech_event_ranking(self, version: int, aime_id: int) -> Optional[List[Dict]]: + sql = select(tech_ranking.c.id, tech_ranking.c.user, tech_ranking.c.date, tech_ranking.c.eventId, func.row_number().over(partition_by=tech_ranking.c.eventId, order_by=[tech_ranking.c.totalTechScore.desc(),tech_ranking.c.totalPlatinumScore.desc()]).label('rank'), tech_ranking.c.totalTechScore, tech_ranking.c.totalPlatinumScore).where(tech_ranking.c.version == version) + result = self.execute(sql) + if result is None: + self.logger.warning(f"aime_id: {aime_id} has no tech ranking ranks") + return None + return result.fetchall() diff --git a/titles/ongeki/schema/static.py b/titles/ongeki/schema/static.py index 829b6be..0a5d176 100644 --- a/titles/ongeki/schema/static.py +++ b/titles/ongeki/schema/static.py @@ -17,6 +17,7 @@ events = Table( Column("type", Integer), Column("name", String(255)), Column("startDate", TIMESTAMP, server_default=func.now()), + Column("endDate", TIMESTAMP, server_default=func.now()), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "eventId", "type", name="ongeki_static_events_uk"), mysql_charset="utf8mb4", @@ -96,6 +97,94 @@ cards = Table( mysql_charset="utf8mb4", ) +music_ranking = Table( + "ongeki_static_music_ranking_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("musicId", Integer, nullable=False), + Column("point", Integer, nullable=False), + Column("userName", String(255)), + UniqueConstraint("version", "musicId", name="ongeki_static_music_ranking_uk"), + mysql_charset="utf8mb4", +) + +rewards = Table( + "ongeki_static_rewards", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("rewardId", Integer, nullable=False), + Column("rewardname", String(255), nullable=False), + Column("itemKind", Integer, nullable=False), + Column("itemId", Integer, nullable=False), + UniqueConstraint("version", "rewardId", name="ongeki_static_rewards_uk"), + mysql_charset="utf8mb4", +) + +present = Table( + "ongeki_static_present_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("presentId", Integer, nullable=False), + Column("presentName", String(255), nullable=False), + Column("rewardId", Integer, nullable=False), + Column("stock", Integer, nullable=False), + Column("message", String(255)), + Column("startDate", String(25), nullable=False), + Column("endDate", String(25), nullable=False), + UniqueConstraint("version", "presentId", name="ongeki_static_present_list_uk"), + mysql_charset="utf8mb4", +) + +tech_music = Table( + "ongeki_static_tech_music", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer, nullable=False), + Column("eventId", Integer, nullable=False), + Column("musicId", Integer, nullable=False), + Column("level", Integer, nullable=False), + UniqueConstraint("version", "musicId", name="ongeki_static_tech_music_uk"), + mysql_charset="utf8mb4", +) + +client_testmode = Table( + "ongeki_static_client_testmode", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("regionId", Integer, nullable=False), + Column("placeId", Integer, nullable=False), + Column("clientId", String(11), nullable=False), + Column("updateDate", TIMESTAMP, nullable=False), + Column("isDelivery", Boolean, nullable=False), + Column("groupId", Integer, nullable=False), + Column("groupRole", Integer, nullable=False), + Column("continueMode", Integer, nullable=False), + Column("selectMusicTime", Integer, nullable=False), + Column("advertiseVolume", Integer, nullable=False), + Column("eventMode", Integer, nullable=False), + Column("eventMusicNum", Integer, nullable=False), + Column("patternGp", Integer, nullable=False), + Column("limitGp", Integer, nullable=False), + Column("maxLeverMovable", Integer, nullable=False), + Column("minLeverMovable", Integer, nullable=False), + UniqueConstraint("clientId", name="ongeki_static_client_testmode_uk"), + mysql_charset="utf8mb4", +) + +game_point = Table( + "ongeki_static_game_point", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("type", Integer, nullable=False), + Column("cost", Integer, nullable=False), + Column("startDate", String(25), nullable=False, server_default="2000-01-01 05:00:00.0"), + Column("endDate", String(25), nullable=False, server_default="2099-01-01 05:00:00.0"), + UniqueConstraint("type", name="ongeki_static_game_point_uk"), + mysql_charset="utf8mb4", +) class OngekiStaticData(BaseData): def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]: @@ -235,6 +324,7 @@ class OngekiStaticData(BaseData): eventId=event_id, type=event_type, name=event_name, + endDate=f"2038-01-01 00:00:00", ) conflict = sql.on_duplicate_key_update( @@ -333,3 +423,90 @@ class OngekiStaticData(BaseData): if result is None: return None return result.fetchone() + + def get_ranking_list(self, version: int) -> Optional[List[Dict]]: + sql = select(music_ranking.c.musicId.label('id'), music_ranking.c.point, music_ranking.c.userName).where(music_ranking.c.version == version) + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def put_reward(self, version: int, rewardId: int, rewardname: str, itemKind: int, itemId: int) -> Optional[int]: + sql = insert(rewards).values( + version=version, + rewardId=rewardId, + rewardname=rewardname, + itemKind=itemKind, + itemId=itemId, + ) + conflict = sql.on_duplicate_key_update( + rewardname=rewardname, + ) + result = self.execute(conflict) + if result is None: + self.logger.warning(f"Failed to insert reward! reward_id: {rewardId}") + return None + return result.lastrowid + + def get_reward_list(self, version: int) -> Optional[List[Dict]]: + sql = select(rewards).where(rewards.c.version == version) + + result = self.execute(sql) + if result is None: + self.logger.warning(f"Failed to load reward list") + return None + return result.fetchall() + + def get_present_list(self, version: int) -> Optional[List[Dict]]: + sql = select(present).where(present.c.version == version) + + result = self.execute(sql) + if result is None: + self.logger.warning(f"Failed to load present list") + return None + return result.fetchall() + + def get_tech_music(self, version: int) -> Optional[List[Dict]]: + sql = select(tech_music).where(tech_music.c.version == version) + + result = self.execute(sql) + + if result is None: + return None + return result.fetchall() + + def put_client_testmode_data(self, region_id: int, client_testmode_data: Dict) -> Optional[List[Dict]]: + sql = insert(client_testmode).values(regionId=region_id, **client_testmode_data) + conflict = sql.on_duplicate_key_update(regionId=region_id, **client_testmode_data) + + result = self.execute(conflict) + if result is None: + self.logger.warning(f"clientId: {clientId} Failed to update ClientTestMode data"), + return None + return result.lastrowid + + def put_client_setting_data(self, client_id: str, client_setting_data: Dict) -> Optional[List[Dict]]: + sql = insert(machine).values(data=client_setting_data) + conflict = sql.on_duplicate_key_update(serial=client_id) + + result = self.execute(conflict) + if result is None: + self.logger.warning(f"clientId: {clientId} Failed to update ClientSetting data"), + return None + return result.lastrowid + + def put_static_game_point_defaults(self) -> Optional[List[Dict]]: + game_point_defaults = [{"type": 0, "cost": 100},{"type": 1, "cost": 230},{"type": 2, "cost": 370},{"type": 3, "cost": 120},{"type": 4, "cost": 240},{"type": 5, "cost": 360}] + sql = insert(game_point).values(game_point_defaults) + result = self.execute(sql) + if result is None: + self.logger.warning(f"Failed to insert default GP table!") + return None + return result.lastrowid + + def get_static_game_point(self) -> Optional[List[Dict]]: + sql = select(game_point.c.type, game_point.c.cost, game_point.c.startDate, game_point.c.endDate) + result = self.execute(sql) + if result is None: + return None + return result.fetchall() diff --git a/titles/sao/base.py b/titles/sao/base.py index 3c4aade..09a9f88 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -1,11 +1,8 @@ -from datetime import datetime, timedelta -import json, logging -from typing import Any, Dict -import random -import struct +import logging from csv import * from random import choice -import random as rand +from typing import Dict, List +from os import path from core.data import Data from core import CoreConfig @@ -22,71 +19,64 @@ class SaoBase: self.version = 0 self.logger = logging.getLogger("sao") - def handle_noop(self, request: Any) -> bytes: - sao_request = request + def load_data_csv(self, file: str) -> List[Dict]: + ret = [] + if path.exists(f"titles/sao/data/{file}.csv"): + with open(f"titles/sao/data/{file}.csv", "r", encoding="utf8") as f: + data = csv.DictReader(f, delimiter=',') + for x in data: + ret.append(x) + + return ret + + self.logger.warning(f"Failed to find csv file {file}.csv") + return ret - sao_id = int(sao_request[:4],16) + 1 + def handle_noop(self, header: SaoRequestHeader, request: bytes) -> bytes: + self.logger.info(f"Using Generic handler") + resp_thing = SaoNoopResponse(header.cmd + 1) + return resp_thing.make() - ret = struct.pack("!HHIIIIIIb", sao_id, 0, 0, 5, 1, 1, 5, 0x01000000, 0).hex() - return bytes.fromhex(ret) - - def handle_c122(self, request: Any) -> bytes: + def handle_c122(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/get_maintenance_info - resp = SaoGetMaintResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetMaintResponse(header.cmd +1) return resp.make() - def handle_c12e(self, request: Any) -> bytes: + def handle_c12e(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/ac_cabinet_boot_notification - resp = SaoCommonAcCabinetBootNotificationResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoCommonAcCabinetBootNotificationResponse(header.cmd +1) return resp.make() - def handle_c100(self, request: Any) -> bytes: + def handle_c100(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/get_app_versions - resp = SaoCommonGetAppVersionsRequest(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoCommonGetAppVersionsRequest(header.cmd +1) return resp.make() - def handle_c102(self, request: Any) -> bytes: + def handle_c102(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/master_data_version_check - resp = SaoMasterDataVersionCheckResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoMasterDataVersionCheckResponse(header.cmd +1) return resp.make() - def handle_c10a(self, request: Any) -> bytes: + def handle_c10a(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/paying_play_start - resp = SaoCommonPayingPlayStartRequest(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoCommonPayingPlayStartRequest(header.cmd +1) return resp.make() - def handle_ca02(self, request: Any) -> bytes: + def handle_ca02(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest_multi_play_room/get_quest_scene_multi_play_photon_server - resp = SaoGetQuestSceneMultiPlayPhotonServerResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetQuestSceneMultiPlayPhotonServerResponse(header.cmd +1) return resp.make() - def handle_c11e(self, request: Any) -> bytes: + def handle_c11e(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/get_auth_card_data - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(16), - "cabinet_type" / Int8ub, # cabinet_type is a byte - "auth_type" / Int8ub, # auth_type is a byte - "store_id_size" / Rebuild(Int32ub, len_(this.store_id) * 2), # calculates the length of the store_id - "store_id" / PaddedString(this.store_id_size, "utf_16_le"), # store_id is a (zero) padded string - "serial_no_size" / Rebuild(Int32ub, len_(this.serial_no) * 2), # calculates the length of the serial_no - "serial_no" / PaddedString(this.serial_no_size, "utf_16_le"), # serial_no is a (zero) padded string - "access_code_size" / Rebuild(Int32ub, len_(this.access_code) * 2), # calculates the length of the access_code - "access_code" / PaddedString(this.access_code_size, "utf_16_le"), # access_code is a (zero) padded string - "chip_id_size" / Rebuild(Int32ub, len_(this.chip_id) * 2), # calculates the length of the chip_id - "chip_id" / PaddedString(this.chip_id_size, "utf_16_le"), # chip_id is a (zero) padded string - ) - - req_data = req_struct.parse(req) - access_code = req_data.access_code + req = SaoGetAuthCardDataRequest(header, request) #Check authentication - user_id = self.core_data.card.get_user_id_from_card( access_code ) + user_id = self.core_data.card.get_user_id_from_card( req.access_code ) if not user_id: user_id = self.core_data.user.create_user() #works - card_id = self.core_data.card.create_card(user_id, access_code) + card_id = self.core_data.card.create_card(user_id, req.access_code) if card_id is None: user_id = -1 @@ -106,7 +96,7 @@ class SaoBase: # Force the tutorial stage to be completed due to potential crash in-game - self.logger.info(f"User Authenticated: { access_code } | { user_id }") + self.logger.info(f"User Authenticated: { req.access_code } | { user_id }") #Grab values from profile profile_data = self.game_data.profile.get_profile(user_id) @@ -127,270 +117,179 @@ class SaoBase: profile_data = self.game_data.profile.get_profile(user_id) - resp = SaoGetAuthCardDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + resp = SaoGetAuthCardDataResponse(header.cmd +1, profile_data) return resp.make() - def handle_c40c(self, request: Any) -> bytes: + def handle_c40c(self, header: SaoRequestHeader, request: bytes) -> bytes: #home/check_ac_login_bonus - resp = SaoHomeCheckAcLoginBonusResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoHomeCheckAcLoginBonusResponse(header.cmd +1) return resp.make() - def handle_c104(self, request: Any) -> bytes: + def handle_c104(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/login - req = bytes.fromhex(request)[24:] + req = SaoCommonLoginRequest(header, request) - req_struct = Struct( - Padding(16), - "cabinet_type" / Int8ub, # cabinet_type is a byte - "auth_type" / Int8ub, # auth_type is a byte - "store_id_size" / Rebuild(Int32ub, len_(this.store_id) * 2), # calculates the length of the store_id - "store_id" / PaddedString(this.store_id_size, "utf_16_le"), # store_id is a (zero) padded string - "store_name_size" / Rebuild(Int32ub, len_(this.store_name) * 2), # calculates the length of the store_name - "store_name" / PaddedString(this.store_name_size, "utf_16_le"), # store_name is a (zero) padded string - "serial_no_size" / Rebuild(Int32ub, len_(this.serial_no) * 2), # calculates the length of the serial_no - "serial_no" / PaddedString(this.serial_no_size, "utf_16_le"), # serial_no is a (zero) padded string - "access_code_size" / Rebuild(Int32ub, len_(this.access_code) * 2), # calculates the length of the access_code - "access_code" / PaddedString(this.access_code_size, "utf_16_le"), # access_code is a (zero) padded string - "chip_id_size" / Rebuild(Int32ub, len_(this.chip_id) * 2), # calculates the length of the chip_id - "chip_id" / PaddedString(this.chip_id_size, "utf_16_le"), # chip_id is a (zero) padded string - "free_ticket_distribution_target_flag" / Int8ub, # free_ticket_distribution_target_flag is a byte - ) - - req_data = req_struct.parse(req) - access_code = req_data.access_code - - user_id = self.core_data.card.get_user_id_from_card( access_code ) + user_id = self.core_data.card.get_user_id_from_card( req.access_code ) profile_data = self.game_data.profile.get_profile(user_id) - resp = SaoCommonLoginResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + resp = SaoCommonLoginResponse(header.cmd +1, profile_data) return resp.make() - def handle_c404(self, request: Any) -> bytes: + def handle_c404(self, header: SaoRequestHeader, request: bytes) -> bytes: #home/check_comeback_event - resp = SaoCheckComebackEventRequest(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoCheckComebackEventRequest(header.cmd +1) return resp.make() - def handle_c000(self, request: Any) -> bytes: + def handle_c000(self, header: SaoRequestHeader, request: bytes) -> bytes: #ticket/ticket - resp = SaoTicketResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoTicketResponse(header.cmd +1) return resp.make() - def handle_c500(self, request: Any) -> bytes: + def handle_c500(self, header: SaoRequestHeader, request: bytes) -> bytes: #user_info/get_user_basic_data - req = bytes.fromhex(request)[24:] + req = SaoGetUserBasicDataRequest(header, request) - req_struct = Struct( - Padding(16), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - ) + profile_data = self.game_data.profile.get_profile(req.user_id) - req_data = req_struct.parse(req) - user_id = req_data.user_id - - profile_data = self.game_data.profile.get_profile(user_id) - - resp = SaoGetUserBasicDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + resp = SaoGetUserBasicDataResponse(header.cmd +1, profile_data) return resp.make() - def handle_c600(self, request: Any) -> bytes: + def handle_c600(self, header: SaoRequestHeader, request: bytes) -> bytes: #have_object/get_hero_log_user_data_list - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(16), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + req = SaoGetHeroLogUserDataListRequest(header, request) - ) - req_data = req_struct.parse(req) - user_id = req_data.user_id - - hero_data = self.game_data.item.get_hero_logs(user_id) + hero_data = self.game_data.item.get_hero_logs(req.user_id) - resp = SaoGetHeroLogUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, hero_data) + resp = SaoGetHeroLogUserDataListResponse(header.cmd +1, hero_data) return resp.make() - def handle_c602(self, request: Any) -> bytes: + def handle_c602(self, header: SaoRequestHeader, request: bytes) -> bytes: #have_object/get_equipment_user_data_list - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(16), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - - ) - req_data = req_struct.parse(req) - user_id = req_data.user_id + req = SaoGetEquipmentUserDataListRequest(header, request) - equipment_data = self.game_data.item.get_user_equipments(user_id) + equipment_data = self.game_data.item.get_user_equipments(req.user_id) - resp = SaoGetEquipmentUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, equipment_data) + resp = SaoGetEquipmentUserDataListResponse(header.cmd +1, equipment_data) return resp.make() - def handle_c604(self, request: Any) -> bytes: + def handle_c604(self, header: SaoRequestHeader, request: bytes) -> bytes: #have_object/get_item_user_data_list - req = bytes.fromhex(request)[24:] + req = SaoGetItemUserDataListRequest(header, request) - req_struct = Struct( - Padding(16), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + item_data = self.game_data.item.get_user_items(req.user_id) - ) - req_data = req_struct.parse(req) - user_id = req_data.user_id - - item_data = self.game_data.item.get_user_items(user_id) - - resp = SaoGetItemUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, item_data) + resp = SaoGetItemUserDataListResponse(header.cmd +1, item_data) return resp.make() - def handle_c606(self, request: Any) -> bytes: + def handle_c606(self, header: SaoRequestHeader, request: bytes) -> bytes: #have_object/get_support_log_user_data_list supportIdsData = self.game_data.static.get_support_log_ids(0, True) - resp = SaoGetSupportLogUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, supportIdsData) + resp = SaoGetSupportLogUserDataListResponse(header.cmd +1, supportIdsData) return resp.make() - def handle_c800(self, request: Any) -> bytes: + def handle_c800(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/get_title_user_data_list titleIdsData = self.game_data.static.get_title_ids(0, True) - resp = SaoGetTitleUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, titleIdsData) + resp = SaoGetTitleUserDataListResponse(header.cmd +1, titleIdsData) return resp.make() - def handle_c608(self, request: Any) -> bytes: + def handle_c608(self, header: SaoRequestHeader, request: bytes) -> bytes: #have_object/get_episode_append_data_list - req = bytes.fromhex(request)[24:] + req = SaoGetEpisodeAppendDataListRequest(header, request) - req_struct = Struct( - Padding(16), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - ) + profile_data = self.game_data.profile.get_profile(req.user_id) - req_data = req_struct.parse(req) - user_id = req_data.user_id - - profile_data = self.game_data.profile.get_profile(user_id) - - resp = SaoGetEpisodeAppendDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + resp = SaoGetEpisodeAppendDataListResponse(header.cmd +1, profile_data) return resp.make() - def handle_c804(self, request: Any) -> bytes: + def handle_c804(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/get_party_data_list - req = bytes.fromhex(request)[24:] + req = SaoGetPartyDataListRequest(header, request) - req_struct = Struct( - Padding(16), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + hero_party = self.game_data.item.get_hero_party(req.user_id, 0) + hero1_data = self.game_data.item.get_hero_log(req.user_id, hero_party[3]) + hero2_data = self.game_data.item.get_hero_log(req.user_id, hero_party[4]) + hero3_data = self.game_data.item.get_hero_log(req.user_id, hero_party[5]) - ) - req_data = req_struct.parse(req) - user_id = req_data.user_id - - hero_party = self.game_data.item.get_hero_party(user_id, 0) - hero1_data = self.game_data.item.get_hero_log(user_id, hero_party[3]) - hero2_data = self.game_data.item.get_hero_log(user_id, hero_party[4]) - hero3_data = self.game_data.item.get_hero_log(user_id, hero_party[5]) - - resp = SaoGetPartyDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, hero1_data, hero2_data, hero3_data) + resp = SaoGetPartyDataListResponse(header.cmd +1, hero1_data, hero2_data, hero3_data) return resp.make() - def handle_c902(self, request: Any) -> bytes: # for whatever reason, having all entries empty or filled changes nothing + def handle_c902(self, header: SaoRequestHeader, request: bytes) -> bytes: # for whatever reason, having all entries empty or filled changes nothing #quest/get_quest_scene_prev_scan_profile_card - resp = SaoGetQuestScenePrevScanProfileCardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetQuestScenePrevScanProfileCardResponse(header.cmd +1) return resp.make() - def handle_c124(self, request: Any) -> bytes: + def handle_c124(self, header: SaoRequestHeader, request: bytes) -> bytes: #common/get_resource_path_info - resp = SaoGetResourcePathInfoResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetResourcePathInfoResponse(header.cmd +1) return resp.make() - def handle_c900(self, request: Any) -> bytes: + def handle_c900(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/get_quest_scene_user_data_list // QuestScene.csv - req = bytes.fromhex(request)[24:] + req = SaoGetQuestSceneUserDataListRequest(header, request) - req_struct = Struct( - Padding(16), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + quest_data = self.game_data.item.get_quest_logs(req.user_id) - ) - req_data = req_struct.parse(req) - user_id = req_data.user_id - - quest_data = self.game_data.item.get_quest_logs(user_id) - - resp = SaoGetQuestSceneUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, quest_data) + resp = SaoGetQuestSceneUserDataListResponse(header.cmd +1, quest_data) return resp.make() - def handle_c400(self, request: Any) -> bytes: + def handle_c400(self, header: SaoRequestHeader, request: bytes) -> bytes: #home/check_yui_medal_get_condition - resp = SaoCheckYuiMedalGetConditionResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoCheckYuiMedalGetConditionResponse(header.cmd +1) return resp.make() - def handle_c402(self, request: Any) -> bytes: + def handle_c402(self, header: SaoRequestHeader, request: bytes) -> bytes: #home/get_yui_medal_bonus_user_data - resp = SaoGetYuiMedalBonusUserDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetYuiMedalBonusUserDataResponse(header.cmd +1) return resp.make() - def handle_c40a(self, request: Any) -> bytes: + def handle_c40a(self, header: SaoRequestHeader, request: bytes) -> bytes: #home/check_profile_card_used_reward - resp = SaoCheckProfileCardUsedRewardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoCheckProfileCardUsedRewardResponse(header.cmd +1) return resp.make() - def handle_c814(self, request: Any) -> bytes: + def handle_c814(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/synthesize_enhancement_hero_log - req = bytes.fromhex(request)[24:] + req = SaoSynthesizeEnhancementHeroLogRequest(header, request) - req_struct = Struct( - Padding(20), - "ticket_id" / Bytes(1), # needs to be parsed as an int - Padding(1), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - "origin_user_hero_log_id_size" / Rebuild(Int32ub, len_(this.origin_user_hero_log_id) * 2), # calculates the length of the origin_user_hero_log_id - "origin_user_hero_log_id" / PaddedString(this.origin_user_hero_log_id_size, "utf_16_le"), # origin_user_hero_log_id is a (zero) padded string - Padding(3), - "material_common_reward_user_data_list_length" / Rebuild(Int8ub, len_(this.material_common_reward_user_data_list)), # material_common_reward_user_data_list is a byte, - "material_common_reward_user_data_list" / Array(this.material_common_reward_user_data_list_length, Struct( - "common_reward_type" / Int16ub, # team_no is a byte - "user_common_reward_id_size" / Rebuild(Int32ub, len_(this.user_common_reward_id) * 2), # calculates the length of the user_common_reward_id - "user_common_reward_id" / PaddedString(this.user_common_reward_id_size, "utf_16_le"), # user_common_reward_id is a (zero) padded string - )), - ) + synthesize_hero_log_data = self.game_data.item.get_hero_log(req.user_id, req.origin_user_hero_log_id) - req_data = req_struct.parse(req) - user_id = req_data.user_id - synthesize_hero_log_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.origin_user_hero_log_id) - - for i in range(0,req_data.material_common_reward_user_data_list_length): - - itemList = self.game_data.static.get_item_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) - heroList = self.game_data.static.get_hero_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) - equipmentList = self.game_data.static.get_equipment_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) + for x in req.material_common_reward_user_data_list: + hero_exp = 0 + itemList = self.game_data.static.get_item_id(x.user_common_reward_id) + heroList = self.game_data.static.get_hero_id(x.user_common_reward_id) + equipmentList = self.game_data.static.get_equipment_id(x.user_common_reward_id) if itemList: hero_exp = 2000 + int(synthesize_hero_log_data["log_exp"]) - self.game_data.item.remove_item(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + self.game_data.item.remove_item(req.user_id, x.user_common_reward_id) if equipmentList: - equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + equipment_data = self.game_data.item.get_user_equipment(req.user_id, x.user_common_reward_id) + if equipment_data is None: + self.logger.error(f"Failed to find equipment {x.user_common_reward_id} for user {req.user_id}!") + continue + hero_exp = int(equipment_data["enhancement_exp"]) + int(synthesize_hero_log_data["log_exp"]) - self.game_data.item.remove_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + self.game_data.item.remove_equipment(req.user_id, x.user_common_reward_id) if heroList: - hero_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + hero_data = self.game_data.item.get_hero_log(req.user_id, x.user_common_reward_id) + if hero_data is None: + self.logger.error(f"Failed to find hero {x.user_common_reward_id} for user {req.user_id}!") + continue + hero_exp = int(hero_data["log_exp"]) + int(synthesize_hero_log_data["log_exp"]) - self.game_data.item.remove_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + self.game_data.item.remove_hero_log(req.user_id, x.user_common_reward_id) + + if hero_exp == 0: + self.logger.warn(f"Hero {x.user_common_reward_id} (type {x.common_reward_type}) not found!") self.game_data.item.put_hero_log( - user_id, - int(req_data.origin_user_hero_log_id), + req.user_id, + int(req.origin_user_hero_log_id), synthesize_hero_log_data["log_level"], hero_exp, synthesize_hero_log_data["main_weapon"], @@ -402,13 +301,13 @@ class SaoBase: synthesize_hero_log_data["skill_slot5_skill_id"] ) - profile = self.game_data.profile.get_profile(req_data.user_id) + profile = self.game_data.profile.get_profile(req.user_id) new_col = int(profile["own_col"]) - 100 # Update profile self.game_data.profile.put_profile( - req_data.user_id, + req.user_id, profile["user_type"], profile["nick_name"], profile["rank_num"], @@ -420,57 +319,48 @@ class SaoBase: ) # Load the item again to push to the response handler - synthesize_hero_log_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.origin_user_hero_log_id) + synthesize_hero_log_data = self.game_data.item.get_hero_log(req.user_id, req.origin_user_hero_log_id) - resp = SaoSynthesizeEnhancementHeroLogResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, synthesize_hero_log_data) + resp = SaoSynthesizeEnhancementHeroLogResponse(header.cmd +1, synthesize_hero_log_data) return resp.make() - def handle_c816(self, request: Any) -> bytes: + def handle_c816(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/synthesize_enhancement_equipment - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(20), - "ticket_id" / Bytes(1), # needs to be parsed as an int - Padding(1), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - "origin_user_equipment_id_size" / Rebuild(Int32ub, len_(this.origin_user_equipment_id) * 2), # calculates the length of the origin_user_equipment_id - "origin_user_equipment_id" / PaddedString(this.origin_user_equipment_id_size, "utf_16_le"), # origin_user_equipment_id is a (zero) padded string - Padding(3), - "material_common_reward_user_data_list_length" / Rebuild(Int8ub, len_(this.material_common_reward_user_data_list)), # material_common_reward_user_data_list is a byte, - "material_common_reward_user_data_list" / Array(this.material_common_reward_user_data_list_length, Struct( - "common_reward_type" / Int16ub, # team_no is a byte - "user_common_reward_id_size" / Rebuild(Int32ub, len_(this.user_common_reward_id) * 2), # calculates the length of the user_common_reward_id - "user_common_reward_id" / PaddedString(this.user_common_reward_id_size, "utf_16_le"), # user_common_reward_id is a (zero) padded string - )), - ) - - req_data = req_struct.parse(req) - - user_id = req_data.user_id + req_data = SaoSynthesizeEnhancementEquipmentRequest(header, request) synthesize_equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.origin_user_equipment_id) - for i in range(0,req_data.material_common_reward_user_data_list_length): - - itemList = self.game_data.static.get_item_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) - heroList = self.game_data.static.get_hero_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) - equipmentList = self.game_data.static.get_equipment_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) + for x in req_data.material_common_reward_user_data_list: + equipment_exp = 0 + itemList = self.game_data.static.get_item_id(x.user_common_reward_id) + heroList = self.game_data.static.get_hero_id(x.user_common_reward_id) + equipmentList = self.game_data.static.get_equipment_id(x.user_common_reward_id) if itemList: equipment_exp = 2000 + int(synthesize_equipment_data["enhancement_exp"]) - self.game_data.item.remove_item(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + self.game_data.item.remove_item(req_data.user_id, x.user_common_reward_id) if equipmentList: - equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, x.user_common_reward_id) + if equipment_data is None: + self.logger.error(f"Failed to find equipment {x.user_common_reward_id} for user {req_data.user_id}!") + continue + equipment_exp = int(equipment_data["enhancement_exp"]) + int(synthesize_equipment_data["enhancement_exp"]) - self.game_data.item.remove_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + self.game_data.item.remove_equipment(req_data.user_id, x.user_common_reward_id) if heroList: - hero_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) - equipment_exp = int(hero_data["log_exp"]) + int(synthesize_equipment_data["enhancement_exp"]) - self.game_data.item.remove_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + hero_data = self.game_data.item.get_hero_log(req_data.user_id, x.user_common_reward_id) + if hero_data is None: + self.logger.error(f"Failed to find hero {x.user_common_reward_id} for user {req_data.user_id}!") + continue + equipment_exp = int(hero_data["log_exp"]) + int(synthesize_equipment_data["enhancement_exp"]) + self.game_data.item.remove_hero_log(req_data.user_id, x.user_common_reward_id) + + if equipment_exp == 0: + self.logger.warn(f"Common reward {x.user_common_reward_id} (type {x.common_reward_type}) not found!") + continue + self.game_data.item.put_equipment_data(req_data.user_id, int(req_data.origin_user_equipment_id), synthesize_equipment_data["enhancement_value"], equipment_exp, 0, 0, 0) profile = self.game_data.profile.get_profile(req_data.user_id) @@ -493,54 +383,16 @@ class SaoBase: # Load the item again to push to the response handler synthesize_equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.origin_user_equipment_id) - resp = SaoSynthesizeEnhancementEquipmentResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, synthesize_equipment_data) + resp = SaoSynthesizeEnhancementEquipmentResponse(header.cmd +1, synthesize_equipment_data) return resp.make() - def handle_c806(self, request: Any) -> bytes: + def handle_c806(self, header: SaoRequestHeader, request: bytes) -> bytes: #custom/change_party - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(20), - "ticket_id" / Bytes(1), # needs to be parsed as an int - Padding(1), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - "act_type" / Int8ub, # play_mode is a byte - Padding(3), - "party_data_list_length" / Rebuild(Int8ub, len_(this.party_data_list)), # party_data_list is a byte, - "party_data_list" / Array(this.party_data_list_length, Struct( - "user_party_id_size" / Rebuild(Int32ub, len_(this.user_party_id) * 2), # calculates the length of the user_party_id - "user_party_id" / PaddedString(this.user_party_id_size, "utf_16_le"), # user_party_id is a (zero) padded string - "team_no" / Int8ub, # team_no is a byte - Padding(3), - "party_team_data_list_length" / Rebuild(Int8ub, len_(this.party_team_data_list)), # party_team_data_list is a byte - "party_team_data_list" / Array(this.party_team_data_list_length, Struct( - "user_party_team_id_size" / Rebuild(Int32ub, len_(this.user_party_team_id) * 2), # calculates the length of the user_party_team_id - "user_party_team_id" / PaddedString(this.user_party_team_id_size, "utf_16_le"), # user_party_team_id is a (zero) padded string - "arrangement_num" / Int8ub, # arrangement_num is a byte - "user_hero_log_id_size" / Rebuild(Int32ub, len_(this.user_hero_log_id) * 2), # calculates the length of the user_hero_log_id - "user_hero_log_id" / PaddedString(this.user_hero_log_id_size, "utf_16_le"), # user_hero_log_id is a (zero) padded string - "main_weapon_user_equipment_id_size" / Rebuild(Int32ub, len_(this.main_weapon_user_equipment_id) * 2), # calculates the length of the main_weapon_user_equipment_id - "main_weapon_user_equipment_id" / PaddedString(this.main_weapon_user_equipment_id_size, "utf_16_le"), # main_weapon_user_equipment_id is a (zero) padded string - "sub_equipment_user_equipment_id_size" / Rebuild(Int32ub, len_(this.sub_equipment_user_equipment_id) * 2), # calculates the length of the sub_equipment_user_equipment_id - "sub_equipment_user_equipment_id" / PaddedString(this.sub_equipment_user_equipment_id_size, "utf_16_le"), # sub_equipment_user_equipment_id is a (zero) padded string - "skill_slot1_skill_id" / Int32ub, # skill_slot1_skill_id is a int, - "skill_slot2_skill_id" / Int32ub, # skill_slot1_skill_id is a int, - "skill_slot3_skill_id" / Int32ub, # skill_slot1_skill_id is a int, - "skill_slot4_skill_id" / Int32ub, # skill_slot1_skill_id is a int, - "skill_slot5_skill_id" / Int32ub, # skill_slot1_skill_id is a int, - )), - )), - - ) - - req_data = req_struct.parse(req) - user_id = req_data.user_id + req_data = SaoChangePartyRequest(header, request) party_hero_list = [] for party_team in req_data.party_data_list[0].party_team_data_list: - hero_data = self.game_data.item.get_hero_log(user_id, party_team["user_hero_log_id"]) + hero_data = self.game_data.item.get_hero_log(req_data.user_id, party_team.user_hero_log_id) hero_level = 1 hero_exp = 0 @@ -549,53 +401,29 @@ class SaoBase: hero_exp = hero_data["log_exp"] self.game_data.item.put_hero_log( - user_id, - party_team["user_hero_log_id"], + req_data.user_id, + party_team.user_hero_log_id, hero_level, hero_exp, - party_team["main_weapon_user_equipment_id"], - party_team["sub_equipment_user_equipment_id"], - party_team["skill_slot1_skill_id"], - party_team["skill_slot2_skill_id"], - party_team["skill_slot3_skill_id"], - party_team["skill_slot4_skill_id"], - party_team["skill_slot5_skill_id"] + party_team.main_weapon_user_equipment_id, + party_team.sub_equipment_user_equipment_id, + party_team.skill_slot1_skill_id, + party_team.skill_slot2_skill_id, + party_team.skill_slot3_skill_id, + party_team.skill_slot4_skill_id, + party_team.skill_slot5_skill_id ) - party_hero_list.append(party_team["user_hero_log_id"]) + party_hero_list.append(party_team.user_hero_log_id) - self.game_data.item.put_hero_party(user_id, req_data.party_data_list[0].party_team_data_list[0].user_party_team_id, party_hero_list[0], party_hero_list[1], party_hero_list[2]) + self.game_data.item.put_hero_party(req_data.user_id, req_data.party_data_list[0].party_team_data_list[0].user_party_team_id, party_hero_list[0], party_hero_list[1], party_hero_list[2]) - resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoNoopResponse(header.cmd +1) return resp.make() - def handle_c904(self, request: Any) -> bytes: + def handle_c904(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/episode_play_start - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(20), - "ticket_id" / Bytes(1), # needs to be parsed as an int - Padding(1), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - "episode_id" / Int32ub, # episode_id is a int, - "play_mode" / Int8ub, # play_mode is a byte - Padding(3), - "play_start_request_data_length" / Rebuild(Int8ub, len_(this.play_start_request_data)), # play_start_request_data_length is a byte, - "play_start_request_data" / Array(this.play_start_request_data_length, Struct( - "user_party_id_size" / Rebuild(Int32ub, len_(this.user_party_id) * 2), # calculates the length of the user_party_id - "user_party_id" / PaddedString(this.user_party_id_size, "utf_16_le"), # user_party_id is a (zero) padded string - "appoint_leader_resource_card_code_size" / Rebuild(Int32ub, len_(this.appoint_leader_resource_card_code) * 2), # calculates the length of the total_damage - "appoint_leader_resource_card_code" / PaddedString(this.appoint_leader_resource_card_code_size, "utf_16_le"), # total_damage is a (zero) padded string - "use_profile_card_code_size" / Rebuild(Int32ub, len_(this.use_profile_card_code) * 2), # calculates the length of the total_damage - "use_profile_card_code" / PaddedString(this.use_profile_card_code_size, "utf_16_le"), # use_profile_card_code is a (zero) padded string - "quest_drop_boost_apply_flag" / Int8ub, # quest_drop_boost_apply_flag is a byte - )), - - ) - - req_data = req_struct.parse(req) + req_data = SaoEpisodePlayStartRequest(header, request) user_id = req_data.user_id profile_data = self.game_data.profile.get_profile(user_id) @@ -608,114 +436,27 @@ class SaoBase: req_data.play_start_request_data[0].quest_drop_boost_apply_flag ) - resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + resp = SaoEpisodePlayStartResponse(header.cmd +1, profile_data) return resp.make() - def handle_c908(self, request: Any) -> bytes: # Level calculation missing for the profile and heroes + def handle_c908(self, header: SaoRequestHeader, request: bytes) -> bytes: # Level calculation missing for the profile and heroes #quest/episode_play_end - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(20), - "ticket_id" / Bytes(1), # needs to be parsed as an int - Padding(1), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - Padding(2), - "episode_id" / Int16ub, # episode_id is a short, - Padding(3), - "play_end_request_data" / Int8ub, # play_end_request_data is a byte - Padding(1), - "play_result_flag" / Int8ub, # play_result_flag is a byte - Padding(2), - "base_get_data_length" / Rebuild(Int8ub, len_(this.base_get_data)), # base_get_data_length is a byte, - "base_get_data" / Array(this.base_get_data_length, Struct( - "get_hero_log_exp" / Int32ub, # get_hero_log_exp is an int - "get_col" / Int32ub, # get_num is a short - )), - Padding(3), - "get_player_trace_data_list_length" / Rebuild(Int8ub, len_(this.get_player_trace_data_list)), # get_player_trace_data_list_length is a byte - "get_player_trace_data_list" / Array(this.get_player_trace_data_list_length, Struct( - "user_quest_scene_player_trace_id" / Int32ub, # user_quest_scene_player_trace_id is an int - )), - Padding(3), - "get_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_rare_drop_data_list)), # get_rare_drop_data_list_length is a byte - "get_rare_drop_data_list" / Array(this.get_rare_drop_data_list_length, Struct( - "quest_rare_drop_id" / Int32ub, # quest_rare_drop_id is an int - )), - Padding(3), - "get_special_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_special_rare_drop_data_list)), # get_special_rare_drop_data_list_length is a byte - "get_special_rare_drop_data_list" / Array(this.get_special_rare_drop_data_list_length, Struct( - "quest_special_rare_drop_id" / Int32ub, # quest_special_rare_drop_id is an int - )), - Padding(3), - "get_unanalyzed_log_tmp_reward_data_list_length" / Rebuild(Int8ub, len_(this.get_unanalyzed_log_tmp_reward_data_list)), # get_unanalyzed_log_tmp_reward_data_list_length is a byte - "get_unanalyzed_log_tmp_reward_data_list" / Array(this.get_unanalyzed_log_tmp_reward_data_list_length, Struct( - "unanalyzed_log_grade_id" / Int32ub, # unanalyzed_log_grade_id is an int, - )), - Padding(3), - "get_event_item_data_list_length" / Rebuild(Int8ub, len_(this.get_event_item_data_list)), # get_event_item_data_list_length is a byte, - "get_event_item_data_list" / Array(this.get_event_item_data_list_length, Struct( - "event_item_id" / Int32ub, # event_item_id is an int - "get_num" / Int16ub, # get_num is a short - )), - Padding(3), - "discovery_enemy_data_list_length" / Rebuild(Int8ub, len_(this.discovery_enemy_data_list)), # discovery_enemy_data_list_length is a byte - "discovery_enemy_data_list" / Array(this.discovery_enemy_data_list_length, Struct( - "enemy_kind_id" / Int32ub, # enemy_kind_id is an int - "destroy_num" / Int16ub, # destroy_num is a short - )), - Padding(3), - "destroy_boss_data_list_length" / Rebuild(Int8ub, len_(this.destroy_boss_data_list)), # destroy_boss_data_list_length is a byte - "destroy_boss_data_list" / Array(this.destroy_boss_data_list_length, Struct( - "boss_type" / Int8ub, # boss_type is a byte - "enemy_kind_id" / Int32ub, # enemy_kind_id is an int - "destroy_num" / Int16ub, # destroy_num is a short - )), - Padding(3), - "mission_data_list_length" / Rebuild(Int8ub, len_(this.mission_data_list)), # mission_data_list_length is a byte - "mission_data_list" / Array(this.mission_data_list_length, Struct( - "mission_id" / Int32ub, # enemy_kind_id is an int - "clear_flag" / Int8ub, # boss_type is a byte - "mission_difficulty_id" / Int16ub, # destroy_num is a short - )), - Padding(3), - "score_data_length" / Rebuild(Int8ub, len_(this.score_data)), # score_data_length is a byte - "score_data" / Array(this.score_data_length, Struct( - "clear_time" / Int32ub, # clear_time is an int - "combo_num" / Int32ub, # boss_type is a int - "total_damage_size" / Rebuild(Int32ub, len_(this.total_damage) * 2), # calculates the length of the total_damage - "total_damage" / PaddedString(this.total_damage_size, "utf_16_le"), # total_damage is a (zero) padded string - "concurrent_destroying_num" / Int16ub, # concurrent_destroying_num is a short - "reaching_skill_level" / Int16ub, # reaching_skill_level is a short - "ko_chara_num" / Int8ub, # ko_chara_num is a byte - "acceleration_invocation_num" / Int16ub, # acceleration_invocation_num is a short - "boss_destroying_num" / Int16ub, # boss_destroying_num is a short - "synchro_skill_used_flag" / Int8ub, # synchro_skill_used_flag is a byte - "used_friend_skill_id" / Int32ub, # used_friend_skill_id is an int - "friend_skill_used_flag" / Int8ub, # friend_skill_used_flag is a byte - "continue_cnt" / Int16ub, # continue_cnt is a short - "total_loss_num" / Int16ub, # total_loss_num is a short - )), - - ) - - req_data = req_struct.parse(req) + req_data = SaoEpisodePlayEndRequest(header, request) # Add stage progression to database user_id = req_data.user_id episode_id = req_data.episode_id - quest_clear_flag = bool(req_data.score_data[0].boss_destroying_num) - clear_time = req_data.score_data[0].clear_time - combo_num = req_data.score_data[0].combo_num - total_damage = req_data.score_data[0].total_damage - concurrent_destroying_num = req_data.score_data[0].concurrent_destroying_num + quest_clear_flag = bool(req_data.play_end_request_data_list[0].score_data_list[0].boss_destroying_num) + clear_time = req_data.play_end_request_data_list[0].score_data_list[0].clear_time + combo_num = req_data.play_end_request_data_list[0].score_data_list[0].combo_num + total_damage = req_data.play_end_request_data_list[0].score_data_list[0].total_damage + concurrent_destroying_num = req_data.play_end_request_data_list[0].score_data_list[0].concurrent_destroying_num profile = self.game_data.profile.get_profile(user_id) vp = int(profile["own_vp"]) exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason - col = int(profile["own_col"]) + int(req_data.base_get_data[0].get_col) + col = int(profile["own_col"]) + int(req_data.play_end_request_data_list[0].base_get_data_list[0].get_col) if quest_clear_flag is True: # Save stage progression - to be revised to avoid saving worse score @@ -781,7 +522,7 @@ class SaoBase: for i in range(0,len(hero_list)): hero_data = self.game_data.item.get_hero_log(user_id, hero_list[i]) - log_exp = int(hero_data["log_exp"]) + int(req_data.base_get_data[0].get_hero_log_exp) + log_exp = int(hero_data["log_exp"]) + int(req_data.play_end_request_data_list[0].base_get_data_list[0].get_hero_log_exp) # Calculate hero level based off experience and the CSV list with open(r'titles/sao/data/HeroLogLevel.csv') as csv_file: @@ -817,8 +558,8 @@ class SaoBase: # Grab the rare loot from the table, match it with the right item and then push to the player profile json_data = {"data": []} - for r in range(0,req_data.get_rare_drop_data_list_length): - rewardList = self.game_data.static.get_rare_drop_id(int(req_data.get_rare_drop_data_list[r].quest_rare_drop_id)) + for r in range(0,req_data.play_end_request_data_list[0].get_rare_drop_data_count): + rewardList = self.game_data.static.get_rare_drop_id(int(req_data.play_end_request_data_list[0].get_rare_drop_data_list[r].quest_rare_drop_id)) commonRewardId = rewardList["commonRewardId"] heroList = self.game_data.static.get_hero_id(commonRewardId) @@ -833,16 +574,12 @@ class SaoBase: self.game_data.item.put_item(user_id, commonRewardId) # Generate random hero(es) based off the response - for a in range(0,req_data.get_unanalyzed_log_tmp_reward_data_list_length): - + for a in range(0,req_data.play_end_request_data_list[0].get_unanalyzed_log_tmp_reward_data_count): with open('titles/sao/data/RewardTable.csv', 'r') as f: keys_unanalyzed = next(f).strip().split(',') data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed)) randomized_unanalyzed_id = choice(data_unanalyzed) - while int(randomized_unanalyzed_id['UnanalyzedLogGradeId']) != req_data.get_unanalyzed_log_tmp_reward_data_list[a].unanalyzed_log_grade_id: - randomized_unanalyzed_id = choice(data_unanalyzed) - heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) equipmentList = self.game_data.static.get_equipment_id(randomized_unanalyzed_id['CommonRewardId']) itemList = self.game_data.static.get_item_id(randomized_unanalyzed_id['CommonRewardId']) @@ -859,35 +596,12 @@ class SaoBase: self.game_data.item.create_end_session(user_id, episode_id, quest_clear_flag, json_data["data"]) - resp = SaoEpisodePlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoEpisodePlayEndResponse(header.cmd +1) return resp.make() - def handle_c914(self, request: Any) -> bytes: + def handle_c914(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/trial_tower_play_start - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(16), - "ticket_id_size" / Rebuild(Int32ub, len_(this.ticket_id) * 2), # calculates the length of the ticket_id - "ticket_id" / PaddedString(this.ticket_id_size, "utf_16_le"), # ticket_id is a (zero) padded string - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - "trial_tower_id" / Int32ub, # trial_tower_id is an int - "play_mode" / Int8ub, # play_mode is a byte - Padding(3), - "play_start_request_data_length" / Rebuild(Int8ub, len_(this.play_start_request_data)), # play_start_request_data_length is a byte, - "play_start_request_data" / Array(this.play_start_request_data_length, Struct( - "user_party_id_size" / Rebuild(Int32ub, len_(this.user_party_id) * 2), # calculates the length of the user_party_id - "user_party_id" / PaddedString(this.user_party_id_size, "utf_16_le"), # user_party_id is a (zero) padded string - "appoint_leader_resource_card_code_size" / Rebuild(Int32ub, len_(this.appoint_leader_resource_card_code) * 2), # calculates the length of the total_damage - "appoint_leader_resource_card_code" / PaddedString(this.appoint_leader_resource_card_code_size, "utf_16_le"), # total_damage is a (zero) padded string - "use_profile_card_code_size" / Rebuild(Int32ub, len_(this.use_profile_card_code) * 2), # calculates the length of the total_damage - "use_profile_card_code" / PaddedString(this.use_profile_card_code_size, "utf_16_le"), # use_profile_card_code is a (zero) padded string - "quest_drop_boost_apply_flag" / Int8ub, # quest_drop_boost_apply_flag is a byte - )), - ) - - req_data = req_struct.parse(req) + req_data = SaoTrialTowerPlayStartRequest(header, request) user_id = req_data.user_id floor_id = req_data.trial_tower_id @@ -901,109 +615,22 @@ class SaoBase: req_data.play_start_request_data[0].quest_drop_boost_apply_flag ) - resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + resp = SaoEpisodePlayStartResponse(header.cmd +1, profile_data) return resp.make() - def handle_c918(self, request: Any) -> bytes: + def handle_c918(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/trial_tower_play_end - req = bytes.fromhex(request)[24:] - - req_struct = Struct( - Padding(20), - "ticket_id" / Bytes(1), # needs to be parsed as an int - Padding(1), - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - Padding(2), - "trial_tower_id" / Int16ub, # trial_tower_id is a short, - Padding(3), - "play_end_request_data" / Int8ub, # play_end_request_data is a byte - Padding(1), - "play_result_flag" / Int8ub, # play_result_flag is a byte - Padding(2), - "base_get_data_length" / Rebuild(Int8ub, len_(this.base_get_data)), # base_get_data_length is a byte, - "base_get_data" / Array(this.base_get_data_length, Struct( - "get_hero_log_exp" / Int32ub, # get_hero_log_exp is an int - "get_col" / Int32ub, # get_num is a short - )), - Padding(3), - "get_player_trace_data_list_length" / Rebuild(Int8ub, len_(this.get_player_trace_data_list)), # get_player_trace_data_list_length is a byte - "get_player_trace_data_list" / Array(this.get_player_trace_data_list_length, Struct( - "user_quest_scene_player_trace_id" / Int32ub, # user_quest_scene_player_trace_id is an int - )), - Padding(3), - "get_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_rare_drop_data_list)), # get_rare_drop_data_list_length is a byte - "get_rare_drop_data_list" / Array(this.get_rare_drop_data_list_length, Struct( - "quest_rare_drop_id" / Int32ub, # quest_rare_drop_id is an int - )), - Padding(3), - "get_special_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_special_rare_drop_data_list)), # get_special_rare_drop_data_list_length is a byte - "get_special_rare_drop_data_list" / Array(this.get_special_rare_drop_data_list_length, Struct( - "quest_special_rare_drop_id" / Int32ub, # quest_special_rare_drop_id is an int - )), - Padding(3), - "get_unanalyzed_log_tmp_reward_data_list_length" / Rebuild(Int8ub, len_(this.get_unanalyzed_log_tmp_reward_data_list)), # get_unanalyzed_log_tmp_reward_data_list_length is a byte - "get_unanalyzed_log_tmp_reward_data_list" / Array(this.get_unanalyzed_log_tmp_reward_data_list_length, Struct( - "unanalyzed_log_grade_id" / Int32ub, # unanalyzed_log_grade_id is an int, - )), - Padding(3), - "get_event_item_data_list_length" / Rebuild(Int8ub, len_(this.get_event_item_data_list)), # get_event_item_data_list_length is a byte, - "get_event_item_data_list" / Array(this.get_event_item_data_list_length, Struct( - "event_item_id" / Int32ub, # event_item_id is an int - "get_num" / Int16ub, # get_num is a short - )), - Padding(3), - "discovery_enemy_data_list_length" / Rebuild(Int8ub, len_(this.discovery_enemy_data_list)), # discovery_enemy_data_list_length is a byte - "discovery_enemy_data_list" / Array(this.discovery_enemy_data_list_length, Struct( - "enemy_kind_id" / Int32ub, # enemy_kind_id is an int - "destroy_num" / Int16ub, # destroy_num is a short - )), - Padding(3), - "destroy_boss_data_list_length" / Rebuild(Int8ub, len_(this.destroy_boss_data_list)), # destroy_boss_data_list_length is a byte - "destroy_boss_data_list" / Array(this.destroy_boss_data_list_length, Struct( - "boss_type" / Int8ub, # boss_type is a byte - "enemy_kind_id" / Int32ub, # enemy_kind_id is an int - "destroy_num" / Int16ub, # destroy_num is a short - )), - Padding(3), - "mission_data_list_length" / Rebuild(Int8ub, len_(this.mission_data_list)), # mission_data_list_length is a byte - "mission_data_list" / Array(this.mission_data_list_length, Struct( - "mission_id" / Int32ub, # enemy_kind_id is an int - "clear_flag" / Int8ub, # boss_type is a byte - "mission_difficulty_id" / Int16ub, # destroy_num is a short - )), - Padding(3), - "score_data_length" / Rebuild(Int8ub, len_(this.score_data)), # score_data_length is a byte - "score_data" / Array(this.score_data_length, Struct( - "clear_time" / Int32ub, # clear_time is an int - "combo_num" / Int32ub, # boss_type is a int - "total_damage_size" / Rebuild(Int32ub, len_(this.total_damage) * 2), # calculates the length of the total_damage - "total_damage" / PaddedString(this.total_damage_size, "utf_16_le"), # total_damage is a (zero) padded string - "concurrent_destroying_num" / Int16ub, # concurrent_destroying_num is a short - "reaching_skill_level" / Int16ub, # reaching_skill_level is a short - "ko_chara_num" / Int8ub, # ko_chara_num is a byte - "acceleration_invocation_num" / Int16ub, # acceleration_invocation_num is a short - "boss_destroying_num" / Int16ub, # boss_destroying_num is a short - "synchro_skill_used_flag" / Int8ub, # synchro_skill_used_flag is a byte - "used_friend_skill_id" / Int32ub, # used_friend_skill_id is an int - "friend_skill_used_flag" / Int8ub, # friend_skill_used_flag is a byte - "continue_cnt" / Int16ub, # continue_cnt is a short - "total_loss_num" / Int16ub, # total_loss_num is a short - )), - - ) - - req_data = req_struct.parse(req) + req_data = SaoTrialTowerPlayEndRequest(header, request) # Add tower progression to database user_id = req_data.user_id trial_tower_id = req_data.trial_tower_id next_tower_id = 0 - quest_clear_flag = bool(req_data.score_data[0].boss_destroying_num) - clear_time = req_data.score_data[0].clear_time - combo_num = req_data.score_data[0].combo_num - total_damage = req_data.score_data[0].total_damage - concurrent_destroying_num = req_data.score_data[0].concurrent_destroying_num + quest_clear_flag = bool(req_data.play_end_request_data_list[0].score_data_list[0].boss_destroying_num) + clear_time = req_data.play_end_request_data_list[0].score_data_list[0].clear_time + combo_num = req_data.play_end_request_data_list[0].score_data_list[0].combo_num + total_damage = req_data.play_end_request_data_list[0].score_data_list[0].total_damage + concurrent_destroying_num = req_data.play_end_request_data_list[0].score_data_list[0].concurrent_destroying_num if quest_clear_flag is True: # Save tower progression - to be revised to avoid saving worse score @@ -1048,7 +675,7 @@ class SaoBase: profile = self.game_data.profile.get_profile(user_id) exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason - col = int(profile["own_col"]) + int(req_data.base_get_data[0].get_col) + col = int(profile["own_col"]) + int(req_data.play_end_request_data_list[0].base_get_data_list[0].get_col) # Calculate level based off experience and the CSV list with open(r'titles/sao/data/PlayerRank.csv') as csv_file: @@ -1091,7 +718,7 @@ class SaoBase: for i in range(0,len(hero_list)): hero_data = self.game_data.item.get_hero_log(user_id, hero_list[i]) - log_exp = int(hero_data["log_exp"]) + int(req_data.base_get_data[0].get_hero_log_exp) + log_exp = int(hero_data["log_exp"]) + int(req_data.play_end_request_data_list[0].base_get_data_list[0].get_hero_log_exp) # Calculate hero level based off experience and the CSV list with open(r'titles/sao/data/HeroLogLevel.csv') as csv_file: @@ -1127,8 +754,8 @@ class SaoBase: json_data = {"data": []} # Grab the rare loot from the table, match it with the right item and then push to the player profile - for r in range(0,req_data.get_rare_drop_data_list_length): - rewardList = self.game_data.static.get_rare_drop_id(int(req_data.get_rare_drop_data_list[r].quest_rare_drop_id)) + for x in req_data.play_end_request_data_list[0].get_rare_drop_data_list: + rewardList = self.game_data.static.get_rare_drop_id(int(x.quest_rare_drop_id)) commonRewardId = rewardList["commonRewardId"] heroList = self.game_data.static.get_hero_id(commonRewardId) @@ -1143,14 +770,14 @@ class SaoBase: self.game_data.item.put_item(user_id, commonRewardId) # Generate random hero(es) based off the response - for a in range(0,req_data.get_unanalyzed_log_tmp_reward_data_list_length): + for x in req_data.play_end_request_data_list[0].get_unanalyzed_log_tmp_reward_data_list: with open('titles/sao/data/RewardTable.csv', 'r') as f: keys_unanalyzed = next(f).strip().split(',') data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed)) randomized_unanalyzed_id = choice(data_unanalyzed) - while int(randomized_unanalyzed_id['UnanalyzedLogGradeId']) != req_data.get_unanalyzed_log_tmp_reward_data_list[a].unanalyzed_log_grade_id: + while int(randomized_unanalyzed_id['UnanalyzedLogGradeId']) != x.unanalyzed_log_grade_id: randomized_unanalyzed_id = choice(data_unanalyzed) heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) @@ -1169,76 +796,178 @@ class SaoBase: self.game_data.item.create_end_session(user_id, trial_tower_id, quest_clear_flag, json_data["data"]) - resp = SaoTrialTowerPlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoTrialTowerPlayEndResponse(header.cmd +1) return resp.make() - def handle_c90a(self, request: Any) -> bytes: + def handle_c90a(self, header: SaoRequestHeader, request: bytes) -> bytes: #quest/episode_play_end_unanalyzed_log_fixed - req = bytes.fromhex(request)[24:] + req = SaoEpisodePlayEndUnanalyzedLogFixedRequest(header, request) - req_struct = Struct( - Padding(16), - "ticket_id_size" / Rebuild(Int32ub, len_(this.ticket_id) * 2), # calculates the length of the ticket_id - "ticket_id" / PaddedString(this.ticket_id_size, "utf_16_le"), # ticket_id is a (zero) padded string - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - ) + end_session_data = self.game_data.item.get_end_session(req.user_id) - req_data = req_struct.parse(req) - user_id = req_data.user_id - - end_session_data = self.game_data.item.get_end_session(user_id) - - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, end_session_data[4]) + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(header.cmd +1, end_session_data[4]) return resp.make() - def handle_c91a(self, request: Any) -> bytes: # handler is identical to the episode + def handle_c91a(self, header: SaoRequestHeader, request: bytes) -> bytes: # handler is identical to the episode #quest/trial_tower_play_end_unanalyzed_log_fixed - req = bytes.fromhex(request)[24:] + req = TrialTowerPlayEndUnanalyzedLogFixed(header, request) - req_struct = Struct( - Padding(16), - "ticket_id_size" / Rebuild(Int32ub, len_(this.ticket_id) * 2), # calculates the length of the ticket_id - "ticket_id" / PaddedString(this.ticket_id_size, "utf_16_le"), # ticket_id is a (zero) padded string - "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id - "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string - ) + end_session_data = self.game_data.item.get_end_session(req.user_id) - req_data = req_struct.parse(req) - user_id = req_data.user_id - - end_session_data = self.game_data.item.get_end_session(user_id) - - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, end_session_data[4]) + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(header.cmd +1, end_session_data[4]) return resp.make() - def handle_cd00(self, request: Any) -> bytes: + def handle_cd00(self, header: SaoRequestHeader, request: bytes) -> bytes: #defrag_match/get_defrag_match_basic_data - resp = SaoGetDefragMatchBasicDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetDefragMatchBasicDataResponse(header.cmd +1) return resp.make() - def handle_cd02(self, request: Any) -> bytes: + def handle_cd02(self, header: SaoRequestHeader, request: bytes) -> bytes: #defrag_match/get_defrag_match_ranking_user_data - resp = SaoGetDefragMatchRankingUserDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetDefragMatchRankingUserDataResponse(header.cmd +1) return resp.make() - def handle_cd04(self, request: Any) -> bytes: + def handle_cd04(self, header: SaoRequestHeader, request: bytes) -> bytes: #defrag_match/get_defrag_match_league_point_ranking_list - resp = SaoGetDefragMatchLeaguePointRankingListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetDefragMatchLeaguePointRankingListResponse(header.cmd +1) return resp.make() - def handle_cd06(self, request: Any) -> bytes: + def handle_cd06(self, header: SaoRequestHeader, request: bytes) -> bytes: #defrag_match/get_defrag_match_league_score_ranking_list - resp = SaoGetDefragMatchLeagueScoreRankingListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoGetDefragMatchLeagueScoreRankingListResponse(header.cmd +1) return resp.make() - def handle_d404(self, request: Any) -> bytes: + def handle_d404(self, header: SaoRequestHeader, request: bytes) -> bytes: #other/bnid_serial_code_check - resp = SaoBnidSerialCodeCheckResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + resp = SaoBnidSerialCodeCheckResponse(header.cmd +1) return resp.make() - def handle_c306(self, request: Any) -> bytes: + def handle_c306(self, header: SaoRequestHeader, request: bytes) -> bytes: #card/scan_qr_quest_profile_card - resp = SaoScanQrQuestProfileCardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) - return resp.make() \ No newline at end of file + resp = SaoScanQrQuestProfileCardResponse(header.cmd +1) + return resp.make() + + def handle_c700(self, header: SaoRequestHeader, request: bytes) -> bytes: + # shop/get_shop_resource_sales_data_list + # TODO: Get user shop data + req = GetShopResourceSalesDataListRequest(header, request) + resp = GetShopResourceSalesDataListResponse(header.cmd + 1) + return resp.make() + + def handle_d100(self, header: SaoRequestHeader, request: bytes) -> bytes: + # shop/get_yui_medal_shop_user_data_list + # TODO: Get user shop data + req = GetYuiMedalShopUserDataListRequest(header, request) + resp = GetYuiMedalShopUserDataListResponse(header.cmd + 1) + return resp.make() + + def handle_cf0e(self, header: SaoRequestHeader, request: bytes) -> bytes: + # gasha/get_gasha_medal_shop_user_data_list + # TODO: Get user shop data + req = GetGashaMedalShopUserDataListRequest(header, request) + resp = GetGashaMedalShopUserDataListResponse(header.cmd + 1) + return resp.make() + + def handle_d5da(self, header: SaoRequestHeader, request: bytes) -> bytes: + # master_data/get_m_yui_medal_shops + req = GetMYuiMedalShopDataRequest(header, request) + resp = GetMYuiMedalShopDataResponse(header.cmd + 1) + + shops = self.load_data_csv("YuiMedalShops") + for shop in shops: + tmp = YuiMedalShopData.from_args(int(shop['YuiMedalShopId']), shop['Name'], shop['Description']) + tmp.selling_yui_medal = int(shop['SellingYuiMedal']) + tmp.selling_col = int(shop['SellingCol']) + tmp.selling_event_item_id = int(shop['SellingEventItemId']) + tmp.selling_event_item_num = int(shop['SellingEventItemNum']) + tmp.selling_ticket_num = int(shop['SellingTicketNum']) + tmp.purchase_limit = int(shop['PurchaseLimit']) + tmp.pick_up_flag = 1 if shop['PickUpFlag'] == "True" else 0 + tmp.product_category = int(shop['ProductCategory']) + tmp.sales_type = int(shop['SalesType']) + tmp.target_days = int(shop['TargetDays']) + tmp.target_hour = int(shop['TargetHour']) + tmp.interval_hour = int(shop['IntervalHour']) + tmp.sort = int(shop['Sort']) + + tmp.sales_end_date = datetime(2121, 1, 1, 0, 0, 0, 0) # always open + + resp.data_list.append(tmp) + self.logger.debug(f"Load {len(resp.data_list)} Yui Medal Shops") + return resp.make() + + def handle_d5dc(self, header: SaoRequestHeader, request: bytes) -> bytes: + # master_data/get_m_yui_medal_shop_items + req = GetMYuiMedalShopItemsRequest(header, request) + resp = GetMYuiMedalShopItemsResponse(header.cmd + 1) + + shops = self.load_data_csv("YuiMedalShopItems") + for shop in shops: + tmp = YuiMedalShopItemData.from_args(int(shop['YuiMedalShopItemId']), int(shop['YuiMedalShopId']), int(shop['CommonRewardType']), int(shop['CommonRewardId']), int(shop['CommonRewardNum']), int(shop['Strength'])) + + tmp.property1_property_id = int(shop['Property1PropertyId']) + tmp.property1_value1 = int(shop['Property1Value1']) + tmp.property1_value2 = int(shop['Property1Value2']) + + tmp.property2_property_id = int(shop['Property2PropertyId']) + tmp.property2_value1 = int(shop['Property2Value1']) + tmp.property2_value2 = int(shop['Property2Value2']) + + tmp.property3_property_id = int(shop['Property3PropertyId']) + tmp.property3_value1 = int(shop['Property3Value1']) + tmp.property3_value2 = int(shop['Property3Value2']) + + tmp.property4_property_id = int(shop['Property4PropertyId']) + tmp.property4_value1 = int(shop['Property4Value1']) + tmp.property4_value2 = int(shop['Property4Value2']) + + resp.data_list.append(tmp) + + self.logger.debug(f"Load {len(resp.data_list)} Yui Medal Shop Items") + return resp.make() + + def handle_d5fc(self, header: SaoRequestHeader, request: bytes) -> bytes: + # master_data/get_m_gasha_medal_shops + req = GetMGashaMedalShopsRequest(header, request) + resp = GetMGashaMedalShopsResponse(header.cmd + 1) + + shops = self.load_data_csv("GashaMedalShops") + for shop in shops: + tmp = GashaMedalShop.from_args(int(shop['GashaMedalShopId']), shop['Name'], int(shop['GashaMedalId']), int(shop['UseGashaMedalNum']), int(shop['PurchaseLimit'])) + tmp.sales_end_date = datetime(2121, 1, 1, 0, 0, 0, 0) # always open + + resp.data_list.append(tmp) + + self.logger.debug(f"Load {len(resp.data_list)} Gasha Medal Shops") + return resp.make() + + def handle_d5fe(self, header: SaoRequestHeader, request: bytes) -> bytes: + # master_data/get_m_gasha_medal_shop_items + return SaoNoopResponse(header.cmd + 1).make() + + def handle_d604(self, header: SaoRequestHeader, request: bytes) -> bytes: + # master_data_2/get_m_res_earn_campaign_shops + req = GetMResEarnCampaignShopsRequest(header, request) + resp = GetMResEarnCampaignShopsResponse(header.cmd + 1) + + shops = self.load_data_csv("ResEarnCampaignShops") + for shop in shops: + tmp = ResEarnCampaignShop.from_args(int(shop['ResEarnCampaignShopId']), int(shop['ResEarnCampaignApplicationId']), shop['Name']) + tmp.selling_yui_medal = int(shop['SellingYuiMedal']) + tmp.selling_col = int(shop['SellingCol']) + tmp.selling_event_item_id = int(shop['SellingEventItemId']) + tmp.selling_event_item_num = int(shop['SellingEventItemNum']) + tmp.purchase_limit = int(shop['PurchaseLimit']) + tmp.get_application_point = int(shop['GetApplicationPoint']) + + tmp.sales_end_date = datetime(2121, 1, 1, 0, 0, 0, 0) # always open + + resp.data_list.append(tmp) + + #self.logger.debug(f"Load {len(resp.data_list)} Res Earn Campaign Shops") + return SaoNoopResponse(header.cmd + 1).make() + + def handle_d606(self, header: SaoRequestHeader, request: bytes) -> bytes: + # master_data_2/get_m_res_earn_campaign_shop_items + return SaoNoopResponse(header.cmd + 1).make() \ No newline at end of file diff --git a/titles/sao/config.py b/titles/sao/config.py index 7b9a2d5..33d319b 100644 --- a/titles/sao/config.py +++ b/titles/sao/config.py @@ -5,12 +5,6 @@ class SaoServerConfig: def __init__(self, parent_config: "SaoConfig"): self.__config = parent_config - @property - def hostname(self) -> str: - return CoreConfig.get_config_field( - self.__config, "sao", "server", "hostname", default="localhost" - ) - @property def enable(self) -> bool: return CoreConfig.get_config_field( @@ -25,12 +19,6 @@ class SaoServerConfig: ) ) - @property - def port(self) -> int: - return CoreConfig.get_config_field( - self.__config, "sao", "server", "port", default=9000 - ) - @property def auto_register(self) -> bool: """ @@ -41,7 +29,47 @@ class SaoServerConfig: self.__config, "sao", "server", "auto_register", default=True ) +class SaoCryptConfig: + def __init__(self, parent_config: "SaoConfig"): + self.__config = parent_config + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "sao", "crypt", "enable", default=False + ) + + @property + def key(self) -> str: + return CoreConfig.get_config_field( + self.__config, "sao", "crypt", "key", default="" + ) + + @property + def iv(self) -> str: + return CoreConfig.get_config_field( + self.__config, "sao", "crypt", "iv", default="" + ) + +class SaoHashConfig: + def __init__(self, parent_config: "SaoConfig"): + self.__config = parent_config + + @property + def verify_hash(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "sao", "hash", "verify_hash", default=False + ) + + @property + def hash_base(self) -> str: + return CoreConfig.get_config_field( + self.__config, "sao", "hash", "hash_base", default="" + ) + class SaoConfig(dict): def __init__(self) -> None: self.server = SaoServerConfig(self) + self.crypt = SaoCryptConfig(self) + self.hash = SaoHashConfig(self) diff --git a/titles/sao/data/GashaMedalShopItems.csv b/titles/sao/data/GashaMedalShopItems.csv new file mode 100644 index 0000000..c5a46ae Binary files /dev/null and b/titles/sao/data/GashaMedalShopItems.csv differ diff --git a/titles/sao/data/GashaMedalShops.csv b/titles/sao/data/GashaMedalShops.csv new file mode 100644 index 0000000..a4a78dc Binary files /dev/null and b/titles/sao/data/GashaMedalShops.csv differ diff --git a/titles/sao/data/ResEarnCampaignShopItems.csv b/titles/sao/data/ResEarnCampaignShopItems.csv new file mode 100644 index 0000000..6d5dd17 Binary files /dev/null and b/titles/sao/data/ResEarnCampaignShopItems.csv differ diff --git a/titles/sao/data/ResEarnCampaignShops.csv b/titles/sao/data/ResEarnCampaignShops.csv new file mode 100644 index 0000000..2c201c9 Binary files /dev/null and b/titles/sao/data/ResEarnCampaignShops.csv differ diff --git a/titles/sao/data/YuiMedalShopItems.csv b/titles/sao/data/YuiMedalShopItems.csv new file mode 100644 index 0000000..f5d91fe Binary files /dev/null and b/titles/sao/data/YuiMedalShopItems.csv differ diff --git a/titles/sao/data/YuiMedalShops.csv b/titles/sao/data/YuiMedalShops.csv new file mode 100644 index 0000000..5bac235 Binary files /dev/null and b/titles/sao/data/YuiMedalShops.csv differ diff --git a/titles/sao/handlers/__init__.py b/titles/sao/handlers/__init__.py index 90a6b4e..4d38136 100644 --- a/titles/sao/handlers/__init__.py +++ b/titles/sao/handlers/__init__.py @@ -1 +1,2 @@ -from titles.sao.handlers.base import * \ No newline at end of file +from .base import * +from .helpers import * \ No newline at end of file diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index ebcc56e..4178640 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -1,16 +1,29 @@ import struct from datetime import datetime +from typing import List from construct import * -import sys +from .helpers import * import csv from csv import * -class SaoBaseRequest: +class SaoRequestHeader: def __init__(self, data: bytes) -> None: - self.cmd = struct.unpack_from("!H", bytes)[0] - # TODO: The rest of the request header + collection = struct.unpack_from("!HHIIII16sI", data) + self.cmd: int = collection[0] + self.err_status = collection[1] + self.error_type = collection[2] + self.vendor_id: int = collection[3] + self.game_id: int = collection[4] + self.version_id: int = collection[5] + self.hash: str = collection[6] + self.data_len: str = collection[7] -class SaoBaseResponse: +class SaoBaseRequest: + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + self.header = header + # TODO: Length check + +class SaoResponseHeader: def __init__(self, cmd_id: int) -> None: self.cmd = cmd_id self.err_status = 0 @@ -23,6 +36,13 @@ class SaoBaseResponse: def make(self) -> bytes: return struct.pack("!HHIIIII", self.cmd, self.err_status, self.error_type, self.vendor_id, self.game_id, self.version_id, self.length) +class SaoBaseResponse: + def __init__(self, cmd_id: int) -> None: + self.header = SaoResponseHeader(cmd_id) + + def make(self) -> bytes: + return self.header.make() + class SaoNoopResponse(SaoBaseResponse): def __init__(self, cmd: int) -> None: super().__init__(cmd) @@ -33,8 +53,8 @@ class SaoNoopResponse(SaoBaseResponse): return super().make() + struct.pack("!bI", self.result, 0) class SaoGetMaintRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) # TODO: The rest of the mait info request class SaoGetMaintResponse(SaoBaseResponse): @@ -68,8 +88,8 @@ class SaoGetMaintResponse(SaoBaseResponse): return super().make() + resp_data class SaoCommonAcCabinetBootNotificationRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoCommonAcCabinetBootNotificationResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -90,8 +110,8 @@ class SaoCommonAcCabinetBootNotificationResponse(SaoBaseResponse): return super().make() + resp_data class SaoMasterDataVersionCheckRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoMasterDataVersionCheckResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -118,8 +138,8 @@ class SaoMasterDataVersionCheckResponse(SaoBaseResponse): return super().make() + resp_data class SaoCommonGetAppVersionsRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoCommonGetAppVersionsRequest(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -154,8 +174,8 @@ class SaoCommonGetAppVersionsRequest(SaoBaseResponse): return super().make() + resp_data class SaoCommonPayingPlayStartRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoCommonPayingPlayStartRequest(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -181,8 +201,30 @@ class SaoCommonPayingPlayStartRequest(SaoBaseResponse): return super().make() + resp_data class SaoGetAuthCardDataRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.cabinet_type = decode_byte(data, off) + off += BYTE_OFF + + self.auth_type = decode_byte(data, off) + off += BYTE_OFF + + store_id = decode_str(data, off) + self.store_id = store_id[0] + off += store_id[1] + + serial_no = decode_str(data, off) + self.serial_no = serial_no[0] + off += serial_no[1] + + access_code = decode_str(data, off) + self.access_code = access_code[0] + off += access_code[1] + + chip_id = decode_str(data, off) + self.chip_id = chip_id[0] + off += chip_id[1] class SaoGetAuthCardDataResponse(SaoBaseResponse): #GssSite.dll / GssSiteSystem / GameConnectProt / public class get_auth_card_data_R : GameConnect.GssProtocolBase def __init__(self, cmd, profile_data) -> None: @@ -225,8 +267,8 @@ class SaoGetAuthCardDataResponse(SaoBaseResponse): #GssSite.dll / GssSiteSystem return super().make() + resp_data class SaoHomeCheckAcLoginBonusRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoHomeCheckAcLoginBonusResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -262,8 +304,8 @@ class SaoHomeCheckAcLoginBonusResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetQuestSceneMultiPlayPhotonServerRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetQuestSceneMultiPlayPhotonServerResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -289,8 +331,8 @@ class SaoGetQuestSceneMultiPlayPhotonServerResponse(SaoBaseResponse): return super().make() + resp_data class SaoTicketRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoTicketResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -318,8 +360,37 @@ class SaoTicketResponse(SaoBaseResponse): return super().make() + resp_data class SaoCommonLoginRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.cabinet_type = decode_byte(data, off) + off += BYTE_OFF + + self.auth_type = decode_byte(data, off) + off += BYTE_OFF + + store_id = decode_str(data, off) + self.store_id = store_id[0] + off += store_id[1] + + store_name = decode_str(data, off) + self.store_name = store_name[0] + off += store_name[1] + + serial_no = decode_str(data, off) + self.serial_no = serial_no[0] + off += serial_no[1] + + access_code = decode_str(data, off) + self.access_code = access_code[0] + off += access_code[1] + + chip_id = decode_str(data, off) + self.chip_id = chip_id[0] + off += chip_id[1] + + self.free_ticket_distribution_target_flag = decode_byte(data, off) + off += BYTE_OFF class SaoCommonLoginResponse(SaoBaseResponse): def __init__(self, cmd, profile_data) -> None: @@ -362,8 +433,8 @@ class SaoCommonLoginResponse(SaoBaseResponse): return super().make() + resp_data class SaoCheckComebackEventRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoCheckComebackEventRequest(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -392,8 +463,9 @@ class SaoCheckComebackEventRequest(SaoBaseResponse): return super().make() + resp_data class SaoGetUserBasicDataRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] class SaoGetUserBasicDataResponse(SaoBaseResponse): def __init__(self, cmd, profile_data) -> None: @@ -476,8 +548,9 @@ class SaoGetUserBasicDataResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetHeroLogUserDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, hero_data) -> None: @@ -647,6 +720,8 @@ class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): resp_data.hero_log_user_data_list.append(hero_data) + resp_data["hero_log_user_data_list_size"] = len(resp_data.hero_log_user_data_list) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -654,8 +729,9 @@ class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetEquipmentUserDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, equipment_data) -> None: @@ -803,6 +879,8 @@ class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): resp_data.equipment_user_data_list.append(equipment_data) + resp_data["equipment_user_data_list_size"] = len(resp_data.equipment_user_data_list) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -810,8 +888,9 @@ class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetItemUserDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] class SaoGetItemUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, item_data) -> None: @@ -867,6 +946,8 @@ class SaoGetItemUserDataListResponse(SaoBaseResponse): resp_data.item_user_data_list.append(item_data) + resp_data["item_user_data_list_size"] = len(resp_data.item_user_data_list) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -874,8 +955,8 @@ class SaoGetItemUserDataListResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetSupportLogUserDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetSupportLogUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, supportIdsData) -> None: @@ -939,14 +1020,16 @@ class SaoGetSupportLogUserDataListResponse(SaoBaseResponse): resp_data.support_log_user_data_list.append(support_data) + resp_data["support_log_user_data_list_size"] = len(resp_data.support_log_user_data_list) + resp_data = resp_struct.build(resp_data) self.length = len(resp_data) return super().make() + resp_data class SaoGetTitleUserDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetTitleUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, titleIdsData) -> None: @@ -986,6 +1069,8 @@ class SaoGetTitleUserDataListResponse(SaoBaseResponse): resp_data.title_user_data_list.append(title_data) + resp_data["title_user_data_list_size"] = len(resp_data.title_user_data_list) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -993,8 +1078,9 @@ class SaoGetTitleUserDataListResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetEpisodeAppendDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): def __init__(self, cmd, profile_data) -> None: @@ -1044,6 +1130,8 @@ class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): own_num=self.own_num_list[i], )) + resp_data["episode_append_data_list_size"] = len(resp_data.episode_append_data_list) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -1051,8 +1139,9 @@ class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetPartyDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] class SaoGetPartyDataListResponse(SaoBaseResponse): # Default party def __init__(self, cmd, hero1_data, hero2_data, hero3_data) -> None: @@ -1215,8 +1304,8 @@ class SaoGetPartyDataListResponse(SaoBaseResponse): # Default party return super().make() + resp_data class SaoGetQuestScenePrevScanProfileCardRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetQuestScenePrevScanProfileCardResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -1352,8 +1441,8 @@ class SaoGetQuestScenePrevScanProfileCardResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetResourcePathInfoRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetResourcePathInfoResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -1394,8 +1483,40 @@ class SaoGetResourcePathInfoResponse(SaoBaseResponse): return super().make() + resp_data class SaoEpisodePlayStartRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + self.episode_id = decode_int(data, off) + off += INT_OFF + + self.play_mode = decode_byte(data, off) + off += BYTE_OFF + + self.play_start_request_data_count = decode_int(data, off) + off += INT_OFF + + self.play_start_request_data: List[PlayStartRequestData] = [] + for _ in range(self.play_start_request_data_count): + tmp = PlayStartRequestData(data, off) + self.play_start_request_data.append(tmp) + off += tmp.get_size() + + self.multi_play_start_request_data_count = decode_int(data, off) + off += INT_OFF + + self.multi_play_start_request_data: List[MultiPlayStartRequestData] = [] + for _ in range(self.multi_play_start_request_data_count): + tmp = MultiPlayStartRequestData(data, off) + off += tmp.get_size() + self.multi_play_start_request_data.append(tmp) class SaoEpisodePlayStartResponse(SaoBaseResponse): def __init__(self, cmd, profile_data) -> None: @@ -1441,8 +1562,37 @@ class SaoEpisodePlayStartResponse(SaoBaseResponse): return super().make() + resp_data class SaoEpisodePlayEndRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + self.episode_id = decode_int(data, off) + off += INT_OFF + + self.play_end_request_data_count = decode_int(data, off) + off += INT_OFF + + self.play_end_request_data_list: List[PlayEndRequestData] = [] + for _ in range(self.play_end_request_data_count): + tmp = PlayEndRequestData(data, off) + off += tmp.get_size() + self.play_end_request_data_list.append(tmp) + + self.multi_play_end_request_data_count = decode_int(data, off) + off += INT_OFF + + self.multi_play_end_request_data_list: List[MultiPlayEndRequestData] = [] + for _ in range(self.multi_play_end_request_data_count): + tmp = MultiPlayEndRequestData(data, off) + off += tmp.get_size() + self.multi_play_end_request_data_list.append(tmp) class SaoEpisodePlayEndResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -1528,9 +1678,74 @@ class SaoEpisodePlayEndResponse(SaoBaseResponse): self.length = len(resp_data) return super().make() + resp_data +class SaoTrialTowerPlayStartRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + self.trial_tower_id = decode_int(data, off) + off += INT_OFF + + self.play_mode = decode_byte(data, off) + off += BYTE_OFF + + self.play_start_request_data_count = decode_int(data, off) + off += INT_OFF + + self.play_start_request_data: List[PlayStartRequestData] = [] + for _ in range(self.play_start_request_data_count): + tmp = PlayStartRequestData(data, off) + self.play_start_request_data.append(tmp) + off += tmp.get_size() + + self.multi_play_start_request_data_count = decode_int(data, off) + off += INT_OFF + + self.multi_play_start_request_data: List[MultiPlayStartRequestData] = [] + for _ in range(self.multi_play_start_request_data_count): + tmp = MultiPlayStartRequestData(data, off) + off += tmp.get_size() + self.multi_play_start_request_data.append(tmp) + class SaoTrialTowerPlayEndRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + self.trial_tower_id = decode_int(data, off) + off += INT_OFF + + self.play_end_request_data_count = decode_int(data, off) + off += INT_OFF + + self.play_end_request_data_list: List[PlayEndRequestData] = [] + for _ in range(self.play_end_request_data_count): + tmp = PlayEndRequestData(data, off) + off += tmp.get_size() + self.play_end_request_data_list.append(tmp) + + self.multi_play_end_request_data_count = decode_int(data, off) + off += INT_OFF + + self.multi_play_end_request_data_list: List[MultiPlayEndRequestData] = [] + for _ in range(self.multi_play_end_request_data_count): + tmp = MultiPlayEndRequestData(data, off) + off += tmp.get_size() + self.multi_play_end_request_data_list.append(tmp) class SaoTrialTowerPlayEndResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -1655,8 +1870,22 @@ class SaoTrialTowerPlayEndResponse(SaoBaseResponse): return super().make() + resp_data class SaoEpisodePlayEndUnanalyzedLogFixedRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + self.episode_id = decode_int(data, off) + off += INT_OFF + + self.rarity_up_exec_flag = decode_byte(data, off) + off += BYTE_OFF class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): def __init__(self, cmd, end_session_data) -> None: @@ -1728,6 +1957,10 @@ class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): resp_data.play_end_unanalyzed_log_reward_data_list.append(reward_resp_data) + resp_data["play_end_unanalyzed_log_reward_data_list_size"] = len(resp_data.play_end_unanalyzed_log_reward_data_list) + for i in range(len(resp_data.play_end_unanalyzed_log_reward_data_list)): + resp_data.play_end_unanalyzed_log_reward_data_list[i]["common_reward_data_size"] = len(resp_data.play_end_unanalyzed_log_reward_data_list[i]["common_reward_data"]) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -1735,12 +1968,14 @@ class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetQuestSceneUserDataListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.user_id = decode_str(data, 0)[0] class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): def __init__(self, cmd, quest_data) -> None: super().__init__(cmd) + self.length = None self.result = 1 # quest_scene_user_data_list_size @@ -1837,6 +2072,10 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): resp_data.quest_scene_user_data_list.append(quest_resp_data) + resp_data["quest_scene_user_data_list_size"] = len(resp_data.quest_scene_user_data_list) + for i in range(len(resp_data.quest_scene_user_data_list)): + resp_data.quest_scene_user_data_list[i]["quest_scene_best_score_user_data_size"] = len(resp_data.quest_scene_user_data_list[i]["quest_scene_best_score_user_data"]) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -1844,8 +2083,8 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): return super().make() + resp_data class SaoCheckYuiMedalGetConditionRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoCheckYuiMedalGetConditionResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -1875,8 +2114,8 @@ class SaoCheckYuiMedalGetConditionResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetYuiMedalBonusUserDataRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetYuiMedalBonusUserDataResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -1920,8 +2159,8 @@ class SaoGetYuiMedalBonusUserDataResponse(SaoBaseResponse): return super().make() + resp_data class SaoCheckProfileCardUsedRewardRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoCheckProfileCardUsedRewardResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -1952,8 +2191,30 @@ class SaoCheckProfileCardUsedRewardResponse(SaoBaseResponse): return super().make() + resp_data class SaoSynthesizeEnhancementHeroLogRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + origin_user_hero_log_id = decode_str(data, off) + self.origin_user_hero_log_id = origin_user_hero_log_id[0] + off += origin_user_hero_log_id[1] + + self.material_common_reward_user_data_list: List[MaterialCommonRewardUserData] = [] + + self.material_common_reward_user_data_count = decode_int(data, off) + off += INT_OFF + + for _ in range(self.material_common_reward_user_data_count): + mat = MaterialCommonRewardUserData(data, off) + off += mat.get_size() + self.material_common_reward_user_data_list.append(mat) class SaoSynthesizeEnhancementHeroLogResponse(SaoBaseResponse): def __init__(self, cmd, hero_data) -> None: @@ -2100,15 +2361,39 @@ class SaoSynthesizeEnhancementHeroLogResponse(SaoBaseResponse): resp_data.hero_log_user_data_list.append(hero_data) + resp_data["hero_log_user_data_list_size"] = len(resp_data.hero_log_user_data_list) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) self.length = len(resp_data) return super().make() + resp_data -class SaoSynthesizeEnhancementEquipment(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) +class SaoSynthesizeEnhancementEquipmentRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + origin_user_equipment_id = decode_str(data, off) + self.origin_user_equipment_id = origin_user_equipment_id[0] + off += origin_user_equipment_id[1] + + self.material_common_reward_user_data_list: List[MaterialCommonRewardUserData] = [] + + self.material_common_reward_user_data_count = decode_int(data, off) + off += INT_OFF + + for _ in range(self.material_common_reward_user_data_count): + mat = MaterialCommonRewardUserData(data, off) + off += mat.get_size() + self.material_common_reward_user_data_list.append(mat) class SaoSynthesizeEnhancementEquipmentResponse(SaoBaseResponse): def __init__(self, cmd, synthesize_equipment_data) -> None: @@ -2237,6 +2522,8 @@ class SaoSynthesizeEnhancementEquipmentResponse(SaoBaseResponse): resp_data.after_equipment_user_data.append(synthesize_equipment_data) + resp_data["after_equipment_user_data_size"] = len(resp_data.after_equipment_user_data) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) @@ -2244,8 +2531,8 @@ class SaoSynthesizeEnhancementEquipmentResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetDefragMatchBasicDataRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetDefragMatchBasicDataResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -2291,8 +2578,8 @@ class SaoGetDefragMatchBasicDataResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetDefragMatchRankingUserDataRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetDefragMatchRankingUserDataResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -2351,8 +2638,8 @@ class SaoGetDefragMatchRankingUserDataResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetDefragMatchLeaguePointRankingListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetDefragMatchLeaguePointRankingListResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -2423,8 +2710,8 @@ class SaoGetDefragMatchLeaguePointRankingListResponse(SaoBaseResponse): return super().make() + resp_data class SaoGetDefragMatchLeagueScoreRankingListRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoGetDefragMatchLeagueScoreRankingListResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -2495,8 +2782,8 @@ class SaoGetDefragMatchLeagueScoreRankingListResponse(SaoBaseResponse): return super().make() + resp_data class SaoBnidSerialCodeCheckRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoBnidSerialCodeCheckResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -2526,8 +2813,8 @@ class SaoBnidSerialCodeCheckResponse(SaoBaseResponse): return super().make() + resp_data class SaoScanQrQuestProfileCardRequest(SaoBaseRequest): - def __init__(self, data: bytes) -> None: - super().__init__(data) + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) class SaoScanQrQuestProfileCardResponse(SaoBaseResponse): def __init__(self, cmd) -> None: @@ -2737,8 +3024,209 @@ class SaoScanQrQuestProfileCardResponse(SaoBaseResponse): resp_data.read_profile_card_data.append(hero_data) + resp_data["read_profile_card_data_size"] = len(resp_data.read_profile_card_data) + # finally, rebuild the resp_data resp_data = resp_struct.build(resp_data) self.length = len(resp_data) - return super().make() + resp_data \ No newline at end of file + return super().make() + resp_data + +class SaoConsumeCreditGuestRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + shop_id = decode_str(data, off) + self.shop_id = shop_id[0] + off += shop_id[1] + + serial_num = decode_str(data, off) + self.serial_num = serial_num[0] + off += serial_num[1] + + self.cab_type = decode_byte(data, off) + off += BYTE_OFF + + self.act_type = decode_byte(data, off) + off += BYTE_OFF + + self.consume_num = decode_byte(data, off) + off += BYTE_OFF + +class SaoChangePartyRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + self.act_type = decode_byte(data, off) + off += BYTE_OFF + + self.party_data_count = decode_int(data, off) + off += INT_OFF + + self.party_data_list: List[PartyData] = [] + + for _ in range(self.party_data_count): + tmp = PartyData(data, off) + self.party_data_list.append(tmp) + off += tmp.get_size() + +class TrialTowerPlayEndUnanalyzedLogFixed(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + + self.trial_tower_id = decode_int(data, off) + off += INT_OFF + + self.rarity_up_exec_flag = decode_byte(data, off) + off += BYTE_OFF + +class GetShopResourceSalesDataListRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + +class GetShopResourceSalesDataListResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.shop_resource_sales_data: List[ShopResourceSalesData] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.shop_resource_sales_data) + + self.header.length = len(ret) + return super().make() + ret + +class GetYuiMedalShopUserDataListRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + +class GetYuiMedalShopUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.user_data_list: List[YuiMedalShopUserData] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.user_data_list) + + self.header.length = len(ret) + return super().make() + ret + +class GetGashaMedalShopUserDataListRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + +class GetGashaMedalShopUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.data_list: List[GashaMedalShopUserData] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.data_list) + + self.header.length = len(ret) + return super().make() + ret + +class GetMYuiMedalShopDataRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.dummy = decode_byte(data, 0) + +class GetMYuiMedalShopDataResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.data_list: List[YuiMedalShopData] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.data_list) + + self.header.length = len(ret) + return super().make() + ret + +class GetMYuiMedalShopItemsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.dummy = decode_byte(data, 0) + +class GetMYuiMedalShopItemsResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.data_list: List[YuiMedalShopItemData] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.data_list) + + self.header.length = len(ret) + return super().make() + ret + +class GetMGashaMedalShopsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.dummy = decode_byte(data, 0) + +class GetMGashaMedalShopsResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.data_list: List[GashaMedalShop] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.data_list) + + self.header.length = len(ret) + return super().make() + ret + +class GetMResEarnCampaignShopsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.dummy = decode_byte(data, 0) + +class GetMResEarnCampaignShopsResponse(SaoBaseResponse): + def __init__(self, cmd_id: int) -> None: + super().__init__(cmd_id) + self.result = 1 # byte + self.data_list: List[ResEarnCampaignShop] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.data_list) + + self.header.length = len(ret) + return super().make() + ret diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py new file mode 100644 index 0000000..6e11146 --- /dev/null +++ b/titles/sao/handlers/helpers.py @@ -0,0 +1,917 @@ +from typing import Tuple, List, Optional +import struct +import logging +from datetime import datetime + +BIGINT_OFF = 16 +LONG_OFF = 8 +INT_OFF = 4 +SHORT_OFF = 2 +BYTE_OFF = 1 + +DT_FMT = "%Y%m%d%H%M%S" + +def fmt_dt(d: Optional[datetime] = None) -> str: + if d is None: + d = datetime.fromtimestamp(0) + return d.strftime(DT_FMT) + +def prs_dt(s: Optional[str] = None) -> datetime: + if not s: + s = "19691231190000" + return datetime.strptime(s, DT_FMT) + +def decode_num(data: bytes, offset: int, size: int) -> int: + try: + return int.from_bytes(data[offset:offset + size], 'big') + except: + logging.getLogger('sao').error(f"Failed to parse {data[offset:offset + size]} as BE number of width {size}") + return 0 + +def decode_byte(data: bytes, offset: int) -> int: + return decode_num(data, offset, BYTE_OFF) + +def decode_short(data: bytes, offset: int) -> int: + return decode_num(data, offset, SHORT_OFF) + +def decode_int(data: bytes, offset: int) -> int: + return decode_num(data, offset, INT_OFF) + +def decode_long(data: bytes, offset: int) -> int: + return decode_num(data, offset, LONG_OFF) + +def decode_bigint(data: bytes, offset: int) -> int: + return decode_num(data, offset, BIGINT_OFF) + +def decode_str(data: bytes, offset: int) -> Tuple[str, int]: + try: + str_len = decode_int(data, offset) + num_bytes_decoded = INT_OFF + str_len + str_out = data[offset + INT_OFF:offset + num_bytes_decoded].decode("utf-16-le", errors="replace") + return (str_out, num_bytes_decoded) + except: + logging.getLogger('sao').error(f"Failed to parse {data[offset:]} as string!") + return ("", 0) + +def decode_arr_num(data: bytes, offset:int, element_size: int) -> Tuple[List[int], int]: + size = 0 + num_obj = decode_int(data, offset + size) + size += INT_OFF + + ret: List[int] = [] + for _ in range(num_obj): + ret.append(decode_num(data, offset + size, element_size)) + size += element_size + + return (ret, size) + +def decode_arr_str(data: bytes, offset: int) -> Tuple[List[str], int]: + size = 0 + num_obj = decode_int(data, offset + size) + size += INT_OFF + + ret: List[str] = [] + for _ in range(num_obj): + tmp = decode_str(data, offset + size) + ret.append(tmp[0]) + size += tmp[1] + + return (ret, size) + +def encode_byte(data: int) -> bytes: + return struct.pack("!b", data) + +def encode_short(data: int) -> bytes: + return struct.pack("!h", data) + +def encode_int(data: int) -> bytes: + return struct.pack("!i", data) + +def encode_long(data: int) -> bytes: + return struct.pack("!l", data) + +def encode_bigint(data: int) -> bytes: + return struct.pack("!q", data) + +def encode_str(s: str) -> bytes: + try: + str_bytes = s.encode("utf-16-le", errors="replace") + str_len_bytes = struct.pack("!I", len(str_bytes)) + return str_len_bytes + str_bytes + except: + logging.getLogger('sao').error(f"Failed to encode {s} as bytes!") + return b"" + +def encode_arr_num(data: List[int], element_size: int) -> bytes: + ret = encode_int(len(data)) + + if element_size == BYTE_OFF: + for x in data: + ret += encode_byte(x) + elif element_size == SHORT_OFF: + for x in data: + ret += encode_short(x) + elif element_size == INT_OFF: + for x in data: + ret += encode_int(x) + elif element_size == LONG_OFF: + for x in data: + ret += encode_long(x) + elif element_size == BIGINT_OFF: + for x in data: + ret += encode_bigint(x) + else: + logging.getLogger('sao').error(f"Unknown element size {element_size}") + return b"\x00" * INT_OFF + + return ret + +class BaseHelper: + def __init__(self, data: bytes, offset: int) -> None: + self._sz = 0 + + @classmethod + def from_args(cls) -> "BaseHelper": + return cls(b"", 0) + + def get_size(self) -> int: + return self._sz + + def make(self) -> bytes: + return b"" + +def decode_arr_cls(data: bytes, offset: int, cls: BaseHelper): + size = 0 + num_cls = decode_int(data, offset + size) + cls_type = type(cls) + + ret: List[cls_type] = [] + for _ in range(num_cls): + tmp = cls(data, offset + size) + size += tmp.get_size() + ret.append(tmp) + + return (ret, size) + +def encode_arr_cls(data: List[BaseHelper]) -> bytes: + ret = encode_int(len(data)) + + for x in data: + ret += x.make() + + return ret + +class MaterialCommonRewardUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.common_reward_type = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + user_common_reward_id = decode_str(data, offset + self._sz) + self.user_common_reward_id = user_common_reward_id[0] + self._sz += user_common_reward_id[1] + +class PartyTeamData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + sz = 0 + user_party_team_id = decode_str(data, offset + sz) + self.user_party_team_id = user_party_team_id[0] + sz += user_party_team_id[1] + + self.arrangement_num = decode_byte(data, offset + sz) + sz += BYTE_OFF + + user_hero_log_id = decode_str(data, offset + sz) + self.user_hero_log_id = user_hero_log_id[0] + sz += user_hero_log_id[1] + + main_weapon_user_equipment_id = decode_str(data, offset + sz) + self.main_weapon_user_equipment_id = main_weapon_user_equipment_id[0] + sz += main_weapon_user_equipment_id[1] + + sub_equipment_user_equipment_id = decode_str(data, offset + sz) + self.sub_equipment_user_equipment_id = sub_equipment_user_equipment_id[0] + sz += sub_equipment_user_equipment_id[1] + + self.skill_slot1_skill_id = decode_int(data, offset + sz) + sz += INT_OFF + + self.skill_slot2_skill_id = decode_int(data, offset + sz) + sz += INT_OFF + + self.skill_slot3_skill_id = decode_int(data, offset + sz) + sz += INT_OFF + + self.skill_slot4_skill_id = decode_int(data, offset + sz) + sz += INT_OFF + + self.skill_slot5_skill_id = decode_int(data, offset + sz) + sz += INT_OFF + + self._sz = sz + +class PartyData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + sz = 0 + user_party_id = decode_str(data, offset + sz) + self.user_party_id = user_party_id[0] + sz += user_party_id[1] + + self.team_no = decode_byte(data, offset + sz) + sz += BYTE_OFF + + self.party_team_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.party_team_data_list: List[PartyTeamData] = [] + for _ in range(self.party_team_data_count): + tmp = PartyTeamData(data, offset + sz) + self.party_team_data_list.append(tmp) + sz += tmp.get_size() + + self._sz = sz + +class PlayStartRequestData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + sz = 0 + user_party_id = decode_str(data, offset + sz) + self.user_party_id = user_party_id[0] + sz += user_party_id[1] + + appoint_leader_resource_card_code = decode_str(data, offset + sz) + self.appoint_leader_resource_card_code = appoint_leader_resource_card_code[0] + sz += appoint_leader_resource_card_code[1] + + use_profile_card_code = decode_str(data, offset + sz) + self.use_profile_card_code = use_profile_card_code[0] + sz += use_profile_card_code[1] + + self.quest_drop_boost_apply_flag = decode_byte(data, offset + sz) + sz += BYTE_OFF + + self._sz = sz + +class GetPlayerTraceData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + user_quest_scene_player_trace_id = decode_str(data, offset) + self.user_quest_scene_player_trace_id = user_quest_scene_player_trace_id[0] + self._sz = user_quest_scene_player_trace_id[1] + +class BaseGetData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.get_hero_log_exp = decode_int(data, offset) + offset += INT_OFF + + self.get_col = decode_int(data, offset) + + self._sz = INT_OFF + INT_OFF + +class RareDropData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.quest_rare_drop_id = decode_int(data, offset) + + self._sz = INT_OFF + +class UnanalyzedLogTmpRewardData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.unanalyzed_log_grade_id = decode_int(data, offset) + + self._sz = INT_OFF + +class SpecialRareDropData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.quest_special_rare_drop_id = decode_int(data, offset) + + self._sz = INT_OFF + +class EventItemData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.event_item_id = decode_int(data, offset) + offset += INT_OFF + self.get_num = decode_short(data, offset) + + self._sz = INT_OFF + SHORT_OFF + +class DiscoveryEnemyData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.enemy_kind_id = decode_int(data, offset) + offset += INT_OFF + self.destroy_num = decode_short(data, offset) + + self._sz = INT_OFF + SHORT_OFF + +class DestroyBossData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.boss_type = decode_byte(data, offset) + offset += BYTE_OFF + self.enemy_kind_id = decode_int(data, offset) + offset += INT_OFF + self.mission_difficulty_id = decode_short(data, offset) + + self._sz = INT_OFF + SHORT_OFF + BYTE_OFF + +class MissionData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + self.mission_id = decode_int(data, offset) + offset += INT_OFF + self.clear_flag = decode_byte(data, offset) + offset += BYTE_OFF + self.destroy_num = decode_short(data, offset) + + self._sz = INT_OFF + SHORT_OFF + BYTE_OFF + +class ScoreData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.clear_time = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.combo_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + total_damage = decode_str(data, offset + self._sz) + self.total_damage = total_damage[0] + self._sz += total_damage[1] + + self.concurrent_destroying_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + self.reaching_skill_level = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + self.ko_chara_num = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + + self.acceleration_invocation_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + self.boss_destroying_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + self.synchro_skill_used_flag = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + + self.used_friend_skill_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.friend_skill_used_flag = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + + self.continue_cnt = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + self.total_loss_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + +class PlayEndRequestData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + sz = 0 + self.play_result_flag = decode_byte(data, offset + sz) + sz += BYTE_OFF + + self.base_get_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.base_get_data_list: List[BaseGetData] = [] + for _ in range(self.base_get_data_count): + tmp = BaseGetData(data, offset + sz) + sz += tmp.get_size() + self.base_get_data_list.append(tmp) + + self.get_player_trace_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.get_player_trace_data: List[GetPlayerTraceData] = [] + for _ in range(self.get_player_trace_data_count): + tmp = GetPlayerTraceData(data, offset + sz) + sz += tmp.get_size() + self.get_player_trace_data.append(tmp) + + self.get_rare_drop_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.get_rare_drop_data_list: List[RareDropData] = [] + for _ in range(self.get_rare_drop_data_count): + tmp = RareDropData(data, offset + sz) + sz += tmp.get_size() + self.get_rare_drop_data_list.append(tmp) + + self.get_special_rare_drop_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.get_special_rare_drop_data_list: List[SpecialRareDropData] = [] + for _ in range(self.get_special_rare_drop_data_count): + tmp = SpecialRareDropData(data, offset + sz) + sz += tmp.get_size() + self.get_special_rare_drop_data_list.append(tmp) + + self.get_unanalyzed_log_tmp_reward_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.get_unanalyzed_log_tmp_reward_data_list: List[UnanalyzedLogTmpRewardData] = [] + for _ in range(self.get_unanalyzed_log_tmp_reward_data_count): + tmp = UnanalyzedLogTmpRewardData(data, offset + sz) + sz += tmp.get_size() + self.get_unanalyzed_log_tmp_reward_data_list.append(tmp) + + self.get_event_item_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.get_event_item_data_list: List[EventItemData] = [] + for _ in range(self.get_event_item_data_count): + tmp = EventItemData(data, offset + sz) + sz += tmp.get_size() + self.get_event_item_data_list.append(tmp) + + self.discovery_enemy_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.discovery_enemy_data_list: List[DiscoveryEnemyData] = [] + for _ in range(self.discovery_enemy_data_count): + tmp = DiscoveryEnemyData(data, offset + sz) + sz += tmp.get_size() + self.discovery_enemy_data_list.append(tmp) + + self.destroy_boss_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.destroy_boss_data_list: List[DestroyBossData] = [] + for _ in range(self.destroy_boss_data_count): + tmp = DestroyBossData(data, offset + sz) + sz += tmp.get_size() + self.destroy_boss_data_list.append(tmp) + + self.mission_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.mission_data_list: List[MissionData] = [] + for _ in range(self.mission_data_count): + tmp = MissionData(data, offset + sz) + sz += tmp.get_size() + self.mission_data_list.append(tmp) + + self.score_data_count = decode_int(data, offset + sz) + sz += INT_OFF + + self.score_data_list: List[ScoreData] = [] + for _ in range(self.score_data_count): + tmp = ScoreData(data, offset + sz) + sz += tmp.get_size() + self.score_data_list.append(tmp) + + self._sz = sz + +class EntryUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + store_id = decode_str(data, offset + self._sz) + self.store_id = store_id[0] + self._sz += store_id[1] + + user_id = decode_str(data, offset + self._sz) + self.user_id = user_id[0] + self._sz += user_id[1] + + self.host_flag = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + +class MultiPlayStartRequestData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + room_id = decode_str(data, offset + self._sz) + self.room_id = room_id[0] + self._sz += room_id[1] + + self.matching_mode = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + + self.entry_user_data_count = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.entry_user_data_list: List[EntryUserData] = [] + for _ in range(self.entry_user_data_count): + tmp = EntryUserData(data, offset + self._sz) + self._sz += tmp.get_size() + self.entry_user_data_list.append(tmp) + +class MultiPlayEndRequestData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.dummy_1 = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + self.dummy_2 = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + self.dummy_3 = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + +class SalesResourceData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.common_reward_type = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + self.common_reward_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property1_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property1_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property1_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property2_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property2_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property2_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property3_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property3_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property3_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property4_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property4_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property4_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + @classmethod + def from_args(cls, reward_type: int = 0, reward_id: int = 0) -> "SalesResourceData": + ret = cls(b"\x00" * 54, 0) + ret.common_reward_type = reward_type # short + ret.common_reward_id = reward_id # int + + return ret + + def make(self) -> bytes: + ret = b"" + ret += encode_short(self.common_reward_type) + ret += encode_int(self.common_reward_id) + + ret += encode_int(self.property1_property_id) + ret += encode_int(self.property1_value1) + ret += encode_int(self.property1_value2) + + ret += encode_int(self.property2_property_id) + ret += encode_int(self.property2_value1) + ret += encode_int(self.property2_value2) + + ret += encode_int(self.property3_property_id) + ret += encode_int(self.property3_value1) + ret += encode_int(self.property3_value2) + + ret += encode_int(self.property4_property_id) + ret += encode_int(self.property4_value1) + ret += encode_int(self.property4_value2) + +class ShopResourceSalesData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + user_shop_resource_id = decode_str(data, offset + self._sz) + self.user_shop_resource_id = user_shop_resource_id[0] + self._sz = user_shop_resource_id[1] + + discharge_user_id = decode_str(data, offset + self._sz) + self.discharge_user_id = discharge_user_id[0] + self._sz = discharge_user_id[1] + + self.remaining_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + self.purchase_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + sales_start_date = decode_str(data, offset + self._sz) + self.sales_start_date = prs_dt(sales_start_date[0]) + self._sz = sales_start_date[1] + + sales_resource_data_list = decode_arr_cls(data, offset + self._sz, SalesResourceData) + self.sales_resource_data_list: List[SalesResourceData] = sales_resource_data_list[0] + self._sz += sales_resource_data_list[1] + + @classmethod + def from_args(cls, resource_id: str = "0", discharge_id: str = "0", remaining: int = 0, purchased: int = 0) -> "ShopResourceSalesData": + ret = cls(b"\x00" * 20, 0) + ret.user_shop_resource_id = resource_id + ret.discharge_user_id = discharge_id + ret.remaining_num = remaining # short + ret.purchase_num = purchased # short + ret.sales_start_date = prs_dt() + + def make(self) -> bytes: + ret = encode_str(self.user_shop_resource_id) + ret += encode_str(self.discharge_user_id) + ret += encode_short(self.remaining_num) + ret += encode_short(self.purchase_num) + ret += encode_str(fmt_dt(self.sales_start_date)) + ret += encode_arr_cls(self.sales_resource_data_list) + return ret + +class YuiMedalShopUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.yui_medal_shop_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.purchase_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + last_purchase_date = decode_str(data, offset + self._sz) + self.last_purchase_date = last_purchase_date[0] + self._sz += last_purchase_date[1] + + @classmethod + def from_args(cls, yui_medal_shop_id: int = 0, purchase_num: int = 0, last_purchase_date: datetime = datetime.fromtimestamp(0)) -> "YuiMedalShopUserData": + ret = cls(b"\x00" * 20, 0) + ret.yui_medal_shop_id = yui_medal_shop_id + ret.purchase_num = purchase_num + ret.last_purchase_date = last_purchase_date + return ret + + def make(self) -> bytes: + ret = encode_int(self.yui_medal_shop_id) + ret += encode_int(self.purchase_num) + ret += encode_str(fmt_dt(self.last_purchase_date)) + return ret + +class GashaMedalShopUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.gasha_medal_shop_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.purchase_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + @classmethod + def from_args(cls, gasha_medal_shop_id: int = 0, purchase_num: int = 0) -> "GashaMedalShopUserData": + ret = cls(b"\x00" * 20, 0) + ret.gasha_medal_shop_id = gasha_medal_shop_id + ret.purchase_num = purchase_num + return ret + + def make(self) -> bytes: + ret = encode_int(self.gasha_medal_shop_id) + ret += encode_int(self.purchase_num) + return ret + +class YuiMedalShopData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.yui_medal_shop_id = decode_int(data, offset + self._sz) + + name = decode_str(data, offset + self._sz) + self.name = name[0] + self._sz += name[1] + + description = decode_str(data, offset + self._sz) + self.description = description[0] + self._sz += description[1] + + self.selling_yui_medal = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + self.selling_col = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.selling_event_item_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.selling_event_item_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.selling_ticket_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.purchase_limit = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + self.pick_up_flag = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + self.product_category = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + self.sales_type = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + self.target_days = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + self.target_hour = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + self.interval_hour = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + + sales_start_date = decode_str(data, offset + self._sz) + self.sales_start_date = prs_dt(sales_start_date[0]) + self._sz += sales_start_date[1] + + sales_end_date = decode_str(data, offset + self._sz) + self.sales_end_date = prs_dt(sales_end_date[0]) + self._sz += sales_end_date[1] + + self.sort = decode_byte(data, offset + self._sz) + + @classmethod + def from_args(cls, shop_id: int = 0, name: str = "", desc: str = "") -> "YuiMedalShopData": + ret = cls(b"\x00" * 43, 0) + ret.yui_medal_shop_id = shop_id + ret.name = name + ret.description = desc + return ret + + def make(self) -> bytes: + ret = encode_int(self.yui_medal_shop_id) + ret += encode_str(self.name) + ret += encode_str(self.description) + ret += encode_short(self.selling_yui_medal) + ret += encode_int(self.selling_col) + ret += encode_int(self.selling_event_item_id) + ret += encode_int(self.selling_event_item_num) + ret += encode_int(self.selling_ticket_num) + ret += encode_short(self.purchase_limit) + ret += encode_byte(self.pick_up_flag) + ret += encode_byte(self.product_category) + ret += encode_byte(self.sales_type) + ret += encode_byte(self.target_days) + ret += encode_byte(self.target_hour) + ret += encode_byte(self.interval_hour) + ret += encode_str(fmt_dt(self.sales_start_date)) + ret += encode_str(fmt_dt(self.sales_end_date)) + ret += encode_byte(self.sort) + return ret + +class YuiMedalShopItemData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.yui_medal_shop_item_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.yui_medal_shop_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.common_reward_type = decode_byte(data, offset + self._sz) + self._sz += BYTE_OFF + self.common_reward_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.common_reward_num = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + self.strength = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property1_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property1_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property1_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property2_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property2_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property2_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property3_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property3_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property3_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + self.property4_property_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property4_value1 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.property4_value2 = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + @classmethod + def from_args(cls, item_id: int = 0, shop_id: int = 0, reward_type: int = 0, reward_id: int = 0, reward_num: int = 0, strength: int = 0) -> "YuiMedalShopItemData": + ret = cls(b"\x00" * 67, 0) + ret.yui_medal_shop_item_id = item_id + ret.yui_medal_shop_id = shop_id + ret.common_reward_type = reward_type + ret.common_reward_id = reward_id + ret.common_reward_num = reward_num + ret.strength = strength + return ret + + def make(self) -> bytes: + ret = encode_int(self.yui_medal_shop_item_id) + ret += encode_int(self.yui_medal_shop_id) + ret += encode_byte(self.common_reward_type) + ret += encode_int(self.common_reward_id) + ret += encode_short(self.common_reward_num) + ret += encode_int(self.strength) + + ret += encode_int(self.property1_property_id) + ret += encode_int(self.property1_value1) + ret += encode_int(self.property1_value2) + + ret += encode_int(self.property2_property_id) + ret += encode_int(self.property2_value1) + ret += encode_int(self.property2_value2) + + ret += encode_int(self.property3_property_id) + ret += encode_int(self.property3_value1) + ret += encode_int(self.property3_value2) + + ret += encode_int(self.property4_property_id) + ret += encode_int(self.property4_value1) + ret += encode_int(self.property4_value2) + return ret + +class ResEarnCampaignShop(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.res_earn_campaign_shop_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.res_earn_campaign_application_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + name = decode_str(data, offset + self._sz) + self.name = name[0] + self._sz += name[1] + + self.selling_yui_medal = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + self.selling_col = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.selling_event_item_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.selling_event_item_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.purchase_limit = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + self.get_application_point = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + sales_start_date = decode_str(data, offset + self._sz) + self.sales_start_date = prs_dt(sales_start_date[0]) + self._sz += sales_start_date[1] + + sales_end_date = decode_str(data, offset + self._sz) + self.sales_end_date = prs_dt(sales_end_date[0]) + self._sz += sales_end_date[1] + + @classmethod + def from_args(cls, shop_id: int = 0, app_id: int = 0, name: str = "") -> "ResEarnCampaignShop": + ret = cls(b"\x00" * 26, 0) + ret.res_earn_campaign_shop_id = shop_id + ret.res_earn_campaign_application_id = app_id + ret.name = name + return ret + + def make(self) -> bytes: + ret = encode_int(self.res_earn_campaign_shop_id) + ret = encode_int(self.res_earn_campaign_application_id) + ret += encode_str(self.name) + ret += encode_short(self.selling_yui_medal) + ret += encode_int(self.selling_col) + ret += encode_int(self.selling_event_item_id) + ret += encode_int(self.selling_event_item_num) + ret += encode_short(self.purchase_limit) + ret += encode_short(self.get_application_point) + ret += encode_str(fmt_dt(self.sales_start_date)) + ret += encode_str(fmt_dt(self.sales_end_date)) + return ret + +class GashaMedalShop(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + self.gasha_medal_shop_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + + name = decode_str(data, offset + self._sz) + self.name = name[0] + self._sz += name[1] + + self.gasha_medal_id = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.use_gasha_medal_num = decode_int(data, offset + self._sz) + self._sz += INT_OFF + self.purchase_limit = decode_short(data, offset + self._sz) + self._sz += SHORT_OFF + + sales_start_date = decode_str(data, offset + self._sz) + self.sales_start_date = prs_dt(sales_start_date[0]) + self._sz += sales_start_date[1] + + sales_end_date = decode_str(data, offset + self._sz) + self.sales_end_date = prs_dt(sales_end_date[0]) + self._sz += sales_end_date[1] + + @classmethod + def from_args(cls, shop_id: int = 0, name: str = "", medal_id: int = 0, medal_num: int = 0, purchase_limit: int = 0) -> "GashaMedalShop": + ret = cls(b"\x00" * 26, 0) + ret.gasha_medal_shop_id = shop_id + ret.name = name + ret.gasha_medal_id = medal_id + ret.use_gasha_medal_num = medal_num + ret.purchase_limit = purchase_limit + return ret + + def make(self) -> bytes: + ret = encode_int(self.gasha_medal_shop_id) + ret += encode_str(self.name) + ret += encode_int(self.gasha_medal_id) + ret += encode_int(self.use_gasha_medal_num) + ret += encode_short(self.purchase_limit) + ret += encode_str(fmt_dt(self.sales_start_date)) + ret += encode_str(fmt_dt(self.sales_end_date)) + return ret diff --git a/titles/sao/index.py b/titles/sao/index.py index f5c92b7..b70f02a 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -4,6 +4,9 @@ import yaml import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler from os import path +from Crypto.Cipher import Blowfish +from hashlib import md5 +import secrets from core import CoreConfig, Utils from core.title import BaseServlet @@ -47,11 +50,15 @@ class SaoServlet(BaseServlet): self.logger.inited = True self.base = SaoBase(core_cfg, self.game_cfg) + self.static_hash = None + + if self.game_cfg.hash.verify_hash: + self.static_hash = md5(self.game_cfg.hash.hash_base.encode()).digest() # Greate hashing guys, really validates the data def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: return ( [], - [("render_POST", "/SaoServlet/{datecode}/proto/if/{endpoint}", {})] + [("render_POST", "/{datecode}/proto/if/{category}/{endpoint}", {})] ) @classmethod @@ -69,10 +76,13 @@ class SaoServlet(BaseServlet): return True def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: - return ( - f"http://{self.game_cfg.server.hostname}:{self.game_cfg.server.port}/SaoServlet/", - f"{self.game_cfg.server.hostname}/SaoServlet/", - ) + if not self.core_cfg.server.is_using_proxy and Utils.get_title_port_ssl(self.core_cfg): + return ( + f"https://{self.core_cfg.title.hostname}:{self.core_cfg.title.port_ssl}/", + f"{self.core_cfg.title.hostname}/", + ) + + return (f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/", "") def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str]: if not self.game_cfg.server.enable: @@ -80,23 +90,62 @@ class SaoServlet(BaseServlet): return (True, "SAO1") - def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: endpoint = matchers.get('endpoint', '') request.responseHeaders.addRawHeader(b"content-type", b"text/html; charset=utf-8") + iv = b"" - sao_request = request.content.getvalue().hex() + req_raw = request.content.read() + sao_request = req_raw.hex() + req_header = SaoRequestHeader(req_raw) + + cmd_str = f"{req_header.cmd:04x}" + + if self.game_cfg.hash.verify_hash and self.static_hash != req_header.hash: + self.logger.error(f"Hash mismatch! Expecting {self.static_hash} but recieved {req_header.hash}") + return b"" + + if self.game_cfg.crypt.enable: + iv = req_raw[40:48] + cipher = Blowfish.new(self.game_cfg.crypt.key.encode(), Blowfish.MODE_CBC, iv) + crypt_data = req_raw[48:] + req_data = cipher.decrypt(crypt_data) + self.logger.debug(f"Decrypted {req_data.hex()} with IV {iv.hex()}") + + else: + req_data = req_raw[40:] - handler = getattr(self.base, f"handle_{sao_request[:4]}", None) - if handler is None: - self.logger.info(f"Generic Handler for {endpoint} - {sao_request[:4]}") - self.logger.debug(f"Request: {request.content.getvalue().hex()}") - resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(sao_request[:4]), "big")+1) - self.logger.debug(f"Response: {resp.make().hex()}") - return resp.make() + handler = getattr(self.base, f"handle_{cmd_str}", self.base.handle_noop) + self.logger.info(f"{endpoint} - {cmd_str} request") + self.logger.debug(f"Request: {req_raw.hex()}") + resp = handler(req_header, req_data) - self.logger.info(f"Handler {endpoint} - {sao_request[:4]} request") - self.logger.debug(f"Request: {request.content.getvalue().hex()}") - resp = handler(sao_request) + if resp is None: + resp = SaoNoopResponse(req_header.cmd + 1).make() + + if type(resp) == bytes: + pass + + elif issubclass(resp, SaoBaseResponse): + resp = resp.make() + + else: + self.logger.error(f"Unknown response type {type(resp)}") + return b"" + self.logger.debug(f"Response: {resp.hex()}") + + if self.game_cfg.crypt.enable: + iv = secrets.token_bytes(8) + data_to_crypt = resp[24:] + while len(data_to_crypt) % 8 != 0: + data_to_crypt += b"\x00" + + cipher = Blowfish.new(self.game_cfg.crypt.key.encode(), Blowfish.MODE_CBC, iv) + data_crypt = cipher.encrypt(data_to_crypt) + crypt_data_len = len(data_crypt) + len(iv) + tmp = struct.pack("!I", crypt_data_len) # does it want the length of the encrypted response?? + resp = resp[:20] + tmp + iv + data_crypt + self.logger.debug(f"Encrypted Response: {resp.hex()}") + return resp \ No newline at end of file diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 0c4cffc..dd3fe24 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -96,6 +96,7 @@ class WaccaBase: def handle_housing_start_request(self, data: Dict) -> Dict: req = HousingStartRequestV1(data) + allnet_region_id = None machine = self.data.arcade.get_machine(req.chipId) if machine is not None: @@ -235,6 +236,11 @@ class WaccaBase: if profileId is None: return BaseResponse().make() + + if profileId == 0: + # We've already made this profile, just return success + new_user = self.data.profile.get_profile(aime_id=req.aimeId) + profileId = new_user['id'] # Insert starting items self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104001) @@ -255,25 +261,11 @@ class WaccaBase: self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 105001 ) - self.data.item.put_item( - req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 205005 - ) # Added lily self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210001 ) - self.data.item.put_item( - req.aimeId, WaccaConstants.ITEM_TYPES["user_plate"], 211001 - ) # Added lily - - self.data.item.put_item( - req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312000 - ) # Added reverse - self.data.item.put_item( - req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312001 - ) # Added reverse - return UserStatusCreateResponseV2(profileId, req.username).make() def handle_user_status_getDetail_request(self, data: Dict) -> Dict: diff --git a/titles/wacca/handlers/housing.py b/titles/wacca/handlers/housing.py index f2f079e..e11c61a 100644 --- a/titles/wacca/handlers/housing.py +++ b/titles/wacca/handlers/housing.py @@ -32,6 +32,8 @@ class HousingStartRequestV1(BaseRequest): class HousingStartRequestV2(HousingStartRequestV1): def __init__(self, data: Dict) -> None: super(HousingStartRequestV1, self).__init__(data) + self.unknown0: str = self.params[0] + self.errorLog: str = self.params[1] self.creditLog: str = self.params[2] self.info: List[HousingInfo] = [] diff --git a/titles/wacca/index.py b/titles/wacca/index.py index d27dc8e..7277fd8 100644 --- a/titles/wacca/index.py +++ b/titles/wacca/index.py @@ -7,6 +7,8 @@ from hashlib import md5 from twisted.web.http import Request from typing import Dict, Tuple, List from os import path +import traceback +import sys from core import CoreConfig, Utils from .config import WaccaConfig @@ -17,7 +19,7 @@ from .lilyr import WaccaLilyR from .lily import WaccaLily from .s import WaccaS from .base import WaccaBase -from .handlers.base import BaseResponse +from .handlers.base import BaseResponse, BaseRequest from .handlers.helpers import Version @@ -114,10 +116,20 @@ class WaccaServlet: try: req_json = json.loads(request.content.getvalue()) version_full = Version(req_json["appVersion"]) + req = BaseRequest(req_json) - except Exception: + except KeyError as e: self.logger.error( - f"Failed to parse request to {url_path} -> {request.content.getvalue()}" + f"Failed to parse request to {request.content.getvalue()} -> Missing required value {e}" + ) + resp = BaseResponse() + resp.status = 1 + resp.message = "不正なリクエスト エラーです" + return end(resp.make()) + + except Exception as e: + self.logger.error( + f"Failed to parse request to {url_path} -> {request.content.getvalue()} -> {e}" ) resp = BaseResponse() resp.status = 1 @@ -143,7 +155,7 @@ class WaccaServlet: else: self.logger.warning( - f"Unsupported version ({req_json['appVersion']}) request {url_path} - {req_json}" + f"Unsupported version ({req.appVersion}) request {url_path} - {req_json}" ) resp = BaseResponse() resp.status = 1 @@ -151,7 +163,7 @@ class WaccaServlet: return end(resp.make()) self.logger.info( - f"v{req_json['appVersion']} {url_path} request from {client_ip} with chipId {req_json['chipId']}" + f"v{req.appVersion} {url_path} request from {client_ip} with chipId {req.chipId}" ) self.logger.debug(req_json) @@ -166,17 +178,20 @@ class WaccaServlet: handler = getattr(self.versions[internal_ver], func_to_find) resp = handler(req_json) - self.logger.debug(f"{req_json['appVersion']} response {resp}") + self.logger.debug(f"{req.appVersion} response {resp}") return end(resp) except Exception as e: self.logger.error( - f"{req_json['appVersion']} Error handling method {url_path} -> {e}" + f"{req.appVersion} Error handling method {url_path} -> {e}" ) - if self.core_cfg.server.is_develop: - raise + if self.logger.level == logging.DEBUG: + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb, limit=3) + with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "wacca"), "a") as f: + traceback.print_exception(tp, val, tb, limit=3, file=f) resp = BaseResponse() resp.status = 1 resp.message = "A server error occoured." - return end(resp) + return end(resp.make()) diff --git a/titles/wacca/lily.py b/titles/wacca/lily.py index 2e70299..33928ec 100644 --- a/titles/wacca/lily.py +++ b/titles/wacca/lily.py @@ -41,28 +41,28 @@ class WaccaLily(WaccaS): resp = GetNewsResponseV3() return resp.make() - def handle_housing_start_request(self, data: Dict) -> Dict: - req = HousingStartRequestV2(data) - - if req.appVersion.country != "JPN" and req.appVersion.country in [ - region.name for region in WaccaConstants.Region - ]: - region_id = WaccaConstants.Region[req.appVersion.country] - else: - region_id = self.region_id - - resp = HousingStartResponseV1(region_id) - return resp.make() - def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) - resp = super().handle_user_status_create_request(data) + ret = super().handle_user_status_create_request(data) + + new_user = self.data.profile.get_profile(aime_id=req.aimeId) + + if new_user is None: + return BaseResponse().make() + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["user_plate"], 211001 + ) # Added lily + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 205005 + ) # Added lily self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210002 ) # Lily, Added Lily - return resp + return ret def handle_user_status_get_request(self, data: Dict) -> Dict: req = UserStatusGetRequest(data) @@ -426,7 +426,7 @@ class WaccaLily(WaccaS): except Exception: self.logger.error( - f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}" + f"Failed to load item {item['item_id']} for user {profile['user']}" ) resp.seasonInfo.level = profile["xp"] diff --git a/titles/wacca/reverse.py b/titles/wacca/reverse.py index 557ac03..6206940 100644 --- a/titles/wacca/reverse.py +++ b/titles/wacca/reverse.py @@ -315,5 +315,12 @@ class WaccaReverse(WaccaLilyR): self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 310002 ) # Added reverse + + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312000 + ) # Added reverse + self.data.item.put_item( + req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312001 + ) # Added reverse return resp