22 Commits

Author SHA1 Message Date
f747a731bf [chuni] Use the map flag event ID when checking in GetGameMapAreaConditionApi 2024-06-23 01:36:48 +07:00
be2bf8b491 Merge pull request '[chuni] Improve GetGameMapAreaConditionApi' (#157) from beerpsi/artemis:feat/chunithm/map-area-conditions-rework into develop
Reviewed-on: Hay1tsme/artemis#157
2024-06-22 17:33:47 +00:00
203aa43fe1 chuni: add sun plus and lumi to get_rankings 2024-06-22 13:31:52 -04:00
ce124ffe13 [chuni] Improve GetGameMapAreaConditionApi 2024-06-23 00:08:53 +07:00
3979a020a6 add idm and chip_id fields to card table 2024-06-22 00:47:12 -04:00
ca9ccbe8a3 chuni: add missing net battle funcs 2024-06-21 23:00:35 -04:00
bf5c959324 chuni: fix reader issue caused by encoding weirdness (#55) 2024-06-21 22:19:57 -04:00
7aa3cf82f1 Diva front end (#150)
Basic diva frontend implemented. Changing name and level string working. And some basic playlog page which needs more data

Co-authored-by: = <=>
Reviewed-on: Hay1tsme/artemis#150
Co-authored-by: ThatzOkay <thatzokay@noreply.gitea.tendokyu.moe>
Co-committed-by: ThatzOkay <thatzokay@noreply.gitea.tendokyu.moe>
2024-06-22 02:03:02 +00:00
3741c286f8 Merge pull request '[chunithm] Support LUMINOUS' (#154) from beerpsi/artemis:feat/chunithm/luminous into develop
Reviewed-on: Hay1tsme/artemis#154
2024-06-20 18:37:12 +00:00
a5b47e2095 wacca: fix vs song updates 2024-06-20 10:59:26 -04:00
a1d54efbac add upserts 2024-06-20 20:28:52 +07:00
cfece7593e Merge pull request 'ongeki: fix base version title work' (#153) from zaphkito/artemis:develop into develop
Reviewed-on: Hay1tsme/artemis#153
2024-06-20 12:19:38 +00:00
5378655c52 [chunithm] Support LUMINOUS 2024-06-20 08:11:24 +07:00
51f65f9293 ongeki: I forgot json load 2024-06-18 04:15:40 +00:00
766912c51d ongeki: fix base version title work 2024-06-18 03:13:32 +00:00
68bf3843ec pokken: fix default title plate id 2024-06-16 14:45:41 -04:00
784ac544f0 fix event log ordering, paging, add timestamp 2024-06-16 14:42:23 -04:00
00224585bb pokken: fix pokemon_data uk 2024-06-14 00:06:39 -04:00
bf54969bc1 pokken: fix some image links 2024-06-13 18:15:38 -04:00
a523c25d84 pokken: finish filling out support list 2024-06-13 18:06:16 -04:00
ee7d5c4e21 pokken: readd mysql_charset to profile and pokemon data 2024-06-13 14:14:07 -04:00
e21568cfd9 pokken: fix profile loading fail 2024-06-12 23:24:45 -04:00
37 changed files with 1949 additions and 89 deletions

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,54 @@
"""pokken_fix_pokemon_uk
Revision ID: 3657efefc5a4
Revises: 4a02e623e5e6
Create Date: 2024-06-13 23:50:57.611998
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '3657efefc5a4'
down_revision = '4a02e623e5e6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('pokken_pokemon_data', 'char_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
op.alter_column('pokken_pokemon_data', 'illustration_book_no',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
op.drop_constraint('pokken_pokemon_data_ibfk_1', table_name='pokken_pokemon_data', type_='foreignkey')
op.drop_index('pokken_pokemon_data_uk', table_name='pokken_pokemon_data')
op.create_unique_constraint('pokken_pokemon_uk', 'pokken_pokemon_data', ['user', 'illustration_book_no'])
op.create_foreign_key("pokken_pokemon_data_ibfk_1", "pokken_pokemon_data", "aime_user", ['user'], ['id'])
op.alter_column('pokken_profile', 'trainer_name',
existing_type=mysql.VARCHAR(length=16),
type_=sa.String(length=14),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('pokken_profile', 'trainer_name',
existing_type=sa.String(length=14),
type_=mysql.VARCHAR(length=16),
existing_nullable=True)
op.drop_constraint('pokken_pokemon_data_ibfk_1', table_name='pokken_pokemon_data', type_='foreignkey')
op.drop_constraint('pokken_pokemon_uk', 'pokken_pokemon_data', type_='unique')
op.create_index('pokken_pokemon_data_uk', 'pokken_pokemon_data', ['user', 'char_id'], unique=True)
op.create_foreign_key("pokken_pokemon_data_ibfk_1", "pokken_pokemon_data", "aime_user", ['user'], ['id'])
op.alter_column('pokken_pokemon_data', 'illustration_book_no',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
op.alter_column('pokken_pokemon_data', 'char_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
# ### end Alembic commands ###

View File

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

View File

@ -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")

View File

@ -109,7 +109,7 @@ class BaseData:
return result.lastrowid
async def get_event_log(self, entries: int = 100) -> Optional[List[Row]]:
sql = event_log.select().limit(entries)
sql = event_log.select().order_by(event_log.c.id.desc()).limit(entries)
result = await self.execute(sql)
if result is None:

View File

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

View File

@ -6,6 +6,7 @@
<thead>
<tr>
<th>Severity</th>
<th>Timestamp</th>
<th>System</th>
<th>Name</th>
<th>User</th>
@ -19,7 +20,7 @@
</thead>
{% if events is not defined or events|length == 0 %}
<tr>
<td colspan="10" style="text-align:center"><i>No Events</i></td>
<td colspan="11" style="text-align:center"><i>No Events</i></td>
</tr>
{% endif %}
</table>
@ -66,7 +67,11 @@ function update_tbl() {
for (var i = 0; i < per_page; i++) {
let off = (page * per_page) + i;
if (off >= TBL_DATA.length ) {
if (off >= TBL_DATA.length) {
if (page != 0) {
document.getElementById("btn_next").disabled = true;
document.getElementById("btn_prev").disabled = false;
}
break;
}
@ -99,56 +104,59 @@ function update_tbl() {
row.classList.add("table-primary");
break;
}
var cell_ts = row.insertCell(1);
cell_ts.innerHTML = data.when_logged;
var cell_mod = row.insertCell(1);
var cell_mod = row.insertCell(2);
cell_mod.innerHTML = data.system;
var cell_name = row.insertCell(2);
var cell_name = row.insertCell(3);
cell_name.innerHTML = data.type;
var cell_usr = row.insertCell(3);
var cell_usr = row.insertCell(4);
if (data.user == 'NONE') {
cell_usr.innerHTML = "---";
} else {
cell_usr.innerHTML = "<a href=\"/user/" + data.user + "\">" + data.user + "</a>";
}
var cell_arcade = row.insertCell(4);
var cell_arcade = row.insertCell(5);
if (data.arcade == 'NONE') {
cell_arcade.innerHTML = "---";
} else {
cell_arcade.innerHTML = "<a href=\"/shop/" + data.arcade + "\">" + data.arcade + "</a>";
}
var cell_machine = row.insertCell(5);
var cell_machine = row.insertCell(6);
if (data.arcade == 'NONE') {
cell_machine.innerHTML = "---";
} else {
cell_machine.innerHTML = "<a href=\"/cab/" + data.machine + "\">" + data.machine + "</a>";
}
var cell_game = row.insertCell(6);
var cell_game = row.insertCell(7);
if (data.game == 'NONE') {
cell_game.innerHTML = "---";
} else {
cell_game.innerHTML = data.game;
}
var cell_version = row.insertCell(7);
var cell_version = row.insertCell(8);
if (data.version == 'NONE') {
cell_version.innerHTML = "---";
} else {
cell_version.innerHTML = data.version;
}
var cell_msg = row.insertCell(8);
var cell_msg = row.insertCell(9);
if (data.message == '') {
cell_msg.innerHTML = "---";
} else {
cell_msg.innerHTML = data.message;
}
var cell_deets = row.insertCell(9);
var cell_deets = row.insertCell(10);
if (data.details == '{}') {
cell_deets.innerHTML = "---";
} else {
@ -160,9 +168,11 @@ function update_tbl() {
function chg_page(num) {
var max_page = TBL_DATA.length / per_page;
console.log(max_page);
page = page + num;
if (page > max_page) {
if (page > max_page && max_page >= 1) {
page = max_page;
document.getElementById("btn_next").disabled = true;
document.getElementById("btn_prev").disabled = false;
@ -172,6 +182,12 @@ function chg_page(num) {
document.getElementById("btn_next").disabled = false;
document.getElementById("btn_prev").disabled = true;
return;
} else if (page == 0) {
document.getElementById("btn_next").disabled = false;
document.getElementById("btn_prev").disabled = true;
} else {
document.getElementById("btn_next").disabled = false;
document.getElementById("btn_prev").disabled = false;
}
update_tbl();

View File

@ -49,7 +49,7 @@ class Utils:
def get_title_port_ssl(cls, cfg: CoreConfig):
if cls.real_title_port_ssl is not None: return cls.real_title_port_ssl
cls.real_title_port_ssl = cfg.server.proxy_port_ssl if cfg.server.is_using_proxy and cfg.server.proxy_port_ssl else Utils.get_title_port(cfg)
cls.real_title_port_ssl = cfg.server.proxy_port_ssl if cfg.server.is_using_proxy and cfg.server.proxy_port_ssl else 443
return cls.real_title_port_ssl

View File

@ -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

View File

@ -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

View File

@ -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": []},
}
}

View File

@ -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]
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

View File

@ -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))
return Response(crypt.encrypt(padded))

298
titles/chuni/luminous.py Normal file
View 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,
}

View File

@ -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

View File

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

View File

@ -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()

View File

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

View File

@ -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%",

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)

View File

@ -165,6 +165,8 @@ class PokkenBase:
f"Register new card {access_code} (UserId {user_id}, CardId {card_id})"
)
await self.data.card.set_chip_id_by_access_code(access_code, int(request.load_user.chip_id[:8], 16))
elif card is None:
self.logger.info(f"Registration of card {access_code} blocked!")
res.load_user.CopyFrom(load_usr)
@ -173,6 +175,8 @@ class PokkenBase:
else:
user_id = card['user']
card_id = card['id']
if not card['chip_id']:
await self.data.card.set_chip_id_by_access_code(access_code, int(request.load_user.chip_id[:8], 16))
"""
TODO: Unlock all supports? Probably
@ -226,7 +230,7 @@ class PokkenBase:
load_usr.rankmatch_success = profile_dict.get("rankmatch_success", 0)
load_usr.beat_num = profile_dict.get("beat_num", 0)
load_usr.title_text_id = profile_dict.get("title_text_id", 2)
load_usr.title_plate_id = profile_dict.get("title_plate_id", 1)
load_usr.title_plate_id = profile_dict.get("title_plate_id", 31)
load_usr.title_decoration_id = profile_dict.get("title_decoration_id", 1)
load_usr.navi_trainer = profile_dict.get("navi_trainer", 0)
load_usr.navi_version_id = profile_dict.get("navi_version_id", 0)
@ -305,12 +309,12 @@ class PokkenBase:
for x in profile_dict.get("event_achievement_param", []):
load_usr.event_achievement_param.append(x)
load_usr.support_set_1.append(profile_dict.get("support_set_1_1", 4294967295))
load_usr.support_set_1.append(profile_dict.get("support_set_1_2", 4294967295))
load_usr.support_set_2.append(profile_dict.get("support_set_2_1", 4294967295))
load_usr.support_set_2.append(profile_dict.get("support_set_2_2", 4294967295))
load_usr.support_set_3.append(profile_dict.get("support_set_3_1", 4294967295))
load_usr.support_set_3.append(profile_dict.get("support_set_3_2", 4294967295))
load_usr.support_set_1.append(profile_dict.get("support_set_1_1", 587))
load_usr.support_set_1.append(profile_dict.get("support_set_1_2", 653))
load_usr.support_set_2.append(profile_dict.get("support_set_2_1", 495))
load_usr.support_set_2.append(profile_dict.get("support_set_2_2", 131))
load_usr.support_set_3.append(profile_dict.get("support_set_3_1", 657))
load_usr.support_set_3.append(profile_dict.get("support_set_3_2", 133))
res.load_user.CopyFrom(load_usr)
return res.SerializeToString()
@ -385,7 +389,7 @@ class PokkenBase:
await self.data.profile.add_profile_points(user_id, get_rank_pts, get_money, get_score_pts, grade_max)
# Inconsistant underscore use AND a typo??
await self.data.profile.update_rankmatch_data(user_id, req.rankmatch_flag, req.rank_match_max, req.rank_match_success, req.rank_match_process)
#await self.data.profile.update_rankmatch_data(user_id, req.rankmatch_flag, req.rank_match_max, req.rank_match_success, req.rank_match_process)
await self.data.profile.update_support_team(user_id, 1, req.support_set_1[0], req.support_set_1[1])
await self.data.profile.update_support_team(user_id, 2, req.support_set_2[0], req.support_set_2[1])

View File

@ -0,0 +1,140 @@
{
"448": {
"name_en": "Lucario",
"name_jp": "ルカリオ",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/448.png"
},
"25": {
"name_en": "Pikachu",
"name_jp": "ピカチュウ",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/25.png"
},
"68": {
"name_en": "Machamp",
"name_jp": "カイリキー",
"type": 1,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/68.png"
},
"282": {
"name_en": "Gardevoir",
"name_jp": "サーナイト",
"type": 2,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/282.png"
},
"461": {
"name_en": "Weavile",
"name_jp": "マニューラ",
"type": 3,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/461.png"
},
"245": {
"name_en": "Suicune",
"name_jp": "スイクン",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/245.png"
},
"6": {
"name_en": "Charizard",
"name_jp": "リザードン",
"type": 1,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/6.png"
},
"94": {
"name_en": "Gengar",
"name_jp": "ゲンガー",
"type": 2,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/94.png"
},
"257": {
"name_en": "Blaziken",
"name_jp": "バシャーモ",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/257.png"
},
"10025": {
"name_en": "Pikachu Libre",
"name_jp": "マスクド・ピカチュウ",
"type": 3,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/10084.png"
},
"254": {
"name_en": "Sceptile",
"name_jp": "ジュカイン",
"type": 3,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/254.png"
},
"609": {
"name_en": "Chandelure",
"name_jp": "シャンデラ",
"type": 1,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/609.png"
},
"150": {
"name_en": "Mewtwo",
"name_jp": "ミュウツー",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/150.png"
},
"10150": {
"name_en": "Shadow Mewtwo",
"name_jp": "ダークミュウツー",
"type": 2,
"artwork": "https://archives.bulbagarden.net/media/upload/7/7a/Pokk%C3%A9n_Shadow_Mewtwo.png"
},
"445": {
"name_en": "Garchomp",
"name_jp": "ガブリアス",
"type": 1,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/445.png"
},
"654": {
"name_en": "Braixen",
"name_jp": "テールナー",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/654.png"
},
"491": {
"name_en": "Darkrai",
"name_jp": "ダークライ",
"type": 2,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/491.png"
},
"212": {
"name_en": "Scizor",
"name_jp": "ハッサム",
"type": 1,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/212.png"
},
"453": {
"name_en": "Croagunk",
"name_jp": "グレッグル",
"type": 3,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/453.png"
},
"395": {
"name_en": "Empoleon",
"name_jp": "エンペルト",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/395.png"
},
"724": {
"name_en": "Decidueye",
"name_jp": "ジュナイパー",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/724.png"
},
"681": {
"name_en": "Aegislash",
"name_jp": "ギルガルド",
"type": 2,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/681.png"
},
"9": {
"name_en": "Blastoise",
"name_jp": "カメックス",
"type": 1,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/9.png"
}
}

View File

@ -0,0 +1,218 @@
{
"587": {
"name_en": "Emolga",
"name_jp": "エモンガ",
"desc": "Uses Shock Wave to shock the opponent and temporarily decrease its speed.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/587.png"
},
"653": {
"name_en": "Fennekin",
"name_jp": "フォッコ",
"desc": "Uses Ember to surround itself with fire, creating a trap.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/653.png"
},
"495": {
"name_en": "Snivy",
"name_jp": "ツタージャ",
"desc": "Uses Leaf Tornado to perform an anti-air attack and send the opponent flying.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/495.png"
},
"131": {
"name_en": "Lapras",
"name_jp": "ラプラス",
"desc": "Uses Surf as it enters the stage, damaging the enemy with a wave of water.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/131.png"
},
"657": {
"name_en": "Frogadier",
"name_jp": "ゲコガシラ",
"desc": "Uses Water Pulse to attack from a distance by firing water bullets. Effective when striking from long distance.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/657.png"
},
"133": {
"name_en": "Eevee",
"name_jp": "イーブイ",
"desc": "Uses Helping Hand to heal the user and temporarily increase their attack power.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/133.png"
},
"385": {
"name_en": "Jirachi",
"name_jp": "ジラーチ",
"desc": "Uses Wish to restore the Synergy Gauge and temporarily strengthen the user's attack power during Synergy Burst.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/385.png"
},
"547": {
"name_en": "Whimsicott",
"name_jp": "エルフーン",
"desc": "Uses Substitute to render attacks from opponents useless and heal the user.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/547.png"
},
"38": {
"name_en": "Ninetales",
"name_jp": "キュウコン",
"desc": "Uses Will-O-Wisp to send small flames in front of the user. Enemy's attack power decreased temporarily when contacted.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/38.png"
},
"429": {
"name_en": "Mismagius",
"name_jp": "ムウマージ",
"desc": "Uses Ominous Wind to attack the opponent and temporarily increase the user's attack power.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/429.png"
},
"83": {
"name_en": "Farfetch'd",
"name_jp": "カモネギ",
"desc": "Uses Fury Cutter to perform a flurry of attacks toward the opponent.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/83.png"
},
"101": {
"name_en": "Electrode",
"name_jp": "マルマイン",
"desc": "Uses Explosion to counter an opponent's attack upon defending.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/101.png"
},
"479": {
"name_en": "Rotom",
"name_jp": "ロトム",
"desc": "Uses Thunder Shock to target enemies in the air and temporarily decrease their speed.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/479.png"
},
"468": {
"name_en": "Togekiss",
"name_jp": "トゲキッス",
"desc": "Uses Tailwind to temporarily increase the user's speed and recover some health.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/468.png"
},
"149": {
"name_en": "Dragonite",
"name_jp": "カイリュー",
"desc": "Uses Draco Meteor to attack multiple times over a wide area.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/149.png"
},
"494": {
"name_en": "Victini",
"name_jp": "ビクティニ",
"desc": "Uses V-create to temporarily make the user's attacks critical hits, restores some of the user's health, and increases the user's Synergy Gauge. Unlike other Enhance Pokémon, Victini can actually damage the foe if they're above it when flying off the screen.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/494.png"
},
"453": {
"name_en": "Croagunk",
"name_jp": "グレッグル",
"desc": "Uses Toxic to attack opponent and temporarily decrease its defense.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/453.png"
},
"700": {
"name_en": "Sylveon",
"name_jp": "ニンフィア",
"desc": "Uses Reflect to heal user and temporarily increase their defense.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/700.png"
},
"417": {
"name_en": "Pachirisu",
"name_jp": "パチリス",
"desc": "Uses Follow Me to eliminate long distance attacks. Effective when get in close.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/417.png"
},
"129": {
"name_en": "Magikarp",
"name_jp": "コイキング",
"desc": "Uses Bounce to disrupt the enemy's attack when hit by an opponent. Effective for interrupting combos.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/129.png"
},
"104": {
"name_en": "Cubone",
"name_jp": "カラカラ",
"desc": "Uses Bonemerang to attack from a distance and can pull an enemy in.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/104.png"
},
"50": {
"name_en": "Diglett",
"name_jp": "ディグダ",
"desc": "Uses Dig to attack from below, making easy to aim for a combo.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/50.png"
},
"82": {
"name_en": "Magneton",
"name_jp": "レアコイル",
"desc": "Uses Tri Attack to attack from a distance diagonally upward and inflict two random negative statuses.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/82.png"
},
"195": {
"name_en": "Quagsire",
"name_jp": "ヌオー",
"desc": "Uses Mud Bomb to attack opponent on the ground, even when blocked.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/195.png"
},
"196": {
"name_en": "Espeon",
"name_jp": "エーフィ",
"desc": "Uses Morning Sun to remove any statuses and recover health, with more health recovered with less time remaining in the round.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/196.png"
},
"197": {
"name_en": "Umbreon",
"name_jp": "ブラッキー",
"desc": "Uses Snarl to absorb an opponent's Synergy Gauge and prevent them from performing any critical hits.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/197.png"
},
"643": {
"name_en": "Reshiram",
"name_jp": "レシラム",
"desc": "Uses Blue Flare to attack straight forward with a powerful flame. In the DX version, it can only be called once per round.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/643.png"
},
"488": {
"name_en": "Cresselia",
"name_jp": "クレセリア",
"desc": "Uses Lunar Dance to heal the user of any negative status, recovers health and Synergy Gauge, but can only be used once per round.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/488.png"
},
"717": {
"name_en": "Yveltal",
"name_jp": "イベルタル",
"desc": "Uses Oblivion Wing to attack from the sky and seal off the opponent's Synergy Burst. In the DX version, it can only be called once per round.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/717.png"
},
"381": {
"name_en": "Latios",
"name_jp": "ラティオス",
"desc": "Uses Luster Purge to place attacks around the enemy in order to restrict their movements. In the DX version, it can only be called once per round.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/381.png"
},
"725": {
"name_en": "Litten",
"name_jp": "ニャビー",
"desc": "Uses Fire Fang to attack toward the enemy. Damage increases when the player's at lower HP.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/725.png"
},
"728": {
"name_en": "Popplio",
"name_jp": "アシマリ",
"desc": "Uses Bubble Beam to temporarily increase attack and grant a double jump while in midair.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/728.png"
},
"10079": {
"name_en": "Mega Rayquaza",
"name_jp": "レックウザ",
"desc": "Uses Dragon Ascent to attack from a distance at tremendous speed. It also consumes the user's Synergy Gauge. It can only be called once per round.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/10079.png"
},
"778": {
"name_en": "Mimikyu",
"name_jp": "ミミッキュ",
"desc": "Uses Play Rough to attack continuously from behind and inflict double negative status.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/778.png"
},
"151": {
"name_en": "Mew",
"name_jp": "ミュウ",
"desc": "Uses Miraculous Power to randomly increase the user's Synergy Gauge, temporarily makes the user's attacks critical hits, and/or gives the user additional random positive status.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/151.png"
},
"251": {
"name_en": "Celebi",
"name_jp": "セレビィ",
"desc": "Uses Time Travel (Japanese: ときわたり Time Travel) to switch between Phases at almost any given moment, even when enemy guards an attack.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/251.png"
}
}

View File

@ -5,7 +5,7 @@ from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select, update, delete
from sqlalchemy.sql.functions import coalesce
from sqlalchemy.engine import Row
from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
from ..const import PokkenConstants
@ -61,7 +61,7 @@ profile = Table(
Column("navi_trainer", Integer),
Column("navi_version_id", Integer),
Column("aid_skill_list", JSON), # Repeated, Integer
Column("aid_skill", Integer),
Column("aid_skill", Integer), # Cheer skill, 6 of them, unlocked by lucky bonus
Column("comment_text_id", Integer),
Column("comment_word_id", Integer),
Column("latest_use_pokemon", Integer),
@ -100,10 +100,11 @@ profile = Table(
Column("battle_num_vs_cpu", Integer), # 2
Column("win_cpu", Integer),
Column("battle_num_tutorial", Integer), # 1?
mysql_charset="utf8mb4"
)
pokemon_data = Table(
"pokken_pokemon",
"pokken_pokemon_data",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
@ -123,6 +124,7 @@ pokemon_data = Table(
Column("bp_point_def", Integer),
Column("bp_point_sp", Integer),
UniqueConstraint("user", "illustration_book_no", name="pokken_pokemon_uk"),
mysql_charset="utf8mb4"
)
@ -295,7 +297,7 @@ class PokkenProfileData(BaseData):
self.logger.warning(f"Failed to add {xp} XP to pokemon ID {pokemon_id} for user {user_id}")
async def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]:
sql = pokemon_data.select(and_(pokemon_data.c.user == user_id, pokemon_data.c.char_id == pokemon_id))
sql = pokemon_data.select(and_(pokemon_data.c.user == user_id, pokemon_data.c.illustration_book_no == pokemon_id))
result = await self.execute(sql)
if result is None:
return None
@ -323,7 +325,7 @@ class PokkenProfileData(BaseData):
Records the match stats (type and win/loss) for the pokemon and profile
coalesce(pokemon_data.c.win_vs_wan, 0)
"""
sql = pokemon_data.update(and_(pokemon_data.c.user==user_id, pokemon_data.c.char_id==pokemon_id)).values(
sql = pokemon_data.update(and_(pokemon_data.c.user==user_id, pokemon_data.c.illustration_book_no==pokemon_id)).values(
battle_num_tutorial=coalesce(pokemon_data.c.battle_num_tutorial, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else coalesce(pokemon_data.c.battle_num_tutorial, 0),
battle_all_num_tutorial=coalesce(pokemon_data.c.battle_all_num_tutorial, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else coalesce(pokemon_data.c.battle_all_num_tutorial, 0),

View File

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

View File

@ -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)