FGO #11

Closed
FGO wants to merge 50 commits from (deleted):idac into develop
24 changed files with 1554 additions and 284 deletions

View 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;

View 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;

View File

@ -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:
```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.
### 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:
```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.
@ -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:
DX:
```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:
```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.
@ -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:
```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
@ -283,7 +285,7 @@ python dbutils.py --game SBZV upgrade
In order to use the importer locate your game installation folder and execute:
```shell
python read.py --game SDDT --version <version ID> --binfolder /path/to/game/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.
@ -415,19 +417,19 @@ In order to use the importer you need to use the provided `.csv` files (which ar
option folders:
```shell
python read.py --game SDED --version <version ID> --binfolder titles/cm/cm_data --optfolder /path/to/cardmaker/option/folder
python read.py --game SDED --version <Version ID> --binfolder titles/cm/cm_data --optfolder </path/to/cardmaker/option/folder>
```
**If you haven't already executed the O.N.G.E.K.I. importer, make sure you import all cards!**
```shell
python read.py --game SDDT --version <version ID> --binfolder /path/to/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:
```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
@ -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:
```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.
@ -669,9 +671,9 @@ Config file is located in `config/idac.yaml`.
| `port_matching` | Port number for the Online Battle Matching |
| `port_echo1/2` | Port numbers for Echos |
| `port_matching_p2p` | Port number for Online Battle (currently unsupported) |
| `stamp.enable` | Enables/Disabled the play stamp events |
| `stamp.enable` | Enables/Disabled the play stamp events |
| `stamp.enabled_stamps` | Define up to 3 play stamp events (without `.json` extension, which are placed in `titles/idac/data/stamps`) |
| `timetrial.enable` | Enables/Disables the time trial event |
| `timetrial.enable` | Enables/Disables the time trial event |
| `timetrial.enabled_timetrial` | Define one! trial event (without `.json` extension, which are placed in `titles/idac/data/timetrial`) |
@ -684,6 +686,7 @@ python dbutils.py --game SDGT upgrade
```
### Notes
- Online Battle is not supported
- Online Battle Matching is not supported
@ -727,11 +730,10 @@ python dbutils.py --game SDGT upgrade
### TimeRelease Courses:
| Course ID | Course Name | Direction |
| --------- | ------------------------- | ------------------------ |
| 0 | Akina Lake(秋名湖) | CounterClockwise(左周り) |
| 2 | Akina Lake(秋名湖) | Clockwise(右周り) |
| 0 | Lake Akina(秋名湖) | CounterClockwise(左周り) |
| 2 | Lake Akina(秋名湖) | Clockwise(右周り) |
| 52 | Hakone(箱根) | Downhill(下り) |
| 54 | Hakone(箱根) | Hillclimb(上り) |
| 36 | Usui(碓氷) | CounterClockwise(左周り) |
@ -744,10 +746,10 @@ python dbutils.py --game SDGT upgrade
| 14 | Akina(秋名) | Hillclimb(上り) |
| 16 | Irohazaka(いろは坂) | Downhill(下り) |
| 18 | Irohazaka(いろは坂) | Reverse(逆走) |
| 56 | Momiji Line(もみじライン) | Downhill(下り) |
| 58 | Momiji Line(もみじライン) | Hillclimb(上り) |
| 20 | Tsukuba(筑波) | Outbound(往路) |
| 22 | Tsukuba(筑波) | Inbound(復路) |
| 56 | Momiji Line(もみじライン) | Downhill(下り) |
| 58 | Momiji Line(もみじライン) | Hillclimb(上り) |
| 24 | Happogahara(八方ヶ原) | Outbound(往路) |
| 26 | Happogahara(八方ヶ原) | Inbound(復路) |
| 40 | Sadamine(定峰) | Downhill(下り) |
@ -759,7 +761,54 @@ python dbutils.py --game SDGT upgrade
| 68 | Odawara(小田原) | Forward(順走) |
| 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
- Kinako: For helping with the timeRelease unlocking of courses and special mode

3
example_config/fgoa.yaml Normal file
View File

@ -0,0 +1,3 @@
server:
enable: True
loglevel: "info"

View File

@ -4,6 +4,9 @@ A network service emulator for games running SEGA'S ALL.NET service, and similar
# 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.
+ Fate/Grand Order Arcade
+ 10.80
+ Card Maker
+ 1.30
+ 1.35

5
titles/fgoa/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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")))

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -1,15 +1,16 @@
import json
import traceback
from starlette.routing import Route
from starlette.requests import Request
from starlette.responses import JSONResponse
import yaml
import logging
import coloredlogs
import asyncio
from os import path
from typing import Dict, List, Tuple
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.title import BaseServlet, JSONResponseNoASCII

96
titles/idac/matching.py Normal file
View 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

View File

@ -224,26 +224,52 @@ vs_info = Table(
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("group_key", String(25)),
Column("win_flg", Integer),
Column("style_car_id", Integer),
Column("course_id", Integer),
Column("course_day", Integer),
Column("players_num", Integer),
Column("winning", Integer),
Column("advantage_1", Integer),
Column("advantage_2", Integer),
Column("advantage_3", Integer),
Column("advantage_4", Integer),
Column("select_course_id", Integer),
Column("select_course_day", Integer),
Column("select_course_random", Integer),
Column("matching_success_sec", Integer),
Column("boost_flag", Integer),
Column("battle_mode", Integer),
Column("invalid", Integer),
Column("str", Integer),
Column("str_now", Integer),
Column("lose_now", Integer),
Column("vs_history", Integer),
Column("break_count", Integer),
Column("break_penalty_flag", Integer),
UniqueConstraint("user", "group_key", name="idac_user_vs_info_uk"),
Column("break_penalty_flag", Boolean),
UniqueConstraint("user", "battle_mode", name="idac_user_vs_info_uk"),
mysql_charset="utf8mb4",
)
vs_course_info = Table(
"idac_user_vs_course_info",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("battle_mode", Integer),
Column("course_id", Integer),
Column("vs_cnt", Integer),
Column("vs_win", Integer),
UniqueConstraint("user", "battle_mode", "course_id", name="idac_user_vs_course_info_uk"),
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",
)
@ -499,23 +525,20 @@ class IDACItemData(BaseData):
async def get_time_trial_best_cars_by_course(
self, version: int, course_id: int, aime_id: Optional[int] = None
) -> Optional[List[Row]]:
subquery = (
select(
trial.c.version,
func.min(trial.c.goal_time).label("min_goal_time"),
trial.c.style_car_id,
)
.where(
and_(
trial.c.version == version,
trial.c.course_id == course_id,
)
subquery = select(
trial.c.version,
func.min(trial.c.goal_time).label("min_goal_time"),
trial.c.style_car_id,
).where(
and_(
trial.c.version == version,
trial.c.course_id == course_id,
)
)
if aime_id is not None:
subquery = subquery.where(trial.c.user == aime_id)
subquery = subquery.group_by(trial.c.style_car_id).subquery()
sql = select(trial).where(
@ -532,12 +555,45 @@ class IDACItemData(BaseData):
return None
return result.fetchall()
async def get_time_trial_ranking_by_course_total(
self,
version: int,
course_id: int,
) -> Optional[List[Row]]:
# count the number of rows returned by the query
subquery = (
select(
trial.c.version,
trial.c.user,
func.min(trial.c.goal_time).label("min_goal_time"),
)
.where(and_(trial.c.version == version, trial.c.course_id == course_id))
.group_by(trial.c.user)
).subquery()
sql = (
select(func.count().label("count"))
.where(
and_(
trial.c.version == subquery.c.version,
trial.c.user == subquery.c.user,
trial.c.goal_time == subquery.c.min_goal_time,
),
)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def get_time_trial_ranking_by_course(
self,
version: int,
course_id: int,
style_car_id: Optional[int] = None,
limit: Optional[int] = 10,
offset: Optional[int] = 0,
) -> Optional[List[Row]]:
# get the top 10 ranking by goal_time for a given course which is grouped by user
subquery = select(
@ -546,7 +602,7 @@ class IDACItemData(BaseData):
func.min(trial.c.goal_time).label("min_goal_time"),
).where(and_(trial.c.version == version, trial.c.course_id == course_id))
# if wantd filter only by style_car_id
# if wanted filter only by style_car_id
if style_car_id is not None:
subquery = subquery.where(trial.c.style_car_id == style_car_id)
@ -568,6 +624,10 @@ class IDACItemData(BaseData):
if limit is not None:
sql = sql.limit(limit)
# offset the result if needed
if offset is not None:
sql = sql.offset(offset)
result = await self.execute(sql)
if result is None:
return None
@ -738,6 +798,27 @@ class IDACItemData(BaseData):
return None
return result.fetchall()
async def get_vs_info_by_mode(self, aime_id: int, battle_mode: int) -> Optional[List[Row]]:
sql = select(vs_info).where(
and_(vs_info.c.user == aime_id, vs_info.c.battle_mode == battle_mode)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
# This method returns a list of course_info
async def get_vs_course_infos_by_mode(self, aime_id: int, battle_mode: int) -> Optional[List[Row]]:
sql = select(vs_course_info).where(
and_(vs_course_info.c.user == aime_id, vs_course_info.c.battle_mode == battle_mode)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchall()
async def get_stamps(self, aime_id: int) -> Optional[List[Row]]:
sql = select(stamp).where(
and_(
@ -934,8 +1015,9 @@ class IDACItemData(BaseData):
return None
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["battle_mode"] = battle_mode
sql = insert(vs_info).values(**vs_info_data)
conflict = sql.on_duplicate_key_update(**vs_info_data)
@ -946,6 +1028,19 @@ class IDACItemData(BaseData):
return None
return result.lastrowid
async def put_vs_course_info(self, aime_id: int, battle_mode: int, course_info_data: Dict) -> Optional[int]:
course_info_data["user"] = aime_id
course_info_data["battle_mode"] = battle_mode
sql = insert(vs_course_info).values(**course_info_data)
conflict = sql.on_duplicate_key_update(**course_info_data)
result = await self.execute(conflict)
if result is None:
self.logger.warn(f"put_vs_course_info: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
async def put_stamp(
self, aime_id: int, stamp_data: Dict
) -> Optional[int]:
@ -956,9 +1051,7 @@ class IDACItemData(BaseData):
result = await self.execute(conflict)
if result is None:
self.logger.warn(
f"putstamp: Failed to update! aime_id: {aime_id}"
)
self.logger.warn(f"putstamp: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid

View File

@ -539,7 +539,9 @@ class IDACSeason2(IDACBase):
async def _generate_special_data(self, user_id: int) -> Dict:
# 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 = []
for s in specials:
@ -589,6 +591,107 @@ class IDACSeason2(IDACBase):
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):
user_id = int(headers["session"])
@ -714,27 +817,43 @@ class IDACSeason2(IDACBase):
# get the users vs info data
vs_info_data = []
vs_info = await self.data.item.get_vs_infos(user_id)
for vs in vs_info:
vs_info_data.append(
{
"battle_mode": 1,
"vs_cnt": 1,
"vs_win": vs["win_flg"],
"invalid": 0,
"str": 0,
"str_now": 0,
"lose_now": 0,
"vs_history": vs["vs_history"],
"course_select_priority": 0,
"vsinfo_course_data": [
{
"course_id": vs["course_id"],
"vs_cnt": 1,
"vs_win": vs["win_flg"],
}
],
}
)
if vs_info is not None:
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(
{
"battle_mode": vs["battle_mode"],
"vs_cnt": total_vs_cnt,
"vs_win": total_vs_win,
"invalid": vs["invalid"],
"str": vs["str"],
"str_now": vs["str_now"],
"lose_now": vs["lose_now"],
"vs_history": vs["vs_history"],
"course_select_priority": 0,
"break_count": vs["break_count"],
"break_penalty_flag": vs["break_penalty_flag"],
"vsinfo_course_data": courses_info,
}
)
# get the user's car
cars = await self.data.item.get_cars(self.version, user_id, only_pickup=True)
@ -798,7 +917,9 @@ class IDACSeason2(IDACBase):
# get the user's 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:
timetrial_event_data = {
"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},
}
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"]
for car_id in data["car_ids"]:
pass
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:
course_mybest_data.append(
{
@ -960,7 +1085,9 @@ class IDACSeason2(IDACBase):
"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"]
course_id = data["course_id"]
@ -1170,7 +1297,9 @@ class IDACSeason2(IDACBase):
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"]
parts_data: List = data.pop("parts_list")
@ -1308,7 +1437,9 @@ class IDACSeason2(IDACBase):
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"]
avatar_data: Dict = data.pop("avatar_obj")
@ -1322,7 +1453,9 @@ class IDACSeason2(IDACBase):
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"]
stock_data: Dict = data.pop("stock_obj")
@ -1344,7 +1477,9 @@ class IDACSeason2(IDACBase):
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"]
stock_data: Dict = data.pop("stock_obj")
@ -1605,7 +1740,9 @@ class IDACSeason2(IDACBase):
"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"]
stock_data: Dict = data.pop("stock_obj")
@ -1676,7 +1813,9 @@ class IDACSeason2(IDACBase):
"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"]
stock_data: Dict = data.pop("stock_obj")
@ -1754,7 +1893,9 @@ class IDACSeason2(IDACBase):
"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
timetrial_data = []
@ -1801,7 +1942,9 @@ class IDACSeason2(IDACBase):
season_id = data.get("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
# GetPastSeasonTAData the game will NOT request GetTAData?!
@ -1811,6 +1954,110 @@ class IDACSeason2(IDACBase):
"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):
user_id = headers["session"]
@ -1881,7 +2128,9 @@ class IDACSeason2(IDACBase):
await self.data.item.put_ticket(user_id, ticket)
# 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
await self.data.profile.put_profile(user_id, self.version, data)
@ -1912,7 +2161,9 @@ class IDACSeason2(IDACBase):
else:
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 = [
{
@ -1983,7 +2234,9 @@ class IDACSeason2(IDACBase):
car = await self.data.item.get_random_car(self.version)
else:
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 = []
for part in car["parts_list"]:
@ -2105,7 +2358,9 @@ class IDACSeason2(IDACBase):
while len(user_list) < count_auto_match:
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
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:
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 {
"status_code": "0",
@ -2383,26 +2640,7 @@ class IDACSeason2(IDACBase):
},
)
await self.data.item.put_vs_info(user_id, 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"),
}
],
}
vs_info = self._update_vs_info(user_id, IDACConstants.BATTLE_MODE_ONLINE, data)
return {
"status_code": "0",
@ -2420,7 +2658,9 @@ class IDACSeason2(IDACBase):
"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"]
stock_data: Dict = data.pop("stock_obj")
@ -2482,26 +2722,9 @@ class IDACSeason2(IDACBase):
)
# save vs_info in database
await self.data.item.put_vs_info(user_id, 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"),
}
],
}
vs_info = await self._update_vs_info(
user_id, IDACConstants.BATTLE_MODE_OFFLINE, data
)
return {
"status_code": "0",

File diff suppressed because one or more lines are too long

View File

@ -2,130 +2,20 @@
{% block content %}
<h1 class="mb-3">頭文字D THE ARCADE</h1>
{% if sesh is defined and sesh["user_id"] > 0 %}
<div class="card mb-3">
<div class="card-body">
{% if profile is defined and profile is not none %}
<div class="card-title">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center">
<h3>{{ sesh["username"] }}'s Profile</h3>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<!--<button type="button" class="btn btn-sm btn-outline-secondary">Share</button>-->
<button type="button" data-bs-toggle="modal" data-bs-target="#export"
class="btn btn-sm btn-outline-primary">Export</button>
</div>
</div>
</div>
</div>
<!--<h4 class="card-subtitle mb-2 text-body-secondary">Card subtitle</h4>-->
<div class="row d-flex justify-content-center h-100">
<div class="col col-lg-3 col-12">
<div class="card mb-3">
<div class="card-body p-4">
<h5>Information</h5>
<hr class="mt-0 mb-4">
<h6>Username</h6>
<p class="text-muted">{{ profile.username }}</p>
<h6>Cash</h6>
<p class="text-muted">{{ profile.cash }} D</p>
<h6>Grade</h6>
<h4>
{% set grade = rank.grade %}
{% if grade >= 1 and grade <= 72 %}
{% set grade_number = (grade - 1) // 9 %}
{% set grade_letters = ['E', 'D', 'C', 'B', 'A', 'S', 'SS', 'X'] %}
{{ grade_letters[grade_number] }}{{ 9 - ((grade-1) % 9) }}
{% else %}
Unknown
{% endif %}
</h4>
</div>
</div>
</div>
<div class="col col-lg-9 col-12">
<div class="card mb-3">
<nav class="mb-3">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link {% if active_tab == 'ranking' %}active{% endif %}" aria-current="page" href="/game/idac/ranking">Ranking</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_tab == 'profile' %}active{% endif %}" href="/game/idac/profile">Profile</a>
</li>
</ul>
</nav>
<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 %}
{% block tab %}
<div class="modal fade" id="export" tabindex="-1" aria-labelledby="export-label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exort-label">Export Profile</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Download your profile as a <strong>.json</strong> file in order to import it into your local ARTEMiS
database.
<div class="alert alert-warning mt-3" role="alert">
{% if profile is defined and profile is not none %}
Are you sure you want to export your profile with the username {{ profile.username }}?
{% endif %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="exportBtn">Download Profile</a>
</div>
</div>
</div>
</div>
{% endblock tab %}
<script type="text/javascript">
{% include "titles/idac/templates/js/idac_scripts.js" %}

View File

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

View 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 %}

View 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');
});
});

View 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 %}

View 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);
}
});
});