diff --git a/changelog.md b/changelog.md
index 3a267ed..773a110 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,6 +1,14 @@
# Changelog
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
### 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/alembic/versions/b23f985100ba_chunithm_luminous.py b/core/data/alembic/versions/b23f985100ba_chunithm_luminous.py
new file mode 100644
index 0000000..dd52974
--- /dev/null
+++ b/core/data/alembic/versions/b23f985100ba_chunithm_luminous.py
@@ -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")
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/docs/game_specific_info.md b/docs/game_specific_info.md
index 1b64ae8..71ecda1 100644
--- a/docs/game_specific_info.md
+++ b/docs/game_specific_info.md
@@ -63,6 +63,7 @@ Games listed below have been tested and confirmed working.
| 12 | CHUNITHM NEW PLUS!! |
| 13 | CHUNITHM SUN |
| 14 | CHUNITHM SUN PLUS |
+| 15 | CHUNITHM LUMINOUS |
### Importer
diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml
index 53da186..4855fa1 100644
--- a/example_config/chuni.yaml
+++ b/example_config/chuni.yaml
@@ -22,6 +22,9 @@ version:
14:
rom: 2.15.00
data: 2.15.00
+ 15:
+ rom: 2.20.00
+ data: 2.20.00
crypto:
encrypted_only: False
diff --git a/titles/chuni/base.py b/titles/chuni/base.py
index 9e8a634..2a662d7 100644
--- a/titles/chuni/base.py
+++ b/titles/chuni/base.py
@@ -941,6 +941,31 @@ class ChuniBase:
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"}
@@ -969,4 +994,4 @@ class ChuniBase:
return {
"userId": data["userId"],
"userNetBattleData": {"recentNBSelectMusicList": []},
- }
\ No newline at end of file
+ }
diff --git a/titles/chuni/const.py b/titles/chuni/const.py
index 3e83378..d037842 100644
--- a/titles/chuni/const.py
+++ b/titles/chuni/const.py
@@ -1,3 +1,6 @@
+from enum import Enum
+
+
class ChuniConstants:
GAME_CODE = "SDBT"
GAME_CODE_NEW = "SDHD"
@@ -20,6 +23,7 @@ class ChuniConstants:
VER_CHUNITHM_NEW_PLUS = 12
VER_CHUNITHM_SUN = 13
VER_CHUNITHM_SUN_PLUS = 14
+ VER_CHUNITHM_LUMINOUS = 15
VERSION_NAMES = [
"CHUNITHM",
"CHUNITHM PLUS",
@@ -35,9 +39,22 @@ class ChuniConstants:
"CHUNITHM NEW!!",
"CHUNITHM NEW PLUS!!",
"CHUNITHM SUN",
- "CHUNITHM SUN PLUS"
+ "CHUNITHM SUN PLUS",
+ "CHUNITHM LUMINOUS",
]
@classmethod
def game_ver_to_string(cls, ver: int):
- return cls.VERSION_NAMES[ver]
\ No newline at end of file
+ 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
diff --git a/titles/chuni/index.py b/titles/chuni/index.py
index f0f1eac..39dd9ba 100644
--- a/titles/chuni/index.py
+++ b/titles/chuni/index.py
@@ -1,7 +1,8 @@
from starlette.requests import Request
from starlette.routing import Route
from starlette.responses import Response
-import logging, coloredlogs
+import logging
+import coloredlogs
from logging.handlers import TimedRotatingFileHandler
import zlib
import yaml
@@ -34,6 +35,7 @@ from .new import ChuniNew
from .newplus import ChuniNewPlus
from .sun import ChuniSun
from .sunplus import ChuniSunPlus
+from .luminous import ChuniLuminous
class ChuniServlet(BaseServlet):
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
@@ -61,6 +63,7 @@ class ChuniServlet(BaseServlet):
ChuniNewPlus,
ChuniSun,
ChuniSunPlus,
+ ChuniLuminous,
]
self.logger = logging.getLogger("chuni")
@@ -103,7 +106,9 @@ class ChuniServlet(BaseServlet):
for method in method_list:
method_fixed = inflection.camelize(method)[6:-7]
# 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
elif version == ChuniConstants.VER_CHUNITHM_SUN:
iter_count = 70
@@ -195,8 +200,10 @@ class ChuniServlet(BaseServlet):
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
elif version >= 210 and version < 215: # 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
+ elif version >= 220: # LUMINOUS
+ internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
elif game_code == "SDGS": # Int
if version < 110: # SUPERSTAR / SUPERSTAR PLUS
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
elif version >= 120 and version < 125: # 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
+ 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 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}")
return Response(zlib.compress(b'{"stat": "0"}'))
- if resp == None:
+ if resp is None:
resp = {"returnCode": 1}
self.logger.debug(f"Response {resp}")
@@ -313,4 +322,4 @@ class ChuniServlet(BaseServlet):
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
)
- return Response(crypt.encrypt(padded))
\ No newline at end of file
+ return Response(crypt.encrypt(padded))
diff --git a/titles/chuni/luminous.py b/titles/chuni/luminous.py
new file mode 100644
index 0000000..8f02820
--- /dev/null
+++ b/titles/chuni/luminous.py
@@ -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,
+ }
diff --git a/titles/chuni/new.py b/titles/chuni/new.py
index 9709f00..2275a6e 100644
--- a/titles/chuni/new.py
+++ b/titles/chuni/new.py
@@ -32,6 +32,8 @@ class ChuniNew(ChuniBase):
return "210"
if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
return "215"
+ if self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS:
+ return "220"
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
# use UTC time and convert it to JST time by adding +9
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 5077e14..30db4b8 100644
--- a/titles/chuni/schema/item.py
+++ b/titles/chuni/schema/item.py
@@ -243,6 +243,36 @@ matching = Table(
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):
async def get_oldest_free_matching(self, version: int) -> Optional[Row]:
@@ -594,3 +624,66 @@ class ChuniItemData(BaseData):
)
return None
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()
diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py
index 2f8bce3..f47b780 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) -> Optional[Row]:
+ result = await self.execute(net_battle.select(net_battle.c.user == user_id))
+ if result:
+ return result.fetchone()
diff --git a/titles/chuni/schema/score.py b/titles/chuni/schema/score.py
index fa95374..766b4b9 100644
--- a/titles/chuni/schema/score.py
+++ b/titles/chuni/schema/score.py
@@ -242,6 +242,8 @@ class ChuniScoreData(BaseData):
# 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
romVer = {
+ 15: "2.20%",
+ 14: "2.15%",
13: "2.10%",
12: "2.05%",
11: "2.00%",
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/ongeki/index.py b/titles/ongeki/index.py
index d7f3e0e..3bd0e15 100644
--- a/titles/ongeki/index.py
+++ b/titles/ongeki/index.py
@@ -216,16 +216,20 @@ class OngekiServlet(BaseServlet):
)
return Response(zlib.compress(b'{"stat": "0"}'))
- try:
- unzip = zlib.decompress(req_raw)
+ if version < 105:
+ # 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:
- self.logger.error(
- f"Failed to decompress v{version} {endpoint} request -> {e}"
- )
- return Response(zlib.compress(b'{"stat": "0"}'))
-
- req_data = json.loads(unzip)
+ req_data = json.loads(unzip)
self.logger.info(
f"v{version} {endpoint} request from {client_ip}"
@@ -251,9 +255,12 @@ class OngekiServlet(BaseServlet):
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 version < 105:
+ return Response(resp_raw)
return Response(zipped)
padded = pad(zipped, 16)
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
diff --git a/titles/wacca/base.py b/titles/wacca/base.py
index 8a37c2b..58b1ba9 100644
--- a/titles/wacca/base.py
+++ b/titles/wacca/base.py
@@ -833,14 +833,14 @@ class WaccaBase:
# TODO: Coop and vs data
async def handle_user_music_updateCoop_request(self, data: Dict) -> Dict:
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:
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:
- 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:
req = UserMissionUpdateRequest(data)