diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index fa2a250..5ea8888 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -748,8 +748,8 @@ python dbutils.py --game SDGT upgrade | 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(左周り) | @@ -762,10 +762,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(下り) | diff --git a/titles/idac/frontend.py b/titles/idac/frontend.py index 78abae8..19c51ff 100644 --- a/titles/idac/frontend.py +++ b/titles/idac/frontend.py @@ -1,7 +1,10 @@ import json import yaml import jinja2 + from os import path +from typing import Any, Type +from twisted.web import resource from twisted.web.util import redirectTo from twisted.web.http import Request from twisted.web.server import Session @@ -15,12 +18,109 @@ from titles.idac.config import IDACConfig from titles.idac.const import IDACConstants +class RankingData: + def __init__( + self, + rank: int, + name: str, + record: 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.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.encode() 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.encode()][0].decode() + + # 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 + + def to_json(self): + return json.dumps(self.make(), default=str, ensure_ascii=False).encode("utf-8") + + 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( @@ -30,13 +130,159 @@ class IDACFrontend(FE_Base): # TODO: Add version list self.version = IDACConstants.VER_IDAC_SEASON_2 + self.putChild(b"profile", IDACProfileFrontend(cfg, self.environment)) + self.putChild(b"ranking", IDACRankingFrontend(cfg, self.environment)) + + + def render_GET(self, request: Request) -> bytes: + uri: str = request.uri.decode() + + template = self.environment.get_template( + "titles/idac/frontend/idac_index.jinja" + ) + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + + # redirect to the ranking page + if uri.startswith("/game/idac"): + return redirectTo(b"/game/idac/ranking", request) + + return 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", + ).encode("utf-16") + + 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 + + def render_GET(self, request: Request) -> bytes: + uri: str = request.uri.decode() + + template = self.environment.get_template( + "titles/idac/frontend/ranking/index.jinja" + ) + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + user_id = usr_sesh.userId + # user_id = usr_sesh.user_id + + # IDAC constants + if uri.startswith("/game/idac/ranking/const.get"): + # set the content type to json + request.responseHeaders.addRawHeader(b"content-type", b"application/json") + + # get the constants + with open("titles/idac/frontend/const.json", "r", encoding="utf-8") as f: + constants = json.load(f) + + return json.dumps(constants, ensure_ascii=False).encode("utf-8") + + # leaderboard ranking + elif uri.startswith("/game/idac/ranking/ranking.get"): + # set the content type to json + request.responseHeaders.addRawHeader(b"content-type", b"application/json") + + req = RankingRequest(request.args) + resp = RankingResponse() + + if not req.success: + resp.error = req.error + return resp.to_json() + + # get the total number of records + total_records = 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 resp.to_json() + + # get the total number of records + total = total_records["count"] + + limit = 50 + offset = (req.page_number - 1) * limit + + ranking = 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 = self.data.profile.get_profile(user_id, self.version) + arcade = 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"], + 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 resp.to_json() + + return 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", + ).encode("utf-16") + + +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 + self.ticket_names = { 3: "car_dressup_points", 5: "avatar_points", 25: "full_tune_tickets", 34: "full_tune_fragments", } - + def generate_all_tables_json(self, user_id: int): json_export = {} @@ -60,7 +306,7 @@ class IDACFrontend(FE_Base): theory_running, vs_info, stamp, - timetrial_event + timetrial_event, } for table in idac_tables: @@ -86,11 +332,12 @@ class IDACFrontend(FE_Base): return json.dumps(json_export, indent=4, default=str, ensure_ascii=False) + def render_GET(self, request: Request) -> bytes: uri: str = request.uri.decode() template = self.environment.get_template( - "titles/idac/frontend/idac_index.jinja" + "titles/idac/frontend/profile/index.jinja" ) sesh: Session = request.getSession() usr_sesh = IUserSession(sesh) @@ -98,7 +345,7 @@ class IDACFrontend(FE_Base): # user_id = usr_sesh.user_id # profile export - if uri.startswith("/game/idac/export"): + if uri.startswith("/game/idac/profile/export.get"): if user_id == 0: return redirectTo(b"/game/idac", request) @@ -136,7 +383,5 @@ class IDACFrontend(FE_Base): rank=rank, sesh=vars(usr_sesh), active_page="idac", + active_tab="profile", ).encode("utf-16") - - def render_POST(self, request: Request) -> bytes: - pass diff --git a/titles/idac/frontend/const.json b/titles/idac/frontend/const.json new file mode 100644 index 0000000..339d382 --- /dev/null +++ b/titles/idac/frontend/const.json @@ -0,0 +1 @@ +{"car":[{"style_car_id":0,"style_name":"SPRINTER TRUENO GT-APEX (AE86) [DH]","route_style_name":"DH"},{"style_car_id":131072,"style_name":"SPRINTER TRUENO GT-APEX (AE86) [HC]","route_style_name":"HC"},{"style_car_id":9,"style_name":"SPRINTER TRUENO 2door GT-APEX (AE86) [DH]","route_style_name":"DH"},{"style_car_id":131073,"style_name":"COROLLA LEVIN GT-APEX (AE86) [HC]","route_style_name":"HC"},{"style_car_id":65537,"style_name":"COROLLA LEVIN GT-APEX (AE86) [AR]","route_style_name":"AR"},{"style_car_id":131074,"style_name":"COROLLA LEVIN SR (AE85) [HC]","route_style_name":"HC"},{"style_car_id":65538,"style_name":"COROLLA LEVIN SR (AE85) [AR]","route_style_name":"AR"},{"style_car_id":7,"style_name":"86 GT (ZN6) [DH]","route_style_name":"DH"},{"style_car_id":131079,"style_name":"86 GT (ZN6) [HC]","route_style_name":"HC"},{"style_car_id":65543,"style_name":"86 GT (ZN6) [AR]","route_style_name":"AR"},{"style_car_id":3,"style_name":"MR2 G-Limited (SW20) [DH]","route_style_name":"DH"},{"style_car_id":65539,"style_name":"MR2 G-Limited (SW20) [AR]","route_style_name":"AR"},{"style_car_id":5,"style_name":"MR-S S EDITION (ZZW30) [DH]","route_style_name":"DH"},{"style_car_id":4,"style_name":"ALTEZZA RS200 Z EDITION (SXE10) [DH]","route_style_name":"DH"},{"style_car_id":131082,"style_name":"CELICA GT-FOUR (ST205) [HC]","route_style_name":"HC"},{"style_car_id":65546,"style_name":"CELICA GT-FOUR (ST205) [AR]","route_style_name":"AR"},{"style_car_id":131078,"style_name":"SUPRA RZ (JZA80) [HC]","route_style_name":"HC"},{"style_car_id":65542,"style_name":"SUPRA RZ (JZA80) [AR]","route_style_name":"AR"},{"style_car_id":65547,"style_name":"GR YARIS 1st Edition RZ “High performance” (GXPA16) [AR]","route_style_name":"AR"},{"style_car_id":12,"style_name":"GR SUPRA RZ (DB42) [DH]","route_style_name":"DH"},{"style_car_id":65548,"style_name":"GR SUPRA RZ (DB42) [AR]","route_style_name":"AR"},{"style_car_id":131328,"style_name":"SKYLINE GT-R V・specⅡ (BNR32) [HC]","route_style_name":"HC"},{"style_car_id":65792,"style_name":"SKYLINE GT-R V・specⅡ (BNR32) [AR]","route_style_name":"AR"},{"style_car_id":131329,"style_name":"SKYLINE GT-R V・specⅡ Nür (BNR34) [HC]","route_style_name":"HC"},{"style_car_id":65793,"style_name":"SKYLINE GT-R V・specⅡ Nür (BNR34) [AR]","route_style_name":"AR"},{"style_car_id":131330,"style_name":"SILVIA K's (S13) [HC]","route_style_name":"HC"},{"style_car_id":65794,"style_name":"SILVIA K's (S13) [AR]","route_style_name":"AR"},{"style_car_id":259,"style_name":"Silvia Q's (S14) [DH]","route_style_name":"DH"},{"style_car_id":131331,"style_name":"Silvia Q's (S14) [HC]","route_style_name":"HC"},{"style_car_id":131332,"style_name":"Silvia spec-R (S15) [HC]","route_style_name":"HC"},{"style_car_id":65796,"style_name":"Silvia spec-R (S15) [AR]","route_style_name":"AR"},{"style_car_id":261,"style_name":"180SX TYPE Ⅱ (RPS13) [DH]","route_style_name":"DH"},{"style_car_id":65797,"style_name":"180SX TYPE Ⅱ (RPS13) [AR]","route_style_name":"AR"},{"style_car_id":131334,"style_name":"FAIRLADY Z Version S (Z33) [HC]","route_style_name":"HC"},{"style_car_id":131335,"style_name":"NISSAN GT-R NISMO (R35) [HC]","route_style_name":"HC"},{"style_car_id":264,"style_name":"SKYLINE 25GT TURBO (ER34) [DH]","route_style_name":"DH"},{"style_car_id":131336,"style_name":"SKYLINE 25GT TURBO (ER34) [HC]","route_style_name":"HC"},{"style_car_id":265,"style_name":"Fairlady Z (S30) [DH]","route_style_name":"DH"},{"style_car_id":131337,"style_name":"Fairlady Z (S30) [HC]","route_style_name":"HC"},{"style_car_id":512,"style_name":"Civic SiR・Ⅱ (EG6) [DH]","route_style_name":"DH"},{"style_car_id":131584,"style_name":"Civic SiR・Ⅱ (EG6) [HC]","route_style_name":"HC"},{"style_car_id":513,"style_name":"CIVIC TYPE R (EK9) [DH]","route_style_name":"DH"},{"style_car_id":66049,"style_name":"CIVIC TYPE R (EK9) [AR]","route_style_name":"AR"},{"style_car_id":514,"style_name":"INTEGRA TYPE R (DC2) [DH]","route_style_name":"DH"},{"style_car_id":131586,"style_name":"INTEGRA TYPE R (DC2) [HC]","route_style_name":"HC"},{"style_car_id":515,"style_name":"S2000 (AP1) [DH]","route_style_name":"DH"},{"style_car_id":66051,"style_name":"S2000 (AP1) [AR]","route_style_name":"AR"},{"style_car_id":131588,"style_name":"NSX (NA1) [HC]","route_style_name":"HC"},{"style_car_id":768,"style_name":"SAVANNA RX-7 ∞Ⅲ (FC3S) [DH]","route_style_name":"DH"},{"style_car_id":66304,"style_name":"SAVANNA RX-7 ∞Ⅲ (FC3S) [AR]","route_style_name":"AR"},{"style_car_id":131841,"style_name":"ε~fini RX-7 Type R (FD3S) [HC]","route_style_name":"HC"},{"style_car_id":66305,"style_name":"ε~fini RX-7 Type R (FD3S) [AR]","route_style_name":"AR"},{"style_car_id":770,"style_name":"RX-8 Type S (SE3P) [DH]","route_style_name":"DH"},{"style_car_id":771,"style_name":"EUNOS ROADSTER (NA6CE) [DH]","route_style_name":"DH"},{"style_car_id":772,"style_name":"ROADSTER RS (NB8C) [DH]","route_style_name":"DH"},{"style_car_id":131845,"style_name":"RX-7 Type RS (FD3S) [HC]","route_style_name":"HC"},{"style_car_id":66309,"style_name":"RX-7 Type RS (FD3S) [AR]","route_style_name":"AR"},{"style_car_id":132096,"style_name":"IMPREZA WRX type R STi Version Ⅴ (GC8) [HC]","route_style_name":"HC"},{"style_car_id":66560,"style_name":"IMPREZA WRX type R STi Version Ⅴ (GC8) [AR]","route_style_name":"AR"},{"style_car_id":1027,"style_name":"SUBARU BRZ S (ZC6) [DH]","route_style_name":"DH"},{"style_car_id":66563,"style_name":"SUBARU BRZ S (ZC6) [AR]","route_style_name":"AR"},{"style_car_id":132100,"style_name":"STI S207 NBR CHALLENGE PACKAGE (VAB) [HC]","route_style_name":"HC"},{"style_car_id":1280,"style_name":"LANCER GSR Evolution Ⅲ (CE9A) [DH]","route_style_name":"DH"},{"style_car_id":66816,"style_name":"LANCER GSR Evolution Ⅲ (CE9A) [AR]","route_style_name":"AR"},{"style_car_id":132353,"style_name":"LANCER RS EVOLUTION Ⅳ (CN9A) [HC]","route_style_name":"HC"},{"style_car_id":66817,"style_name":"LANCER RS EVOLUTION Ⅳ (CN9A) [AR]","route_style_name":"AR"},{"style_car_id":132355,"style_name":"LANCER EVOLUTION Ⅶ GSR (CT9A) [HC]","route_style_name":"HC"},{"style_car_id":66821,"style_name":"LANCER RS EVOLUTION Ⅴ (CP9A) [AR]","route_style_name":"AR"},{"style_car_id":66822,"style_name":"LANCER GSR EVOLUTION Ⅵ T.M.EDITION (CP9A) [AR]","route_style_name":"AR"},{"style_car_id":1536,"style_name":"Cappuccino (EA11R) [DH]","route_style_name":"DH"},{"style_car_id":67073,"style_name":"SWIFT Sport (ZC33S) [AR]","route_style_name":"AR"},{"style_car_id":132864,"style_name":"SILEIGHTY [HC]","route_style_name":"HC"},{"style_car_id":67328,"style_name":"SILEIGHTY [AR]","route_style_name":"AR"},{"style_car_id":133376,"style_name":"911Turbo3.6 (964) [HC]","route_style_name":"HC"},{"style_car_id":67840,"style_name":"911Turbo3.6 (964) [AR]","route_style_name":"AR"},{"style_car_id":2305,"style_name":"718Cayman GTS (982) [DH]","route_style_name":"DH"}],"course":[{"course_id":0,"course_name":"Lake Akina/CCW"},{"course_id":2,"course_name":"Lake Akina/CW"},{"course_id":52,"course_name":"Hakone/DH"},{"course_id":54,"course_name":"Hakone/HC"},{"course_id":36,"course_name":"Usui/CCW"},{"course_id":38,"course_name":"Usui/CW"},{"course_id":4,"course_name":"Myogi/DH"},{"course_id":6,"course_name":"Myogi/HC"},{"course_id":8,"course_name":"Akagi/DH"},{"course_id":10,"course_name":"Akagi/HC"},{"course_id":12,"course_name":"Akina/DH"},{"course_id":14,"course_name":"Akina/HC"},{"course_id":16,"course_name":"Irohazaka/DH"},{"course_id":18,"course_name":"Irohazaka/HC"},{"course_id":20,"course_name":"Tsukuba/OB"},{"course_id":22,"course_name":"Tsukuba/IB"},{"course_id":56,"course_name":"Momiji Line/DH"},{"course_id":58,"course_name":"Momiji Line/HC"},{"course_id":24,"course_name":"Happogahara/OB"},{"course_id":26,"course_name":"Happogahara/IB"},{"course_id":40,"course_name":"Sadamine/DH"},{"course_id":42,"course_name":"Sadamine/HC"},{"course_id":44,"course_name":"Tsuchisaka/OB"},{"course_id":46,"course_name":"Tsuchisaka/IB"},{"course_id":48,"course_name":"Akina Snow/DH"},{"course_id":50,"course_name":"Akina Snow/HC"},{"course_id":68,"course_name":"Odawara/F"},{"course_id":70,"course_name":"Odawara/R"}]} \ No newline at end of file diff --git a/titles/idac/frontend/idac_index.jinja b/titles/idac/frontend/idac_index.jinja index eeecc65..0d71533 100644 --- a/titles/idac/frontend/idac_index.jinja +++ b/titles/idac/frontend/idac_index.jinja @@ -2,130 +2,20 @@ {% block content %}
{{ profile.username }}
-{{ profile.cash }} D
-{{ profile.total_play }}
-{{ profile.last_play_date }}
-{{ profile.mileage / 1000}} km
-{{ tickets.avatar_points }}/30
-{{ tickets.car_dressup_points }}/30
-{{ tickets.full_tune_tickets }}/99
-{{ tickets.full_tune_fragments }}/10
-# | Name | Car | Time | Store | Date |
---|---|---|---|---|---|
' + ranking.rank + ' | '; + tableHtml += '' + ranking.name + ' | '; + tableHtml += '' + getCarName(ranking.style_car_id) + ' | '; + tableHtml += '' + formatGoalTime(ranking.record) + ' | '; + // Ignore the Store and Date columns on small screens + tableHtml += '' + ranking.store + ' | '; + tableHtml += '' + ranking.update_date + ' | '; + tableHtml += '