Compare commits

..

No commits in common. "idac" and "master" have entirely different histories.
idac ... master

54 changed files with 589 additions and 3511 deletions

5
.gitignore vendored
View File

@ -160,7 +160,4 @@ config/*
deliver/*
*.gz
dbdump-*.json
/.vs
/titles/id8
/titles/idac/battle.py
dbdump-*.json

View File

@ -62,12 +62,6 @@ Documenting updates to ARTEMiS, to be updated every time the master branch is pu
### DIVA
+ Fix reader for when dificulty is not a int
## 20240612
+ Support Initial D THE ARCADE v1.70
+ Added special mode based on device version
+ Added number plate lottery
+ Updated online battle rounds handling
## 20240526
### DIVA
+ Fixed missing awaits causing coroutine error

View File

@ -1,5 +1,7 @@
from core.config import CoreConfig
from core.allnet import AllnetServlet, BillingServlet
from core.aimedb import AimedbServlette
from core.title import TitleServlet
from core.utils import Utils
from core.mucha import MuchaServlet
from core.frontend import FrontendServlet

View File

@ -120,7 +120,7 @@ class ADBHeader:
if self.store_id == 0:
raise ADBHeaderException(f"Store ID cannot be 0!")
if re.fullmatch(r"^A[0-9]{2}[A-Z][0-9]{2}[A-HJ-NP-Z][0-9]{4}$", self.keychip_id) is None:
if re.fullmatch(r"^A[0-9]{2}[E|X][0-9]{2}[A-HJ-NP-Z][0-9]{4}$", self.keychip_id) is None:
raise ADBHeaderException(f"Keychip ID {self.keychip_id} is invalid!")
return True

View File

@ -9,8 +9,7 @@ from starlette.responses import PlainTextResponse
from os import environ, path, mkdir, W_OK, access
from typing import List
from core import CoreConfig, TitleServlet, MuchaServlet
from core.allnet import AllnetServlet, BillingServlet
from core import CoreConfig, TitleServlet, MuchaServlet, AllnetServlet, BillingServlet, AimedbServlette
from core.frontend import FrontendServlet
async def dummy_rt(request: Request):

View File

@ -1,75 +0,0 @@
"""idac rounds event info added
Revision ID: 202d1ada1b39
Revises: 7dc13e364e53
Create Date: 2024-05-03 15:51:02.384863
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '202d1ada1b39'
down_revision = '7dc13e364e53'
branch_labels = None
depends_on = None
def upgrade():
op.execute("DROP TABLE IF EXISTS idac_round_info")
op.execute("DROP TABLE IF EXISTS idac_user_round_info")
op.create_table(
"idac_round_info",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("round_id_in_json", sa.Integer(), nullable=True),
sa.Column("name", sa.String(64), nullable=True),
sa.Column("season", sa.Integer(), nullable=True),
sa.Column(
"start_dt",
sa.TIMESTAMP(),
server_default=sa.text("now()"),
nullable=True,
),
sa.Column(
"end_dt",
sa.TIMESTAMP(),
server_default=sa.text("now()"),
nullable=True,
),
sa.PrimaryKeyConstraint("id"),
mysql_charset="utf8mb4",
)
op.create_table(
"idac_user_round_info",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user", sa.Integer(), nullable=False),
sa.Column("round_id", sa.Integer(), nullable=True),
sa.Column("count", sa.Integer(), nullable=True),
sa.Column("win", sa.Integer(), nullable=True),
sa.Column("point", sa.Integer(), nullable=True),
sa.Column(
"play_dt",
sa.TIMESTAMP(),
server_default=sa.text("now()"),
nullable=True,
),
sa.ForeignKeyConstraint(
["user"], ["aime_user.id"], onupdate="cascade", ondelete="cascade"
),
sa.ForeignKeyConstraint(
["round_id"], ["idac_round_info.id"], onupdate="cascade", ondelete="cascade"
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"user", "round_id", name="idac_user_round_info_uk"
),
mysql_charset="utf8mb4",
)
def downgrade():
op.drop_table("idac_round_info")
op.drop_table("idac_user_round_info")

View File

@ -1,7 +1,7 @@
"""add_event_log_info
Revision ID: 2bf9f38d9444
Revises: e4e8d89c9b02
Revises: 81e44dd6047a
Create Date: 2024-05-21 23:00:17.468407
"""
@ -11,7 +11,7 @@ from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '2bf9f38d9444'
down_revision = 'e4e8d89c9b02'
down_revision = '81e44dd6047a'
branch_labels = None
depends_on = None

View File

@ -1,45 +0,0 @@
"""IDAC plate number lottery added
Revision ID: 7e98c2c328b1
Revises: 202d1ada1b39
Create Date: 2024-05-07 12:25:27.606480
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7e98c2c328b1'
down_revision = '202d1ada1b39'
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"idac_user_lottery",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user", sa.Integer(), nullable=False),
sa.Column("version", sa.Integer(), nullable=False),
sa.Column("saved_value", sa.Integer(), nullable=False),
sa.Column("lottery_count", sa.Integer(), nullable=False),
sa.Column(
"create_date",
sa.TIMESTAMP(),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(
["user"], ["aime_user.id"], onupdate="cascade", ondelete="cascade"
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"user", "version", name="idac_user_lottery_uk"
),
mysql_charset="utf8mb4",
)
def downgrade():
op.drop_table("idac_user_lottery")

View File

@ -1,7 +1,7 @@
"""ongeki: fix clearStatus
Revision ID: 8ad40a6e7be2
Revises: aeb6b1e28354
Revises: 7dc13e364e53
Create Date: 2024-05-29 19:03:30.062157
"""
@ -11,7 +11,7 @@ from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '8ad40a6e7be2'
down_revision = 'aeb6b1e28354'
down_revision = '7dc13e364e53'
branch_labels = None
depends_on = None

View File

@ -1,26 +0,0 @@
"""IDAC device_version added
Revision ID: aeb6b1e28354
Revises: 7e98c2c328b1
Create Date: 2024-05-29 17:40:45.123656
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'aeb6b1e28354'
down_revision = '7e98c2c328b1'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('idac_profile', sa.Column('device_version', sa.String(length=7), server_default='1.50.00', nullable=True))
op.drop_column('idac_profile', 'asset_version')
def downgrade():
op.add_column('idac_profile', sa.Column('asset_version', mysql.INTEGER(), server_default="1", nullable=True))
op.drop_column('idac_profile', 'device_version')

View File

@ -1,83 +0,0 @@
"""IDAC Battle Gift and Tips added
Revision ID: e4e8d89c9b02
Revises: 81e44dd6047a
Create Date: 2024-04-01 17:49:50.009718
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = "e4e8d89c9b02"
down_revision = "81e44dd6047a"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"idac_user_battle_gift",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user", sa.Integer(), nullable=False),
sa.Column("battle_gift_event_id", sa.Integer(), nullable=True),
sa.Column("gift_id", sa.Integer(), nullable=True),
sa.Column("gift_status", sa.Integer(), nullable=True),
sa.Column(
"received_date",
sa.TIMESTAMP(),
server_default=sa.text("now()"),
nullable=True,
),
sa.ForeignKeyConstraint(
["user"], ["aime_user.id"], onupdate="cascade", ondelete="cascade"
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"user", "battle_gift_event_id", "gift_id", name="idac_user_battle_gift_uk"
),
mysql_charset="utf8mb4",
)
op.create_table(
"idac_profile_tips",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user", sa.Integer(), nullable=False),
sa.Column("version", sa.Integer(), nullable=False),
sa.Column(
"tips_list",
sa.String(length=16),
server_default="QAAAAAAAAAAAAAAA",
nullable=True,
),
sa.Column(
"timetrial_play_count", sa.Integer(), server_default="0", nullable=True
),
sa.Column("story_play_count", sa.Integer(), server_default="0", nullable=True),
sa.Column(
"store_battle_play_count", sa.Integer(), server_default="0", nullable=True
),
sa.Column(
"online_battle_play_count", sa.Integer(), server_default="0", nullable=True
),
sa.Column(
"special_play_count", sa.Integer(), server_default="0", nullable=True
),
sa.Column(
"challenge_play_count", sa.Integer(), server_default="0", nullable=True
),
sa.Column("theory_play_count", sa.Integer(), server_default="0", nullable=True),
sa.ForeignKeyConstraint(
["user"], ["aime_user.id"], onupdate="cascade", ondelete="cascade"
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("user", "version", name="idac_profile_tips_uk"),
mysql_charset="utf8mb4",
)
def downgrade():
op.drop_table("idac_user_battle_gift")
op.drop_table("idac_profile_tips")

View File

@ -232,7 +232,7 @@ class ArcadeData(BaseData):
return f"{platform_code}{'-' if dash else ''}{platform_rev:02d}{serial_letter}{serial_num:04d}{append:04d}"
def validate_keychip_format(self, serial: str) -> bool:
# For the 2nd letter, E and X are the only "real" values that have been observed (A is used for generated keychips)
# For the 2nd letter, E and X are the only "real" values that have been observed
if re.fullmatch(r"^A[0-9]{2}[A-Z][-]?[0-9]{2}[A-HJ-NP-Z][0-9]{4}([0-9]{4})?$", serial) is None:
return False

View File

@ -1,54 +0,0 @@
SET FOREIGN_KEY_CHECKS=0;
-- WARNING: This script is NOT idempotent! MAKE A BACKUP BEFORE RUNNING THIS SCRIPT!
-- Drop UK idac_user_vs_info_uk
ALTER TABLE idac_user_vs_info
DROP FOREIGN KEY idac_user_vs_info_ibfk_1,
DROP INDEX idac_user_vs_info_uk;
-- Drop the new columns added to the original table
ALTER TABLE idac_user_vs_info
DROP COLUMN battle_mode,
DROP COLUMN invalid,
DROP COLUMN str,
DROP COLUMN str_now,
DROP COLUMN lose_now;
-- Add back the old columns to the original table
ALTER TABLE idac_user_vs_info
ADD COLUMN group_key VARCHAR(25),
ADD COLUMN win_flg INT,
ADD COLUMN style_car_id INT,
ADD COLUMN course_id INT,
ADD COLUMN course_day INT,
ADD COLUMN players_num INT,
ADD COLUMN winning INT,
ADD COLUMN advantage_1 INT,
ADD COLUMN advantage_2 INT,
ADD COLUMN advantage_3 INT,
ADD COLUMN advantage_4 INT,
ADD COLUMN select_course_id INT,
ADD COLUMN select_course_day INT,
ADD COLUMN select_course_random INT,
ADD COLUMN matching_success_sec INT,
ADD COLUMN boost_flag INT;
-- Delete the data from the original table where group_key is NULL
DELETE FROM idac_user_vs_info
WHERE group_key IS NULL;
-- Insert data back to the original table from idac_user_vs_course_info
INSERT INTO idac_user_vs_info (user, group_key, win_flg, style_car_id, course_id, course_day, players_num, winning, advantage_1, advantage_2, advantage_3, advantage_4, select_course_id, select_course_day, select_course_random, matching_success_sec, boost_flag, vs_history, break_count, break_penalty_flag)
SELECT user, CONCAT(FLOOR(RAND()*(99999999999999-10000000000000+1)+10000000000000), 'A69E01A8888'), 0, 0, course_id, 0, 0, vs_cnt, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
FROM idac_user_vs_course_info;
-- Add back the constraints and indexes to the original table
ALTER TABLE idac_user_vs_info
ADD CONSTRAINT idac_user_vs_info_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE,
ADD UNIQUE KEY idac_user_vs_info_uk (user, group_key);
-- Drop the new table idac_user_vs_course_info
DROP TABLE IF EXISTS idac_user_vs_course_info;
SET FOREIGN_KEY_CHECKS=1;

View File

@ -1,71 +0,0 @@
SET FOREIGN_KEY_CHECKS=0;
-- WARNING: This script is NOT idempotent! MAKE A BACKUP BEFORE RUNNING THIS SCRIPT!
-- Create the new table idac_user_vs_course_info
CREATE TABLE idac_user_vs_course_info (
id INT PRIMARY KEY AUTO_INCREMENT,
user INT,
battle_mode INT,
course_id INT,
vs_cnt INT,
vs_win INT,
CONSTRAINT idac_user_vs_course_info_fk FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE KEY idac_user_vs_course_info_uk (user, battle_mode, course_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Insert data from the original table to the new tables
INSERT INTO idac_user_vs_course_info (user, battle_mode, course_id, vs_cnt, vs_win)
SELECT user, 1 as battle_mode, course_id, COUNT(winning) as vs_cnt, SUM(win_flg) as vs_win
FROM idac_user_vs_info
GROUP BY user, course_id;
-- Drop UK idac_user_vs_info_uk
ALTER TABLE idac_user_vs_info
DROP FOREIGN KEY idac_user_vs_info_ibfk_1,
DROP INDEX idac_user_vs_info_uk;
-- Drop/Add the old columns from the original table
ALTER TABLE idac_user_vs_info
DROP COLUMN group_key,
DROP COLUMN win_flg,
DROP COLUMN style_car_id,
DROP COLUMN course_id,
DROP COLUMN course_day,
DROP COLUMN players_num,
DROP COLUMN winning,
DROP COLUMN advantage_1,
DROP COLUMN advantage_2,
DROP COLUMN advantage_3,
DROP COLUMN advantage_4,
DROP COLUMN select_course_id,
DROP COLUMN select_course_day,
DROP COLUMN select_course_random,
DROP COLUMN matching_success_sec,
DROP COLUMN boost_flag,
ADD COLUMN battle_mode TINYINT UNSIGNED DEFAULT 1 NOT NULL AFTER user,
ADD COLUMN invalid INT DEFAULT 0,
ADD COLUMN str INT DEFAULT 0,
ADD COLUMN str_now INT DEFAULT 0,
ADD COLUMN lose_now INT DEFAULT 0;
-- Create a temporary table to store the records you want to keep
CREATE TEMPORARY TABLE temp_table AS
SELECT MIN(id) AS min_id
FROM idac_user_vs_info
GROUP BY battle_mode, user;
-- Delete records from the original table based on the temporary table
DELETE FROM idac_user_vs_info
WHERE id NOT IN (SELECT min_id FROM temp_table);
-- Drop the temporary table
DROP TEMPORARY TABLE IF EXISTS temp_table;
-- Add UK idac_user_vs_info_uk
ALTER TABLE idac_user_vs_info
ADD CONSTRAINT idac_user_vs_info_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE,
ADD UNIQUE KEY idac_user_vs_info_uk (user, battle_mode);
SET FOREIGN_KEY_CHECKS=1;

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
import argparse
import logging
from os import mkdir, path, access, W_OK, environ
from os import mkdir, path, access, W_OK
import yaml
import asyncio
@ -25,11 +25,10 @@ if __name__ == "__main__":
parser.add_argument("action", type=str, help="create, upgrade, downgrade, create-owner, migrate, create-revision, create-autorevision")
args = parser.parse_args()
environ["ARTEMIS_CFG_DIR"] = args.config
cfg = CoreConfig()
if path.exists(f"{args.config}/core.yaml"):
cfg_dict = yaml.safe_load(open(f"{args.config}/core.yaml"))
cfg_dict.get("database", {})["loglevel"] = "info"
cfg.update(cfg_dict)
if not path.exists(cfg.server.log_dir):

View File

@ -71,25 +71,21 @@ Games listed below have been tested and confirmed working.
In order to use the importer locate your game installation folder and execute:
```shell
python read.py --game SDBT --version <Version ID> --binfolder </path/to/game/data> --optfolder </path/to/game/option/folder>
python read.py --game SDBT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
```
**Note: Use the /data not the /bin folder for the Importer!**
The importer for Chunithm will import: Events, Music, Charge Items and Avatar Accesories.
### Config
Config file is located in `config/chuni.yaml`.
| Option | Info |
|------------------|---------------------------------------------------------------------------------------------------------------------|
| `news_msg` | If this is set, the news at the top of the main screen will be displayed (up to Chunithm Paradise Lost) |
| `name` | If this is set, all players that are not on a team will use this one by default. |
| `use_login_bonus`| This is used to enable the login bonuses |
| `stock_tickets` | If this is set, specifies tickets to auto-stock at login. Format is a comma-delimited list of IDs. Defaults to None |
| `stock_count` | Ignored if stock_tickets is not specified. Number to stock of each ticket. Defaults to 99 |
| `crypto` | This option is used to enable the TLS Encryption |
| Option | Info |
|------------------|----------------------------------------------------------------------------------------------------------------|
| `news_msg` | If this is set, the news at the top of the main screen will be displayed (up to Chunithm Paradise Lost) |
| `name` | If this is set, all players that are not on a team will use this one by default. |
| `use_login_bonus`| This is used to enable the login bonuses |
| `crypto` | This option is used to enable the TLS Encryption |
If you would like to use network encryption, add the keys to the `keys` section under `crypto`, where the key
@ -175,7 +171,7 @@ The songId is based on the actual ID within your version of Chunithm.
In order to use the importer you need to use the provided `Export.csv` file:
```shell
python read.py --game SDCA --version <Version ID> --binfolder titles/cxb/data
python read.py --game SDCA --version <version ID> --binfolder titles/cxb/data
```
The importer for crossbeats REV. will import Music.
@ -226,11 +222,11 @@ Presents are items given to the user when they login, with a little animation (f
In order to use the importer locate your game installation folder and execute:
DX:
```shell
python read.py --game <Game Code> --version <Version ID> --binfolder </path/to/Sinmai_Data> --optfolder </path/to/game/option/folder>
python read.py --game <Game Code> --version <Version ID> --binfolder /path/to/StreamingAssets --optfolder /path/to/game/option/folder
```
Pre-DX:
```shell
python read.py --game <Game Code> --version <Version ID> --binfolder </path/to/data> --optfolder </path/to/patch/data>
python read.py --game <Game Code> --version <Version ID> --binfolder /path/to/data --optfolder /path/to/patch/data
```
The importer for maimai DX will import Events, Music and Tickets.
@ -263,7 +259,7 @@ Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code!
In order to use the importer locate your game installation folder and execute:
```shell
python read.py --game SBZV --version <Version ID> --binfolder </path/to/game/data/diva> --optfolder </path/to/game/data/diva/mdata>
python read.py --game SBZV --version <version ID> --binfolder /path/to/game/data/diva --optfolder /path/to/game/data/diva/mdata
```
The importer for Project Diva Arcade will all required data in order to use
@ -315,7 +311,7 @@ python dbutils.py upgrade
In order to use the importer locate your game installation folder and execute:
```shell
python read.py --game SDDT --version <Version ID> --binfolder </path/to/game/mu3_Data> --optfolder </path/to/game/option/folder>
python read.py --game SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
```
The importer for O.N.G.E.K.I. will all all Cards, Music and Events.
@ -448,19 +444,19 @@ In order to use the importer you need to use the provided `.csv` files (which ar
option folders:
```shell
python read.py --game SDED --version <Version ID> --binfolder titles/cm/cm_data --optfolder </path/to/cardmaker/option/folder>
python read.py --game SDED --version <version ID> --binfolder titles/cm/cm_data --optfolder /path/to/cardmaker/option/folder
```
**If you haven't already executed the O.N.G.E.K.I. importer, make sure you import all cards!**
```shell
python read.py --game SDDT --version <Version ID> --binfolder </path/to/ongeki/mu3_Data> --optfolder </path/to/ongeki/option/folder>
python read.py --game SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
```
Also make sure to import all maimai DX and CHUNITHM data as well:
```shell
python read.py --game SDED --version <Version ID> --binfolder </path/to/cardmaker/CardMaker_Data> --optfolder </path/to/cardmaker/option/folder>
python read.py --game SDED --version <version ID> --binfolder /path/to/cardmaker/CardMaker_Data
```
The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai DX/CHUNITHM) and the hardcoded
@ -553,7 +549,7 @@ Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded fo
In order to use the importer locate your game installation folder and execute:
```shell
python read.py --game SDFE --version <Version ID> --binfolder </path/to/game/WindowsNoEditor/Mercury/Content>
python read.py --game SDFE --version <version ID> --binfolder /path/to/game/WindowsNoEditor/Mercury/Content
```
The importer for WACCA will import all Music data.
@ -702,9 +698,9 @@ Config file is located in `config/idac.yaml`.
| `port_matching` | Port number for the Online Battle Matching |
| `port_echo1/2` | Port numbers for Echos |
| `port_matching_p2p` | Port number for Online Battle (currently unsupported) |
| `stamp.enable` | Enables/Disabled the play stamp events |
| `stamp.enable` | Enables/Disabled the play stamp events |
| `stamp.enabled_stamps` | Define up to 3 play stamp events (without `.json` extension, which are placed in `titles/idac/data/stamps`) |
| `timetrial.enable` | Enables/Disables the time trial event |
| `timetrial.enable` | Enables/Disables the time trial event |
| `timetrial.enabled_timetrial` | Define one! trial event (without `.json` extension, which are placed in `titles/idac/data/timetrial`) |
@ -717,7 +713,6 @@ python dbutils.py upgrade
```
### Notes
- Online Battle is not supported
- Online Battle Matching is not supported
@ -754,18 +749,18 @@ python dbutils.py upgrade
### TimeRelease Chapter:
1. Story: 1, 2, 3, 4, 5, 6, 7, 8, 9, 19 (Chapter 10), 29 (Chapter 11)
2. MF Ghost: 10, 11, 12, 13, 14
3. Bunta: 15, 16, 17, 18, 20, 21, 21, 22
4. Touhou Project Special Event: 23, 24, 25, 26, 27, 28
5. Hatsune Miku Special Event: 36, 37, 38
1. Story: 1, 2, 3, 4, 5, 6, 7, 8, 9, 19 (Chapter 10), (29 Chapter 11?)
2. MF Ghost: 10, 11, 12, 13, 14, 15
3. Bunta: 15, 16, 17, 18, 19, 20, (21, 21, 22?)
4. Special Event: 23, 24, 25, 26, 27, 28 (Touhou Project)
### TimeRelease Courses:
| Course ID | Course Name | Direction |
| --------- | ------------------------- | ------------------------ |
| 0 | Lake Akina(秋名湖) | CounterClockwise(左周り) |
| 2 | Lake Akina(秋名湖) | Clockwise(右周り) |
| 0 | Akina Lake(秋名湖) | CounterClockwise(左周り) |
| 2 | Akina Lake(秋名湖) | Clockwise(右周り) |
| 52 | Hakone(箱根) | Downhill(下り) |
| 54 | Hakone(箱根) | Hillclimb(上り) |
| 36 | Usui(碓氷) | CounterClockwise(左周り) |
@ -778,14 +773,12 @@ python dbutils.py upgrade
| 14 | Akina(秋名) | Hillclimb(上り) |
| 16 | Irohazaka(いろは坂) | Downhill(下り) |
| 18 | Irohazaka(いろは坂) | Reverse(逆走) |
| 20 | Tsukuba(筑波) | Outbound(往路) |
| 22 | Tsukuba(筑波) | Inbound(復路) |
| 56 | Momiji Line(もみじライン) | Downhill(下り) |
| 58 | Momiji Line(もみじライン) | Hillclimb(上り) |
| 20 | Tsukuba(筑波) | Outbound(往路) |
| 22 | Tsukuba(筑波) | Inbound(復路) |
| 24 | Happogahara(八方ヶ原) | Outbound(往路) |
| 26 | Happogahara(八方ヶ原) | Inbound(復路) |
| 28 | Nagao(長尾) | Downhill(下り) |
| 30 | Nagao(長尾) | Hillclimb(上り) |
| 40 | Sadamine(定峰) | Downhill(下り) |
| 42 | Sadamine(定峰) | Hillclimb(上り) |
| 44 | Tsuchisaka(土坂) | Outbound(往路) |
@ -794,70 +787,8 @@ python dbutils.py upgrade
| 50 | Akina Snow(秋名雪) | Hillclimb(上り) |
| 68 | Odawara(小田原) | Forward(順走) |
| 70 | Odawara(小田原) | Reverse(逆走) |
| 72 | Tsukuba Snow(筑波雪) | Outbound(往路) |
| 74 | Tsukuba Snow(筑波雪) | Inbound(復路) |
### TimeRelease `announce_image`:
- `save_filename`: Filename without file extension saved in the folder `ImageDelivery`
- `url`: URL to the file on the server with the corresponding file extension (.djg/.gpg)
(except for `display_id=9` where the url is empty)
- `open_dt`: UNIX timestamp when it should be displayed
- `close_dt`: UNIX timestamp when it should be hidden
- `display_id`: One of the following IDS:
| Display ID | Description |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------- |
| 1 | ADV image in the size 1920x1080, shown during attract |
| 2 | Start image in the size 1280x720, shown in the Main Menu after selection the corresponding banner |
| 3 | Banner image in the size 640×120, shown in the Main Menu |
| 5 | Stamp Background image in the size 1780x608 |
| 6 | Online Battle round image in the size 1920x1080 |
| 8 | Stamp Pickup image in the size 624x300, also requires `target_id` set |
| 9 | Attract video from the `C:/Mount/Option` folder on real hardware, also requires a `target_id` set |
- `target_id`:
- Always 0 unless:
- `display_id=8`: Matches an existing stamp pickup abolsute `reward_setting_masu`
and will replace the stock image with the provided one from `url`
- `display_id=9`: Matches the id from `C:/Mount/Option/MV01/targetXXX.bin`,
where XXX is the `target_id`
- `page`:
- Defines the order in which the images being shown, where 1 is the first image
- `display_id` 1, 2, 3: The `page` has to match, so the corresponding images
of an event are shown correctly
- `display_id` 7, 8: The `page` defines the `sheet_design` in the play stamps
- `time`: The time in sec for an image to be shown, always 10
```json
{
"save_filename": "adv_01_example",
"url": "http://example.com/images/04721D5D3595FD29778011EC73A8AE77.dpg",
"open_dt": 1514761200,
"close_dt": 1861916400,
"display_id": 1,
"target_id": 0,
"page": 1,
"time": 10
},
```
### Battle Gift
- `gift_id`: unique gift index (starts from 0 f.e.)
- `battle_gift_event_id`: unique event id
- `reward_category`: item category (f.e. 21 = Chat Stamp)
- `reward_type`: item id (f.e. 483 = Remilia Scarlet)
- `reward_name`: name of the reward
- `rarity`: 2 Golden, 1 Silver, 3 Bronze
- `cash_rate`: ?
- `customize_point_rate`: ?
- `avatar_point_rate`: ?
- `first_distribution_rate`: only used server side for the probability of the first distribution
### Credits:
### Credits
- Bottersnike: For the HUGE Reverse Engineering help
- Kinako: For helping with the timeRelease unlocking of courses and special mode

View File

@ -8,11 +8,7 @@ team:
mods:
use_login_bonus: True
# stock_tickets allows specified ticket IDs to be auto-stocked at login. Format is a comma-delimited string of ticket IDs
# note: quanity is not refreshed on "continue" after set - only on subsequent login
stock_tickets:
stock_count: 99
version:
11:
rom: 2.00.00

View File

@ -10,34 +10,13 @@ server:
port_echo2: 20002
port_matching_p2p: 20003
timerelease:
timerelease_no: 3
timerelease_avatar_gacha_no: 3
stamp:
enable: True
enabled_stamps: # max 3 play stamps
150:
- "touhou_remilia_scarlet"
- "touhou_flandre_scarlet"
- "touhou_sakuya_izayoi"
170:
- "touhou_remilia_scarlet"
- "touhou_flandre_scarlet"
- "touhou_sakuya_izayoi"
- "touhou_remilia_scarlet"
- "touhou_flandre_scarlet"
- "touhou_sakuya_izayoi"
timetrial:
enable: True
enabled_timetrial:
150: "touhou_remilia_scarlet"
170: "touhou_remilia_scarlet"
battle_event:
enable: True
enabled_battle_event:
170: "touhou_1st"
round_event:
enable: True
enabled_round: "S2R2"
last_round: "S2R1"
enabled_timetrial: "touhou_remilia_scarlet"

View File

@ -6,8 +6,7 @@ import uvicorn
import logging
import asyncio
from core.config import CoreConfig
from core.aimedb import AimedbServlette
from core import CoreConfig, AimedbServlette
async def launch_main(cfg: CoreConfig, ssl: bool) -> None:
if ssl:

View File

@ -24,35 +24,20 @@ class ChuniBase:
async def handle_game_login_api_request(self, data: Dict) -> Dict:
"""
Handles the login bonus and ticket stock logic, required for the game
because getUserLoginBonus gets called after getUserItem; therefore the
Handles the login bonus logic, required for the game because
getUserLoginBonus gets called after getUserItem and therefore the
items needs to be inserted in the database before they get requested.
- Adds a stock for each specified ticket (itemKind 5)
- Adds a bonusCount after a user logged in after 24 hours, makes sure
loginBonus 30 gets looped, only show the login banner every 24 hours,
adds the bonus to items (itemKind 6)
Adds a bonusCount after a user logged in after 24 hours, makes sure
loginBonus 30 gets looped, only show the login banner every 24 hours,
adds the bonus to items (itemKind 6)
"""
user_id = data["userId"]
# If we want to make certain tickets always available, stock them now
if self.game_cfg.mods.stock_tickets:
for ticket in self.game_cfg.mods.stock_tickets.split(","):
await self.data.item.put_item(
user_id,
{
"itemId": ticket.strip(),
"itemKind": 5,
"stock": self.game_cfg.mods.stock_count,
"isValid": True,
},
)
# ignore the login bonus if disabled in config
if not self.game_cfg.mods.use_login_bonus:
return {"returnCode": 1}
user_id = data["userId"]
login_bonus_presets = await self.data.static.get_login_bonus_presets(self.version)
for preset in login_bonus_presets:

View File

@ -53,18 +53,6 @@ class ChuniModsConfig:
self.__config, "chuni", "mods", "use_login_bonus", default=True
)
@property
def stock_tickets(self) -> str:
return CoreConfig.get_config_field(
self.__config, "chuni", "mods", "stock_tickets", default=None
)
@property
def stock_count(self) -> int:
return CoreConfig.get_config_field(
self.__config, "chuni", "mods", "stock_count", default=99
)
class ChuniVersionConfig:
def __init__(self, parent_config: "ChuniConfig") -> None:

View File

@ -104,8 +104,7 @@ class ChuniNew(ChuniBase):
return {"returnCode": "1"}
async def handle_get_user_map_area_api_request(self, data: Dict) -> Dict:
map_area_ids = [int(area["mapAreaId"]) for area in data["mapAreaIdList"]]
user_map_areas = await self.data.item.get_map_areas(data["userId"], map_area_ids)
user_map_areas = await self.data.item.get_map_areas(data["userId"])
map_areas = []
for map_area in user_map_areas:

View File

@ -35,15 +35,11 @@ class ChuniReader(BaseReader):
if self.opt_dir is not None:
data_dirs += self.get_data_directories(self.opt_dir)
we_diff = "4"
if self.version >= ChuniConstants.VER_CHUNITHM_NEW:
we_diff = "5"
for dir in data_dirs:
self.logger.info(f"Read from {dir}")
await self.read_events(f"{dir}/event")
await self.read_music(f"{dir}/music", we_diff)
await self.read_music(f"{dir}/music")
await self.read_charges(f"{dir}/chargeItem")
await self.read_avatar(f"{dir}/avatarAccessory")
await self.read_login_bonus(f"{dir}/")
@ -142,7 +138,7 @@ class ChuniReader(BaseReader):
else:
self.logger.warning(f"Failed to insert event {id}")
async def read_music(self, music_dir: str, we_diff: str = "4") -> None:
async def read_music(self, music_dir: str) -> None:
for root, dirs, files in walk(music_dir):
for dir in dirs:
if path.exists(f"{root}/{dir}/Music.xml"):
@ -173,7 +169,7 @@ class ChuniReader(BaseReader):
chart_type = MusicFumenData.find("type")
chart_id = chart_type.find("id").text
chart_diff = chart_type.find("str").text
if chart_diff == "WorldsEnd" and chart_id == we_diff: # 4 in SDBT, 5 in SDHD
if chart_diff == "WorldsEnd" and (chart_id == "4" or chart_id == "5"): # 4 in SDBT, 5 in SDHD
level = float(xml_root.find("starDifType").text)
we_chara = (
xml_root.find("worldsEndTagName")

View File

@ -533,8 +533,8 @@ class ChuniItemData(BaseData):
return None
return result.lastrowid
async def get_map_areas(self, user_id: int, map_area_ids: List[int]) -> Optional[List[Row]]:
sql = select(map_area).where(map_area.c.user == user_id, map_area.c.mapAreaId.in_(map_area_ids))
async def get_map_areas(self, user_id: int) -> Optional[List[Row]]:
sql = select(map_area).where(map_area.c.user == user_id)
result = await self.execute(sql)
if result is None:

View File

@ -1,4 +1,3 @@
from typing import Dict
from core.config import CoreConfig
@ -67,22 +66,6 @@ class IDACServerConfig:
return CoreConfig.get_config_field(
self.__config, "idac", "server", "port_matching_p2p", default=20003
)
class IDACTimereleaseConfig:
def __init__(self, parent: "IDACConfig") -> None:
self.__config = parent
@property
def timerelease_no(self) -> int:
return CoreConfig.get_config_field(
self.__config, "idac", "timerelease", "timerelease_no", default=1
)
@property
def timerelease_avatar_gacha_no(self) -> int:
return CoreConfig.get_config_field(
self.__config, "idac", "timerelease", "timerelease_avatar_gacha_no", default=1
)
class IDACStampConfig:
@ -96,28 +79,17 @@ class IDACStampConfig:
)
@property
def enabled_stamps(self) -> Dict:
"""
In the form of:
<version as int>:
- <stamp name 1>
- <stamp name 2>
- <stamp name 3>
max 3 stamps per version
f.e.:
150:
- "touhou_remilia_scarlet"
- "touhou_flandre_scarlet"
- "touhou_sakuya_izayoi"
"""
def enabled_stamps(self) -> list:
return CoreConfig.get_config_field(
self.__config,
"idac",
"stamp",
"enabled_stamps",
default={},
default=[
"touhou_remilia_scarlet",
"touhou_flandre_scarlet",
"touhou_sakuya_izayoi",
],
)
@ -132,85 +104,18 @@ class IDACTimetrialConfig:
)
@property
def enabled_timetrial(self) -> Dict:
"""
In the form of:
<version as int>: <timetrial name>
f.e.:
150: "touhou_remilia_scarlet"
"""
def enabled_timetrial(self) -> str:
return CoreConfig.get_config_field(
self.__config,
"idac",
"timetrial",
"enabled_timetrial",
default={},
)
class IDACTBattleGiftConfig:
def __init__(self, parent: "IDACConfig") -> None:
self.__config = parent
@property
def enable(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "idac", "battle_event", "enable", default=True
)
@property
def enabled_battle_event(self) -> Dict:
"""
In the form of:
<version as int>: <battle_event name>
f.e.:
170: "touhou_1st"
"""
return CoreConfig.get_config_field(
self.__config,
"idac",
"battle_event",
"enabled_battle_event",
default={},
)
class IDACRoundConfig:
def __init__(self, parent: "IDACConfig") -> None:
self.__config = parent
@property
def enable(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "idac", "round_event", "enable", default=True
)
@property
def enabled_round(self) -> str:
return CoreConfig.get_config_field(
self.__config,
"idac",
"round_event",
"enabled_round",
default="S2R2",
)
@property
def last_round(self) -> str:
return CoreConfig.get_config_field(
self.__config,
"idac",
"round_event",
"last_round",
default="S2R1",
default="touhou_remilia_scarlet",
)
class IDACConfig(dict):
def __init__(self) -> None:
self.server = IDACServerConfig(self)
self.timerelease = IDACTimereleaseConfig(self)
self.stamp = IDACStampConfig(self)
self.timetrial = IDACTimetrialConfig(self)
self.battle_event = IDACTBattleGiftConfig(self)
self.round_event = IDACRoundConfig(self)

View File

@ -6,9 +6,6 @@ class IDACConstants():
VER_IDAC_SEASON_1 = 0
VER_IDAC_SEASON_2 = 1
BATTLE_MODE_ONLINE = 0
BATTLE_MODE_OFFLINE = 1
VERSION_STRING = (
"Initial D THE ARCADE Season 1",
"Initial D THE ARCADE Season 2",

View File

@ -1,82 +0,0 @@
{
"battle_gift_event_id": 2,
"event_nm": "東方Projectコラボ",
"start_dt": "2024-04-01",
"end_dt": "2029-01-01",
"mode_id": 1,
"delivery_type": 1,
"gift_data": [
{
"gift_id": 0,
"battle_gift_event_id": 2,
"reward_category": 21,
"reward_type": 418,
"reward_name": "素敵なお賽銭箱はそこよ",
"rarity": 3,
"cash_rate": 0,
"customize_point_rate": 0,
"avatar_point_rate": 0,
"first_distribution_rate": 60
},
{
"gift_id": 1,
"battle_gift_event_id": 2,
"reward_category": 21,
"reward_type": 419,
"reward_name": "面倒な種族ね",
"rarity": 2,
"cash_rate": 0,
"customize_point_rate": 0,
"avatar_point_rate": 0,
"first_distribution_rate": 20
},
{
"gift_id": 2,
"battle_gift_event_id": 2,
"reward_category": 21,
"reward_type": 423,
"reward_name": "調子がそこそこだぜ",
"rarity": 3,
"cash_rate": 0,
"customize_point_rate": 0,
"avatar_point_rate": 0,
"first_distribution_rate": 60
},
{
"gift_id": 3,
"battle_gift_event_id": 2,
"reward_category": 21,
"reward_type": 422,
"reward_name": "あー?",
"rarity": 2,
"cash_rate": 0,
"customize_point_rate": 0,
"avatar_point_rate": 0,
"first_distribution_rate": 20
},
{
"gift_id": 4,
"battle_gift_event_id": 2,
"reward_category": 21,
"reward_type": 427,
"reward_name": "さ、記事にするわよー",
"rarity": 3,
"cash_rate": 0,
"customize_point_rate": 0,
"avatar_point_rate": 0,
"first_distribution_rate": 60
},
{
"gift_id": 5,
"battle_gift_event_id": 2,
"reward_category": 21,
"reward_type": 426,
"reward_name": "号外~、号外だよ~",
"rarity": 2,
"cash_rate": 0,
"customize_point_rate": 0,
"avatar_point_rate": 0,
"first_distribution_rate": 20
}
]
}

View File

@ -1,183 +0,0 @@
{
"round_event_id": 9,
"round_event_nm": "シーズン2 特別ラウンド",
"start_dt": 1647468000,
"end_dt": 1648062000,
"round_start_rank": 0,
"save_filename": "",
"vscount": [
{
"reward_upper_limit": 30,
"reward_lower_limit": 30,
"reward": [
{
"reward_category": 21,
"reward_type": 379
}
]
},
{
"reward_upper_limit": 10,
"reward_lower_limit": 10,
"reward": [
{
"reward_category": 21,
"reward_type": 367
}
]
}
],
"rank": [
{
"reward_upper_limit": 1,
"reward_lower_limit": 1,
"reward": [
{
"reward_category": 24,
"reward_type": 4328
}
]
},
{
"reward_upper_limit": 2,
"reward_lower_limit": 10,
"reward": [
{
"reward_category": 24,
"reward_type": 4329
}
]
},
{
"reward_upper_limit": 11,
"reward_lower_limit": 50,
"reward": [
{
"reward_category": 24,
"reward_type": 4330
}
]
},
{
"reward_upper_limit": 51,
"reward_lower_limit": 100,
"reward": [
{
"reward_category": 24,
"reward_type": 4331
}
]
},
{
"reward_upper_limit": 101,
"reward_lower_limit": 1000,
"reward": [
{
"reward_category": 24,
"reward_type": 4332
}
]
}
],
"point": [
],
"playable_course_list": [
{
"course_id": 0,
"course_day": 0
},
{
"course_id": 0,
"course_day": 1
},
{
"course_id": 2,
"course_day": 0
},
{
"course_id": 2,
"course_day": 1
},
{
"course_id": 36,
"course_day": 0
},
{
"course_id": 36,
"course_day": 1
},
{
"course_id": 38,
"course_day": 0
},
{
"course_id": 38,
"course_day": 1
},
{
"course_id": 4,
"course_day": 0
},
{
"course_id": 4,
"course_day": 1
},
{
"course_id": 6,
"course_day": 0
},
{
"course_id": 6,
"course_day": 1
},
{
"course_id": 12,
"course_day": 0
},
{
"course_id": 12,
"course_day": 1
},
{
"course_id": 14,
"course_day": 0
},
{
"course_id": 14,
"course_day": 1
},
{
"course_id": 8,
"course_day": 0
},
{
"course_id": 8,
"course_day": 1
},
{
"course_id": 10,
"course_day": 0
},
{
"course_id": 10,
"course_day": 1
},
{
"course_id": 16,
"course_day": 0
},
{
"course_id": 16,
"course_day": 1
},
{
"course_id": 18,
"course_day": 0
},
{
"course_id": 18,
"course_day": 1
}
]
}

View File

@ -1,229 +0,0 @@
{
"round_event_id": 10,
"round_event_nm": "シーズン2 2ndラウンド",
"start_dt": 1648072800,
"end_dt": 1651086000,
"round_start_rank": 0,
"save_filename": "",
"vscount": [
{
"reward_upper_limit": 180,
"reward_lower_limit": 180,
"reward": [
{
"reward_category": 21,
"reward_type": 462
}
]
},
{
"reward_upper_limit": 120,
"reward_lower_limit": 120,
"reward": [
{
"reward_category": 21,
"reward_type": 461
}
]
},
{
"reward_upper_limit": 80,
"reward_lower_limit": 80,
"reward": [
{
"reward_category": 22,
"reward_type": 516
}
]
},
{
"reward_upper_limit": 40,
"reward_lower_limit": 40,
"reward": [
{
"reward_category": 21,
"reward_type": 484
}
]
},
{
"reward_upper_limit": 10,
"reward_lower_limit": 10,
"reward": [
{
"reward_category": 21,
"reward_type": 483
}
]
}
],
"rank": [
{
"reward_upper_limit": 1,
"reward_lower_limit": 1,
"reward": [
{
"reward_category": 24,
"reward_type": 4333
}
]
},
{
"reward_upper_limit": 2,
"reward_lower_limit": 10,
"reward": [
{
"reward_category": 24,
"reward_type": 4334
}
]
},
{
"reward_upper_limit": 11,
"reward_lower_limit": 50,
"reward": [
{
"reward_category": 24,
"reward_type": 4335
}
]
},
{
"reward_upper_limit": 51,
"reward_lower_limit": 100,
"reward": [
{
"reward_category": 24,
"reward_type": 4336
}
]
},
{
"reward_upper_limit": 101,
"reward_lower_limit": 1000,
"reward": [
{
"reward_category": 24,
"reward_type": 4337
}
]
}
],
"point": [
],
"playable_course_list": [
{
"course_id": 4,
"course_day": 0
},
{
"course_id": 4,
"course_day": 1
},
{
"course_id": 6,
"course_day": 0
},
{
"course_id": 6,
"course_day": 1
},
{
"course_id": 12,
"course_day": 0
},
{
"course_id": 12,
"course_day": 1
},
{
"course_id": 14,
"course_day": 0
},
{
"course_id": 14,
"course_day": 1
},
{
"course_id": 16,
"course_day": 0
},
{
"course_id": 16,
"course_day": 1
},
{
"course_id": 18,
"course_day": 0
},
{
"course_id": 18,
"course_day": 1
},
{
"course_id": 20,
"course_day": 0
},
{
"course_id": 20,
"course_day": 1
},
{
"course_id": 22,
"course_day": 0
},
{
"course_id": 22,
"course_day": 1
},
{
"course_id": 24,
"course_day": 0
},
{
"course_id": 24,
"course_day": 1
},
{
"course_id": 26,
"course_day": 0
},
{
"course_id": 26,
"course_day": 1
},
{
"course_id": 44,
"course_day": 0
},
{
"course_id": 44,
"course_day": 1
},
{
"course_id": 46,
"course_day": 0
},
{
"course_id": 46,
"course_day": 1
},
{
"course_id": 48,
"course_day": 0
},
{
"course_id": 48,
"course_day": 1
},
{
"course_id": 50,
"course_day": 0
},
{
"course_id": 50,
"course_day": 1
}
]
}

File diff suppressed because one or more lines are too long

View File

@ -30,6 +30,7 @@
180,
180,
180,
200,
200
],
"reward": [

View File

@ -30,6 +30,7 @@
180,
180,
180,
200,
200
],
"reward": [

View File

@ -30,6 +30,7 @@
180,
180,
180,
200,
200
],
"reward": [

View File

@ -2,8 +2,6 @@ from core.data import Data
from core.config import CoreConfig
from titles.idac.schema.profile import IDACProfileData
from titles.idac.schema.item import IDACItemData
from titles.idac.schema.rounds import IDACOnlineRounds
from titles.idac.schema.factory import IDACFactoryData
class IDACData(Data):
@ -12,5 +10,3 @@ class IDACData(Data):
self.profile = IDACProfileData(cfg, self.session)
self.item = IDACItemData(cfg, self.session)
self.rounds = IDACOnlineRounds(cfg, self.session)
self.factory = IDACFactoryData(cfg, self.session)

View File

@ -1,11 +1,10 @@
import json
from typing import List
from starlette.routing import Route
from starlette.responses import Response, RedirectResponse
import yaml
import jinja2
from os import path
from typing import List, Any, Type
from starlette.routing import Route, Mount
from starlette.responses import Response, RedirectResponse, JSONResponse
from starlette.requests import Request
from core.frontend import FE_Base, UserSession
@ -17,271 +16,19 @@ from titles.idac.config import IDACConfig
from titles.idac.const import IDACConstants
class RankingData:
def __init__(
self,
rank: int,
name: str,
record: int,
eval_id: int,
store: str,
style_car_id: int,
update_date: str,
) -> None:
self.rank: int = rank
self.name: str = name
self.record: str = record
self.store: str = store
self.eval_id: int = eval_id
self.style_car_id: int = style_car_id
self.update_date: str = update_date
def make(self):
return vars(self)
class RequestValidator:
def __init__(self) -> None:
self.success: bool = True
self.error: str = ""
def validate_param(
self,
request_args: Dict[bytes, bytes],
param_name: str,
param_type: Type[None],
default=None,
required: bool = True,
) -> None:
# Check if the parameter is missing
if param_name not in request_args:
if required:
self.success = False
self.error += f"Missing parameter: '{param_name}'. "
else:
# If the parameter is not required,
# return the default value if it exists
return default
return None
param_value = request_args[param_name]
# Check if the parameter type is not empty
if param_type:
try:
# Attempt to convert the parameter value to the specified type
param_value = param_type(param_value)
except ValueError:
# If the conversion fails, return an error
self.success = False
self.error += f"Invalid parameter type for '{param_name}'. "
return None
return param_value
class RankingRequest(RequestValidator):
def __init__(self, request_args: Dict[bytes, bytes]) -> None:
super().__init__()
self.course_id: int = self.validate_param(request_args, "courseId", int)
self.page_number: int = self.validate_param(
request_args, "pageNumber", int, default=1, required=False
)
class RankingResponse:
def __init__(self) -> None:
self.success: bool = False
self.error: str = ""
self.total_pages: int = 0
self.total_records: int = 0
self.updated_at: str = ""
self.ranking: list[RankingData] = []
def make(self):
ret = vars(self)
self.error = (
"Unknown error." if not self.success and self.error == "" else self.error
)
ret["ranking"] = [rank.make() for rank in self.ranking]
return ret
class IDACFrontend(FE_Base):
isLeaf = False
children: Dict[str, Any] = {}
def __init__(
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
) -> None:
super().__init__(cfg, environment)
self.data = IDACData(cfg)
self.core_cfg = 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"
# self.nav_name = "IDAC"
# TODO: Add version list
self.version = IDACConstants.VER_IDAC_SEASON_2
self.profile = IDACProfileFrontend(cfg, self.environment)
self.ranking = IDACRankingFrontend(cfg, self.environment)
def get_routes(self) -> List[Route]:
return [
Route("/", self.render_GET),
Mount("/profile", routes=[
Route("/", self.profile.render_GET),
# dirty hack
Route("/export.get", self.profile.render_GET),
]),
Mount("/ranking", routes=[
Route("/", self.ranking.render_GET),
# dirty hack
Route("/const.get", self.ranking.render_GET),
Route("/ranking.get", self.ranking.render_GET),
]),
]
async def render_GET(self, request: Request) -> bytes:
uri: str = request.url.path
template = self.environment.get_template(
"titles/idac/templates/idac_index.jinja"
)
usr_sesh = self.validate_session(request)
if not usr_sesh:
usr_sesh = UserSession()
# redirect to the ranking page
if uri.startswith("/game/idac"):
return RedirectResponse("/game/idac/ranking", 303)
return Response(template.render(
title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh),
active_page="idac",
), media_type="text/html; charset=utf-8")
async def render_POST(self, request: Request) -> bytes:
pass
class IDACRankingFrontend(FE_Base):
def __init__(self, cfg: CoreConfig, environment: jinja2.Environment) -> None:
super().__init__(cfg, environment)
self.data = IDACData(cfg)
self.core_cfg = cfg
self.nav_name = "頭文字D THE ARCADE"
# TODO: Add version list
self.version = IDACConstants.VER_IDAC_SEASON_2
async def render_GET(self, request: Request) -> bytes:
uri: str = request.url.path
template = self.environment.get_template(
"titles/idac/templates/ranking/index.jinja"
)
usr_sesh = self.validate_session(request)
if not usr_sesh:
usr_sesh = UserSession()
user_id = usr_sesh.user_id
# IDAC constants
if uri.startswith("/game/idac/ranking/const.get"):
# get the constants
with open("titles/idac/templates/const.json", "r", encoding="utf-8") as f:
constants = json.load(f)
return JSONResponse(constants)
# leaderboard ranking
elif uri.startswith("/game/idac/ranking/ranking.get"):
req = RankingRequest(request.query_params._dict)
resp = RankingResponse()
if not req.success:
resp.error = req.error
return JSONResponse(resp.make())
# get the total number of records
total_records = await self.data.item.get_time_trial_ranking_by_course_total(
self.version, req.course_id
)
# return an error if there are no records
if total_records is None or total_records == 0:
resp.error = "No records found."
return JSONResponse(resp.make())
# get the total number of records
total = total_records["count"]
limit = 50
offset = (req.page_number - 1) * limit
ranking = await self.data.item.get_time_trial_ranking_by_course(
self.version,
req.course_id,
limit=limit,
offset=offset,
)
for i, rank in enumerate(ranking):
user_id = rank["user"]
# get the username, country and store from the profile
profile = await self.data.profile.get_profile(user_id, self.version)
arcade = await self.data.arcade.get_arcade(profile["store"])
if arcade is None:
arcade = {}
arcade["name"] = self.core_config.server.name
# should never happen
if profile is None:
continue
resp.ranking.append(
RankingData(
rank=offset + i + 1,
name=profile["username"],
record=rank["goal_time"],
store=arcade["name"],
eval_id=rank["eval_id"],
style_car_id=rank["style_car_id"],
update_date=str(rank["play_dt"]),
)
)
# now return the json data, with the total number of pages and records
# round up the total pages
resp.success = True
resp.total_pages = (total // limit) + 1
resp.total_records = total
return JSONResponse(resp.make())
return Response(template.render(
title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh),
active_page="idac",
active_tab="ranking",
), media_type="text/html; charset=utf-8")
class IDACProfileFrontend(FE_Base):
def __init__(self, cfg: CoreConfig, environment: jinja2.Environment) -> None:
super().__init__(cfg, environment)
self.data = IDACData(cfg)
self.core_cfg = cfg
self.nav_name = "頭文字D THE ARCADE"
#self.nav_name = "頭文字D THE ARCADE"
self.nav_name = "IDAC"
# TODO: Add version list
self.version = IDACConstants.VER_IDAC_SEASON_2
@ -291,6 +38,11 @@ class IDACProfileFrontend(FE_Base):
25: "full_tune_tickets",
34: "full_tune_fragments",
}
def get_routes(self) -> List[Route]:
return [
Route("/", self.render_GET)
]
async def generate_all_tables_json(self, user_id: int):
json_export = {}
@ -315,7 +67,7 @@ class IDACProfileFrontend(FE_Base):
theory_running,
vs_info,
stamp,
timetrial_event,
timetrial_event
}
for table in idac_tables:
@ -345,30 +97,25 @@ class IDACProfileFrontend(FE_Base):
uri: str = request.url.path
template = self.environment.get_template(
"titles/idac/templates/profile/index.jinja"
"titles/idac/templates/idac_index.jinja"
)
usr_sesh = self.validate_session(request)
if not usr_sesh:
usr_sesh = UserSession()
user_id = usr_sesh.user_id
user = await self.data.user.get_user(user_id)
if user is None:
self.logger.debug(f"User {user_id} not found")
return RedirectResponse("/user/", 303)
# user_id = usr_sesh.user_id
# profile export
if uri.startswith("/game/idac/profile/export.get"):
if uri.startswith("/game/idac/export"):
if user_id == 0:
return RedirectResponse("/game/idac", 303)
return RedirectResponse(b"/game/idac", request)
# set the file name, content type and size to download the json
content = await self.generate_all_tables_json(user_id)
content = await self.generate_all_tables_json(user_id).encode("utf-8")
self.logger.info(f"User {user_id} exported their IDAC data")
return Response(
content.encode("utf-8"),
content,
200,
{'content-disposition': 'attachment; filename=idac_profile.json'},
"application/octet-stream"
@ -393,7 +140,5 @@ class IDACProfileFrontend(FE_Base):
tickets=tickets,
rank=rank,
sesh=vars(usr_sesh),
username=user["username"],
active_page="idac",
active_tab="profile",
), media_type="text/html; charset=utf-8")

View File

@ -1,16 +1,15 @@
import json
import traceback
import yaml
import logging
import coloredlogs
import asyncio
from os import path
from typing import Dict, List, Tuple
from logging.handlers import TimedRotatingFileHandler
from starlette.routing import Route
from starlette.requests import Request
from starlette.responses import JSONResponse
import yaml
import logging
import coloredlogs
from os import path
from typing import Dict, List, Tuple
from logging.handlers import TimedRotatingFileHandler
import asyncio
from core.config import CoreConfig
from core.title import BaseServlet, JSONResponseNoASCII
@ -30,37 +29,34 @@ class IDACServlet(BaseServlet):
yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"))
)
self.logger = logging.getLogger("idac")
if not hasattr(self.logger, "inited"):
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
)
self.logger.inited = True
self.versions = [
IDACBase(core_cfg, self.game_cfg),
IDACSeason2(core_cfg, self.game_cfg)
]
self.logger = logging.getLogger("idac")
log_fmt_str = "[%(asctime)s] IDAC | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.core_cfg.server.log_dir, "idac"),
encoding="utf8",
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler)
self.logger.setLevel(self.game_cfg.server.loglevel)
coloredlogs.install(
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
)
@classmethod
def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool:
game_cfg = IDACConfig()

View File

@ -1,96 +0,0 @@
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
from random import randint
class IDACMatching(resource.Resource):
isLeaf = True
SessionQueue = {}
Rooms = {}
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")
def getMatchingState(self, machineSerial): #We use official state code here
if len(self.SessionQueue) == 1:
self.logger.info(f"IDAC Matching queued player {machineSerial}: empty dict, returned by default")
return self.SessionQueue[machineSerial]
elif self.SessionQueue[machineSerial] == 0:
self.logger.info(f"IDAC Matching queued player {machineSerial}: matched player, returned by default")
return self.SessionQueue[machineSerial]
else:
for sessionID in self.SessionQueue.keys():
if sessionID == machineSerial:
continue
if self.SessionQueue[sessionID] == 1:
#uncomment these to process into actual game
#self.SessionQueue[machineSerial] = 0
#self.SessionQueue[sessionID] = 0
self.joinRoom(machineSerial, sessionID)
self.logger.info(f"IDAC Matching queued player {machineSerial}: rival {sessionID} found!! return matched state")
return self.SessionQueue[machineSerial]
self.logger.info(f"IDAC Matching queued player {machineSerial}: cannot find any rival, returned by default")
return self.SessionQueue[machineSerial]
def joinRoom(self, machineSerial, sessionID): #Random room name, It should be handled by game itself in later process
roomName = "INDTA-Zenkoku-Room" #+randint(1, 1001)
self.Rooms[machineSerial] = roomName
self.Rooms[sessionID] = roomName
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())
machineSerial = header_application["a_serial"]
self.logger.info(
f"IDAC Matching request from {req.getClientIP()}: {url} - {req_data}"
)
if url == "/regist":
self.SessionQueue[machineSerial] = 1
self.logger.info(f"IDAC Matching registed player {machineSerial}")
return json.dumps({"status_code": "0"}, ensure_ascii=False).encode("utf-8")
elif url == "/status":
if req_data.get('cancel_flag'):
if machineSerial in self.SessionQueue:
self.SessionQueue.pop(machineSerial)
self.logger.info(f"IDAC Matching endpoint {req.getClientIP()} had quited")
return json.dumps({"status_code": "0", "host": "", "port": self.game_config.server.matching_p2p, "room_name": self.Rooms[machineSerial], "state": 1}, ensure_ascii=False).encode("utf-8")
if machineSerial not in self.Rooms.keys():
self.Rooms[machineSerial] = "None"
return json.dumps({"status_code": "0", "host": self.game_config.server.matching_host, "port": self.game_config.server.matching_p2p, "room_name": self.Rooms[machineSerial], "state": self.getMatchingState(machineSerial)}, ensure_ascii=False).encode("utf-8")
# 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

View File

@ -81,7 +81,7 @@ class IDACReader(BaseReader):
self.logger.warning("Invalid access code, please try again.")
# check if access code already exists, if not create a new profile
user_id = await self.card_data.get_user_id_from_card(access_code)
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":
@ -143,10 +143,6 @@ class IDACReader(BaseReader):
# check if the table has a version column
if "version" in table.c:
data["version"] = self.version
# remain compatible with old profile dumps
if "asset_version" in data:
data.pop("asset_version", None)
sql = insert(table).values(
**data

View File

@ -1,63 +0,0 @@
from datetime import datetime
from typing import Dict, Optional
from sqlalchemy import (
Table,
Column,
UniqueConstraint,
and_,
)
from sqlalchemy.types import Integer, TIMESTAMP
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
lottery = Table(
"idac_user_lottery",
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("saved_value", Integer, nullable=False),
Column("lottery_count", Integer, nullable=False),
Column("create_date", TIMESTAMP, server_default=func.now()),
UniqueConstraint("user", "version", name="idac_user_lottery_uk"),
mysql_charset="utf8mb4",
)
class IDACFactoryData(BaseData):
async def get_lottery(self, aime_id: int, version: int) -> Optional[Row]:
sql = select(lottery).where(
and_(
lottery.c.user == aime_id,
lottery.c.version == version
)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def put_lottery(
self, aime_id: int, version: int, saved_value: int, lottery_count: int, create_date: datetime
) -> Optional[int]:
lottery_data = {
"user": aime_id,
"version": version,
"saved_value": saved_value,
"lottery_count": lottery_count,
"create_date": create_date
}
sql = insert(lottery).values(**lottery_data)
conflict = sql.on_duplicate_key_update(**lottery_data)
result = await self.execute(conflict)
if result is None:
self.logger.warn(f"put_lottery: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid

View File

@ -224,28 +224,26 @@ vs_info = Table(
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("battle_mode", Integer),
Column("invalid", Integer),
Column("str", Integer),
Column("str_now", Integer),
Column("lose_now", Integer),
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", Boolean),
UniqueConstraint("user", "battle_mode", name="idac_user_vs_info_uk"),
mysql_charset="utf8mb4",
)
vs_course_info = Table(
"idac_user_vs_course_info",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("battle_mode", Integer),
Column("course_id", Integer),
Column("vs_cnt", Integer),
Column("vs_win", Integer),
UniqueConstraint("user", "battle_mode", "course_id", name="idac_user_vs_course_info_uk"),
Column("break_penalty_flag", Integer),
UniqueConstraint("user", "group_key", name="idac_user_vs_info_uk"),
mysql_charset="utf8mb4",
)
@ -297,23 +295,6 @@ timetrial_event = Table(
mysql_charset="utf8mb4",
)
battle_gift = Table(
"idac_user_battle_gift",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("battle_gift_event_id", Integer),
Column("gift_id", Integer),
Column("gift_status", Integer),
Column("received_date", TIMESTAMP, server_default=func.now()),
UniqueConstraint("user", "battle_gift_event_id", "gift_id", name="idac_user_battle_gift_uk"),
mysql_charset="utf8mb4",
)
class IDACItemData(BaseData):
async def get_random_user_car(self, aime_id: int, version: int) -> Optional[List[Row]]:
@ -330,7 +311,7 @@ class IDACItemData(BaseData):
return result.fetchone()
async def get_random_car(self, version: int) -> Optional[List[Row]]:
sql = select(car).where(car.c.version <= version).order_by(func.rand()).limit(1)
sql = select(car).where(car.c.version == version).order_by(func.rand()).limit(1)
result = await self.execute(sql)
if result is None:
@ -343,10 +324,10 @@ class IDACItemData(BaseData):
sql = select(car).where(
and_(
car.c.user == aime_id,
car.c.version <= version,
car.c.version == version,
car.c.style_car_id == style_car_id,
)
).order_by(car.c.version.desc())
)
result = await self.execute(sql)
if result is None:
@ -360,16 +341,14 @@ class IDACItemData(BaseData):
sql = select(car).where(
and_(
car.c.user == aime_id,
car.c.version <= version,
car.c.version == version,
car.c.pickup_seq != 0,
)
)
else:
sql = select(car).where(
and_(car.c.user == aime_id, car.c.version <= version)
and_(car.c.user == aime_id, car.c.version == version)
)
sql = sql.order_by(car.c.version.desc())
result = await self.execute(sql)
if result is None:
@ -520,20 +499,23 @@ class IDACItemData(BaseData):
async def get_time_trial_best_cars_by_course(
self, version: int, course_id: int, aime_id: Optional[int] = None
) -> Optional[List[Row]]:
subquery = select(
trial.c.version,
func.min(trial.c.goal_time).label("min_goal_time"),
trial.c.style_car_id,
).where(
and_(
trial.c.version == version,
trial.c.course_id == course_id,
subquery = (
select(
trial.c.version,
func.min(trial.c.goal_time).label("min_goal_time"),
trial.c.style_car_id,
)
.where(
and_(
trial.c.version == version,
trial.c.course_id == course_id,
)
)
)
if aime_id is not None:
subquery = subquery.where(trial.c.user == aime_id)
subquery = subquery.group_by(trial.c.style_car_id).subquery()
sql = select(trial).where(
@ -550,45 +532,12 @@ class IDACItemData(BaseData):
return None
return result.fetchall()
async def get_time_trial_ranking_by_course_total(
self,
version: int,
course_id: int,
) -> Optional[List[Row]]:
# count the number of rows returned by the query
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))
.group_by(trial.c.user)
).subquery()
sql = (
select(func.count().label("count"))
.where(
and_(
trial.c.version == subquery.c.version,
trial.c.user == subquery.c.user,
trial.c.goal_time == subquery.c.min_goal_time,
),
)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def get_time_trial_ranking_by_course(
self,
version: int,
course_id: int,
style_car_id: Optional[int] = None,
limit: Optional[int] = 10,
offset: Optional[int] = 0,
) -> Optional[List[Row]]:
# get the top 10 ranking by goal_time for a given course which is grouped by user
subquery = select(
@ -597,7 +546,7 @@ class IDACItemData(BaseData):
func.min(trial.c.goal_time).label("min_goal_time"),
).where(and_(trial.c.version == version, trial.c.course_id == course_id))
# if wanted filter only by style_car_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)
@ -619,10 +568,6 @@ class IDACItemData(BaseData):
if limit is not None:
sql = sql.limit(limit)
# offset the result if needed
if offset is not None:
sql = sql.offset(offset)
result = await self.execute(sql)
if result is None:
return None
@ -695,7 +640,7 @@ class IDACItemData(BaseData):
challenge.c.vs_type,
challenge.c.cleared_difficulty.label("max_clear_lv"),
challenge.c.play_difficulty.label("last_play_lv"),
challenge.c.last_play_course_id,
challenge.c.course_id,
challenge.c.play_count,
)
.where(
@ -793,27 +738,6 @@ class IDACItemData(BaseData):
return None
return result.fetchall()
async def get_vs_info_by_mode(self, aime_id: int, battle_mode: int) -> Optional[List[Row]]:
sql = select(vs_info).where(
and_(vs_info.c.user == aime_id, vs_info.c.battle_mode == battle_mode)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
# This method returns a list of course_info
async def get_vs_course_infos_by_mode(self, aime_id: int, battle_mode: int) -> Optional[List[Row]]:
sql = select(vs_course_info).where(
and_(vs_course_info.c.user == aime_id, vs_course_info.c.battle_mode == battle_mode)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchall()
async def get_stamps(self, aime_id: int) -> Optional[List[Row]]:
sql = select(stamp).where(
and_(
@ -838,19 +762,6 @@ class IDACItemData(BaseData):
if result is None:
return None
return result.fetchone()
async def get_battle_gifts(self, aime_id: int, battle_gift_event_id: int) -> Optional[Row]:
sql = select(battle_gift).where(
and_(
battle_gift.c.user == aime_id,
battle_gift.c.battle_gift_event_id == battle_gift_event_id,
)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchall()
async def put_car(self, aime_id: int, version: int, car_data: Dict) -> Optional[int]:
car_data["user"] = aime_id
@ -1023,9 +934,8 @@ class IDACItemData(BaseData):
return None
return result.lastrowid
async def put_vs_info(self, aime_id: int, battle_mode: int, vs_info_data: Dict) -> Optional[int]:
async def put_vs_info(self, aime_id: int, vs_info_data: Dict) -> Optional[int]:
vs_info_data["user"] = aime_id
vs_info_data["battle_mode"] = battle_mode
sql = insert(vs_info).values(**vs_info_data)
conflict = sql.on_duplicate_key_update(**vs_info_data)
@ -1036,19 +946,6 @@ class IDACItemData(BaseData):
return None
return result.lastrowid
async def put_vs_course_info(self, aime_id: int, battle_mode: int, course_info_data: Dict) -> Optional[int]:
course_info_data["user"] = aime_id
course_info_data["battle_mode"] = battle_mode
sql = insert(vs_course_info).values(**course_info_data)
conflict = sql.on_duplicate_key_update(**course_info_data)
result = await self.execute(conflict)
if result is None:
self.logger.warn(f"put_vs_course_info: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
async def put_stamp(
self, aime_id: int, stamp_data: Dict
) -> Optional[int]:
@ -1059,7 +956,9 @@ class IDACItemData(BaseData):
result = await self.execute(conflict)
if result is None:
self.logger.warn(f"putstamp: Failed to update! aime_id: {aime_id}")
self.logger.warn(
f"putstamp: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid
@ -1082,19 +981,3 @@ class IDACItemData(BaseData):
)
return None
return result.lastrowid
async def put_battle_gift(
self, aime_id: int, battle_gift_data: Dict
) -> Optional[int]:
battle_gift_data["user"] = aime_id
sql = insert(battle_gift).values(**battle_gift_data)
conflict = sql.on_duplicate_key_update(**battle_gift_data)
result = await self.execute(conflict)
if result is None:
self.logger.warn(
f"put_battle_gift: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid

View File

@ -28,7 +28,7 @@ profile = Table(
Column("daily_play", Integer, server_default="0"),
Column("day_play", Integer, server_default="0"),
Column("mileage", Integer, server_default="0"),
Column("device_version", String(7), server_default="1.50.00"),
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"),
@ -244,28 +244,6 @@ theory = Table(
mysql_charset="utf8mb4",
)
tips = Table(
"idac_profile_tips",
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("tips_list", String(16), server_default="QAAAAAAAAAAAAAAA"),
Column("timetrial_play_count", Integer, server_default="0"),
Column("story_play_count", Integer, server_default="0"),
Column("store_battle_play_count", Integer, server_default="0"),
Column("online_battle_play_count", Integer, server_default="0"),
Column("special_play_count", Integer, server_default="0"),
Column("challenge_play_count", Integer, server_default="0"),
Column("theory_play_count", Integer, server_default="0"),
UniqueConstraint("user", "version", name="idac_profile_tips_uk"),
mysql_charset="utf8mb4",
)
class IDACProfileData(BaseData):
def __init__(self, cfg: CoreConfig, conn: Connection) -> None:
@ -279,9 +257,9 @@ class IDACProfileData(BaseData):
sql = select(profile).where(
and_(
profile.c.user == aime_id,
profile.c.version <= version,
profile.c.version == version,
)
).order_by(profile.c.version.desc())
)
result = await self.execute(sql)
if result is None:
@ -336,9 +314,9 @@ class IDACProfileData(BaseData):
sql = select(rank).where(
and_(
rank.c.user == aime_id,
rank.c.version <= version,
rank.c.version == version,
)
).order_by(rank.c.version.desc())
)
result = await self.execute(sql)
if result is None:
@ -349,9 +327,9 @@ class IDACProfileData(BaseData):
sql = select(stock).where(
and_(
stock.c.user == aime_id,
stock.c.version <= version,
stock.c.version == version,
)
).order_by(stock.c.version.desc())
)
result = await self.execute(sql)
if result is None:
@ -362,22 +340,9 @@ class IDACProfileData(BaseData):
sql = select(theory).where(
and_(
theory.c.user == aime_id,
theory.c.version <= version,
theory.c.version == version,
)
).order_by(theory.c.version.desc())
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def get_profile_tips(self, aime_id: int, version: int) -> Optional[Row]:
sql = select(tips).where(
and_(
tips.c.user == aime_id,
tips.c.version <= version,
)
).order_by(tips.c.version.desc())
)
result = await self.execute(sql)
if result is None:
@ -473,20 +438,3 @@ class IDACProfileData(BaseData):
)
return None
return result.lastrowid
async def put_profile_tips(
self, aime_id: int, version: int, tips_data: Dict
) -> Optional[int]:
tips_data["user"] = aime_id
tips_data["version"] = version
sql = insert(tips).values(**tips_data)
conflict = sql.on_duplicate_key_update(**tips_data)
result = await self.execute(conflict)
if result is None:
self.logger.warn(
f"put_profile_tips: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid

View File

@ -1,145 +0,0 @@
from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, update
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 datetime import datetime
from core.data.schema import BaseData, metadata
from core.config import CoreConfig
round_details = Table(
"idac_round_info",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("round_id_in_json", Integer),
Column("name", String(64)),
Column("season", Integer),
Column("start_dt", TIMESTAMP, server_default=func.now()),
Column("end_dt", TIMESTAMP, server_default=func.now()),
mysql_charset="utf8mb4",
)
round_info = Table(
"idac_user_round_info",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("round_id", ForeignKey("idac_round_info.id", ondelete="cascade", onupdate="cascade")),
Column("count", Integer),
Column("win", Integer),
Column("point", Integer),
Column("play_dt", TIMESTAMP, server_default=func.now()),
UniqueConstraint("user", "round_id", name="idac_user_round_info_uk"),
mysql_charset="utf8mb4",
)
class IDACOnlineRounds(BaseData):
# get player's ranking from a specified round event
async def get_round_rank_by_id(self, aime_id: int, round_event_id: int) -> Optional[Row]:
subquery = (
select([func.group_concat(func.concat(round_info.c.user, '|', round_info.c.point),order_by=[round_info.c.point.desc(), round_info.c.play_dt])])
.select_from(round_info)
.where(round_info.c.round_id == round_event_id)
.as_scalar()
)
sql = (
select([func.find_in_set(func.concat(round_info.c.user, '|', round_info.c.point), subquery.label('rank'))])
.select_from(round_info)
.where(
and_(
round_info.c.user == aime_id,
round_info.c.round_id == round_event_id
)
)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
# get player's info from a specified round event
async def get_round_info_by_id(self, aime_id: int, round_event_id: int) -> Optional[Row]:
sql = select(round_info).where(
and_(
round_info.c.user == aime_id,
round_info.c.round_id == round_event_id
)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
# get top 5 of a specified round event
async def get_round_top_five(self, round_event_id: int) -> Optional[Row]:
subquery = (
select([func.group_concat(func.concat(round_info.c.user, '|', round_info.c.point),order_by=[round_info.c.point.desc(), round_info.c.play_dt])])
.select_from(round_info)
.where(round_info.c.round_id == round_event_id)
.as_scalar()
)
sql = (
select([func.find_in_set(func.concat(round_info.c.user, '|', round_info.c.point), subquery.label('rank'))], round_info.c.user)
.select_from(round_info)
.where(round_info.c.round_id == round_event_id)
.limit(5)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchall()
# save players info to a specified round event
async def put_round_event(
self, aime_id: int, round_id: int, round_data: Dict
) -> Optional[int]:
round_data["user"] = aime_id
round_data["round_id"] = round_id
sql = insert(round_info).values(**round_data)
conflict = sql.on_duplicate_key_update(**round_data)
result = await self.execute(conflict)
if result is None:
self.logger.warn(f"putround: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
# insert if the event does not exist in database
async def _try_load_round_event(
self, round_id: int, version: int, round_data: Dict
) -> Optional[int]:
sql = select(round_details).where(round_details.c.round_id_in_json == round_id)
result = await self.execute(sql)
# check if the round already exists
existing_round = result.fetchone() if result else None
if existing_round is None:
tmp = {
"round_id_in_json": round_id,
"name": round_data["round_event_nm"],
"season": version,
"start_dt": datetime.fromtimestamp(round_data["start_dt"]),
"end_dt": datetime.fromtimestamp(round_data["end_dt"]),
}
sql = insert(round_details).values(**tmp)
result = await self.execute(sql)
return result.lastrowid
return existing_round["id"]
# TODO: get top five players of last round event for Boot/GetConfigData

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -2,20 +2,130 @@
{% block content %}
<h1 class="mb-3">頭文字D THE ARCADE</h1>
<nav class="mb-3">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link {% if active_tab == 'ranking' %}active{% endif %}" aria-current="page" href="/game/idac/ranking">Ranking</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_tab == 'profile' %}active{% endif %}" href="/game/idac/profile">Profile</a>
</li>
</ul>
</nav>
{% if sesh is defined and sesh["user_id"] > 0 %}
<div class="card mb-3">
<div class="card-body">
{% if profile is defined and profile is not none %}
<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>-->
<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>Cash</h6>
<p class="text-muted">{{ profile.cash }} D</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 %}
</h4>
</div>
</div>
</div>
<div class="col col-lg-9 col-12">
<div class="card mb-3">
{% block tab %}
<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 / 1000}} km</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 %}
{% endblock tab %}
<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/templates/js/idac_scripts.js" %}

View File

@ -1,79 +1,10 @@
// Declare a global variable to store the JSON data
var constData;
function evaluateRank(evalId) {
if (evalId >= 1 && evalId <= 4) {
return "Rookie";
} else if (evalId >= 5 && evalId <= 8) {
return "Regular";
} else if (evalId >= 9 && evalId <= 12) {
return "Specialist";
} else if (evalId >= 13 && evalId <= 16) {
return "Expert";
} else if (evalId >= 17 && evalId <= 20) {
return "Pro";
} else if (evalId >= 21 && evalId <= 24) {
return "Master";
} else if (evalId == 25) {
return "Master+";
} else {
return "Invalid";
}
}
function formatGoalTime(milliseconds) {
// Convert the milliseconds to a time string
var minutes = Math.floor(milliseconds / 60000);
var seconds = Math.floor((milliseconds % 60000) / 1000);
milliseconds %= 1000;
return `${parseInt(minutes)}'${seconds.toString().padStart(2, '0')}"${milliseconds.toString().padStart(3, '0')}`;
}
// Function to get style_name for a given style_car_id
function getCarName(style_car_id) {
// Find the car with the matching style_car_id
var foundCar = constData.car.find(function (style) {
return style.style_car_id === style_car_id;
});
// Return the style_name if found, otherwise return Unknown
return foundCar ? foundCar.style_name : "Unknown car";
}
$(document).ready(function () {
// Make an AJAX request to load the JSON file
$.ajax({
url: "/game/idac/ranking/const.get",
type: "GET",
dataType: "json",
success: function (data) {
// Check if the 'course' array exists in the JSON data
if (data && data.course) {
// Assign the JSON data to the global variable
constData = data;
$('#exportBtn').click(function () {
window.location = "/game/idac/export";
// Get the select element
var selectElement = $("#course-select");
// appendAlert('Successfully exported the profile', 'success');
// Remove the Loading text
selectElement.empty();
// Loop through the 'course' array and add options to the select
$.each(constData.course, function (index, course) {
var option = '<option value="' + course.course_id + '"' + (index === 0 ? 'selected' : '') + '>' + course.course_name + '</option>';
selectElement.append(option);
});
// Simulate a change event on page load with the default value (0)
$("#course-select").val("0").change();
}
},
error: function (jqXHR, textStatus, errorThrown) {
// Print the error message as an option element
$("#course-select").html("<option value='0' selected disabled>" + textStatus + "</option>");
console.error("Error loading JSON file:", textStatus, errorThrown);
}
// Close the modal on success
$('#export').modal('hide');
});
});
});

View File

@ -1,129 +0,0 @@
{% extends "titles/idac/templates/idac_index.jinja" %}
{% block tab %}
{% 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>{{ 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>Cash</h6>
<p class="text-muted">{{ profile.cash }} D</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 %}
</h4>
</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 / 1000}} km</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/templates/profile/js/scripts.js" %}
</script>
{% endblock tab %}

View File

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

View File

@ -1,30 +0,0 @@
{% extends "titles/idac/templates/idac_index.jinja" %}
{% block tab %}
<div class="tab-content" id="nav-tabContent">
<!-- Ranking -->
<div class="tab-pane fade show active" id="nav-ranking" role="tabpanel" aria-labelledby="nav-ranking-tab"
tabindex="0">
<div class="row justify-content-md-center form-signin">
<div class="col col-lg-4">
<select class="form-select mb-3" id="course-select">
<option value="0" selected disabled>Loading Courses...</option>
</select>
</div>
</div>
<div class="card">
<div class="card-body">
<div id="table-ranking">
<div class="text-center">Loading Ranking...</div>
</div>
</div>
</div>
<div id="pagination-ranking"></div>
</div>
</div>
<script type="text/javascript">
{% include "titles/idac/templates/ranking/js/scripts.js" %}
</script>
{% endblock tab %}

View File

@ -1,95 +0,0 @@
// Function to load data based on the selected value
function loadRanking(courseId, pageNumber = 1) {
// Make a GET request to the server
$.ajax({
url: "/game/idac/ranking/ranking.get",
type: "GET",
data: { courseId: courseId, pageNumber: pageNumber },
dataType: "json",
success: function (data) {
// check if an error inside the json exists
if (!data.success) {
// Inject the table into the container
$("#table-ranking").html("<div class='text-center'>" + data.error + "</div>");
console.error("Error: " + data.error);
return;
}
// get the total number of pages
var total_pages = data.total_pages;
// Generate the HTML table
var tableHtml = '<div data-bs-spy="scroll" data-bs-smooth-scroll="true" class="table-responsive"><table class="table table-hover"><thead><tr><th scope="col">#</th><th scope="col">Name/Car</th><th scope="col">Time</th><th scope="col" class="d-none d-sm-table-cell">Eval</th><th scope="col" class="d-none d-lg-table-cell">Store/Date</th></tr></thead><tbody>';
$.each(data.ranking, function (i, ranking) {
// Add a 1 to the i variable to get the correct rank number
tableHtml += `<tr id="rank-${i+1}" class="align-middle">`;
tableHtml += '<td>' + ranking.rank + '</td>';
tableHtml += '<td>' + ranking.name + '<br/>' + getCarName(ranking.style_car_id) + '</td>';
tableHtml += '<td class="fs-3">' + formatGoalTime(ranking.record) + '</td>';
tableHtml += '<td class="fs-4 d-none d-sm-table-cell">' + evaluateRank(ranking.eval_id) + '</td>';
// Ignore the Store and Date columns on small screens
tableHtml += '<td class="d-none d-lg-table-cell">' + ranking.store + '<br/>' + ranking.update_date + '</td>';
tableHtml += '</tr>';
});
tableHtml += '</tbody></table></div>';
// Inject the table into the container
$("#table-ranking").html(tableHtml);
// Generate the Pagination HTML
var paginationHtml = '<nav class="mt-3"><ul class="pagination justify-content-center">';
// Deactivate the previous button if the current page is the first page
paginationHtml += '<li class="page-item ' + (pageNumber === 1 ? 'disabled' : '') + '">';
paginationHtml += '<a class="page-link" href="#rank-1" data-page="' + (pageNumber - 1) + '">Previous</a>';
paginationHtml += '</li>';
for (var i = 1; i <= total_pages; i++) {
// Set the active class to the current page
paginationHtml += '<li class="page-item ' + (pageNumber === i ? 'active disabled' : '') + '"><a class="page-link" href="#rank-1" data-page="' + i + '">' + i + '</a></li>';
}
// Deactivate the next button if the current page is the last page
paginationHtml += '<li class="page-item ' + (pageNumber === total_pages ? 'disabled' : '') + '">';
paginationHtml += '<a class="page-link" href="#rank-1" data-page="' + (pageNumber + 1) + '">Next</a>';
paginationHtml += '</li>';
paginationHtml += '</ul></nav>';
// Inject the pagination into the container
$("#pagination-ranking").html(paginationHtml);
},
error: function (jqXHR, textStatus, errorThrown) {
// Inject the table into the container
$("#table-ranking").html("<div class='text-center'>" + textStatus + "</div>");
console.error("Error: " + textStatus, errorThrown);
}
});
}
// Function to handle page changes
function changePage(pageNumber) {
// Get the selected value
var courseId = $("#course-select").val();
// Call the function to load data with the new page number
loadRanking(courseId, pageNumber);
}
$(document).ready(function () {
// Attach an event handler to the select element
$("#course-select").change(function () {
// Get the selected value
var courseId = $(this).val();
// Call the function to load data
loadRanking(courseId);
});
// Event delegation for pagination links
$("#pagination-ranking").on("click", "a.page-link", function (event) {
// event.preventDefault(); // Prevent the default behavior of the link
var clickedPage = $(this).data("page");
// Check if the changePage function is not already in progress
if (!$(this).hasClass('disabled')) {
// Handle the page change here
changePage(clickedPage);
}
});
});

View File

@ -818,8 +818,7 @@ class Mai2Base:
}
async def handle_upload_user_portrait_api_request(self, data: Dict) -> Dict:
self.logger.warning("Portrait uploading not supported at this time.")
return {'returnCode': 0, 'apiName': 'UploadUserPortraitApi'}
self.logger.debug(data)
async def handle_upload_user_photo_api_request(self, data: Dict) -> Dict:
if not self.game_config.uploads.photos or not self.game_config.uploads.photos_dir: