forked from Dniel97/artemis
Merge branch 'develop' into idac
This commit is contained in:
commit
66114238a5
@ -1,6 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
|
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
|
||||||
|
|
||||||
|
## 20240620
|
||||||
|
### CHUNITHM
|
||||||
|
+ CHUNITHM LUMINOUS support
|
||||||
|
|
||||||
|
## 20240616
|
||||||
|
### DIVA
|
||||||
|
+ Working frontend with name and level strings edit and playlog
|
||||||
|
|
||||||
## 20240530
|
## 20240530
|
||||||
### DIVA
|
### DIVA
|
||||||
+ Fix reader for when dificulty is not a int
|
+ Fix reader for when dificulty is not a int
|
||||||
|
@ -10,13 +10,14 @@ class ADBFelicaLookupRequest(ADBBaseRequest):
|
|||||||
self.pmm = hex(pmm)[2:].upper()
|
self.pmm = hex(pmm)[2:].upper()
|
||||||
|
|
||||||
class ADBFelicaLookupResponse(ADBBaseResponse):
|
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)
|
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.access_code = access_code if access_code is not None else "00000000000000000000"
|
||||||
|
self.idx = idx
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_req(cls, req: ADBHeader, access_code: str = None) -> "ADBFelicaLookupResponse":
|
def from_req(cls, req: ADBHeader, access_code: str = None, idx: int = 0) -> "ADBFelicaLookupResponse":
|
||||||
c = cls(access_code, req.game_id, req.store_id, req.keychip_id)
|
c = cls(access_code, idx, req.game_id, req.store_id, req.keychip_id)
|
||||||
c.head.protocol_ver = req.protocol_ver
|
c.head.protocol_ver = req.protocol_ver
|
||||||
return c
|
return c
|
||||||
|
|
||||||
@ -26,7 +27,7 @@ class ADBFelicaLookupResponse(ADBBaseResponse):
|
|||||||
"access_code" / Int8ub[10],
|
"access_code" / Int8ub[10],
|
||||||
Padding(2)
|
Padding(2)
|
||||||
).build(dict(
|
).build(dict(
|
||||||
felica_idx = 0,
|
felica_idx = self.idx,
|
||||||
access_code = bytes.fromhex(self.access_code)
|
access_code = bytes.fromhex(self.access_code)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@ -194,6 +194,9 @@ class AimedbServlette():
|
|||||||
|
|
||||||
if user_id and user_id > 0:
|
if user_id and user_id > 0:
|
||||||
await self.data.card.update_card_last_login(req.access_code)
|
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
|
return ret
|
||||||
|
|
||||||
async def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
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:
|
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
|
On official, the IDm is used as a key to look up the stored access code in a large
|
||||||
I'm making the executive decision to not implement that and just kick back our
|
database. We do not have access to that database so we have to make due with what we got.
|
||||||
faux generated access code. The real felica IDm -> access code conversion is done
|
Interestingly, namco games are able to read S_PAD0 and send the server the correct access
|
||||||
on the ADB server, which we do not and will not ever have access to. Because we can
|
code, but aimedb doesn't. Until somebody either enters the correct code manually, or scans
|
||||||
assure that all IDms will be unique, this basic 0-padded hex -> int conversion will
|
on a game that reads it correctly from the card, this will have to do. It's the same conversion
|
||||||
be fine.
|
used on the big boy networks.
|
||||||
"""
|
"""
|
||||||
req = ADBFelicaLookupRequest(data)
|
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(
|
self.logger.info(
|
||||||
f"idm {req.idm} ipm {req.pmm} -> access_code {ac}"
|
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:
|
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)
|
req = ADBFelicaLookupRequest(data)
|
||||||
ac = self.data.card.to_access_code(req.idm)
|
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:
|
async def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes:
|
||||||
req = ADBFelicaLookup2Request(data)
|
req = ADBFelicaLookup2Request(data)
|
||||||
access_code = self.data.card.to_access_code(req.idm)
|
user_id = None
|
||||||
user_id = await self.data.card.get_user_id_from_card(access_code=access_code)
|
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:
|
if user_id is None:
|
||||||
user_id = -1
|
user_id = -1
|
||||||
@ -290,6 +313,14 @@ class AimedbServlette():
|
|||||||
)
|
)
|
||||||
|
|
||||||
resp = ADBFelicaLookup2Response.from_req(req.head, user_id, access_code)
|
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:
|
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)
|
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(
|
self.logger.info(
|
||||||
f"Registration blocked!: access code {req.access_code}"
|
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)
|
resp = ADBLookupResponse.from_req(req.head, user_id)
|
||||||
if resp.user_id <= 0:
|
if resp.user_id <= 0:
|
||||||
|
@ -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 ###
|
@ -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 ###
|
87
core/data/alembic/versions/b23f985100ba_chunithm_luminous.py
Normal file
87
core/data/alembic/versions/b23f985100ba_chunithm_luminous.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
"""CHUNITHM LUMINOUS
|
||||||
|
|
||||||
|
Revision ID: b23f985100ba
|
||||||
|
Revises: 3657efefc5a4
|
||||||
|
Create Date: 2024-06-20 08:08:08.759261
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy import Column, Integer, Boolean, UniqueConstraint
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b23f985100ba'
|
||||||
|
down_revision = '3657efefc5a4'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.create_table(
|
||||||
|
"chuni_profile_net_battle",
|
||||||
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
|
Column("user", Integer, nullable=False),
|
||||||
|
Column("isRankUpChallengeFailed", Boolean),
|
||||||
|
Column("highestBattleRankId", Integer),
|
||||||
|
Column("battleIconId", Integer),
|
||||||
|
Column("battleIconNum", Integer),
|
||||||
|
Column("avatarEffectPoint", Integer),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
None,
|
||||||
|
"chuni_profile_net_battle",
|
||||||
|
"aime_user",
|
||||||
|
["user"],
|
||||||
|
["id"],
|
||||||
|
ondelete="cascade",
|
||||||
|
onupdate="cascade",
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"chuni_item_cmission",
|
||||||
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
|
Column("user", Integer, nullable=False),
|
||||||
|
Column("missionId", Integer, nullable=False),
|
||||||
|
Column("point", Integer),
|
||||||
|
UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
None,
|
||||||
|
"chuni_item_cmission",
|
||||||
|
"aime_user",
|
||||||
|
["user"],
|
||||||
|
["id"],
|
||||||
|
ondelete="cascade",
|
||||||
|
onupdate="cascade",
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"chuni_item_cmission_progress",
|
||||||
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
|
Column("user", Integer, nullable=False),
|
||||||
|
Column("missionId", Integer, nullable=False),
|
||||||
|
Column("order", Integer),
|
||||||
|
Column("stage", Integer),
|
||||||
|
Column("progress", Integer),
|
||||||
|
UniqueConstraint(
|
||||||
|
"user", "missionId", "order", name="chuni_item_cmission_progress_uk"
|
||||||
|
),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
None,
|
||||||
|
"chuni_item_cmission_progress",
|
||||||
|
"aime_user",
|
||||||
|
["user"],
|
||||||
|
["id"],
|
||||||
|
ondelete="cascade",
|
||||||
|
onupdate="cascade",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_table("chuni_profile_net_battle")
|
||||||
|
op.drop_table("chuni_item_cmission")
|
||||||
|
op.drop_table("chuni_item_cmission_progress")
|
@ -1,6 +1,6 @@
|
|||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
from sqlalchemy import Table, Column, UniqueConstraint
|
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.schema import ForeignKey
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from sqlalchemy.engine import Row
|
from sqlalchemy.engine import Row
|
||||||
@ -11,12 +11,10 @@ aime_card = Table(
|
|||||||
"aime_card",
|
"aime_card",
|
||||||
metadata,
|
metadata,
|
||||||
Column("id", Integer, primary_key=True, nullable=False),
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
Column(
|
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
|
||||||
"user",
|
Column("access_code", String(20), nullable=False, unique=True),
|
||||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
Column("idm", String(16), unique=True),
|
||||||
nullable=False,
|
Column("chip_id", BIGINT, unique=True),
|
||||||
),
|
|
||||||
Column("access_code", String(20)),
|
|
||||||
Column("created_date", TIMESTAMP, server_default=func.now()),
|
Column("created_date", TIMESTAMP, server_default=func.now()),
|
||||||
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
|
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
|
||||||
Column("is_locked", Boolean, server_default="0"),
|
Column("is_locked", Boolean, server_default="0"),
|
||||||
@ -122,6 +120,26 @@ class CardData(BaseData):
|
|||||||
if result is None:
|
if result is None:
|
||||||
self.logger.warn(f"Failed to update last login time for {access_code}")
|
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:
|
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
|
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
|
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}"
|
||||||
|
@ -63,6 +63,7 @@ Games listed below have been tested and confirmed working.
|
|||||||
| 12 | CHUNITHM NEW PLUS!! |
|
| 12 | CHUNITHM NEW PLUS!! |
|
||||||
| 13 | CHUNITHM SUN |
|
| 13 | CHUNITHM SUN |
|
||||||
| 14 | CHUNITHM SUN PLUS |
|
| 14 | CHUNITHM SUN PLUS |
|
||||||
|
| 15 | CHUNITHM LUMINOUS |
|
||||||
|
|
||||||
|
|
||||||
### Importer
|
### Importer
|
||||||
|
@ -22,6 +22,9 @@ version:
|
|||||||
14:
|
14:
|
||||||
rom: 2.15.00
|
rom: 2.15.00
|
||||||
data: 2.15.00
|
data: 2.15.00
|
||||||
|
15:
|
||||||
|
rom: 2.20.00
|
||||||
|
data: 2.20.00
|
||||||
|
|
||||||
crypto:
|
crypto:
|
||||||
encrypted_only: False
|
encrypted_only: False
|
||||||
|
@ -941,6 +941,31 @@ class ChuniBase:
|
|||||||
rating_type,
|
rating_type,
|
||||||
upsert[rating_type],
|
upsert[rating_type],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# added in LUMINOUS
|
||||||
|
if "userCMissionList" in upsert:
|
||||||
|
for cmission in upsert["userCMissionList"]:
|
||||||
|
mission_id = cmission["missionId"]
|
||||||
|
|
||||||
|
await self.data.item.put_cmission(
|
||||||
|
user_id,
|
||||||
|
{
|
||||||
|
"missionId": mission_id,
|
||||||
|
"point": cmission["point"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
for progress in cmission["userCMissionProgressList"]:
|
||||||
|
await self.data.item.put_cmission_progress(user_id, mission_id, progress)
|
||||||
|
|
||||||
|
if "userNetBattleData" in upsert:
|
||||||
|
net_battle = upsert["userNetBattleData"][0]
|
||||||
|
|
||||||
|
# fix the boolean
|
||||||
|
net_battle["isRankUpChallengeFailed"] = (
|
||||||
|
False if net_battle["isRankUpChallengeFailed"] == "false" else True
|
||||||
|
)
|
||||||
|
await self.data.profile.put_net_battle(user_id, net_battle)
|
||||||
|
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
@ -969,4 +994,4 @@ class ChuniBase:
|
|||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"userNetBattleData": {"recentNBSelectMusicList": []},
|
"userNetBattleData": {"recentNBSelectMusicList": []},
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class ChuniConstants:
|
class ChuniConstants:
|
||||||
GAME_CODE = "SDBT"
|
GAME_CODE = "SDBT"
|
||||||
GAME_CODE_NEW = "SDHD"
|
GAME_CODE_NEW = "SDHD"
|
||||||
@ -20,6 +23,7 @@ class ChuniConstants:
|
|||||||
VER_CHUNITHM_NEW_PLUS = 12
|
VER_CHUNITHM_NEW_PLUS = 12
|
||||||
VER_CHUNITHM_SUN = 13
|
VER_CHUNITHM_SUN = 13
|
||||||
VER_CHUNITHM_SUN_PLUS = 14
|
VER_CHUNITHM_SUN_PLUS = 14
|
||||||
|
VER_CHUNITHM_LUMINOUS = 15
|
||||||
VERSION_NAMES = [
|
VERSION_NAMES = [
|
||||||
"CHUNITHM",
|
"CHUNITHM",
|
||||||
"CHUNITHM PLUS",
|
"CHUNITHM PLUS",
|
||||||
@ -35,9 +39,22 @@ class ChuniConstants:
|
|||||||
"CHUNITHM NEW!!",
|
"CHUNITHM NEW!!",
|
||||||
"CHUNITHM NEW PLUS!!",
|
"CHUNITHM NEW PLUS!!",
|
||||||
"CHUNITHM SUN",
|
"CHUNITHM SUN",
|
||||||
"CHUNITHM SUN PLUS"
|
"CHUNITHM SUN PLUS",
|
||||||
|
"CHUNITHM LUMINOUS",
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def game_ver_to_string(cls, ver: int):
|
def game_ver_to_string(cls, ver: int):
|
||||||
return cls.VERSION_NAMES[ver]
|
return cls.VERSION_NAMES[ver]
|
||||||
|
|
||||||
|
|
||||||
|
class MapAreaConditionType(Enum):
|
||||||
|
UNLOCKED = 0
|
||||||
|
MAP_CLEARED = 1
|
||||||
|
MAP_AREA_CLEARED = 2
|
||||||
|
TROPHY_OBTAINED = 3
|
||||||
|
|
||||||
|
|
||||||
|
class MapAreaConditionLogicalOperator(Enum):
|
||||||
|
AND = 1
|
||||||
|
OR = 2
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.routing import Route
|
from starlette.routing import Route
|
||||||
from starlette.responses import Response
|
from starlette.responses import Response
|
||||||
import logging, coloredlogs
|
import logging
|
||||||
|
import coloredlogs
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
import zlib
|
import zlib
|
||||||
import yaml
|
import yaml
|
||||||
@ -34,6 +35,7 @@ from .new import ChuniNew
|
|||||||
from .newplus import ChuniNewPlus
|
from .newplus import ChuniNewPlus
|
||||||
from .sun import ChuniSun
|
from .sun import ChuniSun
|
||||||
from .sunplus import ChuniSunPlus
|
from .sunplus import ChuniSunPlus
|
||||||
|
from .luminous import ChuniLuminous
|
||||||
|
|
||||||
class ChuniServlet(BaseServlet):
|
class ChuniServlet(BaseServlet):
|
||||||
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||||
@ -61,6 +63,7 @@ class ChuniServlet(BaseServlet):
|
|||||||
ChuniNewPlus,
|
ChuniNewPlus,
|
||||||
ChuniSun,
|
ChuniSun,
|
||||||
ChuniSunPlus,
|
ChuniSunPlus,
|
||||||
|
ChuniLuminous,
|
||||||
]
|
]
|
||||||
|
|
||||||
self.logger = logging.getLogger("chuni")
|
self.logger = logging.getLogger("chuni")
|
||||||
@ -103,7 +106,9 @@ class ChuniServlet(BaseServlet):
|
|||||||
for method in method_list:
|
for method in method_list:
|
||||||
method_fixed = inflection.camelize(method)[6:-7]
|
method_fixed = inflection.camelize(method)[6:-7]
|
||||||
# number of iterations was changed to 70 in SUN and then to 36
|
# number of iterations was changed to 70 in SUN and then to 36
|
||||||
if version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
|
if version == ChuniConstants.VER_CHUNITHM_LUMINOUS:
|
||||||
|
iter_count = 8
|
||||||
|
elif version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
|
||||||
iter_count = 36
|
iter_count = 36
|
||||||
elif version == ChuniConstants.VER_CHUNITHM_SUN:
|
elif version == ChuniConstants.VER_CHUNITHM_SUN:
|
||||||
iter_count = 70
|
iter_count = 70
|
||||||
@ -195,8 +200,10 @@ class ChuniServlet(BaseServlet):
|
|||||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||||
elif version >= 210 and version < 215: # SUN
|
elif version >= 210 and version < 215: # SUN
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
|
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
|
||||||
elif version >= 215: # SUN PLUS
|
elif version >= 215 and version < 220: # SUN PLUS
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
||||||
|
elif version >= 220: # LUMINOUS
|
||||||
|
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
||||||
elif game_code == "SDGS": # Int
|
elif game_code == "SDGS": # Int
|
||||||
if version < 110: # SUPERSTAR / SUPERSTAR PLUS
|
if version < 110: # SUPERSTAR / SUPERSTAR PLUS
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # SUPERSTAR / SUPERSTAR PLUS worked fine with it
|
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # SUPERSTAR / SUPERSTAR PLUS worked fine with it
|
||||||
@ -206,8 +213,10 @@ class ChuniServlet(BaseServlet):
|
|||||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||||
elif version >= 120 and version < 125: # SUN
|
elif version >= 120 and version < 125: # SUN
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
|
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
|
||||||
elif version >= 125: # SUN PLUS
|
elif version >= 125 and version < 130: # SUN PLUS
|
||||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
||||||
|
elif version >= 130: # LUMINOUS
|
||||||
|
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
||||||
|
|
||||||
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
||||||
# If we get a 32 character long hex string, it's a hash and we're
|
# If we get a 32 character long hex string, it's a hash and we're
|
||||||
@ -295,7 +304,7 @@ class ChuniServlet(BaseServlet):
|
|||||||
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
|
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
|
||||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||||
|
|
||||||
if resp == None:
|
if resp is None:
|
||||||
resp = {"returnCode": 1}
|
resp = {"returnCode": 1}
|
||||||
|
|
||||||
self.logger.debug(f"Response {resp}")
|
self.logger.debug(f"Response {resp}")
|
||||||
@ -313,4 +322,4 @@ class ChuniServlet(BaseServlet):
|
|||||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
|
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(crypt.encrypt(padded))
|
return Response(crypt.encrypt(padded))
|
||||||
|
298
titles/chuni/luminous.py
Normal file
298
titles/chuni/luminous.py
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from titles.chuni.sunplus import ChuniSunPlus
|
||||||
|
from titles.chuni.const import ChuniConstants, MapAreaConditionLogicalOperator, MapAreaConditionType
|
||||||
|
from titles.chuni.config import ChuniConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ChuniLuminous(ChuniSunPlus):
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg)
|
||||||
|
self.version = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
||||||
|
|
||||||
|
async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||||
|
user_data = await super().handle_cm_get_user_preview_api_request(data)
|
||||||
|
|
||||||
|
# Does CARD MAKER 1.35 work this far up?
|
||||||
|
user_data["lastDataVersion"] = "2.20.00"
|
||||||
|
return user_data
|
||||||
|
|
||||||
|
async def handle_get_user_c_mission_api_request(self, data: Dict) -> Dict:
|
||||||
|
user_id = data["userId"]
|
||||||
|
mission_id = data["missionId"]
|
||||||
|
|
||||||
|
progress_list = []
|
||||||
|
point = 0
|
||||||
|
|
||||||
|
mission_data = await self.data.item.get_cmission(user_id, mission_id)
|
||||||
|
progress_data = await self.data.item.get_cmission_progress(user_id, mission_id)
|
||||||
|
|
||||||
|
if mission_data and progress_data:
|
||||||
|
point = mission_data["point"]
|
||||||
|
|
||||||
|
for progress in progress_data:
|
||||||
|
progress_list.append(
|
||||||
|
{
|
||||||
|
"order": progress["order"],
|
||||||
|
"stage": progress["stage"],
|
||||||
|
"progress": progress["progress"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"userId": user_id,
|
||||||
|
"missionId": mission_id,
|
||||||
|
"point": point,
|
||||||
|
"userCMissionProgressList": progress_list,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def handle_get_user_net_battle_ranking_info_api_request(self, data: Dict) -> Dict:
|
||||||
|
user_id = data["userId"]
|
||||||
|
|
||||||
|
net_battle = {}
|
||||||
|
net_battle_data = await self.data.profile.get_net_battle(user_id)
|
||||||
|
|
||||||
|
if net_battle_data:
|
||||||
|
net_battle = {
|
||||||
|
"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"],
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"userId": user_id,
|
||||||
|
"userNetBattleData": net_battle,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def handle_get_game_map_area_condition_api_request(self, data: Dict) -> Dict:
|
||||||
|
# There is no game data for this, everything is server side.
|
||||||
|
# However, we can selectively show/hide events as data is imported into the server.
|
||||||
|
events = await self.data.static.get_enabled_events(self.version)
|
||||||
|
event_by_id = {evt["eventId"]: evt for evt in events}
|
||||||
|
conditions = []
|
||||||
|
|
||||||
|
# The Mystic Rainbow of LUMINOUS map unlocks when any mainline LUMINOUS area
|
||||||
|
# (ep. I, ep. II, ep. III) are completed.
|
||||||
|
mystic_area_1_conditions = {
|
||||||
|
"mapAreaId": 3229301, # Mystic Rainbow of LUMINOUS Area 1
|
||||||
|
"length": 0,
|
||||||
|
"mapAreaConditionList": [],
|
||||||
|
}
|
||||||
|
mystic_area_1_added = False
|
||||||
|
|
||||||
|
# Secret AREA: MUSIC GAME
|
||||||
|
if 14029 in event_by_id:
|
||||||
|
start_date = event_by_id[14029]["startDate"].strftime(self.date_time_format)
|
||||||
|
mission_in_progress_end_date = "2099-12-31 00:00:00.0"
|
||||||
|
|
||||||
|
# The "MISSION in progress" trophy required to trigger the secret area
|
||||||
|
# is only available in the first CHUNITHM mission. If the second mission
|
||||||
|
# (event ID 14214) was imported into ARTEMiS, we disable the requirement
|
||||||
|
# for this trophy.
|
||||||
|
if 14214 in event_by_id:
|
||||||
|
mission_in_progress_end_date = (event_by_id[14214]["startDate"] - timedelta(hours=2)).strftime(self.date_time_format)
|
||||||
|
|
||||||
|
conditions.extend([
|
||||||
|
{
|
||||||
|
"mapAreaId": 2206201, # BlythE ULTIMA
|
||||||
|
"length": 1,
|
||||||
|
# Obtain the trophy "MISSION in progress".
|
||||||
|
"mapAreaConditionList": [
|
||||||
|
{
|
||||||
|
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
|
||||||
|
"conditionId": 6832,
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": start_date,
|
||||||
|
"endDate": mission_in_progress_end_date,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mapAreaId": 2206202, # PRIVATE SERVICE ULTIMA
|
||||||
|
"length": 1,
|
||||||
|
# Obtain the trophy "MISSION in progress".
|
||||||
|
"mapAreaConditionList": [
|
||||||
|
{
|
||||||
|
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
|
||||||
|
"conditionId": 6832,
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": start_date,
|
||||||
|
"endDate": mission_in_progress_end_date,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mapAreaId": 2206203, # New York Back Raise
|
||||||
|
"length": 1,
|
||||||
|
# SS NightTheater's EXPERT chart and get the title
|
||||||
|
# "今宵、劇場に映し出される景色とは――――。"
|
||||||
|
"mapAreaConditionList": [
|
||||||
|
{
|
||||||
|
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
|
||||||
|
"conditionId": 6833,
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": start_date,
|
||||||
|
"endDate": "2099-12-31 00:00:00.0",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mapAreaId": 2206204, # Spasmodic
|
||||||
|
"length": 2,
|
||||||
|
# - Get 1 miss on Random (any difficulty) and get the title "当たり待ち"
|
||||||
|
# - Get 1 miss on 花たちに希望を (any difficulty) and get the title "花たちに希望を"
|
||||||
|
"mapAreaConditionList": [
|
||||||
|
{
|
||||||
|
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
|
||||||
|
"conditionId": 6834,
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": start_date,
|
||||||
|
"endDate": "2099-12-31 00:00:00.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
|
||||||
|
"conditionId": 6835,
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": start_date,
|
||||||
|
"endDate": "2099-12-31 00:00:00.0",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mapAreaId": 2206205, # ΩΩPARTS
|
||||||
|
"length": 2,
|
||||||
|
# - S Sage EXPERT to get the title "マターリ進行キボンヌ"
|
||||||
|
# - Equip this title and play cab-to-cab with another person with this title
|
||||||
|
# to get "マターリしようよ". Disabled because it is difficult to play cab2cab
|
||||||
|
# on data setups. A network operator may consider re-enabling it by uncommenting
|
||||||
|
# the second condition.
|
||||||
|
"mapAreaConditionList": [
|
||||||
|
{
|
||||||
|
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
|
||||||
|
"conditionId": 6836,
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": start_date,
|
||||||
|
"endDate": "2099-12-31 00:00:00.0",
|
||||||
|
},
|
||||||
|
# {
|
||||||
|
# "type": MapAreaConditionType.TROPHY_OBTAINED.value,
|
||||||
|
# "conditionId": 6837,
|
||||||
|
# "logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
# "startDate": start_date,
|
||||||
|
# "endDate": "2099-12-31 00:00:00.0",
|
||||||
|
# },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mapAreaId": 2206206, # Blow My Mind
|
||||||
|
"length": 1,
|
||||||
|
# SS on CHAOS EXPERT, Hydra EXPERT, Surive EXPERT and Jakarta PROGRESSION EXPERT
|
||||||
|
# to get the title "Can you hear me?"
|
||||||
|
"mapAreaConditionList": [
|
||||||
|
{
|
||||||
|
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
|
||||||
|
"conditionId": 6838,
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": start_date,
|
||||||
|
"endDate": "2099-12-31 00:00:00.0",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mapAreaId": 2206207, # VALLIS-NERIA
|
||||||
|
"length": 6,
|
||||||
|
# Finish the 6 other areas
|
||||||
|
"mapAreaConditionList": [
|
||||||
|
{
|
||||||
|
"type": MapAreaConditionType.MAP_AREA_CLEARED.value,
|
||||||
|
"conditionId": x,
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": start_date,
|
||||||
|
"endDate": "2099-12-31 00:00:00.0",
|
||||||
|
}
|
||||||
|
for x in range(2206201, 2206207)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
# LUMINOUS ep. I
|
||||||
|
if 14005 in event_by_id:
|
||||||
|
start_date = event_by_id[14005]["startDate"].strftime(self.date_time_format)
|
||||||
|
|
||||||
|
if not mystic_area_1_added:
|
||||||
|
conditions.append(mystic_area_1_conditions)
|
||||||
|
mystic_area_1_added = True
|
||||||
|
|
||||||
|
mystic_area_1_conditions["length"] += 1
|
||||||
|
mystic_area_1_conditions["mapAreaConditionList"].append(
|
||||||
|
{
|
||||||
|
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||||
|
"conditionId": 3020701,
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.OR.value,
|
||||||
|
"startDate": start_date,
|
||||||
|
"endDate": "2099-12-31 00:00:00.0",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
conditions.append(
|
||||||
|
{
|
||||||
|
"mapAreaId": 3229302, # Mystic Rainbow of LUMINOUS Area 2,
|
||||||
|
"length": 1,
|
||||||
|
# Unlocks when LUMINOUS ep. I is completed.
|
||||||
|
"mapAreaConditionList": [
|
||||||
|
{
|
||||||
|
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||||
|
"conditionId": 3020701,
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": start_date,
|
||||||
|
"endDate": "2099-12-31 00:00:00.0",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# LUMINOUS ep. II
|
||||||
|
if 14251 in event_by_id:
|
||||||
|
start_date = event_by_id[14251]["startDate"].strftime(self.date_time_format)
|
||||||
|
|
||||||
|
if not mystic_area_1_added:
|
||||||
|
conditions.append(mystic_area_1_conditions)
|
||||||
|
mystic_area_1_added = True
|
||||||
|
|
||||||
|
mystic_area_1_conditions["length"] += 1
|
||||||
|
mystic_area_1_conditions["mapAreaConditionList"].append(
|
||||||
|
{
|
||||||
|
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||||
|
"conditionId": 3020702,
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.OR.value,
|
||||||
|
"startDate": start_date,
|
||||||
|
"endDate": "2099-12-31 00:00:00.0",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
conditions.append(
|
||||||
|
{
|
||||||
|
"mapAreaId": 3229303, # Mystic Rainbow of LUMINOUS Area 3,
|
||||||
|
"length": 1,
|
||||||
|
# Unlocks when LUMINOUS ep. II is completed.
|
||||||
|
"mapAreaConditionList": [
|
||||||
|
{
|
||||||
|
"type": MapAreaConditionType.MAP_CLEARED.value,
|
||||||
|
"conditionId": 3020702,
|
||||||
|
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
|
||||||
|
"startDate": start_date,
|
||||||
|
"endDate": "2099-12-31 00:00:00.0",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
"length": len(conditions),
|
||||||
|
"gameMapAreaConditionList": conditions,
|
||||||
|
}
|
@ -32,6 +32,8 @@ class ChuniNew(ChuniBase):
|
|||||||
return "210"
|
return "210"
|
||||||
if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
|
if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
|
||||||
return "215"
|
return "215"
|
||||||
|
if self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS:
|
||||||
|
return "220"
|
||||||
|
|
||||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
# use UTC time and convert it to JST time by adding +9
|
# use UTC time and convert it to JST time by adding +9
|
||||||
|
@ -48,9 +48,8 @@ class ChuniReader(BaseReader):
|
|||||||
for root, dirs, files in walk(f"{root_dir}loginBonusPreset"):
|
for root, dirs, files in walk(f"{root_dir}loginBonusPreset"):
|
||||||
for dir in dirs:
|
for dir in dirs:
|
||||||
if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"):
|
if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"):
|
||||||
with open(f"{root}/{dir}/LoginBonusPreset.xml", "rb") as fp:
|
with open(f"{root}/{dir}/LoginBonusPreset.xml", "r", encoding="utf-8") as fp:
|
||||||
bytedata = fp.read()
|
strdata = fp.read()
|
||||||
strdata = bytedata.decode("UTF-8")
|
|
||||||
|
|
||||||
xml_root = ET.fromstring(strdata)
|
xml_root = ET.fromstring(strdata)
|
||||||
for name in xml_root.findall("name"):
|
for name in xml_root.findall("name"):
|
||||||
@ -121,9 +120,8 @@ class ChuniReader(BaseReader):
|
|||||||
for root, dirs, files in walk(evt_dir):
|
for root, dirs, files in walk(evt_dir):
|
||||||
for dir in dirs:
|
for dir in dirs:
|
||||||
if path.exists(f"{root}/{dir}/Event.xml"):
|
if path.exists(f"{root}/{dir}/Event.xml"):
|
||||||
with open(f"{root}/{dir}/Event.xml", "rb") as fp:
|
with open(f"{root}/{dir}/Event.xml", "r", encoding="utf-8") as fp:
|
||||||
bytedata = fp.read()
|
strdata = fp.read()
|
||||||
strdata = bytedata.decode("UTF-8")
|
|
||||||
|
|
||||||
xml_root = ET.fromstring(strdata)
|
xml_root = ET.fromstring(strdata)
|
||||||
for name in xml_root.findall("name"):
|
for name in xml_root.findall("name"):
|
||||||
@ -144,9 +142,8 @@ class ChuniReader(BaseReader):
|
|||||||
for root, dirs, files in walk(music_dir):
|
for root, dirs, files in walk(music_dir):
|
||||||
for dir in dirs:
|
for dir in dirs:
|
||||||
if path.exists(f"{root}/{dir}/Music.xml"):
|
if path.exists(f"{root}/{dir}/Music.xml"):
|
||||||
with open(f"{root}/{dir}/Music.xml", "rb") as fp:
|
with open(f"{root}/{dir}/Music.xml", "r", encoding='utf-8') as fp:
|
||||||
bytedata = fp.read()
|
strdata = fp.read()
|
||||||
strdata = bytedata.decode("UTF-8")
|
|
||||||
|
|
||||||
xml_root = ET.fromstring(strdata)
|
xml_root = ET.fromstring(strdata)
|
||||||
for name in xml_root.findall("name"):
|
for name in xml_root.findall("name"):
|
||||||
@ -210,9 +207,8 @@ class ChuniReader(BaseReader):
|
|||||||
for root, dirs, files in walk(charge_dir):
|
for root, dirs, files in walk(charge_dir):
|
||||||
for dir in dirs:
|
for dir in dirs:
|
||||||
if path.exists(f"{root}/{dir}/ChargeItem.xml"):
|
if path.exists(f"{root}/{dir}/ChargeItem.xml"):
|
||||||
with open(f"{root}/{dir}/ChargeItem.xml", "rb") as fp:
|
with open(f"{root}/{dir}/ChargeItem.xml", "r", encoding='utf-8') as fp:
|
||||||
bytedata = fp.read()
|
strdata = fp.read()
|
||||||
strdata = bytedata.decode("UTF-8")
|
|
||||||
|
|
||||||
xml_root = ET.fromstring(strdata)
|
xml_root = ET.fromstring(strdata)
|
||||||
for name in xml_root.findall("name"):
|
for name in xml_root.findall("name"):
|
||||||
@ -240,9 +236,8 @@ class ChuniReader(BaseReader):
|
|||||||
for root, dirs, files in walk(avatar_dir):
|
for root, dirs, files in walk(avatar_dir):
|
||||||
for dir in dirs:
|
for dir in dirs:
|
||||||
if path.exists(f"{root}/{dir}/AvatarAccessory.xml"):
|
if path.exists(f"{root}/{dir}/AvatarAccessory.xml"):
|
||||||
with open(f"{root}/{dir}/AvatarAccessory.xml", "rb") as fp:
|
with open(f"{root}/{dir}/AvatarAccessory.xml", "r", encoding='utf-8') as fp:
|
||||||
bytedata = fp.read()
|
strdata = fp.read()
|
||||||
strdata = bytedata.decode("UTF-8")
|
|
||||||
|
|
||||||
xml_root = ET.fromstring(strdata)
|
xml_root = ET.fromstring(strdata)
|
||||||
for name in xml_root.findall("name"):
|
for name in xml_root.findall("name"):
|
||||||
|
@ -243,6 +243,36 @@ matching = Table(
|
|||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cmission = Table(
|
||||||
|
"chuni_item_cmission",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
|
Column(
|
||||||
|
"user",
|
||||||
|
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
Column("missionId", Integer, nullable=False),
|
||||||
|
Column("point", Integer),
|
||||||
|
UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
|
||||||
|
cmission_progress = Table(
|
||||||
|
"chuni_item_cmission_progress",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True, 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),
|
||||||
|
Column("progress", Integer),
|
||||||
|
UniqueConstraint(
|
||||||
|
"user", "missionId", "order", name="chuni_item_cmission_progress_uk"
|
||||||
|
),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChuniItemData(BaseData):
|
class ChuniItemData(BaseData):
|
||||||
async def get_oldest_free_matching(self, version: int) -> Optional[Row]:
|
async def get_oldest_free_matching(self, version: int) -> Optional[Row]:
|
||||||
@ -594,3 +624,66 @@ class ChuniItemData(BaseData):
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
|
async def put_cmission_progress(
|
||||||
|
self, user_id: int, mission_id: int, progress_data: Dict
|
||||||
|
) -> Optional[int]:
|
||||||
|
progress_data["user"] = user_id
|
||||||
|
progress_data["missionId"] = mission_id
|
||||||
|
|
||||||
|
sql = insert(cmission_progress).values(**progress_data)
|
||||||
|
conflict = sql.on_duplicate_key_update(**progress_data)
|
||||||
|
result = await self.execute(conflict)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return result.lastrowid
|
||||||
|
|
||||||
|
async def get_cmission_progress(
|
||||||
|
self, user_id: int, mission_id: int
|
||||||
|
) -> Optional[List[Row]]:
|
||||||
|
sql = cmission_progress.select(
|
||||||
|
and_(
|
||||||
|
cmission_progress.c.user == user_id,
|
||||||
|
cmission_progress.c.missionId == mission_id,
|
||||||
|
)
|
||||||
|
).order_by(cmission_progress.c.order.asc())
|
||||||
|
result = await self.execute(sql)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return result.fetchall()
|
||||||
|
|
||||||
|
async def get_cmission(self, user_id: int, mission_id: int) -> Optional[Row]:
|
||||||
|
sql = cmission.select(
|
||||||
|
and_(cmission.c.user == user_id, cmission.c.missionId == mission_id)
|
||||||
|
)
|
||||||
|
result = await self.execute(sql)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return result.fetchone()
|
||||||
|
|
||||||
|
async def put_cmission(self, user_id: int, mission_data: Dict) -> Optional[int]:
|
||||||
|
mission_data["user"] = user_id
|
||||||
|
|
||||||
|
sql = insert(cmission).values(**mission_data)
|
||||||
|
conflict = sql.on_duplicate_key_update(**mission_data)
|
||||||
|
result = await self.execute(conflict)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return result.lastrowid
|
||||||
|
|
||||||
|
async def get_cmissions(self, user_id: int) -> Optional[List[Row]]:
|
||||||
|
sql = cmission.select(cmission.c.user == user_id)
|
||||||
|
result = await self.execute(sql)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return result.fetchall()
|
||||||
|
@ -412,6 +412,18 @@ rating = Table(
|
|||||||
mysql_charset="utf8mb4",
|
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):
|
class ChuniProfileData(BaseData):
|
||||||
async def update_name(self, user_id: int, new_name: str) -> bool:
|
async def update_name(self, user_id: int, new_name: str) -> bool:
|
||||||
@ -779,4 +791,32 @@ class ChuniProfileData(BaseData):
|
|||||||
else:
|
else:
|
||||||
versions_raw = result.fetchall()
|
versions_raw = result.fetchall()
|
||||||
versions = [row[0] for row in versions_raw]
|
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) -> Optional[Row]:
|
||||||
|
result = await self.execute(net_battle.select(net_battle.c.user == user_id))
|
||||||
|
if result:
|
||||||
|
return result.fetchone()
|
||||||
|
@ -242,6 +242,8 @@ class ChuniScoreData(BaseData):
|
|||||||
# Calculates the ROM version that should be fetched for rankings, based on the game version being retrieved
|
# Calculates the ROM version that should be fetched for rankings, based on the game version being retrieved
|
||||||
# This prevents tracks that are not accessible in your version from counting towards the 10 results
|
# This prevents tracks that are not accessible in your version from counting towards the 10 results
|
||||||
romVer = {
|
romVer = {
|
||||||
|
15: "2.20%",
|
||||||
|
14: "2.15%",
|
||||||
13: "2.10%",
|
13: "2.10%",
|
||||||
12: "2.05%",
|
12: "2.05%",
|
||||||
11: "2.00%",
|
11: "2.00%",
|
||||||
|
@ -2,8 +2,10 @@ from titles.diva.index import DivaServlet
|
|||||||
from titles.diva.const import DivaConstants
|
from titles.diva.const import DivaConstants
|
||||||
from titles.diva.database import DivaData
|
from titles.diva.database import DivaData
|
||||||
from titles.diva.read import DivaReader
|
from titles.diva.read import DivaReader
|
||||||
|
from .frontend import DivaFrontend
|
||||||
|
|
||||||
index = DivaServlet
|
index = DivaServlet
|
||||||
database = DivaData
|
database = DivaData
|
||||||
reader = DivaReader
|
reader = DivaReader
|
||||||
|
frontend = DivaFrontend
|
||||||
game_codes = [DivaConstants.GAME_CODE]
|
game_codes = [DivaConstants.GAME_CODE]
|
||||||
|
182
titles/diva/frontend.py
Normal file
182
titles/diva/frontend.py
Normal 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)
|
@ -90,7 +90,7 @@ class DivaProfileData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.lastrowid
|
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
|
Given an aime_id update the profile corresponding to the arguments
|
||||||
which are the diva_profile Columns
|
which are the diva_profile Columns
|
||||||
@ -102,7 +102,9 @@ class DivaProfileData(BaseData):
|
|||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"update_profile: failed to update profile! profile: {aime_id}"
|
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]]:
|
async def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]:
|
||||||
"""
|
"""
|
||||||
|
@ -239,3 +239,23 @@ class DivaScoreData(BaseData):
|
|||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchall()
|
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()
|
||||||
|
195
titles/diva/templates/css/diva_style.css
Normal file
195
titles/diva/templates/css/diva_style.css
Normal 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%);
|
||||||
|
}
|
||||||
|
}
|
17
titles/diva/templates/diva_header.jinja
Normal file
17
titles/diva/templates/diva_header.jinja
Normal 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>
|
111
titles/diva/templates/diva_index.jinja
Normal file
111
titles/diva/templates/diva_index.jinja
Normal 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 %}
|
169
titles/diva/templates/diva_playlog.jinja
Normal file
169
titles/diva/templates/diva_playlog.jinja
Normal 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>
|
||||||
|
 
|
||||||
|
</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 %}
|
@ -216,16 +216,20 @@ class OngekiServlet(BaseServlet):
|
|||||||
)
|
)
|
||||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||||
|
|
||||||
try:
|
if version < 105:
|
||||||
unzip = zlib.decompress(req_raw)
|
# O.N.G.E.K.I base don't use zlib
|
||||||
|
req_data = json.loads(req_raw)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
unzip = zlib.decompress(req_raw)
|
||||||
|
|
||||||
|
except zlib.error as e:
|
||||||
|
self.logger.error(
|
||||||
|
f"Failed to decompress v{version} {endpoint} request -> {e}"
|
||||||
|
)
|
||||||
|
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||||
|
|
||||||
except zlib.error as e:
|
req_data = json.loads(unzip)
|
||||||
self.logger.error(
|
|
||||||
f"Failed to decompress v{version} {endpoint} request -> {e}"
|
|
||||||
)
|
|
||||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
|
||||||
|
|
||||||
req_data = json.loads(unzip)
|
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"v{version} {endpoint} request from {client_ip}"
|
f"v{version} {endpoint} request from {client_ip}"
|
||||||
@ -251,9 +255,12 @@ class OngekiServlet(BaseServlet):
|
|||||||
|
|
||||||
self.logger.debug(f"Response {resp}")
|
self.logger.debug(f"Response {resp}")
|
||||||
|
|
||||||
zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
|
resp_raw = json.dumps(resp, ensure_ascii=False).encode("utf-8")
|
||||||
|
zipped = zlib.compress(resp_raw)
|
||||||
|
|
||||||
if not encrtped or version < 120:
|
if not encrtped or version < 120:
|
||||||
|
if version < 105:
|
||||||
|
return Response(resp_raw)
|
||||||
return Response(zipped)
|
return Response(zipped)
|
||||||
|
|
||||||
padded = pad(zipped, 16)
|
padded = pad(zipped, 16)
|
||||||
|
@ -165,6 +165,8 @@ class PokkenBase:
|
|||||||
f"Register new card {access_code} (UserId {user_id}, CardId {card_id})"
|
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:
|
elif card is None:
|
||||||
self.logger.info(f"Registration of card {access_code} blocked!")
|
self.logger.info(f"Registration of card {access_code} blocked!")
|
||||||
res.load_user.CopyFrom(load_usr)
|
res.load_user.CopyFrom(load_usr)
|
||||||
@ -173,6 +175,8 @@ class PokkenBase:
|
|||||||
else:
|
else:
|
||||||
user_id = card['user']
|
user_id = card['user']
|
||||||
card_id = card['id']
|
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
|
TODO: Unlock all supports? Probably
|
||||||
|
@ -83,6 +83,10 @@ class SaoBase:
|
|||||||
if not user_id:
|
if not user_id:
|
||||||
user_id = await self.data.user.create_user() #works
|
user_id = await self.data.user.create_user() #works
|
||||||
card_id = await self.data.card.create_card(user_id, req.access_code)
|
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:
|
if card_id is None:
|
||||||
user_id = -1
|
user_id = -1
|
||||||
|
@ -833,14 +833,14 @@ class WaccaBase:
|
|||||||
# TODO: Coop and vs data
|
# TODO: Coop and vs data
|
||||||
async def handle_user_music_updateCoop_request(self, data: Dict) -> Dict:
|
async def handle_user_music_updateCoop_request(self, data: Dict) -> Dict:
|
||||||
coop_info = data["params"][4]
|
coop_info = data["params"][4]
|
||||||
return self.handle_user_music_update_request(data)
|
return await self.handle_user_music_update_request(data)
|
||||||
|
|
||||||
async def handle_user_music_updateVersus_request(self, data: Dict) -> Dict:
|
async def handle_user_music_updateVersus_request(self, data: Dict) -> Dict:
|
||||||
vs_info = data["params"][4]
|
vs_info = data["params"][4]
|
||||||
return self.handle_user_music_update_request(data)
|
return await self.handle_user_music_update_request(data)
|
||||||
|
|
||||||
async def handle_user_music_updateTrial_request(self, data: Dict) -> Dict:
|
async def handle_user_music_updateTrial_request(self, data: Dict) -> Dict:
|
||||||
return self.handle_user_music_update_request(data)
|
return await self.handle_user_music_update_request(data)
|
||||||
|
|
||||||
async def handle_user_mission_update_request(self, data: Dict) -> Dict:
|
async def handle_user_mission_update_request(self, data: Dict) -> Dict:
|
||||||
req = UserMissionUpdateRequest(data)
|
req = UserMissionUpdateRequest(data)
|
||||||
|
Loading…
Reference in New Issue
Block a user