diff --git a/changelog.md b/changelog.md
index 07f17c8..8148a9d 100644
--- a/changelog.md
+++ b/changelog.md
@@ -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
diff --git a/core/adb_handlers/felica.py b/core/adb_handlers/felica.py
index 479e84d..22c9f05 100644
--- a/core/adb_handlers/felica.py
+++ b/core/adb_handlers/felica.py
@@ -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)
))
diff --git a/core/aimedb.py b/core/aimedb.py
index 08a1b65..c6887d3 100644
--- a/core/aimedb.py
+++ b/core/aimedb.py
@@ -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:
diff --git a/core/data/alembic/versions/1e150d16ab6b_chuni_add_net_battle_uk.py b/core/data/alembic/versions/1e150d16ab6b_chuni_add_net_battle_uk.py
new file mode 100644
index 0000000..5fcd5a8
--- /dev/null
+++ b/core/data/alembic/versions/1e150d16ab6b_chuni_add_net_battle_uk.py
@@ -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 ###
diff --git a/core/data/alembic/versions/48f4acc43a7e_card_add_idm_chip_id.py b/core/data/alembic/versions/48f4acc43a7e_card_add_idm_chip_id.py
new file mode 100644
index 0000000..12fca3a
--- /dev/null
+++ b/core/data/alembic/versions/48f4acc43a7e_card_add_idm_chip_id.py
@@ -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 ###
diff --git a/core/data/schema/card.py b/core/data/schema/card.py
index cd5c647..798dd03 100644
--- a/core/data/schema/card.py
+++ b/core/data/schema/card.py
@@ -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}"
diff --git a/titles/chuni/read.py b/titles/chuni/read.py
index 5082f7a..db7435c 100644
--- a/titles/chuni/read.py
+++ b/titles/chuni/read.py
@@ -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"):
diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py
index 2f386ef..30db4b8 100644
--- a/titles/chuni/schema/item.py
+++ b/titles/chuni/schema/item.py
@@ -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),
diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py
index 2f8bce3..7ee0164 100644
--- a/titles/chuni/schema/profile.py
+++ b/titles/chuni/schema/profile.py
@@ -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)
\ No newline at end of file
+ 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()
diff --git a/titles/diva/__init__.py b/titles/diva/__init__.py
index d298ba2..7bfa2cc 100644
--- a/titles/diva/__init__.py
+++ b/titles/diva/__init__.py
@@ -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]
diff --git a/titles/diva/frontend.py b/titles/diva/frontend.py
new file mode 100644
index 0000000..cc5c332
--- /dev/null
+++ b/titles/diva/frontend.py
@@ -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)
diff --git a/titles/diva/schema/profile.py b/titles/diva/schema/profile.py
index f3d00ae..10d3b51 100644
--- a/titles/diva/schema/profile.py
+++ b/titles/diva/schema/profile.py
@@ -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]]:
"""
diff --git a/titles/diva/schema/score.py b/titles/diva/schema/score.py
index e802a41..ce89f74 100644
--- a/titles/diva/schema/score.py
+++ b/titles/diva/schema/score.py
@@ -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()
diff --git a/titles/diva/templates/css/diva_style.css b/titles/diva/templates/css/diva_style.css
new file mode 100644
index 0000000..672db0f
--- /dev/null
+++ b/titles/diva/templates/css/diva_style.css
@@ -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%);
+ }
+}
\ No newline at end of file
diff --git a/titles/diva/templates/diva_header.jinja b/titles/diva/templates/diva_header.jinja
new file mode 100644
index 0000000..b92379a
--- /dev/null
+++ b/titles/diva/templates/diva_header.jinja
@@ -0,0 +1,17 @@
+
+
\ No newline at end of file
diff --git a/titles/diva/templates/diva_index.jinja b/titles/diva/templates/diva_index.jinja
new file mode 100644
index 0000000..c2f0888
--- /dev/null
+++ b/titles/diva/templates/diva_index.jinja
@@ -0,0 +1,111 @@
+{% extends "core/templates/index.jinja" %}
+{% block content %}
+
+
+ {% include 'titles/diva/templates/diva_header.jinja' %}
+ {% if profile is defined and profile is not none and profile|length > 0 %}
+
+
+
+
+ OVERVIEW
+
+ Player name:
+ {{ profile[3] }}
+
+ Edit
+
+ Level string:
+ {{ profile[4] }}
+
+ Edit
+
+
+
+ Lvl:
+ {{ profile[5] }}
+
+
+
+
+
+
+ Lvl points:
+ {{ profile[6] }}
+
+
+
+
+
+
+ Vocaloid points:
+ {{ profile[7] }}
+
+
+
+
+
+
+
+
+
+ {% 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 %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/titles/diva/templates/diva_playlog.jinja b/titles/diva/templates/diva_playlog.jinja
new file mode 100644
index 0000000..c5a5618
--- /dev/null
+++ b/titles/diva/templates/diva_playlog.jinja
@@ -0,0 +1,169 @@
+{% extends "core/templates/index.jinja" %}
+{% block content %}
+
+
+ {% include 'titles/diva/templates/diva_header.jinja' %}
+ {% if playlog is defined and playlog is not none %}
+
+
Score counts: {{ playlog_count }}
+ {% set difficultyName = ['easy', 'normal', 'hard', 'extreme', 'extra extreme'] %}
+ {% set clearState = ['MISSxTAKE', 'STANDARD', 'GREAT', 'EXELLENT', 'PERFECT'] %}
+ {% for record in playlog %}
+
+
+
+
+
+
+
{{ record.raw.score }}
+ {{ record.raw.atn_pnt / 100 }}%
+ {{ difficultyName[record.raw.difficulty] }}
+
+
+
+
+ COOL
+ {{ record.raw.cool }}
+
+
+ FINE
+ {{ record.raw.fine }}
+
+
+ SAFE
+ {{ record.raw.safe }}
+
+
+ SAD
+ {{ record.raw.sad }}
+
+
+ WORST
+ {{ record.raw.worst }}
+
+
+
+
+
{{ record.raw.max_combo }}
+ {% if record.raw.clr_kind == -1 %}
+ {{ clearState[0] }}
+ {% elif record.raw.clr_kind == 2 %}
+ {{ clearState[1] }}
+ {% elif record.raw.clr_kind == 3 %}
+ {{ clearState[2] }}
+ {% elif record.raw.clr_kind == 4 %}
+ {{ clearState[3] }}
+ {% elif record.raw.clr_kind == 5 %}
+ {{ clearState[4] }}
+ {% endif %}
+ {% if record.raw.clr_kind == -1 %}
+ NOT CLEAR
+ {% else %}
+ CLEAR
+ {% endif %}
+
+
+
+
+
+ {% endfor %}
+
+ {% 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 %}
+
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/titles/pokken/base.py b/titles/pokken/base.py
index f864824..ee0d134 100644
--- a/titles/pokken/base.py
+++ b/titles/pokken/base.py
@@ -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
diff --git a/titles/sao/base.py b/titles/sao/base.py
index 80f91a2..76a1def 100644
--- a/titles/sao/base.py
+++ b/titles/sao/base.py
@@ -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