idac: added "simple" ranking to frontend
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user