develop #6

Merged
Kayori merged 4 commits from Hay1tsme/artemis:develop into develop 2024-06-22 14:12:09 +00:00
19 changed files with 923 additions and 41 deletions

View File

@ -5,6 +5,10 @@ Documenting updates to ARTEMiS, to be updated every time the master branch is pu
### CHUNITHM
+ CHUNITHM LUMINOUS support
## 20240616
### DIVA
+ Working frontend with name and level strings edit and playlog
## 20240530
### DIVA
+ Fix reader for when dificulty is not a int

View File

@ -10,13 +10,14 @@ class ADBFelicaLookupRequest(ADBBaseRequest):
self.pmm = hex(pmm)[2:].upper()
class ADBFelicaLookupResponse(ADBBaseResponse):
def __init__(self, access_code: str = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x03, length: int = 0x30, status: int = 1) -> None:
def __init__(self, access_code: str = None, idx: int = 0, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x03, length: int = 0x30, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.access_code = access_code if access_code is not None else "00000000000000000000"
self.idx = idx
@classmethod
def from_req(cls, req: ADBHeader, access_code: str = None) -> "ADBFelicaLookupResponse":
c = cls(access_code, req.game_id, req.store_id, req.keychip_id)
def from_req(cls, req: ADBHeader, access_code: str = None, idx: int = 0) -> "ADBFelicaLookupResponse":
c = cls(access_code, idx, req.game_id, req.store_id, req.keychip_id)
c.head.protocol_ver = req.protocol_ver
return c
@ -26,7 +27,7 @@ class ADBFelicaLookupResponse(ADBBaseResponse):
"access_code" / Int8ub[10],
Padding(2)
).build(dict(
felica_idx = 0,
felica_idx = self.idx,
access_code = bytes.fromhex(self.access_code)
))

View File

@ -194,6 +194,9 @@ class AimedbServlette():
if user_id and user_id > 0:
await self.data.card.update_card_last_login(req.access_code)
if req.access_code.startswith("010") or req.access_code.startswith("3"):
await self.data.card.set_chip_id_by_access_code(req.access_code, req.serial_number)
self.logger.info(f"Attempt to set chip id to {req.serial_number} for access code {req.access_code}")
return ret
async def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse:
@ -229,15 +232,24 @@ class AimedbServlette():
async def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes:
"""
On official, I think a card has to be registered for this to actually work, but
I'm making the executive decision to not implement that and just kick back our
faux generated access code. The real felica IDm -> access code conversion is done
on the ADB server, which we do not and will not ever have access to. Because we can
assure that all IDms will be unique, this basic 0-padded hex -> int conversion will
be fine.
On official, the IDm is used as a key to look up the stored access code in a large
database. We do not have access to that database so we have to make due with what we got.
Interestingly, namco games are able to read S_PAD0 and send the server the correct access
code, but aimedb doesn't. Until somebody either enters the correct code manually, or scans
on a game that reads it correctly from the card, this will have to do. It's the same conversion
used on the big boy networks.
"""
req = ADBFelicaLookupRequest(data)
ac = self.data.card.to_access_code(req.idm)
card = await self.data.card.get_card_by_idm(req.idm)
if not card:
ac = self.data.card.to_access_code(req.idm)
test = await self.data.card.get_card_by_access_code(ac)
if test:
await self.data.card.set_idm_by_access_code(ac, req.idm)
else:
ac = card['access_code']
self.logger.info(
f"idm {req.idm} ipm {req.pmm} -> access_code {ac}"
)
@ -245,7 +257,8 @@ class AimedbServlette():
async def handle_felica_register(self, data: bytes, resp_code: int) -> bytes:
"""
I've never seen this used.
Used to register felica moble access codes. Will never be used on our network
because we don't implement felica_lookup properly.
"""
req = ADBFelicaLookupRequest(data)
ac = self.data.card.to_access_code(req.idm)
@ -279,8 +292,18 @@ class AimedbServlette():
async def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes:
req = ADBFelicaLookup2Request(data)
access_code = self.data.card.to_access_code(req.idm)
user_id = await self.data.card.get_user_id_from_card(access_code=access_code)
user_id = None
card = await self.data.card.get_card_by_idm(req.idm)
if not card:
access_code = self.data.card.to_access_code(req.idm)
card = await self.data.card.get_card_by_access_code(access_code)
if card:
user_id = card['user']
await self.data.card.set_idm_by_access_code(access_code, req.idm)
else:
user_id = card['user']
access_code = card['access_code']
if user_id is None:
user_id = -1
@ -290,6 +313,14 @@ class AimedbServlette():
)
resp = ADBFelicaLookup2Response.from_req(req.head, user_id, access_code)
if user_id > 0:
if card['is_banned'] and card['is_locked']:
resp.head.status = ADBStatus.BAN_SYS_USER
elif card['is_banned']:
resp.head.status = ADBStatus.BAN_SYS
elif card['is_locked']:
resp.head.status = ADBStatus.LOCK_USER
if user_id and user_id > 0 and self.config.aimedb.id_secret:
auth_key = create_sega_auth_key(user_id, req.head.game_id, req.head.store_id, req.head.keychip_id, self.config.aimedb.id_secret, self.config.aimedb.id_lifetime_seconds)
@ -337,6 +368,16 @@ class AimedbServlette():
self.logger.info(
f"Registration blocked!: access code {req.access_code}"
)
if user_id > 0:
if req.access_code.startswith("010") or req.access_code.startswith("3"):
await self.data.card.set_chip_id_by_access_code(req.access_code, req.serial_number)
self.logger.info(f"Attempt to set chip id to {req.serial_number} for access code {req.access_code}")
elif req.access_code.startswith("0008"):
idm = self.data.card.to_idm(req.access_code)
await self.data.card.set_idm_by_access_code(req.access_code, idm)
self.logger.info(f"Attempt to set IDm to {idm} for access code {req.access_code}")
resp = ADBLookupResponse.from_req(req.head, user_id)
if resp.user_id <= 0:

View File

@ -0,0 +1,27 @@
"""chuni_add_net_battle_uk
Revision ID: 1e150d16ab6b
Revises: b23f985100ba
Create Date: 2024-06-21 22:57:18.418488
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '1e150d16ab6b'
down_revision = 'b23f985100ba'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint(None, 'chuni_profile_net_battle', ['user'])
# ### end Alembic commands ###
def downgrade():
op.drop_constraint(None, 'chuni_profile_net_battle', type_='unique')
# ### end Alembic commands ###

View File

@ -0,0 +1,50 @@
"""card_add_idm_chip_id
Revision ID: 48f4acc43a7e
Revises: 1e150d16ab6b
Create Date: 2024-06-21 23:53:34.369134
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '48f4acc43a7e'
down_revision = '1e150d16ab6b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('aime_card', sa.Column('idm', sa.String(length=16), nullable=True))
op.add_column('aime_card', sa.Column('chip_id', sa.BIGINT(), nullable=True))
op.alter_column('aime_card', 'access_code',
existing_type=mysql.VARCHAR(length=20),
nullable=False)
op.alter_column('aime_card', 'created_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('now()'),
existing_nullable=True)
op.create_unique_constraint(None, 'aime_card', ['chip_id'])
op.create_unique_constraint(None, 'aime_card', ['idm'])
op.create_unique_constraint(None, 'aime_card', ['access_code'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'aime_card', type_='unique')
op.drop_constraint(None, 'aime_card', type_='unique')
op.drop_constraint(None, 'aime_card', type_='unique')
op.alter_column('aime_card', 'created_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('CURRENT_TIMESTAMP'),
existing_nullable=True)
op.alter_column('aime_card', 'access_code',
existing_type=mysql.VARCHAR(length=20),
nullable=True)
op.drop_column('aime_card', 'chip_id')
op.drop_column('aime_card', 'idm')
# ### end Alembic commands ###

View File

@ -1,6 +1,6 @@
from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint
from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP
from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP, BIGINT
from sqlalchemy.sql.schema import ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.engine import Row
@ -11,12 +11,10 @@ aime_card = Table(
"aime_card",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("access_code", String(20)),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("access_code", String(20), nullable=False, unique=True),
Column("idm", String(16), unique=True),
Column("chip_id", BIGINT, unique=True),
Column("created_date", TIMESTAMP, server_default=func.now()),
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
Column("is_locked", Boolean, server_default="0"),
@ -122,6 +120,26 @@ class CardData(BaseData):
if result is None:
self.logger.warn(f"Failed to update last login time for {access_code}")
async def get_card_by_idm(self, idm: str) -> Optional[Row]:
result = await self.execute(aime_card.select(aime_card.c.idm == idm))
if result:
return result.fetchone()
async def get_card_by_chip_id(self, chip_id: int) -> Optional[Row]:
result = await self.execute(aime_card.select(aime_card.c.chip_id == chip_id))
if result:
return result.fetchone()
async def set_chip_id_by_access_code(self, access_code: str, chip_id: int) -> Optional[Row]:
result = await self.execute(aime_card.update(aime_card.c.access_code == access_code).values(chip_id=chip_id))
if not result:
self.logger.error(f"Failed to update chip ID to {chip_id} for {access_code}")
async def set_idm_by_access_code(self, access_code: str, idm: str) -> Optional[Row]:
result = await self.execute(aime_card.update(aime_card.c.access_code == access_code).values(idm=idm))
if not result:
self.logger.error(f"Failed to update IDm to {idm} for {access_code}")
def to_access_code(self, luid: str) -> str:
"""
Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string
@ -132,4 +150,4 @@ class CardData(BaseData):
"""
Given a 20 digit access code as a string, return the 16 hex character luid
"""
return f"{int(access_code):0{16}x}"
return f"{int(access_code):0{16}X}"

View File

@ -48,9 +48,8 @@ class ChuniReader(BaseReader):
for root, dirs, files in walk(f"{root_dir}loginBonusPreset"):
for dir in dirs:
if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"):
with open(f"{root}/{dir}/LoginBonusPreset.xml", "rb") as fp:
bytedata = fp.read()
strdata = bytedata.decode("UTF-8")
with open(f"{root}/{dir}/LoginBonusPreset.xml", "r", encoding="utf-8") as fp:
strdata = fp.read()
xml_root = ET.fromstring(strdata)
for name in xml_root.findall("name"):
@ -121,9 +120,8 @@ class ChuniReader(BaseReader):
for root, dirs, files in walk(evt_dir):
for dir in dirs:
if path.exists(f"{root}/{dir}/Event.xml"):
with open(f"{root}/{dir}/Event.xml", "rb") as fp:
bytedata = fp.read()
strdata = bytedata.decode("UTF-8")
with open(f"{root}/{dir}/Event.xml", "r", encoding="utf-8") as fp:
strdata = fp.read()
xml_root = ET.fromstring(strdata)
for name in xml_root.findall("name"):
@ -144,9 +142,8 @@ class ChuniReader(BaseReader):
for root, dirs, files in walk(music_dir):
for dir in dirs:
if path.exists(f"{root}/{dir}/Music.xml"):
with open(f"{root}/{dir}/Music.xml", "rb") as fp:
bytedata = fp.read()
strdata = bytedata.decode("UTF-8")
with open(f"{root}/{dir}/Music.xml", "r", encoding='utf-8') as fp:
strdata = fp.read()
xml_root = ET.fromstring(strdata)
for name in xml_root.findall("name"):
@ -210,9 +207,8 @@ class ChuniReader(BaseReader):
for root, dirs, files in walk(charge_dir):
for dir in dirs:
if path.exists(f"{root}/{dir}/ChargeItem.xml"):
with open(f"{root}/{dir}/ChargeItem.xml", "rb") as fp:
bytedata = fp.read()
strdata = bytedata.decode("UTF-8")
with open(f"{root}/{dir}/ChargeItem.xml", "r", encoding='utf-8') as fp:
strdata = fp.read()
xml_root = ET.fromstring(strdata)
for name in xml_root.findall("name"):
@ -240,9 +236,8 @@ class ChuniReader(BaseReader):
for root, dirs, files in walk(avatar_dir):
for dir in dirs:
if path.exists(f"{root}/{dir}/AvatarAccessory.xml"):
with open(f"{root}/{dir}/AvatarAccessory.xml", "rb") as fp:
bytedata = fp.read()
strdata = bytedata.decode("UTF-8")
with open(f"{root}/{dir}/AvatarAccessory.xml", "r", encoding='utf-8') as fp:
strdata = fp.read()
xml_root = ET.fromstring(strdata)
for name in xml_root.findall("name"):

View File

@ -262,7 +262,7 @@ cmission_progress = Table(
"chuni_item_cmission_progress",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("missionId", Integer, nullable=False),
Column("order", Integer),
Column("stage", Integer),

View File

@ -412,6 +412,18 @@ rating = Table(
mysql_charset="utf8mb4",
)
net_battle = Table(
"chuni_profile_net_battle",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True),
Column("isRankUpChallengeFailed", Boolean),
Column("highestBattleRankId", Integer),
Column("battleIconId", Integer),
Column("battleIconNum", Integer),
Column("avatarEffectPoint", Integer),
mysql_charset="utf8mb4",
)
class ChuniProfileData(BaseData):
async def update_name(self, user_id: int, new_name: str) -> bool:
@ -779,4 +791,32 @@ class ChuniProfileData(BaseData):
else:
versions_raw = result.fetchall()
versions = [row[0] for row in versions_raw]
return sorted(versions, reverse=True)
return sorted(versions, reverse=True)
async def put_net_battle(self, user_id: int, net_battle_data: Dict) -> Optional[int]:
sql = insert(net_battle).values(
user=user_id,
isRankUpChallengeFailed=net_battle_data['isRankUpChallengeFailed'],
highestBattleRankId=net_battle_data['highestBattleRankId'],
battleIconId=net_battle_data['battleIconId'],
battleIconNum=net_battle_data['battleIconNum'],
avatarEffectPoint=net_battle_data['avatarEffectPoint'],
)
conflict = sql.on_duplicate_key_update(
isRankUpChallengeFailed=net_battle_data['isRankUpChallengeFailed'],
highestBattleRankId=net_battle_data['highestBattleRankId'],
battleIconId=net_battle_data['battleIconId'],
battleIconNum=net_battle_data['battleIconNum'],
avatarEffectPoint=net_battle_data['avatarEffectPoint'],
)
result = await self.execute(conflict)
if result:
return result.inserted_primary_key['id']
self.logger.error(f"Failed to put net battle data for user {user_id}")
async def get_net_battle(self, user_id: int, net_battle_data: Dict) -> Optional[Row]:
result = await self.execute(net_battle.select(net_battle.c.user == user_id))
if result:
return result.fetchone()

View File

@ -2,8 +2,10 @@ from titles.diva.index import DivaServlet
from titles.diva.const import DivaConstants
from titles.diva.database import DivaData
from titles.diva.read import DivaReader
from .frontend import DivaFrontend
index = DivaServlet
database = DivaData
reader = DivaReader
frontend = DivaFrontend
game_codes = [DivaConstants.GAME_CODE]

182
titles/diva/frontend.py Normal file
View File

@ -0,0 +1,182 @@
from typing import List
from starlette.routing import Route, Mount
from starlette.requests import Request
from starlette.responses import Response, RedirectResponse
from os import path
import yaml
import jinja2
from core.frontend import FE_Base, UserSession
from core.config import CoreConfig
from .database import DivaData
from .config import DivaConfig
from .const import DivaConstants
class DivaFrontend(FE_Base):
def __init__(
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
) -> None:
super().__init__(cfg, environment)
self.data = DivaData(cfg)
self.game_cfg = DivaConfig()
if path.exists(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"):
self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"))
)
self.nav_name = "diva"
def get_routes(self) -> List[Route]:
return [
Route("/", self.render_GET, methods=['GET']),
Mount("/playlog", routes=[
Route("/", self.render_GET_playlog, methods=['GET']),
Route("/{index}", self.render_GET_playlog, methods=['GET']),
]),
Route("/update.name", self.update_name, methods=['POST']),
Route("/update.lv", self.update_lv, methods=['POST']),
]
async def render_GET(self, request: Request) -> bytes:
template = self.environment.get_template(
"titles/diva/templates/diva_index.jinja"
)
usr_sesh = self.validate_session(request)
if not usr_sesh:
usr_sesh = UserSession()
if usr_sesh.user_id > 0:
profile = await self.data.profile.get_profile(usr_sesh.user_id, 1)
resp = Response(template.render(
title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh),
user_id=usr_sesh.user_id,
profile=profile
), media_type="text/html; charset=utf-8")
return resp
else:
return RedirectResponse("/gate")
async def render_GET_playlog(self, request: Request) -> bytes:
template = self.environment.get_template(
"titles/diva/templates/diva_playlog.jinja"
)
usr_sesh = self.validate_session(request)
if not usr_sesh:
usr_sesh = UserSession()
if usr_sesh.user_id > 0:
path_index = request.path_params.get("index")
if not path_index or int(path_index) < 1:
index = 0
else:
index = int(path_index) - 1 # 0 and 1 are 1st page
user_id = usr_sesh.user_id
playlog_count = await self.data.score.get_user_playlogs_count(user_id)
if playlog_count < index * 20 :
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),
score_count=0
), media_type="text/html; charset=utf-8")
playlog = await self.data.score.get_playlogs(user_id, index, 20) #Maybe change to the playlog instead of direct scores
playlog_with_title = []
for record in playlog:
song = await self.data.static.get_music_chart(record[2], record[3], record[4])
if song:
title = song.title
vocaloid_arranger = song.vocaloid_arranger
else:
title = "Unknown"
vocaloid_arranger = "Unknown"
playlog_with_title.append({
"raw": record,
"title": title,
"vocaloid_arranger": vocaloid_arranger
})
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),
user_id=usr_sesh.user_id,
playlog=playlog_with_title,
playlog_count=playlog_count
), media_type="text/html; charset=utf-8")
else:
return RedirectResponse("/gate/", 300)
async def update_name(self, request: Request) -> Response:
usr_sesh = self.validate_session(request)
if not usr_sesh:
return RedirectResponse("/gate")
form_data = await request.form()
new_name: str = form_data.get("new_name")
new_name_full = ""
if not new_name:
return RedirectResponse("/gate/?e=4", 303)
if len(new_name) > 8:
return RedirectResponse("/gate/?e=8", 303)
for x in new_name: # FIXME: This will let some invalid characters through atm
o = ord(x)
try:
if o == 0x20:
new_name_full += chr(0x3000)
elif o < 0x7F and o > 0x20:
new_name_full += chr(o + 0xFEE0)
elif o <= 0x7F:
self.logger.warn(f"Invalid ascii character {o:02X}")
return RedirectResponse("/gate/?e=4", 303)
else:
new_name_full += x
except Exception as e:
self.logger.error(f"Something went wrong parsing character {o:04X} - {e}")
return RedirectResponse("/gate/?e=4", 303)
if not await self.data.profile.update_profile(usr_sesh.user_id, player_name=new_name_full):
return RedirectResponse("/gate/?e=999", 303)
return RedirectResponse("/game/diva", 303)
async def update_lv(self, request: Request) -> Response:
usr_sesh = self.validate_session(request)
if not usr_sesh:
return RedirectResponse("/gate")
form_data = await request.form()
new_lv: str = form_data.get("new_lv")
new_lv_full = ""
if not new_lv:
return RedirectResponse("/gate/?e=4", 303)
if len(new_lv) > 8:
return RedirectResponse("/gate/?e=8", 303)
for x in new_lv: # FIXME: This will let some invalid characters through atm
o = ord(x)
try:
if o == 0x20:
new_lv_full += chr(0x3000)
elif o < 0x7F and o > 0x20:
new_lv_full += chr(o + 0xFEE0)
elif o <= 0x7F:
self.logger.warn(f"Invalid ascii character {o:02X}")
return RedirectResponse("/gate/?e=4", 303)
else:
new_lv_full += x
except Exception as e:
self.logger.error(f"Something went wrong parsing character {o:04X} - {e}")
return RedirectResponse("/gate/?e=4", 303)
if not await self.data.profile.update_profile(usr_sesh.user_id, lv_str=new_lv_full):
return RedirectResponse("/gate/?e=999", 303)
return RedirectResponse("/game/diva", 303)

View File

@ -90,7 +90,7 @@ class DivaProfileData(BaseData):
return None
return result.lastrowid
async def update_profile(self, aime_id: int, **profile_args) -> None:
async def update_profile(self, aime_id: int, **profile_args) -> bool:
"""
Given an aime_id update the profile corresponding to the arguments
which are the diva_profile Columns
@ -102,7 +102,9 @@ class DivaProfileData(BaseData):
self.logger.error(
f"update_profile: failed to update profile! profile: {aime_id}"
)
return None
return False
return True
async def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]:
"""

View File

@ -239,3 +239,23 @@ class DivaScoreData(BaseData):
if result is None:
return None
return result.fetchall()
async def get_playlogs(self, aime_id: int, idx: int = 0, limit: int = 0) -> Optional[Row]:
sql = select(playlog).where(playlog.c.user == aime_id).order_by(playlog.c.date_scored.desc())
if limit:
sql = sql.limit(limit)
if idx:
sql = sql.offset(idx)
result = await self.execute(sql)
if result:
return result.fetchall()
async def get_user_playlogs_count(self, aime_id: int) -> Optional[int]:
sql = select(func.count()).where(playlog.c.user == aime_id)
result = await self.execute(sql)
if result is None:
self.logger.warning(f"aimu_id {aime_id} has no scores ")
return None
return result.scalar()

View File

@ -0,0 +1,195 @@
.diva-header {
text-align: center;
}
ul.diva-navi {
list-style-type: none;
padding: 0;
overflow: hidden;
background-color: #333;
text-align: center;
display: inline-block;
}
ul.diva-navi li {
display: inline-block;
}
ul.diva-navi li a {
display: block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
ul.diva-navi li a:hover:not(.active) {
background-color: #111;
}
ul.diva-navi li a.active {
background-color: #4CAF50;
}
ul.diva-navi li.right {
float: right;
}
@media screen and (max-width: 600px) {
ul.diva-navi li.right,
ul.diva-navi li {
float: none;
display: block;
text-align: center;
}
}
table {
border-collapse: collapse;
border-spacing: 0;
border-collapse: separate;
overflow: hidden;
background-color: #555555;
}
th, td {
text-align: left;
border: none;
}
th {
color: white;
}
.table-rowdistinct tr:nth-child(even) {
background-color: #303030;
}
.table-rowdistinct tr:nth-child(odd) {
background-color: #555555;
}
caption {
text-align: center;
color: white;
font-size: 18px;
font-weight: bold;
}
.table-large {
margin: 16px;
}
.table-large th,
.table-large td {
padding: 8px;
}
.table-small {
width: 100%;
margin: 4px;
}
.table-small th,
.table-small td {
padding: 2px;
}
.bg-card {
background-color: #555555;
}
.card-hover {
transition: all 0.2s ease-in-out;
}
.card-hover:hover {
transform: scale(1.02);
}
.basic {
color: #28a745;
font-weight: bold;
}
.hard {
color: #ffc107;
font-weight: bold;
}
.expert {
color: #dc3545;
font-weight: bold;
}
.master {
color: #dd09e8;
font-weight: bold;
}
.ultimate {
color: #000000;
font-weight: bold;
}
.score {
color: #ffffff;
font-weight: bold;
}
.rainbow {
background: linear-gradient(to right, red, yellow, lime, aqua, blue, fuchsia) 0 / 5em;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
.platinum {
color: #FFFF00;
font-weight: bold;
}
.gold {
color: #FFFF00;
font-weight: bold;
}
.scrolling-text {
overflow: hidden;
}
.scrolling-text p {
white-space: nowrap;
display: inline-block;
}
.scrolling-text h6 {
white-space: nowrap;
display: inline-block;
}
.scrolling-text h5 {
white-space: nowrap;
display: inline-block;
}
.scrolling {
animation: scroll 10s linear infinite;
}
@keyframes scroll {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}

View File

@ -0,0 +1,17 @@
<div class="diva-header">
<h1>diva</h1>
<ul class="diva-navi">
<li><a class="nav-link" href="/game/diva/">PROFILE</a></li>
<li><a class="nav-link" href="/game/diva/playlog/">RECORD</a></li>
</ul>
</div>
<script>
$(document).ready(function () {
var currentPath = window.location.pathname;
if (currentPath === '/game/diva/') {
$('.nav-link[href="/game/diva/"]').addClass('active');
} else if (currentPath.startsWith('/game/diva/playlog/')) {
$('.nav-link[href="/game/diva/playlog/"]').addClass('active');
}
});
</script>

View File

@ -0,0 +1,111 @@
{% extends "core/templates/index.jinja" %}
{% block content %}
<style>
{% include 'titles/diva/templates/css/diva_style.css' %}
</style>
<div class="container">
{% include 'titles/diva/templates/diva_header.jinja' %}
{% if profile is defined and profile is not none and profile|length > 0 %}
<div class="row">
<div class="col-lg-8 m-auto mt-3">
<div class="card bg-card rounded">
<table class="table-large table-rowdistinct">
<caption align="top" class="text-center">OVERVIEW</caption>
<tr>
<th>Player name:</th>
<th>{{ profile[3] }}</th>
<th>
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal"
data-bs-target="#name_change">Edit</button>
</th>
<th>Level string:</th>
<th>{{ profile[4] }}</th>
<th>
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal"
data-bs-target="#lv_change">Edit</button>
</th>
</tr>
<tr>
<td>Lvl:</td>
<td>{{ profile[5] }}</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Lvl points:</td>
<td>{{ profile[6] }}</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Vocaloid points:</td>
<td>{{ profile[7] }}</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</table>
</div>
</div>
</div>
{% if error is defined %}
{% include "core/templates/widgets/err_banner.jinja" %}
{% endif %}
{% elif sesh is defined and sesh is not none and sesh.user_id > 0 %}
No profile information found for this account.
{% else %}
Login to view profile information.
{% endif %}
</div>
<div class="modal fade" id="name_change" tabindex="-1" aria-labelledby="name_change_label" data-bs-theme="dark"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Name change</h5>
</div>
<div class="modal-body">
<form id="new_name_form" action="/game/diva/update.name" method="post" style="outline: 0;">
<label class="form-label" for="new_name">new name:</label>
<input class="form-control" aria-describedby="newNameHelp" form="new_name_form" id="new_name"
name="new_name" maxlength="14" type="text" required>
<div id="newNameHelp" class="form-text">name must be full-width character string.
</div>
</form>
</div>
<div class="modal-footer">
<input type=submit class="btn btn-primary" type="button" form="new_name_form">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="lv_change" tabindex="-1" aria-labelledby="lv_change_label" data-bs-theme="dark"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Level string change</h5>
</div>
<div class="modal-body">
<form id="new_lv_form" action="/game/diva/update.lv" method="post" style="outline: 0;">
<label class="form-label" for="new_lv">new level string:</label>
<input class="form-control" aria-describedby="newLvHelp" form="new_lv_form" id="new_lv" name="new_lv"
maxlength="14" type="text" required>
<div id="newLvHelp" class="form-text">level string must be full-width character string.
</div>
</form>
<div class="modal-footer">
<input type=submit class="btn btn-primary" type="button" form="new_lv_form">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,169 @@
{% extends "core/templates/index.jinja" %}
{% block content %}
<style>
{% include 'titles/diva/templates/css/diva_style.css' %}
</style>
<div class="container">
{% include 'titles/diva/templates/diva_header.jinja' %}
{% if playlog is defined and playlog is not none %}
<div class="row">
<h4 style="text-align: center;">Score counts: {{ playlog_count }}</h4>
{% set difficultyName = ['easy', 'normal', 'hard', 'extreme', 'extra extreme'] %}
{% set clearState = ['MISSxTAKE', 'STANDARD', 'GREAT', 'EXELLENT', 'PERFECT'] %}
{% for record in playlog %}
<div class="col-lg-6 mt-3">
<div class="card bg-card rounded card-hover">
<div class="card bg-card rounded card-hover">
<div class="card-header row">
<div class="col-8 scrolling-text">
<h5 class="card-text">{{ record.title }}</h5>
<br>
<h6 class="card-text">{{ record.vocaloid_arranger }}</h6>
</div>
<div class="col-4">
<h6 class="card-text">{{record.raw.date_scored}}</h6>
</div>
</div>
<div class="card-body row">
<div class="col-3" style="text-align: center;">
<h4 class="card-text">{{ record.raw.score }}</h4>
<h2>{{ record.raw.atn_pnt / 100 }}%</h2>
<h6>{{ difficultyName[record.raw.difficulty] }}</h6>
</div>
<div class="col-6" style="text-align: center;">
<table class="table-small table-rowdistinc">
<tr>
<td>COOL</td>
<td>{{ record.raw.cool }}</td>
</tr>
<tr>
<td>FINE</td>
<td>{{ record.raw.fine }}</td>
</tr>
<tr>
<td>SAFE</td>
<td>{{ record.raw.safe }}</td>
</tr>
<tr>
<td>SAD</td>
<td>{{ record.raw.sad }}</td>
</tr>
<tr>
<td>WORST</td>
<td>{{ record.raw.worst }}</td>
</tr>
</table>
</div>
<div class="col-3" style="text-align: center;">
<h6>{{ record.raw.max_combo }}</h6>
{% if record.raw.clr_kind == -1 %}
<h6>{{ clearState[0] }}</h6>
{% elif record.raw.clr_kind == 2 %}
<h6>{{ clearState[1] }}</h6>
{% elif record.raw.clr_kind == 3 %}
<h6>{{ clearState[2] }}</h6>
{% elif record.raw.clr_kind == 4 %}
<h6>{{ clearState[3] }}</h6>
{% elif record.raw.clr_kind == 5 %}
<h6>{{ clearState[4] }}</h6>
{% endif %}
{% if record.raw.clr_kind == -1 %}
<h6>NOT CLEAR</h6>
{% else %}
<h6>CLEAR</h6>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% set playlog_pages = playlog_count // 20 + 1 %}
{% elif sesh is defined and sesh is not none and sesh.user_id > 0 %}
No Score information found for this account.
{% else %}
Login to view profile information.
{% endif %}
</div>
<footer class="navbar-fixed-bottom">
<nav aria-label="Score page navication">
<ul class="pagination justify-content-center mt-3">
<li class="page-item"><a id="prev_page" class="page-link" href="#">Previous</a></li>
<li class="page-item"><a id="first_page" class="page-link" href="/game/diva/playlog/">1</a></li>
<li class="page-item"><a id="prev_3_page" class="page-link" href="">...</a></li>
<li class="page-item"><a id="front_page" class="page-link" href="">2</a></li>
<li class="page-item"><a id="cur_page" class="page-link active" href="">3</a></li>
<li class="page-item"><a id="back_page" class="page-link" href="">4</a></li>
<li class="page-item"><a id="next_3_page" class="page-link" href="">...</a></li>
<li class="page-item"><a id="last_page" class="page-link" href="/game/diva/playlog/{{ playlog_pages }}">{{
playlog_pages }}</a></li>
<li class="page-item"><a id="next_page" class="page-link" href="#">Next</a></li>
&nbsp
</ul>
</nav>
</footer>
<script>
$(document).ready(function () {
$('.scrolling-text p, .scrolling-text h1, .scrolling-text h2, .scrolling-text h3, .scrolling-text h4, .scrolling-text h5, .scrolling-text h6').each(function () {
var parentWidth = $(this).parent().width();
var elementWidth = $(this).outerWidth();
var elementWidthWithPadding = $(this).outerWidth(true);
if (elementWidthWithPadding > parentWidth) {
$(this).addClass('scrolling');
}
});
var currentUrl = window.location.pathname;
var currentPage = parseInt(currentUrl.split('/').pop());
var rootUrl = '/game/diva/playlog/';
var scorePages = {{ playlog_pages }};
if (Number.isNaN(currentPage)) {
currentPage = 1;
}
$('#cur_page').text(currentPage);
$('#prev_page').attr('href', rootUrl + (currentPage - 1))
$('#next_page').attr('href', rootUrl + (currentPage + 1))
$('#front_page').attr('href', rootUrl + (currentPage - 1))
$('#front_page').text(currentPage - 1);
$('#back_page').attr('href', rootUrl + (currentPage + 1))
$('#back_page').text(currentPage + 1);
$('#prev_3_page').attr('href', rootUrl + (currentPage - 3))
$('#next_3_page').attr('href', rootUrl + (currentPage + 3))
if ((currentPage - 1) < 3) {
$('#prev_3_page').hide();
if ((currentPage - 1) < 2) {
$('#front_page').hide();
if (currentPage === 1) {
$('#first_page').hide();
$('#prev_page').addClass('disabled');
}
}
}
if ((scorePages - currentPage) < 3) {
$('#next_3_page').hide();
if ((scorePages - currentPage) < 2) {
$('#back_page').hide();
if (currentPage === scorePages) {
$('#last_page').hide();
$('#next_page').addClass('disabled');
}
}
}
$('#go_button').click(function () {
var pageNumber = parseInt($('#page_input').val());
if (!Number.isNaN(pageNumber) && pageNumber <= scorePages && pageNumber >= 0) {
var url = '/game/diva/playlog/' + pageNumber;
window.location.href = url;
} else {
$('#page_input').val('');
$('#page_input').attr('placeholder', 'invalid input!');
}
});
});
</script>
{% endblock content %}

View File

@ -165,6 +165,8 @@ class PokkenBase:
f"Register new card {access_code} (UserId {user_id}, CardId {card_id})"
)
await self.data.card.set_chip_id_by_access_code(access_code, int(request.load_user.chip_id[:8], 16))
elif card is None:
self.logger.info(f"Registration of card {access_code} blocked!")
res.load_user.CopyFrom(load_usr)
@ -173,6 +175,8 @@ class PokkenBase:
else:
user_id = card['user']
card_id = card['id']
if not card['chip_id']:
await self.data.card.set_chip_id_by_access_code(access_code, int(request.load_user.chip_id[:8], 16))
"""
TODO: Unlock all supports? Probably

View File

@ -83,6 +83,10 @@ class SaoBase:
if not user_id:
user_id = await self.data.user.create_user() #works
card_id = await self.data.card.create_card(user_id, req.access_code)
if req.access_code.startswith("5"):
await self.data.card.set_idm_by_access_code(card_id, req.chip_id[:16])
elif req.access_code.startswith("010") or req.access_code.startswith("3"):
await self.data.card.set_chip_id_by_access_code(card_id, int(req.chip_id[:8], 16))
if card_id is None:
user_id = -1