Initial D THE ARCADE S2 support added

This commit is contained in:
Dniel97 2023-10-01 03:54:23 +02:00
parent 38c1c31cf5
commit e0265485ff
Signed by untrusted user: Dniel97
GPG Key ID: 6180B3C768FB2E08
28 changed files with 244427 additions and 29 deletions

View File

@ -16,6 +16,7 @@ using the megaime database. Clean installations always create the latest databas
- [Card Maker](#card-maker)
- [WACCA](#wacca)
- [Sword Art Online Arcade](#sao)
- [Initial D THE ARCADE](#initial-d-the-arcade)
# Supported Games
@ -27,7 +28,7 @@ Games listed below have been tested and confirmed working.
### SDBT
| Version ID | Version Name |
|------------|-----------------------|
| ---------- | --------------------- |
| 0 | CHUNITHM |
| 1 | CHUNITHM PLUS |
| 2 | CHUNITHM AIR |
@ -43,7 +44,7 @@ Games listed below have been tested and confirmed working.
### SDHD/SDBT
| Version ID | Version Name |
|------------|---------------------|
| ---------- | ------------------- |
| 11 | CHUNITHM NEW!! |
| 12 | CHUNITHM NEW PLUS!! |
| 13 | CHUNITHM SUN |
@ -94,7 +95,7 @@ After a failed Online Battle the room will be deleted. The host is used for the
### SDCA
| Version ID | Version Name |
|------------|------------------------------------|
| ---------- | ---------------------------------- |
| 0 | crossbeats REV. |
| 1 | crossbeats REV. SUNRISE |
| 2 | crossbeats REV. SUNRISE S2 |
@ -114,26 +115,26 @@ The importer for crossbeats REV. will import Music.
Config file is located in `config/cxb.yaml`.
| Option | Info |
|------------------------|------------------------------------------------------------|
| `hostname` | Requires a proper `hostname` (not localhost!) to run |
| `ssl_enable` | Enables/Disables the use of the `ssl_cert` and `ssl_key` |
| `port` | Set your unsecure port number |
| `port_secure` | Set your secure/SSL port number |
| `ssl_cert`, `ssl_key` | Enter your SSL certificate (requires not self signed cert) |
| Option | Info |
| --------------------- | ---------------------------------------------------------- |
| `hostname` | Requires a proper `hostname` (not localhost!) to run |
| `ssl_enable` | Enables/Disables the use of the `ssl_cert` and `ssl_key` |
| `port` | Set your unsecure port number |
| `port_secure` | Set your secure/SSL port number |
| `ssl_cert`, `ssl_key` | Enter your SSL certificate (requires not self signed cert) |
## maimai DX
### SDEZ
| Game Code | Version ID | Version Name |
|-----------|------------|-------------------------|
| Game Code | Version ID | Version Name |
| --------- | ---------- | ------------ |
For versions pre-dx
| Game Code | Version ID | Version Name |
|-----------|------------|-------------------------|
| --------- | ---------- | ----------------------- |
| SBXL | 0 | maimai |
| SBXL | 1 | maimai PLUS |
| SBZF | 2 | maimai GreeN |
@ -186,7 +187,7 @@ Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code!
### SBZV
| Version ID | Version Name |
|------------|---------------------------------|
| ---------- | ------------------------------- |
| 0 | Project Diva Arcade |
| 1 | Project Diva Arcade Future Tone |
@ -207,7 +208,7 @@ the Shop, Modules and Customizations.
Config file is located in `config/diva.yaml`.
| Option | Info |
|----------------------|-------------------------------------------------------------------------------------------------|
| -------------------- | ----------------------------------------------------------------------------------------------- |
| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased |
| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased |
@ -227,7 +228,7 @@ python dbutils.py --game SBZV upgrade
### SDDT
| Version ID | Version Name |
|------------|----------------------------|
| ---------- | -------------------------- |
| 0 | O.N.G.E.K.I. |
| 1 | O.N.G.E.K.I. + |
| 2 | O.N.G.E.K.I. SUMMER |
@ -255,7 +256,7 @@ The importer for O.N.G.E.K.I. will all all Cards, Music and Events.
Config file is located in `config/ongeki.yaml`.
| Option | Info |
|------------------|----------------------------------------------------------------------------------------------------------------|
| ---------------- | -------------------------------------------------------------------------------------------------------------- |
| `enabled_gachas` | Enter all gacha IDs for Card Maker to work, other than default may not work due to missing cards added to them |
Note: 1149 and higher are only for Card Maker 1.35 and higher and will be ignored on lower versions.
@ -275,7 +276,7 @@ python dbutils.py --game SDDT upgrade
### SDED
| Version ID | Version Name |
|------------|-----------------|
| ---------- | --------------- |
| 0 | Card Maker 1.30 |
| 1 | Card Maker 1.35 |
@ -391,7 +392,7 @@ Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded fo
### SDFE
| Version ID | Version Name |
|------------|---------------|
| ---------- | ------------- |
| 0 | WACCA |
| 1 | WACCA S |
| 2 | WACCA Lily |
@ -414,7 +415,7 @@ The importer for WACCA will import all Music data.
Config file is located in `config/wacca.yaml`.
| Option | Info |
|--------------------|-----------------------------------------------------------------------------|
| ------------------ | --------------------------------------------------------------------------- |
| `always_vip` | Enables/Disables VIP, if disabled it needs to be purchased manually in game |
| `infinite_tickets` | Always set the "unlock expert" tickets to 5 |
| `infinite_wp` | Sets the user WP to `999999` |
@ -468,9 +469,9 @@ Below is a list of VIP rewards. Currently, VIP is not implemented, and thus thes
### SDEW
| Version ID | Version Name |
|------------|---------------|
| 0 | SAO |
| Version ID | Version Name |
| ---------- | ------------ |
| 0 | SAO |
### Importer
@ -487,11 +488,11 @@ The importer for SAO will import all items, heroes, support skills and titles da
Config file is located in `config/sao.yaml`.
| Option | Info |
|--------------------|-----------------------------------------------------------------------------|
| `hostname` | Changes the server listening address for Mucha |
| `port` | Changes the listing port |
| `auto_register` | Allows the game to handle the automatic registration of new cards |
| Option | Info |
| --------------- | ----------------------------------------------------------------- |
| `hostname` | Changes the server listening address for Mucha |
| `port` | Changes the listing port |
| `auto_register` | Allows the game to handle the automatic registration of new cards |
### Database upgrade
@ -513,4 +514,89 @@ python dbutils.py --game SDEW upgrade
- Midorica - Limited Network Support
- Dniel97 - Helping with network base
- tungnotpunk - Source
- tungnotpunk - Source
## Initial D THE ARCADE
### SDGT
| Version ID | Version Name |
| ---------- | ----------------------------- |
| 0 | Initial D THE ARCADE Season 1 |
| 1 | Initial D THE ARCADE Season 2 |
**Important: Only version 1.50.00 (Season 2) is currently working and actively supported!**
### Profile Importer
In order to use the profile importer download the `idac_profile.json` file from the frontend
and either directly use the folder path with `idac_profile.json` in it or specify the complete
path to the `.json` file
```shell
python read.py --game SDGT --version <Version ID> --optfolder /path/to/game/download/folder
```
The importer for SDGT will import the complete profile data with personal high scores as well.
### Config
Config file is located in `config/idac.yaml`.
| Option | Info |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `ssl` | Enables/Disables the use of the `ssl_cert` and `ssl_key` (currently unsuported) |
| `matching_host` | IPv4 address of your PC for the Online Battle (currently unsupported) |
| `port_matching` | Port number for the Online Battle Matching |
| `port_echo1/2` | Port numbers for Echos |
| `port_matching_p2p` | Port number for Online Battle (currently unsupported) |
| `stamp.enabled` | Enables/Disabled the play stamp events |
| `stamp.enabled_stamps` | Define up to 3 play stamp events (without `.json` extension, which are placed in `titles/idac/data/stamps`) |
| `timetrial.enabled` | Enables/Disables the time trial event |
| `timetrial.enabled_timetrial` | Define one! trial event (without `.json` extension, which are placed in `titles/idac/data/timetrial`) |
### Database upgrade
Always make sure your database (tables) are up-to-date
```shell
python dbutils.py --game SDGT upgrade
```
### Notes
- Online Battle is not supported
- Online Battle Matching is not supported
### Item categories
1. D Coin
3. Car Dressup Token
5. Avatar Dressup Token
6. Tachometer
7. Aura
8. Aura Color
9. Avatar Face
10. Avatar Eye
11. Avatar Mouth
12. Avatar Hair
13. Avatar Glasses
14. Avatar Face accessories
15. Avatar Body
18. Avatar Background
21. Chat Stamp
22. Keychain
24. Title
25. Full Tune Ticket
26. Paper Cup
27. BGM
28. Drifting Text
31. Start Menu BG
32. Car Color/Paint
33. Aura Level?
34. Full Tune Ticket Fragment
35. Underneon Lights
### Credits
A huge thanks to all people who helped shaping this project to what it is now and don't want to be mentioned here.

22
example_config/idac.yaml Normal file
View File

@ -0,0 +1,22 @@
server:
enabled: True
loglevel: "debug"
ssl: False
ssl_key: "cert/idac.key"
ssl_cert: "cert/idac.crt"
matching_host: "127.0.0.1"
port_matching: 20000
port_echo1: 20001
port_echo2: 20002
port_matching_p2p: 20003
stamp:
enabled: True
enabled_stamps: # max 3 play stamps
- "touhou_remilia_scarlet"
- "touhou_flandre_scarlet"
- "touhou_sakuya_izayoi"
timetrial:
enabled: True
enabled_timetrial: "touhou_remilia_scarlet"

View File

@ -33,6 +33,9 @@ Games listed below have been tested and confirmed working. Only game versions ol
+ Sword Art Online Arcade (partial support)
+ Final
+ Initial D THE ARCADE
+ Season 2
## Requirements
- python 3 (tested working with 3.9 and 3.10, other versions YMMV)
- pip

12
titles/idac/__init__.py Normal file
View File

@ -0,0 +1,12 @@
from titles.idac.index import IDACServlet
from titles.idac.const import IDACConstants
from titles.idac.database import IDACData
from titles.idac.read import IDACReader
from titles.idac.frontend import IDACFrontend
index = IDACServlet
database = IDACData
reader = IDACReader
frontend = IDACFrontend
game_codes = [IDACConstants.GAME_CODE]
current_schema_version = 1

16
titles/idac/base.py Normal file
View File

@ -0,0 +1,16 @@
import logging
from core.config import CoreConfig
from titles.idac.config import IDACConfig
from titles.idac.const import IDACConstants
from titles.idac.database import IDACData
class IDACBase:
def __init__(self, cfg: CoreConfig, game_cfg: IDACConfig) -> None:
self.core_cfg = cfg
self.game_config = game_cfg
self.game = IDACConstants.GAME_CODE
self.version = IDACConstants.VER_IDAC_SEASON_1
self.data = IDACData(cfg)
self.logger = logging.getLogger("idac")

121
titles/idac/config.py Normal file
View File

@ -0,0 +1,121 @@
from core.config import CoreConfig
class IDACServerConfig:
def __init__(self, parent: "IDACConfig") -> None:
self.__config = parent
@property
def enable(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "idac", "server", "enable", default=True
)
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "idac", "server", "loglevel", default="info"
)
)
@property
def ssl(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "idac", "server", "ssl", default=False
)
@property
def ssl_cert(self) -> str:
return CoreConfig.get_config_field(
self.__config, "idac", "server", "ssl_cert", default="cert/title.crt"
)
@property
def ssl_key(self) -> str:
return CoreConfig.get_config_field(
self.__config, "idac", "server", "ssl_key", default="cert/title.key"
)
@property
def matching_host(self) -> str:
return CoreConfig.get_config_field(
self.__config, "idac", "server", "matching_host", default="127.0.0.1"
)
@property
def matching(self) -> int:
return CoreConfig.get_config_field(
self.__config, "idac", "server", "port_matching", default=20000
)
@property
def echo1(self) -> int:
return CoreConfig.get_config_field(
self.__config, "idac", "server", "port_echo1", default=20001
)
@property
def echo2(self) -> int:
return CoreConfig.get_config_field(
self.__config, "idac", "server", "port_echo2", default=20002
)
@property
def matching_p2p(self) -> int:
return CoreConfig.get_config_field(
self.__config, "idac", "server", "port_matching_p2p", default=20003
)
class IDACStampConfig:
def __init__(self, parent: "IDACConfig") -> None:
self.__config = parent
@property
def enable(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "idac", "stamp", "enable", default=True
)
@property
def enabled_stamps(self) -> list:
return CoreConfig.get_config_field(
self.__config,
"idac",
"stamp",
"enabled_stamps",
default=[
"touhou_remilia_scarlet",
"touhou_flandre_scarlet",
"touhou_sakuya_izayoi",
],
)
class IDACTimetrialConfig:
def __init__(self, parent: "IDACConfig") -> None:
self.__config = parent
@property
def enable(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "idac", "timetrial", "enable", default=True
)
@property
def enabled_timetrial(self) -> str:
return CoreConfig.get_config_field(
self.__config,
"idac",
"timetrial",
"enabled_stamps",
default="touhou_remilia_scarlet",
)
class IDACConfig(dict):
def __init__(self) -> None:
self.server = IDACServerConfig(self)
self.stamp = IDACStampConfig(self)
self.timetrial = IDACTimetrialConfig(self)

16
titles/idac/const.py Normal file
View File

@ -0,0 +1,16 @@
class IDACConstants():
GAME_CODE = "SDGT"
CONFIG_NAME = "idac.yaml"
VER_IDAC_SEASON_1 = 0
VER_IDAC_SEASON_2 = 1
VERSION_STRING = (
"Initial D THE ARCADE Season 1",
"Initial D THE ARCADE Season 2",
)
@classmethod
def game_ver_to_string(cls, ver: int):
return cls.VERSION_STRING[ver]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
import os
import hashlib
def prepare_images(image_folder="./images"):
print(f"Preparing image delivery files in {image_folder}...")
for file in os.listdir(image_folder):
if file.endswith(".png") or file.endswith(".jpg"):
dpg_name = "adv-" + file[:-4].upper()
if file.endswith(".png"):
dpg_name += ".dpg"
else:
dpg_name += ".djg"
if os.path.exists(os.path.join(image_folder, dpg_name)):
continue
else:
with open(
os.path.join(image_folder, file), "rb"
) as original_image_file:
original_image = original_image_file.read()
image_hash = hashlib.md5(original_image).hexdigest()
print(
f"DPG for {file} not found, creating with hash {image_hash}..."
)
md5_buf = bytes.fromhex(image_hash)
dpg_buf = md5_buf + original_image
with open(os.path.join(image_folder, dpg_name), "wb") as dpg_file:
dpg_file.write(dpg_buf)
print(f"Created {dpg_name}.")
# Call the function to execute it
prepare_images()

View File

@ -0,0 +1,298 @@
{
"m_stamp_event_id": 25,
"stamp_event_nm": "*フランドール・スカーレットスタンプ",
"url": "https://info-initialdac.sega.jp/2290/",
"start_dt": 1667797169,
"end_dt": "2029-01-01",
"play_bonus": 1,
"daily_bonus": 2,
"weekly_bonus": 4,
"add_bonus": [
{
"bonus_category": 0,
"bonus_play_num": 0,
"bonus_stamp_num": 0,
"bonus_daily_flag": 0
}
],
"sheet_design": 5,
"sheet_stamp": 0,
"sheet_set": [
{
"sheet_no": 1,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 24,
"reward_type_a": 4383,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "悪魔の妹"
},
{
"reward_setting_masu": 20,
"reward_category_a": 24,
"reward_type_a": 4401,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "ネオン(フランドール)獲得"
}
]
},
{
"sheet_no": 2,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 12,
"reward_type_a": 966,
"reward_category_b": 12,
"reward_type_b": 969,
"reward_is_pickup": 1,
"reward_pickup_display_name": "フランドールのナイトキャップ"
},
{
"reward_setting_masu": 20,
"reward_category_a": 21,
"reward_type_a": 494,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "おまたせ"
}
]
},
{
"sheet_no": 3,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
},
{
"reward_setting_masu": 20,
"reward_category_a": 24,
"reward_type_a": 4395,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "バイナルEX(フランドール)獲得"
}
]
},
{
"sheet_no": 4,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 3,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "ドレスアップトークン×1"
},
{
"reward_setting_masu": 20,
"reward_category_a": 15,
"reward_type_a": 462,
"reward_category_b": 15,
"reward_type_b": 465,
"reward_is_pickup": 1,
"reward_pickup_display_name": "吸血鬼の服(フランドール)"
}
]
},
{
"sheet_no": 5,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 6,
"reward_type_a": 62,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "スペシャル(フランドール・スカーレット)"
},
{
"reward_setting_masu": 20,
"reward_category_a": 24,
"reward_type_a": 4386,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "禁忌「クランベリートラップ」"
}
]
},
{
"sheet_no": 6,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 21,
"reward_type_a": 495,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "・・ここは私の家よ?"
},
{
"reward_setting_masu": 20,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
}
]
},
{
"sheet_no": 7,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 24,
"reward_type_a": 4404,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "ネオンDX(フランドール)獲得"
},
{
"reward_setting_masu": 20,
"reward_category_a": 3,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "ドレスアップトークン×1"
}
]
},
{
"sheet_no": 8,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 24,
"reward_type_a": 4398,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "バイナルDX(フランドール)獲得"
},
{
"reward_setting_masu": 20,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
}
]
},
{
"sheet_no": 9,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 3,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "ドレスアップトークン×1"
},
{
"reward_setting_masu": 15,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
},
{
"reward_setting_masu": 20,
"reward_category_a": 3,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "ドレスアップトークン×1"
}
]
},
{
"sheet_no": 10,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
},
{
"reward_setting_masu": 20,
"reward_category_a": 24,
"reward_type_a": 4389,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "QED「495年の波紋」"
}
]
},
{
"sheet_no": 11,
"loop_flag": 1,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 3,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "ドレスアップトークン×1"
},
{
"reward_setting_masu": 20,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
}
]
}
],
"weekday_bonus": 0,
"weekend_bonus": 0,
"sheet_prohibitfreeplaystampcount": false
}

View File

@ -0,0 +1,298 @@
{
"m_stamp_event_id": 24,
"stamp_event_nm": "*レミリア・スカーレットスタンプ",
"url": "https://info-initialdac.sega.jp/2096/",
"start_dt": 1667797169,
"end_dt": "2029-01-01",
"play_bonus": 1,
"daily_bonus": 2,
"weekly_bonus": 4,
"add_bonus": [
{
"bonus_category": 0,
"bonus_play_num": 0,
"bonus_stamp_num": 0,
"bonus_daily_flag": 0
}
],
"sheet_design": 7,
"sheet_stamp": 0,
"sheet_set": [
{
"sheet_no": 1,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 24,
"reward_type_a": 4382,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "永遠に紅い幼き月"
},
{
"reward_setting_masu": 20,
"reward_category_a": 24,
"reward_type_a": 4400,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "ネオン(レミリア)獲得"
}
]
},
{
"sheet_no": 2,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 12,
"reward_type_a": 965,
"reward_category_b": 12,
"reward_type_b": 968,
"reward_is_pickup": 1,
"reward_pickup_display_name": "レミリアのナイトキャップ"
},
{
"reward_setting_masu": 20,
"reward_category_a": 21,
"reward_type_a": 490,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "ここは、私の城よ?"
}
]
},
{
"sheet_no": 3,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
},
{
"reward_setting_masu": 20,
"reward_category_a": 24,
"reward_type_a": 4394,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "バイナルEX(レミリア)獲得"
}
]
},
{
"sheet_no": 4,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 3,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "ドレスアップトークン×1"
},
{
"reward_setting_masu": 20,
"reward_category_a": 15,
"reward_type_a": 461,
"reward_category_b": 15,
"reward_type_b": 464,
"reward_is_pickup": 1,
"reward_pickup_display_name": "吸血鬼の服(レミリア)"
}
]
},
{
"sheet_no": 5,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 6,
"reward_type_a": 61,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "スペシャル(レミリア・スカーレット)"
},
{
"reward_setting_masu": 20,
"reward_category_a": 24,
"reward_type_a": 4385,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "天罰「スターオブダビデ」"
}
]
},
{
"sheet_no": 6,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 21,
"reward_type_a": 491,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "楽しい夜になりそうね"
},
{
"reward_setting_masu": 20,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
}
]
},
{
"sheet_no": 7,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 24,
"reward_type_a": 4403,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "ネオンDX(レミリア)獲得"
},
{
"reward_setting_masu": 20,
"reward_category_a": 3,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "ドレスアップトークン×1"
}
]
},
{
"sheet_no": 8,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 24,
"reward_type_a": 4397,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "バイナルDX(レミリア)獲得"
},
{
"reward_setting_masu": 20,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
}
]
},
{
"sheet_no": 9,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 3,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "ドレスアップトークン×1"
},
{
"reward_setting_masu": 15,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
},
{
"reward_setting_masu": 20,
"reward_category_a": 3,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "ドレスアップトークン×1"
}
]
},
{
"sheet_no": 10,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
},
{
"reward_setting_masu": 20,
"reward_category_a": 24,
"reward_type_a": 4388,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": ""
}
]
},
{
"sheet_no": 11,
"loop_flag": 1,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 3,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "ドレスアップトークン×1"
},
{
"reward_setting_masu": 20,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
}
]
}
],
"weekday_bonus": 0,
"weekend_bonus": 0,
"sheet_prohibitfreeplaystampcount": false
}

View File

@ -0,0 +1,298 @@
{
"m_stamp_event_id": 26,
"stamp_event_nm": "*十六夜咲夜スタンプ",
"url": "https://info-initialdac.sega.jp/2306/",
"start_dt": 1667797169,
"end_dt": "2029-01-01",
"play_bonus": 1,
"daily_bonus": 2,
"weekly_bonus": 4,
"add_bonus": [
{
"bonus_category": 0,
"bonus_play_num": 0,
"bonus_stamp_num": 0,
"bonus_daily_flag": 0
}
],
"sheet_design": 6,
"sheet_stamp": 0,
"sheet_set": [
{
"sheet_no": 1,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 24,
"reward_type_a": 4381,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "紅魔館のメイド"
},
{
"reward_setting_masu": 20,
"reward_category_a": 24,
"reward_type_a": 4399,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "ネオン(十六夜咲夜)獲得"
}
]
},
{
"sheet_no": 2,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 12,
"reward_type_a": 964,
"reward_category_b": 12,
"reward_type_b": 967,
"reward_is_pickup": 1,
"reward_pickup_display_name": "メイドのホワイトブリム"
},
{
"reward_setting_masu": 20,
"reward_category_a": 21,
"reward_type_a": 486,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "2時間前に出直してきな"
}
]
},
{
"sheet_no": 3,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
},
{
"reward_setting_masu": 20,
"reward_category_a": 24,
"reward_type_a": 4393,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "バイナルEX(十六夜咲夜)獲得"
}
]
},
{
"sheet_no": 4,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 3,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "ドレスアップトークン×1"
},
{
"reward_setting_masu": 20,
"reward_category_a": 15,
"reward_type_a": 460,
"reward_category_b": 15,
"reward_type_b": 463,
"reward_is_pickup": 1,
"reward_pickup_display_name": "メイドの服"
}
]
},
{
"sheet_no": 5,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 6,
"reward_type_a": 60,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "スペシャル(十六夜咲夜)"
},
{
"reward_setting_masu": 20,
"reward_category_a": 24,
"reward_type_a": 4384,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "幻在「クロックコープス」"
}
]
},
{
"sheet_no": 6,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 21,
"reward_type_a": 487,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "私のナイフから逃げられると思って?"
},
{
"reward_setting_masu": 20,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
}
]
},
{
"sheet_no": 7,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 24,
"reward_type_a": 4402,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "ネオンDX(十六夜咲夜)獲得"
},
{
"reward_setting_masu": 20,
"reward_category_a": 3,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "ドレスアップトークン×1"
}
]
},
{
"sheet_no": 8,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 24,
"reward_type_a": 4396,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "バイナルDX(十六夜咲夜)獲得"
},
{
"reward_setting_masu": 20,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
}
]
},
{
"sheet_no": 9,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 3,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "ドレスアップトークン×1"
},
{
"reward_setting_masu": 15,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
},
{
"reward_setting_masu": 20,
"reward_category_a": 3,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "ドレスアップトークン×1"
}
]
},
{
"sheet_no": 10,
"loop_flag": 0,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": "アバタートークン×1"
},
{
"reward_setting_masu": 20,
"reward_category_a": 24,
"reward_type_a": 4387,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 1,
"reward_pickup_display_name": "メイド秘技「操りドール」"
}
]
},
{
"sheet_no": 11,
"loop_flag": 1,
"stamp_sheet": [
{
"reward_setting_masu": 10,
"reward_category_a": 3,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": ""
},
{
"reward_setting_masu": 20,
"reward_category_a": 5,
"reward_type_a": 1,
"reward_category_b": 0,
"reward_type_b": 0,
"reward_is_pickup": 0,
"reward_pickup_display_name": ""
}
]
}
],
"weekday_bonus": 0,
"weekend_bonus": 0,
"sheet_prohibitfreeplaystampcount": false
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
{
"timetrial_event_id": 4,
"name": "レミリア・スカーレット",
"start_dt": "2023-10-01",
"end_dt": "2023-11-01",
"course_id": 22,
"point": [
50,
80,
80,
80,
80,
100,
100,
100,
100,
120,
120,
120,
120,
140,
140,
140,
140,
160,
160,
160,
160,
180,
180,
180,
180,
200,
200,
200,
200
],
"reward": [
{
"point": 500,
"reward_category_a": 21,
"reward_type_a": 492,
"reward_category_b": 0,
"reward_type_b": 0
},
{
"point": 1000,
"reward_category_a": 21,
"reward_type_a": 493,
"reward_category_b": 0,
"reward_type_b": 0
},
{
"point": 1500,
"reward_category_a": 18,
"reward_type_a": 116,
"reward_category_b": 0,
"reward_type_b": 0
}
]
}

12
titles/idac/database.py Normal file
View File

@ -0,0 +1,12 @@
from core.data import Data
from core.config import CoreConfig
from titles.idac.schema.profile import IDACProfileData
from titles.idac.schema.item import IDACItemData
class IDACData(Data):
def __init__(self, cfg: CoreConfig) -> None:
super().__init__(cfg)
self.profile = IDACProfileData(cfg, self.session)
self.item = IDACItemData(cfg, self.session)

64
titles/idac/echo.py Normal file
View File

@ -0,0 +1,64 @@
import logging
from random import randbytes
import socket
from twisted.internet.protocol import DatagramProtocol
from socketserver import BaseRequestHandler, TCPServer
from typing import Tuple
from core.config import CoreConfig
from titles.idac.config import IDACConfig
from titles.idac.database import IDACData
class IDACEchoUDP(DatagramProtocol):
def __init__(self, cfg: CoreConfig, game_cfg: IDACConfig, port: int) -> None:
super().__init__()
self.port = port
self.core_config = cfg
self.game_config = game_cfg
self.logger = logging.getLogger("idac")
def datagramReceived(self, data, addr):
self.logger.info(
f"UDP Ping from from {addr[0]}:{addr[1]} -> {self.port} - {data.hex()}"
)
self.transport.write(data, addr)
class IDACEchoTCP(BaseRequestHandler):
def __init__(
self, request, client_address, server, cfg: CoreConfig, game_cfg: IDACConfig
) -> None:
self.core_config = cfg
self.game_config = game_cfg
self.logger = logging.getLogger("idac")
self.data = IDACData(cfg)
super().__init__(request, client_address, server)
def handle(self):
data = self.request.recv(1024).strip()
self.logger.debug(
f"TCP Ping from {self.client_address[0]}:{self.client_address[1]} -> {self.server.server_address[1]}: {data.hex()}"
)
self.request.sendall(data)
self.request.shutdown(socket.SHUT_WR)
class IDACEchoTCPFactory(TCPServer):
def __init__(
self,
server_address: Tuple[str, int],
RequestHandlerClass,
cfg: CoreConfig,
game_cfg: IDACConfig,
bind_and_activate: bool = ...,
) -> None:
super().__init__(server_address, RequestHandlerClass, bind_and_activate)
self.core_config = cfg
self.game_config = game_cfg
def finish_request(self, request, client_address):
self.RequestHandlerClass(
request, client_address, self, self.core_config, self.game_config
)

140
titles/idac/frontend.py Normal file
View File

@ -0,0 +1,140 @@
import json
import yaml
import jinja2
from os import path
from twisted.web.util import redirectTo
from twisted.web.http import Request
from twisted.web.server import Session
from core.frontend import FE_Base, IUserSession
from core.config import CoreConfig
from titles.idac.database import IDACData
from titles.idac.schema.profile import *
from titles.idac.schema.item import *
from titles.idac.config import IDACConfig
from titles.idac.const import IDACConstants
class IDACFrontend(FE_Base):
def __init__(
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
) -> None:
super().__init__(cfg, environment)
self.data = IDACData(cfg)
self.game_cfg = IDACConfig()
if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"):
self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"))
)
self.nav_name = "頭文字D THE ARCADE"
# TODO: Add version list
self.version = IDACConstants.VER_IDAC_SEASON_2
self.ticket_names = {
3: "car_dressup_points",
5: "avatar_points",
25: "full_tune_tickets",
34: "full_tune_fragments",
}
def generate_all_tables_json(self, user_id: int):
json_export = {}
idac_tables = {
profile,
config,
avatar,
rank,
stock,
theory,
car,
ticket,
story,
episode,
difficulty,
course,
trial,
challenge,
theory_course,
theory_partner,
theory_running,
vs_info,
stamp,
timetrial_event
}
for table in idac_tables:
sql = select(table).where(
table.c.user == user_id,
)
# check if the table has a version column
if "version" in table.c:
sql = sql.where(table.c.version == self.version)
# lol use the profile connection for items, dirty hack
result = self.data.profile.execute(sql)
data_list = result.fetchall()
# add the list to the json export with the correct table name
json_export[table.name] = []
for data in data_list:
tmp = data._asdict()
tmp.pop("id")
tmp.pop("user")
json_export[table.name].append(tmp)
return json.dumps(json_export, indent=4, default=str, ensure_ascii=False)
def render_GET(self, request: Request) -> bytes:
uri: str = request.uri.decode()
template = self.environment.get_template(
"titles/idac/frontend/idac_index.jinja"
)
sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh)
# profile export
if uri.startswith("/game/idac/export"):
if usr_sesh.user_id == 0:
return redirectTo(b"/game/idac", request)
# set the file name, content type and size to download the json
content = self.generate_all_tables_json(usr_sesh.user_id).encode("utf-8")
request.responseHeaders.addRawHeader(
b"content-type", b"application/octet-stream"
)
request.responseHeaders.addRawHeader(
b"content-disposition", b"attachment; filename=idac_profile.json"
)
request.responseHeaders.addRawHeader(
b"content-length", str(len(content)).encode("utf-8")
)
self.logger.info(f"User {usr_sesh.user_id} exported their IDAC data")
return content
profile_data, tickets, rank = None, None, None
if usr_sesh.user_id > 0:
profile_data = self.data.profile.get_profile(usr_sesh.user_id, self.version)
ticket_data = self.data.item.get_tickets(usr_sesh.user_id)
rank = self.data.profile.get_profile_rank(usr_sesh.user_id, self.version)
tickets = {
self.ticket_names[ticket["ticket_id"]]: ticket["ticket_cnt"]
for ticket in ticket_data
}
return template.render(
title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"],
profile=profile_data,
tickets=tickets,
rank=rank,
sesh=vars(usr_sesh),
active_page="idac",
).encode("utf-16")
def render_POST(self, request: Request) -> bytes:
pass

View File

@ -0,0 +1,132 @@
{% extends "core/frontend/index.jinja" %}
{% block content %}
<h1 class="mb-3">頭文字D THE ARCADE</h1>
{% if sesh is defined and sesh["user_id"] > 0 %}
<div class="card mb-3">
<div class="card-body">
<div class="card-title">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center">
<h3>{{ sesh["username"] }}'s Profile</h3>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<!--<button type="button" class="btn btn-sm btn-outline-secondary">Share</button>-->
<button type="button" data-bs-toggle="modal" data-bs-target="#export"
class="btn btn-sm btn-outline-primary">Export</button>
</div>
</div>
</div>
</div>
<!--<h4 class="card-subtitle mb-2 text-body-secondary">Card subtitle</h4>-->
{% if profile is defined and profile is not none %}
<div class="row d-flex justify-content-center h-100">
<div class="col col-lg-3 col-12">
<div class="card mb-3">
<div class="card-body p-4">
<h5>Information</h5>
<hr class="mt-0 mb-4">
<h6>Username</h6>
<p class="text-muted">{{ profile.username }}</p>
<h6>Grade</h6>
<h4>
{% set grade = rank.grade %}
{% if grade >= 1 and grade <= 72 %}
{% set grade_number = (grade - 1) // 9 %}
{% set grade_letters = ['E', 'D', 'C', 'B', 'A', 'S', 'SS', 'X'] %}
{{ grade_letters[grade_number] }}{{ 9 - ((grade-1) % 9) }}
{% else %}
Unknown
{% endif %}
</h6>
</div>
</div>
</div>
<div class="col col-lg-9 col-12">
<div class="card mb-3">
<div class="card-body p-4">
<h5>Statistics</h5>
<hr class="mt-0 mb-4">
<div class="row pt-1">
<div class="col-lg-4 col-md-6 mb-3">
<h6>Total Plays</h6>
<p class="text-muted">{{ profile.total_play }}</p>
</div>
<div class="col-lg-4 col-md-6 mb-3">
<h6>Last Played</h6>
<p class="text-muted">{{ profile.last_play_date }}</p>
</div>
<div class="col-lg-4 col-md-6 mb-3">
<h6>Mileage</h6>
<p class="text-muted">{{ profile.mileage }} m</p>
</div>
</div>
{% if tickets is defined and tickets|length > 0 %}
<h5>Tokens/Tickets</h5>
<hr class="mt-0 mb-4">
<div class="row pt-1">
<div class="col-lg-3 col-md-6 mb-3">
<h6>Avatar Tokens</h6>
<p class="text-muted">{{ tickets.avatar_points }}/30</p>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<h6>Car Dressup Tokens</h6>
<p class="text-muted">{{ tickets.car_dressup_points }}/30</p>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<h6>FullTune Tickets</h6>
<p class="text-muted">{{ tickets.full_tune_tickets }}/99</p>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<h6>FullTune Fragments</h6>
<p class="text-muted">{{ tickets.full_tune_fragments }}/10</p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% else %}
<div class="alert alert-warning" role="alert">
You need to play 頭文字D THE ARCADE first to view your profile.
</div>
{% endif %}
<!--<a href="#" data-bs-toggle="modal" data-bs-target="#card-add" class="card-link">Add Card</a>-->
</div>
</div>
{% else %}
<div class="alert alert-info" role="alert">
You need to be logged in to view this page. <a href="/gate">Login</a></a>
</div>
{% endif %}
<div class="modal fade" id="export" tabindex="-1" aria-labelledby="export-label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exort-label">Export Profile</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Download your profile as a <strong>.json</strong> file in order to import it into your local ARTEMiS
database.
<div class="alert alert-warning mt-3" role="alert">
{% if profile is defined and profile is not none %}
Are you sure you want to export your profile with the username {{ profile.username }}?
{% endif %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="exportBtn">Download Profile</a>
</div>
</div>
</div>
</div>
<script type="text/javascript">
{% include "titles/idac/frontend/js/idac_scripts.js" %}
</script>
{% endblock content %}

View File

@ -0,0 +1,10 @@
$(document).ready(function () {
$('#exportBtn').click(function () {
window.location = "/game/idac/export";
// appendAlert('Successfully exported the profile', 'success');
// Close the modal on success
$('#export').modal('hide');
});
});

166
titles/idac/index.py Normal file
View File

@ -0,0 +1,166 @@
import json
import traceback
import inflection
import yaml
import logging
import coloredlogs
from os import path
from typing import Dict, Tuple
from logging.handlers import TimedRotatingFileHandler
from twisted.web import server
from twisted.web.http import Request
from twisted.internet import reactor, endpoints
from core.config import CoreConfig
from core.utils import Utils
from titles.idac.base import IDACBase
from titles.idac.season2 import IDACSeason2
from titles.idac.config import IDACConfig
from titles.idac.const import IDACConstants
from titles.idac.echo import IDACEchoUDP
from titles.idac.matching import IDACMatching
class IDACServlet:
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
self.core_cfg = core_cfg
self.game_cfg = IDACConfig()
self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"))
)
self.versions = [
IDACBase(core_cfg, self.game_cfg),
IDACSeason2(core_cfg, self.game_cfg)
]
self.logger = logging.getLogger("idac")
log_fmt_str = "[%(asctime)s] IDAC | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.core_cfg.server.log_dir, "idac"),
encoding="utf8",
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler)
self.logger.setLevel(self.game_cfg.server.loglevel)
coloredlogs.install(
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
)
@classmethod
def get_allnet_info(
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
) -> Tuple[bool, str, str]:
game_cfg = IDACConfig()
if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"):
game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"))
)
if not game_cfg.server.enable:
return (False, "", "")
if core_cfg.server.is_develop:
return (
True,
f"",
# requires http or else it defautls to https
f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/",
)
return (
True,
f"",
# requires http or else it defautls to https
f"http://{core_cfg.title.hostname}/{game_code}/$v/",
)
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
req_raw = request.content.getvalue()
url_split = url_path.split("/")
internal_ver = 0
endpoint = url_split[len(url_split) - 1]
client_ip = Utils.get_ip_addr(request)
if version >= 100 and version < 140: # IDAC Season 1
internal_ver = IDACConstants.VER_IDAC_SEASON_1
elif version >= 140 and version < 171: # IDAC Season 2
internal_ver = IDACConstants.VER_IDAC_SEASON_2
if url_split[0] == "initiald":
header_application = self.decode_header(request.getAllHeaders())
req_data = json.loads(req_raw)
self.logger.info(f"v{version} {endpoint} request from {client_ip}")
self.logger.debug(f"Headers: {header_application}")
self.logger.debug(req_data)
# func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
func_to_find = "handle_"
for x in url_split:
func_to_find += f"{x.lower()}_" if not x == "" and not x == "initiald" else ""
func_to_find += f"request"
if not hasattr(self.versions[internal_ver], func_to_find):
self.logger.warning(f"Unhandled v{version} request {endpoint}")
return '{"status_code": "0"}'.encode("utf-8")
resp = None
try:
handler = getattr(self.versions[internal_ver], func_to_find)
resp = handler(req_data, header_application)
except Exception as e:
traceback.print_exc()
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
return '{"status_code": "0"}'.encode("utf-8")
if resp is None:
resp = {"status_code": "0"}
self.logger.debug(f"Response {resp}")
return json.dumps(resp, ensure_ascii=False).encode("utf-8")
self.logger.warning(
f"IDAC unknown request {url_path} - {request.content.getvalue().decode()}"
)
return '{"status_code": "0"}'.encode("utf-8")
def decode_header(self, data: Dict) -> Dict:
app: str = data[b"application"].decode()
ret = {}
for x in app.split(", "):
y = x.split("=")
ret[y[0]] = y[1].replace('"', "")
return ret
def setup(self):
if self.game_cfg.server.enable:
endpoints.serverFromString(
reactor,
f"tcp:{self.game_cfg.server.matching}:interface={self.core_cfg.server.listen_address}",
).listen(server.Site(IDACMatching(self.core_cfg, self.game_cfg)))
reactor.listenUDP(
self.game_cfg.server.echo1,
IDACEchoUDP(self.core_cfg, self.game_cfg, self.game_cfg.server.echo1),
)
reactor.listenUDP(
self.game_cfg.server.echo2,
IDACEchoUDP(self.core_cfg, self.game_cfg, self.game_cfg.server.echo2),
)

72
titles/idac/matching.py Normal file
View File

@ -0,0 +1,72 @@
import json
import logging
from typing import Dict
from twisted.web import resource
from core import CoreConfig
from titles.idac.season2 import IDACBase
from titles.idac.config import IDACConfig
class IDACMatching(resource.Resource):
isLeaf = True
def __init__(self, cfg: CoreConfig, game_cfg: IDACConfig) -> None:
self.core_config = cfg
self.game_config = game_cfg
self.base = IDACBase(cfg, game_cfg)
self.logger = logging.getLogger("idac")
self.queue = 0
def get_matching_state(self):
if self.queue >= 1:
self.queue -= 1
return 0
else:
return 1
def render_POST(self, req) -> bytes:
url = req.uri.decode()
req_data = json.loads(req.content.getvalue().decode())
header_application = self.decode_header(req.getAllHeaders())
user_id = int(header_application["session"])
# self.getMatchingStatus(user_id)
self.logger.info(
f"IDAC Matching request from {req.getClientIP()}: {url} - {req_data}"
)
resp = {"status_code": "0"}
if url == "/regist":
self.queue = self.queue + 1
elif url == "/status":
if req_data.get("cancel_flag"):
self.queue = self.queue - 1
self.logger.info(
f"IDAC Matching endpoint {req.getClientIP()} had quited"
)
resp = {
"status_code": "0",
# Only IPv4 is supported
"host": self.game_config.server.matching_host,
"port": self.game_config.server.matching_p2p,
"room_name": "INDTA",
"state": self.get_matching_state(),
}
self.logger.debug(f"Response {resp}")
return json.dumps(resp, ensure_ascii=False).encode("utf-8")
def decode_header(self, data: Dict) -> Dict:
app: str = data[b"application"].decode()
ret = {}
for x in app.split(", "):
y = x.split("=")
ret[y[0]] = y[1].replace('"', "")
return ret

161
titles/idac/read.py Normal file
View File

@ -0,0 +1,161 @@
import json
import logging
import os
from typing import Any, Dict, List, Optional
from read import BaseReader
from core.data import Data
from core.config import CoreConfig
from titles.idac.const import IDACConstants
from titles.idac.database import IDACData
from titles.idac.schema.profile import *
from titles.idac.schema.item import *
class IDACReader(BaseReader):
def __init__(
self,
config: CoreConfig,
version: int,
bin_dir: Optional[str],
opt_dir: Optional[str],
extra: Optional[str],
) -> None:
super().__init__(config, version, bin_dir, opt_dir, extra)
self.card_data = Data(config).card
self.data = IDACData(config)
try:
self.logger.info(
f"Start importer for {IDACConstants.game_ver_to_string(version)}"
)
except IndexError:
self.logger.error(f"Invalid Initial D THE ARCADE version {version}")
exit(1)
def read(self) -> None:
if self.bin_dir is None and self.opt_dir is None:
self.logger.error(
(
"To import your profile specify the '--optfolder'",
" path to your idac_profile.json file, exiting",
)
)
exit(1)
if self.opt_dir is not None:
if not os.path.exists(self.opt_dir):
self.logger.error(
f"Path to idac_profile.json does not exist: {self.opt_dir}"
)
exit(1)
if os.path.isdir(self.opt_dir):
self.opt_dir = os.path.join(self.opt_dir, "idac_profile.json")
if not os.path.isfile(self.opt_dir) or self.opt_dir[-5:] != ".json":
self.logger.error(
f"Path to idac_profile.json does not exist: {self.opt_dir}"
)
exit(1)
self.read_idac_profile(self.opt_dir)
def read_idac_profile(self, file_path: str) -> None:
self.logger.info(f"Reading profile from {file_path}...")
# read it as binary to avoid encoding issues
profile_data: Dict[str, Any] = {}
with open(file_path, "rb") as f:
profile_data = json.loads(f.read().decode("utf-8"))
if not profile_data:
self.logger.error("Profile could not be parsed, exiting")
exit(1)
access_code = None
while access_code is None:
access_code = input("Enter your 20 digits access code: ")
if len(access_code) != 20 or not access_code.isdigit():
access_code = None
self.logger.warning("Invalid access code, please try again.")
# check if access code already exists, if not create a new profile
user_id = self.card_data.get_user_id_from_card(access_code)
if user_id is None:
choice = input("Access code does not exist, do you want to create a new profile? (Y/n): ")
if choice.lower() == "n":
self.logger.info("Exiting...")
exit(0)
user_id = self.data.user.create_user()
if user_id is None:
self.logger.error("Failed to register user!")
user_id = -1
else:
card_id = self.data.card.create_card(user_id, access_code)
if card_id is None:
self.logger.error("Failed to register card!")
user_id = -1
if user_id == -1:
self.logger.error("Failed to create profile, exiting")
exit(1)
# table mapping to insert the data properly
tables = {
"idac_profile": profile,
"idac_profile_config": config,
"idac_profile_avatar": avatar,
"idac_profile_rank": rank,
"idac_profile_stock": stock,
"idac_profile_theory": theory,
"idac_user_car": car,
"idac_user_ticket": ticket,
"idac_user_story": story,
"idac_user_story_episode": episode,
"idac_user_story_episode_difficulty": difficulty,
"idac_user_course": course,
"idac_user_time_trial": trial,
"idac_user_challenge": challenge,
"idac_user_theory_course": theory_course,
"idac_user_theory_partner": theory_partner,
"idac_user_theory_running": theory_running,
"idac_user_vs_info": vs_info,
"idac_user_stamp": stamp,
"idac_user_timetrial_event": timetrial_event,
}
for name, data_list in profile_data.items():
# get the SQLAlchemy table object from the name
table = tables.get(name)
if table is None:
self.logger.warning(f"Unknown table {name}, skipping")
continue
for data in data_list:
# add user to the data
data["user"] = user_id
# check if the table has a version column
if "version" in table.c:
data["version"] = self.version
sql = insert(table).values(
**data
)
# lol use the profile connection for items, dirty hack
conflict = sql.on_duplicate_key_update(**data)
result = self.data.profile.execute(conflict)
if result is None:
self.logger.error(f"Failed to insert data into table {name}")
exit(1)
self.logger.info(f"Inserted data into table {name}")
self.logger.info("Profile import complete!")

964
titles/idac/schema/item.py Normal file
View File

@ -0,0 +1,964 @@
from typing import Dict, Optional, List
from sqlalchemy import (
Table,
Column,
UniqueConstraint,
PrimaryKeyConstraint,
and_,
update,
)
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.engine import Row
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
car = Table(
"idac_user_car",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("version", Integer, nullable=False),
Column("car_id", Integer),
Column("style_car_id", Integer),
Column("color", Integer),
Column("bureau", Integer),
Column("kana", Integer),
Column("s_no", Integer),
Column("l_no", Integer),
Column("car_flag", Integer),
Column("tune_point", Integer),
Column("tune_level", Integer, server_default="1"),
Column("tune_parts", Integer),
Column("infinity_tune", Integer, server_default="0"),
Column("online_vs_win", Integer, server_default="0"),
Column(
"pickup_seq", Integer, server_default="1"
), # the order in which the car was picked up
Column(
"purchase_seq", Integer, server_default="1"
), # the order in which the car was purchased
Column("color_stock_list", String(32)),
Column("color_stock_new_list", String(32)),
Column("parts_stock_list", String(48)),
Column("parts_stock_new_list", String(48)),
Column("parts_set_equip_list", String(48)),
Column("parts_list", JSON),
Column("equip_parts_count", Integer, server_default="0"),
Column("total_car_parts_count", Integer, server_default="0"),
Column("use_count", Integer, server_default="0"),
Column("story_use_count", Integer, server_default="0"),
Column("timetrial_use_count", Integer, server_default="0"),
Column("vs_use_count", Integer, server_default="0"),
Column("net_vs_use_count", Integer, server_default="0"),
Column("theory_use_count", Integer, server_default="0"),
Column("car_mileage", Integer, server_default="0"),
UniqueConstraint("user", "version", "style_car_id", name="idac_user_car_uk"),
mysql_charset="utf8mb4",
)
ticket = Table(
"idac_user_ticket",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("ticket_id", Integer),
Column("ticket_cnt", Integer),
UniqueConstraint("user", "ticket_id", name="idac_user_ticket_uk"),
mysql_charset="utf8mb4",
)
story = Table(
"idac_user_story",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("story_type", Integer),
Column("chapter", Integer),
Column("loop_count", Integer, server_default="1"),
UniqueConstraint("user", "chapter", name="idac_user_story_uk"),
mysql_charset="utf8mb4",
)
episode = Table(
"idac_user_story_episode",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("chapter", Integer),
Column("episode", Integer),
Column("play_status", Integer),
UniqueConstraint("user", "chapter", "episode", name="idac_user_story_episode_uk"),
mysql_charset="utf8mb4",
)
difficulty = Table(
"idac_user_story_episode_difficulty",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("episode", Integer),
Column("difficulty", Integer),
Column("play_count", Integer),
Column("clear_count", Integer),
Column("play_status", Integer),
Column("play_score", Integer),
UniqueConstraint(
"user", "episode", "difficulty", name="idac_user_story_episode_difficulty_uk"
),
mysql_charset="utf8mb4",
)
course = Table(
"idac_user_course",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("course_id", Integer),
Column("run_counts", Integer, server_default="1"),
Column("skill_level_exp", Integer, server_default="0"),
UniqueConstraint("user", "course_id", name="idac_user_course_uk"),
mysql_charset="utf8mb4",
)
trial = Table(
"idac_user_time_trial",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("version", Integer, nullable=False),
Column("style_car_id", Integer),
Column("course_id", Integer),
Column("eval_id", Integer, server_default="0"),
Column("goal_time", Integer),
Column("section_time_1", Integer),
Column("section_time_2", Integer),
Column("section_time_3", Integer),
Column("section_time_4", Integer),
Column("mission", Integer),
Column("play_dt", TIMESTAMP, server_default=func.now()),
UniqueConstraint(
"user", "version", "course_id", "style_car_id", name="idac_user_time_trial_uk"
),
mysql_charset="utf8mb4",
)
challenge = Table(
"idac_user_challenge",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("vs_type", Integer),
Column("play_difficulty", Integer),
Column("cleared_difficulty", Integer),
Column("story_type", Integer),
Column("play_count", Integer, server_default="1"),
Column("weak_difficulty", Integer, server_default="0"),
Column("eval_id", Integer),
Column("advantage", Integer),
Column("sec1_advantage_avg", Integer),
Column("sec2_advantage_avg", Integer),
Column("sec3_advantage_avg", Integer),
Column("sec4_advantage_avg", Integer),
Column("nearby_advantage_rate", Integer),
Column("win_flag", Integer),
Column("result", Integer),
Column("record", Integer),
Column("course_id", Integer),
Column("last_play_course_id", Integer),
Column("style_car_id", Integer),
Column("course_day", Integer),
UniqueConstraint(
"user", "vs_type", "play_difficulty", name="idac_user_challenge_uk"
),
mysql_charset="utf8mb4",
)
theory_course = Table(
"idac_user_theory_course",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("course_id", Integer),
Column("max_victory_grade", Integer, server_default="0"),
Column("run_count", Integer, server_default="1"),
Column("powerhouse_lv", Integer),
Column("powerhouse_exp", Integer),
Column("played_powerhouse_lv", Integer),
Column("update_dt", TIMESTAMP, server_default=func.now()),
UniqueConstraint("user", "course_id", name="idac_user_theory_course_uk"),
mysql_charset="utf8mb4",
)
theory_partner = Table(
"idac_user_theory_partner",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("partner_id", Integer),
Column("fellowship_lv", Integer),
Column("fellowship_exp", Integer),
UniqueConstraint("user", "partner_id", name="idac_user_theory_partner_uk"),
mysql_charset="utf8mb4",
)
theory_running = Table(
"idac_user_theory_running",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("course_id", Integer),
Column("attack", Integer),
Column("defense", Integer),
Column("safety", Integer),
Column("runaway", Integer),
Column("trick_flag", Integer),
UniqueConstraint("user", "course_id", name="idac_user_theory_running_uk"),
mysql_charset="utf8mb4",
)
vs_info = Table(
"idac_user_vs_info",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("group_key", String(25)),
Column("win_flg", Integer),
Column("style_car_id", Integer),
Column("course_id", Integer),
Column("course_day", Integer),
Column("players_num", Integer),
Column("winning", Integer),
Column("advantage_1", Integer),
Column("advantage_2", Integer),
Column("advantage_3", Integer),
Column("advantage_4", Integer),
Column("select_course_id", Integer),
Column("select_course_day", Integer),
Column("select_course_random", Integer),
Column("matching_success_sec", Integer),
Column("boost_flag", Integer),
Column("vs_history", Integer),
Column("break_count", Integer),
Column("break_penalty_flag", Integer),
UniqueConstraint("user", "group_key", name="idac_user_vs_info_uk"),
mysql_charset="utf8mb4",
)
stamp = Table(
"idac_user_stamp",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("m_stamp_event_id", Integer),
Column("select_flag", Integer),
Column("stamp_masu", Integer),
Column("daily_bonus", Integer),
Column("weekly_bonus", Integer),
Column("weekday_bonus", Integer),
Column("weekend_bonus", Integer),
Column("total_bonus", Integer),
Column("day_total_bonus", Integer),
Column("store_battle_bonus", Integer),
Column("story_bonus", Integer),
Column("online_battle_bonus", Integer),
Column("timetrial_bonus", Integer),
Column("fasteststreetlegaltheory_bonus", Integer),
Column("collaboration_bonus", Integer),
Column("add_bonus_daily_flag_1", Integer),
Column("add_bonus_daily_flag_2", Integer),
Column("add_bonus_daily_flag_3", Integer),
Column("create_date_daily", TIMESTAMP, server_default=func.now()),
Column("create_date_weekly", TIMESTAMP, server_default=func.now()),
UniqueConstraint("user", "m_stamp_event_id", name="idac_user_stamp_uk"),
mysql_charset="utf8mb4",
)
timetrial_event = Table(
"idac_user_timetrial_event",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("timetrial_event_id", Integer),
Column("point", Integer),
UniqueConstraint("user", "timetrial_event_id", name="idac_user_timetrial_event_uk"),
mysql_charset="utf8mb4",
)
class IDACItemData(BaseData):
def get_random_user_car(self, aime_id: int, version: int) -> Optional[List[Row]]:
sql = (
select(car)
.where(and_(car.c.user == aime_id, car.c.version == version))
.order_by(func.rand())
.limit(1)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_random_car(self, version: int) -> Optional[List[Row]]:
sql = select(car).where(car.c.version == version).order_by(func.rand()).limit(1)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_car(
self, aime_id: int, version: int, style_car_id: int
) -> Optional[List[Row]]:
sql = select(car).where(
and_(
car.c.user == aime_id,
car.c.version == version,
car.c.style_car_id == style_car_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_cars(
self, version: int, aime_id: int, only_pickup: bool = False
) -> Optional[List[Row]]:
if only_pickup:
sql = select(car).where(
and_(
car.c.user == aime_id,
car.c.version == version,
car.c.pickup_seq != 0,
)
)
else:
sql = select(car).where(
and_(car.c.user == aime_id, car.c.version == version)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_ticket(self, aime_id: int, ticket_id: int) -> Optional[Row]:
sql = select(ticket).where(
ticket.c.user == aime_id, ticket.c.ticket_id == ticket_id
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_tickets(self, aime_id: int) -> Optional[List[Row]]:
sql = select(ticket).where(ticket.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_story(self, aime_id: int, chapter_id: int) -> Optional[Row]:
sql = select(story).where(
and_(story.c.user == aime_id, story.c.chapter == chapter_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_stories(self, aime_id: int) -> Optional[List[Row]]:
sql = select(story).where(story.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_story_episodes(self, aime_id: int, chapter_id: int) -> Optional[List[Row]]:
sql = select(episode).where(
and_(episode.c.user == aime_id, episode.c.chapter == chapter_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_story_episode(self, aime_id: int, episode_id: int) -> Optional[Row]:
sql = select(episode).where(
and_(episode.c.user == aime_id, episode.c.episode == episode_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_story_episode_difficulties(
self, aime_id: int, episode_id: int
) -> Optional[List[Row]]:
sql = select(difficulty).where(
and_(difficulty.c.user == aime_id, difficulty.c.episode == episode_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_courses(self, aime_id: int) -> Optional[List[Row]]:
sql = select(course).where(course.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_course(self, aime_id: int, course_id: int) -> Optional[Row]:
sql = select(course).where(
and_(course.c.user == aime_id, course.c.course_id == course_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_time_trial_courses(self, version: int) -> Optional[List[Row]]:
sql = select(trial.c.course_id).where(trial.c.version == version).distinct()
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_time_trial_user_best_time_by_course_car(
self, version: int, aime_id: int, course_id: int, style_car_id: int
) -> Optional[Row]:
sql = select(trial).where(
and_(
trial.c.user == aime_id,
trial.c.version == version,
trial.c.course_id == course_id,
trial.c.style_car_id == style_car_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_time_trial_user_best_courses(
self, version: int, aime_id: int
) -> Optional[List[Row]]:
# get for a given aime_id the best time for each course
subquery = (
select(
trial.c.version,
func.min(trial.c.goal_time).label("min_goal_time"),
trial.c.course_id,
)
.where(and_(trial.c.version == version, trial.c.user == aime_id))
.group_by(trial.c.course_id)
.subquery()
)
# now get the full row for each best time
sql = select(trial).where(
and_(
trial.c.version == subquery.c.version,
trial.c.goal_time == subquery.c.min_goal_time,
trial.c.course_id == subquery.c.course_id,
trial.c.user == aime_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_time_trial_best_cars_by_course(
self, version: int, aime_id: int, course_id: int
) -> Optional[List[Row]]:
subquery = (
select(
trial.c.version,
func.min(trial.c.goal_time).label("min_goal_time"),
trial.c.style_car_id,
)
.where(
and_(
trial.c.version == version,
trial.c.user == aime_id,
trial.c.course_id == course_id,
)
)
.group_by(trial.c.style_car_id)
.subquery()
)
sql = select(trial).where(
and_(
trial.c.version == subquery.c.version,
trial.c.goal_time == subquery.c.min_goal_time,
trial.c.style_car_id == subquery.c.style_car_id,
trial.c.course_id == course_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_time_trial_ranking_by_course(
self,
version: int,
course_id: int,
style_car_id: Optional[int] = None,
limit: Optional[int] = 10,
) -> Optional[List[Row]]:
# get the top 10 ranking by goal_time for a given course which is grouped by user
subquery = select(
trial.c.version,
trial.c.user,
func.min(trial.c.goal_time).label("min_goal_time"),
).where(and_(trial.c.version == version, trial.c.course_id == course_id))
# if wantd filter only by style_car_id
if style_car_id is not None:
subquery = subquery.where(trial.c.style_car_id == style_car_id)
subquery = subquery.group_by(trial.c.user).subquery()
sql = (
select(trial)
.where(
and_(
trial.c.version == subquery.c.version,
trial.c.user == subquery.c.user,
trial.c.goal_time == subquery.c.min_goal_time,
),
)
.order_by(trial.c.goal_time)
)
# limit the result if needed
if limit is not None:
sql = sql.limit(limit)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_time_trial_best_ranking_by_course(
self, version: int, aime_id: int, course_id: int
) -> Optional[Row]:
sql = (
select(trial)
.where(
and_(
trial.c.version == version,
trial.c.user == aime_id,
trial.c.course_id == course_id,
),
)
.order_by(trial.c.goal_time)
.limit(1)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_challenge(
self, aime_id: int, vs_type: int, play_difficulty: int
) -> Optional[Row]:
sql = select(challenge).where(
and_(
challenge.c.user == aime_id,
challenge.c.vs_type == vs_type,
challenge.c.play_difficulty == play_difficulty,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_challenges(self, aime_id: int) -> Optional[List[Row]]:
sql = select(challenge).where(challenge.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_best_challenges_by_vs_type(
self, aime_id: int, story_type: int = 4
) -> Optional[List[Row]]:
sql = (
select(
challenge.c.story_type,
challenge.c.vs_type,
func.max(challenge.c.cleared_difficulty).label("max_clear_lv"),
func.max(challenge.c.play_difficulty).label("last_play_lv"),
challenge.c.course_id,
challenge.c.play_count,
)
.where(
and_(challenge.c.user == aime_id, challenge.c.story_type == story_type)
)
.group_by(challenge.c.vs_type, challenge.c.course_id, challenge.c.play_count)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_theory_courses(self, aime_id: int) -> Optional[List[Row]]:
sql = select(theory_course).where(theory_course.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_theory_course_by_powerhouse_lv(
self, aime_id: int, course_id: int, powerhouse_lv: int, count: int = 3
) -> Optional[List[Row]]:
sql = (
select(theory_course)
.where(
and_(
theory_course.c.user != aime_id,
theory_course.c.course_id == course_id,
theory_course.c.powerhouse_lv == powerhouse_lv,
)
)
.order_by(func.rand())
.limit(count)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_theory_course(self, aime_id: int, course_id: int) -> Optional[List[Row]]:
sql = select(theory_course).where(
and_(
theory_course.c.user == aime_id, theory_course.c.course_id == course_id
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_theory_partners(self, aime_id: int) -> Optional[List[Row]]:
sql = select(theory_partner).where(theory_partner.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_theory_running(self, aime_id: int) -> Optional[List[Row]]:
sql = select(theory_running).where(theory_running.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_theory_running_by_course(
self, aime_id: int, course_id: int
) -> Optional[Row]:
sql = select(theory_running).where(
and_(
theory_running.c.user == aime_id,
theory_running.c.course_id == course_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_vs_infos(self, aime_id: int) -> Optional[List[Row]]:
sql = select(vs_info).where(vs_info.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_stamps(self, aime_id: int) -> Optional[List[Row]]:
sql = select(stamp).where(
and_(
stamp.c.user == aime_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_timetrial_event(self, aime_id: int, timetrial_event_id: int) -> Optional[Row]:
sql = select(timetrial_event).where(
and_(
timetrial_event.c.user == aime_id,
timetrial_event.c.timetrial_event_id == timetrial_event_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_car(self, aime_id: int, version: int, car_data: Dict) -> Optional[int]:
car_data["user"] = aime_id
car_data["version"] = version
sql = insert(car).values(**car_data)
conflict = sql.on_duplicate_key_update(**car_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_car: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def put_ticket(self, aime_id: int, ticket_data: Dict) -> Optional[int]:
ticket_data["user"] = aime_id
sql = insert(ticket).values(**ticket_data)
conflict = sql.on_duplicate_key_update(**ticket_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_ticket: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def put_story(self, aime_id: int, story_data: Dict) -> Optional[int]:
story_data["user"] = aime_id
sql = insert(story).values(**story_data)
conflict = sql.on_duplicate_key_update(**story_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_story: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def put_story_episode_play_status(
self, aime_id: int, chapter_id: int, play_status: int = 1
) -> Optional[int]:
sql = (
update(episode)
.where(and_(episode.c.user == aime_id, episode.c.chapter == chapter_id))
.values(play_status=play_status)
)
result = self.execute(sql)
if result is None:
self.logger.warn(
f"put_story_episode_play_status: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid
def put_story_episode(
self, aime_id: int, chapter_id: int, episode_data: Dict
) -> Optional[int]:
episode_data["user"] = aime_id
episode_data["chapter"] = chapter_id
sql = insert(episode).values(**episode_data)
conflict = sql.on_duplicate_key_update(**episode_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_story_episode: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def put_story_episode_difficulty(
self, aime_id: int, episode_id: int, difficulty_data: Dict
) -> Optional[int]:
difficulty_data["user"] = aime_id
difficulty_data["episode"] = episode_id
sql = insert(difficulty).values(**difficulty_data)
conflict = sql.on_duplicate_key_update(**difficulty_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"put_story_episode_difficulty: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid
def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]:
course_data["user"] = aime_id
sql = insert(course).values(**course_data)
conflict = sql.on_duplicate_key_update(**course_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_course: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def put_time_trial(
self, version: int, aime_id: int, time_trial_data: Dict
) -> Optional[int]:
time_trial_data["user"] = aime_id
time_trial_data["version"] = version
sql = insert(trial).values(**time_trial_data)
conflict = sql.on_duplicate_key_update(**time_trial_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_time_trial: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def put_challenge(self, aime_id: int, challenge_data: Dict) -> Optional[int]:
challenge_data["user"] = aime_id
sql = insert(challenge).values(**challenge_data)
conflict = sql.on_duplicate_key_update(**challenge_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_challenge: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def put_theory_course(
self, aime_id: int, theory_course_data: Dict
) -> Optional[int]:
theory_course_data["user"] = aime_id
sql = insert(theory_course).values(**theory_course_data)
conflict = sql.on_duplicate_key_update(**theory_course_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_theory_course: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def put_theory_partner(
self, aime_id: int, theory_partner_data: Dict
) -> Optional[int]:
theory_partner_data["user"] = aime_id
sql = insert(theory_partner).values(**theory_partner_data)
conflict = sql.on_duplicate_key_update(**theory_partner_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"put_theory_partner: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid
def put_theory_running(
self, aime_id: int, theory_running_data: Dict
) -> Optional[int]:
theory_running_data["user"] = aime_id
sql = insert(theory_running).values(**theory_running_data)
conflict = sql.on_duplicate_key_update(**theory_running_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"put_theory_running: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid
def put_vs_info(self, aime_id: int, vs_info_data: Dict) -> Optional[int]:
vs_info_data["user"] = aime_id
sql = insert(vs_info).values(**vs_info_data)
conflict = sql.on_duplicate_key_update(**vs_info_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_vs_info: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def put_stamp(
self, aime_id: int, stamp_data: Dict
) -> Optional[int]:
stamp_data["user"] = aime_id
sql = insert(stamp).values(**stamp_data)
conflict = sql.on_duplicate_key_update(**stamp_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"putstamp: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid
def put_timetrial_event(
self, aime_id: int, time_trial_event_id: int, point: int
) -> Optional[int]:
timetrial_event_data = {
"user": aime_id,
"timetrial_event_id": time_trial_event_id,
"point": point,
}
sql = insert(timetrial_event).values(**timetrial_event_data)
conflict = sql.on_duplicate_key_update(**timetrial_event_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"put_timetrial_event: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid

View File

@ -0,0 +1,440 @@
from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger
from sqlalchemy.engine.base import Connection
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
from core.config import CoreConfig
profile = Table(
"idac_profile",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("username", String(8)),
Column("country", Integer),
Column("store", Integer),
Column("team_id", Integer, server_default="0"),
Column("total_play", Integer, server_default="0"),
Column("daily_play", Integer, server_default="0"),
Column("day_play", Integer, server_default="0"),
Column("mileage", Integer, server_default="0"),
Column("asset_version", Integer, server_default="1"),
Column("last_play_date", TIMESTAMP, server_default=func.now()),
Column("mytitle_id", Integer, server_default="0"),
Column("mytitle_efffect_id", Integer, server_default="0"),
Column("sticker_id", Integer, server_default="0"),
Column("sticker_effect_id", Integer, server_default="0"),
Column("papercup_id", Integer, server_default="0"),
Column("tachometer_id", Integer, server_default="0"),
Column("aura_id", Integer, server_default="0"),
Column("aura_color_id", Integer, server_default="0"),
Column("aura_line_id", Integer, server_default="0"),
Column("bgm_id", Integer, server_default="0"),
Column("keyholder_id", Integer, server_default="0"),
Column("start_menu_bg_id", Integer, server_default="0"),
Column("use_car_id", Integer, server_default="1"),
Column("use_style_car_id", Integer, server_default="1"),
Column("bothwin_count", Integer, server_default="0"),
Column("bothwin_score", Integer, server_default="0"),
Column("subcard_count", Integer, server_default="0"),
Column("vs_history", Integer, server_default="0"),
Column("stamp_key_assign_0", Integer),
Column("stamp_key_assign_1", Integer),
Column("stamp_key_assign_2", Integer),
Column("stamp_key_assign_3", Integer),
Column("name_change_category", Integer, server_default="0"),
Column("factory_disp", Integer, server_default="0"),
Column("create_date", TIMESTAMP, server_default=func.now()),
Column("cash", Integer, server_default="0"),
Column("dressup_point", Integer, server_default="0"),
Column("avatar_point", Integer, server_default="0"),
Column("total_cash", Integer, server_default="0"),
UniqueConstraint("user", "version", name="idac_profile_uk"),
mysql_charset="utf8mb4",
)
# No point setting defaults since the game sends everything on profile creation anyway
config = Table(
"idac_profile_config",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("config_id", Integer),
Column("steering_intensity", Integer),
Column("transmission_type", Integer),
Column("default_viewpoint", Integer),
Column("favorite_bgm", Integer),
Column("bgm_volume", Integer),
Column("se_volume", Integer),
Column("master_volume", Integer),
Column("store_battle_policy", Integer),
Column("battle_onomatope_display", Integer),
Column("cornering_guide", Integer),
Column("minimap", Integer),
Column("line_guide", Integer),
Column("ghost", Integer),
Column("race_exit", Integer),
Column("result_skip", Integer),
Column("stamp_select_skip", Integer),
UniqueConstraint("user", name="idac_profile_config_uk"),
mysql_charset="utf8mb4",
)
# No point setting defaults since the game sends everything on profile creation anyway
avatar = Table(
"idac_profile_avatar",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("sex", Integer),
Column("face", Integer),
Column("eye", Integer),
Column("mouth", Integer),
Column("hair", Integer),
Column("glasses", Integer),
Column("face_accessory", Integer),
Column("body", Integer),
Column("body_accessory", Integer),
Column("behind", Integer),
Column("bg", Integer),
Column("effect", Integer),
Column("special", Integer),
UniqueConstraint("user", name="idac_profile_avatar_uk"),
mysql_charset="utf8mb4",
)
rank = Table(
"idac_profile_rank",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("story_rank_exp", Integer, server_default="0"),
Column("story_rank", Integer, server_default="1"),
Column("time_trial_rank_exp", Integer, server_default="0"),
Column("time_trial_rank", Integer, server_default="1"),
Column("online_battle_rank_exp", Integer, server_default="0"),
Column("online_battle_rank", Integer, server_default="1"),
Column("store_battle_rank_exp", Integer, server_default="0"),
Column("store_battle_rank", Integer, server_default="1"),
Column("theory_exp", Integer, server_default="0"),
Column("theory_rank", Integer, server_default="1"),
Column("pride_group_id", Integer, server_default="0"),
Column("pride_point", Integer, server_default="0"),
Column("grade_exp", Integer, server_default="0"),
Column("grade", Integer, server_default="1"),
Column("grade_reward_dist", Integer, server_default="0"),
Column("story_rank_reward_dist", Integer, server_default="0"),
Column("time_trial_rank_reward_dist", Integer, server_default="0"),
Column("online_battle_rank_reward_dist", Integer, server_default="0"),
Column("store_battle_rank_reward_dist", Integer, server_default="0"),
Column("theory_rank_reward_dist", Integer, server_default="0"),
Column("max_attained_online_battle_rank", Integer, server_default="1"),
Column("max_attained_pride_point", Integer, server_default="0"),
Column("is_last_max", Integer, server_default="0"),
UniqueConstraint("user", "version", name="idac_profile_rank_uk"),
mysql_charset="utf8mb4",
)
stock = Table(
"idac_profile_stock",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("mytitle_list", String(1024), server_default=""),
Column("mytitle_new_list", String(1024), server_default=""),
Column("avatar_face_list", String(255), server_default=""),
Column("avatar_face_new_list", String(255), server_default=""),
Column("avatar_eye_list", String(255), server_default=""),
Column("avatar_eye_new_list", String(255), server_default=""),
Column("avatar_hair_list", String(255), server_default=""),
Column("avatar_hair_new_list", String(255), server_default=""),
Column("avatar_body_list", String(255), server_default=""),
Column("avatar_body_new_list", String(255), server_default=""),
Column("avatar_mouth_list", String(255), server_default=""),
Column("avatar_mouth_new_list", String(255), server_default=""),
Column("avatar_glasses_list", String(255), server_default=""),
Column("avatar_glasses_new_list", String(255), server_default=""),
Column("avatar_face_accessory_list", String(255), server_default=""),
Column("avatar_face_accessory_new_list", String(255), server_default=""),
Column("avatar_body_accessory_list", String(255), server_default=""),
Column("avatar_body_accessory_new_list", String(255), server_default=""),
Column("avatar_behind_list", String(255), server_default=""),
Column("avatar_behind_new_list", String(255), server_default=""),
Column("avatar_bg_list", String(255), server_default=""),
Column("avatar_bg_new_list", String(255), server_default=""),
Column("avatar_effect_list", String(255), server_default=""),
Column("avatar_effect_new_list", String(255), server_default=""),
Column("avatar_special_list", String(255), server_default=""),
Column("avatar_special_new_list", String(255), server_default=""),
Column("stamp_list", String(255), server_default=""),
Column("stamp_new_list", String(255), server_default=""),
Column("keyholder_list", String(256), server_default=""),
Column("keyholder_new_list", String(256), server_default=""),
Column("papercup_list", String(255), server_default=""),
Column("papercup_new_list", String(255), server_default=""),
Column("tachometer_list", String(255), server_default=""),
Column("tachometer_new_list", String(255), server_default=""),
Column("aura_list", String(255), server_default=""),
Column("aura_new_list", String(255), server_default=""),
Column("aura_color_list", String(255), server_default=""),
Column("aura_color_new_list", String(255), server_default=""),
Column("aura_line_list", String(255), server_default=""),
Column("aura_line_new_list", String(255), server_default=""),
Column("bgm_list", String(255), server_default=""),
Column("bgm_new_list", String(255), server_default=""),
Column("dx_color_list", String(255), server_default=""),
Column("dx_color_new_list", String(255), server_default=""),
Column("start_menu_bg_list", String(255), server_default=""),
Column("start_menu_bg_new_list", String(255), server_default=""),
Column("under_neon_list", String(255), server_default=""),
UniqueConstraint("user", "version", name="idac_profile_stock_uk"),
mysql_charset="utf8mb4",
)
theory = Table(
"idac_profile_theory",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("play_count", Integer, server_default="0"),
Column("play_count_multi", Integer, server_default="0"),
Column("partner_id", Integer),
Column("partner_progress", Integer),
Column("partner_progress_score", Integer),
Column("practice_start_rank", Integer, server_default="0"),
Column("general_flag", Integer, server_default="0"),
Column("vs_history", Integer, server_default="0"),
Column("vs_history_multi", Integer, server_default="0"),
Column("win_count", Integer, server_default="0"),
Column("win_count_multi", Integer, server_default="0"),
UniqueConstraint("user", "version", name="idac_profile_theory_uk"),
mysql_charset="utf8mb4",
)
class IDACProfileData(BaseData):
def __init__(self, cfg: CoreConfig, conn: Connection) -> None:
super().__init__(cfg, conn)
self.date_time_format_ext = (
"%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5]
)
self.date_time_format_short = "%Y-%m-%d"
def get_profile(self, aime_id: int, version: int) -> Optional[Row]:
sql = select(profile).where(
and_(
profile.c.user == aime_id,
profile.c.version == version,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_different_random_profiles(
self, aime_id: int, version: int, count: int = 9
) -> Optional[Row]:
sql = (
select(profile)
.where(
and_(
profile.c.user != aime_id,
profile.c.version == version,
)
)
.order_by(func.rand())
.limit(count)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_profile_config(self, aime_id: int) -> Optional[Row]:
sql = select(config).where(
and_(
config.c.user == aime_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_profile_avatar(self, aime_id: int) -> Optional[Row]:
sql = select(avatar).where(
and_(
avatar.c.user == aime_id,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_profile_rank(self, aime_id: int, version: int) -> Optional[Row]:
sql = select(rank).where(
and_(
rank.c.user == aime_id,
rank.c.version == version,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_profile_stock(self, aime_id: int, version: int) -> Optional[Row]:
sql = select(stock).where(
and_(
stock.c.user == aime_id,
stock.c.version == version,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_profile_theory(self, aime_id: int, version: int) -> Optional[Row]:
sql = select(theory).where(
and_(
theory.c.user == aime_id,
theory.c.version == version,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_profile(
self, aime_id: int, version: int, profile_data: Dict
) -> Optional[int]:
profile_data["user"] = aime_id
profile_data["version"] = version
sql = insert(profile).values(**profile_data)
conflict = sql.on_duplicate_key_update(**profile_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def put_profile_config(self, aime_id: int, config_data: Dict) -> Optional[int]:
config_data["user"] = aime_id
sql = insert(config).values(**config_data)
conflict = sql.on_duplicate_key_update(**config_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"put_profile_config: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid
def put_profile_avatar(self, aime_id: int, avatar_data: Dict) -> Optional[int]:
avatar_data["user"] = aime_id
sql = insert(avatar).values(**avatar_data)
conflict = sql.on_duplicate_key_update(**avatar_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"put_profile_avatar: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid
def put_profile_rank(
self, aime_id: int, version: int, rank_data: Dict
) -> Optional[int]:
rank_data["user"] = aime_id
rank_data["version"] = version
sql = insert(rank).values(**rank_data)
conflict = sql.on_duplicate_key_update(**rank_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile_rank: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def put_profile_stock(
self, aime_id: int, version: int, stock_data: Dict
) -> Optional[int]:
stock_data["user"] = aime_id
stock_data["version"] = version
sql = insert(stock).values(**stock_data)
conflict = sql.on_duplicate_key_update(**stock_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile_stock: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def put_profile_theory(
self, aime_id: int, version: int, theory_data: Dict
) -> Optional[int]:
theory_data["user"] = aime_id
theory_data["version"] = version
sql = insert(theory).values(**theory_data)
conflict = sql.on_duplicate_key_update(**theory_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"put_profile_theory: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid

2106
titles/idac/season2.py Normal file

File diff suppressed because it is too large Load Diff