forked from Hay1tsme/artemis
Merge branch 'develop' into diva_handler_classes
This commit is contained in:
commit
3b852ea739
@ -371,7 +371,7 @@ class AllnetPowerOnResponse3:
|
||||
self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ"
|
||||
)
|
||||
self.setting = ""
|
||||
self.setting = "1"
|
||||
self.res_ver = "3"
|
||||
self.token = str(token)
|
||||
|
||||
|
@ -1,99 +0,0 @@
|
||||
CREATE TABLE ongeki_user_gacha (
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
user INT NOT NULL,
|
||||
gachaId INT NOT NULL,
|
||||
totalGachaCnt INT DEFAULT 0,
|
||||
ceilingGachaCnt INT DEFAULT 0,
|
||||
selectPoint INT DEFAULT 0,
|
||||
useSelectPoint INT DEFAULT 0,
|
||||
dailyGachaCnt INT DEFAULT 0,
|
||||
fiveGachaCnt INT DEFAULT 0,
|
||||
elevenGachaCnt INT DEFAULT 0,
|
||||
dailyGachaDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT ongeki_user_gacha_uk UNIQUE (user, gachaId),
|
||||
CONSTRAINT ongeki_user_gacha_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user (id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE ongeki_user_gacha_supply (
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
user INT NOT NULL,
|
||||
cardId INT NOT NULL,
|
||||
CONSTRAINT ongeki_user_gacha_supply_uk UNIQUE (user, cardId),
|
||||
CONSTRAINT ongeki_user_gacha_supply_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user (id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE ongeki_static_gachas (
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
version INT NOT NULL,
|
||||
gachaId INT NOT NULL,
|
||||
gachaName VARCHAR(255) NOT NULL,
|
||||
kind INT NOT NULL,
|
||||
type INT DEFAULT 0,
|
||||
isCeiling BOOLEAN DEFAULT 0,
|
||||
maxSelectPoint INT DEFAULT 0,
|
||||
ceilingCnt INT DEFAULT 10,
|
||||
changeRateCnt1 INT DEFAULT 0,
|
||||
changeRateCnt2 INT DEFAULT 0,
|
||||
startDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0',
|
||||
endDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0',
|
||||
noticeStartDate TIMESTAMP DEFAULT '2018-01-01 00:00:00.0',
|
||||
noticeEndDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0',
|
||||
convertEndDate TIMESTAMP DEFAULT '2038-01-01 00:00:00.0',
|
||||
CONSTRAINT ongeki_static_gachas_uk UNIQUE (version, gachaId, gachaName)
|
||||
);
|
||||
|
||||
CREATE TABLE ongeki_static_gacha_cards (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
gachaId INT NOT NULL,
|
||||
cardId INT NOT NULL,
|
||||
rarity INT NOT NULL,
|
||||
weight INT DEFAULT 1,
|
||||
isPickup BOOLEAN DEFAULT 0,
|
||||
isSelect BOOLEAN DEFAULT 1,
|
||||
CONSTRAINT ongeki_static_gacha_cards_uk UNIQUE (gachaId, cardId)
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE ongeki_static_cards (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
version INT NOT NULL,
|
||||
cardId INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
charaId INT NOT NULL,
|
||||
nickName VARCHAR(255),
|
||||
school VARCHAR(255) NOT NULL,
|
||||
attribute VARCHAR(5) NOT NULL,
|
||||
gakunen VARCHAR(255) NOT NULL,
|
||||
rarity INT NOT NULL,
|
||||
levelParam VARCHAR(255) NOT NULL,
|
||||
skillId INT NOT NULL,
|
||||
choKaikaSkillId INT NOT NULL,
|
||||
cardNumber VARCHAR(255),
|
||||
CONSTRAINT ongeki_static_cards_uk UNIQUE (version, cardId)
|
||||
) CHARACTER SET utf8mb4;
|
||||
|
||||
CREATE TABLE ongeki_user_print_detail (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user INT NOT NULL,
|
||||
cardId INT NOT NULL,
|
||||
cardType INT DEFAULT 0,
|
||||
printDate TIMESTAMP NOT NULL,
|
||||
serialId VARCHAR(20) NOT NULL,
|
||||
placeId INT NOT NULL,
|
||||
clientId VARCHAR(11) NOT NULL,
|
||||
printerSerialId VARCHAR(20) NOT NULL,
|
||||
isHolograph BOOLEAN DEFAULT 0,
|
||||
isAutographed BOOLEAN DEFAULT 0,
|
||||
printOption1 BOOLEAN DEFAULT 1,
|
||||
printOption2 BOOLEAN DEFAULT 1,
|
||||
printOption3 BOOLEAN DEFAULT 1,
|
||||
printOption4 BOOLEAN DEFAULT 1,
|
||||
printOption5 BOOLEAN DEFAULT 1,
|
||||
printOption6 BOOLEAN DEFAULT 1,
|
||||
printOption7 BOOLEAN DEFAULT 1,
|
||||
printOption8 BOOLEAN DEFAULT 1,
|
||||
printOption9 BOOLEAN DEFAULT 1,
|
||||
printOption10 BOOLEAN DEFAULT 0,
|
||||
FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT ongeki_user_print_detail_uk UNIQUE (serialId)
|
||||
) CHARACTER SET utf8mb4;
|
21
core/data/schema/versions/SDEZ_3_upgrade.sql
Normal file
21
core/data/schema/versions/SDEZ_3_upgrade.sql
Normal file
@ -0,0 +1,21 @@
|
||||
ALTER TABLE mai2_item_card
|
||||
CHANGE COLUMN card_id cardId INT NOT NULL AFTER user,
|
||||
CHANGE COLUMN card_kind cardTypeId INT NOT NULL,
|
||||
CHANGE COLUMN chara_id charaId INT NOT NULL,
|
||||
CHANGE COLUMN map_id mapId INT NOT NULL,
|
||||
CHANGE COLUMN startDate startDate TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00',
|
||||
CHANGE COLUMN endDate endDate TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00';
|
||||
|
||||
ALTER TABLE mai2_item_item
|
||||
CHANGE COLUMN item_id itemId INT NOT NULL AFTER user,
|
||||
CHANGE COLUMN item_kind itemKind INT NOT NULL,
|
||||
CHANGE COLUMN is_valid isValid TINYINT(1) NOT NULL DEFAULT '1';
|
||||
|
||||
ALTER TABLE mai2_item_character
|
||||
CHANGE COLUMN character_id characterId INT NOT NULL,
|
||||
CHANGE COLUMN use_count useCount INT NOT NULL DEFAULT '0';
|
||||
|
||||
ALTER TABLE mai2_item_charge
|
||||
CHANGE COLUMN charge_id chargeId INT NOT NULL,
|
||||
CHANGE COLUMN purchase_date purchaseDate TIMESTAMP NOT NULL,
|
||||
CHANGE COLUMN valid_date validDate TIMESTAMP NOT NULL;
|
13
dbutils.py
13
dbutils.py
@ -2,7 +2,7 @@ import yaml
|
||||
import argparse
|
||||
from core.config import CoreConfig
|
||||
from core.data import Data
|
||||
from os import path
|
||||
from os import path, mkdir, access, W_OK
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Database utilities")
|
||||
@ -33,7 +33,18 @@ if __name__ == "__main__":
|
||||
cfg = CoreConfig()
|
||||
if path.exists(f"{args.config}/core.yaml"):
|
||||
cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
|
||||
|
||||
if not path.exists(cfg.server.log_dir):
|
||||
mkdir(cfg.server.log_dir)
|
||||
|
||||
if not access(cfg.server.log_dir, W_OK):
|
||||
print(
|
||||
f"Log directory {cfg.server.log_dir} NOT writable, please check permissions"
|
||||
)
|
||||
exit(1)
|
||||
|
||||
data = Data(cfg)
|
||||
|
||||
|
||||
if args.action == "create":
|
||||
data.create_database()
|
||||
|
351
docs/game_specific_info.md
Normal file
351
docs/game_specific_info.md
Normal file
@ -0,0 +1,351 @@
|
||||
# ARTEMiS Games Documentation
|
||||
|
||||
Below are all supported games with supported version ids in order to use
|
||||
the corresponding importer and database upgrades.
|
||||
|
||||
**Important: The described database upgrades are only required if you are using an old database schema, f.e. still
|
||||
using the megaime database. Clean installations always create the latest database structure!**
|
||||
|
||||
# Table of content
|
||||
|
||||
- [Supported Games](#Supported-Games)
|
||||
- [Chunithm](#Chunithm)
|
||||
- [crossbeats REV.](#crossbeats-REV)
|
||||
- [maimai DX](#maimai-DX)
|
||||
- [O.N.G.E.K.I.](#ONGEKI)
|
||||
- [Card Maker](#Card-Maker)
|
||||
- [WACCA](#WACCA)
|
||||
|
||||
|
||||
# Supported Games
|
||||
|
||||
Games listed below have been tested and confirmed working.
|
||||
|
||||
## Chunithm
|
||||
|
||||
### SDBT
|
||||
|
||||
| Version ID | Version Name |
|
||||
|------------|--------------------|
|
||||
| 0 | Chunithm |
|
||||
| 1 | Chunithm+ |
|
||||
| 2 | Chunithm Air |
|
||||
| 3 | Chunithm Air + |
|
||||
| 4 | Chunithm Star |
|
||||
| 5 | Chunithm Star + |
|
||||
| 6 | Chunithm Amazon |
|
||||
| 7 | Chunithm Amazon + |
|
||||
| 8 | Chunithm Crystal |
|
||||
| 9 | Chunithm Crystal + |
|
||||
| 10 | Chunithm Paradise |
|
||||
|
||||
### SDHD/SDBT
|
||||
|
||||
| Version ID | Version Name |
|
||||
|------------|-----------------|
|
||||
| 11 | Chunithm New!! |
|
||||
| 12 | Chunithm New!!+ |
|
||||
|
||||
|
||||
### Importer
|
||||
|
||||
In order to use the importer locate your game installation folder and execute:
|
||||
|
||||
```shell
|
||||
python read.py --series SDBT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
|
||||
```
|
||||
|
||||
The importer for Chunithm will import: Events, Music, Charge Items and Avatar Accesories.
|
||||
|
||||
### Database upgrade
|
||||
|
||||
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
|
||||
which version is the latest, f.e. `SDBT_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to
|
||||
perform all previous updates as well:
|
||||
|
||||
```shell
|
||||
python dbutils.py --game SDBT --version 2 upgrade
|
||||
python dbutils.py --game SDBT --version 3 upgrade
|
||||
```
|
||||
|
||||
## crossbeats REV.
|
||||
|
||||
### SDCA
|
||||
|
||||
| Version ID | Version Name |
|
||||
|------------|------------------------------------|
|
||||
| 0 | crossbeats REV. |
|
||||
| 1 | crossbeats REV. SUNRISE |
|
||||
| 2 | crossbeats REV. SUNRISE S2 |
|
||||
| 3 | crossbeats REV. SUNRISE S2 Omnimix |
|
||||
|
||||
### Importer
|
||||
|
||||
In order to use the importer you need to use the provided `Export.csv` file:
|
||||
|
||||
```shell
|
||||
python read.py --series SDCA --version <version ID> --binfolder titles/cxb/data
|
||||
```
|
||||
|
||||
The importer for crossbeats REV. will import Music.
|
||||
|
||||
### Config
|
||||
|
||||
Config file is located in `config/cxb.yaml`.
|
||||
|
||||
| Option | Info |
|
||||
|------------------------|------------------------------------------------------------|
|
||||
| `hostname` | Requires a proper `hostname` (not localhost!) to run |
|
||||
| `ssl_enable` | Enables/Disables the use of the `ssl_cert` and `ssl_key` |
|
||||
| `port` | Set your unsecure port number |
|
||||
| `port_secure` | Set your secure/SSL port number |
|
||||
| `ssl_cert`, `ssl_key` | Enter your SSL certificate (requires not self signed cert) |
|
||||
|
||||
|
||||
## maimai DX
|
||||
|
||||
### SDEZ
|
||||
|
||||
| Version ID | Version Name |
|
||||
|------------|-------------------------|
|
||||
| 0 | maimai DX |
|
||||
| 1 | maimai DX PLUS |
|
||||
| 2 | maimai DX Splash |
|
||||
| 3 | maimai DX Splash PLUS |
|
||||
| 4 | maimai DX Universe |
|
||||
| 5 | maimai DX Universe PLUS |
|
||||
|
||||
### Importer
|
||||
|
||||
In order to use the importer locate your game installation folder and execute:
|
||||
|
||||
```shell
|
||||
python read.py --series SDEZ --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
|
||||
```
|
||||
|
||||
The importer for maimai DX will import Events, Music and Tickets.
|
||||
|
||||
**NOTE: It is required to use the importer because the game will
|
||||
crash without it!**
|
||||
|
||||
### Database upgrade
|
||||
|
||||
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDEZ_2_upgrade.sql`. In order to upgrade to version 2 in this case you need to perform all previous updates as well:
|
||||
|
||||
```shell
|
||||
python dbutils.py --game SDEZ --version 2 upgrade
|
||||
```
|
||||
|
||||
## Hatsune Miku Project Diva
|
||||
|
||||
### SBZV
|
||||
|
||||
| Version ID | Version Name |
|
||||
|------------|---------------------------------|
|
||||
| 0 | Project Diva Arcade |
|
||||
| 1 | Project Diva Arcade Future Tone |
|
||||
|
||||
|
||||
### Importer
|
||||
|
||||
In order to use the importer locate your game installation folder and execute:
|
||||
|
||||
```shell
|
||||
python read.py --series SBZV --version <version ID> --binfolder /path/to/game/data/diva --optfolder /path/to/game/data/diva/mdata
|
||||
```
|
||||
|
||||
The importer for Project Diva Arcade will all required data in order to use
|
||||
the Shop, Modules and Customizations.
|
||||
|
||||
### Config
|
||||
|
||||
Config file is located in `config/diva.yaml`.
|
||||
|
||||
| Option | Info |
|
||||
|----------------------|-------------------------------------------------------------------------------------------------|
|
||||
| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased |
|
||||
| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased |
|
||||
|
||||
|
||||
### Database upgrade
|
||||
|
||||
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
|
||||
which version is the latest, f.e. `SBZV_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to
|
||||
perform all previous updates as well:
|
||||
|
||||
```shell
|
||||
python dbutils.py --game SBZV --version 2 upgrade
|
||||
python dbutils.py --game SBZV --version 3 upgrade
|
||||
python dbutils.py --game SBZV --version 4 upgrade
|
||||
```
|
||||
|
||||
## O.N.G.E.K.I.
|
||||
|
||||
### SDDT
|
||||
|
||||
| Version ID | Version Name |
|
||||
|------------|----------------------------|
|
||||
| 0 | O.N.G.E.K.I. |
|
||||
| 1 | O.N.G.E.K.I. + |
|
||||
| 2 | O.N.G.E.K.I. Summer |
|
||||
| 3 | O.N.G.E.K.I. Summer + |
|
||||
| 4 | O.N.G.E.K.I. Red |
|
||||
| 5 | O.N.G.E.K.I. Red + |
|
||||
| 6 | O.N.G.E.K.I. Bright |
|
||||
| 7 | O.N.G.E.K.I. Bright Memory |
|
||||
|
||||
|
||||
### Importer
|
||||
|
||||
In order to use the importer locate your game installation folder and execute:
|
||||
|
||||
```shell
|
||||
python read.py --series SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
|
||||
```
|
||||
|
||||
The importer for O.N.G.E.K.I. will all all Cards, Music and Events.
|
||||
|
||||
**NOTE: The Importer is required for Card Maker.**
|
||||
|
||||
### Config
|
||||
|
||||
Config file is located in `config/ongeki.yaml`.
|
||||
|
||||
| Option | Info |
|
||||
|------------------|----------------------------------------------------------------------------------------------------------------|
|
||||
| `enabled_gachas` | Enter all gacha IDs for Card Maker to work, other than default may not work due to missing cards added to them |
|
||||
|
||||
Note: 1149 and higher are only for Card Maker 1.35 and higher and will be ignored on lower versions.
|
||||
|
||||
### Database upgrade
|
||||
|
||||
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
|
||||
which version is the latest, f.e. `SDDT_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to
|
||||
perform all previous updates as well:
|
||||
|
||||
```shell
|
||||
python dbutils.py --game SDDT --version 2 upgrade
|
||||
python dbutils.py --game SDDT --version 3 upgrade
|
||||
python dbutils.py --game SDDT --version 4 upgrade
|
||||
```
|
||||
|
||||
## Card Maker
|
||||
|
||||
### SDED
|
||||
|
||||
| Version ID | Version Name |
|
||||
|------------|-----------------|
|
||||
| 0 | Card Maker 1.34 |
|
||||
| 1 | Card Maker 1.35 |
|
||||
|
||||
|
||||
### Support status
|
||||
|
||||
* Card Maker 1.34:
|
||||
* Chunithm New!!: Yes
|
||||
* maimai DX Universe: Yes
|
||||
* O.N.G.E.K.I. Bright: Yes
|
||||
|
||||
* Card Maker 1.35:
|
||||
* Chunithm New!!+: Yes
|
||||
* maimai DX Universe PLUS: Yes
|
||||
* O.N.G.E.K.I. Bright Memory: Yes
|
||||
|
||||
|
||||
### Importer
|
||||
|
||||
In order to use the importer you need to use the provided `.csv` files (which are required for O.N.G.E.K.I.) and the
|
||||
option folders:
|
||||
|
||||
```shell
|
||||
python read.py --series SDED --version <version ID> --binfolder titles/cm/cm_data --optfolder /path/to/cardmaker/option/folder
|
||||
```
|
||||
|
||||
**If you haven't already executed the O.N.G.E.K.I. importer, make sure you import all cards!**
|
||||
|
||||
```shell
|
||||
python read.py --series SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
|
||||
```
|
||||
|
||||
Also make sure to import all maimai and Chunithm data as well:
|
||||
|
||||
```shell
|
||||
python read.py --series SDED --version <version ID> --binfolder /path/to/cardmaker/CardMaker_Data
|
||||
```
|
||||
|
||||
The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai/Chunithm) and the hardcoded
|
||||
Cards for each Gacha (O.N.G.E.K.I. only).
|
||||
|
||||
**NOTE: Without executing the importer Card Maker WILL NOT work!**
|
||||
|
||||
|
||||
### O.N.G.E.K.I. Gachas
|
||||
|
||||
Gacha "無料ガチャ" can only pull from the free cards with the following probabilities: 94%: R, 5% SR and 1% chance of
|
||||
getting an SSR card
|
||||
|
||||
Gacha "無料ガチャ(SR確定)" can only pull from free SR cards with prob: 92% SR and 8% chance of getting an SSR card
|
||||
|
||||
Gacha "レギュラーガチャ" can pull from every card added to ongeki_static_cards with the following prob: 77% R, 20% SR
|
||||
and 3% chance of getting an SSR card
|
||||
|
||||
All other (limited) gachas can pull from every card added to ongeki_static_cards but with the promoted cards
|
||||
(click on the green button under the banner) having a 10 times higher chance to get pulled
|
||||
|
||||
### Chunithm Gachas
|
||||
|
||||
All cards in Chunithm (basically just the characters) have the same rarity to it just pulls randomly from all cards
|
||||
from a given gacha but made sure you cannot pull the same card twice in the same 5 times gacha roll.
|
||||
|
||||
### Notes
|
||||
|
||||
Card Maker 1.34 will only load an O.N.G.E.K.I. Bright profile (1.30). Card Maker 1.35 will only load an O.N.G.E.K.I.
|
||||
Bright Memory profile (1.35).
|
||||
The gachas inside the `ongeki.yaml` will make sure only the right gacha ids for the right CM version will be loaded.
|
||||
Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded for CM 1.35.
|
||||
|
||||
**NOTE: There is currently no way to load/use the (printed) maimai DX cards!**
|
||||
|
||||
## WACCA
|
||||
|
||||
### SDFE
|
||||
|
||||
| Version ID | Version Name |
|
||||
|------------|---------------|
|
||||
| 0 | WACCA |
|
||||
| 1 | WACCA S |
|
||||
| 2 | WACCA Lily |
|
||||
| 3 | WACCA Lily R |
|
||||
| 4 | WACCA Reverse |
|
||||
|
||||
|
||||
### Importer
|
||||
|
||||
In order to use the importer locate your game installation folder and execute:
|
||||
|
||||
```shell
|
||||
python read.py --series SDFE --version <version ID> --binfolder /path/to/game/WindowsNoEditor/Mercury/Content
|
||||
```
|
||||
|
||||
The importer for WACCA will import all Music data.
|
||||
|
||||
### Config
|
||||
|
||||
Config file is located in `config/wacca.yaml`.
|
||||
|
||||
| Option | Info |
|
||||
|--------------------|-----------------------------------------------------------------------------|
|
||||
| `always_vip` | Enables/Disables VIP, if disabled it needs to be purchased manually in game |
|
||||
| `infinite_tickets` | Always set the "unlock expert" tickets to 5 |
|
||||
| `infinite_wp` | Sets the user WP to `999999` |
|
||||
| `enabled_gates` | Enter all gate IDs which should be enabled in game |
|
||||
|
||||
|
||||
### Database upgrade
|
||||
|
||||
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDFE_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to perform all previous updates as well:
|
||||
|
||||
```shell
|
||||
python dbutils.py --game SDFE --version 2 upgrade
|
||||
python dbutils.py --game SDFE --version 3 upgrade
|
||||
```
|
12
index.py
12
index.py
@ -170,6 +170,12 @@ if __name__ == "__main__":
|
||||
if not path.exists(cfg.server.log_dir):
|
||||
mkdir(cfg.server.log_dir)
|
||||
|
||||
if not access(cfg.server.log_dir, W_OK):
|
||||
print(
|
||||
f"Log directory {cfg.server.log_dir} NOT writable, please check permissions"
|
||||
)
|
||||
exit(1)
|
||||
|
||||
logger = logging.getLogger("core")
|
||||
log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s"
|
||||
log_fmt = logging.Formatter(log_fmt_str)
|
||||
@ -189,12 +195,6 @@ if __name__ == "__main__":
|
||||
logger.setLevel(log_lv)
|
||||
coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str)
|
||||
|
||||
if not access(cfg.server.log_dir, W_OK):
|
||||
logger.error(
|
||||
f"Log directory {cfg.server.log_dir} NOT writable, please check permissions"
|
||||
)
|
||||
exit(1)
|
||||
|
||||
if not cfg.aimedb.key:
|
||||
logger.error("!!AIMEDB KEY BLANK, SET KEY IN CORE.YAML!!")
|
||||
exit(1)
|
||||
|
6
read.py
6
read.py
@ -4,7 +4,8 @@ import re
|
||||
import os
|
||||
import yaml
|
||||
from os import path
|
||||
import logging, coloredlogs
|
||||
import logging
|
||||
import coloredlogs
|
||||
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from typing import List, Optional
|
||||
@ -134,7 +135,8 @@ if __name__ == "__main__":
|
||||
|
||||
for dir, mod in titles.items():
|
||||
if args.series in mod.game_codes:
|
||||
handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra)
|
||||
handler = mod.reader(config, args.version,
|
||||
bin_arg, opt_arg, args.extra)
|
||||
handler.read()
|
||||
|
||||
logger.info("Done")
|
||||
|
@ -17,7 +17,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
||||
|
||||
+ Card Maker
|
||||
+ 1.34.xx
|
||||
+ 1.36.xx
|
||||
+ 1.35.xx
|
||||
|
||||
+ Ongeki
|
||||
+ All versions up to Bright Memory
|
||||
@ -36,5 +36,8 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
||||
## Setup guides
|
||||
Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md) and [ubuntu](docs/INSTALL_UBUNTU.md) to setup and run the server.
|
||||
|
||||
## Game specific information
|
||||
Read [Games specific info](docs/game_specific_info.md) for all supported games, importer settings, configuration option and database upgrades.
|
||||
|
||||
## Production guide
|
||||
See the [production guide](docs/prod.md) for running a production server.
|
||||
|
@ -588,3 +588,11 @@ class ChuniBase:
|
||||
|
||||
def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": "1"}
|
||||
|
||||
def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"userNetBattleData": {
|
||||
"recentNBSelectMusicList": []
|
||||
}
|
||||
}
|
@ -8,8 +8,10 @@ import inflection
|
||||
import string
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad
|
||||
from Crypto.Protocol.KDF import PBKDF2
|
||||
from Crypto.Hash import SHA1
|
||||
from os import path
|
||||
from typing import Tuple
|
||||
from typing import Tuple, Dict
|
||||
|
||||
from core import CoreConfig, Utils
|
||||
from titles.chuni.config import ChuniConfig
|
||||
@ -33,25 +35,26 @@ class ChuniServlet:
|
||||
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||
self.core_cfg = core_cfg
|
||||
self.game_cfg = ChuniConfig()
|
||||
self.hash_table: Dict[Dict[str, str]] = {}
|
||||
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
|
||||
self.game_cfg.update(
|
||||
yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"))
|
||||
)
|
||||
|
||||
self.versions = [
|
||||
ChuniBase(core_cfg, self.game_cfg),
|
||||
ChuniPlus(core_cfg, self.game_cfg),
|
||||
ChuniAir(core_cfg, self.game_cfg),
|
||||
ChuniAirPlus(core_cfg, self.game_cfg),
|
||||
ChuniStar(core_cfg, self.game_cfg),
|
||||
ChuniStarPlus(core_cfg, self.game_cfg),
|
||||
ChuniAmazon(core_cfg, self.game_cfg),
|
||||
ChuniAmazonPlus(core_cfg, self.game_cfg),
|
||||
ChuniCrystal(core_cfg, self.game_cfg),
|
||||
ChuniCrystalPlus(core_cfg, self.game_cfg),
|
||||
ChuniParadise(core_cfg, self.game_cfg),
|
||||
ChuniNew(core_cfg, self.game_cfg),
|
||||
ChuniNewPlus(core_cfg, self.game_cfg),
|
||||
ChuniBase,
|
||||
ChuniPlus,
|
||||
ChuniAir,
|
||||
ChuniAirPlus,
|
||||
ChuniStar,
|
||||
ChuniStarPlus,
|
||||
ChuniAmazon,
|
||||
ChuniAmazonPlus,
|
||||
ChuniCrystal,
|
||||
ChuniCrystalPlus,
|
||||
ChuniParadise,
|
||||
ChuniNew,
|
||||
ChuniNewPlus,
|
||||
]
|
||||
|
||||
self.logger = logging.getLogger("chuni")
|
||||
@ -79,6 +82,21 @@ class ChuniServlet:
|
||||
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||
)
|
||||
self.logger.inited = True
|
||||
|
||||
for version, keys in self.game_cfg.crypto.keys.items():
|
||||
if len(keys) < 3:
|
||||
continue
|
||||
|
||||
self.hash_table[version] = {}
|
||||
|
||||
method_list = [method for method in dir(self.versions[version]) if not method.startswith('__')]
|
||||
for method in method_list:
|
||||
method_fixed = inflection.camelize(method)[6:-7]
|
||||
hash = PBKDF2(method_fixed, bytes.fromhex(keys[2]), 128, count=44, hmac_hash_module=SHA1)
|
||||
|
||||
self.hash_table[version][hash.hex()] = method_fixed
|
||||
|
||||
self.logger.debug(f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}")
|
||||
|
||||
@classmethod
|
||||
def get_allnet_info(
|
||||
@ -103,7 +121,7 @@ class ChuniServlet:
|
||||
return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "")
|
||||
|
||||
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
|
||||
if url_path.lower() == "/ping":
|
||||
if url_path.lower() == "ping":
|
||||
return zlib.compress(b'{"returnCode": "1"}')
|
||||
|
||||
req_raw = request.content.getvalue()
|
||||
@ -144,25 +162,38 @@ class ChuniServlet:
|
||||
# If we get a 32 character long hex string, it's a hash and we're
|
||||
# doing encrypted. The likelyhood of false positives is low but
|
||||
# technically not 0
|
||||
endpoint = request.getHeader("User-Agent").split("#")[0]
|
||||
if internal_ver < ChuniConstants.VER_CHUNITHM_NEW:
|
||||
endpoint = request.getHeader("User-Agent").split("#")[0]
|
||||
|
||||
else:
|
||||
if internal_ver not in self.hash_table:
|
||||
self.logger.error(f"v{version} does not support encryption or no keys entered")
|
||||
return zlib.compress(b'{"stat": "0"}')
|
||||
|
||||
elif endpoint.lower() not in self.hash_table[internal_ver]:
|
||||
self.logger.error(f"No hash found for v{version} endpoint {endpoint}")
|
||||
return zlib.compress(b'{"stat": "0"}')
|
||||
|
||||
endpoint = self.hash_table[internal_ver][endpoint.lower()]
|
||||
|
||||
try:
|
||||
crypt = AES.new(
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][0]),
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]),
|
||||
AES.MODE_CBC,
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][1]),
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
|
||||
)
|
||||
|
||||
req_raw = crypt.decrypt(req_raw)
|
||||
|
||||
except:
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Failed to decrypt v{version} request to {endpoint} -> {req_raw}"
|
||||
f"Failed to decrypt v{version} request to {endpoint} -> {e}"
|
||||
)
|
||||
return zlib.compress(b'{"stat": "0"}')
|
||||
|
||||
encrtped = True
|
||||
|
||||
if not encrtped and self.game_cfg.crypto.encrypted_only:
|
||||
if not encrtped and self.game_cfg.crypto.encrypted_only and internal_ver >= ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS:
|
||||
self.logger.error(
|
||||
f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}"
|
||||
)
|
||||
@ -185,14 +216,15 @@ class ChuniServlet:
|
||||
self.logger.debug(req_data)
|
||||
|
||||
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
||||
handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg)
|
||||
|
||||
if not hasattr(self.versions[internal_ver], func_to_find):
|
||||
if not hasattr(handler_cls, func_to_find):
|
||||
self.logger.warning(f"Unhandled v{version} request {endpoint}")
|
||||
resp = {"returnCode": 1}
|
||||
|
||||
else:
|
||||
try:
|
||||
handler = getattr(self.versions[internal_ver], func_to_find)
|
||||
handler = getattr(handler_cls, func_to_find)
|
||||
resp = handler(req_data)
|
||||
|
||||
except Exception as e:
|
||||
@ -212,9 +244,9 @@ class ChuniServlet:
|
||||
padded = pad(zipped, 16)
|
||||
|
||||
crypt = AES.new(
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][0]),
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]),
|
||||
AES.MODE_CBC,
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][1]),
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
|
||||
)
|
||||
|
||||
return crypt.encrypt(padded)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from random import randint
|
||||
from typing import Dict
|
||||
|
||||
from core.config import CoreConfig
|
||||
@ -61,8 +61,8 @@ class ChuniNew(ChuniBase):
|
||||
}
|
||||
|
||||
def handle_remove_token_api_request(self, data: Dict) -> Dict:
|
||||
return { "returnCode": "1" }
|
||||
|
||||
return {"returnCode": "1"}
|
||||
|
||||
def handle_delete_token_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": "1"}
|
||||
|
||||
@ -122,11 +122,355 @@ class ChuniNew(ChuniBase):
|
||||
"playerLevel": profile["playerLevel"],
|
||||
"rating": profile["rating"],
|
||||
"headphone": profile["headphone"],
|
||||
"chargeState": 0,
|
||||
"userNameEx": "0",
|
||||
# Enables favorites and teams
|
||||
"chargeState": 1,
|
||||
"userNameEx": "",
|
||||
"banState": 0,
|
||||
"classEmblemMedal": profile["classEmblemMedal"],
|
||||
"classEmblemBase": profile["classEmblemBase"],
|
||||
"battleRankId": profile["battleRankId"],
|
||||
}
|
||||
return data1
|
||||
|
||||
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
p = self.data.profile.get_profile_data(data["userId"], self.version)
|
||||
if p is None:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"userName": p["userName"],
|
||||
"level": p["level"],
|
||||
"medal": p["medal"],
|
||||
"lastDataVersion": "2.00.00",
|
||||
"isLogin": False,
|
||||
}
|
||||
|
||||
def handle_printer_login_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": 1}
|
||||
|
||||
def handle_printer_logout_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": 1}
|
||||
|
||||
def handle_get_game_gacha_api_request(self, data: Dict) -> Dict:
|
||||
"""
|
||||
returns all current active banners (gachas)
|
||||
"""
|
||||
game_gachas = self.data.static.get_gachas(self.version)
|
||||
|
||||
# clean the database rows
|
||||
game_gacha_list = []
|
||||
for gacha in game_gachas:
|
||||
tmp = gacha._asdict()
|
||||
tmp.pop("id")
|
||||
tmp.pop("version")
|
||||
tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S")
|
||||
tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S")
|
||||
tmp["noticeStartDate"] = datetime.strftime(
|
||||
tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
tmp["noticeEndDate"] = datetime.strftime(
|
||||
tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
|
||||
game_gacha_list.append(tmp)
|
||||
|
||||
return {
|
||||
"length": len(game_gacha_list),
|
||||
"gameGachaList": game_gacha_list,
|
||||
# no clue
|
||||
"registIdList": [],
|
||||
}
|
||||
|
||||
def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict:
|
||||
"""
|
||||
returns all valid cards for a given gachaId
|
||||
"""
|
||||
game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"])
|
||||
|
||||
game_gacha_card_list = []
|
||||
for gacha_card in game_gacha_cards:
|
||||
tmp = gacha_card._asdict()
|
||||
tmp.pop("id")
|
||||
game_gacha_card_list.append(tmp)
|
||||
|
||||
return {
|
||||
"gachaId": data["gachaId"],
|
||||
"length": len(game_gacha_card_list),
|
||||
# check isPickup from the chuni_static_gachas?
|
||||
"isPickup": False,
|
||||
"gameGachaCardList": game_gacha_card_list,
|
||||
# again no clue
|
||||
"emissionList": [],
|
||||
"afterCalcList": [],
|
||||
"ssrBookCalcList": [],
|
||||
}
|
||||
|
||||
def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
|
||||
p = self.data.profile.get_profile_data(data["userId"], self.version)
|
||||
if p is None:
|
||||
return {}
|
||||
|
||||
profile = p._asdict()
|
||||
profile.pop("id")
|
||||
profile.pop("user")
|
||||
profile.pop("version")
|
||||
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"userData": profile,
|
||||
"userEmoney": [
|
||||
{
|
||||
"type": 0,
|
||||
"emoneyCredit": 100,
|
||||
"emoneyBrand": 1,
|
||||
"ext1": 0,
|
||||
"ext2": 0,
|
||||
"ext3": 0,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
def handle_get_user_gacha_api_request(self, data: Dict) -> Dict:
|
||||
user_gachas = self.data.item.get_user_gachas(data["userId"])
|
||||
if user_gachas is None:
|
||||
return {"userId": data["userId"], "length": 0, "userGachaList": []}
|
||||
|
||||
user_gacha_list = []
|
||||
for gacha in user_gachas:
|
||||
tmp = gacha._asdict()
|
||||
tmp.pop("id")
|
||||
tmp.pop("user")
|
||||
tmp["dailyGachaDate"] = datetime.strftime(tmp["dailyGachaDate"], "%Y-%m-%d")
|
||||
user_gacha_list.append(tmp)
|
||||
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": len(user_gacha_list),
|
||||
"userGachaList": user_gacha_list,
|
||||
}
|
||||
|
||||
def handle_get_user_printed_card_api_request(self, data: Dict) -> Dict:
|
||||
user_print_list = self.data.item.get_user_print_states(
|
||||
data["userId"], has_completed=True
|
||||
)
|
||||
if user_print_list is None:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": 0,
|
||||
"nextIndex": -1,
|
||||
"userPrintedCardList": [],
|
||||
}
|
||||
|
||||
print_list = []
|
||||
next_idx = int(data["nextIndex"])
|
||||
max_ct = int(data["maxCount"])
|
||||
|
||||
for x in range(next_idx, len(user_print_list)):
|
||||
tmp = user_print_list[x]._asdict()
|
||||
print_list.append(tmp["cardId"])
|
||||
|
||||
if len(user_print_list) >= max_ct:
|
||||
break
|
||||
|
||||
if len(user_print_list) >= max_ct:
|
||||
next_idx = next_idx + max_ct
|
||||
else:
|
||||
next_idx = -1
|
||||
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": len(print_list),
|
||||
"nextIndex": next_idx,
|
||||
"userPrintedCardList": print_list,
|
||||
}
|
||||
|
||||
def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
|
||||
user_id = data["userId"]
|
||||
|
||||
user_print_states = self.data.item.get_user_print_states(
|
||||
user_id, has_completed=False
|
||||
)
|
||||
|
||||
card_print_state_list = []
|
||||
for card in user_print_states:
|
||||
tmp = card._asdict()
|
||||
tmp["orderId"] = tmp["id"]
|
||||
tmp.pop("user")
|
||||
tmp["limitDate"] = datetime.strftime(tmp["limitDate"], "%Y-%m-%d")
|
||||
|
||||
card_print_state_list.append(tmp)
|
||||
|
||||
return {
|
||||
"userId": user_id,
|
||||
"length": len(card_print_state_list),
|
||||
"userCardPrintStateList": card_print_state_list,
|
||||
}
|
||||
|
||||
def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
|
||||
return super().handle_get_user_character_api_request(data)
|
||||
|
||||
def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
|
||||
return super().handle_get_user_item_api_request(data)
|
||||
|
||||
def handle_roll_gacha_api_request(self, data: Dict) -> Dict:
|
||||
"""
|
||||
Handle a gacha roll API request, with:
|
||||
gachaId: the gachaId where the cards should be pulled from
|
||||
times: the number of gacha rolls
|
||||
characterId: the character which the user wants
|
||||
"""
|
||||
gacha_id = data["gachaId"]
|
||||
num_rolls = data["times"]
|
||||
chara_id = data["characterId"]
|
||||
|
||||
rolled_cards = []
|
||||
|
||||
# characterId is set after 10 rolls, where the user can select a card
|
||||
# from all gameGachaCards, therefore the correct cardId for a given
|
||||
# characterId should be returned
|
||||
if chara_id != -1:
|
||||
# get the
|
||||
card = self.data.static.get_gacha_card_by_character(gacha_id, chara_id)
|
||||
|
||||
tmp = card._asdict()
|
||||
tmp.pop("id")
|
||||
|
||||
rolled_cards.append(tmp)
|
||||
else:
|
||||
gacha_cards = self.data.static.get_gacha_cards(gacha_id)
|
||||
|
||||
# get the card id for each roll
|
||||
for _ in range(num_rolls):
|
||||
# get the index from all possible cards
|
||||
card_idx = randint(0, len(gacha_cards) - 1)
|
||||
# remove the index from the cards so it wont get pulled again
|
||||
card = gacha_cards.pop(card_idx)
|
||||
|
||||
# remove the "id" fronm the card
|
||||
tmp = card._asdict()
|
||||
tmp.pop("id")
|
||||
|
||||
rolled_cards.append(tmp)
|
||||
|
||||
return {"length": len(rolled_cards), "gameGachaCardList": rolled_cards}
|
||||
|
||||
def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict:
|
||||
upsert = data["cmUpsertUserGacha"]
|
||||
user_id = data["userId"]
|
||||
place_id = data["placeId"]
|
||||
|
||||
# save the user data
|
||||
user_data = upsert["userData"]
|
||||
user_data.pop("rankUpChallengeResults")
|
||||
user_data.pop("userEmoney")
|
||||
|
||||
self.data.profile.put_profile_data(user_id, self.version, user_data)
|
||||
|
||||
# save the user gacha
|
||||
user_gacha = upsert["userGacha"]
|
||||
gacha_id = user_gacha["gachaId"]
|
||||
user_gacha.pop("gachaId")
|
||||
user_gacha.pop("dailyGachaDate")
|
||||
|
||||
self.data.item.put_user_gacha(user_id, gacha_id, user_gacha)
|
||||
|
||||
# save all user items
|
||||
if "userItemList" in upsert:
|
||||
for item in upsert["userItemList"]:
|
||||
self.data.item.put_item(user_id, item)
|
||||
|
||||
# add every gamegachaCard to database
|
||||
for card in upsert["gameGachaCardList"]:
|
||||
self.data.item.put_user_print_state(
|
||||
user_id,
|
||||
hasCompleted=False,
|
||||
placeId=place_id,
|
||||
cardId=card["cardId"],
|
||||
gachaId=card["gachaId"],
|
||||
)
|
||||
|
||||
# retrieve every game gacha card which has been added in order to get
|
||||
# the orderId for the next request
|
||||
user_print_states = self.data.item.get_user_print_states_by_gacha(
|
||||
user_id, gacha_id, has_completed=False
|
||||
)
|
||||
card_print_state_list = []
|
||||
for card in user_print_states:
|
||||
tmp = card._asdict()
|
||||
tmp["orderId"] = tmp["id"]
|
||||
tmp.pop("user")
|
||||
tmp["limitDate"] = datetime.strftime(tmp["limitDate"], "%Y-%m-%d")
|
||||
|
||||
card_print_state_list.append(tmp)
|
||||
|
||||
return {
|
||||
"returnCode": "1",
|
||||
"apiName": "CMUpsertUserGachaApi",
|
||||
"userCardPrintStateList": card_print_state_list,
|
||||
}
|
||||
|
||||
def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
|
||||
return {
|
||||
"returnCode": 1,
|
||||
"orderId": 0,
|
||||
"serialId": "11111111111111111111",
|
||||
"apiName": "CMUpsertUserPrintlogApi",
|
||||
}
|
||||
|
||||
def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
|
||||
user_print_detail = data["userPrintDetail"]
|
||||
user_id = data["userId"]
|
||||
|
||||
# generate random serial id
|
||||
serial_id = "".join([str(randint(0, 9)) for _ in range(20)])
|
||||
|
||||
# not needed because are either zero or unset
|
||||
user_print_detail.pop("orderId")
|
||||
user_print_detail.pop("printNumber")
|
||||
user_print_detail.pop("serialId")
|
||||
user_print_detail["printDate"] = datetime.strptime(
|
||||
user_print_detail["printDate"], "%Y-%m-%d"
|
||||
)
|
||||
|
||||
# add the entry to the user print table with the random serialId
|
||||
self.data.item.put_user_print_detail(user_id, serial_id, user_print_detail)
|
||||
|
||||
return {
|
||||
"returnCode": 1,
|
||||
"orderId": 0,
|
||||
"serialId": serial_id,
|
||||
"apiName": "CMUpsertUserPrintApi",
|
||||
}
|
||||
|
||||
def handle_cm_upsert_user_print_subtract_api_request(self, data: Dict) -> Dict:
|
||||
upsert = data["userCardPrintState"]
|
||||
user_id = data["userId"]
|
||||
place_id = data["placeId"]
|
||||
|
||||
# save all user items
|
||||
if "userItemList" in data:
|
||||
for item in data["userItemList"]:
|
||||
self.data.item.put_item(user_id, item)
|
||||
|
||||
# set the card print state to success and use the orderId as the key
|
||||
self.data.item.put_user_print_state(
|
||||
user_id,
|
||||
id=upsert["orderId"],
|
||||
hasCompleted=True
|
||||
)
|
||||
|
||||
return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"}
|
||||
|
||||
def handle_cm_upsert_user_print_cancel_api_request(self, data: Dict) -> Dict:
|
||||
order_ids = data["orderIdList"]
|
||||
user_id = data["userId"]
|
||||
|
||||
# set the card print state to success and use the orderId as the key
|
||||
for order_id in order_ids:
|
||||
self.data.item.put_user_print_state(
|
||||
user_id,
|
||||
id=order_id,
|
||||
hasCompleted=True
|
||||
)
|
||||
|
||||
return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"}
|
||||
|
@ -30,3 +30,10 @@ class ChuniNewPlus(ChuniNew):
|
||||
"reflectorUri"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
||||
return ret
|
||||
|
||||
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
user_data = super().handle_cm_get_user_preview_api_request(data)
|
||||
|
||||
# hardcode lastDataVersion for CardMaker 1.35
|
||||
user_data["lastDataVersion"] = "2.05.00"
|
||||
return user_data
|
||||
|
@ -114,6 +114,76 @@ map_area = Table(
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
gacha = Table(
|
||||
"chuni_item_gacha",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column(
|
||||
"user",
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("gachaId", Integer, nullable=False),
|
||||
Column("totalGachaCnt", Integer, server_default="0"),
|
||||
Column("ceilingGachaCnt", Integer, server_default="0"),
|
||||
Column("dailyGachaCnt", Integer, server_default="0"),
|
||||
Column("fiveGachaCnt", Integer, server_default="0"),
|
||||
Column("elevenGachaCnt", Integer, server_default="0"),
|
||||
Column("dailyGachaDate", TIMESTAMP, nullable=False, server_default=func.now()),
|
||||
UniqueConstraint("user", "gachaId", name="chuni_item_gacha_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
print_state = Table(
|
||||
"chuni_item_print_state",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column(
|
||||
"user",
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("hasCompleted", Boolean, nullable=False, server_default="0"),
|
||||
Column(
|
||||
"limitDate", TIMESTAMP, nullable=False, server_default="2038-01-01 00:00:00.0"
|
||||
),
|
||||
Column("placeId", Integer),
|
||||
Column("cardId", Integer),
|
||||
Column("gachaId", Integer),
|
||||
UniqueConstraint("id", "user", name="chuni_item_print_state_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
print_detail = Table(
|
||||
"chuni_item_print_detail",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column(
|
||||
"user",
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("cardId", Integer, nullable=False),
|
||||
Column("printDate", TIMESTAMP, nullable=False),
|
||||
Column("serialId", String(20), nullable=False),
|
||||
Column("placeId", Integer, nullable=False),
|
||||
Column("clientId", String(11), nullable=False),
|
||||
Column("printerSerialId", String(20), nullable=False),
|
||||
Column("printOption1", Boolean, server_default="0"),
|
||||
Column("printOption2", Boolean, server_default="0"),
|
||||
Column("printOption3", Boolean, server_default="0"),
|
||||
Column("printOption4", Boolean, server_default="0"),
|
||||
Column("printOption5", Boolean, server_default="0"),
|
||||
Column("printOption6", Boolean, server_default="0"),
|
||||
Column("printOption7", Boolean, server_default="0"),
|
||||
Column("printOption8", Boolean, server_default="0"),
|
||||
Column("printOption9", Boolean, server_default="0"),
|
||||
Column("printOption10", Boolean, server_default="0"),
|
||||
Column("created", String(255), server_default=""),
|
||||
UniqueConstraint("serialId", name="chuni_item_print_detail_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
|
||||
class ChuniItemData(BaseData):
|
||||
def put_character(self, user_id: int, character_data: Dict) -> Optional[int]:
|
||||
@ -235,3 +305,89 @@ class ChuniItemData(BaseData):
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]:
|
||||
sql = gacha.select(gacha.c.user == aime_id)
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
def put_user_gacha(
|
||||
self, aime_id: int, gacha_id: int, gacha_data: Dict
|
||||
) -> Optional[int]:
|
||||
sql = insert(gacha).values(user=aime_id, gachaId=gacha_id, **gacha_data)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(
|
||||
user=aime_id, gachaId=gacha_id, **gacha_data
|
||||
)
|
||||
result = self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
self.logger.warn(f"put_user_gacha: Failed to insert! aime_id: {aime_id}")
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
def get_user_print_states(
|
||||
self, aime_id: int, has_completed: bool = False
|
||||
) -> Optional[List[Row]]:
|
||||
sql = print_state.select(
|
||||
and_(
|
||||
print_state.c.user == aime_id,
|
||||
print_state.c.hasCompleted == has_completed
|
||||
)
|
||||
)
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
def get_user_print_states_by_gacha(
|
||||
self, aime_id: int, gacha_id: int, has_completed: bool = False
|
||||
) -> Optional[List[Row]]:
|
||||
sql = print_state.select(
|
||||
and_(
|
||||
print_state.c.user == aime_id,
|
||||
print_state.c.gachaId == gacha_id,
|
||||
print_state.c.hasCompleted == has_completed
|
||||
)
|
||||
)
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
def put_user_print_state(self, aime_id: int, **print_data) -> Optional[int]:
|
||||
sql = insert(print_state).values(user=aime_id, **print_data)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(user=aime_id, **print_data)
|
||||
result = self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
self.logger.warn(
|
||||
f"put_user_print_state: Failed to insert! aime_id: {aime_id}"
|
||||
)
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
def put_user_print_detail(
|
||||
self, aime_id: int, serial_id: str, user_print_data: Dict
|
||||
) -> Optional[int]:
|
||||
sql = insert(print_detail).values(
|
||||
user=aime_id, serialId=serial_id, **user_print_data
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(
|
||||
user=aime_id, **user_print_data
|
||||
)
|
||||
result = self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
self.logger.warn(
|
||||
f"put_user_print_detail: Failed to insert! aime_id: {aime_id}"
|
||||
)
|
||||
return None
|
||||
return result.lastrowid
|
@ -68,6 +68,60 @@ avatar = Table(
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
gachas = Table(
|
||||
"chuni_static_gachas",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("version", Integer, nullable=False),
|
||||
Column("gachaId", Integer, nullable=False),
|
||||
Column("gachaName", String(255), nullable=False),
|
||||
Column("type", Integer, nullable=False, server_default="0"),
|
||||
Column("kind", Integer, nullable=False, server_default="0"),
|
||||
Column("isCeiling", Boolean, server_default="0"),
|
||||
Column("ceilingCnt", Integer, server_default="10"),
|
||||
Column("changeRateCnt1", Integer, server_default="0"),
|
||||
Column("changeRateCnt2", Integer, server_default="0"),
|
||||
Column("startDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"),
|
||||
Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
|
||||
Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"),
|
||||
Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
|
||||
UniqueConstraint("version", "gachaId", "gachaName", name="chuni_static_gachas_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
cards = Table(
|
||||
"chuni_static_cards",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("version", Integer, nullable=False),
|
||||
Column("cardId", Integer, nullable=False),
|
||||
Column("charaName", String(255), nullable=False),
|
||||
Column("charaId", Integer, nullable=False),
|
||||
Column("presentName", String(255), nullable=False),
|
||||
Column("rarity", Integer, server_default="2"),
|
||||
Column("labelType", Integer, nullable=False),
|
||||
Column("difType", Integer, nullable=False),
|
||||
Column("miss", Integer, nullable=False),
|
||||
Column("combo", Integer, nullable=False),
|
||||
Column("chain", Integer, nullable=False),
|
||||
Column("skillName", String(255), nullable=False),
|
||||
UniqueConstraint("version", "cardId", name="chuni_static_cards_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
gacha_cards = Table(
|
||||
"chuni_static_gacha_cards",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("gachaId", Integer, nullable=False),
|
||||
Column("cardId", Integer, nullable=False),
|
||||
Column("rarity", Integer, nullable=False),
|
||||
Column("weight", Integer, server_default="1"),
|
||||
Column("isPickup", Boolean, server_default="0"),
|
||||
UniqueConstraint("gachaId", "cardId", name="chuni_static_gacha_cards_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
|
||||
class ChuniStaticData(BaseData):
|
||||
def put_event(
|
||||
@ -265,3 +319,112 @@ class ChuniStaticData(BaseData):
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
def put_gacha(
|
||||
self,
|
||||
version: int,
|
||||
gacha_id: int,
|
||||
gacha_name: int,
|
||||
**gacha_data,
|
||||
) -> Optional[int]:
|
||||
sql = insert(gachas).values(
|
||||
version=version,
|
||||
gachaId=gacha_id,
|
||||
gachaName=gacha_name,
|
||||
**gacha_data,
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(
|
||||
version=version,
|
||||
gachaId=gacha_id,
|
||||
gachaName=gacha_name,
|
||||
**gacha_data,
|
||||
)
|
||||
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
self.logger.warn(f"Failed to insert gacha! gacha_id {gacha_id}")
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
def get_gachas(self, version: int) -> Optional[List[Dict]]:
|
||||
sql = gachas.select(gachas.c.version <= version).order_by(
|
||||
gachas.c.gachaId.asc()
|
||||
)
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]:
|
||||
sql = gachas.select(
|
||||
and_(gachas.c.version <= version, gachas.c.gachaId == gacha_id)
|
||||
)
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
def put_gacha_card(
|
||||
self, gacha_id: int, card_id: int, **gacha_card
|
||||
) -> Optional[int]:
|
||||
sql = insert(gacha_cards).values(gachaId=gacha_id, cardId=card_id, **gacha_card)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(
|
||||
gachaId=gacha_id, cardId=card_id, **gacha_card
|
||||
)
|
||||
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
self.logger.warn(f"Failed to insert gacha card! gacha_id {gacha_id}")
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]:
|
||||
sql = gacha_cards.select(gacha_cards.c.gachaId == gacha_id)
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
def get_gacha_card_by_character(self, gacha_id: int, chara_id: int) -> Optional[Dict]:
|
||||
sql_sub = (
|
||||
select(cards.c.cardId)
|
||||
.filter(
|
||||
cards.c.charaId == chara_id
|
||||
)
|
||||
.scalar_subquery()
|
||||
)
|
||||
|
||||
# Perform the main query, also rename the resulting column to ranking
|
||||
sql = gacha_cards.select(and_(
|
||||
gacha_cards.c.gachaId == gacha_id,
|
||||
gacha_cards.c.cardId == sql_sub
|
||||
))
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]:
|
||||
sql = insert(cards).values(version=version, cardId=card_id, **card_data)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(**card_data)
|
||||
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
self.logger.warn(f"Failed to insert card! card_id {card_id}")
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
def get_card(self, version: int, card_id: int) -> Optional[Dict]:
|
||||
sql = cards.select(and_(cards.c.version <= version, cards.c.cardId == card_id))
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
@ -1,9 +1,11 @@
|
||||
from titles.cm.index import CardMakerServlet
|
||||
from titles.cm.const import CardMakerConstants
|
||||
from titles.cm.read import CardMakerReader
|
||||
from titles.cm.database import CardMakerData
|
||||
|
||||
index = CardMakerServlet
|
||||
reader = CardMakerReader
|
||||
database = CardMakerData
|
||||
|
||||
game_codes = [CardMakerConstants.GAME_CODE]
|
||||
|
||||
|
@ -11,10 +11,10 @@ from titles.cm.const import CardMakerConstants
|
||||
from titles.cm.config import CardMakerConfig
|
||||
|
||||
|
||||
class CardMaker136(CardMakerBase):
|
||||
class CardMaker135(CardMakerBase):
|
||||
def __init__(self, core_cfg: CoreConfig, game_cfg: CardMakerConfig) -> None:
|
||||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = CardMakerConstants.VER_CARD_MAKER_136
|
||||
self.version = CardMakerConstants.VER_CARD_MAKER_135
|
||||
|
||||
def handle_get_game_connect_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_connect_api_request(data)
|
||||
@ -26,13 +26,13 @@ class CardMaker136(CardMakerBase):
|
||||
ret["gameConnectList"][0]["titleUri"] = f"{uri}/SDHD/205/"
|
||||
ret["gameConnectList"][1]["titleUri"] = f"{uri}/SDEZ/125/"
|
||||
ret["gameConnectList"][2]["titleUri"] = f"{uri}/SDDT/135/"
|
||||
|
||||
|
||||
return ret
|
||||
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["dataVersion"] = "1.35.00"
|
||||
ret["gameSetting"]["ongekiCmVersion"] = "1.35.04"
|
||||
ret["gameSetting"]["ongekiCmVersion"] = "1.35.03"
|
||||
ret["gameSetting"]["chuniCmVersion"] = "2.05.00"
|
||||
ret["gameSetting"]["maimaiCmVersion"] = "1.25.00"
|
||||
return ret
|
@ -4,9 +4,9 @@ class CardMakerConstants:
|
||||
CONFIG_NAME = "cardmaker.yaml"
|
||||
|
||||
VER_CARD_MAKER = 0
|
||||
VER_CARD_MAKER_136 = 1
|
||||
VER_CARD_MAKER_135 = 1
|
||||
|
||||
VERSION_NAMES = ("Card Maker 1.34", "Card Maker 1.36")
|
||||
VERSION_NAMES = ("Card Maker 1.34", "Card Maker 1.35")
|
||||
|
||||
@classmethod
|
||||
def game_ver_to_string(cls, ver: int):
|
||||
|
8
titles/cm/database.py
Normal file
8
titles/cm/database.py
Normal file
@ -0,0 +1,8 @@
|
||||
from core.data import Data
|
||||
from core.config import CoreConfig
|
||||
|
||||
|
||||
class CardMakerData(Data):
|
||||
def __init__(self, cfg: CoreConfig) -> None:
|
||||
super().__init__(cfg)
|
||||
# empty Card Maker database
|
@ -15,7 +15,7 @@ from core.config import CoreConfig
|
||||
from titles.cm.config import CardMakerConfig
|
||||
from titles.cm.const import CardMakerConstants
|
||||
from titles.cm.base import CardMakerBase
|
||||
from titles.cm.cm136 import CardMaker136
|
||||
from titles.cm.cm135 import CardMaker135
|
||||
|
||||
|
||||
class CardMakerServlet:
|
||||
@ -29,7 +29,7 @@ class CardMakerServlet:
|
||||
|
||||
self.versions = [
|
||||
CardMakerBase(core_cfg, self.game_cfg),
|
||||
CardMaker136(core_cfg, self.game_cfg),
|
||||
CardMaker135(core_cfg, self.game_cfg),
|
||||
]
|
||||
|
||||
self.logger = logging.getLogger("cardmaker")
|
||||
@ -87,8 +87,8 @@ class CardMakerServlet:
|
||||
|
||||
if version >= 130 and version < 135: # Card Maker
|
||||
internal_ver = CardMakerConstants.VER_CARD_MAKER
|
||||
elif version >= 135 and version < 140: # Card Maker
|
||||
internal_ver = CardMakerConstants.VER_CARD_MAKER_136
|
||||
elif version >= 135 and version < 136: # Card Maker 1.35
|
||||
internal_ver = CardMakerConstants.VER_CARD_MAKER_135
|
||||
|
||||
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
||||
# If we get a 32 character long hex string, it's a hash and we're
|
||||
|
@ -12,6 +12,10 @@ from titles.ongeki.database import OngekiData
|
||||
from titles.cm.const import CardMakerConstants
|
||||
from titles.ongeki.const import OngekiConstants
|
||||
from titles.ongeki.config import OngekiConfig
|
||||
from titles.mai2.database import Mai2Data
|
||||
from titles.mai2.const import Mai2Constants
|
||||
from titles.chuni.database import ChuniData
|
||||
from titles.chuni.const import ChuniConstants
|
||||
|
||||
|
||||
class CardMakerReader(BaseReader):
|
||||
@ -25,6 +29,8 @@ class CardMakerReader(BaseReader):
|
||||
) -> None:
|
||||
super().__init__(config, version, bin_dir, opt_dir, extra)
|
||||
self.ongeki_data = OngekiData(config)
|
||||
self.mai2_data = Mai2Data(config)
|
||||
self.chuni_data = ChuniData(config)
|
||||
|
||||
try:
|
||||
self.logger.info(
|
||||
@ -34,15 +40,29 @@ class CardMakerReader(BaseReader):
|
||||
self.logger.error(f"Invalid Card Maker version {version}")
|
||||
exit(1)
|
||||
|
||||
def _get_card_maker_directory(self, directory: str) -> str:
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for dir in dirs:
|
||||
if (
|
||||
os.path.exists(f"{root}/{dir}/MU3")
|
||||
and os.path.exists(f"{root}/{dir}/MAI")
|
||||
and os.path.exists(f"{root}/{dir}/CHU")
|
||||
):
|
||||
return f"{root}/{dir}"
|
||||
|
||||
def read(self) -> None:
|
||||
static_datas = {
|
||||
"static_gachas.csv": "read_ongeki_gacha_csv",
|
||||
"static_gacha_cards.csv": "read_ongeki_gacha_card_csv",
|
||||
}
|
||||
|
||||
data_dirs = []
|
||||
|
||||
if self.bin_dir is not None:
|
||||
data_dir = self._get_card_maker_directory(self.bin_dir)
|
||||
|
||||
self.read_chuni_card(f"{data_dir}/CHU/Data/A000/card")
|
||||
self.read_chuni_gacha(f"{data_dir}/CHU/Data/A000/gacha")
|
||||
|
||||
self.read_mai2_card(f"{data_dir}/MAI/Data/A000/card")
|
||||
for file, func in static_datas.items():
|
||||
if os.path.exists(f"{self.bin_dir}/MU3/{file}"):
|
||||
read_csv = getattr(CardMakerReader, func)
|
||||
@ -53,13 +73,163 @@ class CardMakerReader(BaseReader):
|
||||
)
|
||||
|
||||
if self.opt_dir is not None:
|
||||
data_dirs += self.get_data_directories(self.opt_dir)
|
||||
data_dirs = self.get_data_directories(self.opt_dir)
|
||||
|
||||
# ONGEKI (MU3) cnnot easily access the bin data(A000.pac)
|
||||
# so only opt_dir will work for now
|
||||
for dir in data_dirs:
|
||||
self.read_chuni_card(f"{dir}/CHU/card")
|
||||
self.read_chuni_gacha(f"{dir}/CHU/gacha")
|
||||
|
||||
self.read_ongeki_gacha(f"{dir}/MU3/gacha")
|
||||
|
||||
def read_chuni_card(self, base_dir: str) -> None:
|
||||
self.logger.info(f"Reading cards from {base_dir}...")
|
||||
|
||||
version_ids = {
|
||||
"v2_00": ChuniConstants.VER_CHUNITHM_NEW,
|
||||
"v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS,
|
||||
# Chunithm SUN, ignore for now
|
||||
"v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1
|
||||
}
|
||||
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
for dir in dirs:
|
||||
if os.path.exists(f"{root}/{dir}/Card.xml"):
|
||||
with open(f"{root}/{dir}/Card.xml", "r", encoding="utf-8") as f:
|
||||
troot = ET.fromstring(f.read())
|
||||
|
||||
card_id = int(troot.find("name").find("id").text)
|
||||
|
||||
chara_name = troot.find("chuniCharaName").find("str").text
|
||||
chara_id = troot.find("chuniCharaName").find("id").text
|
||||
version = version_ids[
|
||||
troot.find("netOpenName").find("str").text[:5]
|
||||
]
|
||||
present_name = troot.find("chuniPresentName").find("str").text
|
||||
rarity = int(troot.find("rareType").text)
|
||||
label = int(troot.find("labelType").text)
|
||||
dif = int(troot.find("difType").text)
|
||||
miss = int(troot.find("miss").text)
|
||||
combo = int(troot.find("combo").text)
|
||||
chain = int(troot.find("chain").text)
|
||||
skill_name = troot.find("skillName").text
|
||||
|
||||
self.chuni_data.static.put_card(
|
||||
version,
|
||||
card_id,
|
||||
charaName=chara_name,
|
||||
charaId=chara_id,
|
||||
presentName=present_name,
|
||||
rarity=rarity,
|
||||
labelType=label,
|
||||
difType=dif,
|
||||
miss=miss,
|
||||
combo=combo,
|
||||
chain=chain,
|
||||
skillName=skill_name,
|
||||
)
|
||||
|
||||
self.logger.info(f"Added chuni card {card_id}")
|
||||
|
||||
def read_chuni_gacha(self, base_dir: str) -> None:
|
||||
self.logger.info(f"Reading gachas from {base_dir}...")
|
||||
|
||||
version_ids = {
|
||||
"v2_00": ChuniConstants.VER_CHUNITHM_NEW,
|
||||
"v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS,
|
||||
# Chunithm SUN, ignore for now
|
||||
"v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1,
|
||||
}
|
||||
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
for dir in dirs:
|
||||
if os.path.exists(f"{root}/{dir}/Gacha.xml"):
|
||||
with open(f"{root}/{dir}/Gacha.xml", "r", encoding="utf-8") as f:
|
||||
troot = ET.fromstring(f.read())
|
||||
|
||||
name = troot.find("gachaName").text
|
||||
gacha_id = int(troot.find("name").find("id").text)
|
||||
|
||||
version = version_ids[
|
||||
troot.find("netOpenName").find("str").text[:5]
|
||||
]
|
||||
ceiling_cnt = int(troot.find("ceilingNum").text)
|
||||
gacha_type = int(troot.find("gachaType").text)
|
||||
is_ceiling = (
|
||||
True if troot.find("ceilingType").text == "1" else False
|
||||
)
|
||||
|
||||
self.chuni_data.static.put_gacha(
|
||||
version,
|
||||
gacha_id,
|
||||
name,
|
||||
type=gacha_type,
|
||||
isCeiling=is_ceiling,
|
||||
ceilingCnt=ceiling_cnt,
|
||||
)
|
||||
|
||||
self.logger.info(f"Added chuni gacha {gacha_id}")
|
||||
|
||||
for gacha_card in troot.find("infos").iter("GachaCardDataInfo"):
|
||||
# get the card ID from the id element
|
||||
card_id = gacha_card.find("cardName").find("id").text
|
||||
|
||||
# get the weight from the weight element
|
||||
weight = int(gacha_card.find("weight").text)
|
||||
|
||||
# get the pickup flag from the pickup element
|
||||
is_pickup = (
|
||||
True if gacha_card.find("pickup").text == "1" else False
|
||||
)
|
||||
|
||||
self.chuni_data.static.put_gacha_card(
|
||||
gacha_id,
|
||||
card_id,
|
||||
weight=weight,
|
||||
rarity=2,
|
||||
isPickup=is_pickup,
|
||||
)
|
||||
|
||||
self.logger.info(
|
||||
f"Added chuni card {card_id} to gacha {gacha_id}"
|
||||
)
|
||||
|
||||
def read_mai2_card(self, base_dir: str) -> None:
|
||||
self.logger.info(f"Reading cards from {base_dir}...")
|
||||
|
||||
version_ids = {
|
||||
"1.00": Mai2Constants.VER_MAIMAI_DX,
|
||||
"1.05": Mai2Constants.VER_MAIMAI_DX_PLUS,
|
||||
"1.09": Mai2Constants.VER_MAIMAI_DX_PLUS,
|
||||
"1.10": Mai2Constants.VER_MAIMAI_DX_SPLASH,
|
||||
"1.15": Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS,
|
||||
"1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE,
|
||||
"1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS,
|
||||
}
|
||||
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
for dir in dirs:
|
||||
if os.path.exists(f"{root}/{dir}/Card.xml"):
|
||||
with open(f"{root}/{dir}/Card.xml", "r", encoding="utf-8") as f:
|
||||
troot = ET.fromstring(f.read())
|
||||
|
||||
name = troot.find("name").find("str").text
|
||||
card_id = int(troot.find("name").find("id").text)
|
||||
|
||||
version = version_ids[
|
||||
troot.find("enableVersion").find("str").text
|
||||
]
|
||||
|
||||
enabled = (
|
||||
True if troot.find("disable").text == "false" else False
|
||||
)
|
||||
|
||||
self.mai2_data.static.put_card(
|
||||
version, card_id, name, enabled=enabled
|
||||
)
|
||||
self.logger.info(f"Added mai2 card {card_id}")
|
||||
|
||||
def read_ongeki_gacha_csv(self, file_path: str) -> None:
|
||||
self.logger.info(f"Reading gachas from {file_path}...")
|
||||
|
||||
@ -76,7 +246,7 @@ class CardMakerReader(BaseReader):
|
||||
maxSelectPoint=row["maxSelectPoint"],
|
||||
)
|
||||
|
||||
self.logger.info(f"Added gacha {row['gachaId']}")
|
||||
self.logger.info(f"Added ongeki gacha {row['gachaId']}")
|
||||
|
||||
def read_ongeki_gacha_card_csv(self, file_path: str) -> None:
|
||||
self.logger.info(f"Reading gacha cards from {file_path}...")
|
||||
@ -93,7 +263,7 @@ class CardMakerReader(BaseReader):
|
||||
isSelect=True if row["isSelect"] == "1" else False,
|
||||
)
|
||||
|
||||
self.logger.info(f"Added card {row['cardId']} to gacha")
|
||||
self.logger.info(f"Added ongeki card {row['cardId']} to gacha")
|
||||
|
||||
def read_ongeki_gacha(self, base_dir: str) -> None:
|
||||
self.logger.info(f"Reading gachas from {base_dir}...")
|
||||
@ -152,4 +322,4 @@ class CardMakerReader(BaseReader):
|
||||
isCeiling=is_ceiling,
|
||||
maxSelectPoint=max_select_point,
|
||||
)
|
||||
self.logger.info(f"Added gacha {gacha_id}")
|
||||
self.logger.info(f"Added ongeki gacha {gacha_id}")
|
||||
|
1
titles/cm/schema/__init__.py
Normal file
1
titles/cm/schema/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
__all__ = []
|
@ -7,4 +7,4 @@ index = Mai2Servlet
|
||||
database = Mai2Data
|
||||
reader = Mai2Reader
|
||||
game_codes = [Mai2Constants.GAME_CODE]
|
||||
current_schema_version = 2
|
||||
current_schema_version = 3
|
||||
|
@ -202,6 +202,16 @@ class Mai2Base:
|
||||
for act in v:
|
||||
self.data.profile.put_profile_activity(user_id, act)
|
||||
|
||||
if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0:
|
||||
for charge in upsert["userChargeList"]:
|
||||
self.data.item.put_charge(
|
||||
user_id,
|
||||
charge["chargeId"],
|
||||
charge["stock"],
|
||||
datetime.strptime(charge["purchaseDate"], "%Y-%m-%d %H:%M:%S"),
|
||||
datetime.strptime(charge["validDate"], "%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
|
||||
if upsert["isNewCharacterList"] and int(upsert["isNewCharacterList"]) > 0:
|
||||
for char in upsert["userCharacterList"]:
|
||||
self.data.item.put_character(
|
||||
@ -299,10 +309,67 @@ class Mai2Base:
|
||||
return {"userId": data["userId"], "userOption": options_dict}
|
||||
|
||||
def handle_get_user_card_api_request(self, data: Dict) -> Dict:
|
||||
return {"userId": data["userId"], "nextIndex": 0, "userCardList": []}
|
||||
user_cards = self.data.item.get_cards(data["userId"])
|
||||
if user_cards is None:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"nextIndex": 0,
|
||||
"userCardList": []
|
||||
}
|
||||
|
||||
max_ct = data["maxCount"]
|
||||
next_idx = data["nextIndex"]
|
||||
start_idx = next_idx
|
||||
end_idx = max_ct + start_idx
|
||||
|
||||
if len(user_cards[start_idx:]) > max_ct:
|
||||
next_idx += max_ct
|
||||
else:
|
||||
next_idx = 0
|
||||
|
||||
card_list = []
|
||||
for card in user_cards:
|
||||
tmp = card._asdict()
|
||||
tmp.pop("id")
|
||||
tmp.pop("user")
|
||||
tmp["startDate"] = datetime.strftime(
|
||||
tmp["startDate"], "%Y-%m-%d %H:%M:%S")
|
||||
tmp["endDate"] = datetime.strftime(
|
||||
tmp["endDate"], "%Y-%m-%d %H:%M:%S")
|
||||
card_list.append(tmp)
|
||||
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"nextIndex": next_idx,
|
||||
"userCardList": card_list[start_idx:end_idx]
|
||||
}
|
||||
|
||||
def handle_get_user_charge_api_request(self, data: Dict) -> Dict:
|
||||
return {"userId": data["userId"], "length": 0, "userChargeList": []}
|
||||
user_charges = self.data.item.get_charges(data["userId"])
|
||||
if user_charges is None:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": 0,
|
||||
"userChargeList": []
|
||||
}
|
||||
|
||||
user_charge_list = []
|
||||
for charge in user_charges:
|
||||
tmp = charge._asdict()
|
||||
tmp.pop("id")
|
||||
tmp.pop("user")
|
||||
tmp["purchaseDate"] = datetime.strftime(
|
||||
tmp["purchaseDate"], "%Y-%m-%d %H:%M:%S")
|
||||
tmp["validDate"] = datetime.strftime(
|
||||
tmp["validDate"], "%Y-%m-%d %H:%M:%S")
|
||||
|
||||
user_charge_list.append(tmp)
|
||||
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": len(user_charge_list),
|
||||
"userChargeList": user_charge_list
|
||||
}
|
||||
|
||||
def handle_get_user_item_api_request(self, data: Dict) -> Dict:
|
||||
kind = int(data["nextIndex"] / 10000000000)
|
||||
@ -313,15 +380,13 @@ class Mai2Base:
|
||||
|
||||
for x in range(next_idx, data["maxCount"]):
|
||||
try:
|
||||
user_item_list.append(
|
||||
{
|
||||
"item_kind": user_items[x]["item_kind"],
|
||||
"item_id": user_items[x]["item_id"],
|
||||
"stock": user_items[x]["stock"],
|
||||
"isValid": user_items[x]["is_valid"],
|
||||
}
|
||||
)
|
||||
except:
|
||||
user_item_list.append({
|
||||
"itemKind": user_items[x]["itemKind"],
|
||||
"itemId": user_items[x]["itemId"],
|
||||
"stock": user_items[x]["stock"],
|
||||
"isValid": user_items[x]["isValid"]
|
||||
})
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
if len(user_item_list) == data["maxCount"]:
|
||||
@ -332,21 +397,18 @@ class Mai2Base:
|
||||
"userId": data["userId"],
|
||||
"nextIndex": next_idx,
|
||||
"itemKind": kind,
|
||||
"userItemList": user_item_list,
|
||||
"userItemList": user_item_list
|
||||
}
|
||||
|
||||
def handle_get_user_character_api_request(self, data: Dict) -> Dict:
|
||||
characters = self.data.item.get_characters(data["userId"])
|
||||
|
||||
chara_list = []
|
||||
for chara in characters:
|
||||
chara_list.append(
|
||||
{
|
||||
"characterId": chara["character_id"],
|
||||
"level": chara["level"],
|
||||
"awakening": chara["awakening"],
|
||||
"useCount": chara["use_count"],
|
||||
}
|
||||
)
|
||||
tmp = chara._asdict()
|
||||
tmp.pop("id")
|
||||
tmp.pop("user")
|
||||
chara_list.append(tmp)
|
||||
|
||||
return {"userId": data["userId"], "userCharacterList": chara_list}
|
||||
|
||||
@ -417,10 +479,21 @@ class Mai2Base:
|
||||
tmp.pop("user")
|
||||
mlst.append(tmp)
|
||||
|
||||
return {"userActivity": {"playList": plst, "musicList": mlst}}
|
||||
return {
|
||||
"userActivity": {
|
||||
"playList": plst,
|
||||
"musicList": mlst
|
||||
}
|
||||
}
|
||||
|
||||
def handle_get_user_course_api_request(self, data: Dict) -> Dict:
|
||||
user_courses = self.data.score.get_courses(data["userId"])
|
||||
if user_courses is None:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"nextIndex": 0,
|
||||
"userCourseList": []
|
||||
}
|
||||
|
||||
course_list = []
|
||||
for course in user_courses:
|
||||
@ -429,7 +502,11 @@ class Mai2Base:
|
||||
tmp.pop("id")
|
||||
course_list.append(tmp)
|
||||
|
||||
return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list}
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"nextIndex": 0,
|
||||
"userCourseList": course_list
|
||||
}
|
||||
|
||||
def handle_get_user_portrait_api_request(self, data: Dict) -> Dict:
|
||||
# No support for custom pfps
|
||||
@ -540,18 +617,11 @@ class Mai2Base:
|
||||
|
||||
if songs is not None:
|
||||
for song in songs:
|
||||
music_detail_list.append(
|
||||
{
|
||||
"musicId": song["song_id"],
|
||||
"level": song["chart_id"],
|
||||
"playCount": song["play_count"],
|
||||
"achievement": song["achievement"],
|
||||
"comboStatus": song["combo_status"],
|
||||
"syncStatus": song["sync_status"],
|
||||
"deluxscoreMax": song["dx_score"],
|
||||
"scoreRank": song["score_rank"],
|
||||
}
|
||||
)
|
||||
tmp = song._asdict()
|
||||
tmp.pop("id")
|
||||
tmp.pop("user")
|
||||
music_detail_list.append(tmp)
|
||||
|
||||
if len(music_detail_list) == data["maxCount"]:
|
||||
next_index = data["maxCount"] + data["nextIndex"]
|
||||
break
|
||||
@ -559,5 +629,5 @@ class Mai2Base:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"nextIndex": next_index,
|
||||
"userMusicList": [{"userMusicDetailList": music_detail_list}],
|
||||
"userMusicList": [{"userMusicDetailList": music_detail_list}]
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ class Mai2Servlet:
|
||||
)
|
||||
|
||||
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
|
||||
if url_path.lower() == "/ping":
|
||||
if url_path.lower() == "ping":
|
||||
return zlib.compress(b'{"returnCode": "1"}')
|
||||
|
||||
req_raw = request.content.getvalue()
|
||||
|
@ -1,5 +1,6 @@
|
||||
from core.data.schema import BaseData, metadata
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, List
|
||||
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
|
||||
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
|
||||
@ -17,11 +18,11 @@ character = Table(
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("character_id", Integer, nullable=False),
|
||||
Column("characterId", Integer, nullable=False),
|
||||
Column("level", Integer, nullable=False, server_default="1"),
|
||||
Column("awakening", Integer, nullable=False, server_default="0"),
|
||||
Column("use_count", Integer, nullable=False, server_default="0"),
|
||||
UniqueConstraint("user", "character_id", name="mai2_item_character_uk"),
|
||||
Column("useCount", Integer, nullable=False, server_default="0"),
|
||||
UniqueConstraint("user", "characterId", name="mai2_item_character_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
@ -34,13 +35,13 @@ card = Table(
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("card_kind", Integer, nullable=False),
|
||||
Column("card_id", Integer, nullable=False),
|
||||
Column("chara_id", Integer, nullable=False),
|
||||
Column("map_id", Integer, nullable=False),
|
||||
Column("start_date", String(255), nullable=False),
|
||||
Column("end_date", String(255), nullable=False),
|
||||
UniqueConstraint("user", "card_kind", "card_id", name="mai2_item_card_uk"),
|
||||
Column("cardId", Integer, nullable=False),
|
||||
Column("cardTypeId", Integer, nullable=False),
|
||||
Column("charaId", Integer, nullable=False),
|
||||
Column("mapId", Integer, nullable=False),
|
||||
Column("startDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"),
|
||||
Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
|
||||
UniqueConstraint("user", "cardId", "cardTypeId", name="mai2_item_card_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
@ -53,11 +54,11 @@ item = Table(
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("item_kind", Integer, nullable=False),
|
||||
Column("item_id", Integer, nullable=False),
|
||||
Column("itemId", Integer, nullable=False),
|
||||
Column("itemKind", Integer, nullable=False),
|
||||
Column("stock", Integer, nullable=False, server_default="1"),
|
||||
Column("is_valid", Boolean, nullable=False, server_default="1"),
|
||||
UniqueConstraint("user", "item_kind", "item_id", name="mai2_item_item_uk"),
|
||||
Column("isValid", Boolean, nullable=False, server_default="1"),
|
||||
UniqueConstraint("user", "itemId", "itemKind", name="mai2_item_item_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
@ -139,11 +140,44 @@ charge = Table(
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("charge_id", Integer, nullable=False),
|
||||
Column("chargeId", Integer, nullable=False),
|
||||
Column("stock", Integer, nullable=False),
|
||||
Column("purchase_date", String(255), nullable=False),
|
||||
Column("valid_date", String(255), nullable=False),
|
||||
UniqueConstraint("user", "charge_id", name="mai2_item_charge_uk"),
|
||||
Column("purchaseDate", String(255), nullable=False),
|
||||
Column("validDate", String(255), nullable=False),
|
||||
UniqueConstraint("user", "chargeId", name="mai2_item_charge_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
print_detail = Table(
|
||||
"mai2_item_print_detail",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column(
|
||||
"user",
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("orderId", Integer),
|
||||
Column("printNumber", Integer),
|
||||
Column("printDate", TIMESTAMP, nullable=False, server_default=func.now()),
|
||||
Column("serialId", String(20), nullable=False),
|
||||
Column("placeId", Integer, nullable=False),
|
||||
Column("clientId", String(11), nullable=False),
|
||||
Column("printerSerialId", String(20), nullable=False),
|
||||
Column("cardRomVersion", Integer),
|
||||
Column("isHolograph", Boolean, server_default="1"),
|
||||
Column("printOption1", Boolean, server_default="0"),
|
||||
Column("printOption2", Boolean, server_default="0"),
|
||||
Column("printOption3", Boolean, server_default="0"),
|
||||
Column("printOption4", Boolean, server_default="0"),
|
||||
Column("printOption5", Boolean, server_default="0"),
|
||||
Column("printOption6", Boolean, server_default="0"),
|
||||
Column("printOption7", Boolean, server_default="0"),
|
||||
Column("printOption8", Boolean, server_default="0"),
|
||||
Column("printOption9", Boolean, server_default="0"),
|
||||
Column("printOption10", Boolean, server_default="0"),
|
||||
Column("created", String(255), server_default=""),
|
||||
UniqueConstraint("user", "serialId", name="mai2_item_print_detail_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
@ -154,15 +188,15 @@ class Mai2ItemData(BaseData):
|
||||
) -> None:
|
||||
sql = insert(item).values(
|
||||
user=user_id,
|
||||
item_kind=item_kind,
|
||||
item_id=item_id,
|
||||
itemKind=item_kind,
|
||||
itemId=item_id,
|
||||
stock=stock,
|
||||
is_valid=is_valid,
|
||||
isValid=is_valid,
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(
|
||||
stock=stock,
|
||||
is_valid=is_valid,
|
||||
isValid=is_valid,
|
||||
)
|
||||
|
||||
result = self.execute(conflict)
|
||||
@ -178,7 +212,7 @@ class Mai2ItemData(BaseData):
|
||||
sql = item.select(item.c.user == user_id)
|
||||
else:
|
||||
sql = item.select(
|
||||
and_(item.c.user == user_id, item.c.item_kind == item_kind)
|
||||
and_(item.c.user == user_id, item.c.itemKind == item_kind)
|
||||
)
|
||||
|
||||
result = self.execute(sql)
|
||||
@ -190,8 +224,8 @@ class Mai2ItemData(BaseData):
|
||||
sql = item.select(
|
||||
and_(
|
||||
item.c.user == user_id,
|
||||
item.c.item_kind == item_kind,
|
||||
item.c.item_id == item_id,
|
||||
item.c.itemKind == item_kind,
|
||||
item.c.itemId == item_id,
|
||||
)
|
||||
)
|
||||
|
||||
@ -382,3 +416,93 @@ class Mai2ItemData(BaseData):
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
def put_card(
|
||||
self,
|
||||
user_id: int,
|
||||
card_type_id: int,
|
||||
card_kind: int,
|
||||
chara_id: int,
|
||||
map_id: int,
|
||||
) -> Optional[Row]:
|
||||
sql = insert(card).values(
|
||||
user=user_id,
|
||||
cardId=card_type_id,
|
||||
cardTypeId=card_kind,
|
||||
charaId=chara_id,
|
||||
mapId=map_id,
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(charaId=chara_id, mapId=map_id)
|
||||
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
self.logger.warn(
|
||||
f"put_card: failed to insert card! user_id: {user_id}, kind: {kind}"
|
||||
)
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
def get_cards(self, user_id: int, kind: int = None) -> Optional[Row]:
|
||||
if kind is None:
|
||||
sql = card.select(card.c.user == user_id)
|
||||
else:
|
||||
sql = card.select(and_(card.c.user == user_id, card.c.cardKind == kind))
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
def put_charge(
|
||||
self,
|
||||
user_id: int,
|
||||
charge_id: int,
|
||||
stock: int,
|
||||
purchase_date: datetime,
|
||||
valid_date: datetime,
|
||||
) -> Optional[Row]:
|
||||
sql = insert(charge).values(
|
||||
user=user_id,
|
||||
chargeId=charge_id,
|
||||
stock=stock,
|
||||
purchaseDate=purchase_date,
|
||||
validDate=valid_date,
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(
|
||||
stock=stock, purchaseDate=purchase_date, validDate=valid_date
|
||||
)
|
||||
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
self.logger.warn(
|
||||
f"put_card: failed to insert charge! user_id: {user_id}, chargeId: {charge_id}"
|
||||
)
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
def get_charges(self, user_id: int) -> Optional[Row]:
|
||||
sql = charge.select(charge.c.user == user_id)
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
def put_user_print_detail(
|
||||
self, aime_id: int, serial_id: str, user_print_data: Dict
|
||||
) -> Optional[int]:
|
||||
sql = insert(print_detail).values(
|
||||
user=aime_id, serialId=serial_id, **user_print_data
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(**user_print_data)
|
||||
result = self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
self.logger.warn(
|
||||
f"put_user_print_detail: Failed to insert! aime_id: {aime_id}"
|
||||
)
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
@ -448,7 +448,9 @@ class Mai2ProfileData(BaseData):
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
def get_profile_activity(self, user_id: int, kind: int = None) -> Optional[Row]:
|
||||
def get_profile_activity(
|
||||
self, user_id: int, kind: int = None
|
||||
) -> Optional[List[Row]]:
|
||||
sql = activity.select(
|
||||
and_(
|
||||
activity.c.user == user_id,
|
||||
@ -459,4 +461,4 @@ class Mai2ProfileData(BaseData):
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
return result.fetchall()
|
||||
|
@ -242,7 +242,7 @@ class Mai2ScoreData(BaseData):
|
||||
return result.lastrowid
|
||||
|
||||
def get_courses(self, user_id: int) -> Optional[List[Row]]:
|
||||
sql = course.select(best_score.c.user == user_id)
|
||||
sql = course.select(course.c.user == user_id)
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
|
@ -53,6 +53,22 @@ ticket = Table(
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
cards = Table(
|
||||
"mai2_static_cards",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("version", Integer, nullable=False),
|
||||
Column("cardId", Integer, nullable=False),
|
||||
Column("cardName", String(255), nullable=False),
|
||||
Column("startDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"),
|
||||
Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
|
||||
Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"),
|
||||
Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
|
||||
Column("enabled", Boolean, server_default="1"),
|
||||
UniqueConstraint("version", "cardId", "cardName", name="mai2_static_cards_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
|
||||
class Mai2StaticData(BaseData):
|
||||
def put_game_event(
|
||||
@ -166,6 +182,8 @@ class Mai2StaticData(BaseData):
|
||||
|
||||
conflict = sql.on_duplicate_key_update(price=ticket_price)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(price=ticket_price)
|
||||
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
self.logger.warn(f"Failed to insert charge {ticket_id} type {ticket_type}")
|
||||
@ -208,3 +226,24 @@ class Mai2StaticData(BaseData):
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
def put_card(self, version: int, card_id: int, card_name: str, **card_data) -> int:
|
||||
sql = insert(cards).values(
|
||||
version=version, cardId=card_id, cardName=card_name, **card_data
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(**card_data)
|
||||
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
self.logger.warn(f"Failed to insert card {card_id}")
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
def get_enabled_cards(self, version: int) -> Optional[List[Row]]:
|
||||
sql = cards.select(and_(cards.c.version == version, cards.c.enabled == True))
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
@ -1,4 +1,5 @@
|
||||
from typing import Any, List, Dict
|
||||
from random import randint
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
import json
|
||||
@ -13,3 +14,176 @@ class Mai2Universe(Mai2Base):
|
||||
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
|
||||
super().__init__(cfg, game_cfg)
|
||||
self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE
|
||||
|
||||
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
p = self.data.profile.get_profile_detail(data["userId"], self.version)
|
||||
if p is None:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"userName": p["userName"],
|
||||
"rating": p["playerRating"],
|
||||
# hardcode lastDataVersion for CardMaker 1.34
|
||||
"lastDataVersion": "1.20.00",
|
||||
"isLogin": False,
|
||||
"isExistSellingCard": False,
|
||||
}
|
||||
|
||||
def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
|
||||
# user already exists, because the preview checks that already
|
||||
p = self.data.profile.get_profile_detail(data["userId"], self.version)
|
||||
|
||||
cards = self.data.card.get_user_cards(data["userId"])
|
||||
if cards is None or len(cards) == 0:
|
||||
# This should never happen
|
||||
self.logger.error(
|
||||
f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}"
|
||||
)
|
||||
return {}
|
||||
|
||||
# get the dict representation of the row so we can modify values
|
||||
user_data = p._asdict()
|
||||
|
||||
# remove the values the game doesn't want
|
||||
user_data.pop("id")
|
||||
user_data.pop("user")
|
||||
user_data.pop("version")
|
||||
|
||||
return {"userId": data["userId"], "userData": user_data}
|
||||
|
||||
def handle_cm_login_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": 1}
|
||||
|
||||
def handle_cm_logout_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": 1}
|
||||
|
||||
def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict:
|
||||
selling_cards = self.data.static.get_enabled_cards(self.version)
|
||||
if selling_cards is None:
|
||||
return {"length": 0, "sellingCardList": []}
|
||||
|
||||
selling_card_list = []
|
||||
for card in selling_cards:
|
||||
tmp = card._asdict()
|
||||
tmp.pop("id")
|
||||
tmp.pop("version")
|
||||
tmp.pop("cardName")
|
||||
tmp.pop("enabled")
|
||||
|
||||
tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S")
|
||||
tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S")
|
||||
tmp["noticeStartDate"] = datetime.strftime(
|
||||
tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
tmp["noticeEndDate"] = datetime.strftime(
|
||||
tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
|
||||
selling_card_list.append(tmp)
|
||||
|
||||
return {"length": len(selling_card_list), "sellingCardList": selling_card_list}
|
||||
|
||||
def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict:
|
||||
user_cards = self.data.item.get_cards(data["userId"])
|
||||
if user_cards is None:
|
||||
return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []}
|
||||
|
||||
max_ct = data["maxCount"]
|
||||
next_idx = data["nextIndex"]
|
||||
start_idx = next_idx
|
||||
end_idx = max_ct + start_idx
|
||||
|
||||
if len(user_cards[start_idx:]) > max_ct:
|
||||
next_idx += max_ct
|
||||
else:
|
||||
next_idx = 0
|
||||
|
||||
card_list = []
|
||||
for card in user_cards:
|
||||
tmp = card._asdict()
|
||||
tmp.pop("id")
|
||||
tmp.pop("user")
|
||||
|
||||
tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S")
|
||||
tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S")
|
||||
card_list.append(tmp)
|
||||
|
||||
return {
|
||||
"returnCode": 1,
|
||||
"length": len(card_list[start_idx:end_idx]),
|
||||
"nextIndex": next_idx,
|
||||
"userCardList": card_list[start_idx:end_idx],
|
||||
}
|
||||
|
||||
def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
|
||||
super().handle_get_user_item_api_request(data)
|
||||
|
||||
def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
|
||||
characters = self.data.item.get_characters(data["userId"])
|
||||
|
||||
chara_list = []
|
||||
for chara in characters:
|
||||
chara_list.append(
|
||||
{
|
||||
"characterId": chara["characterId"],
|
||||
# no clue why those values are even needed
|
||||
"point": 0,
|
||||
"count": 0,
|
||||
"level": chara["level"],
|
||||
"nextAwake": 0,
|
||||
"nextAwakePercent": 0,
|
||||
"favorite": False,
|
||||
"awakening": chara["awakening"],
|
||||
"useCount": chara["useCount"],
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"returnCode": 1,
|
||||
"length": len(chara_list),
|
||||
"userCharacterList": chara_list,
|
||||
}
|
||||
|
||||
def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
|
||||
return {"length": 0, "userPrintDetailList": []}
|
||||
|
||||
def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
|
||||
user_id = data["userId"]
|
||||
upsert = data["userPrintDetail"]
|
||||
|
||||
# set a random card serial number
|
||||
serial_id = "".join([str(randint(0, 9)) for _ in range(20)])
|
||||
|
||||
user_card = upsert["userCard"]
|
||||
self.data.item.put_card(
|
||||
user_id,
|
||||
user_card["cardId"],
|
||||
user_card["cardTypeId"],
|
||||
user_card["charaId"],
|
||||
user_card["mapId"],
|
||||
)
|
||||
|
||||
# properly format userPrintDetail for the database
|
||||
upsert.pop("userCard")
|
||||
upsert.pop("serialId")
|
||||
upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d")
|
||||
|
||||
self.data.item.put_user_print_detail(user_id, serial_id, upsert)
|
||||
|
||||
return {
|
||||
"returnCode": 1,
|
||||
"orderId": 0,
|
||||
"serialId": serial_id,
|
||||
"startDate": "2018-01-01 00:00:00",
|
||||
"endDate": "2038-01-01 00:00:00",
|
||||
}
|
||||
|
||||
def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
|
||||
return {
|
||||
"returnCode": 1,
|
||||
"orderId": 0,
|
||||
"serialId": data["userPrintlog"]["serialId"],
|
||||
}
|
||||
|
||||
def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": 1}
|
||||
|
@ -4,12 +4,19 @@ import pytz
|
||||
import json
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.mai2.base import Mai2Base
|
||||
from titles.mai2.universe import Mai2Universe
|
||||
from titles.mai2.const import Mai2Constants
|
||||
from titles.mai2.config import Mai2Config
|
||||
|
||||
|
||||
class Mai2UniversePlus(Mai2Base):
|
||||
class Mai2UniversePlus(Mai2Universe):
|
||||
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
|
||||
super().__init__(cfg, game_cfg)
|
||||
self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS
|
||||
|
||||
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
user_data = super().handle_cm_get_user_preview_api_request(data)
|
||||
|
||||
# hardcode lastDataVersion for CardMaker 1.35
|
||||
user_data["lastDataVersion"] = "1.25.00"
|
||||
return user_data
|
||||
|
@ -452,7 +452,8 @@ class OngekiBase:
|
||||
tmp.pop("id")
|
||||
items.append(tmp)
|
||||
|
||||
xout = kind * 10000000000 + (data["nextIndex"] % 10000000000) + len(items)
|
||||
xout = kind * 10000000000 + \
|
||||
(data["nextIndex"] % 10000000000) + len(items)
|
||||
|
||||
if len(items) < data["maxCount"] or data["maxCount"] == 0:
|
||||
nextIndex = 0
|
||||
@ -851,7 +852,8 @@ class OngekiBase:
|
||||
)
|
||||
|
||||
if "userOption" in upsert and len(upsert["userOption"]) > 0:
|
||||
self.data.profile.put_profile_options(user_id, upsert["userOption"][0])
|
||||
self.data.profile.put_profile_options(
|
||||
user_id, upsert["userOption"][0])
|
||||
|
||||
if "userPlaylogList" in upsert:
|
||||
for playlog in upsert["userPlaylogList"]:
|
||||
|
@ -93,7 +93,12 @@ class OngekiBright(OngekiBase):
|
||||
def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
|
||||
user_characters = self.data.item.get_characters(data["userId"])
|
||||
if user_characters is None:
|
||||
return {}
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": 0,
|
||||
"nextIndex": 0,
|
||||
"userCharacterList": []
|
||||
}
|
||||
|
||||
max_ct = data["maxCount"]
|
||||
next_idx = data["nextIndex"]
|
||||
@ -543,7 +548,7 @@ class OngekiBright(OngekiBase):
|
||||
"returnCode": 1,
|
||||
"orderId": 0,
|
||||
"serialId": "11111111111111111111",
|
||||
"apiName": "CMUpsertUserPrintPlaylogApi",
|
||||
"apiName": "CMUpsertUserPrintPlaylogApi"
|
||||
}
|
||||
|
||||
def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
|
||||
@ -551,7 +556,7 @@ class OngekiBright(OngekiBase):
|
||||
"returnCode": 1,
|
||||
"orderId": 0,
|
||||
"serialId": "11111111111111111111",
|
||||
"apiName": "CMUpsertUserPrintlogApi",
|
||||
"apiName": "CMUpsertUserPrintlogApi"
|
||||
}
|
||||
|
||||
def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
|
||||
|
@ -142,8 +142,8 @@ class OngekiBrightMemory(OngekiBright):
|
||||
user_data = super().handle_cm_get_user_data_api_request(data)
|
||||
|
||||
# hardcode Card Maker version for now
|
||||
# Card Maker 1.34.00 = 1.30.01
|
||||
# Card Maker 1.36.00 = 1.35.04
|
||||
user_data["userData"]["compatibleCmVersion"] = "1.35.04"
|
||||
# Card Maker 1.34 = 1.30.01
|
||||
# Card Maker 1.35 = 1.35.03
|
||||
user_data["userData"]["compatibleCmVersion"] = "1.35.03"
|
||||
|
||||
return user_data
|
||||
|
@ -3,7 +3,8 @@ import json
|
||||
import inflection
|
||||
import yaml
|
||||
import string
|
||||
import logging, coloredlogs
|
||||
import logging
|
||||
import coloredlogs
|
||||
import zlib
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from os import path
|
||||
@ -93,7 +94,7 @@ class OngekiServlet:
|
||||
)
|
||||
|
||||
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
|
||||
if url_path.lower() == "/ping":
|
||||
if url_path.lower() == "ping":
|
||||
return zlib.compress(b'{"returnCode": 1}')
|
||||
|
||||
req_raw = request.content.getvalue()
|
||||
|
@ -706,7 +706,7 @@ class OngekiItemData(BaseData):
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(
|
||||
user=aime_id, serialId=serial_id, **user_print_data
|
||||
user=aime_id, **user_print_data
|
||||
)
|
||||
result = self.execute(conflict)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user