forked from Hay1tsme/artemis
FGO #11
54
core/data/schema/versions/SDGT_1_rollback.sql
Normal file
54
core/data/schema/versions/SDGT_1_rollback.sql
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
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;
|
71
core/data/schema/versions/SDGT_2_upgrade.sql
Normal file
71
core/data/schema/versions/SDGT_2_upgrade.sql
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
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;
|
@ -62,9 +62,11 @@ Games listed below have been tested and confirmed working.
|
|||||||
In order to use the importer locate your game installation folder and execute:
|
In order to use the importer locate your game installation folder and execute:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python read.py --game SDBT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
|
python read.py --game SDBT --version <Version ID> --binfolder </path/to/game/data> --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.
|
The importer for Chunithm will import: Events, Music, Charge Items and Avatar Accesories.
|
||||||
|
|
||||||
### Config
|
### Config
|
||||||
@ -156,7 +158,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:
|
In order to use the importer you need to use the provided `Export.csv` file:
|
||||||
|
|
||||||
```shell
|
```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.
|
The importer for crossbeats REV. will import Music.
|
||||||
@ -198,11 +200,11 @@ Config file is located in `config/cxb.yaml`.
|
|||||||
In order to use the importer locate your game installation folder and execute:
|
In order to use the importer locate your game installation folder and execute:
|
||||||
DX:
|
DX:
|
||||||
```shell
|
```shell
|
||||||
python read.py --game <Game Code> --version <Version ID> --binfolder /path/to/StreamingAssets --optfolder /path/to/game/option/folder
|
python read.py --game <Game Code> --version <Version ID> --binfolder </path/to/Sinmai_Data> --optfolder </path/to/game/option/folder>
|
||||||
```
|
```
|
||||||
Pre-DX:
|
Pre-DX:
|
||||||
```shell
|
```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.
|
The importer for maimai DX will import Events, Music and Tickets.
|
||||||
|
|
||||||
@ -235,7 +237,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:
|
In order to use the importer locate your game installation folder and execute:
|
||||||
|
|
||||||
```shell
|
```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
|
The importer for Project Diva Arcade will all required data in order to use
|
||||||
@ -283,7 +285,7 @@ python dbutils.py --game SBZV upgrade
|
|||||||
In order to use the importer locate your game installation folder and execute:
|
In order to use the importer locate your game installation folder and execute:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python read.py --game SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
|
python read.py --game SDDT --version <Version ID> --binfolder </path/to/game/mu3_Data> --optfolder </path/to/game/option/folder>
|
||||||
```
|
```
|
||||||
|
|
||||||
The importer for O.N.G.E.K.I. will all all Cards, Music and Events.
|
The importer for O.N.G.E.K.I. will all all Cards, Music and Events.
|
||||||
@ -415,19 +417,19 @@ In order to use the importer you need to use the provided `.csv` files (which ar
|
|||||||
option folders:
|
option folders:
|
||||||
|
|
||||||
```shell
|
```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!**
|
**If you haven't already executed the O.N.G.E.K.I. importer, make sure you import all cards!**
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python read.py --game SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
|
python read.py --game SDDT --version <Version ID> --binfolder </path/to/ongeki/mu3_Data> --optfolder </path/to/ongeki/option/folder>
|
||||||
```
|
```
|
||||||
|
|
||||||
Also make sure to import all maimai DX and CHUNITHM data as well:
|
Also make sure to import all maimai DX and CHUNITHM data as well:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python read.py --game SDED --version <version ID> --binfolder /path/to/cardmaker/CardMaker_Data
|
python read.py --game SDED --version <Version ID> --binfolder </path/to/cardmaker/CardMaker_Data> --optfolder </path/to/cardmaker/option/folder>
|
||||||
```
|
```
|
||||||
|
|
||||||
The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai DX/CHUNITHM) and the hardcoded
|
The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai DX/CHUNITHM) and the hardcoded
|
||||||
@ -520,7 +522,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:
|
In order to use the importer locate your game installation folder and execute:
|
||||||
|
|
||||||
```shell
|
```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.
|
The importer for WACCA will import all Music data.
|
||||||
@ -684,6 +686,7 @@ python dbutils.py --game SDGT upgrade
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
|
|
||||||
- Online Battle is not supported
|
- Online Battle is not supported
|
||||||
- Online Battle Matching is not supported
|
- Online Battle Matching is not supported
|
||||||
|
|
||||||
@ -727,11 +730,10 @@ python dbutils.py --game SDGT upgrade
|
|||||||
|
|
||||||
### TimeRelease Courses:
|
### TimeRelease Courses:
|
||||||
|
|
||||||
|
|
||||||
| Course ID | Course Name | Direction |
|
| Course ID | Course Name | Direction |
|
||||||
| --------- | ------------------------- | ------------------------ |
|
| --------- | ------------------------- | ------------------------ |
|
||||||
| 0 | Akina Lake(秋名湖) | CounterClockwise(左周り) |
|
| 0 | Lake Akina(秋名湖) | CounterClockwise(左周り) |
|
||||||
| 2 | Akina Lake(秋名湖) | Clockwise(右周り) |
|
| 2 | Lake Akina(秋名湖) | Clockwise(右周り) |
|
||||||
| 52 | Hakone(箱根) | Downhill(下り) |
|
| 52 | Hakone(箱根) | Downhill(下り) |
|
||||||
| 54 | Hakone(箱根) | Hillclimb(上り) |
|
| 54 | Hakone(箱根) | Hillclimb(上り) |
|
||||||
| 36 | Usui(碓氷) | CounterClockwise(左周り) |
|
| 36 | Usui(碓氷) | CounterClockwise(左周り) |
|
||||||
@ -744,10 +746,10 @@ python dbutils.py --game SDGT upgrade
|
|||||||
| 14 | Akina(秋名) | Hillclimb(上り) |
|
| 14 | Akina(秋名) | Hillclimb(上り) |
|
||||||
| 16 | Irohazaka(いろは坂) | Downhill(下り) |
|
| 16 | Irohazaka(いろは坂) | Downhill(下り) |
|
||||||
| 18 | Irohazaka(いろは坂) | Reverse(逆走) |
|
| 18 | Irohazaka(いろは坂) | Reverse(逆走) |
|
||||||
| 56 | Momiji Line(もみじライン) | Downhill(下り) |
|
|
||||||
| 58 | Momiji Line(もみじライン) | Hillclimb(上り) |
|
|
||||||
| 20 | Tsukuba(筑波) | Outbound(往路) |
|
| 20 | Tsukuba(筑波) | Outbound(往路) |
|
||||||
| 22 | Tsukuba(筑波) | Inbound(復路) |
|
| 22 | Tsukuba(筑波) | Inbound(復路) |
|
||||||
|
| 56 | Momiji Line(もみじライン) | Downhill(下り) |
|
||||||
|
| 58 | Momiji Line(もみじライン) | Hillclimb(上り) |
|
||||||
| 24 | Happogahara(八方ヶ原) | Outbound(往路) |
|
| 24 | Happogahara(八方ヶ原) | Outbound(往路) |
|
||||||
| 26 | Happogahara(八方ヶ原) | Inbound(復路) |
|
| 26 | Happogahara(八方ヶ原) | Inbound(復路) |
|
||||||
| 40 | Sadamine(定峰) | Downhill(下り) |
|
| 40 | Sadamine(定峰) | Downhill(下り) |
|
||||||
@ -759,7 +761,54 @@ python dbutils.py --game SDGT upgrade
|
|||||||
| 68 | Odawara(小田原) | Forward(順走) |
|
| 68 | Odawara(小田原) | Forward(順走) |
|
||||||
| 70 | Odawara(小田原) | Reverse(逆走) |
|
| 70 | Odawara(小田原) | Reverse(逆走) |
|
||||||
|
|
||||||
### Credits
|
|
||||||
|
### 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 1280x720, 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
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
### Credits:
|
||||||
|
|
||||||
- Bottersnike: For the HUGE Reverse Engineering help
|
- Bottersnike: For the HUGE Reverse Engineering help
|
||||||
- Kinako: For helping with the timeRelease unlocking of courses and special mode
|
- Kinako: For helping with the timeRelease unlocking of courses and special mode
|
||||||
|
|
||||||
|
3
example_config/fgoa.yaml
Normal file
3
example_config/fgoa.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
server:
|
||||||
|
enable: True
|
||||||
|
loglevel: "info"
|
@ -4,6 +4,9 @@ A network service emulator for games running SEGA'S ALL.NET service, and similar
|
|||||||
# Supported games
|
# Supported games
|
||||||
Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported.
|
Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported.
|
||||||
|
|
||||||
|
+ Fate/Grand Order Arcade
|
||||||
|
+ 10.80
|
||||||
|
|
||||||
+ Card Maker
|
+ Card Maker
|
||||||
+ 1.30
|
+ 1.30
|
||||||
+ 1.35
|
+ 1.35
|
||||||
|
5
titles/fgoa/__init__.py
Normal file
5
titles/fgoa/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from titles.fgoa.index import FGOAServlet
|
||||||
|
from titles.fgoa.const import FGOAConstants
|
||||||
|
|
||||||
|
index = FGOAServlet
|
||||||
|
game_codes = [FGOAConstants.GAME_CODE]
|
30
titles/fgoa/base.py
Normal file
30
titles/fgoa/base.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from titles.fgoa.config import FGOAConfig
|
||||||
|
from titles.fgoa.const import FGOAConstants
|
||||||
|
|
||||||
|
|
||||||
|
class FGOABase:
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: FGOAConfig) -> None:
|
||||||
|
self.core_cfg = core_cfg
|
||||||
|
self.game_config = game_cfg
|
||||||
|
self.date_time_format = "%Y-%m-%d %H:%M:%S"
|
||||||
|
self.date_time_format_ext = (
|
||||||
|
"%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5]
|
||||||
|
)
|
||||||
|
self.date_time_format_short = "%Y-%m-%d"
|
||||||
|
self.logger = logging.getLogger("fgoa")
|
||||||
|
self.game = FGOAConstants.GAME_CODE
|
||||||
|
self.version = FGOAConstants.VER_FGOA_SEASON_1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_int_ver(version: str) -> str:
|
||||||
|
return version.replace(".", "")[:3]
|
||||||
|
|
||||||
|
async def handle_game_init_request(self, data: Dict) -> Dict:
|
||||||
|
return f""
|
24
titles/fgoa/config.py
Normal file
24
titles/fgoa/config.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from core.config import CoreConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FGOAServerConfig:
|
||||||
|
def __init__(self, parent: "FGOAConfig") -> None:
|
||||||
|
self.__config = parent
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enable(self) -> bool:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "fgo", "server", "enable", default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def loglevel(self) -> int:
|
||||||
|
return CoreConfig.str_to_loglevel(
|
||||||
|
CoreConfig.get_config_field(
|
||||||
|
self.__config, "fgo", "server", "loglevel", default="info"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
class FGOAConfig(dict):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.server = FGOAServerConfig(self)
|
14
titles/fgoa/const.py
Normal file
14
titles/fgoa/const.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
class FGOAConstants():
|
||||||
|
GAME_CODE = "SDEJ"
|
||||||
|
|
||||||
|
CONFIG_NAME = "fgoa.yaml"
|
||||||
|
|
||||||
|
VER_FGOA_SEASON_1 = 0
|
||||||
|
|
||||||
|
VERSION_STRING = (
|
||||||
|
"Fate/Grand Order Arcade",
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def game_ver_to_string(cls, ver: int):
|
||||||
|
return cls.VERSION_STRING[ver]
|
123
titles/fgoa/index.py
Normal file
123
titles/fgoa/index.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import json
|
||||||
|
import inflection
|
||||||
|
import yaml
|
||||||
|
import string
|
||||||
|
import logging
|
||||||
|
import coloredlogs
|
||||||
|
import zlib
|
||||||
|
import base64
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
from os import path
|
||||||
|
from typing import Dict, List, Tuple
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
|
||||||
|
from starlette.routing import Route
|
||||||
|
from starlette.responses import Response
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import PlainTextResponse
|
||||||
|
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from core.title import BaseServlet
|
||||||
|
from core.utils import Utils
|
||||||
|
|
||||||
|
from titles.fgoa.base import FGOABase
|
||||||
|
from titles.fgoa.config import FGOAConfig
|
||||||
|
from titles.fgoa.const import FGOAConstants
|
||||||
|
|
||||||
|
class FGOAServlet(BaseServlet):
|
||||||
|
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||||
|
self.core_cfg = core_cfg
|
||||||
|
self.game_cfg = FGOAConfig()
|
||||||
|
if path.exists(f"{cfg_dir}/{FGOAConstants.CONFIG_NAME}"):
|
||||||
|
self.game_cfg.update(
|
||||||
|
yaml.safe_load(open(f"{cfg_dir}/{FGOAConstants.CONFIG_NAME}"))
|
||||||
|
)
|
||||||
|
|
||||||
|
self.versions = [
|
||||||
|
FGOABase(core_cfg, self.game_cfg),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.logger = logging.getLogger("fgoa")
|
||||||
|
log_fmt_str = "[%(asctime)s] FGOA | %(levelname)s | %(message)s"
|
||||||
|
log_fmt = logging.Formatter(log_fmt_str)
|
||||||
|
fileHandler = TimedRotatingFileHandler(
|
||||||
|
"{0}/{1}.log".format(self.core_cfg.server.log_dir, "fgoa"),
|
||||||
|
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 = FGOAConfig()
|
||||||
|
|
||||||
|
if path.exists(f"{cfg_dir}/{FGOAConstants.CONFIG_NAME}"):
|
||||||
|
game_cfg.update(
|
||||||
|
yaml.safe_load(open(f"{cfg_dir}/{FGOAConstants.CONFIG_NAME}"))
|
||||||
|
)
|
||||||
|
|
||||||
|
if not game_cfg.server.enable:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_routes(self) -> List[Route]:
|
||||||
|
return [
|
||||||
|
Route("/SDEJ/{version:int}/{endpoint:str}", self.render_POST, methods=['POST'])
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]:
|
||||||
|
if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80:
|
||||||
|
return (f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}", self.core_cfg.server.hostname)
|
||||||
|
|
||||||
|
return (f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}", self.core_cfg.server.hostname)
|
||||||
|
|
||||||
|
|
||||||
|
async def render_POST(self, request: Request) -> bytes:
|
||||||
|
version: int = request.path_params.get('version')
|
||||||
|
endpoint: str = request.path_params.get('endpoint')
|
||||||
|
req_raw = await request.body()
|
||||||
|
internal_ver = 0
|
||||||
|
client_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
|
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
||||||
|
# If we get a 32 character long hex string, it's a hash and we're
|
||||||
|
# doing encrypted. The likelyhood of false positives is low but
|
||||||
|
# technically not 0
|
||||||
|
self.logger.error("Encryption not supported at this time")
|
||||||
|
|
||||||
|
self.logger.debug(req_raw)
|
||||||
|
|
||||||
|
self.logger.info(f"v{version} {endpoint} request from {client_ip}")
|
||||||
|
|
||||||
|
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
||||||
|
|
||||||
|
try:
|
||||||
|
handler = getattr(self.versions[internal_ver], func_to_find)
|
||||||
|
resp = await handler(req_raw)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
|
||||||
|
raise
|
||||||
|
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||||
|
|
||||||
|
if resp is None:
|
||||||
|
resp = {"returnCode": 1}
|
||||||
|
|
||||||
|
self.logger.debug(f"Response {resp}")
|
||||||
|
|
||||||
|
return Response(zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")))
|
@ -6,6 +6,9 @@ class IDACConstants():
|
|||||||
VER_IDAC_SEASON_1 = 0
|
VER_IDAC_SEASON_1 = 0
|
||||||
VER_IDAC_SEASON_2 = 1
|
VER_IDAC_SEASON_2 = 1
|
||||||
|
|
||||||
|
BATTLE_MODE_ONLINE = 0
|
||||||
|
BATTLE_MODE_OFFLINE = 1
|
||||||
|
|
||||||
VERSION_STRING = (
|
VERSION_STRING = (
|
||||||
"Initial D THE ARCADE Season 1",
|
"Initial D THE ARCADE Season 1",
|
||||||
"Initial D THE ARCADE Season 2",
|
"Initial D THE ARCADE Season 2",
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,10 +1,11 @@
|
|||||||
import json
|
import json
|
||||||
from typing import List
|
|
||||||
from starlette.routing import Route
|
|
||||||
from starlette.responses import Response, RedirectResponse
|
|
||||||
import yaml
|
import yaml
|
||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
from os import path
|
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 starlette.requests import Request
|
||||||
|
|
||||||
from core.frontend import FE_Base, UserSession
|
from core.frontend import FE_Base, UserSession
|
||||||
@ -16,19 +17,271 @@ from titles.idac.config import IDACConfig
|
|||||||
from titles.idac.const import IDACConstants
|
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):
|
class IDACFrontend(FE_Base):
|
||||||
|
isLeaf = False
|
||||||
|
children: Dict[str, Any] = {}
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
|
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(cfg, environment)
|
super().__init__(cfg, environment)
|
||||||
self.data = IDACData(cfg)
|
self.data = IDACData(cfg)
|
||||||
|
self.core_cfg = cfg
|
||||||
self.game_cfg = IDACConfig()
|
self.game_cfg = IDACConfig()
|
||||||
if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"):
|
if path.exists(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"):
|
||||||
self.game_cfg.update(
|
self.game_cfg.update(
|
||||||
yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"))
|
yaml.safe_load(open(f"{cfg_dir}/{IDACConstants.CONFIG_NAME}"))
|
||||||
)
|
)
|
||||||
#self.nav_name = "頭文字D THE ARCADE"
|
self.nav_name = "頭文字D THE ARCADE"
|
||||||
self.nav_name = "IDAC"
|
# 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"
|
||||||
# TODO: Add version list
|
# TODO: Add version list
|
||||||
self.version = IDACConstants.VER_IDAC_SEASON_2
|
self.version = IDACConstants.VER_IDAC_SEASON_2
|
||||||
|
|
||||||
@ -39,11 +292,6 @@ class IDACFrontend(FE_Base):
|
|||||||
34: "full_tune_fragments",
|
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):
|
async def generate_all_tables_json(self, user_id: int):
|
||||||
json_export = {}
|
json_export = {}
|
||||||
|
|
||||||
@ -67,7 +315,7 @@ class IDACFrontend(FE_Base):
|
|||||||
theory_running,
|
theory_running,
|
||||||
vs_info,
|
vs_info,
|
||||||
stamp,
|
stamp,
|
||||||
timetrial_event
|
timetrial_event,
|
||||||
}
|
}
|
||||||
|
|
||||||
for table in idac_tables:
|
for table in idac_tables:
|
||||||
@ -97,25 +345,29 @@ class IDACFrontend(FE_Base):
|
|||||||
uri: str = request.url.path
|
uri: str = request.url.path
|
||||||
|
|
||||||
template = self.environment.get_template(
|
template = self.environment.get_template(
|
||||||
"titles/idac/templates/idac_index.jinja"
|
"titles/idac/templates/profile/index.jinja"
|
||||||
)
|
)
|
||||||
usr_sesh = self.validate_session(request)
|
usr_sesh = self.validate_session(request)
|
||||||
if not usr_sesh:
|
if not usr_sesh:
|
||||||
usr_sesh = UserSession()
|
usr_sesh = UserSession()
|
||||||
user_id = usr_sesh.user_id
|
user_id = usr_sesh.user_id
|
||||||
# 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)
|
||||||
|
|
||||||
# profile export
|
# profile export
|
||||||
if uri.startswith("/game/idac/export"):
|
if uri.startswith("/game/idac/profile/export.get"):
|
||||||
if user_id == 0:
|
if user_id == 0:
|
||||||
return RedirectResponse(b"/game/idac", request)
|
return RedirectResponse("/game/idac", 303)
|
||||||
|
|
||||||
# set the file name, content type and size to download the json
|
# set the file name, content type and size to download the json
|
||||||
content = await self.generate_all_tables_json(user_id).encode("utf-8")
|
content = await self.generate_all_tables_json(user_id)
|
||||||
|
|
||||||
self.logger.info(f"User {user_id} exported their IDAC data")
|
self.logger.info(f"User {user_id} exported their IDAC data")
|
||||||
return Response(
|
return Response(
|
||||||
content,
|
content.encode("utf-8"),
|
||||||
200,
|
200,
|
||||||
{'content-disposition': 'attachment; filename=idac_profile.json'},
|
{'content-disposition': 'attachment; filename=idac_profile.json'},
|
||||||
"application/octet-stream"
|
"application/octet-stream"
|
||||||
@ -140,5 +392,7 @@ class IDACFrontend(FE_Base):
|
|||||||
tickets=tickets,
|
tickets=tickets,
|
||||||
rank=rank,
|
rank=rank,
|
||||||
sesh=vars(usr_sesh),
|
sesh=vars(usr_sesh),
|
||||||
|
username=user["username"],
|
||||||
active_page="idac",
|
active_page="idac",
|
||||||
|
active_tab="profile",
|
||||||
), media_type="text/html; charset=utf-8")
|
), media_type="text/html; charset=utf-8")
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import json
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
from starlette.routing import Route
|
|
||||||
from starlette.requests import Request
|
|
||||||
from starlette.responses import JSONResponse
|
|
||||||
import yaml
|
import yaml
|
||||||
import logging
|
import logging
|
||||||
import coloredlogs
|
import coloredlogs
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from os import path
|
from os import path
|
||||||
from typing import Dict, List, Tuple
|
from typing import Dict, List, Tuple
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
import asyncio
|
from starlette.routing import Route
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from core.title import BaseServlet, JSONResponseNoASCII
|
from core.title import BaseServlet, JSONResponseNoASCII
|
||||||
|
96
titles/idac/matching.py
Normal file
96
titles/idac/matching.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
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
|
@ -224,26 +224,52 @@ vs_info = Table(
|
|||||||
metadata,
|
metadata,
|
||||||
Column("id", Integer, primary_key=True, nullable=False),
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
|
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
|
||||||
Column("group_key", String(25)),
|
Column("battle_mode", Integer),
|
||||||
Column("win_flg", Integer),
|
Column("invalid", Integer),
|
||||||
Column("style_car_id", Integer),
|
Column("str", Integer),
|
||||||
Column("course_id", Integer),
|
Column("str_now", Integer),
|
||||||
Column("course_day", Integer),
|
Column("lose_now", 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("vs_history", Integer),
|
||||||
Column("break_count", Integer),
|
Column("break_count", Integer),
|
||||||
Column("break_penalty_flag", Integer),
|
Column("break_penalty_flag", Boolean),
|
||||||
UniqueConstraint("user", "group_key", name="idac_user_vs_info_uk"),
|
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"),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
|
||||||
|
round_infos = Table(
|
||||||
|
"idac_round_info",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
|
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", Integer),
|
||||||
|
Column("count", Integer),
|
||||||
|
Column("win", Integer),
|
||||||
|
Column("points", Integer),
|
||||||
|
UniqueConstraint("user", "round_id", name="idac_user_round_info_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -499,19 +525,16 @@ class IDACItemData(BaseData):
|
|||||||
async def get_time_trial_best_cars_by_course(
|
async def get_time_trial_best_cars_by_course(
|
||||||
self, version: int, course_id: int, aime_id: Optional[int] = None
|
self, version: int, course_id: int, aime_id: Optional[int] = None
|
||||||
) -> Optional[List[Row]]:
|
) -> Optional[List[Row]]:
|
||||||
subquery = (
|
subquery = select(
|
||||||
select(
|
|
||||||
trial.c.version,
|
trial.c.version,
|
||||||
func.min(trial.c.goal_time).label("min_goal_time"),
|
func.min(trial.c.goal_time).label("min_goal_time"),
|
||||||
trial.c.style_car_id,
|
trial.c.style_car_id,
|
||||||
)
|
).where(
|
||||||
.where(
|
|
||||||
and_(
|
and_(
|
||||||
trial.c.version == version,
|
trial.c.version == version,
|
||||||
trial.c.course_id == course_id,
|
trial.c.course_id == course_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if aime_id is not None:
|
if aime_id is not None:
|
||||||
subquery = subquery.where(trial.c.user == aime_id)
|
subquery = subquery.where(trial.c.user == aime_id)
|
||||||
@ -532,12 +555,45 @@ class IDACItemData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.fetchall()
|
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(
|
async def get_time_trial_ranking_by_course(
|
||||||
self,
|
self,
|
||||||
version: int,
|
version: int,
|
||||||
course_id: int,
|
course_id: int,
|
||||||
style_car_id: Optional[int] = None,
|
style_car_id: Optional[int] = None,
|
||||||
limit: Optional[int] = 10,
|
limit: Optional[int] = 10,
|
||||||
|
offset: Optional[int] = 0,
|
||||||
) -> Optional[List[Row]]:
|
) -> Optional[List[Row]]:
|
||||||
# get the top 10 ranking by goal_time for a given course which is grouped by user
|
# get the top 10 ranking by goal_time for a given course which is grouped by user
|
||||||
subquery = select(
|
subquery = select(
|
||||||
@ -546,7 +602,7 @@ class IDACItemData(BaseData):
|
|||||||
func.min(trial.c.goal_time).label("min_goal_time"),
|
func.min(trial.c.goal_time).label("min_goal_time"),
|
||||||
).where(and_(trial.c.version == version, trial.c.course_id == course_id))
|
).where(and_(trial.c.version == version, trial.c.course_id == course_id))
|
||||||
|
|
||||||
# if wantd filter only by style_car_id
|
# if wanted filter only by style_car_id
|
||||||
if style_car_id is not None:
|
if style_car_id is not None:
|
||||||
subquery = subquery.where(trial.c.style_car_id == style_car_id)
|
subquery = subquery.where(trial.c.style_car_id == style_car_id)
|
||||||
|
|
||||||
@ -568,6 +624,10 @@ class IDACItemData(BaseData):
|
|||||||
if limit is not None:
|
if limit is not None:
|
||||||
sql = sql.limit(limit)
|
sql = sql.limit(limit)
|
||||||
|
|
||||||
|
# offset the result if needed
|
||||||
|
if offset is not None:
|
||||||
|
sql = sql.offset(offset)
|
||||||
|
|
||||||
result = await self.execute(sql)
|
result = await self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
@ -738,6 +798,27 @@ class IDACItemData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.fetchall()
|
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]]:
|
async def get_stamps(self, aime_id: int) -> Optional[List[Row]]:
|
||||||
sql = select(stamp).where(
|
sql = select(stamp).where(
|
||||||
and_(
|
and_(
|
||||||
@ -934,8 +1015,9 @@ class IDACItemData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
async def put_vs_info(self, aime_id: int, vs_info_data: Dict) -> Optional[int]:
|
async def put_vs_info(self, aime_id: int, battle_mode: int, vs_info_data: Dict) -> Optional[int]:
|
||||||
vs_info_data["user"] = aime_id
|
vs_info_data["user"] = aime_id
|
||||||
|
vs_info_data["battle_mode"] = battle_mode
|
||||||
|
|
||||||
sql = insert(vs_info).values(**vs_info_data)
|
sql = insert(vs_info).values(**vs_info_data)
|
||||||
conflict = sql.on_duplicate_key_update(**vs_info_data)
|
conflict = sql.on_duplicate_key_update(**vs_info_data)
|
||||||
@ -946,6 +1028,19 @@ class IDACItemData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.lastrowid
|
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(
|
async def put_stamp(
|
||||||
self, aime_id: int, stamp_data: Dict
|
self, aime_id: int, stamp_data: Dict
|
||||||
) -> Optional[int]:
|
) -> Optional[int]:
|
||||||
@ -956,9 +1051,7 @@ class IDACItemData(BaseData):
|
|||||||
result = await self.execute(conflict)
|
result = await self.execute(conflict)
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
self.logger.warn(
|
self.logger.warn(f"putstamp: Failed to update! aime_id: {aime_id}")
|
||||||
f"putstamp: Failed to update! aime_id: {aime_id}"
|
|
||||||
)
|
|
||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
|
@ -539,7 +539,9 @@ class IDACSeason2(IDACBase):
|
|||||||
|
|
||||||
async def _generate_special_data(self, user_id: int) -> Dict:
|
async def _generate_special_data(self, user_id: int) -> Dict:
|
||||||
# 4 = special mode
|
# 4 = special mode
|
||||||
specials = await self.data.item.get_best_challenges_by_vs_type(user_id, story_type=4)
|
specials = await self.data.item.get_best_challenges_by_vs_type(
|
||||||
|
user_id, story_type=4
|
||||||
|
)
|
||||||
|
|
||||||
special_data = []
|
special_data = []
|
||||||
for s in specials:
|
for s in specials:
|
||||||
@ -589,6 +591,107 @@ class IDACSeason2(IDACBase):
|
|||||||
user_id, self.version, updated_stock_data
|
user_id, self.version, updated_stock_data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _update_vs_info(self, user_id: int, battle_mode: int, data: Dict) -> Dict:
|
||||||
|
vs_info = await self.data.item.get_vs_info_by_mode(user_id, battle_mode)
|
||||||
|
|
||||||
|
if vs_info is not None:
|
||||||
|
vs_info = vs_info._asdict()
|
||||||
|
del vs_info["id"]
|
||||||
|
del vs_info["user"]
|
||||||
|
|
||||||
|
vs_info["invalid"] = vs_info["invalid"] + data.get("result")
|
||||||
|
vs_info["str_now"] = (
|
||||||
|
vs_info["str_now"] + data.get("win_flg")
|
||||||
|
if data.get("win_flg") == 1
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
vs_info["str"] = (
|
||||||
|
vs_info["str"]
|
||||||
|
if vs_info["str"] > vs_info["str_now"]
|
||||||
|
else vs_info["str_now"]
|
||||||
|
)
|
||||||
|
vs_info["lose_now"] += 1 if data.get("win_flg") == 0 else 0
|
||||||
|
vs_info["vs_history"] = data.get("vs_history")
|
||||||
|
vs_info["break_count"] += data.get("break_count")
|
||||||
|
vs_info["break_penalty_flag"] = data.get("break_penalty_flag")
|
||||||
|
|
||||||
|
await self.data.item.put_vs_info(user_id, battle_mode, vs_info)
|
||||||
|
|
||||||
|
vs_info["vs_cnt"] = 0
|
||||||
|
vs_info["vs_win"] = 0
|
||||||
|
vs_info["vsinfo_course_data"] = []
|
||||||
|
|
||||||
|
vs_courses_info = await self.data.item.get_vs_course_infos_by_mode(
|
||||||
|
user_id, battle_mode
|
||||||
|
)
|
||||||
|
course_not_exists = True
|
||||||
|
|
||||||
|
if vs_courses_info is not None:
|
||||||
|
for course in vs_courses_info:
|
||||||
|
course = course._asdict()
|
||||||
|
del course["id"]
|
||||||
|
del course["user"]
|
||||||
|
|
||||||
|
if course["course_id"] == data.get("course_id"):
|
||||||
|
course["vs_cnt"] += 1
|
||||||
|
course["vs_win"] += data.get("win_flg")
|
||||||
|
vs_info["vs_cnt"] += course["vs_cnt"]
|
||||||
|
vs_info["vs_win"] += course["vs_win"]
|
||||||
|
await self.data.item.put_vs_course_info(
|
||||||
|
user_id, battle_mode, course
|
||||||
|
)
|
||||||
|
course_not_exists = False
|
||||||
|
else:
|
||||||
|
vs_info["vs_cnt"] += course["vs_cnt"]
|
||||||
|
vs_info["vs_win"] += course["vs_win"]
|
||||||
|
vs_info["vsinfo_course_data"].append(course)
|
||||||
|
|
||||||
|
if course_not_exists:
|
||||||
|
course = {}
|
||||||
|
course["course_id"] = data.get("course_id")
|
||||||
|
course["vs_cnt"] = 1
|
||||||
|
course["vs_win"] = data.get("win_flg")
|
||||||
|
vs_info["vs_cnt"] += course["vs_cnt"]
|
||||||
|
vs_info["vs_win"] += course["vs_win"]
|
||||||
|
vs_info["vsinfo_course_data"].append(course)
|
||||||
|
await self.data.item.put_vs_course_info(user_id, battle_mode, course)
|
||||||
|
else:
|
||||||
|
vs_info = {
|
||||||
|
"battle_mode": battle_mode,
|
||||||
|
# "vs_cnt": 1,
|
||||||
|
# "vs_win": data.get("win_flg"),
|
||||||
|
"invalid": data.get("result"),
|
||||||
|
"str": data.get("win_flg"),
|
||||||
|
"str_now": data.get("win_flg"),
|
||||||
|
"lose_now": 1 if data.get("win_flg") == 0 else 0,
|
||||||
|
"vs_history": data.get("vs_history"),
|
||||||
|
"break_count": data.get("break_count"),
|
||||||
|
"break_penalty_flag": data.get("break_penalty_flag"),
|
||||||
|
# "vsinfo_course_data": [
|
||||||
|
# {
|
||||||
|
# "course_id": data.get("course_id"),
|
||||||
|
# "vs_cnt": 1,
|
||||||
|
# "vs_win": data.get("win_flg")
|
||||||
|
# }
|
||||||
|
# ],
|
||||||
|
}
|
||||||
|
await self.data.item.put_vs_info(user_id, battle_mode, vs_info)
|
||||||
|
|
||||||
|
course_info = {
|
||||||
|
"course_id": data.get("course_id"),
|
||||||
|
"vs_cnt": 1,
|
||||||
|
"vs_win": data.get("win_flg"),
|
||||||
|
}
|
||||||
|
await self.data.item.put_vs_course_info(user_id, battle_mode, course_info)
|
||||||
|
|
||||||
|
vs_info["vs_cnt"] = 1
|
||||||
|
vs_info["vs_win"] = data.get("win_flg")
|
||||||
|
vs_info["vsinfo_course_data"] = []
|
||||||
|
vs_info["vsinfo_course_data"].append(course_info)
|
||||||
|
|
||||||
|
vs_info["course_select_priority"] = data.get("course_select_priority")
|
||||||
|
return vs_info
|
||||||
|
|
||||||
async def handle_user_getdata_request(self, data: Dict, headers: Dict):
|
async def handle_user_getdata_request(self, data: Dict, headers: Dict):
|
||||||
user_id = int(headers["session"])
|
user_id = int(headers["session"])
|
||||||
|
|
||||||
@ -714,25 +817,41 @@ class IDACSeason2(IDACBase):
|
|||||||
# get the users vs info data
|
# get the users vs info data
|
||||||
vs_info_data = []
|
vs_info_data = []
|
||||||
vs_info = await self.data.item.get_vs_infos(user_id)
|
vs_info = await self.data.item.get_vs_infos(user_id)
|
||||||
|
if vs_info is not None:
|
||||||
for vs in vs_info:
|
for vs in vs_info:
|
||||||
|
vs = vs._asdict()
|
||||||
|
vs_courses_infos = await self.data.item.get_vs_course_infos_by_mode(
|
||||||
|
user_id, vs["battle_mode"]
|
||||||
|
)
|
||||||
|
total_vs_win = 0
|
||||||
|
total_vs_cnt = 0
|
||||||
|
courses_info = []
|
||||||
|
if vs_courses_infos is not None:
|
||||||
|
for course in vs_courses_infos:
|
||||||
|
tmp = course._asdict()
|
||||||
|
del tmp["id"]
|
||||||
|
del tmp["user"]
|
||||||
|
del tmp["battle_mode"]
|
||||||
|
|
||||||
|
total_vs_win += tmp["vs_win"]
|
||||||
|
total_vs_cnt += tmp["vs_cnt"]
|
||||||
|
|
||||||
|
courses_info.append(tmp)
|
||||||
|
|
||||||
vs_info_data.append(
|
vs_info_data.append(
|
||||||
{
|
{
|
||||||
"battle_mode": 1,
|
"battle_mode": vs["battle_mode"],
|
||||||
"vs_cnt": 1,
|
"vs_cnt": total_vs_cnt,
|
||||||
"vs_win": vs["win_flg"],
|
"vs_win": total_vs_win,
|
||||||
"invalid": 0,
|
"invalid": vs["invalid"],
|
||||||
"str": 0,
|
"str": vs["str"],
|
||||||
"str_now": 0,
|
"str_now": vs["str_now"],
|
||||||
"lose_now": 0,
|
"lose_now": vs["lose_now"],
|
||||||
"vs_history": vs["vs_history"],
|
"vs_history": vs["vs_history"],
|
||||||
"course_select_priority": 0,
|
"course_select_priority": 0,
|
||||||
"vsinfo_course_data": [
|
"break_count": vs["break_count"],
|
||||||
{
|
"break_penalty_flag": vs["break_penalty_flag"],
|
||||||
"course_id": vs["course_id"],
|
"vsinfo_course_data": courses_info,
|
||||||
"vs_cnt": 1,
|
|
||||||
"vs_win": vs["win_flg"],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -798,7 +917,9 @@ class IDACSeason2(IDACBase):
|
|||||||
|
|
||||||
# get the user's timetrial event data
|
# get the user's timetrial event data
|
||||||
timetrial_event_data = {}
|
timetrial_event_data = {}
|
||||||
timetrial = await self.data.item.get_timetrial_event(user_id, self.timetrial_event_id)
|
timetrial = await self.data.item.get_timetrial_event(
|
||||||
|
user_id, self.timetrial_event_id
|
||||||
|
)
|
||||||
if timetrial is not None:
|
if timetrial is not None:
|
||||||
timetrial_event_data = {
|
timetrial_event_data = {
|
||||||
"timetrial_event_id": timetrial["timetrial_event_id"],
|
"timetrial_event_id": timetrial["timetrial_event_id"],
|
||||||
@ -885,14 +1006,18 @@ class IDACSeason2(IDACBase):
|
|||||||
"special_mode_hint_data": {"story_type": 0, "hint_display_flag": 0},
|
"special_mode_hint_data": {"story_type": 0, "hint_display_flag": 0},
|
||||||
}
|
}
|
||||||
|
|
||||||
async def handle_timetrial_getbestrecordpreta_request(self, data: Dict, headers: Dict):
|
async def handle_timetrial_getbestrecordpreta_request(
|
||||||
|
self, data: Dict, headers: Dict
|
||||||
|
):
|
||||||
user_id = headers["session"]
|
user_id = headers["session"]
|
||||||
|
|
||||||
for car_id in data["car_ids"]:
|
for car_id in data["car_ids"]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
course_mybest_data = []
|
course_mybest_data = []
|
||||||
courses = await self.data.item.get_time_trial_user_best_courses(self.version, user_id)
|
courses = await self.data.item.get_time_trial_user_best_courses(
|
||||||
|
self.version, user_id
|
||||||
|
)
|
||||||
for course in courses:
|
for course in courses:
|
||||||
course_mybest_data.append(
|
course_mybest_data.append(
|
||||||
{
|
{
|
||||||
@ -960,7 +1085,9 @@ class IDACSeason2(IDACBase):
|
|||||||
"course_pickup_car_best_data": course_pickup_car_best_data,
|
"course_pickup_car_best_data": course_pickup_car_best_data,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def handle_timetrial_getbestrecordprerace_request(self, data: Dict, headers: Dict):
|
async def handle_timetrial_getbestrecordprerace_request(
|
||||||
|
self, data: Dict, headers: Dict
|
||||||
|
):
|
||||||
user_id = headers["session"]
|
user_id = headers["session"]
|
||||||
|
|
||||||
course_id = data["course_id"]
|
course_id = data["course_id"]
|
||||||
@ -1170,7 +1297,9 @@ class IDACSeason2(IDACBase):
|
|||||||
|
|
||||||
return {"status_code": "0"}
|
return {"status_code": "0"}
|
||||||
|
|
||||||
async def handle_factory_updatecustomizeresult_request(self, data: Dict, headers: Dict):
|
async def handle_factory_updatecustomizeresult_request(
|
||||||
|
self, data: Dict, headers: Dict
|
||||||
|
):
|
||||||
user_id = headers["session"]
|
user_id = headers["session"]
|
||||||
|
|
||||||
parts_data: List = data.pop("parts_list")
|
parts_data: List = data.pop("parts_list")
|
||||||
@ -1308,7 +1437,9 @@ class IDACSeason2(IDACBase):
|
|||||||
|
|
||||||
return {"status_code": "0"}
|
return {"status_code": "0"}
|
||||||
|
|
||||||
async def handle_factory_updatecustomizeavatar_request(self, data: Dict, headers: Dict):
|
async def handle_factory_updatecustomizeavatar_request(
|
||||||
|
self, data: Dict, headers: Dict
|
||||||
|
):
|
||||||
user_id = headers["session"]
|
user_id = headers["session"]
|
||||||
|
|
||||||
avatar_data: Dict = data.pop("avatar_obj")
|
avatar_data: Dict = data.pop("avatar_obj")
|
||||||
@ -1322,7 +1453,9 @@ class IDACSeason2(IDACBase):
|
|||||||
|
|
||||||
return {"status_code": "0"}
|
return {"status_code": "0"}
|
||||||
|
|
||||||
async def handle_factory_updatecustomizeuser_request(self, data: Dict, headers: Dict):
|
async def handle_factory_updatecustomizeuser_request(
|
||||||
|
self, data: Dict, headers: Dict
|
||||||
|
):
|
||||||
user_id = headers["session"]
|
user_id = headers["session"]
|
||||||
|
|
||||||
stock_data: Dict = data.pop("stock_obj")
|
stock_data: Dict = data.pop("stock_obj")
|
||||||
@ -1344,7 +1477,9 @@ class IDACSeason2(IDACBase):
|
|||||||
|
|
||||||
return {"status_code": "0"}
|
return {"status_code": "0"}
|
||||||
|
|
||||||
async def handle_user_updatetimetrialresult_request(self, data: Dict, headers: Dict):
|
async def handle_user_updatetimetrialresult_request(
|
||||||
|
self, data: Dict, headers: Dict
|
||||||
|
):
|
||||||
user_id = headers["session"]
|
user_id = headers["session"]
|
||||||
|
|
||||||
stock_data: Dict = data.pop("stock_obj")
|
stock_data: Dict = data.pop("stock_obj")
|
||||||
@ -1605,7 +1740,9 @@ class IDACSeason2(IDACBase):
|
|||||||
"maker_use_count": [],
|
"maker_use_count": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def handle_user_updatespecialmoderesult_request(self, data: Dict, headers: Dict):
|
async def handle_user_updatespecialmoderesult_request(
|
||||||
|
self, data: Dict, headers: Dict
|
||||||
|
):
|
||||||
user_id = headers["session"]
|
user_id = headers["session"]
|
||||||
|
|
||||||
stock_data: Dict = data.pop("stock_obj")
|
stock_data: Dict = data.pop("stock_obj")
|
||||||
@ -1676,7 +1813,9 @@ class IDACSeason2(IDACBase):
|
|||||||
"maker_use_count": [],
|
"maker_use_count": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def handle_user_updatechallengemoderesult_request(self, data: Dict, headers: Dict):
|
async def handle_user_updatechallengemoderesult_request(
|
||||||
|
self, data: Dict, headers: Dict
|
||||||
|
):
|
||||||
user_id = headers["session"]
|
user_id = headers["session"]
|
||||||
|
|
||||||
stock_data: Dict = data.pop("stock_obj")
|
stock_data: Dict = data.pop("stock_obj")
|
||||||
@ -1754,7 +1893,9 @@ class IDACSeason2(IDACBase):
|
|||||||
"maker_use_count": [],
|
"maker_use_count": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _generate_time_trial_data(self, season_id: int, user_id: int) -> List[Dict]:
|
async def _generate_time_trial_data(
|
||||||
|
self, season_id: int, user_id: int
|
||||||
|
) -> List[Dict]:
|
||||||
# get the season time trial data from database
|
# get the season time trial data from database
|
||||||
timetrial_data = []
|
timetrial_data = []
|
||||||
|
|
||||||
@ -1801,7 +1942,9 @@ class IDACSeason2(IDACBase):
|
|||||||
season_id = data.get("season_id")
|
season_id = data.get("season_id")
|
||||||
|
|
||||||
# so to get the season 1 data just subtract 1 from the season id
|
# so to get the season 1 data just subtract 1 from the season id
|
||||||
past_timetrial_data = await self._generate_time_trial_data(season_id - 1, user_id)
|
past_timetrial_data = await self._generate_time_trial_data(
|
||||||
|
season_id - 1, user_id
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: get the current season timetrial data somehow, because after requesting
|
# TODO: get the current season timetrial data somehow, because after requesting
|
||||||
# GetPastSeasonTAData the game will NOT request GetTAData?!
|
# GetPastSeasonTAData the game will NOT request GetTAData?!
|
||||||
@ -1811,6 +1954,110 @@ class IDACSeason2(IDACBase):
|
|||||||
"past_season_timetrial_data": past_timetrial_data,
|
"past_season_timetrial_data": past_timetrial_data,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def handle_user_getpastseasonrounddata_request(self, data: Dict, headers: Dict):
|
||||||
|
user_id = headers["session"]
|
||||||
|
season_id = data.get("season_id")
|
||||||
|
|
||||||
|
# so to get the season 1 data just subtract 1 from the season id
|
||||||
|
past_timetrial_data = self._generate_time_trial_data(season_id - 1, user_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status_code": "0",
|
||||||
|
"season_id": season_id,
|
||||||
|
"past_season_round_event_data": [
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"win": 0,
|
||||||
|
"rank": 0,
|
||||||
|
"area_rank": 0,
|
||||||
|
"point": 0,
|
||||||
|
"total_round_point": 0,
|
||||||
|
"round_name": "DAC稼働記念 1stラウンド",
|
||||||
|
"round_id": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"win": 0,
|
||||||
|
"rank": 0,
|
||||||
|
"area_rank": 0,
|
||||||
|
"point": 0,
|
||||||
|
"total_round_point": 0,
|
||||||
|
"round_name": "シーズン1 2ndラウンド",
|
||||||
|
"round_id": 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"win": 0,
|
||||||
|
"rank": 0,
|
||||||
|
"area_rank": 0,
|
||||||
|
"point": 0,
|
||||||
|
"total_round_point": 0,
|
||||||
|
"round_name": "シーズン1 3rdラウンド",
|
||||||
|
"round_id": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"win": 0,
|
||||||
|
"rank": 0,
|
||||||
|
"area_rank": 0,
|
||||||
|
"point": 0,
|
||||||
|
"total_round_point": 0,
|
||||||
|
"round_name": "シーズン1 4thラウンド",
|
||||||
|
"round_id": 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"win": 0,
|
||||||
|
"rank": 0,
|
||||||
|
"area_rank": 0,
|
||||||
|
"point": 0,
|
||||||
|
"total_round_point": 0,
|
||||||
|
"round_name": "シーズン1 5thラウンド",
|
||||||
|
"round_id": 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"win": 0,
|
||||||
|
"rank": 0,
|
||||||
|
"area_rank": 0,
|
||||||
|
"point": 0,
|
||||||
|
"total_round_point": 0,
|
||||||
|
"round_name": "シーズン1 6thラウンド",
|
||||||
|
"round_id": 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"win": 0,
|
||||||
|
"rank": 0,
|
||||||
|
"area_rank": 0,
|
||||||
|
"point": 0,
|
||||||
|
"total_round_point": 0,
|
||||||
|
"round_name": "シーズン1 7thラウンド",
|
||||||
|
"round_id": 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"win": 0,
|
||||||
|
"rank": 0,
|
||||||
|
"area_rank": 0,
|
||||||
|
"point": 0,
|
||||||
|
"total_round_point": 0,
|
||||||
|
"round_name": "シーズン1 8thラウンド",
|
||||||
|
"round_id": 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"win": 0,
|
||||||
|
"rank": 0,
|
||||||
|
"area_rank": 0,
|
||||||
|
"point": 0,
|
||||||
|
"total_round_point": 0,
|
||||||
|
"round_name": "シーズン1 9thラウンド",
|
||||||
|
"round_id": 8,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
async def handle_user_gettadata_request(self, data: Dict, headers: Dict):
|
async def handle_user_gettadata_request(self, data: Dict, headers: Dict):
|
||||||
user_id = headers["session"]
|
user_id = headers["session"]
|
||||||
|
|
||||||
@ -1881,7 +2128,9 @@ class IDACSeason2(IDACBase):
|
|||||||
await self.data.item.put_ticket(user_id, ticket)
|
await self.data.item.put_ticket(user_id, ticket)
|
||||||
|
|
||||||
# save rank dist data in database
|
# save rank dist data in database
|
||||||
await self.data.profile.put_profile_rank(user_id, self.version, reward_dist_data)
|
await self.data.profile.put_profile_rank(
|
||||||
|
user_id, self.version, reward_dist_data
|
||||||
|
)
|
||||||
|
|
||||||
# update profile data and config in database
|
# update profile data and config in database
|
||||||
await self.data.profile.put_profile(user_id, self.version, data)
|
await self.data.profile.put_profile(user_id, self.version, data)
|
||||||
@ -1912,7 +2161,9 @@ class IDACSeason2(IDACBase):
|
|||||||
else:
|
else:
|
||||||
profile = await self.data.profile.get_profile(user_id, self.version)
|
profile = await self.data.profile.get_profile(user_id, self.version)
|
||||||
|
|
||||||
rank = await self.data.profile.get_profile_rank(profile["user"], self.version)
|
rank = await self.data.profile.get_profile_rank(
|
||||||
|
profile["user"], self.version
|
||||||
|
)
|
||||||
|
|
||||||
avatars = [
|
avatars = [
|
||||||
{
|
{
|
||||||
@ -1983,7 +2234,9 @@ class IDACSeason2(IDACBase):
|
|||||||
car = await self.data.item.get_random_car(self.version)
|
car = await self.data.item.get_random_car(self.version)
|
||||||
else:
|
else:
|
||||||
avatar = await self.data.profile.get_profile_avatar(profile["user"])
|
avatar = await self.data.profile.get_profile_avatar(profile["user"])
|
||||||
car = await self.data.item.get_random_user_car(profile["user"], self.version)
|
car = await self.data.item.get_random_user_car(
|
||||||
|
profile["user"], self.version
|
||||||
|
)
|
||||||
|
|
||||||
parts_list = []
|
parts_list = []
|
||||||
for part in car["parts_list"]:
|
for part in car["parts_list"]:
|
||||||
@ -2105,7 +2358,9 @@ class IDACSeason2(IDACBase):
|
|||||||
while len(user_list) < count_auto_match:
|
while len(user_list) < count_auto_match:
|
||||||
user_list.append(-1)
|
user_list.append(-1)
|
||||||
|
|
||||||
auto_match = await self._generate_theory_rival_data(user_list, course_id, user_id)
|
auto_match = await self._generate_theory_rival_data(
|
||||||
|
user_list, course_id, user_id
|
||||||
|
)
|
||||||
|
|
||||||
# get profiles with the same powerhouse_lv for power match
|
# get profiles with the same powerhouse_lv for power match
|
||||||
theory_courses = await self.data.item.get_theory_course_by_powerhouse_lv(
|
theory_courses = await self.data.item.get_theory_course_by_powerhouse_lv(
|
||||||
@ -2117,7 +2372,9 @@ class IDACSeason2(IDACBase):
|
|||||||
while len(user_list) < count_power_match:
|
while len(user_list) < count_power_match:
|
||||||
user_list.append(-1)
|
user_list.append(-1)
|
||||||
|
|
||||||
power_match = await self._generate_theory_rival_data(user_list, course_id, user_id)
|
power_match = await self._generate_theory_rival_data(
|
||||||
|
user_list, course_id, user_id
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status_code": "0",
|
"status_code": "0",
|
||||||
@ -2383,26 +2640,7 @@ class IDACSeason2(IDACBase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.data.item.put_vs_info(user_id, data)
|
vs_info = self._update_vs_info(user_id, IDACConstants.BATTLE_MODE_ONLINE, data)
|
||||||
|
|
||||||
vs_info = {
|
|
||||||
"battle_mode": 0,
|
|
||||||
"vs_cnt": 1,
|
|
||||||
"vs_win": data.get("win_flg"),
|
|
||||||
"invalid": 0,
|
|
||||||
"str": 0,
|
|
||||||
"str_now": 0,
|
|
||||||
"lose_now": 0,
|
|
||||||
"vs_history": data.get("vs_history"),
|
|
||||||
"course_select_priority": data.get("course_select_priority"),
|
|
||||||
"vsinfo_course_data": [
|
|
||||||
{
|
|
||||||
"course_id": data.get("course_id"),
|
|
||||||
"vs_cnt": 1,
|
|
||||||
"vs_win": data.get("win_flg"),
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status_code": "0",
|
"status_code": "0",
|
||||||
@ -2420,7 +2658,9 @@ class IDACSeason2(IDACBase):
|
|||||||
"maker_use_count": [],
|
"maker_use_count": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def handle_user_updatestorebattleresult_request(self, data: Dict, headers: Dict):
|
async def handle_user_updatestorebattleresult_request(
|
||||||
|
self, data: Dict, headers: Dict
|
||||||
|
):
|
||||||
user_id = headers["session"]
|
user_id = headers["session"]
|
||||||
|
|
||||||
stock_data: Dict = data.pop("stock_obj")
|
stock_data: Dict = data.pop("stock_obj")
|
||||||
@ -2482,26 +2722,9 @@ class IDACSeason2(IDACBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# save vs_info in database
|
# save vs_info in database
|
||||||
await self.data.item.put_vs_info(user_id, data)
|
vs_info = await self._update_vs_info(
|
||||||
|
user_id, IDACConstants.BATTLE_MODE_OFFLINE, data
|
||||||
vs_info = {
|
)
|
||||||
"battle_mode": 0,
|
|
||||||
"vs_cnt": 1,
|
|
||||||
"vs_win": data.get("win_flg"),
|
|
||||||
"invalid": 0,
|
|
||||||
"str": 0,
|
|
||||||
"str_now": 0,
|
|
||||||
"lose_now": 0,
|
|
||||||
"vs_history": data.get("vs_history"),
|
|
||||||
"course_select_priority": 0,
|
|
||||||
"vsinfo_course_data": [
|
|
||||||
{
|
|
||||||
"course_id": data.get("course_id"),
|
|
||||||
"vs_cnt": 1,
|
|
||||||
"vs_win": data.get("win_flg"),
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status_code": "0",
|
"status_code": "0",
|
||||||
|
1
titles/idac/templates/const.json
Normal file
1
titles/idac/templates/const.json
Normal file
File diff suppressed because one or more lines are too long
@ -2,130 +2,20 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="mb-3">頭文字D THE ARCADE</h1>
|
<h1 class="mb-3">頭文字D THE ARCADE</h1>
|
||||||
|
|
||||||
{% if sesh is defined and sesh["user_id"] > 0 %}
|
<nav class="mb-3">
|
||||||
<div class="card mb-3">
|
<ul class="nav nav-tabs">
|
||||||
<div class="card-body">
|
<li class="nav-item">
|
||||||
{% if profile is defined and profile is not none %}
|
<a class="nav-link {% if active_tab == 'ranking' %}active{% endif %}" aria-current="page" href="/game/idac/ranking">Ranking</a>
|
||||||
<div class="card-title">
|
</li>
|
||||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center">
|
<li class="nav-item">
|
||||||
<h3>{{ sesh["username"] }}'s Profile</h3>
|
<a class="nav-link {% if active_tab == 'profile' %}active{% endif %}" href="/game/idac/profile">Profile</a>
|
||||||
<div class="btn-toolbar mb-2 mb-md-0">
|
</li>
|
||||||
<div class="btn-group me-2">
|
</ul>
|
||||||
<!--<button type="button" class="btn btn-sm btn-outline-secondary">Share</button>-->
|
</nav>
|
||||||
<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">
|
|
||||||
|
|
||||||
<div class="card-body p-4">
|
{% block tab %}
|
||||||
<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">
|
{% endblock tab %}
|
||||||
<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">
|
<script type="text/javascript">
|
||||||
{% include "titles/idac/templates/js/idac_scripts.js" %}
|
{% include "titles/idac/templates/js/idac_scripts.js" %}
|
||||||
|
@ -1,10 +1,79 @@
|
|||||||
|
// 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 () {
|
$(document).ready(function () {
|
||||||
$('#exportBtn').click(function () {
|
// Make an AJAX request to load the JSON file
|
||||||
window.location = "/game/idac/export";
|
$.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;
|
||||||
|
|
||||||
// appendAlert('Successfully exported the profile', 'success');
|
// Get the select element
|
||||||
|
var selectElement = $("#course-select");
|
||||||
|
|
||||||
// Close the modal on success
|
// Remove the Loading text
|
||||||
$('#export').modal('hide');
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
129
titles/idac/templates/profile/index.jinja
Normal file
129
titles/idac/templates/profile/index.jinja
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
{% 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 %}
|
10
titles/idac/templates/profile/js/scripts.js
Normal file
10
titles/idac/templates/profile/js/scripts.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
$(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');
|
||||||
|
});
|
||||||
|
});
|
30
titles/idac/templates/ranking/index.jinja
Normal file
30
titles/idac/templates/ranking/index.jinja
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{% 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 %}
|
95
titles/idac/templates/ranking/js/scripts.js
Normal file
95
titles/idac/templates/ranking/js/scripts.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user