forked from Hay1tsme/artemis
Compare commits
No commits in common. "idac" and "master" have entirely different histories.
5
.gitignore
vendored
5
.gitignore
vendored
@ -160,7 +160,4 @@ config/*
|
||||
deliver/*
|
||||
*.gz
|
||||
|
||||
dbdump-*.json
|
||||
/.vs
|
||||
/titles/id8
|
||||
/titles/idac/battle.py
|
||||
dbdump-*.json
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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")
|
@ -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
|
||||
|
||||
|
@ -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")
|
@ -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
|
||||
|
||||
|
@ -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')
|
@ -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")
|
@ -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
|
||||
|
||||
|
@ -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;
|
@ -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;
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
3
index.py
3
index.py
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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")
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
@ -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
@ -30,6 +30,7 @@
|
||||
180,
|
||||
180,
|
||||
180,
|
||||
200,
|
||||
200
|
||||
],
|
||||
"reward": [
|
||||
|
@ -30,6 +30,7 @@
|
||||
180,
|
||||
180,
|
||||
180,
|
||||
200,
|
||||
200
|
||||
],
|
||||
"reward": [
|
||||
|
@ -30,6 +30,7 @@
|
||||
180,
|
||||
180,
|
||||
180,
|
||||
200,
|
||||
200
|
||||
],
|
||||
"reward": [
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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()
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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
@ -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" %}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
@ -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 %}
|
@ -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');
|
||||
});
|
||||
});
|
@ -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 %}
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user