2023-10-01 01:54:23 +00:00
|
|
|
import json
|
|
|
|
import yaml
|
|
|
|
import jinja2
|
2023-11-21 21:51:10 +00:00
|
|
|
|
2023-10-01 01:54:23 +00:00
|
|
|
from os import path
|
2024-03-12 16:48:12 +00:00
|
|
|
from typing import List, Any, Type
|
|
|
|
from starlette.routing import Route, Mount
|
|
|
|
from starlette.responses import Response, RedirectResponse, JSONResponse
|
2024-01-09 08:07:04 +00:00
|
|
|
from starlette.requests import Request
|
2023-10-01 01:54:23 +00:00
|
|
|
|
2024-01-09 20:54:34 +00:00
|
|
|
from core.frontend import FE_Base, UserSession
|
2023-10-01 01:54:23 +00:00
|
|
|
from core.config import CoreConfig
|
|
|
|
from titles.idac.database import IDACData
|
|
|
|
from titles.idac.schema.profile import *
|
|
|
|
from titles.idac.schema.item import *
|
|
|
|
from titles.idac.config import IDACConfig
|
|
|
|
from titles.idac.const import IDACConstants
|
|
|
|
|
|
|
|
|
2023-11-21 21:51:10 +00:00
|
|
|
class RankingData:
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
rank: int,
|
|
|
|
name: str,
|
|
|
|
record: int,
|
2023-11-21 22:54:02 +00:00
|
|
|
eval_id: int,
|
2023-11-21 21:51:10 +00:00
|
|
|
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
|
2023-11-21 22:54:02 +00:00
|
|
|
self.eval_id: int = eval_id
|
2023-11-21 21:51:10 +00:00
|
|
|
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
|
2024-03-12 16:48:12 +00:00
|
|
|
if param_name not in request_args:
|
2023-11-21 21:51:10 +00:00
|
|
|
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
|
|
|
|
|
2024-03-12 16:48:12 +00:00
|
|
|
param_value = request_args[param_name]
|
2023-11-21 21:51:10 +00:00
|
|
|
|
|
|
|
# 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
|
|
|
|
|
2023-10-01 01:54:23 +00:00
|
|
|
class IDACFrontend(FE_Base):
|
2023-11-21 21:51:10 +00:00
|
|
|
isLeaf = False
|
|
|
|
children: Dict[str, Any] = {}
|
|
|
|
|
2023-10-01 01:54:23 +00:00
|
|
|
def __init__(
|
|
|
|
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
|
|
|
|
) -> None:
|
|
|
|
super().__init__(cfg, environment)
|
|
|
|
self.data = IDACData(cfg)
|
2023-11-21 21:51:10 +00:00
|
|
|
self.core_cfg = cfg
|
2023-10-01 01:54:23 +00:00
|
|
|
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}"))
|
|
|
|
)
|
2024-03-12 16:48:12 +00:00
|
|
|
self.nav_name = "頭文字D THE ARCADE"
|
|
|
|
# self.nav_name = "IDAC"
|
2023-10-01 01:54:23 +00:00
|
|
|
# TODO: Add version list
|
|
|
|
self.version = IDACConstants.VER_IDAC_SEASON_2
|
|
|
|
|
2024-03-12 16:48:12 +00:00
|
|
|
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),
|
|
|
|
]),
|
|
|
|
]
|
2023-11-21 21:51:10 +00:00
|
|
|
|
2024-03-12 16:48:12 +00:00
|
|
|
async def render_GET(self, request: Request) -> bytes:
|
|
|
|
uri: str = request.url.path
|
2023-11-21 21:51:10 +00:00
|
|
|
|
|
|
|
template = self.environment.get_template(
|
2024-03-12 16:48:12 +00:00
|
|
|
"titles/idac/templates/idac_index.jinja"
|
2023-11-21 21:51:10 +00:00
|
|
|
)
|
2024-03-12 16:48:12 +00:00
|
|
|
usr_sesh = self.validate_session(request)
|
|
|
|
if not usr_sesh:
|
|
|
|
usr_sesh = UserSession()
|
2023-11-21 21:51:10 +00:00
|
|
|
|
|
|
|
# redirect to the ranking page
|
|
|
|
if uri.startswith("/game/idac"):
|
2024-03-12 16:48:12 +00:00
|
|
|
return RedirectResponse("/game/idac/ranking", 303)
|
2023-11-21 21:51:10 +00:00
|
|
|
|
2024-03-12 16:48:12 +00:00
|
|
|
return Response(template.render(
|
2023-11-21 21:51:10 +00:00
|
|
|
title=f"{self.core_config.server.name} | {self.nav_name}",
|
|
|
|
game_list=self.environment.globals["game_list"],
|
|
|
|
sesh=vars(usr_sesh),
|
|
|
|
active_page="idac",
|
2024-03-12 16:48:12 +00:00
|
|
|
), media_type="text/html; charset=utf-8")
|
2023-11-21 21:51:10 +00:00
|
|
|
|
2024-03-12 16:48:12 +00:00
|
|
|
async def render_POST(self, request: Request) -> bytes:
|
2023-11-21 21:51:10 +00:00
|
|
|
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
|
|
|
|
|
2024-03-12 16:48:12 +00:00
|
|
|
async def render_GET(self, request: Request) -> bytes:
|
|
|
|
uri: str = request.url.path
|
2023-11-21 21:51:10 +00:00
|
|
|
|
|
|
|
template = self.environment.get_template(
|
2024-03-12 16:48:12 +00:00
|
|
|
"titles/idac/templates/ranking/index.jinja"
|
2023-11-21 21:51:10 +00:00
|
|
|
)
|
2024-03-12 16:48:12 +00:00
|
|
|
usr_sesh = self.validate_session(request)
|
|
|
|
if not usr_sesh:
|
|
|
|
usr_sesh = UserSession()
|
|
|
|
user_id = usr_sesh.user_id
|
2023-11-21 21:51:10 +00:00
|
|
|
|
|
|
|
# IDAC constants
|
|
|
|
if uri.startswith("/game/idac/ranking/const.get"):
|
|
|
|
# get the constants
|
2024-03-12 16:48:12 +00:00
|
|
|
with open("titles/idac/templates/const.json", "r", encoding="utf-8") as f:
|
2023-11-21 21:51:10 +00:00
|
|
|
constants = json.load(f)
|
|
|
|
|
2024-03-12 16:48:12 +00:00
|
|
|
return JSONResponse(constants)
|
2023-11-21 21:51:10 +00:00
|
|
|
|
|
|
|
# leaderboard ranking
|
|
|
|
elif uri.startswith("/game/idac/ranking/ranking.get"):
|
2024-03-12 16:48:12 +00:00
|
|
|
req = RankingRequest(request.query_params._dict)
|
2023-11-21 21:51:10 +00:00
|
|
|
resp = RankingResponse()
|
|
|
|
|
|
|
|
if not req.success:
|
|
|
|
resp.error = req.error
|
2024-03-12 16:48:12 +00:00
|
|
|
return JSONResponse(resp.make())
|
2023-11-21 21:51:10 +00:00
|
|
|
|
|
|
|
# get the total number of records
|
2024-03-12 16:48:12 +00:00
|
|
|
total_records = await self.data.item.get_time_trial_ranking_by_course_total(
|
2023-11-21 21:51:10 +00:00
|
|
|
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."
|
2024-03-12 16:48:12 +00:00
|
|
|
return JSONResponse(resp.make())
|
2023-11-21 21:51:10 +00:00
|
|
|
|
|
|
|
# get the total number of records
|
|
|
|
total = total_records["count"]
|
|
|
|
|
|
|
|
limit = 50
|
|
|
|
offset = (req.page_number - 1) * limit
|
|
|
|
|
2024-03-12 16:48:12 +00:00
|
|
|
ranking = await self.data.item.get_time_trial_ranking_by_course(
|
2023-11-21 21:51:10 +00:00
|
|
|
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
|
2024-03-12 16:48:12 +00:00
|
|
|
profile = await self.data.profile.get_profile(user_id, self.version)
|
|
|
|
arcade = await self.data.arcade.get_arcade(profile["store"])
|
2023-11-21 21:51:10 +00:00
|
|
|
|
|
|
|
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"],
|
2023-11-21 22:54:02 +00:00
|
|
|
eval_id=rank["eval_id"],
|
2023-11-21 21:51:10 +00:00
|
|
|
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
|
2024-03-12 16:48:12 +00:00
|
|
|
return JSONResponse(resp.make())
|
2023-11-21 21:51:10 +00:00
|
|
|
|
2024-03-12 16:48:12 +00:00
|
|
|
return Response(template.render(
|
2023-11-21 21:51:10 +00:00
|
|
|
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",
|
2024-03-12 16:48:12 +00:00
|
|
|
), media_type="text/html; charset=utf-8")
|
2023-11-21 21:51:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2023-10-01 01:54:23 +00:00
|
|
|
self.ticket_names = {
|
|
|
|
3: "car_dressup_points",
|
|
|
|
5: "avatar_points",
|
|
|
|
25: "full_tune_tickets",
|
|
|
|
34: "full_tune_fragments",
|
|
|
|
}
|
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
async def generate_all_tables_json(self, user_id: int):
|
2023-10-01 01:54:23 +00:00
|
|
|
json_export = {}
|
|
|
|
|
|
|
|
idac_tables = {
|
|
|
|
profile,
|
|
|
|
config,
|
|
|
|
avatar,
|
|
|
|
rank,
|
|
|
|
stock,
|
|
|
|
theory,
|
|
|
|
car,
|
|
|
|
ticket,
|
|
|
|
story,
|
|
|
|
episode,
|
|
|
|
difficulty,
|
|
|
|
course,
|
|
|
|
trial,
|
|
|
|
challenge,
|
|
|
|
theory_course,
|
|
|
|
theory_partner,
|
|
|
|
theory_running,
|
|
|
|
vs_info,
|
|
|
|
stamp,
|
2023-11-21 21:51:10 +00:00
|
|
|
timetrial_event,
|
2023-10-01 01:54:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for table in idac_tables:
|
|
|
|
sql = select(table).where(
|
|
|
|
table.c.user == user_id,
|
|
|
|
)
|
|
|
|
|
|
|
|
# check if the table has a version column
|
|
|
|
if "version" in table.c:
|
|
|
|
sql = sql.where(table.c.version == self.version)
|
|
|
|
|
|
|
|
# lol use the profile connection for items, dirty hack
|
2024-01-09 19:42:17 +00:00
|
|
|
result = await self.data.profile.execute(sql)
|
2023-10-01 01:54:23 +00:00
|
|
|
data_list = result.fetchall()
|
|
|
|
|
|
|
|
# add the list to the json export with the correct table name
|
|
|
|
json_export[table.name] = []
|
|
|
|
for data in data_list:
|
|
|
|
tmp = data._asdict()
|
|
|
|
tmp.pop("id")
|
|
|
|
tmp.pop("user")
|
|
|
|
json_export[table.name].append(tmp)
|
|
|
|
|
|
|
|
return json.dumps(json_export, indent=4, default=str, ensure_ascii=False)
|
|
|
|
|
2024-01-09 19:42:17 +00:00
|
|
|
async def render_GET(self, request: Request) -> bytes:
|
2024-01-09 20:54:34 +00:00
|
|
|
uri: str = request.url.path
|
2023-10-01 01:54:23 +00:00
|
|
|
|
|
|
|
template = self.environment.get_template(
|
2024-03-12 16:48:12 +00:00
|
|
|
"titles/idac/templates/profile/index.jinja"
|
2023-10-01 01:54:23 +00:00
|
|
|
)
|
2024-01-09 20:54:34 +00:00
|
|
|
usr_sesh = self.validate_session(request)
|
|
|
|
if not usr_sesh:
|
|
|
|
usr_sesh = UserSession()
|
|
|
|
user_id = usr_sesh.user_id
|
2024-03-12 16:48:12 +00:00
|
|
|
|
|
|
|
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)
|
2023-10-01 01:54:23 +00:00
|
|
|
|
|
|
|
# profile export
|
2023-11-21 21:51:10 +00:00
|
|
|
if uri.startswith("/game/idac/profile/export.get"):
|
2023-10-03 15:00:59 +00:00
|
|
|
if user_id == 0:
|
2024-03-12 16:48:12 +00:00
|
|
|
return RedirectResponse("/game/idac", 303)
|
2023-10-01 01:54:23 +00:00
|
|
|
|
|
|
|
# set the file name, content type and size to download the json
|
2024-03-12 16:48:12 +00:00
|
|
|
content = await self.generate_all_tables_json(user_id)
|
2023-10-01 01:54:23 +00:00
|
|
|
|
2023-10-03 15:00:59 +00:00
|
|
|
self.logger.info(f"User {user_id} exported their IDAC data")
|
2024-01-09 20:54:34 +00:00
|
|
|
return Response(
|
2024-03-12 16:48:12 +00:00
|
|
|
content.encode("utf-8"),
|
2024-01-09 20:54:34 +00:00
|
|
|
200,
|
|
|
|
{'content-disposition': 'attachment; filename=idac_profile.json'},
|
|
|
|
"application/octet-stream"
|
|
|
|
)
|
2023-10-01 01:54:23 +00:00
|
|
|
|
|
|
|
profile_data, tickets, rank = None, None, None
|
2023-10-03 15:00:59 +00:00
|
|
|
if user_id > 0:
|
2024-01-09 19:42:17 +00:00
|
|
|
profile_data = await self.data.profile.get_profile(user_id, self.version)
|
|
|
|
ticket_data = await self.data.item.get_tickets(user_id)
|
|
|
|
rank = await self.data.profile.get_profile_rank(user_id, self.version)
|
2023-10-01 01:54:23 +00:00
|
|
|
|
2024-01-10 02:03:58 +00:00
|
|
|
if ticket_data:
|
|
|
|
tickets = {
|
|
|
|
self.ticket_names[ticket["ticket_id"]]: ticket["ticket_cnt"]
|
|
|
|
for ticket in ticket_data
|
|
|
|
}
|
2023-10-01 01:54:23 +00:00
|
|
|
|
2024-01-09 20:54:34 +00:00
|
|
|
return Response(template.render(
|
2023-10-01 01:54:23 +00:00
|
|
|
title=f"{self.core_config.server.name} | {self.nav_name}",
|
|
|
|
game_list=self.environment.globals["game_list"],
|
|
|
|
profile=profile_data,
|
|
|
|
tickets=tickets,
|
|
|
|
rank=rank,
|
|
|
|
sesh=vars(usr_sesh),
|
2024-03-12 16:48:12 +00:00
|
|
|
username=user["username"],
|
2023-10-01 01:54:23 +00:00
|
|
|
active_page="idac",
|
2024-03-12 16:48:12 +00:00
|
|
|
active_tab="profile",
|
2024-01-13 22:15:02 +00:00
|
|
|
), media_type="text/html; charset=utf-8")
|