119 Commits

Author SHA1 Message Date
Dniel97
6a41dac46c ongeki: card maker config added, small fixes, improved credits
- Changed the credits config to the default 370 instead of 360
- Added `start_date` to the events to show new events
- Fixed Card Maker Gachas to only allow "Select Gacha" once
- Fixed the `get_profile_rating_log` database query
2023-07-12 11:25:46 +02:00
Dniel97
36d338e618 Merge remote-tracking branch 'origin/develop' into fork_develop 2023-07-08 17:38:41 +02:00
Kevin Trocolli
03cf535ff6 make threading optional 2023-07-08 00:34:55 -04:00
Kevin Trocolli
6c155a5e48 database: add static variables to prevent having multiple sessions 2023-07-08 00:01:52 -04:00
Midorica
737312ca3d Threading support to main twisted reactor 2023-07-07 21:50:24 -04:00
Hay1tsme
1edec7dba2 sao: add response debug logging 2023-07-05 12:35:00 -04:00
Hay1tsme
d60f827000 fix typing across multiple games, fixes #23 2023-07-05 10:47:43 -04:00
Midorica
da422e602b fixing trial_tower_play_end_unanalyzed_log_fixed for SAO 2023-07-02 15:50:42 -04:00
Midorica
d276ac8598 Merge branch 'develop' of https://gitea.tendokyu.moe/Hay1tsme/artemis into develop 2023-07-02 14:25:36 -04:00
Midorica
84e880e94f fixing unanalyzed reward request for SAO 2023-07-02 14:25:24 -04:00
Kevin Trocolli
432177957a pokken: save most profile data 2023-07-02 02:42:49 -04:00
Kevin Trocolli
a89247cdd6 wacca: add note about VIP rewards 2023-07-02 02:33:45 -04:00
Kevin Trocolli
f279adb894 mai2: add consecutive day login count, update db to v7, fix reader, courses, and docs 2023-07-01 21:51:18 -04:00
Kevin Trocolli
a680699939 Merge branch 'finale' into develop 2023-07-01 02:59:12 -04:00
Kevin Trocolli
d204954447 mai2: add missing finale endpoints 2023-07-01 02:40:07 -04:00
Kevin Trocolli
042440c76e mai2: fix handle_get_user_preview_api_request 2023-07-01 02:27:26 -04:00
Kevin Trocolli
c4c0566cd5 mai2: fix userWebOption 2023-07-01 02:19:19 -04:00
Kevin Trocolli
3e9cec3a20 mai2: put_recent_rating final fix 2023-07-01 02:11:37 -04:00
Kevin Trocolli
8f9584c3d2 mai2: hotfix put_recent_rating 2023-07-01 02:07:19 -04:00
Kevin Trocolli
b29cb0fbaa mai2: fix put_recent_rating 2023-07-01 02:06:00 -04:00
Kevin Trocolli
d9a92f5865 mai2: 4th round of fixes 2023-07-01 02:04:30 -04:00
Kevin Trocolli
9859ab4fdb mai2: fix playlog saving 2023-07-01 01:59:19 -04:00
Kevin Trocolli
d89eb61e62 mai2: fixes round 3 2023-07-01 01:56:52 -04:00
Kevin Trocolli
dc8c27046e mai2: more finale fixes 2023-07-01 01:42:38 -04:00
Kevin Trocolli
3e461f4d71 mai2: finale fixes 2023-07-01 01:41:34 -04:00
Kevin Trocolli
2c6902a546 mai2: fix typos 2023-07-01 01:12:15 -04:00
Kevin Trocolli
318b73dd57 finale: finish porting request data from aqua 2023-07-01 01:08:54 -04:00
Kevin Trocolli
9d33091bb8 allnet: use parse_qsl 2023-06-30 01:34:46 -04:00
Kevin Trocolli
8b43d554fc allnet: make use of urllib.parse where applicable 2023-06-30 01:19:17 -04:00
Kevin Trocolli
610ef70bad allnet: add Alive get and post handlers 2023-06-30 00:32:52 -04:00
Kevin Trocolli
60b3bc7750 Merge branch 'develop' of https://gitea.tendokyu.moe/Hay1tsme/artemis into develop 2023-06-30 00:26:10 -04:00
Kevin Trocolli
4ea83f6025 allnet: add handler for LoaderStateRecorder 2023-06-30 00:26:07 -04:00
Midorica
20389011e9 Adding proper hero unlock after stage clear on SAO 2023-06-28 12:54:16 -04:00
Midorica
e446816b9a fixing issue where SaoItemData was not working 2023-06-28 08:24:53 -04:00
Midorica
9dd2b4d524 Adding dummy hero QR code scanning for SAO 2023-06-28 00:18:02 -04:00
Midorica
b60cf6258d Dummy defrag match handler for SAO 2023-06-27 21:32:46 -04:00
Kevin Trocolli
127e6f8aa8 mai2: add finale databases 2023-06-27 00:32:35 -04:00
Midorica
5155353360 fixing chapter progression after chapter 2 on SAO 2023-06-26 19:30:03 -04:00
Kevin Trocolli
e3d38dacde mai2: fix movies 2023-06-25 19:10:34 -04:00
Kevin Trocolli
0c6d9a36ce mai2: add movie server endpoints 2023-06-25 18:43:00 -04:00
Kevin Trocolli
b1968fe320 Merge branch 'develop' into finale 2023-06-25 18:35:12 -04:00
Midorica
03f91d18c9 fixing hero party saving for SAO 2023-06-25 14:40:34 -04:00
Midorica
17508f09b2 fixed episode VP saving & hero level in DB for SAO 2023-06-25 13:47:31 -04:00
Midorica
aae4afe7b8 Adding debug logging to SAO 2023-06-25 11:59:17 -04:00
Kevin Trocolli
514f786e2d pokken: Switch to using external STUN server 2023-06-25 01:09:23 -04:00
Midorica
ec9ad1ebb0 fixing stage progression for SAO 2023-06-25 00:08:50 -04:00
Midorica
08ebb5c907 another quick fix for SAO tower stage 2023-06-24 20:42:00 -04:00
Midorica
571b92d0cd forgot one line, see previous commit 2023-06-24 20:33:30 -04:00
Midorica
01b5282899 small fix about next tower stage progression for SAO 2023-06-24 20:31:14 -04:00
Midorica
391edd3354 Tower progression now working for SAO 2023-06-24 20:09:37 -04:00
Midorica
d5bff0e891 Stage progression done for SAO 2023-06-24 18:48:48 -04:00
Kevin Trocolli
402e753469 wacca: fix tabbing error in util_put_items 2023-06-24 15:09:38 -04:00
Kevin Trocolli
154ccbdae5 Merge branch 'develop' of https://gitea.tendokyu.moe/Hay1tsme/artemis into develop 2023-06-24 13:14:42 -04:00
Kevin Trocolli
858b101a36 dbutils: add command to show versions 2023-06-24 13:14:40 -04:00
Midorica
1d10e798a5 Fixed few issues for SAO & removed static hex ranges 2023-06-24 12:29:28 -04:00
Kevin Trocolli
3c385f505b pokken: add requirement for autobahn, add stun, turn and admission servers 2023-06-23 00:30:25 -04:00
Kevin Trocolli
b12938bcd8 pokken: add partial profile save logic 2023-06-14 03:00:52 -04:00
Kevin Trocolli
1b2f5e3709 mai2: fix logic in handle_get_user_music_api_request 2023-06-13 22:50:57 -04:00
Kevin Trocolli
65686fb615 mai2: add loggin to handle_get_user_music_api_request 2023-06-13 22:35:09 -04:00
Kevin Trocolli
f56332141e mai2: fix old server (finale isn't ready yet) 2023-06-13 22:16:30 -04:00
Kevin Trocolli
5a35b1c823 mai2: GetUserMusicApi hotfix 2023-06-13 22:10:35 -04:00
Kevin Trocolli
5ca16f2067 mai2: fix GetUserMusicApi pagination 2023-06-13 22:07:48 -04:00
Midorica
a0b25e2b7b Adding rare drops saving to SAO 2023-06-03 11:42:50 -04:00
Midorica
84fc002cdb Adding trial tower support for SAO 2023-06-02 13:53:49 -04:00
Midorica
3bd03c592e Item progression and synthesize of hero and equipment done 2023-06-01 13:19:48 -04:00
Midorica
cf6cfdbd3b adding partial synthetize system for SAO 2023-05-31 21:58:30 -04:00
Kevin Trocolli
db77e61b79 allnet: add event logging 2023-05-30 21:52:21 -04:00
Kevin Trocolli
ac9e71ee2f hotfix allnet logging 2023-05-30 21:46:26 -04:00
Kevin Trocolli
20865dc495 allnet: add logging 2023-05-30 21:45:37 -04:00
Kevin Trocolli
37d24b3b4d mucha: now respects log level set in core.yaml 2023-05-30 21:32:27 -04:00
Kevin Trocolli
2418abacce title: convert version to int to match POST endpoint 2023-05-30 21:31:09 -04:00
Kevin Trocolli
5c3f812caf cxb: fix missing parameters on render_POST 2023-05-30 21:30:34 -04:00
Midorica
4854bcfcad Merge pull request 'Added individual Card Maker versions and maimai DX card/passes working' (#21) from Dniel97/artemis:cardmaker_maimai into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/21
2023-05-30 23:59:44 +00:00
Midorica
bf6d126f8a Equipments saving for SAO now completed 2023-05-30 18:03:52 -04:00
Midorica
e466ddce55 Adding SAO rewards saving for heroes 2023-05-30 14:29:50 -04:00
Dniel97
ad820ed091 cm: Added individual Card Maker version and maimai DX passes working 2023-05-30 12:14:18 +02:00
Dniel97
960a0e3fd9 Merge branch 'develop' into fork_develop 2023-05-30 12:08:36 +02:00
Midorica
a2fe11d654 Fixing level calculation saving & loading on SAO 2023-05-29 20:57:02 -04:00
Midorica
2b4ac06389 adding more profile & hero saving stuff to SAO 2023-05-29 19:21:26 -04:00
Midorica
d8af7be4a4 Adding SAO item table and adding party saving 2023-05-29 16:51:41 -04:00
Midorica
84cb786bde Adding some structs for SAO for later use 2023-05-29 11:10:32 -04:00
Kevin Trocolli
05dee87a9a allnet: update default values, add debug log for unknown but allowed auths 2023-05-26 21:41:16 -04:00
Midorica
049dc40a8b small typo of the documentation 2023-05-26 14:04:49 -04:00
Midorica
cab1d6814a added game specifics for SAO 2023-05-26 13:57:16 -04:00
Midorica
72594fef31 adding partial Sword Art Online Arcade support 2023-05-26 13:45:20 -04:00
Kevin Trocolli
7ed294e9f7 delivery: remove period from version 2023-05-24 01:08:53 -04:00
Hay1tsme
b9fd4f294d wacca: fix type mismatch in user/music/unlock 2023-05-22 12:33:43 -04:00
Hay1tsme
5ddfb88182 wacca: fix user/music/unlock error when using tickets. 2023-05-22 12:24:16 -04:00
Hay1tsme
4da8622977 frontend: user page fixes, add card display 2023-05-20 15:32:02 -04:00
Kevin Trocolli
97892d6a7d idz: try-catch for userdb request decryption 2023-05-18 21:20:28 -04:00
Kevin Trocolli
02078080a8 index: additional logging for malformed return data 2023-05-12 22:12:03 -04:00
Kevin Trocolli
61e3a2c930 index: remove hanging debug log call 2023-05-12 22:06:19 -04:00
Kevin Trocolli
8ae0aba89c mai2: update default config 2023-05-12 22:05:05 -04:00
Kevin Trocolli
49166c1a7b mai2: fix handle_get_game_setting_api_request 2023-05-11 09:52:18 -04:00
Midorica
013e83420b Merge pull request 'CHUNITHM SUN support (with basic matchmaking)' (#20) from Dniel97/artemis:chuni_sun into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/20
2023-05-11 13:44:30 +00:00
Dniel97
0dce7e7849 docs: fixed opt typo 2023-05-11 15:33:29 +02:00
Raymonf
f959236af0 SUN encryption support 2023-05-10 17:20:13 -04:00
Dniel97
b85a65204f chuni: added SUN support, matchmaking, fixed bugs, added docs
- Added CHUNITHM SUN support
- Added first matchmaking support with CPU spawning and messages
- Fixed wrong `next_idx` calculations
- Added `startDate` to events to spawn the correct items
- Fixed login bonus per version
- Added information to docs
2023-05-10 21:32:35 +02:00
Dniel97
0ab539173a Merge branch 'develop' into fork_develop 2023-05-10 12:49:01 +02:00
Kevin Trocolli
42ed222095 mai2: add gamesetting urls 2023-05-10 02:31:30 -04:00
Kevin Trocolli
d172e5582b fixup allnet response for res class 2 2023-05-09 03:53:31 -04:00
Kevin Trocolli
9766e3ab78 mai2: hardcode reboot time 2023-05-07 02:16:50 -04:00
Kevin Trocolli
b34b441ba8 mai2: reimplement pre-dx versions 2023-05-06 19:04:10 -04:00
Kevin Trocolli
8149f09a40 mai2: stub music reader 2023-05-05 00:37:05 -04:00
Kevin Trocolli
cad523dfce mai2: add patch reader 2023-05-05 00:36:07 -04:00
Kevin Trocolli
8b9771b5af mai2: implement event reader for pre-dx games 2023-05-05 00:24:47 -04:00
Kevin Trocolli
989c080657 mai2: further documentation clarification 2023-05-04 20:25:14 -04:00
Kevin Trocolli
dcff8adbab mai2: update documentation 2023-05-04 20:22:41 -04:00
Kevin Trocolli
e3b1addce6 mai2: fix up version comments 2023-05-04 20:12:31 -04:00
Kevin Trocolli
b6f43d887a Merge branch 'develop' into finale 2023-05-04 20:12:02 -04:00
Kevin Trocolli
efd8f86e48 re-add docker files for #19 2023-05-04 09:46:16 -04:00
Kevin Trocolli
d0242b456d mai2: fix for dx 1.00 2023-05-03 22:29:08 -04:00
Kevin Trocolli
7bb8c2c80c billing: handle malformed requests 2023-05-03 03:26:39 -04:00
Kevin Trocolli
6d1855a6bc billing: handle malformed requests 2023-05-03 03:25:55 -04:00
Kevin Trocolli
8d94d25893 mai2: add version seperators 2023-05-03 03:25:29 -04:00
Kevin Trocolli
ae6dcb68df Merge branch 'develop' into finale 2023-05-02 23:56:10 -04:00
Kevin Trocolli
3b6fc6618c fix naomitest 2023-05-02 23:55:57 -04:00
Kevin Trocolli
deeac1d8db add finale handler, pre-dx game codes 2023-04-30 22:19:31 -04:00
Midorica
6ad5194bb8 Merge pull request 'Project Diva Arcade: Added Clear Status calculation + small improvements' (#18) from Dniel97/artemis:diva_clear_set into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/18
2023-04-30 23:18:19 +00:00
109 changed files with 27117 additions and 691 deletions

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM python:3.9.15-slim-bullseye
RUN apt update && apt install default-libmysqlclient-dev build-essential libtk nodejs npm -y
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
RUN npm i -g nodemon
COPY entrypoint.sh entrypoint.sh
RUN chmod +x entrypoint.sh
COPY index.py index.py
COPY dbutils.py dbutils.py
ADD core core
ADD titles titles
ADD config config
ADD log log
ADD cert cert
ENTRYPOINT [ "/app/entrypoint.sh" ]

View File

@@ -1,4 +1,4 @@
from typing import Dict, List, Any, Optional, Tuple from typing import Dict, List, Any, Optional, Tuple, Union
import logging, coloredlogs import logging, coloredlogs
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from twisted.web.http import Request from twisted.web.http import Request
@@ -11,6 +11,7 @@ from Crypto.Hash import SHA
from Crypto.Signature import PKCS1_v1_5 from Crypto.Signature import PKCS1_v1_5
from time import strptime from time import strptime
from os import path from os import path
import urllib.parse
from core.config import CoreConfig from core.config import CoreConfig
from core.utils import Utils from core.utils import Utils
@@ -79,7 +80,7 @@ class AllnetServlet:
req = AllnetPowerOnRequest(req_dict[0]) req = AllnetPowerOnRequest(req_dict[0])
# Validate the request. Currently we only validate the fields we plan on using # Validate the request. Currently we only validate the fields we plan on using
if not req.game_id or not req.ver or not req.serial or not req.ip: if not req.game_id or not req.ver or not req.serial or not req.ip or not req.firm_ver or not req.boot_ver:
raise AllnetRequestException( raise AllnetRequestException(
f"Bad auth request params from {request_ip} - {vars(req)}" f"Bad auth request params from {request_ip} - {vars(req)}"
) )
@@ -89,10 +90,12 @@ class AllnetServlet:
self.logger.error(e) self.logger.error(e)
return b"" return b""
if req.format_ver == "3": if req.format_ver == 3:
resp = AllnetPowerOnResponse3(req.token) resp = AllnetPowerOnResponse3(req.token)
else: elif req.format_ver == 2:
resp = AllnetPowerOnResponse2() resp = AllnetPowerOnResponse2()
else:
resp = AllnetPowerOnResponse()
self.logger.debug(f"Allnet request: {vars(req)}") self.logger.debug(f"Allnet request: {vars(req)}")
if req.game_id not in self.uri_registry: if req.game_id not in self.uri_registry:
@@ -103,8 +106,9 @@ class AllnetServlet:
) )
self.logger.warn(msg) self.logger.warn(msg)
resp.stat = 0 resp.stat = -1
return self.dict_to_http_form_string([vars(resp)]) resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
else: else:
self.logger.info( self.logger.info(
@@ -112,7 +116,12 @@ class AllnetServlet:
) )
resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/" resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/"
resp.host = f"{self.config.title.hostname}:{self.config.title.port}" resp.host = f"{self.config.title.hostname}:{self.config.title.port}"
return self.dict_to_http_form_string([vars(resp)])
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict))
self.logger.debug(f"Allnet response: {resp_str}")
return (resp_str + "\n").encode("utf-8")
resp.uri, resp.host = self.uri_registry[req.game_id] resp.uri, resp.host = self.uri_registry[req.game_id]
@@ -124,8 +133,9 @@ class AllnetServlet:
) )
self.logger.warn(msg) self.logger.warn(msg)
resp.stat = 0 resp.stat = -2
return self.dict_to_http_form_string([vars(resp)]) resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
if machine is not None: if machine is not None:
arcade = self.data.arcade.get_arcade(machine["arcade"]) arcade = self.data.arcade.get_arcade(machine["arcade"])
@@ -167,9 +177,13 @@ class AllnetServlet:
msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}" msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}"
self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg) self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg)
self.logger.info(msg) self.logger.info(msg)
self.logger.debug(f"Allnet response: {vars(resp)}")
return self.dict_to_http_form_string([vars(resp)]).encode("utf-8") resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict))
self.logger.debug(f"Allnet response: {resp_dict}")
resp_str += "\n"
return resp_str.encode("utf-8")
def handle_dlorder(self, request: Request, _: Dict): def handle_dlorder(self, request: Request, _: Dict):
request_ip = Utils.get_ip_addr(request) request_ip = Utils.get_ip_addr(request)
@@ -194,27 +208,29 @@ class AllnetServlet:
self.logger.info( self.logger.info(
f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}" f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}"
) )
resp = AllnetDownloadOrderResponse() resp = AllnetDownloadOrderResponse(serial=req.serial)
if ( if (
not self.config.allnet.allow_online_updates not self.config.allnet.allow_online_updates
or not self.config.allnet.update_cfg_folder or not self.config.allnet.update_cfg_folder
): ):
return self.dict_to_http_form_string([vars(resp)]) return urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n"
else: # TODO: Keychip check else: # TODO: Keychip check
if path.exists( if path.exists(
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-app.ini" f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
): ):
resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini" resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
if path.exists( if path.exists(
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-opt.ini" f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
): ):
resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
self.logger.debug(f"Sending download uri {resp.uri}") self.logger.debug(f"Sending download uri {resp.uri}")
return self.dict_to_http_form_string([vars(resp)]) self.data.base.log_event("allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}")
return urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n"
def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes: def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes:
if "file" not in match: if "file" not in match:
@@ -223,6 +239,8 @@ class AllnetServlet:
req_file = match["file"].replace("%0A", "") req_file = match["file"].replace("%0A", "")
if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"): if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"):
self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful")
self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}")
return open( return open(
f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb" f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb"
).read() ).read()
@@ -236,6 +254,27 @@ class AllnetServlet:
) )
return b"" return b""
def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes:
req_data = request.content.getvalue()
sections = req_data.decode("utf-8").split("\r\n")
req_dict = dict(urllib.parse.parse_qsl(sections[0]))
serial: Union[str, None] = req_dict.get("serial", None)
num_files_to_dl: Union[str, None] = req_dict.get("nb_ftd", None)
num_files_dld: Union[str, None] = req_dict.get("nb_dld", None)
dl_state: Union[str, None] = req_dict.get("dld_st", None)
ip = Utils.get_ip_addr(request)
if serial is None or num_files_dld is None or num_files_to_dl is None or dl_state is None:
return "NG".encode()
self.logger.info(f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})")
return "OK".encode()
def handle_alive(self, request: Request, match: Dict) -> bytes:
return "OK".encode()
def handle_billing_request(self, request: Request, _: Dict): def handle_billing_request(self, request: Request, _: Dict):
req_dict = self.billing_req_to_dict(request.content.getvalue()) req_dict = self.billing_req_to_dict(request.content.getvalue())
request_ip = Utils.get_ip_addr(request) request_ip = Utils.get_ip_addr(request)
@@ -249,6 +288,7 @@ class AllnetServlet:
signer = PKCS1_v1_5.new(rsa) signer = PKCS1_v1_5.new(rsa)
digest = SHA.new() digest = SHA.new()
try:
kc_playlimit = int(req_dict[0]["playlimit"]) kc_playlimit = int(req_dict[0]["playlimit"])
kc_nearfull = int(req_dict[0]["nearfull"]) kc_nearfull = int(req_dict[0]["nearfull"])
kc_billigtype = int(req_dict[0]["billingtype"]) kc_billigtype = int(req_dict[0]["billingtype"])
@@ -258,6 +298,9 @@ class AllnetServlet:
kc_date = strptime(req_dict[0]["date"], "%Y%m%d%H%M%S") kc_date = strptime(req_dict[0]["date"], "%Y%m%d%H%M%S")
kc_serial_bytes = kc_serial.encode() kc_serial_bytes = kc_serial.encode()
except KeyError as e:
return f"result=5&linelimit=&message={e} field is missing".encode()
machine = self.data.arcade.get_machine(kc_serial) machine = self.data.arcade.get_machine(kc_serial)
if machine is None and not self.config.server.allow_unregistered_serials: if machine is None and not self.config.server.allow_unregistered_serials:
msg = f"Unrecognised serial {kc_serial} attempted billing checkin from {request_ip} for game {kc_game}." msg = f"Unrecognised serial {kc_serial} attempted billing checkin from {request_ip} for game {kc_game}."
@@ -295,7 +338,7 @@ class AllnetServlet:
resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig) resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig)
resp_str = self.dict_to_http_form_string([vars(resp)], True) resp_str = self.dict_to_http_form_string([vars(resp)])
if resp_str is None: if resp_str is None:
self.logger.error(f"Failed to parse response {vars(resp)}") self.logger.error(f"Failed to parse response {vars(resp)}")
@@ -306,21 +349,6 @@ class AllnetServlet:
self.logger.info(f"Ping from {Utils.get_ip_addr(request)}") self.logger.info(f"Ping from {Utils.get_ip_addr(request)}")
return b"naomi ok" return b"naomi ok"
def kvp_to_dict(self, kvp: List[str]) -> List[Dict[str, Any]]:
ret: List[Dict[str, Any]] = []
for x in kvp:
items = x.split("&")
tmp = {}
for item in items:
kvp = item.split("=")
if len(kvp) == 2:
tmp[kvp[0]] = kvp[1]
ret.append(tmp)
return ret
def billing_req_to_dict(self, data: bytes): def billing_req_to_dict(self, data: bytes):
""" """
Parses an billing request string into a python dictionary Parses an billing request string into a python dictionary
@@ -330,7 +358,10 @@ class AllnetServlet:
unzipped = decomp.decompress(data) unzipped = decomp.decompress(data)
sections = unzipped.decode("ascii").split("\r\n") sections = unzipped.decode("ascii").split("\r\n")
return self.kvp_to_dict(sections) ret = []
for x in sections:
ret.append(dict(urllib.parse.parse_qsl(x)))
return ret
except Exception as e: except Exception as e:
self.logger.error(f"billing_req_to_dict: {e} while parsing {data}") self.logger.error(f"billing_req_to_dict: {e} while parsing {data}")
@@ -345,7 +376,10 @@ class AllnetServlet:
unzipped = zlib.decompress(zipped) unzipped = zlib.decompress(zipped)
sections = unzipped.decode("utf-8").split("\r\n") sections = unzipped.decode("utf-8").split("\r\n")
return self.kvp_to_dict(sections) ret = []
for x in sections:
ret.append(dict(urllib.parse.parse_qsl(x)))
return ret
except Exception as e: except Exception as e:
self.logger.error(f"allnet_req_to_dict: {e} while parsing {data}") self.logger.error(f"allnet_req_to_dict: {e} while parsing {data}")
@@ -354,7 +388,7 @@ class AllnetServlet:
def dict_to_http_form_string( def dict_to_http_form_string(
self, self,
data: List[Dict[str, Any]], data: List[Dict[str, Any]],
crlf: bool = False, crlf: bool = True,
trailing_newline: bool = True, trailing_newline: bool = True,
) -> Optional[str]: ) -> Optional[str]:
""" """
@@ -364,21 +398,19 @@ class AllnetServlet:
urlencode = "" urlencode = ""
for item in data: for item in data:
for k, v in item.items(): for k, v in item.items():
if k is None or v is None:
continue
urlencode += f"{k}={v}&" urlencode += f"{k}={v}&"
if crlf: if crlf:
urlencode = urlencode[:-1] + "\r\n" urlencode = urlencode[:-1] + "\r\n"
else: else:
urlencode = urlencode[:-1] + "\n" urlencode = urlencode[:-1] + "\n"
if not trailing_newline: if not trailing_newline:
if crlf: if crlf:
urlencode = urlencode[:-2] urlencode = urlencode[:-2]
else: else:
urlencode = urlencode[:-1] urlencode = urlencode[:-1]
return urlencode return urlencode
except Exception as e: except Exception as e:
self.logger.error(f"dict_to_http_form_string: {e} while parsing {data}") self.logger.error(f"dict_to_http_form_string: {e} while parsing {data}")
return None return None
@@ -388,64 +420,68 @@ class AllnetPowerOnRequest:
def __init__(self, req: Dict) -> None: def __init__(self, req: Dict) -> None:
if req is None: if req is None:
raise AllnetRequestException("Request processing failed") raise AllnetRequestException("Request processing failed")
self.game_id: str = req.get("game_id", "") self.game_id: str = req.get("game_id", None)
self.ver: str = req.get("ver", "") self.ver: str = req.get("ver", None)
self.serial: str = req.get("serial", "") self.serial: str = req.get("serial", None)
self.ip: str = req.get("ip", "") self.ip: str = req.get("ip", None)
self.firm_ver: str = req.get("firm_ver", "") self.firm_ver: str = req.get("firm_ver", None)
self.boot_ver: str = req.get("boot_ver", "") self.boot_ver: str = req.get("boot_ver", None)
self.encode: str = req.get("encode", "") self.encode: str = req.get("encode", "EUC-JP")
self.hops = int(req.get("hops", "0")) self.hops = int(req.get("hops", "-1"))
self.format_ver = req.get("format_ver", "2") self.format_ver = float(req.get("format_ver", "1.00"))
self.token = int(req.get("token", "0")) self.token: str = req.get("token", "0")
class AllnetPowerOnResponse:
class AllnetPowerOnResponse3:
def __init__(self, token) -> None:
self.stat = 1
self.uri = ""
self.host = ""
self.place_id = "123"
self.name = ""
self.nickname = ""
self.region0 = "1"
self.region_name0 = "W"
self.region_name1 = ""
self.region_name2 = ""
self.region_name3 = ""
self.country = "JPN"
self.allnet_id = "123"
self.client_timezone = "+0900"
self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
self.setting = "1"
self.res_ver = "3"
self.token = str(token)
class AllnetPowerOnResponse2:
def __init__(self) -> None: def __init__(self) -> None:
self.stat = 1 self.stat = 1
self.uri = "" self.uri = ""
self.host = "" self.host = ""
self.place_id = "123" self.place_id = "123"
self.name = "Test" self.name = "ARTEMiS"
self.nickname = "Test123" self.nickname = "ARTEMiS"
self.region0 = "1" self.region0 = "1"
self.region_name0 = "W" self.region_name0 = "W"
self.region_name1 = "X" self.region_name1 = ""
self.region_name2 = "Y" self.region_name2 = ""
self.region_name3 = "Z" self.region_name3 = ""
self.country = "JPN" self.setting = "1"
self.year = datetime.now().year self.year = datetime.now().year
self.month = datetime.now().month self.month = datetime.now().month
self.day = datetime.now().day self.day = datetime.now().day
self.hour = datetime.now().hour self.hour = datetime.now().hour
self.minute = datetime.now().minute self.minute = datetime.now().minute
self.second = datetime.now().second self.second = datetime.now().second
self.setting = "1"
self.timezone = "+0900" class AllnetPowerOnResponse3(AllnetPowerOnResponse):
def __init__(self, token) -> None:
super().__init__()
# Added in v3
self.country = "JPN"
self.allnet_id = "123"
self.client_timezone = "+0900"
self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
self.res_ver = "3"
self.token = token
# Removed in v3
self.year = None
self.month = None
self.day = None
self.hour = None
self.minute = None
self.second = None
class AllnetPowerOnResponse2(AllnetPowerOnResponse):
def __init__(self) -> None:
super().__init__()
# Added in v2
self.country = "JPN"
self.timezone = "+09:00"
self.res_class = "PowerOnResponseV2" self.res_class = "PowerOnResponseV2"

View File

@@ -36,6 +36,12 @@ class ServerConfig:
self.__config, "core", "server", "is_develop", default=True self.__config, "core", "server", "is_develop", default=True
) )
@property
def threading(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "core", "server", "threading", default=False
)
@property @property
def log_dir(self) -> str: def log_dir(self) -> str:
return CoreConfig.get_config_field( return CoreConfig.get_config_field(

View File

@@ -15,6 +15,13 @@ from core.utils import Utils
class Data: class Data:
current_schema_version = 4
engine = None
session = None
user = None
arcade = None
card = None
base = None
def __init__(self, cfg: CoreConfig) -> None: def __init__(self, cfg: CoreConfig) -> None:
self.config = cfg self.config = cfg
@@ -24,22 +31,32 @@ class Data:
else: else:
self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{self.config.database.password}@{self.config.database.host}/{self.config.database.name}?charset=utf8mb4" self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{self.config.database.password}@{self.config.database.host}/{self.config.database.name}?charset=utf8mb4"
self.__engine = create_engine(self.__url, pool_recycle=3600) if Data.engine is None:
session = sessionmaker(bind=self.__engine, autoflush=True, autocommit=True) Data.engine = create_engine(self.__url, pool_recycle=3600)
self.session = scoped_session(session) self.__engine = Data.engine
self.user = UserData(self.config, self.session) if Data.session is None:
self.arcade = ArcadeData(self.config, self.session) s = sessionmaker(bind=Data.engine, autoflush=True, autocommit=True)
self.card = CardData(self.config, self.session) Data.session = scoped_session(s)
self.base = BaseData(self.config, self.session)
self.current_schema_version = 4 if Data.user is None:
Data.user = UserData(self.config, self.session)
if Data.arcade is None:
Data.arcade = ArcadeData(self.config, self.session)
if Data.card is None:
Data.card = CardData(self.config, self.session)
if Data.base is None:
Data.base = BaseData(self.config, self.session)
log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
self.logger = logging.getLogger("database") self.logger = logging.getLogger("database")
# Prevent the logger from adding handlers multiple times # Prevent the logger from adding handlers multiple times
if not getattr(self.logger, "handler_set", None): if not getattr(self.logger, "handler_set", None):
log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler( fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.config.server.log_dir, "db"), "{0}/{1}.log".format(self.config.server.log_dir, "db"),
encoding="utf-8", encoding="utf-8",
@@ -333,3 +350,8 @@ class Data:
if not failed: if not failed:
self.base.set_schema_ver(latest_ver, game) self.base.set_schema_ver(latest_ver, game)
def show_versions(self) -> None:
all_game_versions = self.base.get_all_schema_vers()
for ver in all_game_versions:
self.logger.info(f"{ver['game']} -> v{ver['version']}")

View File

@@ -80,6 +80,9 @@ class UserData(BaseData):
if usr["password"] is None: if usr["password"] is None:
return False return False
if passwd is None or not passwd:
return False
return bcrypt.checkpw(passwd, usr["password"].encode()) return bcrypt.checkpw(passwd, usr["password"].encode())
def reset_autoincrement(self, ai_value: int) -> None: def reset_autoincrement(self, ai_value: int) -> None:

View File

@@ -0,0 +1,30 @@
SET FOREIGN_KEY_CHECKS = 0;
ALTER TABLE chuni_score_playlog
DROP COLUMN regionId,
DROP COLUMN machineType;
ALTER TABLE chuni_static_events
DROP COLUMN startDate;
ALTER TABLE chuni_profile_data
DROP COLUMN rankUpChallengeResults;
ALTER TABLE chuni_static_login_bonus
DROP FOREIGN KEY chuni_static_login_bonus_ibfk_1;
ALTER TABLE chuni_static_login_bonus_preset
DROP PRIMARY KEY;
ALTER TABLE chuni_static_login_bonus_preset
CHANGE COLUMN presetId id INT NOT NULL;
ALTER TABLE chuni_static_login_bonus_preset
ADD PRIMARY KEY(id);
ALTER TABLE chuni_static_login_bonus_preset
ADD CONSTRAINT chuni_static_login_bonus_preset_uk UNIQUE(id, version);
ALTER TABLE chuni_static_login_bonus
ADD CONSTRAINT chuni_static_login_bonus_ibfk_1 FOREIGN KEY(presetId)
REFERENCES chuni_static_login_bonus_preset(id) ON UPDATE CASCADE ON DELETE CASCADE;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,29 @@
SET FOREIGN_KEY_CHECKS = 0;
ALTER TABLE chuni_score_playlog
ADD COLUMN regionId INT,
ADD COLUMN machineType INT;
ALTER TABLE chuni_static_events
ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp();
ALTER TABLE chuni_profile_data
ADD COLUMN rankUpChallengeResults JSON;
ALTER TABLE chuni_static_login_bonus
DROP FOREIGN KEY chuni_static_login_bonus_ibfk_1;
ALTER TABLE chuni_static_login_bonus_preset
CHANGE COLUMN id presetId INT NOT NULL;
ALTER TABLE chuni_static_login_bonus_preset
DROP PRIMARY KEY;
ALTER TABLE chuni_static_login_bonus_preset
DROP INDEX chuni_static_login_bonus_preset_uk;
ALTER TABLE chuni_static_login_bonus_preset
ADD CONSTRAINT chuni_static_login_bonus_preset_pk PRIMARY KEY (presetId, version);
ALTER TABLE chuni_static_login_bonus
ADD CONSTRAINT chuni_static_login_bonus_ibfk_1 FOREIGN KEY (presetId, version)
REFERENCES chuni_static_login_bonus_preset(presetId, version) ON UPDATE CASCADE ON DELETE CASCADE;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,2 @@
ALTER TABLE ongeki_static_events
DROP COLUMN startDate;

View File

@@ -0,0 +1,2 @@
ALTER TABLE ongeki_static_events
ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp();

View File

@@ -0,0 +1,3 @@
ALTER TABLE mai2_item_card
CHANGE COLUMN startDate startDate TIMESTAMP DEFAULT "2018-01-01 00:00:00.0",
CHANGE COLUMN endDate endDate TIMESTAMP DEFAULT "2038-01-01 00:00:00.0";

View File

@@ -0,0 +1,78 @@
DELETE FROM mai2_static_event WHERE version < 13;
UPDATE mai2_static_event SET version = version - 13 WHERE version >= 13;
DELETE FROM mai2_static_music WHERE version < 13;
UPDATE mai2_static_music SET version = version - 13 WHERE version >= 13;
DELETE FROM mai2_static_ticket WHERE version < 13;
UPDATE mai2_static_ticket SET version = version - 13 WHERE version >= 13;
DELETE FROM mai2_static_cards WHERE version < 13;
UPDATE mai2_static_cards SET version = version - 13 WHERE version >= 13;
DELETE FROM mai2_profile_detail WHERE version < 13;
UPDATE mai2_profile_detail SET version = version - 13 WHERE version >= 13;
DELETE FROM mai2_profile_extend WHERE version < 13;
UPDATE mai2_profile_extend SET version = version - 13 WHERE version >= 13;
DELETE FROM mai2_profile_option WHERE version < 13;
UPDATE mai2_profile_option SET version = version - 13 WHERE version >= 13;
DELETE FROM mai2_profile_ghost WHERE version < 13;
UPDATE mai2_profile_ghost SET version = version - 13 WHERE version >= 13;
DELETE FROM mai2_profile_rating WHERE version < 13;
UPDATE mai2_profile_rating SET version = version - 13 WHERE version >= 13;
DROP TABLE maimai_score_best;
DROP TABLE maimai_playlog;
DROP TABLE maimai_profile_detail;
DROP TABLE maimai_profile_option;
DROP TABLE maimai_profile_web_option;
DROP TABLE maimai_profile_grade_status;
ALTER TABLE mai2_item_character DROP COLUMN point;
ALTER TABLE mai2_item_card MODIFY COLUMN cardId int(11) NOT NULL;
ALTER TABLE mai2_item_card MODIFY COLUMN cardTypeId int(11) NOT NULL;
ALTER TABLE mai2_item_card MODIFY COLUMN charaId int(11) NOT NULL;
ALTER TABLE mai2_item_card MODIFY COLUMN mapId int(11) NOT NULL;
ALTER TABLE mai2_item_character MODIFY COLUMN characterId int(11) NOT NULL;
ALTER TABLE mai2_item_character MODIFY COLUMN level int(11) NOT NULL;
ALTER TABLE mai2_item_character MODIFY COLUMN awakening int(11) NOT NULL;
ALTER TABLE mai2_item_character MODIFY COLUMN useCount int(11) NOT NULL;
ALTER TABLE mai2_item_charge MODIFY COLUMN chargeId int(11) NOT NULL;
ALTER TABLE mai2_item_charge MODIFY COLUMN stock int(11) NOT NULL;
ALTER TABLE mai2_item_favorite MODIFY COLUMN itemKind int(11) NOT NULL;
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN seasonId int(11) NOT NULL;
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN point int(11) NOT NULL;
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rank int(11) NOT NULL;
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rewardGet tinyint(1) NOT NULL;
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN userName varchar(8) NOT NULL;
ALTER TABLE mai2_item_item MODIFY COLUMN itemId int(11) NOT NULL;
ALTER TABLE mai2_item_item MODIFY COLUMN itemKind int(11) NOT NULL;
ALTER TABLE mai2_item_item MODIFY COLUMN stock int(11) NOT NULL;
ALTER TABLE mai2_item_item MODIFY COLUMN isValid tinyint(1) NOT NULL;
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN bonusId int(11) NOT NULL;
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN point int(11) NOT NULL;
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isCurrent tinyint(1) NOT NULL;
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isComplete tinyint(1) NOT NULL;
ALTER TABLE mai2_item_map MODIFY COLUMN mapId int(11) NOT NULL;
ALTER TABLE mai2_item_map MODIFY COLUMN distance int(11) NOT NULL;
ALTER TABLE mai2_item_map MODIFY COLUMN isLock tinyint(1) NOT NULL;
ALTER TABLE mai2_item_map MODIFY COLUMN isClear tinyint(1) NOT NULL;
ALTER TABLE mai2_item_map MODIFY COLUMN isComplete tinyint(1) NOT NULL;
ALTER TABLE mai2_item_print_detail MODIFY COLUMN printDate timestamp DEFAULT current_timestamp() NOT NULL;
ALTER TABLE mai2_item_print_detail MODIFY COLUMN serialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL;
ALTER TABLE mai2_item_print_detail MODIFY COLUMN placeId int(11) NOT NULL;
ALTER TABLE mai2_item_print_detail MODIFY COLUMN clientId varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL;
ALTER TABLE mai2_item_print_detail MODIFY COLUMN printerSerialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL;

View File

@@ -0,0 +1,3 @@
ALTER TABLE mai2_item_card
CHANGE COLUMN startDate startDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHANGE COLUMN endDate endDate TIMESTAMP NOT NULL;

View File

@@ -0,0 +1 @@
DROP TABLE aime.mai2_profile_consec_logins;

View File

@@ -0,0 +1,62 @@
UPDATE mai2_static_event SET version = version + 13 WHERE version < 1000;
UPDATE mai2_static_music SET version = version + 13 WHERE version < 1000;
UPDATE mai2_static_ticket SET version = version + 13 WHERE version < 1000;
UPDATE mai2_static_cards SET version = version + 13 WHERE version < 1000;
UPDATE mai2_profile_detail SET version = version + 13 WHERE version < 1000;
UPDATE mai2_profile_extend SET version = version + 13 WHERE version < 1000;
UPDATE mai2_profile_option SET version = version + 13 WHERE version < 1000;
UPDATE mai2_profile_ghost SET version = version + 13 WHERE version < 1000;
UPDATE mai2_profile_rating SET version = version + 13 WHERE version < 1000;
ALTER TABLE mai2_item_character ADD point int(11) NULL;
ALTER TABLE mai2_item_card MODIFY COLUMN cardId int(11) NULL;
ALTER TABLE mai2_item_card MODIFY COLUMN cardTypeId int(11) NULL;
ALTER TABLE mai2_item_card MODIFY COLUMN charaId int(11) NULL;
ALTER TABLE mai2_item_card MODIFY COLUMN mapId int(11) NULL;
ALTER TABLE mai2_item_character MODIFY COLUMN characterId int(11) NULL;
ALTER TABLE mai2_item_character MODIFY COLUMN level int(11) NULL;
ALTER TABLE mai2_item_character MODIFY COLUMN awakening int(11) NULL;
ALTER TABLE mai2_item_character MODIFY COLUMN useCount int(11) NULL;
ALTER TABLE mai2_item_charge MODIFY COLUMN chargeId int(11) NULL;
ALTER TABLE mai2_item_charge MODIFY COLUMN stock int(11) NULL;
ALTER TABLE mai2_item_favorite MODIFY COLUMN itemKind int(11) NULL;
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN seasonId int(11) NULL;
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN point int(11) NULL;
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rank int(11) NULL;
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN rewardGet tinyint(1) NULL;
ALTER TABLE mai2_item_friend_season_ranking MODIFY COLUMN userName varchar(8) NULL;
ALTER TABLE mai2_item_item MODIFY COLUMN itemId int(11) NULL;
ALTER TABLE mai2_item_item MODIFY COLUMN itemKind int(11) NULL;
ALTER TABLE mai2_item_item MODIFY COLUMN stock int(11) NULL;
ALTER TABLE mai2_item_item MODIFY COLUMN isValid tinyint(1) NULL;
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN bonusId int(11) NULL;
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN point int(11) NULL;
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isCurrent tinyint(1) NULL;
ALTER TABLE mai2_item_login_bonus MODIFY COLUMN isComplete tinyint(1) NULL;
ALTER TABLE mai2_item_map MODIFY COLUMN mapId int(11) NULL;
ALTER TABLE mai2_item_map MODIFY COLUMN distance int(11) NULL;
ALTER TABLE mai2_item_map MODIFY COLUMN isLock tinyint(1) NULL;
ALTER TABLE mai2_item_map MODIFY COLUMN isClear tinyint(1) NULL;
ALTER TABLE mai2_item_map MODIFY COLUMN isComplete tinyint(1) NULL;
ALTER TABLE mai2_item_print_detail MODIFY COLUMN printDate timestamp DEFAULT current_timestamp() NULL;
ALTER TABLE mai2_item_print_detail MODIFY COLUMN serialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL;
ALTER TABLE mai2_item_print_detail MODIFY COLUMN placeId int(11) NULL;
ALTER TABLE mai2_item_print_detail MODIFY COLUMN clientId varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL;
ALTER TABLE mai2_item_print_detail MODIFY COLUMN printerSerialId varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL;

View File

@@ -0,0 +1,9 @@
CREATE TABLE `mai2_profile_consec_logins` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user` int(11) NOT NULL,
`version` int(11) NOT NULL,
`logins` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `mai2_profile_consec_logins_uk` (`user`,`version`),
CONSTRAINT `mai2_profile_consec_logins_ibfk_1` FOREIGN KEY (`user`) REFERENCES `aime_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

View File

@@ -182,7 +182,7 @@ class FE_Gate(FE_Base):
access_code: str = request.args[b"access_code"][0].decode() access_code: str = request.args[b"access_code"][0].decode()
username: str = request.args[b"username"][0] username: str = request.args[b"username"][0]
email: str = request.args[b"email"][0].decode() email: str = request.args[b"email"][0].decode()
passwd: str = request.args[b"passwd"][0] passwd: bytes = request.args[b"passwd"][0]
uid = self.data.card.get_user_id_from_card(access_code) uid = self.data.card.get_user_id_from_card(access_code)
if uid is None: if uid is None:
@@ -197,7 +197,7 @@ class FE_Gate(FE_Base):
if result is None: if result is None:
return redirectTo(b"/gate?e=3", request) return redirectTo(b"/gate?e=3", request)
if not self.data.user.check_password(uid, passwd.encode()): if not self.data.user.check_password(uid, passwd):
return redirectTo(b"/gate", request) return redirectTo(b"/gate", request)
return redirectTo(b"/user", request) return redirectTo(b"/user", request)
@@ -228,8 +228,21 @@ class FE_User(FE_Base):
if usr_sesh.userId == 0: if usr_sesh.userId == 0:
return redirectTo(b"/gate", request) return redirectTo(b"/gate", request)
cards = self.data.card.get_user_cards(usr_sesh.userId)
user = self.data.user.get_user(usr_sesh.userId)
card_data = []
for c in cards:
if c['is_locked']:
status = 'Locked'
elif c['is_banned']:
status = 'Banned'
else:
status = 'Active'
card_data.append({'access_code': c['access_code'], 'status': status})
return template.render( return template.render(
title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh) title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh), cards=card_data, username=user['username']
).encode("utf-16") ).encode("utf-16")

View File

@@ -1,4 +1,31 @@
{% extends "core/frontend/index.jinja" %} {% extends "core/frontend/index.jinja" %}
{% block content %} {% block content %}
<h1>testing</h1> <h1>Management for {{ username }}</h1>
<h2>Cards <button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#card_add">Add</button></h2>
<ul>
{% for c in cards %}
<li>{{ c.access_code }}: {{ c.status }} <button class="btn-danger btn">Delete</button></li>
{% endfor %}
</ul>
<div class="modal fade" id="card_add" tabindex="-1" aria-labelledby="card_add_label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="card_add_label">Add Card</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
HOW TO:<br>
Scan your card on any networked game and press the "View Access Code" button (varies by game) and enter the 20 digit code below.<br>
!!FOR AMUSEIC CARDS: DO NOT ENTER THE CODE SHOWN ON THE BACK OF THE CARD ITSELF OR IT WILL NOT WORK!!
<p /><label for="card_add_frm_access_code">Access Code:&nbsp;</label><input id="card_add_frm_access_code" maxlength="20" type="text" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary">Add</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endblock content %} {% endblock content %}

View File

@@ -4,7 +4,7 @@
<div style="background: #333; color: #f9f9f9; width: 80%; height: 50px; line-height: 50px; padding-left: 10px; float: left;"> <div style="background: #333; color: #f9f9f9; width: 80%; height: 50px; line-height: 50px; padding-left: 10px; float: left;">
<a href=/><button class="btn btn-primary">Home</button></a>&nbsp; <a href=/><button class="btn btn-primary">Home</button></a>&nbsp;
{% for game in game_list %} {% for game in game_list %}
<a href=game/{{ game.url }}><button class="btn btn-success">{{ game.name }}</button></a>&nbsp; <a href=/game/{{ game.url }}><button class="btn btn-success">{{ game.name }}</button></a>&nbsp;
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@@ -33,8 +33,8 @@ class MuchaServlet:
self.logger.addHandler(fileHandler) self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler) self.logger.addHandler(consoleHandler)
self.logger.setLevel(logging.INFO) self.logger.setLevel(cfg.mucha.loglevel)
coloredlogs.install(level=logging.INFO, logger=self.logger, fmt=log_fmt_str) coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str)
all_titles = Utils.get_all_titles() all_titles = Utils.get_all_titles()

View File

@@ -84,7 +84,7 @@ class TitleServlet:
request.setResponseCode(405) request.setResponseCode(405)
return b"" return b""
return index.render_GET(request, endpoints["version"], endpoints["endpoint"]) return index.render_GET(request, int(endpoints["version"]), endpoints["endpoint"])
def render_POST(self, request: Request, endpoints: dict) -> bytes: def render_POST(self, request: Request, endpoints: dict) -> bytes:
code = endpoints["game"] code = endpoints["game"]

View File

@@ -85,4 +85,7 @@ if __name__ == "__main__":
elif args.action == "cleanup": elif args.action == "cleanup":
data.delete_hanging_users() data.delete_hanging_users()
elif args.action == "version":
data.show_versions()
data.logger.info("Done") data.logger.info("Done")

57
docker-compose.yml Normal file
View File

@@ -0,0 +1,57 @@
version: "3.9"
services:
app:
hostname: ma.app
build: .
volumes:
- ./aime:/app/aime
environment:
CFG_DEV: 1
CFG_CORE_SERVER_HOSTNAME: 0.0.0.0
CFG_CORE_DATABASE_HOST: ma.db
CFG_CORE_MEMCACHED_HOSTNAME: ma.memcached
CFG_CORE_AIMEDB_KEY: keyhere
CFG_CHUNI_SERVER_LOGLEVEL: debug
ports:
- "80:80"
- "8443:8443"
- "22345:22345"
- "8080:8080"
- "8090:8090"
depends_on:
db:
condition: service_healthy
db:
hostname: ma.db
image: mysql:8.0.31-debian
environment:
MYSQL_DATABASE: aime
MYSQL_USER: aime
MYSQL_PASSWORD: aime
MYSQL_ROOT_PASSWORD: AimeRootPassword
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
timeout: 5s
retries: 5
memcached:
hostname: ma.memcached
image: memcached:1.6.17-bullseye
phpmyadmin:
hostname: ma.phpmyadmin
image: phpmyadmin:latest
environment:
PMA_HOSTS: ma.db
PMA_USER: root
PMA_PASSWORD: AimeRootPassword
APACHE_PORT: 8080
ports:
- "8080:8080"

View File

@@ -5,6 +5,7 @@
- `allow_unregistered_serials`: Allows games that do not have registered keychips to connect and authenticate. Disable to restrict who can connect to your server. Recomended to disable for production setups. Default `True` - `allow_unregistered_serials`: Allows games that do not have registered keychips to connect and authenticate. Disable to restrict who can connect to your server. Recomended to disable for production setups. Default `True`
- `name`: Name for the server, used by some games in their default MOTDs. Default `ARTEMiS` - `name`: Name for the server, used by some games in their default MOTDs. Default `ARTEMiS`
- `is_develop`: Flags that the server is a development instance without a proxy standing in front of it. Setting to `False` tells the server not to listen for SSL, because the proxy should be handling all SSL-related things, among other things. Default `True` - `is_develop`: Flags that the server is a development instance without a proxy standing in front of it. Setting to `False` tells the server not to listen for SSL, because the proxy should be handling all SSL-related things, among other things. Default `True`
- `threading`: Flags that `reactor.run` should be called via the `Thread` standard library. May provide a speed boost, but removes the ability to kill the server via `Ctrl + C`. Default: `False`
- `log_dir`: Directory to store logs. Server MUST have read and write permissions to this directory or you will have issues. Default `logs` - `log_dir`: Directory to store logs. Server MUST have read and write permissions to this directory or you will have issues. Default `logs`
## Title ## Title
- `loglevel`: Logging level for the title server. Default `info` - `loglevel`: Logging level for the title server. Default `info`

View File

@@ -9,42 +9,44 @@ using the megaime database. Clean installations always create the latest databas
# Table of content # Table of content
- [Supported Games](#supported-games) - [Supported Games](#supported-games)
- [Chunithm](#chunithm) - [CHUNITHM](#chunithm)
- [crossbeats REV.](#crossbeats-rev) - [crossbeats REV.](#crossbeats-rev)
- [maimai DX](#maimai-dx) - [maimai DX](#maimai-dx)
- [O.N.G.E.K.I.](#o-n-g-e-k-i) - [O.N.G.E.K.I.](#o-n-g-e-k-i)
- [Card Maker](#card-maker) - [Card Maker](#card-maker)
- [WACCA](#wacca) - [WACCA](#wacca)
- [Sword Art Online Arcade](#sao)
# Supported Games # Supported Games
Games listed below have been tested and confirmed working. Games listed below have been tested and confirmed working.
## Chunithm ## CHUNITHM
### SDBT ### SDBT
| Version ID | Version Name | | Version ID | Version Name |
|------------|--------------------| |------------|-----------------------|
| 0 | Chunithm | | 0 | CHUNITHM |
| 1 | Chunithm+ | | 1 | CHUNITHM PLUS |
| 2 | Chunithm Air | | 2 | CHUNITHM AIR |
| 3 | Chunithm Air + | | 3 | CHUNITHM AIR PLUS |
| 4 | Chunithm Star | | 4 | CHUNITHM STAR |
| 5 | Chunithm Star + | | 5 | CHUNITHM STAR PLUS |
| 6 | Chunithm Amazon | | 6 | CHUNITHM AMAZON |
| 7 | Chunithm Amazon + | | 7 | CHUNITHM AMAZON PLUS |
| 8 | Chunithm Crystal | | 8 | CHUNITHM CRYSTAL |
| 9 | Chunithm Crystal + | | 9 | CHUNITHM CRYSTAL PLUS |
| 10 | Chunithm Paradise | | 10 | CHUNITHM PARADISE |
### SDHD/SDBT ### SDHD/SDBT
| Version ID | Version Name | | Version ID | Version Name |
|------------|-----------------| |------------|---------------------|
| 11 | Chunithm New!! | | 11 | CHUNITHM NEW!! |
| 12 | Chunithm New!!+ | | 12 | CHUNITHM NEW PLUS!! |
| 13 | CHUNITHM SUN |
### Importer ### Importer
@@ -60,13 +62,33 @@ The importer for Chunithm will import: Events, Music, Charge Items and Avatar Ac
### Database upgrade ### Database upgrade
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
which version is the latest, f.e. `SDBT_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to which version is the latest, f.e. `SDBT_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to
perform all previous updates as well: perform all previous updates as well:
```shell ```shell
python dbutils.py --game SDBT upgrade python dbutils.py --game SDBT upgrade
``` ```
### Online Battle
**Only matchmaking (with your imaginary friends) is supported! Online Battle does not (yet?) work!**
The first person to start the Online Battle (now called host) will create a "matching room" with a given `roomId`, after that max 3 other people can join the created room.
Non used slots during the matchmaking will be filled with CPUs after the timer runs out.
As soon as a new member will join the room the timer will jump back to 60 secs again.
Sending those 4 messages to all other users is also working properly.
In order to use the Online Battle every user needs the same ICF, same rom version and same data version!
If a room is full a new room will be created if another user starts an Online Battle.
After a failed Online Battle the room will be deleted. The host is used for the timer countdown, so if the connection failes to the host the timer will stop and could create a "frozen" state.
#### Information/Problems:
- Online Battle uses UDP hole punching and opens port 50201?
- `reflectorUri` seems related to that?
- Timer countdown should be handled globally and not by one user
- Game can freeze or can crash if someone (especially the host) leaves the matchmaking
## crossbeats REV. ## crossbeats REV.
### SDCA ### SDCA
@@ -105,28 +127,50 @@ Config file is located in `config/cxb.yaml`.
### SDEZ ### SDEZ
| Version ID | Version Name | | Game Code | Version ID | Version Name |
|------------|-------------------------| |-----------|------------|-------------------------|
| 0 | maimai DX |
| 1 | maimai DX PLUS |
| 2 | maimai DX Splash | For versions pre-dx
| 3 | maimai DX Splash PLUS | | Game Code | Version ID | Version Name |
| 4 | maimai DX Universe | |-----------|------------|-------------------------|
| 5 | maimai DX Universe PLUS | | SBXL | 0 | maimai |
| 6 | maimai DX Festival | | SBXL | 1 | maimai PLUS |
| SBZF | 2 | maimai GreeN |
| SBZF | 3 | maimai GreeN PLUS |
| SDBM | 4 | maimai ORANGE |
| SDBM | 5 | maimai ORANGE PLUS |
| SDCQ | 6 | maimai PiNK |
| SDCQ | 7 | maimai PiNK PLUS |
| SDDK | 8 | maimai MURASAKI |
| SDDK | 9 | maimai MURASAKI PLUS |
| SDDZ | 10 | maimai MILK |
| SDDZ | 11 | maimai MILK PLUS |
| SDEY | 12 | maimai FiNALE |
| SDEZ | 13 | maimai DX |
| SDEZ | 14 | maimai DX PLUS |
| SDEZ | 15 | maimai DX Splash |
| SDEZ | 16 | maimai DX Splash PLUS |
| SDEZ | 17 | maimai DX Universe |
| SDEZ | 18 | maimai DX Universe PLUS |
| SDEZ | 19 | maimai DX Festival |
### Importer ### Importer
In order to use the importer locate your game installation folder and execute: In order to use the importer locate your game installation folder and execute:
DX:
```shell ```shell
python read.py --series SDEZ --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder python read.py --series <Game Code> --version <Version ID> --binfolder /path/to/StreamingAssets --optfolder /path/to/game/option/folder
```
Pre-DX:
```shell
python read.py --series <Game Code> --version <Version ID> --binfolder /path/to/data --optfolder /path/to/patch/data
``` ```
The importer for maimai DX will import Events, Music and Tickets. The importer for maimai DX will import Events, Music and Tickets.
**NOTE: It is required to use the importer because the game will The importer for maimai Pre-DX will import Events and Music. Not all games will have patch data. Milk - Finale have file encryption, and need an AES key. That key is not provided by the developers. For games that do use encryption, provide the key, as a hex string, with the `--extra` flag. Ex `--extra 00112233445566778899AABBCCDDEEFF`
crash without Events!**
**Important: It is required to use the importer because some games may not function properly or even crash without Events!**
### Database upgrade ### Database upgrade
@@ -135,6 +179,7 @@ Always make sure your database (tables) are up-to-date, to do so go to the `core
```shell ```shell
python dbutils.py --game SDEZ upgrade python dbutils.py --game SDEZ upgrade
``` ```
Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code!
## Hatsune Miku Project Diva ## Hatsune Miku Project Diva
@@ -185,12 +230,12 @@ python dbutils.py --game SBZV upgrade
|------------|----------------------------| |------------|----------------------------|
| 0 | O.N.G.E.K.I. | | 0 | O.N.G.E.K.I. |
| 1 | O.N.G.E.K.I. + | | 1 | O.N.G.E.K.I. + |
| 2 | O.N.G.E.K.I. Summer | | 2 | O.N.G.E.K.I. SUMMER |
| 3 | O.N.G.E.K.I. Summer + | | 3 | O.N.G.E.K.I. SUMMER + |
| 4 | O.N.G.E.K.I. Red | | 4 | O.N.G.E.K.I. R.E.D. |
| 5 | O.N.G.E.K.I. Red + | | 5 | O.N.G.E.K.I. R.E.D. + |
| 6 | O.N.G.E.K.I. Bright | | 6 | O.N.G.E.K.I. bright |
| 7 | O.N.G.E.K.I. Bright Memory | | 7 | O.N.G.E.K.I. bright MEMORY |
### Importer ### Importer
@@ -231,21 +276,21 @@ python dbutils.py --game SDDT upgrade
| Version ID | Version Name | | Version ID | Version Name |
|------------|-----------------| |------------|-----------------|
| 0 | Card Maker 1.34 | | 0 | Card Maker 1.30 |
| 1 | Card Maker 1.35 | | 1 | Card Maker 1.35 |
### Support status ### Support status
* Card Maker 1.34: * Card Maker 1.30:
* Chunithm New!!: Yes * CHUNITHM NEW!!: Yes
* maimai DX Universe: Yes * maimai DX UNiVERSE: Yes
* O.N.G.E.K.I. Bright: Yes * O.N.G.E.K.I. bright: Yes
* Card Maker 1.35: * Card Maker 1.35:
* Chunithm New!!+: Yes * CHUNITHM SUN: Yes (NEW PLUS!! up to A032)
* maimai DX Universe PLUS: Yes * maimai DX FESTiVAL: Yes (up to A035) (UNiVERSE PLUS up to A031)
* O.N.G.E.K.I. Bright Memory: Yes * O.N.G.E.K.I. bright MEMORY: Yes
### Importer ### Importer
@@ -263,19 +308,54 @@ python read.py --series SDED --version <version ID> --binfolder titles/cm/cm_dat
python read.py --series SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder python read.py --series SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
``` ```
Also make sure to import all maimai and Chunithm data as well: Also make sure to import all maimai DX and CHUNITHM data as well:
```shell ```shell
python read.py --series SDED --version <version ID> --binfolder /path/to/cardmaker/CardMaker_Data python read.py --series SDED --version <version ID> --binfolder /path/to/cardmaker/CardMaker_Data
``` ```
The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai/Chunithm) and the hardcoded The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai DX/CHUNITHM) and the hardcoded
Cards for each Gacha (O.N.G.E.K.I. only). Cards for each Gacha (O.N.G.E.K.I. only).
**NOTE: Without executing the importer Card Maker WILL NOT work!** **NOTE: Without executing the importer Card Maker WILL NOT work!**
### O.N.G.E.K.I. Gachas ### Config setup
Make sure to update your `config/cardmaker.yaml` with the correct version for each game. To get the current version required to run a specific game, open every opt (Axxx) folder descending until you find all three folders:
- `MU3`: O.N.G.E.K.I.
- `MAI`: maimai DX
- `CHU`: CHUNITHM
Inside each folder is a `DataConfig.xml` file, for example:
`MU3/DataConfig.xml`:
```xml
<cardMakerVersion>
<major>1</major>
<minor>35</minor>
<release>3</release>
</cardMakerVersion>
```
Now update your `config/cardmaker.yaml` with the correct version number, for example:
```yaml
version:
1: # Card Maker 1.35
ongeki: 1.35.03
```
For now you also need to update your `config/ongeki.yaml` with the correct version number, for example:
```yaml
version:
7: # O.N.G.E.K.I. bright MEMORY
card_maker: 1.35.03
```
### O.N.G.E.K.I.
Gacha "無料ガチャ" can only pull from the free cards with the following probabilities: 94%: R, 5% SR and 1% chance of Gacha "無料ガチャ" can only pull from the free cards with the following probabilities: 94%: R, 5% SR and 1% chance of
getting an SSR card getting an SSR card
@@ -288,20 +368,24 @@ and 3% chance of getting an SSR card
All other (limited) gachas can pull from every card added to ongeki_static_cards but with the promoted cards All other (limited) gachas can pull from every card added to ongeki_static_cards but with the promoted cards
(click on the green button under the banner) having a 10 times higher chance to get pulled (click on the green button under the banner) having a 10 times higher chance to get pulled
### Chunithm Gachas ### CHUNITHM
All cards in Chunithm (basically just the characters) have the same rarity to it just pulls randomly from all cards All cards in CHUNITHM (basically just the characters) have the same rarity to it just pulls randomly from all cards
from a given gacha but made sure you cannot pull the same card twice in the same 5 times gacha roll. from a given gacha but made sure you cannot pull the same card twice in the same 5 times gacha roll.
### maimai DX
Printed maimai DX cards: Freedom (`cardTypeId=6`) or Gold Pass (`cardTypeId=4`) can now be selected during the login process. You can only have ONE Freedom and ONE Gold Pass active at a given time. The cards will expire after 15 days.
Thanks GetzeAvenue for the `selectedCardList` rarity hint!
### Notes ### Notes
Card Maker 1.34 will only load an O.N.G.E.K.I. Bright profile (1.30). Card Maker 1.35 will only load an O.N.G.E.K.I. Card Maker 1.30-1.34 will only load an O.N.G.E.K.I. Bright profile (1.30). Card Maker 1.35+ will only load an O.N.G.E.K.I.
Bright Memory profile (1.35). Bright Memory profile (1.35).
The gachas inside the `ongeki.yaml` will make sure only the right gacha ids for the right CM version will be loaded. The gachas inside the `config/ongeki.yaml` will make sure only the right gacha ids for the right CM version will be loaded.
Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded for CM 1.35. Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded for CM 1.35.
**NOTE: There is currently no way to load/use the (printed) maimai DX cards!**
## WACCA ## WACCA
### SDFE ### SDFE
@@ -344,3 +428,89 @@ Always make sure your database (tables) are up-to-date, to do so go to the `core
```shell ```shell
python dbutils.py --game SDFE upgrade python dbutils.py --game SDFE upgrade
``` ```
### VIP Rewards
Below is a list of VIP rewards. Currently, VIP is not implemented, and thus these are not obtainable. These 23 rewards were distributed once per month for VIP users on the real network.
Plates:
211004 リッチ
211018 特盛えりざべす
211025 イースター
211026 特盛りりぃ
311004 ファンシー
311005 インカンテーション
311014 夜明け
311015 ネイビー
311016 特盛るーん
Ring Colors:
203002 Gold Rushイエロー
203009 トロピカル
303005 ネイチャー
Icons:
202020 どらみんぐ
202063 ユニコーン
202086 ゴリラ
302014 ローズ
302015 ファラオ
302045 肉球
302046 WACCA
302047 WACCA Lily
302048 WACCA Reverse
Note Sound Effect:
205002 テニス
205008 シャワー
305003 タンバリンMk-Ⅱ
## SAO
### SDEW
| Version ID | Version Name |
|------------|---------------|
| 0 | SAO |
### Importer
In order to use the importer locate your game installation folder and execute:
```shell
python read.py --series SDEW --version <version ID> --binfolder /path/to/game/extractedassets
```
The importer for SAO will import all items, heroes, support skills and titles data.
### Config
Config file is located in `config/sao.yaml`.
| Option | Info |
|--------------------|-----------------------------------------------------------------------------|
| `hostname` | Changes the server listening address for Mucha |
| `port` | Changes the listing port |
| `auto_register` | Allows the game to handle the automatic registration of new cards |
### Database upgrade
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDEW_1_upgrade.sql`. In order to upgrade to version 3 in this case you need to perform all previous updates as well:
```shell
python dbutils.py --game SDEW upgrade
```
### Notes
- Defrag Match will crash at loading
- Co-Op Online is not supported
- Shop is not functionnal
- Player title is currently static and cannot be changed in-game
- QR Card Scanning currently only load a static hero
### Credits for SAO support:
- Midorica - Limited Network Support
- Dniel97 - Helping with network base
- tungnotpunk - Source

11
entrypoint.sh Normal file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
if [[ -z "${CFG_DEV}" ]]; then
echo Production mode
python3 index.py
else
echo Development mode
python3 dbutils.py create
nodemon -w aime --legacy-watch index.py
fi

View File

@@ -1,3 +1,13 @@
server: server:
enable: True enable: True
loglevel: "info" loglevel: "info"
version:
0:
ongeki: 1.30.01
chuni: 2.00.00
maimai: 1.20.00
1:
ongeki: 1.35.03
chuni: 2.10.00
maimai: 1.30.00

View File

@@ -15,6 +15,9 @@ version:
12: 12:
rom: 2.05.00 rom: 2.05.00
data: 2.05.00 data: 2.05.00
13:
rom: 2.10.00
data: 2.10.00
crypto: crypto:
encrypted_only: False encrypted_only: False

View File

@@ -4,6 +4,7 @@ server:
allow_unregistered_serials: True allow_unregistered_serials: True
name: "ARTEMiS" name: "ARTEMiS"
is_develop: True is_develop: True
threading: False
log_dir: "logs" log_dir: "logs"
title: title:

View File

@@ -1,3 +1,8 @@
server: server:
enable: True enable: True
loglevel: "info" loglevel: "info"
deliver:
enable: False
udbdl_enable: False
content_folder: ""

View File

@@ -29,3 +29,9 @@ gachas:
- 1156 - 1156
- 1163 - 1163
- 1164 - 1164
version:
6:
card_maker: 1.30.01
7:
card_maker: 1.35.03

View File

@@ -2,8 +2,11 @@ server:
hostname: "localhost" hostname: "localhost"
enable: True enable: True
loglevel: "info" loglevel: "info"
port: 9000
port_stun: 9001
port_turn: 9002
port_admission: 9003
auto_register: True auto_register: True
enable_matching: False
stun_server_host: "stunserver.stunprotocol.org"
stun_server_port: 3478
ports:
game: 9000
admission: 9001

6
example_config/sao.yaml Normal file
View File

@@ -0,0 +1,6 @@
server:
hostname: "localhost"
enable: True
loglevel: "info"
port: 9000
auto_register: True

View File

@@ -11,7 +11,7 @@ from twisted.web import server, resource
from twisted.internet import reactor, endpoints from twisted.internet import reactor, endpoints
from twisted.web.http import Request from twisted.web.http import Request
from routes import Mapper from routes import Mapper
from threading import Thread
class HttpDispatcher(resource.Resource): class HttpDispatcher(resource.Resource):
def __init__(self, cfg: CoreConfig, config_dir: str): def __init__(self, cfg: CoreConfig, config_dir: str):
@@ -42,7 +42,7 @@ class HttpDispatcher(resource.Resource):
conditions=dict(method=["POST"]), conditions=dict(method=["POST"]),
) )
self.map_post.connect( self.map_get.connect(
"allnet_ping", "allnet_ping",
"/naomitest.html", "/naomitest.html",
controller="allnet", controller="allnet",
@@ -63,6 +63,27 @@ class HttpDispatcher(resource.Resource):
action="handle_dlorder", action="handle_dlorder",
conditions=dict(method=["POST"]), conditions=dict(method=["POST"]),
) )
self.map_post.connect(
"allnet_loaderstaterecorder",
"/sys/servlet/LoaderStateRecorder",
controller="allnet",
action="handle_loaderstaterecorder",
conditions=dict(method=["POST"]),
)
self.map_post.connect(
"allnet_alive",
"/sys/servlet/Alive",
controller="allnet",
action="handle_alive",
conditions=dict(method=["POST"]),
)
self.map_get.connect(
"allnet_alive",
"/sys/servlet/Alive",
controller="allnet",
action="handle_alive",
conditions=dict(method=["GET"]),
)
self.map_post.connect( self.map_post.connect(
"allnet_billing", "allnet_billing",
"/request", "/request",
@@ -111,7 +132,6 @@ class HttpDispatcher(resource.Resource):
) )
def render_GET(self, request: Request) -> bytes: def render_GET(self, request: Request) -> bytes:
self.logger.debug(request.uri)
test = self.map_get.match(request.uri.decode()) test = self.map_get.match(request.uri.decode())
client_ip = Utils.get_ip_addr(request) client_ip = Utils.get_ip_addr(request)
@@ -161,9 +181,16 @@ class HttpDispatcher(resource.Resource):
if type(ret) == str: if type(ret) == str:
return ret.encode() return ret.encode()
elif type(ret) == bytes:
elif type(ret) == bytes or type(ret) == tuple: # allow for bytes or tuple (data, response code) responses
return ret return ret
elif ret is None:
self.logger.warn(f"None returned by controller for {request.uri.decode()} endpoint")
return b""
else: else:
self.logger.warn(f"Unknown data type returned by controller for {request.uri.decode()} endpoint")
return b"" return b""
@@ -256,4 +283,7 @@ if __name__ == "__main__":
server.Site(dispatcher) server.Site(dispatcher)
) )
reactor.run() # type: ignore if cfg.server.threading:
Thread(target=reactor.run, args=(False,)).start()
else:
reactor.run()

View File

@@ -3,32 +3,36 @@ A network service emulator for games running SEGA'S ALL.NET service, and similar
# Supported games # Supported games
Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported. Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported.
+ Chunithm
+ All versions up to New!! Plus
+ Crossbeats Rev + CHUNITHM
+ All versions up to SUN
+ crossbeats REV.
+ All versions + omnimix + All versions + omnimix
+ maimai DX + maimai DX
+ All versions up to Festival + All versions up to FESTiVAL
+ Hatsune Miku Arcade + Hatsune Miku: Project DIVA Arcade
+ All versions + All versions
+ Card Maker + Card Maker
+ 1.34.xx + 1.30
+ 1.35.xx + 1.35
+ Ongeki + O.N.G.E.K.I.
+ All versions up to Bright Memory + All versions up to bright MEMORY
+ Wacca + WACCA
+ Lily R + Lily R
+ Reverse + Reverse
+ Pokken + POKKÉN TOURNAMENT
+ Final Online + Final Online
+ Sword Art Online Arcade (partial support)
+ Final
## Requirements ## Requirements
- python 3 (tested working with 3.9 and 3.10, other versions YMMV) - python 3 (tested working with 3.9 and 3.10, other versions YMMV)
- pip - pip

View File

@@ -16,3 +16,4 @@ Routes
bcrypt bcrypt
jinja2 jinja2
protobuf protobuf
autobahn

View File

@@ -7,4 +7,4 @@ index = ChuniServlet
database = ChuniData database = ChuniData
reader = ChuniReader reader = ChuniReader
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW] game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW]
current_schema_version = 3 current_schema_version = 4

View File

@@ -4,7 +4,7 @@ from datetime import datetime, timedelta
from time import strftime from time import strftime
import pytz import pytz
from typing import Dict, Any from typing import Dict, Any, List
from core.config import CoreConfig from core.config import CoreConfig
from titles.chuni.const import ChuniConstants from titles.chuni.const import ChuniConstants
@@ -44,13 +44,15 @@ class ChuniBase:
# check if a user already has some pogress and if not add the # check if a user already has some pogress and if not add the
# login bonus entry # login bonus entry
user_login_bonus = self.data.item.get_login_bonus( user_login_bonus = self.data.item.get_login_bonus(
user_id, self.version, preset["id"] user_id, self.version, preset["presetId"]
) )
if user_login_bonus is None: if user_login_bonus is None:
self.data.item.put_login_bonus(user_id, self.version, preset["id"]) self.data.item.put_login_bonus(
user_id, self.version, preset["presetId"]
)
# yeah i'm lazy # yeah i'm lazy
user_login_bonus = self.data.item.get_login_bonus( user_login_bonus = self.data.item.get_login_bonus(
user_id, self.version, preset["id"] user_id, self.version, preset["presetId"]
) )
# skip the login bonus entirely if its already finished # skip the login bonus entirely if its already finished
@@ -66,13 +68,13 @@ class ChuniBase:
last_update_date = datetime.now() last_update_date = datetime.now()
all_login_boni = self.data.static.get_login_bonus( all_login_boni = self.data.static.get_login_bonus(
self.version, preset["id"] self.version, preset["presetId"]
) )
# skip the current bonus preset if no boni were found # skip the current bonus preset if no boni were found
if all_login_boni is None or len(all_login_boni) < 1: if all_login_boni is None or len(all_login_boni) < 1:
self.logger.warn( self.logger.warn(
f"No bonus entries found for bonus preset {preset['id']}" f"No bonus entries found for bonus preset {preset['presetId']}"
) )
continue continue
@@ -83,14 +85,14 @@ class ChuniBase:
if bonus_count > max_needed_days: if bonus_count > max_needed_days:
# assume that all login preset ids under 3000 needs to be # assume that all login preset ids under 3000 needs to be
# looped, like 30 and 40 are looped, 40 does not work? # looped, like 30 and 40 are looped, 40 does not work?
if preset["id"] < 3000: if preset["presetId"] < 3000:
bonus_count = 1 bonus_count = 1
else: else:
is_finished = True is_finished = True
# grab the item for the corresponding day # grab the item for the corresponding day
login_item = self.data.static.get_login_bonus_by_required_days( login_item = self.data.static.get_login_bonus_by_required_days(
self.version, preset["id"], bonus_count self.version, preset["presetId"], bonus_count
) )
if login_item is not None: if login_item is not None:
# now add the present to the database so the # now add the present to the database so the
@@ -108,7 +110,7 @@ class ChuniBase:
self.data.item.put_login_bonus( self.data.item.put_login_bonus(
user_id, user_id,
self.version, self.version,
preset["id"], preset["presetId"],
bonusCount=bonus_count, bonusCount=bonus_count,
lastUpdateDate=last_update_date, lastUpdateDate=last_update_date,
isWatched=False, isWatched=False,
@@ -156,12 +158,18 @@ class ChuniBase:
event_list = [] event_list = []
for evt_row in game_events: for evt_row in game_events:
tmp = {} event_list.append(
tmp["id"] = evt_row["eventId"] {
tmp["type"] = evt_row["type"] "id": evt_row["eventId"],
tmp["startDate"] = "2017-12-05 07:00:00.0" "type": evt_row["type"],
tmp["endDate"] = "2099-12-31 00:00:00.0" # actually use the startDate from the import so it
event_list.append(tmp) # properly shows all the events when new ones are imported
"startDate": datetime.strftime(
evt_row["startDate"], "%Y-%m-%d %H:%M:%S"
),
"endDate": "2099-12-31 00:00:00",
}
)
return { return {
"type": data["type"], "type": data["type"],
@@ -228,29 +236,36 @@ class ChuniBase:
def handle_get_user_character_api_request(self, data: Dict) -> Dict: def handle_get_user_character_api_request(self, data: Dict) -> Dict:
characters = self.data.item.get_characters(data["userId"]) characters = self.data.item.get_characters(data["userId"])
if characters is None: if characters is None:
return {} return {
next_idx = -1 "userId": data["userId"],
"length": 0,
"nextIndex": -1,
"userCharacterList": [],
}
characterList = [] character_list = []
for x in range(int(data["nextIndex"]), len(characters)): next_idx = int(data["nextIndex"])
max_ct = int(data["maxCount"])
for x in range(next_idx, len(characters)):
tmp = characters[x]._asdict() tmp = characters[x]._asdict()
tmp.pop("user") tmp.pop("user")
tmp.pop("id") tmp.pop("id")
characterList.append(tmp) character_list.append(tmp)
if len(characterList) >= int(data["maxCount"]): if len(character_list) >= max_ct:
break break
if len(characterList) >= int(data["maxCount"]) and len(characters) > int( if len(characters) >= next_idx + max_ct:
data["maxCount"] next_idx += max_ct
) + int(data["nextIndex"]): else:
next_idx = int(data["maxCount"]) + int(data["nextIndex"]) + 1 next_idx = -1
return { return {
"userId": data["userId"], "userId": data["userId"],
"length": len(characterList), "length": len(character_list),
"nextIndex": next_idx, "nextIndex": next_idx,
"userCharacterList": characterList, "userCharacterList": character_list,
} }
def handle_get_user_charge_api_request(self, data: Dict) -> Dict: def handle_get_user_charge_api_request(self, data: Dict) -> Dict:
@@ -292,8 +307,8 @@ class ChuniBase:
if len(user_course_list) >= max_ct: if len(user_course_list) >= max_ct:
break break
if len(user_course_list) >= max_ct: if len(user_course_list) >= next_idx + max_ct:
next_idx = next_idx + max_ct next_idx += max_ct
else: else:
next_idx = -1 next_idx = -1
@@ -347,12 +362,23 @@ class ChuniBase:
} }
def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
user_fav_item_list = []
# still needs to be implemented on WebUI
# 1: Music, 3: Character
fav_list = self.data.item.get_all_favorites(
data["userId"], self.version, fav_kind=int(data["kind"])
)
if fav_list is not None:
for fav in fav_list:
user_fav_item_list.append({"id": fav["favId"]})
return { return {
"userId": data["userId"], "userId": data["userId"],
"length": 0, "length": len(user_fav_item_list),
"kind": data["kind"], "kind": data["kind"],
"nextIndex": -1, "nextIndex": -1,
"userFavoriteItemList": [], "userFavoriteItemList": user_fav_item_list,
} }
def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict: def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict:
@@ -375,7 +401,7 @@ class ChuniBase:
"userItemList": [], "userItemList": [],
} }
items: list[Dict[str, Any]] = [] items: List[Dict[str, Any]] = []
for i in range(next_idx, len(user_item_list)): for i in range(next_idx, len(user_item_list)):
tmp = user_item_list[i]._asdict() tmp = user_item_list[i]._asdict()
tmp.pop("user") tmp.pop("user")
@@ -387,13 +413,13 @@ class ChuniBase:
xout = kind * 10000000000 + next_idx + len(items) xout = kind * 10000000000 + next_idx + len(items)
if len(items) < int(data["maxCount"]): if len(items) < int(data["maxCount"]):
nextIndex = 0 next_idx = 0
else: else:
nextIndex = xout next_idx = xout
return { return {
"userId": data["userId"], "userId": data["userId"],
"nextIndex": nextIndex, "nextIndex": next_idx,
"itemKind": kind, "itemKind": kind,
"length": len(items), "length": len(items),
"userItemList": items, "userItemList": items,
@@ -452,6 +478,7 @@ class ChuniBase:
"nextIndex": -1, "nextIndex": -1,
"userMusicList": [], # 240 "userMusicList": [], # 240
} }
song_list = [] song_list = []
next_idx = int(data["nextIndex"]) next_idx = int(data["nextIndex"])
max_ct = int(data["maxCount"]) max_ct = int(data["maxCount"])
@@ -474,10 +501,10 @@ class ChuniBase:
if len(song_list) >= max_ct: if len(song_list) >= max_ct:
break break
if len(song_list) >= max_ct: if len(song_list) >= next_idx + max_ct:
next_idx += max_ct next_idx += max_ct
else: else:
next_idx = 0 next_idx = -1
return { return {
"userId": data["userId"], "userId": data["userId"],
@@ -623,12 +650,15 @@ class ChuniBase:
self.data.profile.put_profile_data( self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0] user_id, self.version, upsert["userData"][0]
) )
if "userDataEx" in upsert: if "userDataEx" in upsert:
self.data.profile.put_profile_data_ex( self.data.profile.put_profile_data_ex(
user_id, self.version, upsert["userDataEx"][0] user_id, self.version, upsert["userDataEx"][0]
) )
if "userGameOption" in upsert: if "userGameOption" in upsert:
self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0]) self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0])
if "userGameOptionEx" in upsert: if "userGameOptionEx" in upsert:
self.data.profile.put_profile_option_ex( self.data.profile.put_profile_option_ex(
user_id, upsert["userGameOptionEx"][0] user_id, upsert["userGameOptionEx"][0]
@@ -672,6 +702,10 @@ class ChuniBase:
if "userPlaylogList" in upsert: if "userPlaylogList" in upsert:
for playlog in upsert["userPlaylogList"]: for playlog in upsert["userPlaylogList"]:
# convert the player names to utf-8
playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"])
playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"])
playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"])
self.data.score.put_playlog(user_id, playlog) self.data.score.put_playlog(user_id, playlog)
if "userTeamPoint" in upsert: if "userTeamPoint" in upsert:

View File

@@ -17,21 +17,23 @@ class ChuniConstants:
VER_CHUNITHM_PARADISE = 10 VER_CHUNITHM_PARADISE = 10
VER_CHUNITHM_NEW = 11 VER_CHUNITHM_NEW = 11
VER_CHUNITHM_NEW_PLUS = 12 VER_CHUNITHM_NEW_PLUS = 12
VER_CHUNITHM_SUN = 13
VERSION_NAMES = [ VERSION_NAMES = [
"Chunithm", "CHUNITHM",
"Chunithm+", "CHUNITHM PLUS",
"Chunithm Air", "CHUNITHM AIR",
"Chunithm Air+", "CHUNITHM AIR PLUS",
"Chunithm Star", "CHUNITHM STAR",
"Chunithm Star+", "CHUNITHM STAR PLUS",
"Chunithm Amazon", "CHUNITHM AMAZON",
"Chunithm Amazon+", "CHUNITHM AMAZON PLUS",
"Chunithm Crystal", "CHUNITHM CRYSTAL",
"Chunithm Crystal+", "CHUNITHM CRYSTAL PLUS",
"Chunithm Paradise", "CHUNITHM PARADISE",
"Chunithm New!!", "CHUNITHM NEW!!",
"Chunithm New!!+", "CHUNITHM NEW PLUS!!",
"CHUNITHM SUN"
] ]
@classmethod @classmethod

View File

@@ -29,6 +29,7 @@ from titles.chuni.crystalplus import ChuniCrystalPlus
from titles.chuni.paradise import ChuniParadise from titles.chuni.paradise import ChuniParadise
from titles.chuni.new import ChuniNew from titles.chuni.new import ChuniNew
from titles.chuni.newplus import ChuniNewPlus from titles.chuni.newplus import ChuniNewPlus
from titles.chuni.sun import ChuniSun
class ChuniServlet: class ChuniServlet:
@@ -55,6 +56,7 @@ class ChuniServlet:
ChuniParadise, ChuniParadise,
ChuniNew, ChuniNew,
ChuniNewPlus, ChuniNewPlus,
ChuniSun,
] ]
self.logger = logging.getLogger("chuni") self.logger = logging.getLogger("chuni")
@@ -96,15 +98,18 @@ class ChuniServlet:
] ]
for method in method_list: for method in method_list:
method_fixed = inflection.camelize(method)[6:-7] method_fixed = inflection.camelize(method)[6:-7]
# number of iterations was changed to 70 in SUN
iter_count = 70 if version >= ChuniConstants.VER_CHUNITHM_SUN else 44
hash = PBKDF2( hash = PBKDF2(
method_fixed, method_fixed,
bytes.fromhex(keys[2]), bytes.fromhex(keys[2]),
128, 128,
count=44, count=iter_count,
hmac_hash_module=SHA1, hmac_hash_module=SHA1,
) )
self.hash_table[version][hash.hex()] = method_fixed hashed_name = hash.hex()[:32] # truncate unused bytes like the game does
self.hash_table[version][hashed_name] = method_fixed
self.logger.debug( self.logger.debug(
f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}" f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}"
@@ -145,30 +150,32 @@ class ChuniServlet:
if version < 105: # 1.0 if version < 105: # 1.0
internal_ver = ChuniConstants.VER_CHUNITHM internal_ver = ChuniConstants.VER_CHUNITHM
elif version >= 105 and version < 110: # Plus elif version >= 105 and version < 110: # PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_PLUS
elif version >= 110 and version < 115: # Air elif version >= 110 and version < 115: # AIR
internal_ver = ChuniConstants.VER_CHUNITHM_AIR internal_ver = ChuniConstants.VER_CHUNITHM_AIR
elif version >= 115 and version < 120: # Air Plus elif version >= 115 and version < 120: # AIR PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS
elif version >= 120 and version < 125: # Star elif version >= 120 and version < 125: # STAR
internal_ver = ChuniConstants.VER_CHUNITHM_STAR internal_ver = ChuniConstants.VER_CHUNITHM_STAR
elif version >= 125 and version < 130: # Star Plus elif version >= 125 and version < 130: # STAR PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS
elif version >= 130 and version < 135: # Amazon elif version >= 130 and version < 135: # AMAZON
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON
elif version >= 135 and version < 140: # Amazon Plus elif version >= 135 and version < 140: # AMAZON PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
elif version >= 140 and version < 145: # Crystal elif version >= 140 and version < 145: # CRYSTAL
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL
elif version >= 145 and version < 150: # Crystal Plus elif version >= 145 and version < 150: # CRYSTAL PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
elif version >= 150 and version < 200: # Paradise elif version >= 150 and version < 200: # PARADISE
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE
elif version >= 200 and version < 205: # New elif version >= 200 and version < 205: # NEW!!
internal_ver = ChuniConstants.VER_CHUNITHM_NEW internal_ver = ChuniConstants.VER_CHUNITHM_NEW
elif version >= 205 and version < 210: # New Plus elif version >= 205 and version < 210: # NEW PLUS!!
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
elif version >= 210: # SUN
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
# If we get a 32 character long hex string, it's a hash and we're # If we get a 32 character long hex string, it's a hash and we're

View File

@@ -23,41 +23,44 @@ class ChuniNew(ChuniBase):
self.version = ChuniConstants.VER_CHUNITHM_NEW self.version = ChuniConstants.VER_CHUNITHM_NEW
def handle_get_game_setting_api_request(self, data: Dict) -> Dict: def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
# use UTC time and convert it to JST time by adding +9
# matching therefore starts one hour before and lasts for 8 hours
match_start = datetime.strftime( match_start = datetime.strftime(
datetime.now() - timedelta(hours=10), self.date_time_format datetime.utcnow() + timedelta(hours=8), self.date_time_format
) )
match_end = datetime.strftime( match_end = datetime.strftime(
datetime.now() + timedelta(hours=10), self.date_time_format datetime.utcnow() + timedelta(hours=16), self.date_time_format
) )
reboot_start = datetime.strftime( reboot_start = datetime.strftime(
datetime.now() - timedelta(hours=11), self.date_time_format datetime.utcnow() + timedelta(hours=6), self.date_time_format
) )
reboot_end = datetime.strftime( reboot_end = datetime.strftime(
datetime.now() - timedelta(hours=10), self.date_time_format datetime.utcnow() + timedelta(hours=7), self.date_time_format
) )
return { return {
"gameSetting": { "gameSetting": {
"isMaintenance": "false", "isMaintenance": False,
"requestInterval": 10, "requestInterval": 10,
"rebootStartTime": reboot_start, "rebootStartTime": reboot_start,
"rebootEndTime": reboot_end, "rebootEndTime": reboot_end,
"isBackgroundDistribute": "false", "isBackgroundDistribute": False,
"maxCountCharacter": 300, "maxCountCharacter": 300,
"maxCountItem": 300, "maxCountItem": 300,
"maxCountMusic": 300, "maxCountMusic": 300,
"matchStartTime": match_start, "matchStartTime": match_start,
"matchEndTime": match_end, "matchEndTime": match_end,
"matchTimeLimit": 99, "matchTimeLimit": 60,
"matchErrorLimit": 9999, "matchErrorLimit": 9999,
"romVersion": self.game_cfg.version.version(self.version)["rom"], "romVersion": self.game_cfg.version.version(self.version)["rom"],
"dataVersion": self.game_cfg.version.version(self.version)["data"], "dataVersion": self.game_cfg.version.version(self.version)["data"],
"matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
"matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
# might be really important for online battle to connect the cabs via UDP port 50201
"udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
"reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
}, },
"isDumpUpload": "false", "isDumpUpload": False,
"isAou": "false", "isAou": False,
} }
def handle_remove_token_api_request(self, data: Dict) -> Dict: def handle_remove_token_api_request(self, data: Dict) -> Dict:
@@ -468,3 +471,162 @@ class ChuniNew(ChuniBase):
self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True) self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True)
return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"} return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"}
def handle_ping_request(self, data: Dict) -> Dict:
# matchmaking ping request
return {"returnCode": "1"}
def handle_begin_matching_api_request(self, data: Dict) -> Dict:
room_id = 1
# check if there is a free matching room
matching_room = self.data.item.get_oldest_free_matching(self.version)
if matching_room is None:
# grab the latest roomId and add 1 for the new room
newest_matching = self.data.item.get_newest_matching(self.version)
if newest_matching is not None:
room_id = newest_matching["roomId"] + 1
# fix userName WTF8
new_member = data["matchingMemberInfo"]
new_member["userName"] = self.read_wtf8(new_member["userName"])
# create the new room with room_id and the current user id (host)
# user id is required for the countdown later on
self.data.item.put_matching(
self.version, room_id, [new_member], user_id=new_member["userId"]
)
# get the newly created matching room
matching_room = self.data.item.get_matching(self.version, room_id)
else:
# a room already exists, so just add the new member to it
matching_member_list = matching_room["matchingMemberInfoList"]
# fix userName WTF8
new_member = data["matchingMemberInfo"]
new_member["userName"] = self.read_wtf8(new_member["userName"])
matching_member_list.append(new_member)
# add the updated room to the database, make sure to set isFull correctly!
self.data.item.put_matching(
self.version,
matching_room["roomId"],
matching_member_list,
user_id=matching_room["user"],
is_full=True if len(matching_member_list) >= 4 else False,
)
matching_wait = {
"isFinish": False,
"restMSec": matching_room["restMSec"], # in sec
"pollingInterval": 1, # in sec
"matchingMemberInfoList": matching_room["matchingMemberInfoList"],
}
return {"roomId": 1, "matchingWaitState": matching_wait}
def handle_end_matching_api_request(self, data: Dict) -> Dict:
matching_room = self.data.item.get_matching(self.version, data["roomId"])
members = matching_room["matchingMemberInfoList"]
# only set the host user to role 1 every other to 0?
role_list = [
{"role": 1} if m["userId"] == matching_room["user"] else {"role": 0}
for m in members
]
self.data.item.put_matching(
self.version,
matching_room["roomId"],
members,
user_id=matching_room["user"],
rest_sec=0, # make sure to always set 0
is_full=True, # and full, so no one can join
)
return {
"matchingResult": 1, # needs to be 1 for successful matching
"matchingMemberInfoList": members,
# no idea, maybe to differentiate between CPUs and real players?
"matchingMemberRoleList": role_list,
# TCP/UDP connection?
"reflectorUri": f"{self.core_cfg.title.hostname}",
}
def handle_remove_matching_member_api_request(self, data: Dict) -> Dict:
# get all matching rooms, because Chuni only returns the userId
# not the actual roomId
matching_rooms = self.data.item.get_all_matchings(self.version)
if matching_rooms is None:
return {"returnCode": "1"}
for room in matching_rooms:
old_members = room["matchingMemberInfoList"]
new_members = [m for m in old_members if m["userId"] != data["userId"]]
# if nothing changed go to the next room
if len(old_members) == len(new_members):
continue
# if the last user got removed, delete the matching room
if len(new_members) <= 0:
self.data.item.delete_matching(self.version, room["roomId"])
else:
# remove the user from the room
self.data.item.put_matching(
self.version,
room["roomId"],
new_members,
user_id=room["user"],
rest_sec=room["restMSec"],
)
return {"returnCode": "1"}
def handle_get_matching_state_api_request(self, data: Dict) -> Dict:
polling_interval = 1
# get the current active room
matching_room = self.data.item.get_matching(self.version, data["roomId"])
members = matching_room["matchingMemberInfoList"]
rest_sec = matching_room["restMSec"]
# grab the current member
current_member = data["matchingMemberInfo"]
# only the host user can decrease the countdown
if matching_room["user"] == int(current_member["userId"]):
# cap the restMSec to 0
if rest_sec > 0:
rest_sec -= polling_interval
else:
rest_sec = 0
# update the members in order to recieve messages
for i, member in enumerate(members):
if member["userId"] == current_member["userId"]:
# replace the old user data with the current user data,
# also parse WTF-8 everytime
current_member["userName"] = self.read_wtf8(current_member["userName"])
members[i] = current_member
self.data.item.put_matching(
self.version,
data["roomId"],
members,
rest_sec=rest_sec,
user_id=matching_room["user"],
)
# only add the other members to the list
diff_members = [m for m in members if m["userId"] != current_member["userId"]]
matching_wait = {
# makes no difference? Always use False?
"isFinish": True if rest_sec == 0 else False,
"restMSec": rest_sec,
"pollingInterval": polling_interval,
# the current user needs to be the first one?
"matchingMemberInfoList": [current_member] + diff_members,
}
return {"matchingWaitState": matching_wait}

View File

@@ -36,6 +36,6 @@ class ChuniNewPlus(ChuniNew):
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
user_data = super().handle_cm_get_user_preview_api_request(data) user_data = super().handle_cm_get_user_preview_api_request(data)
# hardcode lastDataVersion for CardMaker 1.35 # hardcode lastDataVersion for CardMaker 1.35 A028
user_data["lastDataVersion"] = "2.05.00" user_data["lastDataVersion"] = "2.05.00"
return user_data return user_data

View File

@@ -1,5 +1,12 @@
from typing import Dict, List, Optional from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ from sqlalchemy import (
Table,
Column,
UniqueConstraint,
PrimaryKeyConstraint,
and_,
delete,
)
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.engine.base import Connection from sqlalchemy.engine.base import Connection
from sqlalchemy.schema import ForeignKey from sqlalchemy.schema import ForeignKey
@@ -203,8 +210,141 @@ login_bonus = Table(
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
favorite = Table(
"chuni_item_favorite",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("favId", Integer, nullable=False),
Column("favKind", Integer, nullable=False, server_default="1"),
UniqueConstraint("version", "user", "favId", name="chuni_item_favorite_uk"),
mysql_charset="utf8mb4",
)
matching = Table(
"chuni_item_matching",
metadata,
Column("roomId", Integer, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("restMSec", Integer, nullable=False, server_default="60"),
Column("isFull", Boolean, nullable=False, server_default="0"),
PrimaryKeyConstraint("roomId", "version", name="chuni_item_matching_pk"),
Column("matchingMemberInfoList", JSON, nullable=False),
mysql_charset="utf8mb4",
)
class ChuniItemData(BaseData): class ChuniItemData(BaseData):
def get_oldest_free_matching(self, version: int) -> Optional[Row]:
sql = matching.select(
and_(
matching.c.version == version,
matching.c.isFull == False
)
).order_by(matching.c.roomId.asc())
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_newest_matching(self, version: int) -> Optional[Row]:
sql = matching.select(
and_(
matching.c.version == version
)
).order_by(matching.c.roomId.desc())
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_all_matchings(self, version: int) -> Optional[List[Row]]:
sql = matching.select(
and_(
matching.c.version == version
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_matching(self, version: int, room_id: int) -> Optional[Row]:
sql = matching.select(
and_(matching.c.version == version, matching.c.roomId == room_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_matching(
self,
version: int,
room_id: int,
matching_member_info_list: List,
user_id: int = None,
rest_sec: int = 60,
is_full: bool = False
) -> Optional[int]:
sql = insert(matching).values(
roomId=room_id,
version=version,
restMSec=rest_sec,
user=user_id,
isFull=is_full,
matchingMemberInfoList=matching_member_info_list,
)
conflict = sql.on_duplicate_key_update(
restMSec=rest_sec, matchingMemberInfoList=matching_member_info_list
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def delete_matching(self, version: int, room_id: int):
sql = delete(matching).where(
and_(matching.c.roomId == room_id, matching.c.version == version)
)
result = self.execute(sql)
if result is None:
return None
return result.lastrowid
def get_all_favorites(
self, user_id: int, version: int, fav_kind: int = 1
) -> Optional[List[Row]]:
sql = favorite.select(
and_(
favorite.c.version == version,
favorite.c.user == user_id,
favorite.c.favKind == fav_kind,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_login_bonus( def put_login_bonus(
self, user_id: int, version: int, preset_id: int, **login_bonus_data self, user_id: int, version: int, preset_id: int, **login_bonus_data
) -> Optional[int]: ) -> Optional[int]:

View File

@@ -89,8 +89,6 @@ profile = Table(
Integer, Integer,
ForeignKey("chuni_profile_team.id", ondelete="SET NULL", onupdate="SET NULL"), ForeignKey("chuni_profile_team.id", ondelete="SET NULL", onupdate="SET NULL"),
), ),
Column("avatarBack", Integer, server_default="0"),
Column("avatarFace", Integer, server_default="0"),
Column("eliteRankPoint", Integer, server_default="0"), Column("eliteRankPoint", Integer, server_default="0"),
Column("stockedGridCount", Integer, server_default="0"), Column("stockedGridCount", Integer, server_default="0"),
Column("netBattleLoseCount", Integer, server_default="0"), Column("netBattleLoseCount", Integer, server_default="0"),
@@ -98,10 +96,8 @@ profile = Table(
Column("netBattle4thCount", Integer, server_default="0"), Column("netBattle4thCount", Integer, server_default="0"),
Column("overPowerRate", Integer, server_default="0"), Column("overPowerRate", Integer, server_default="0"),
Column("battleRewardStatus", Integer, server_default="0"), Column("battleRewardStatus", Integer, server_default="0"),
Column("avatarPoint", Integer, server_default="0"),
Column("netBattle1stCount", Integer, server_default="0"), Column("netBattle1stCount", Integer, server_default="0"),
Column("charaIllustId", Integer, server_default="0"), Column("charaIllustId", Integer, server_default="0"),
Column("avatarItem", Integer, server_default="0"),
Column("userNameEx", String(8), server_default=""), Column("userNameEx", String(8), server_default=""),
Column("netBattleWinCount", Integer, server_default="0"), Column("netBattleWinCount", Integer, server_default="0"),
Column("netBattleCorrection", Integer, server_default="0"), Column("netBattleCorrection", Integer, server_default="0"),
@@ -112,7 +108,6 @@ profile = Table(
Column("netBattle3rdCount", Integer, server_default="0"), Column("netBattle3rdCount", Integer, server_default="0"),
Column("netBattleConsecutiveWinCount", Integer, server_default="0"), Column("netBattleConsecutiveWinCount", Integer, server_default="0"),
Column("overPowerLowerRank", Integer, server_default="0"), Column("overPowerLowerRank", Integer, server_default="0"),
Column("avatarWear", Integer, server_default="0"),
Column("classEmblemBase", Integer, server_default="0"), Column("classEmblemBase", Integer, server_default="0"),
Column("battleRankPoint", Integer, server_default="0"), Column("battleRankPoint", Integer, server_default="0"),
Column("netBattle2ndCount", Integer, server_default="0"), Column("netBattle2ndCount", Integer, server_default="0"),
@@ -120,13 +115,19 @@ profile = Table(
Column("skillId", Integer, server_default="0"), Column("skillId", Integer, server_default="0"),
Column("lastCountryCode", String(5), server_default="JPN"), Column("lastCountryCode", String(5), server_default="JPN"),
Column("isNetBattleHost", Boolean, server_default="0"), Column("isNetBattleHost", Boolean, server_default="0"),
Column("avatarFront", Integer, server_default="0"),
Column("avatarSkin", Integer, server_default="0"),
Column("battleRewardCount", Integer, server_default="0"), Column("battleRewardCount", Integer, server_default="0"),
Column("battleRewardIndex", Integer, server_default="0"), Column("battleRewardIndex", Integer, server_default="0"),
Column("netBattlePlayCount", Integer, server_default="0"), Column("netBattlePlayCount", Integer, server_default="0"),
Column("exMapLoopCount", Integer, server_default="0"), Column("exMapLoopCount", Integer, server_default="0"),
Column("netBattleEndState", Integer, server_default="0"), Column("netBattleEndState", Integer, server_default="0"),
Column("rankUpChallengeResults", JSON),
Column("avatarBack", Integer, server_default="0"),
Column("avatarFace", Integer, server_default="0"),
Column("avatarPoint", Integer, server_default="0"),
Column("avatarItem", Integer, server_default="0"),
Column("avatarWear", Integer, server_default="0"),
Column("avatarFront", Integer, server_default="0"),
Column("avatarSkin", Integer, server_default="0"),
Column("avatarHead", Integer, server_default="0"), Column("avatarHead", Integer, server_default="0"),
UniqueConstraint("user", "version", name="chuni_profile_profile_uk"), UniqueConstraint("user", "version", name="chuni_profile_profile_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
@@ -417,8 +418,8 @@ class ChuniProfileData(BaseData):
sql = ( sql = (
select([profile, option]) select([profile, option])
.join(option, profile.c.user == option.c.user) .join(option, profile.c.user == option.c.user)
.filter(and_(profile.c.user == aime_id, profile.c.version == version)) .filter(and_(profile.c.user == aime_id, profile.c.version <= version))
) ).order_by(profile.c.version.desc())
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
@@ -429,9 +430,9 @@ class ChuniProfileData(BaseData):
sql = select(profile).where( sql = select(profile).where(
and_( and_(
profile.c.user == aime_id, profile.c.user == aime_id,
profile.c.version == version, profile.c.version <= version,
)
) )
).order_by(profile.c.version.desc())
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
@@ -461,9 +462,9 @@ class ChuniProfileData(BaseData):
sql = select(profile_ex).where( sql = select(profile_ex).where(
and_( and_(
profile_ex.c.user == aime_id, profile_ex.c.user == aime_id,
profile_ex.c.version == version, profile_ex.c.version <= version,
)
) )
).order_by(profile_ex.c.version.desc())
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:

View File

@@ -134,7 +134,9 @@ playlog = Table(
Column("charaIllustId", Integer), Column("charaIllustId", Integer),
Column("romVersion", String(255)), Column("romVersion", String(255)),
Column("judgeHeaven", Integer), Column("judgeHeaven", Integer),
mysql_charset="utf8mb4", Column("regionId", Integer),
Column("machineType", Integer),
mysql_charset="utf8mb4"
) )

View File

@@ -1,11 +1,19 @@
from typing import Dict, List, Optional from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ from sqlalchemy import (
ForeignKeyConstraint,
Table,
Column,
UniqueConstraint,
PrimaryKeyConstraint,
and_,
)
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, Float from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, Float
from sqlalchemy.engine.base import Connection from sqlalchemy.engine.base import Connection
from sqlalchemy.engine import Row from sqlalchemy.engine import Row
from sqlalchemy.schema import ForeignKey from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert from sqlalchemy.dialects.mysql import insert
from datetime import datetime
from core.data.schema import BaseData, metadata from core.data.schema import BaseData, metadata
@@ -17,6 +25,7 @@ events = Table(
Column("eventId", Integer), Column("eventId", Integer),
Column("type", Integer), Column("type", Integer),
Column("name", String(255)), Column("name", String(255)),
Column("startDate", TIMESTAMP, server_default=func.now()),
Column("enabled", Boolean, server_default="1"), Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "eventId", name="chuni_static_events_uk"), UniqueConstraint("version", "eventId", name="chuni_static_events_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
@@ -125,11 +134,13 @@ gacha_cards = Table(
login_bonus_preset = Table( login_bonus_preset = Table(
"chuni_static_login_bonus_preset", "chuni_static_login_bonus_preset",
metadata, metadata,
Column("id", Integer, primary_key=True, nullable=False), Column("presetId", Integer, nullable=False),
Column("version", Integer, nullable=False), Column("version", Integer, nullable=False),
Column("presetName", String(255), nullable=False), Column("presetName", String(255), nullable=False),
Column("isEnabled", Boolean, server_default="1"), Column("isEnabled", Boolean, server_default="1"),
UniqueConstraint("version", "id", name="chuni_static_login_bonus_preset_uk"), PrimaryKeyConstraint(
"presetId", "version", name="chuni_static_login_bonus_preset_pk"
),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -138,15 +149,7 @@ login_bonus = Table(
metadata, metadata,
Column("id", Integer, primary_key=True, nullable=False), Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False), Column("version", Integer, nullable=False),
Column( Column("presetId", Integer, nullable=False),
"presetId",
ForeignKey(
"chuni_static_login_bonus_preset.id",
ondelete="cascade",
onupdate="cascade",
),
nullable=False,
),
Column("loginBonusId", Integer, nullable=False), Column("loginBonusId", Integer, nullable=False),
Column("loginBonusName", String(255), nullable=False), Column("loginBonusName", String(255), nullable=False),
Column("presentId", Integer, nullable=False), Column("presentId", Integer, nullable=False),
@@ -157,6 +160,16 @@ login_bonus = Table(
UniqueConstraint( UniqueConstraint(
"version", "presetId", "loginBonusId", name="chuni_static_login_bonus_uk" "version", "presetId", "loginBonusId", name="chuni_static_login_bonus_uk"
), ),
ForeignKeyConstraint(
["presetId", "version"],
[
"chuni_static_login_bonus_preset.presetId",
"chuni_static_login_bonus_preset.version",
],
onupdate="CASCADE",
ondelete="CASCADE",
name="chuni_static_login_bonus_ibfk_1",
),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -236,7 +249,7 @@ class ChuniStaticData(BaseData):
self, version: int, preset_id: int, preset_name: str, is_enabled: bool self, version: int, preset_id: int, preset_name: str, is_enabled: bool
) -> Optional[int]: ) -> Optional[int]:
sql = insert(login_bonus_preset).values( sql = insert(login_bonus_preset).values(
id=preset_id, presetId=preset_id,
version=version, version=version,
presetName=preset_name, presetName=preset_name,
isEnabled=is_enabled, isEnabled=is_enabled,
@@ -416,6 +429,14 @@ class ChuniStaticData(BaseData):
return None return None
return result.fetchall() return result.fetchall()
def get_music(self, version: int) -> Optional[List[Row]]:
sql = music.select(music.c.version <= version)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_music_chart( def get_music_chart(
self, version: int, song_id: int, chart_id: int self, version: int, song_id: int, chart_id: int
) -> Optional[List[Row]]: ) -> Optional[List[Row]]:

37
titles/chuni/sun.py Normal file
View File

@@ -0,0 +1,37 @@
from typing import Dict, Any
from core.config import CoreConfig
from titles.chuni.newplus import ChuniNewPlus
from titles.chuni.const import ChuniConstants
from titles.chuni.config import ChuniConfig
class ChuniSun(ChuniNewPlus):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_SUN
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)["rom"]
ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"]
ret["gameSetting"][
"matchingUri"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
ret["gameSetting"][
"matchingUriX"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
ret["gameSetting"][
"udpHolePunchUri"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
ret["gameSetting"][
"reflectorUri"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
return ret
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
user_data = super().handle_cm_get_user_preview_api_request(data)
# hardcode lastDataVersion for CardMaker 1.35 A032
user_data["lastDataVersion"] = "2.10.00"
return user_data

View File

@@ -23,19 +23,40 @@ class CardMakerBase:
self.game = CardMakerConstants.GAME_CODE self.game = CardMakerConstants.GAME_CODE
self.version = CardMakerConstants.VER_CARD_MAKER self.version = CardMakerConstants.VER_CARD_MAKER
@staticmethod
def _parse_int_ver(version: str) -> str:
return version.replace(".", "")[:3]
def handle_get_game_connect_api_request(self, data: Dict) -> Dict: def handle_get_game_connect_api_request(self, data: Dict) -> Dict:
if self.core_cfg.server.is_develop: if self.core_cfg.server.is_develop:
uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}" uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}"
else: else:
uri = f"http://{self.core_cfg.title.hostname}" uri = f"http://{self.core_cfg.title.hostname}"
# CHUNITHM = 0, maimai = 1, ONGEKI = 2 # grab the dict with all games version numbers from user config
games_ver = self.game_cfg.version.version(self.version)
return { return {
"length": 3, "length": 3,
"gameConnectList": [ "gameConnectList": [
{"modelKind": 0, "type": 1, "titleUri": f"{uri}/SDHD/200/"}, # CHUNITHM
{"modelKind": 1, "type": 1, "titleUri": f"{uri}/SDEZ/120/"}, {
{"modelKind": 2, "type": 1, "titleUri": f"{uri}/SDDT/130/"}, "modelKind": 0,
"type": 1,
"titleUri": f"{uri}/SDHD/{self._parse_int_ver(games_ver['chuni'])}/",
},
# maimai DX
{
"modelKind": 1,
"type": 1,
"titleUri": f"{uri}/SDEZ/{self._parse_int_ver(games_ver['maimai'])}/",
},
# ONGEKI
{
"modelKind": 2,
"type": 1,
"titleUri": f"{uri}/SDDT/{self._parse_int_ver(games_ver['ongeki'])}/",
},
], ],
} }
@@ -47,12 +68,15 @@ class CardMakerBase:
datetime.now() + timedelta(hours=4), self.date_time_format datetime.now() + timedelta(hours=4), self.date_time_format
) )
# grab the dict with all games version numbers from user config
games_ver = self.game_cfg.version.version(self.version)
return { return {
"gameSetting": { "gameSetting": {
"dataVersion": "1.30.00", "dataVersion": "1.30.00",
"ongekiCmVersion": "1.30.01", "ongekiCmVersion": games_ver["ongeki"],
"chuniCmVersion": "2.00.00", "chuniCmVersion": games_ver["chuni"],
"maimaiCmVersion": "1.20.00", "maimaiCmVersion": games_ver["maimai"],
"requestInterval": 10, "requestInterval": 10,
"rebootStartTime": reboot_start, "rebootStartTime": reboot_start,
"rebootEndTime": reboot_end, "rebootEndTime": reboot_end,

View File

@@ -1,8 +1,4 @@
from datetime import date, datetime, timedelta from typing import Dict
from typing import Any, Dict, List
import json
import logging
from enum import Enum
from core.config import CoreConfig from core.config import CoreConfig
from core.data.cache import cached from core.data.cache import cached
@@ -16,23 +12,7 @@ class CardMaker135(CardMakerBase):
super().__init__(core_cfg, game_cfg) super().__init__(core_cfg, game_cfg)
self.version = CardMakerConstants.VER_CARD_MAKER_135 self.version = CardMakerConstants.VER_CARD_MAKER_135
def handle_get_game_connect_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_connect_api_request(data)
if self.core_cfg.server.is_develop:
uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}"
else:
uri = f"http://{self.core_cfg.title.hostname}"
ret["gameConnectList"][0]["titleUri"] = f"{uri}/SDHD/205/"
ret["gameConnectList"][1]["titleUri"] = f"{uri}/SDEZ/125/"
ret["gameConnectList"][2]["titleUri"] = f"{uri}/SDDT/135/"
return ret
def handle_get_game_setting_api_request(self, data: Dict) -> Dict: def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data) ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["dataVersion"] = "1.35.00" ret["gameSetting"]["dataVersion"] = "1.35.00"
ret["gameSetting"]["ongekiCmVersion"] = "1.35.03"
ret["gameSetting"]["chuniCmVersion"] = "2.05.00"
ret["gameSetting"]["maimaiCmVersion"] = "1.25.00"
return ret return ret

View File

@@ -1,3 +1,4 @@
from typing import Dict
from core.config import CoreConfig from core.config import CoreConfig
@@ -20,6 +21,21 @@ class CardMakerServerConfig:
) )
class CardMakerVersionConfig:
def __init__(self, parent_config: "CardMakerConfig") -> None:
self.__config = parent_config
def version(self, version: int) -> Dict:
"""
in the form of:
1: {"ongeki": 1.30.01, "chuni": 2.00.00, "maimai": 1.20.00}
"""
return CoreConfig.get_config_field(
self.__config, "cardmaker", "version", default={}
)[version]
class CardMakerConfig(dict): class CardMakerConfig(dict):
def __init__(self) -> None: def __init__(self) -> None:
self.server = CardMakerServerConfig(self) self.server = CardMakerServerConfig(self)
self.version = CardMakerVersionConfig(self)

View File

@@ -6,7 +6,7 @@ class CardMakerConstants:
VER_CARD_MAKER = 0 VER_CARD_MAKER = 0
VER_CARD_MAKER_135 = 1 VER_CARD_MAKER_135 = 1
VERSION_NAMES = ("Card Maker 1.34", "Card Maker 1.35") VERSION_NAMES = ("Card Maker 1.30", "Card Maker 1.35")
@classmethod @classmethod
def game_ver_to_string(cls, ver: int): def game_ver_to_string(cls, ver: int):

View File

@@ -30,7 +30,7 @@ class CardMakerServlet:
self.versions = [ self.versions = [
CardMakerBase(core_cfg, self.game_cfg), CardMakerBase(core_cfg, self.game_cfg),
CardMaker135(core_cfg, self.game_cfg), CardMaker135(core_cfg, self.game_cfg)
] ]
self.logger = logging.getLogger("cardmaker") self.logger = logging.getLogger("cardmaker")
@@ -89,7 +89,7 @@ class CardMakerServlet:
if version >= 130 and version < 135: # Card Maker if version >= 130 and version < 135: # Card Maker
internal_ver = CardMakerConstants.VER_CARD_MAKER internal_ver = CardMakerConstants.VER_CARD_MAKER
elif version >= 135 and version < 136: # Card Maker 1.35 elif version >= 135 and version < 140: # Card Maker 1.35
internal_ver = CardMakerConstants.VER_CARD_MAKER_135 internal_ver = CardMakerConstants.VER_CARD_MAKER_135
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:

View File

@@ -2,7 +2,7 @@ import logging
import json import json
from decimal import Decimal from decimal import Decimal
from base64 import b64encode from base64 import b64encode
from typing import Any, Dict from typing import Any, Dict, List
from hashlib import md5 from hashlib import md5
from datetime import datetime from datetime import datetime
@@ -416,7 +416,7 @@ class CxbBase:
self.logger.info(f"Get best rankings for {uid}") self.logger.info(f"Get best rankings for {uid}")
p = self.data.score.get_best_rankings(uid) p = self.data.score.get_best_rankings(uid)
rankList: list[Dict[str, Any]] = [] rankList: List[Dict[str, Any]] = []
for rank in p: for rank in p:
if rank["song_id"] is not None: if rank["song_id"] is not None:

View File

@@ -103,7 +103,7 @@ class CxbServlet(resource.Resource):
else: else:
self.logger.info(f"Ready on port {self.game_cfg.server.port}") self.logger.info(f"Ready on port {self.game_cfg.server.port}")
def render_POST(self, request: Request): def render_POST(self, request: Request, version: int, endpoint: str):
version = 0 version = 0
internal_ver = 0 internal_ver = 0
func_to_find = "" func_to_find = ""

View File

@@ -83,7 +83,13 @@ class IDZUserDBProtocol(Protocol):
def dataReceived(self, data: bytes) -> None: def dataReceived(self, data: bytes) -> None:
self.logger.debug(f"Receive data {data.hex()}") self.logger.debug(f"Receive data {data.hex()}")
crypt = AES.new(self.static_key, AES.MODE_ECB) crypt = AES.new(self.static_key, AES.MODE_ECB)
try:
data_dec = crypt.decrypt(data) data_dec = crypt.decrypt(data)
except Exception as e:
self.logger.error(f"Failed to decrypt UserDB request from {self.transport.getPeer().host} because {e} - {data.hex()}")
self.logger.debug(f"Decrypt data {data_dec.hex()}") self.logger.debug(f"Decrypt data {data_dec.hex()}")
magic = struct.unpack_from("<I", data_dec, 0)[0] magic = struct.unpack_from("<I", data_dec, 0)[0]

View File

@@ -6,5 +6,14 @@ from titles.mai2.read import Mai2Reader
index = Mai2Servlet index = Mai2Servlet
database = Mai2Data database = Mai2Data
reader = Mai2Reader reader = Mai2Reader
game_codes = [Mai2Constants.GAME_CODE] game_codes = [
current_schema_version = 4 Mai2Constants.GAME_CODE_DX,
Mai2Constants.GAME_CODE_FINALE,
Mai2Constants.GAME_CODE_MILK,
Mai2Constants.GAME_CODE_MURASAKI,
Mai2Constants.GAME_CODE_PINK,
Mai2Constants.GAME_CODE_ORANGE,
Mai2Constants.GAME_CODE_GREEN,
Mai2Constants.GAME_CODE,
]
current_schema_version = 7

View File

@@ -1,5 +1,5 @@
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
from typing import Any, Dict from typing import Any, Dict, List
import logging import logging
from core.config import CoreConfig from core.config import CoreConfig
@@ -12,33 +12,35 @@ class Mai2Base:
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
self.core_config = cfg self.core_config = cfg
self.game_config = game_cfg self.game_config = game_cfg
self.game = Mai2Constants.GAME_CODE self.version = Mai2Constants.VER_MAIMAI
self.version = Mai2Constants.VER_MAIMAI_DX
self.data = Mai2Data(cfg) self.data = Mai2Data(cfg)
self.logger = logging.getLogger("mai2") self.logger = logging.getLogger("mai2")
self.can_deliver = False
self.can_usbdl = False
self.old_server = ""
if self.core_config.server.is_develop and self.core_config.title.port > 0:
self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/197/"
else:
self.old_server = f"http://{self.core_config.title.hostname}/SDEY/197/"
def handle_get_game_setting_api_request(self, data: Dict): def handle_get_game_setting_api_request(self, data: Dict):
reboot_start = date.strftime(
datetime.now() + timedelta(hours=3), Mai2Constants.DATE_TIME_FORMAT
)
reboot_end = date.strftime(
datetime.now() + timedelta(hours=4), Mai2Constants.DATE_TIME_FORMAT
)
return { return {
"isDevelop": False,
"isAouAccession": False,
"gameSetting": { "gameSetting": {
"isMaintenance": "false", "isMaintenance": False,
"requestInterval": 10, "requestInterval": 1800,
"rebootStartTime": reboot_start, "rebootStartTime": "2020-01-01 07:00:00.0",
"rebootEndTime": reboot_end, "rebootEndTime": "2020-01-01 07:59:59.0",
"movieUploadLimit": 10000, "movieUploadLimit": 100,
"movieStatus": 0, "movieStatus": 1,
"movieServerUri": "", "movieServerUri": self.old_server + "api/movie" if self.game_config.uploads.movies else "movie",
"deliverServerUri": "", "deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "",
"oldServerUri": "", "oldServerUri": self.old_server + "old",
"usbDlServerUri": "", "usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "",
"rebootInterval": 0,
}, },
"isAouAccession": "true",
} }
def handle_get_game_ranking_api_request(self, data: Dict) -> Dict: def handle_get_game_ranking_api_request(self, data: Dict) -> Dict:
@@ -51,7 +53,7 @@ class Mai2Base:
def handle_get_game_event_api_request(self, data: Dict) -> Dict: def handle_get_game_event_api_request(self, data: Dict) -> Dict:
events = self.data.static.get_enabled_events(self.version) events = self.data.static.get_enabled_events(self.version)
events_lst = [] events_lst = []
if events is None: if events is None or not events:
self.logger.warn("No enabled events, did you run the reader?") self.logger.warn("No enabled events, did you run the reader?")
return {"type": data["type"], "length": 0, "gameEventList": []} return {"type": data["type"], "length": 0, "gameEventList": []}
@@ -110,43 +112,35 @@ class Mai2Base:
return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"}
def handle_get_user_preview_api_request(self, data: Dict) -> Dict: def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
p = self.data.profile.get_profile_detail(data["userId"], self.version) p = self.data.profile.get_profile_detail(data["userId"], self.version, False)
o = self.data.profile.get_profile_option(data["userId"], self.version) w = self.data.profile.get_web_option(data["userId"], self.version)
if p is None or o is None: if p is None or w is None:
return {} # Register return {} # Register
profile = p._asdict() profile = p._asdict()
option = o._asdict() web_opt = w._asdict()
return { return {
"userId": data["userId"], "userId": data["userId"],
"userName": profile["userName"], "userName": profile["userName"],
"isLogin": False, "isLogin": False,
"lastGameId": profile["lastGameId"],
"lastDataVersion": profile["lastDataVersion"], "lastDataVersion": profile["lastDataVersion"],
"lastRomVersion": profile["lastRomVersion"], "lastLoginDate": profile["lastPlayDate"],
"lastLoginDate": profile["lastLoginDate"],
"lastPlayDate": profile["lastPlayDate"], "lastPlayDate": profile["lastPlayDate"],
"playerRating": profile["playerRating"], "playerRating": profile["playerRating"],
"nameplateId": 0, # Unused "nameplateId": profile["nameplateId"],
"iconId": profile["iconId"],
"trophyId": 0, # Unused
"partnerId": profile["partnerId"],
"frameId": profile["frameId"], "frameId": profile["frameId"],
"dispRate": option[ "iconId": profile["iconId"],
"dispRate" "trophyId": profile["trophyId"],
], # 0: all/begin, 1: disprate, 2: dispDan, 3: hide, 4: end "dispRate": web_opt["dispRate"], # 0: all, 1: dispRate, 2: dispDan, 3: hide
"totalAwake": profile["totalAwake"], "dispRank": web_opt["dispRank"],
"isNetMember": profile["isNetMember"], "dispHomeRanker": web_opt["dispHomeRanker"],
"dailyBonusDate": profile["dailyBonusDate"], "dispTotalLv": web_opt["dispTotalLv"],
"headPhoneVolume": option["headPhoneVolume"], "totalLv": profile["totalLv"],
"isInherit": False, # Not sure what this is or does??
"banState": profile["banState"]
if profile["banState"] is not None
else 0, # New with uni+
} }
def handle_user_login_api_request(self, data: Dict) -> Dict: def handle_user_login_api_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile_detail(data["userId"], self.version) profile = self.data.profile.get_profile_detail(data["userId"], self.version)
consec = self.data.profile.get_consec_login(data["userId"], self.version)
if profile is not None: if profile is not None:
lastLoginDate = profile["lastLoginDate"] lastLoginDate = profile["lastLoginDate"]
@@ -158,12 +152,31 @@ class Mai2Base:
loginCt = 0 loginCt = 0
lastLoginDate = "2017-12-05 07:00:00.0" lastLoginDate = "2017-12-05 07:00:00.0"
if consec is None or not consec:
consec_ct = 1
else:
lastlogindate_ = datetime.strptime(profile["lastLoginDate"], "%Y-%m-%d %H:%M:%S.%f").timestamp()
today_midnight = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp()
yesterday_midnight = today_midnight - 86400
if lastlogindate_ < today_midnight:
consec_ct = consec['logins'] + 1
self.data.profile.add_consec_login(data["userId"], self.version)
elif lastlogindate_ < yesterday_midnight:
consec_ct = 1
self.data.profile.reset_consec_login(data["userId"], self.version)
else:
consec_ct = consec['logins']
return { return {
"returnCode": 1, "returnCode": 1,
"lastLoginDate": lastLoginDate, "lastLoginDate": lastLoginDate,
"loginCount": loginCt, "loginCount": loginCt,
"consecutiveLoginCount": 0, # We don't really have a way to track this... "consecutiveLoginCount": consec_ct, # Number of consecutive days we've logged in.
"loginId": loginCt, # Used with the playlog!
} }
def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict: def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict:
@@ -195,10 +208,33 @@ class Mai2Base:
upsert = data["upsertUserAll"] upsert = data["upsertUserAll"]
if "userData" in upsert and len(upsert["userData"]) > 0: if "userData" in upsert and len(upsert["userData"]) > 0:
upsert["userData"][0]["isNetMember"] = 1
upsert["userData"][0].pop("accessCode") upsert["userData"][0].pop("accessCode")
upsert["userData"][0].pop("userId")
self.data.profile.put_profile_detail( self.data.profile.put_profile_detail(
user_id, self.version, upsert["userData"][0] user_id, self.version, upsert["userData"][0], False
)
if "userWebOption" in upsert and len(upsert["userWebOption"]) > 0:
upsert["userWebOption"][0]["isNetMember"] = True
self.data.profile.put_web_option(
user_id, self.version, upsert["userWebOption"][0]
)
if "userGradeStatusList" in upsert and len(upsert["userGradeStatusList"]) > 0:
self.data.profile.put_grade_status(
user_id, upsert["userGradeStatusList"][0]
)
if "userBossList" in upsert and len(upsert["userBossList"]) > 0:
self.data.profile.put_boss_list(
user_id, upsert["userBossList"][0]
)
if "userPlaylogList" in upsert and len(upsert["userPlaylogList"]) > 0:
for playlog in upsert["userPlaylogList"]:
self.data.score.put_playlog(
user_id, playlog, False
) )
if "userExtend" in upsert and len(upsert["userExtend"]) > 0: if "userExtend" in upsert and len(upsert["userExtend"]) > 0:
@@ -208,11 +244,15 @@ class Mai2Base:
if "userGhost" in upsert: if "userGhost" in upsert:
for ghost in upsert["userGhost"]: for ghost in upsert["userGhost"]:
self.data.profile.put_profile_extend(user_id, self.version, ghost) self.data.profile.put_profile_ghost(user_id, self.version, ghost)
if "userRecentRatingList" in upsert:
self.data.profile.put_recent_rating(user_id, upsert["userRecentRatingList"])
if "userOption" in upsert and len(upsert["userOption"]) > 0: if "userOption" in upsert and len(upsert["userOption"]) > 0:
upsert["userOption"][0].pop("userId")
self.data.profile.put_profile_option( self.data.profile.put_profile_option(
user_id, self.version, upsert["userOption"][0] user_id, self.version, upsert["userOption"][0], False
) )
if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0: if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0:
@@ -221,8 +261,7 @@ class Mai2Base:
) )
if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0: if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0:
for k, v in upsert["userActivityList"][0].items(): for act in upsert["userActivityList"]:
for act in v:
self.data.profile.put_profile_activity(user_id, act) self.data.profile.put_profile_activity(user_id, act)
if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0: if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0:
@@ -243,12 +282,9 @@ class Mai2Base:
if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0: if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0:
for char in upsert["userCharacterList"]: for char in upsert["userCharacterList"]:
self.data.item.put_character( self.data.item.put_character_(
user_id, user_id,
char["characterId"], char
char["level"],
char["awakening"],
char["useCount"],
) )
if "userItemList" in upsert and len(upsert["userItemList"]) > 0: if "userItemList" in upsert and len(upsert["userItemList"]) > 0:
@@ -258,7 +294,7 @@ class Mai2Base:
int(item["itemKind"]), int(item["itemKind"]),
item["itemId"], item["itemId"],
item["stock"], item["stock"],
item["isValid"], True
) )
if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0:
@@ -284,7 +320,7 @@ class Mai2Base:
if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0: if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0:
for music in upsert["userMusicDetailList"]: for music in upsert["userMusicDetailList"]:
self.data.score.put_best_score(user_id, music) self.data.score.put_best_score(user_id, music, False)
if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0: if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0:
for course in upsert["userCourseList"]: for course in upsert["userCourseList"]:
@@ -312,7 +348,7 @@ class Mai2Base:
return {"returnCode": 1} return {"returnCode": 1}
def handle_get_user_data_api_request(self, data: Dict) -> Dict: def handle_get_user_data_api_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile_detail(data["userId"], self.version) profile = self.data.profile.get_profile_detail(data["userId"], self.version, False)
if profile is None: if profile is None:
return return
@@ -336,7 +372,7 @@ class Mai2Base:
return {"userId": data["userId"], "userExtend": extend_dict} return {"userId": data["userId"], "userExtend": extend_dict}
def handle_get_user_option_api_request(self, data: Dict) -> Dict: def handle_get_user_option_api_request(self, data: Dict) -> Dict:
options = self.data.profile.get_profile_option(data["userId"], self.version) options = self.data.profile.get_profile_option(data["userId"], self.version, False)
if options is None: if options is None:
return return
@@ -406,12 +442,31 @@ class Mai2Base:
"userChargeList": user_charge_list, "userChargeList": user_charge_list,
} }
def handle_get_user_present_api_request(self, data: Dict) -> Dict:
return { "userId": data.get("userId", 0), "length": 0, "userPresentList": []}
def handle_get_transfer_friend_api_request(self, data: Dict) -> Dict:
return {}
def handle_get_user_present_event_api_request(self, data: Dict) -> Dict:
return { "userId": data.get("userId", 0), "length": 0, "userPresentEventList": []}
def handle_get_user_boss_api_request(self, data: Dict) -> Dict:
b = self.data.profile.get_boss_list(data["userId"])
if b is None:
return { "userId": data.get("userId", 0), "userBossData": {}}
boss_lst = b._asdict()
boss_lst.pop("id")
boss_lst.pop("user")
return { "userId": data.get("userId", 0), "userBossData": boss_lst}
def handle_get_user_item_api_request(self, data: Dict) -> Dict: def handle_get_user_item_api_request(self, data: Dict) -> Dict:
kind = int(data["nextIndex"] / 10000000000) kind = int(data["nextIndex"] / 10000000000)
next_idx = int(data["nextIndex"] % 10000000000) next_idx = int(data["nextIndex"] % 10000000000)
user_item_list = self.data.item.get_items(data["userId"], kind) user_item_list = self.data.item.get_items(data["userId"], kind)
items: list[Dict[str, Any]] = [] items: List[Dict[str, Any]] = []
for i in range(next_idx, len(user_item_list)): for i in range(next_idx, len(user_item_list)):
tmp = user_item_list[i]._asdict() tmp = user_item_list[i]._asdict()
tmp.pop("user") tmp.pop("user")
@@ -442,6 +497,8 @@ class Mai2Base:
tmp = chara._asdict() tmp = chara._asdict()
tmp.pop("id") tmp.pop("id")
tmp.pop("user") tmp.pop("user")
tmp.pop("awakening")
tmp.pop("useCount")
chara_list.append(tmp) chara_list.append(tmp)
return {"userId": data["userId"], "userCharacterList": chara_list} return {"userId": data["userId"], "userCharacterList": chara_list}
@@ -475,6 +532,16 @@ class Mai2Base:
return {"userId": data["userId"], "userGhost": ghost_dict} return {"userId": data["userId"], "userGhost": ghost_dict}
def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict:
rating = self.data.profile.get_recent_rating(data["userId"])
if rating is None:
return
r = rating._asdict()
lst = r.get("userRecentRatingList", [])
return {"userId": data["userId"], "length": len(lst), "userRecentRatingList": lst}
def handle_get_user_rating_api_request(self, data: Dict) -> Dict: def handle_get_user_rating_api_request(self, data: Dict) -> Dict:
rating = self.data.profile.get_profile_rating(data["userId"], self.version) rating = self.data.profile.get_profile_rating(data["userId"], self.version)
if rating is None: if rating is None:
@@ -638,24 +705,72 @@ class Mai2Base:
def handle_get_user_region_api_request(self, data: Dict) -> Dict: def handle_get_user_region_api_request(self, data: Dict) -> Dict:
return {"userId": data["userId"], "length": 0, "userRegionList": []} return {"userId": data["userId"], "length": 0, "userRegionList": []}
def handle_get_user_music_api_request(self, data: Dict) -> Dict: def handle_get_user_web_option_api_request(self, data: Dict) -> Dict:
songs = self.data.score.get_best_scores(data["userId"]) w = self.data.profile.get_web_option(data["userId"], self.version)
music_detail_list = [] if w is None:
next_index = 0 return {"userId": data["userId"], "userWebOption": {}}
if songs is not None: web_opt = w._asdict()
for song in songs: web_opt.pop("id")
tmp = song._asdict() web_opt.pop("user")
web_opt.pop("version")
return {"userId": data["userId"], "userWebOption": web_opt}
def handle_get_user_survival_api_request(self, data: Dict) -> Dict:
return {"userId": data["userId"], "length": 0, "userSurvivalList": []}
def handle_get_user_grade_api_request(self, data: Dict) -> Dict:
g = self.data.profile.get_grade_status(data["userId"])
if g is None:
return {"userId": data["userId"], "userGradeStatus": {}, "length": 0, "userGradeList": []}
grade_stat = g._asdict()
grade_stat.pop("id")
grade_stat.pop("user")
return {"userId": data["userId"], "userGradeStatus": grade_stat, "length": 0, "userGradeList": []}
def handle_get_user_music_api_request(self, data: Dict) -> Dict:
user_id = data.get("userId", 0)
next_index = data.get("nextIndex", 0)
max_ct = data.get("maxCount", 50)
upper_lim = next_index + max_ct
music_detail_list = []
if user_id <= 0:
self.logger.warn("handle_get_user_music_api_request: Could not find userid in data, or userId is 0")
return {}
songs = self.data.score.get_best_scores(user_id, is_dx=False)
if songs is None:
self.logger.debug("handle_get_user_music_api_request: get_best_scores returned None!")
return {
"userId": data["userId"],
"nextIndex": 0,
"userMusicList": [],
}
num_user_songs = len(songs)
for x in range(next_index, upper_lim):
if num_user_songs <= x:
break
tmp = songs[x]._asdict()
tmp.pop("id") tmp.pop("id")
tmp.pop("user") tmp.pop("user")
music_detail_list.append(tmp) music_detail_list.append(tmp)
if len(music_detail_list) == data["maxCount"]: next_index = 0 if len(music_detail_list) < max_ct or num_user_songs == upper_lim else upper_lim
next_index = data["maxCount"] + data["nextIndex"] self.logger.info(f"Send songs {next_index}-{upper_lim} ({len(music_detail_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})")
break
return { return {
"userId": data["userId"], "userId": data["userId"],
"nextIndex": next_index, "nextIndex": next_index,
"userMusicList": [{"userMusicDetailList": music_detail_list}], "userMusicList": [{"userMusicDetailList": music_detail_list}],
} }
def handle_upload_user_portrait_api_request(self, data: Dict) -> Dict:
self.logger.debug(data)
def handle_upload_user_photo_api_request(self, data: Dict) -> Dict:
self.logger.debug(data)

View File

@@ -19,7 +19,59 @@ class Mai2ServerConfig:
) )
) )
class Mai2DeliverConfig:
def __init__(self, parent: "Mai2Config") -> None:
self.__config = parent
@property
def enable(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "mai2", "deliver", "enable", default=False
)
@property
def udbdl_enable(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "mai2", "deliver", "udbdl_enable", default=False
)
@property
def content_folder(self) -> int:
return CoreConfig.get_config_field(
self.__config, "mai2", "server", "content_folder", default=""
)
class Mai2UploadsConfig:
def __init__(self, parent: "Mai2Config") -> None:
self.__config = parent
@property
def photos(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "mai2", "uploads", "photos", default=False
)
@property
def photos_dir(self) -> str:
return CoreConfig.get_config_field(
self.__config, "mai2", "uploads", "photos_dir", default=""
)
@property
def movies(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "mai2", "uploads", "movies", default=False
)
@property
def movies_dir(self) -> str:
return CoreConfig.get_config_field(
self.__config, "mai2", "uploads", "movies_dir", default=""
)
class Mai2Config(dict): class Mai2Config(dict):
def __init__(self) -> None: def __init__(self) -> None:
self.server = Mai2ServerConfig(self) self.server = Mai2ServerConfig(self)
self.deliver = Mai2DeliverConfig(self)
self.uploads = Mai2UploadsConfig(self)

View File

@@ -20,26 +20,60 @@ class Mai2Constants:
DATE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S" DATE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
GAME_CODE = "SDEZ" GAME_CODE = "SBXL"
GAME_CODE_GREEN = "SBZF"
GAME_CODE_ORANGE = "SDBM"
GAME_CODE_PINK = "SDCQ"
GAME_CODE_MURASAKI = "SDDK"
GAME_CODE_MILK = "SDDZ"
GAME_CODE_FINALE = "SDEY"
GAME_CODE_DX = "SDEZ"
CONFIG_NAME = "mai2.yaml" CONFIG_NAME = "mai2.yaml"
VER_MAIMAI_DX = 0 VER_MAIMAI = 0
VER_MAIMAI_DX_PLUS = 1 VER_MAIMAI_PLUS = 1
VER_MAIMAI_DX_SPLASH = 2 VER_MAIMAI_GREEN = 2
VER_MAIMAI_DX_SPLASH_PLUS = 3 VER_MAIMAI_GREEN_PLUS = 3
VER_MAIMAI_DX_UNIVERSE = 4 VER_MAIMAI_ORANGE = 4
VER_MAIMAI_DX_UNIVERSE_PLUS = 5 VER_MAIMAI_ORANGE_PLUS = 5
VER_MAIMAI_DX_FESTIVAL = 6 VER_MAIMAI_PINK = 6
VER_MAIMAI_PINK_PLUS = 7
VER_MAIMAI_MURASAKI = 8
VER_MAIMAI_MURASAKI_PLUS = 9
VER_MAIMAI_MILK = 10
VER_MAIMAI_MILK_PLUS = 11
VER_MAIMAI_FINALE = 12
VER_MAIMAI_DX = 13
VER_MAIMAI_DX_PLUS = 14
VER_MAIMAI_DX_SPLASH = 15
VER_MAIMAI_DX_SPLASH_PLUS = 16
VER_MAIMAI_DX_UNIVERSE = 17
VER_MAIMAI_DX_UNIVERSE_PLUS = 18
VER_MAIMAI_DX_FESTIVAL = 19
VERSION_STRING = ( VERSION_STRING = (
"maimai",
"maimai PLUS",
"maimai GreeN",
"maimai GreeN PLUS",
"maimai ORANGE",
"maimai ORANGE PLUS",
"maimai PiNK",
"maimai PiNK PLUS",
"maimai MURASAKi",
"maimai MURASAKi PLUS",
"maimai MiLK",
"maimai MiLK PLUS",
"maimai FiNALE",
"maimai DX", "maimai DX",
"maimai DX PLUS", "maimai DX PLUS",
"maimai DX Splash", "maimai DX Splash",
"maimai DX Splash PLUS", "maimai DX Splash PLUS",
"maimai DX Universe", "maimai DX UNiVERSE",
"maimai DX Universe PLUS", "maimai DX UNiVERSE PLUS",
"maimai DX Festival", "maimai DX FESTiVAL",
) )
@classmethod @classmethod

743
titles/mai2/dx.py Normal file
View File

@@ -0,0 +1,743 @@
from typing import Any, List, Dict
from datetime import datetime, timedelta
import pytz
import json
from random import randint
from core.config import CoreConfig
from titles.mai2.base import Mai2Base
from titles.mai2.config import Mai2Config
from titles.mai2.const import Mai2Constants
class Mai2DX(Mai2Base):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX
if self.core_config.server.is_develop and self.core_config.title.port > 0:
self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEZ/100/"
else:
self.old_server = f"http://{self.core_config.title.hostname}/SDEZ/100/"
def handle_get_game_setting_api_request(self, data: Dict):
return {
"gameSetting": {
"isMaintenance": False,
"requestInterval": 1800,
"rebootStartTime": "2020-01-01 07:00:00.0",
"rebootEndTime": "2020-01-01 07:59:59.0",
"movieUploadLimit": 100,
"movieStatus": 1,
"movieServerUri": self.old_server + "movie/",
"deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "",
"oldServerUri": self.old_server + "old",
"usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "",
"rebootInterval": 0,
},
"isAouAccession": False,
}
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
p = self.data.profile.get_profile_detail(data["userId"], self.version)
o = self.data.profile.get_profile_option(data["userId"], self.version)
if p is None or o is None:
return {} # Register
profile = p._asdict()
option = o._asdict()
return {
"userId": data["userId"],
"userName": profile["userName"],
"isLogin": False,
"lastGameId": profile["lastGameId"],
"lastDataVersion": profile["lastDataVersion"],
"lastRomVersion": profile["lastRomVersion"],
"lastLoginDate": profile["lastLoginDate"],
"lastPlayDate": profile["lastPlayDate"],
"playerRating": profile["playerRating"],
"nameplateId": 0, # Unused
"iconId": profile["iconId"],
"trophyId": 0, # Unused
"partnerId": profile["partnerId"],
"frameId": profile["frameId"],
"dispRate": option[
"dispRate"
], # 0: all/begin, 1: disprate, 2: dispDan, 3: hide, 4: end
"totalAwake": profile["totalAwake"],
"isNetMember": profile["isNetMember"],
"dailyBonusDate": profile["dailyBonusDate"],
"headPhoneVolume": option["headPhoneVolume"],
"isInherit": False, # Not sure what this is or does??
"banState": profile["banState"]
if profile["banState"] is not None
else 0, # New with uni+
}
def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
playlog = data["userPlaylog"]
self.data.score.put_playlog(user_id, playlog)
return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"}
def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
charge = data["userCharge"]
# remove the ".0" from the date string, festival only?
charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "")
self.data.item.put_charge(
user_id,
charge["chargeId"],
charge["stock"],
datetime.strptime(charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT),
datetime.strptime(charge["validDate"], Mai2Constants.DATE_TIME_FORMAT),
)
return {"returnCode": 1, "apiName": "UpsertUserChargelogApi"}
def handle_upsert_user_all_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
upsert = data["upsertUserAll"]
if "userData" in upsert and len(upsert["userData"]) > 0:
upsert["userData"][0]["isNetMember"] = 1
upsert["userData"][0].pop("accessCode")
self.data.profile.put_profile_detail(
user_id, self.version, upsert["userData"][0]
)
if "userExtend" in upsert and len(upsert["userExtend"]) > 0:
self.data.profile.put_profile_extend(
user_id, self.version, upsert["userExtend"][0]
)
if "userGhost" in upsert:
for ghost in upsert["userGhost"]:
self.data.profile.put_profile_extend(user_id, self.version, ghost)
if "userOption" in upsert and len(upsert["userOption"]) > 0:
self.data.profile.put_profile_option(
user_id, self.version, upsert["userOption"][0]
)
if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0:
self.data.profile.put_profile_rating(
user_id, self.version, upsert["userRatingList"][0]
)
if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0:
for k, v in upsert["userActivityList"][0].items():
for act in v:
self.data.profile.put_profile_activity(user_id, act)
if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0:
for charge in upsert["userChargeList"]:
# remove the ".0" from the date string, festival only?
charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "")
self.data.item.put_charge(
user_id,
charge["chargeId"],
charge["stock"],
datetime.strptime(
charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT
),
datetime.strptime(
charge["validDate"], Mai2Constants.DATE_TIME_FORMAT
),
)
if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0:
for char in upsert["userCharacterList"]:
self.data.item.put_character(
user_id,
char["characterId"],
char["level"],
char["awakening"],
char["useCount"],
)
if "userItemList" in upsert and len(upsert["userItemList"]) > 0:
for item in upsert["userItemList"]:
self.data.item.put_item(
user_id,
int(item["itemKind"]),
item["itemId"],
item["stock"],
item["isValid"],
)
if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0:
for login_bonus in upsert["userLoginBonusList"]:
self.data.item.put_login_bonus(
user_id,
login_bonus["bonusId"],
login_bonus["point"],
login_bonus["isCurrent"],
login_bonus["isComplete"],
)
if "userMapList" in upsert and len(upsert["userMapList"]) > 0:
for map in upsert["userMapList"]:
self.data.item.put_map(
user_id,
map["mapId"],
map["distance"],
map["isLock"],
map["isClear"],
map["isComplete"],
)
if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0:
for music in upsert["userMusicDetailList"]:
self.data.score.put_best_score(user_id, music)
if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0:
for course in upsert["userCourseList"]:
self.data.score.put_course(user_id, course)
if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0:
for fav in upsert["userFavoriteList"]:
self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"])
if (
"userFriendSeasonRankingList" in upsert
and len(upsert["userFriendSeasonRankingList"]) > 0
):
for fsr in upsert["userFriendSeasonRankingList"]:
fsr["recordDate"] = (
datetime.strptime(
fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0"
),
)
self.data.item.put_friend_season_ranking(user_id, fsr)
return {"returnCode": 1, "apiName": "UpsertUserAllApi"}
def handle_user_logout_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1}
def handle_get_user_data_api_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile_detail(data["userId"], self.version)
if profile is None:
return
profile_dict = profile._asdict()
profile_dict.pop("id")
profile_dict.pop("user")
profile_dict.pop("version")
return {"userId": data["userId"], "userData": profile_dict}
def handle_get_user_extend_api_request(self, data: Dict) -> Dict:
extend = self.data.profile.get_profile_extend(data["userId"], self.version)
if extend is None:
return
extend_dict = extend._asdict()
extend_dict.pop("id")
extend_dict.pop("user")
extend_dict.pop("version")
return {"userId": data["userId"], "userExtend": extend_dict}
def handle_get_user_option_api_request(self, data: Dict) -> Dict:
options = self.data.profile.get_profile_option(data["userId"], self.version)
if options is None:
return
options_dict = options._asdict()
options_dict.pop("id")
options_dict.pop("user")
options_dict.pop("version")
return {"userId": data["userId"], "userOption": options_dict}
def handle_get_user_card_api_request(self, data: Dict) -> Dict:
user_cards = self.data.item.get_cards(data["userId"])
if user_cards is None:
return {"userId": data["userId"], "nextIndex": 0, "userCardList": []}
max_ct = data["maxCount"]
next_idx = data["nextIndex"]
start_idx = next_idx
end_idx = max_ct + start_idx
if len(user_cards[start_idx:]) > max_ct:
next_idx += max_ct
else:
next_idx = 0
card_list = []
for card in user_cards:
tmp = card._asdict()
tmp.pop("id")
tmp.pop("user")
tmp["startDate"] = datetime.strftime(
tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["endDate"] = datetime.strftime(
tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
)
card_list.append(tmp)
return {
"userId": data["userId"],
"nextIndex": next_idx,
"userCardList": card_list[start_idx:end_idx],
}
def handle_get_user_charge_api_request(self, data: Dict) -> Dict:
user_charges = self.data.item.get_charges(data["userId"])
if user_charges is None:
return {"userId": data["userId"], "length": 0, "userChargeList": []}
user_charge_list = []
for charge in user_charges:
tmp = charge._asdict()
tmp.pop("id")
tmp.pop("user")
tmp["purchaseDate"] = datetime.strftime(
tmp["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["validDate"] = datetime.strftime(
tmp["validDate"], Mai2Constants.DATE_TIME_FORMAT
)
user_charge_list.append(tmp)
return {
"userId": data["userId"],
"length": len(user_charge_list),
"userChargeList": user_charge_list,
}
def handle_get_user_item_api_request(self, data: Dict) -> Dict:
kind = int(data["nextIndex"] / 10000000000)
next_idx = int(data["nextIndex"] % 10000000000)
user_item_list = self.data.item.get_items(data["userId"], kind)
items: List[Dict[str, Any]] = []
for i in range(next_idx, len(user_item_list)):
tmp = user_item_list[i]._asdict()
tmp.pop("user")
tmp.pop("id")
items.append(tmp)
if len(items) >= int(data["maxCount"]):
break
xout = kind * 10000000000 + next_idx + len(items)
if len(items) < int(data["maxCount"]):
next_idx = 0
else:
next_idx = xout
return {
"userId": data["userId"],
"nextIndex": next_idx,
"itemKind": kind,
"userItemList": items,
}
def handle_get_user_character_api_request(self, data: Dict) -> Dict:
characters = self.data.item.get_characters(data["userId"])
chara_list = []
for chara in characters:
tmp = chara._asdict()
tmp.pop("id")
tmp.pop("user")
chara_list.append(tmp)
return {"userId": data["userId"], "userCharacterList": chara_list}
def handle_get_user_favorite_api_request(self, data: Dict) -> Dict:
favorites = self.data.item.get_favorites(data["userId"], data["itemKind"])
if favorites is None:
return
userFavs = []
for fav in favorites:
userFavs.append(
{
"userId": data["userId"],
"itemKind": fav["itemKind"],
"itemIdList": fav["itemIdList"],
}
)
return {"userId": data["userId"], "userFavoriteData": userFavs}
def handle_get_user_ghost_api_request(self, data: Dict) -> Dict:
ghost = self.data.profile.get_profile_ghost(data["userId"], self.version)
if ghost is None:
return
ghost_dict = ghost._asdict()
ghost_dict.pop("user")
ghost_dict.pop("id")
ghost_dict.pop("version_int")
return {"userId": data["userId"], "userGhost": ghost_dict}
def handle_get_user_rating_api_request(self, data: Dict) -> Dict:
rating = self.data.profile.get_profile_rating(data["userId"], self.version)
if rating is None:
return
rating_dict = rating._asdict()
rating_dict.pop("user")
rating_dict.pop("id")
rating_dict.pop("version")
return {"userId": data["userId"], "userRating": rating_dict}
def handle_get_user_activity_api_request(self, data: Dict) -> Dict:
"""
kind 1 is playlist, kind 2 is music list
"""
playlist = self.data.profile.get_profile_activity(data["userId"], 1)
musiclist = self.data.profile.get_profile_activity(data["userId"], 2)
if playlist is None or musiclist is None:
return
plst = []
mlst = []
for play in playlist:
tmp = play._asdict()
tmp["id"] = tmp["activityId"]
tmp.pop("activityId")
tmp.pop("user")
plst.append(tmp)
for music in musiclist:
tmp = music._asdict()
tmp["id"] = tmp["activityId"]
tmp.pop("activityId")
tmp.pop("user")
mlst.append(tmp)
return {"userActivity": {"playList": plst, "musicList": mlst}}
def handle_get_user_course_api_request(self, data: Dict) -> Dict:
user_courses = self.data.score.get_courses(data["userId"])
if user_courses is None:
return {"userId": data["userId"], "nextIndex": 0, "userCourseList": []}
course_list = []
for course in user_courses:
tmp = course._asdict()
tmp.pop("user")
tmp.pop("id")
course_list.append(tmp)
return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list}
def handle_get_user_portrait_api_request(self, data: Dict) -> Dict:
# No support for custom pfps
return {"length": 0, "userPortraitList": []}
def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict:
friend_season_ranking = self.data.item.get_friend_season_ranking(data["userId"])
if friend_season_ranking is None:
return {
"userId": data["userId"],
"nextIndex": 0,
"userFriendSeasonRankingList": [],
}
friend_season_ranking_list = []
next_idx = int(data["nextIndex"])
max_ct = int(data["maxCount"])
for x in range(next_idx, len(friend_season_ranking)):
tmp = friend_season_ranking[x]._asdict()
tmp.pop("user")
tmp.pop("id")
tmp["recordDate"] = datetime.strftime(
tmp["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0"
)
friend_season_ranking_list.append(tmp)
if len(friend_season_ranking_list) >= max_ct:
break
if len(friend_season_ranking) >= next_idx + max_ct:
next_idx += max_ct
else:
next_idx = 0
return {
"userId": data["userId"],
"nextIndex": next_idx,
"userFriendSeasonRankingList": friend_season_ranking_list,
}
def handle_get_user_map_api_request(self, data: Dict) -> Dict:
maps = self.data.item.get_maps(data["userId"])
if maps is None:
return {
"userId": data["userId"],
"nextIndex": 0,
"userMapList": [],
}
map_list = []
next_idx = int(data["nextIndex"])
max_ct = int(data["maxCount"])
for x in range(next_idx, len(maps)):
tmp = maps[x]._asdict()
tmp.pop("user")
tmp.pop("id")
map_list.append(tmp)
if len(map_list) >= max_ct:
break
if len(maps) >= next_idx + max_ct:
next_idx += max_ct
else:
next_idx = 0
return {
"userId": data["userId"],
"nextIndex": next_idx,
"userMapList": map_list,
}
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
login_bonuses = self.data.item.get_login_bonuses(data["userId"])
if login_bonuses is None:
return {
"userId": data["userId"],
"nextIndex": 0,
"userLoginBonusList": [],
}
login_bonus_list = []
next_idx = int(data["nextIndex"])
max_ct = int(data["maxCount"])
for x in range(next_idx, len(login_bonuses)):
tmp = login_bonuses[x]._asdict()
tmp.pop("user")
tmp.pop("id")
login_bonus_list.append(tmp)
if len(login_bonus_list) >= max_ct:
break
if len(login_bonuses) >= next_idx + max_ct:
next_idx += max_ct
else:
next_idx = 0
return {
"userId": data["userId"],
"nextIndex": next_idx,
"userLoginBonusList": login_bonus_list,
}
def handle_get_user_region_api_request(self, data: Dict) -> Dict:
return {"userId": data["userId"], "length": 0, "userRegionList": []}
def handle_get_user_music_api_request(self, data: Dict) -> Dict:
songs = self.data.score.get_best_scores(data["userId"])
music_detail_list = []
next_index = 0
if songs is not None:
for song in songs:
tmp = song._asdict()
tmp.pop("id")
tmp.pop("user")
music_detail_list.append(tmp)
if len(music_detail_list) == data["maxCount"]:
next_index = data["maxCount"] + data["nextIndex"]
break
return {
"userId": data["userId"],
"nextIndex": next_index,
"userMusicList": [{"userMusicDetailList": music_detail_list}],
}
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
p = self.data.profile.get_profile_detail(data["userId"], self.version)
if p is None:
return {}
return {
"userName": p["userName"],
"rating": p["playerRating"],
# hardcode lastDataVersion for CardMaker 1.34
"lastDataVersion": "1.20.00",
"isLogin": False,
"isExistSellingCard": False,
}
def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
# user already exists, because the preview checks that already
p = self.data.profile.get_profile_detail(data["userId"], self.version)
cards = self.data.card.get_user_cards(data["userId"])
if cards is None or len(cards) == 0:
# This should never happen
self.logger.error(
f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}"
)
return {}
# get the dict representation of the row so we can modify values
user_data = p._asdict()
# remove the values the game doesn't want
user_data.pop("id")
user_data.pop("user")
user_data.pop("version")
return {"userId": data["userId"], "userData": user_data}
def handle_cm_login_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1}
def handle_cm_logout_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1}
def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict:
selling_cards = self.data.static.get_enabled_cards(self.version)
if selling_cards is None:
return {"length": 0, "sellingCardList": []}
selling_card_list = []
for card in selling_cards:
tmp = card._asdict()
tmp.pop("id")
tmp.pop("version")
tmp.pop("cardName")
tmp.pop("enabled")
tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S")
tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S")
tmp["noticeStartDate"] = datetime.strftime(
tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S"
)
tmp["noticeEndDate"] = datetime.strftime(
tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S"
)
selling_card_list.append(tmp)
return {"length": len(selling_card_list), "sellingCardList": selling_card_list}
def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict:
user_cards = self.data.item.get_cards(data["userId"])
if user_cards is None:
return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []}
max_ct = data["maxCount"]
next_idx = data["nextIndex"]
start_idx = next_idx
end_idx = max_ct + start_idx
if len(user_cards[start_idx:]) > max_ct:
next_idx += max_ct
else:
next_idx = 0
card_list = []
for card in user_cards:
tmp = card._asdict()
tmp.pop("id")
tmp.pop("user")
tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S")
tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S")
card_list.append(tmp)
return {
"returnCode": 1,
"length": len(card_list[start_idx:end_idx]),
"nextIndex": next_idx,
"userCardList": card_list[start_idx:end_idx],
}
def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
super().handle_get_user_item_api_request(data)
def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
characters = self.data.item.get_characters(data["userId"])
chara_list = []
for chara in characters:
chara_list.append(
{
"characterId": chara["characterId"],
# no clue why those values are even needed
"point": 0,
"count": 0,
"level": chara["level"],
"nextAwake": 0,
"nextAwakePercent": 0,
"favorite": False,
"awakening": chara["awakening"],
"useCount": chara["useCount"],
}
)
return {
"returnCode": 1,
"length": len(chara_list),
"userCharacterList": chara_list,
}
def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
return {"length": 0, "userPrintDetailList": []}
def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
upsert = data["userPrintDetail"]
# set a random card serial number
serial_id = "".join([str(randint(0, 9)) for _ in range(20)])
user_card = upsert["userCard"]
self.data.item.put_card(
user_id,
user_card["cardId"],
user_card["cardTypeId"],
user_card["charaId"],
user_card["mapId"],
)
# properly format userPrintDetail for the database
upsert.pop("userCard")
upsert.pop("serialId")
upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d")
self.data.item.put_user_print_detail(user_id, serial_id, upsert)
return {
"returnCode": 1,
"orderId": 0,
"serialId": serial_id,
"startDate": "2018-01-01 00:00:00",
"endDate": "2038-01-01 00:00:00",
}
def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
return {
"returnCode": 1,
"orderId": 0,
"serialId": data["userPrintlog"]["serialId"],
}
def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1}

View File

@@ -4,12 +4,12 @@ import pytz
import json import json
from core.config import CoreConfig from core.config import CoreConfig
from titles.mai2.base import Mai2Base from titles.mai2.dx import Mai2DX
from titles.mai2.config import Mai2Config from titles.mai2.config import Mai2Config
from titles.mai2.const import Mai2Constants from titles.mai2.const import Mai2Constants
class Mai2Plus(Mai2Base): class Mai2DXPlus(Mai2DX):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg) super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_PLUS self.version = Mai2Constants.VER_MAIMAI_DX_PLUS

View File

@@ -1,12 +1,12 @@
from typing import Dict from typing import Dict
from core.config import CoreConfig from core.config import CoreConfig
from titles.mai2.universeplus import Mai2UniversePlus from titles.mai2.dx import Mai2DX
from titles.mai2.const import Mai2Constants from titles.mai2.const import Mai2Constants
from titles.mai2.config import Mai2Config from titles.mai2.config import Mai2Config
class Mai2Festival(Mai2UniversePlus): class Mai2Festival(Mai2DX):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg) super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL

23
titles/mai2/finale.py Normal file
View File

@@ -0,0 +1,23 @@
from typing import Any, List, Dict
from datetime import datetime, timedelta
import pytz
import json
from core.config import CoreConfig
from titles.mai2.base import Mai2Base
from titles.mai2.config import Mai2Config
from titles.mai2.const import Mai2Constants
class Mai2Finale(Mai2Base):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_FINALE
self.can_deliver = True
self.can_usbdl = True
if self.core_config.server.is_develop and self.core_config.title.port > 0:
self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/197/"
else:
self.old_server = f"http://{self.core_config.title.hostname}/SDEY/197/"

View File

@@ -1,4 +1,5 @@
from twisted.web.http import Request from twisted.web.http import Request
from twisted.web.server import NOT_DONE_YET
import json import json
import inflection import inflection
import yaml import yaml
@@ -14,7 +15,9 @@ from core.utils import Utils
from titles.mai2.config import Mai2Config from titles.mai2.config import Mai2Config
from titles.mai2.const import Mai2Constants from titles.mai2.const import Mai2Constants
from titles.mai2.base import Mai2Base from titles.mai2.base import Mai2Base
from titles.mai2.plus import Mai2Plus from titles.mai2.finale import Mai2Finale
from titles.mai2.dx import Mai2DX
from titles.mai2.dxplus import Mai2DXPlus
from titles.mai2.splash import Mai2Splash from titles.mai2.splash import Mai2Splash
from titles.mai2.splashplus import Mai2SplashPlus from titles.mai2.splashplus import Mai2SplashPlus
from titles.mai2.universe import Mai2Universe from titles.mai2.universe import Mai2Universe
@@ -33,7 +36,20 @@ class Mai2Servlet:
self.versions = [ self.versions = [
Mai2Base, Mai2Base,
Mai2Plus, None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
Mai2Finale,
Mai2DX,
Mai2DXPlus,
Mai2Splash, Mai2Splash,
Mai2SplashPlus, Mai2SplashPlus,
Mai2Universe, Mai2Universe,
@@ -42,6 +58,7 @@ class Mai2Servlet:
] ]
self.logger = logging.getLogger("mai2") self.logger = logging.getLogger("mai2")
if not hasattr(self.logger, "initted"):
log_fmt_str = "[%(asctime)s] Mai2 | %(levelname)s | %(message)s" log_fmt_str = "[%(asctime)s] Mai2 | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str) log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler( fileHandler = TimedRotatingFileHandler(
@@ -63,6 +80,7 @@ class Mai2Servlet:
coloredlogs.install( coloredlogs.install(
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
) )
self.logger.initted = True
@classmethod @classmethod
def get_allnet_info( def get_allnet_info(
@@ -82,19 +100,23 @@ class Mai2Servlet:
return ( return (
True, True,
f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/",
f"{core_cfg.title.hostname}:{core_cfg.title.port}/", f"{core_cfg.title.hostname}",
) )
return ( return (
True, True,
f"http://{core_cfg.title.hostname}/{game_code}/$v/", f"http://{core_cfg.title.hostname}/{game_code}/$v/",
f"{core_cfg.title.hostname}/", f"{core_cfg.title.hostname}",
) )
def render_POST(self, request: Request, version: int, url_path: str) -> bytes: def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
if url_path.lower() == "ping": if url_path.lower() == "ping":
return zlib.compress(b'{"returnCode": "1"}') return zlib.compress(b'{"returnCode": "1"}')
elif url_path.startswith("api/movie/"):
self.logger.info(f"Movie data: {url_path} - {request.content.getvalue()}")
return b""
req_raw = request.content.getvalue() req_raw = request.content.getvalue()
url = request.uri.decode() url = request.uri.decode()
url_split = url_path.split("/") url_split = url_path.split("/")
@@ -102,6 +124,7 @@ class Mai2Servlet:
endpoint = url_split[len(url_split) - 1] endpoint = url_split[len(url_split) - 1]
client_ip = Utils.get_ip_addr(request) client_ip = Utils.get_ip_addr(request)
if request.uri.startswith(b"/SDEZ"):
if version < 105: # 1.0 if version < 105: # 1.0
internal_ver = Mai2Constants.VER_MAIMAI_DX internal_ver = Mai2Constants.VER_MAIMAI_DX
elif version >= 105 and version < 110: # Plus elif version >= 105 and version < 110: # Plus
@@ -117,6 +140,34 @@ class Mai2Servlet:
elif version >= 130: # Festival elif version >= 130: # Festival
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL
else:
if version < 110: # 1.0
internal_ver = Mai2Constants.VER_MAIMAI
elif version >= 110 and version < 120: # Plus
internal_ver = Mai2Constants.VER_MAIMAI_PLUS
elif version >= 120 and version < 130: # Green
internal_ver = Mai2Constants.VER_MAIMAI_GREEN
elif version >= 130 and version < 140: # Green Plus
internal_ver = Mai2Constants.VER_MAIMAI_GREEN_PLUS
elif version >= 140 and version < 150: # Orange
internal_ver = Mai2Constants.VER_MAIMAI_ORANGE
elif version >= 150 and version < 160: # Orange Plus
internal_ver = Mai2Constants.VER_MAIMAI_ORANGE_PLUS
elif version >= 160 and version < 170: # Pink
internal_ver = Mai2Constants.VER_MAIMAI_PINK
elif version >= 170 and version < 180: # Pink Plus
internal_ver = Mai2Constants.VER_MAIMAI_PINK_PLUS
elif version >= 180 and version < 185: # Murasaki
internal_ver = Mai2Constants.VER_MAIMAI_MURASAKI
elif version >= 185 and version < 190: # Murasaki Plus
internal_ver = Mai2Constants.VER_MAIMAI_MURASAKI_PLUS
elif version >= 190 and version < 195: # Milk
internal_ver = Mai2Constants.VER_MAIMAI_MILK
elif version >= 195 and version < 197: # Milk Plus
internal_ver = Mai2Constants.VER_MAIMAI_MILK_PLUS
elif version >= 197: # Finale
internal_ver = Mai2Constants.VER_MAIMAI_FINALE
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
# If we get a 32 character long hex string, it's a hash and we're # If we get a 32 character long hex string, it's a hash and we're
# doing encrypted. The likelyhood of false positives is low but # doing encrypted. The likelyhood of false positives is low but
@@ -159,3 +210,39 @@ class Mai2Servlet:
self.logger.debug(f"Response {resp}") self.logger.debug(f"Response {resp}")
return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
def render_GET(self, request: Request, version: int, url_path: str) -> bytes:
self.logger.info(f"v{version} GET {url_path}")
url_split = url_path.split("/")
if (url_split[0] == "api" and url_split[1] == "movie") or url_split[0] == "movie":
if url_split[2] == "moviestart":
return json.dumps({"moviestart":{"status":"OK"}}).encode()
if url_split[0] == "old":
if url_split[1] == "ping":
self.logger.info(f"v{version} old server ping")
return zlib.compress(b"ok")
elif url_split[1].startswith("userdata"):
self.logger.info(f"v{version} old server userdata inquire")
return zlib.compress(b"{}")
elif url_split[1].startswith("friend"):
self.logger.info(f"v{version} old server friend inquire")
return zlib.compress(b"{}")
elif url_split[0] == "usbdl":
if url_split[1] == "CONNECTIONTEST":
self.logger.info(f"v{version} usbdl server test")
return zlib.compress(b"ok")
elif url_split[0] == "deliver":
file = url_split[len(url_split) - 1]
self.logger.info(f"v{version} {file} deliver inquire")
if not self.game_cfg.deliver.enable or not path.exists(f"{self.game_cfg.deliver.content_folder}/{file}"):
return zlib.compress(b"")
else:
return zlib.compress(b"{}")

View File

@@ -4,6 +4,9 @@ import os
import re import re
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from Crypto.Cipher import AES
import zlib
import codecs
from core.config import CoreConfig from core.config import CoreConfig
from core.data import Data from core.data import Data
@@ -34,6 +37,7 @@ class Mai2Reader(BaseReader):
def read(self) -> None: def read(self) -> None:
data_dirs = [] data_dirs = []
if self.version >= Mai2Constants.VER_MAIMAI_DX:
if self.bin_dir is not None: if self.bin_dir is not None:
data_dirs += self.get_data_directories(self.bin_dir) data_dirs += self.get_data_directories(self.bin_dir)
@@ -47,6 +51,134 @@ class Mai2Reader(BaseReader):
self.read_music(f"{dir}/music") self.read_music(f"{dir}/music")
self.read_tickets(f"{dir}/ticket") self.read_tickets(f"{dir}/ticket")
else:
if not os.path.exists(f"{self.bin_dir}/tables"):
self.logger.error(f"tables directory not found in {self.bin_dir}")
return
if self.version >= Mai2Constants.VER_MAIMAI_MILK:
if self.extra is None:
self.logger.error("Milk - Finale requre an AES key via a hex string send as the --extra flag")
return
key = bytes.fromhex(self.extra)
else:
key = None
evt_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmEvent.bin", key)
txt_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmtextout_jp.bin", key)
score_table = self.load_table_raw(f"{self.bin_dir}/tables", "mmScore.bin", key)
self.read_old_events(evt_table)
self.read_old_music(score_table, txt_table)
if self.opt_dir is not None:
evt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmEvent.bin", key)
txt_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmtextout_jp.bin", key)
score_table = self.load_table_raw(f"{self.opt_dir}/tables", "mmScore.bin", key)
self.read_old_events(evt_table)
self.read_old_music(score_table, txt_table)
return
def load_table_raw(self, dir: str, file: str, key: Optional[bytes]) -> Optional[List[Dict[str, str]]]:
if not os.path.exists(f"{dir}/{file}"):
self.logger.warn(f"file {file} does not exist in directory {dir}, skipping")
return
self.logger.info(f"Load table {file} from {dir}")
if key is not None:
cipher = AES.new(key, AES.MODE_CBC)
with open(f"{dir}/{file}", "rb") as f:
f_encrypted = f.read()
f_data = cipher.decrypt(f_encrypted)[0x10:]
else:
with open(f"{dir}/{file}", "rb") as f:
f_data = f.read()[0x10:]
if f_data is None or not f_data:
self.logger.warn(f"file {dir} could not be read, skipping")
return
f_data_deflate = zlib.decompress(f_data, wbits = zlib.MAX_WBITS | 16)[0x12:] # lop off the junk at the beginning
f_decoded = codecs.utf_16_le_decode(f_data_deflate)[0]
f_split = f_decoded.splitlines()
has_struct_def = "struct " in f_decoded
is_struct = False
struct_def = []
tbl_content = []
if has_struct_def:
for x in f_split:
if x.startswith("struct "):
is_struct = True
struct_name = x[7:-1]
continue
if x.startswith("};"):
is_struct = False
break
if is_struct:
try:
struct_def.append(x[x.rindex(" ") + 2: -1])
except ValueError:
self.logger.warn(f"rindex failed on line {x}")
if is_struct:
self.logger.warn("Struct not formatted properly")
if not struct_def:
self.logger.warn("Struct def not found")
name = file[:file.index(".")]
if "_" in name:
name = name[:file.index("_")]
for x in f_split:
if not x.startswith(name.upper()):
continue
line_match = re.match(r"(\w+)\((.*?)\)([ ]+\/{3}<[ ]+(.*))?", x)
if line_match is None:
continue
if not line_match.group(1) == name.upper():
self.logger.warn(f"Strange regex match for line {x} -> {line_match}")
continue
vals = line_match.group(2)
comment = line_match.group(4)
line_dict = {}
vals_split = vals.split(",")
for y in range(len(vals_split)):
stripped = vals_split[y].strip().lstrip("L\"").lstrip("\"").rstrip("\"")
if not stripped or stripped is None:
continue
if has_struct_def and len(struct_def) > y:
line_dict[struct_def[y]] = stripped
else:
line_dict[f'item_{y}'] = stripped
if comment:
line_dict['comment'] = comment
tbl_content.append(line_dict)
if tbl_content:
return tbl_content
else:
self.logger.warning("Failed load table content, skipping")
return
def get_events(self, base_dir: str) -> None: def get_events(self, base_dir: str) -> None:
self.logger.info(f"Reading events from {base_dir}...") self.logger.info(f"Reading events from {base_dir}...")
@@ -188,3 +320,24 @@ class Mai2Reader(BaseReader):
self.version, id, ticket_type, price, name self.version, id, ticket_type, price, name
) )
self.logger.info(f"Added ticket {id}...") self.logger.info(f"Added ticket {id}...")
def read_old_events(self, events: Optional[List[Dict[str, str]]]) -> None:
if events is None:
return
for event in events:
evt_id = int(event.get('イベントID', '0'))
evt_expire_time = float(event.get('オフ時強制時期', '0.0'))
is_exp = bool(int(event.get('海外許可', '0')))
is_aou = bool(int(event.get('AOU許可', '0')))
name = event.get('comment', f'evt_{evt_id}')
self.data.static.put_game_event(self.version, 0, evt_id, name)
if not (is_exp or is_aou):
self.data.static.toggle_game_event(self.version, evt_id, False)
def read_old_music(self, scores: Optional[List[Dict[str, str]]], text: Optional[List[Dict[str, str]]]) -> None:
if scores is None or text is None:
return
# TODO

View File

@@ -18,10 +18,11 @@ character = Table(
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False, nullable=False,
), ),
Column("characterId", Integer, nullable=False), Column("characterId", Integer),
Column("level", Integer, nullable=False, server_default="1"), Column("level", Integer),
Column("awakening", Integer, nullable=False, server_default="0"), Column("awakening", Integer),
Column("useCount", Integer, nullable=False, server_default="0"), Column("useCount", Integer),
Column("point", Integer),
UniqueConstraint("user", "characterId", name="mai2_item_character_uk"), UniqueConstraint("user", "characterId", name="mai2_item_character_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -35,12 +36,12 @@ card = Table(
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False, nullable=False,
), ),
Column("cardId", Integer, nullable=False), Column("cardId", Integer),
Column("cardTypeId", Integer, nullable=False), Column("cardTypeId", Integer),
Column("charaId", Integer, nullable=False), Column("charaId", Integer),
Column("mapId", Integer, nullable=False), Column("mapId", Integer),
Column("startDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), Column("startDate", TIMESTAMP, server_default=func.now()),
Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), Column("endDate", TIMESTAMP),
UniqueConstraint("user", "cardId", "cardTypeId", name="mai2_item_card_uk"), UniqueConstraint("user", "cardId", "cardTypeId", name="mai2_item_card_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -54,10 +55,10 @@ item = Table(
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False, nullable=False,
), ),
Column("itemId", Integer, nullable=False), Column("itemId", Integer),
Column("itemKind", Integer, nullable=False), Column("itemKind", Integer),
Column("stock", Integer, nullable=False, server_default="1"), Column("stock", Integer),
Column("isValid", Boolean, nullable=False, server_default="1"), Column("isValid", Boolean),
UniqueConstraint("user", "itemId", "itemKind", name="mai2_item_item_uk"), UniqueConstraint("user", "itemId", "itemKind", name="mai2_item_item_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -71,11 +72,11 @@ map = Table(
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False, nullable=False,
), ),
Column("mapId", Integer, nullable=False), Column("mapId", Integer),
Column("distance", Integer, nullable=False), Column("distance", Integer),
Column("isLock", Boolean, nullable=False, server_default="0"), Column("isLock", Boolean),
Column("isClear", Boolean, nullable=False, server_default="0"), Column("isClear", Boolean),
Column("isComplete", Boolean, nullable=False, server_default="0"), Column("isComplete", Boolean),
UniqueConstraint("user", "mapId", name="mai2_item_map_uk"), UniqueConstraint("user", "mapId", name="mai2_item_map_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -89,10 +90,10 @@ login_bonus = Table(
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False, nullable=False,
), ),
Column("bonusId", Integer, nullable=False), Column("bonusId", Integer),
Column("point", Integer, nullable=False), Column("point", Integer),
Column("isCurrent", Boolean, nullable=False, server_default="0"), Column("isCurrent", Boolean),
Column("isComplete", Boolean, nullable=False, server_default="0"), Column("isComplete", Boolean),
UniqueConstraint("user", "bonusId", name="mai2_item_login_bonus_uk"), UniqueConstraint("user", "bonusId", name="mai2_item_login_bonus_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -106,12 +107,12 @@ friend_season_ranking = Table(
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False, nullable=False,
), ),
Column("seasonId", Integer, nullable=False), Column("seasonId", Integer),
Column("point", Integer, nullable=False), Column("point", Integer),
Column("rank", Integer, nullable=False), Column("rank", Integer),
Column("rewardGet", Boolean, nullable=False), Column("rewardGet", Boolean),
Column("userName", String(8), nullable=False), Column("userName", String(8)),
Column("recordDate", TIMESTAMP, nullable=False), Column("recordDate", TIMESTAMP),
UniqueConstraint( UniqueConstraint(
"user", "seasonId", "userName", name="mai2_item_friend_season_ranking_uk" "user", "seasonId", "userName", name="mai2_item_friend_season_ranking_uk"
), ),
@@ -127,7 +128,7 @@ favorite = Table(
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False, nullable=False,
), ),
Column("itemKind", Integer, nullable=False), Column("itemKind", Integer),
Column("itemIdList", JSON), Column("itemIdList", JSON),
UniqueConstraint("user", "itemKind", name="mai2_item_favorite_uk"), UniqueConstraint("user", "itemKind", name="mai2_item_favorite_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
@@ -142,10 +143,10 @@ charge = Table(
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False, nullable=False,
), ),
Column("chargeId", Integer, nullable=False), Column("chargeId", Integer),
Column("stock", Integer, nullable=False), Column("stock", Integer),
Column("purchaseDate", String(255), nullable=False), Column("purchaseDate", String(255)),
Column("validDate", String(255), nullable=False), Column("validDate", String(255)),
UniqueConstraint("user", "chargeId", name="mai2_item_charge_uk"), UniqueConstraint("user", "chargeId", name="mai2_item_charge_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -161,11 +162,11 @@ print_detail = Table(
), ),
Column("orderId", Integer), Column("orderId", Integer),
Column("printNumber", Integer), Column("printNumber", Integer),
Column("printDate", TIMESTAMP, nullable=False, server_default=func.now()), Column("printDate", TIMESTAMP, server_default=func.now()),
Column("serialId", String(20), nullable=False), Column("serialId", String(20)),
Column("placeId", Integer, nullable=False), Column("placeId", Integer),
Column("clientId", String(11), nullable=False), Column("clientId", String(11)),
Column("printerSerialId", String(20), nullable=False), Column("printerSerialId", String(20)),
Column("cardRomVersion", Integer), Column("cardRomVersion", Integer),
Column("isHolograph", Boolean, server_default="1"), Column("isHolograph", Boolean, server_default="1"),
Column("printOption1", Boolean, server_default="0"), Column("printOption1", Boolean, server_default="0"),
@@ -333,6 +334,19 @@ class Mai2ItemData(BaseData):
return None return None
return result.fetchone() return result.fetchone()
def put_character_(self, user_id: int, char_data: Dict) -> Optional[int]:
char_data["user"] = user_id
sql = insert(character).values(**char_data)
conflict = sql.on_duplicate_key_update(**char_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"put_character_: failed to insert item! user_id: {user_id}"
)
return None
return result.lastrowid
def put_character( def put_character(
self, self,
user_id: int, user_id: int,
@@ -444,6 +458,8 @@ class Mai2ItemData(BaseData):
card_kind: int, card_kind: int,
chara_id: int, chara_id: int,
map_id: int, map_id: int,
start_date: datetime,
end_date: datetime,
) -> Optional[Row]: ) -> Optional[Row]:
sql = insert(card).values( sql = insert(card).values(
user=user_id, user=user_id,
@@ -451,9 +467,13 @@ class Mai2ItemData(BaseData):
cardTypeId=card_kind, cardTypeId=card_kind,
charaId=chara_id, charaId=chara_id,
mapId=map_id, mapId=map_id,
startDate=start_date,
endDate=end_date,
) )
conflict = sql.on_duplicate_key_update(charaId=chara_id, mapId=map_id) conflict = sql.on_duplicate_key_update(
charaId=chara_id, mapId=map_id, startDate=start_date, endDate=end_date
)
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:

View File

@@ -99,6 +99,68 @@ detail = Table(
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
detail_old = Table(
"maimai_profile_detail",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("lastDataVersion", Integer),
Column("userName", String(8)),
Column("point", Integer),
Column("totalPoint", Integer),
Column("iconId", Integer),
Column("nameplateId", Integer),
Column("frameId", Integer),
Column("trophyId", Integer),
Column("playCount", Integer),
Column("playVsCount", Integer),
Column("playSyncCount", Integer),
Column("winCount", Integer),
Column("helpCount", Integer),
Column("comboCount", Integer),
Column("feverCount", Integer),
Column("totalHiScore", Integer),
Column("totalEasyHighScore", Integer),
Column("totalBasicHighScore", Integer),
Column("totalAdvancedHighScore", Integer),
Column("totalExpertHighScore", Integer),
Column("totalMasterHighScore", Integer),
Column("totalReMasterHighScore", Integer),
Column("totalHighSync", Integer),
Column("totalEasySync", Integer),
Column("totalBasicSync", Integer),
Column("totalAdvancedSync", Integer),
Column("totalExpertSync", Integer),
Column("totalMasterSync", Integer),
Column("totalReMasterSync", Integer),
Column("playerRating", Integer),
Column("highestRating", Integer),
Column("rankAuthTailId", Integer),
Column("eventWatchedDate", String(255)),
Column("webLimitDate", String(255)),
Column("challengeTrackPhase", Integer),
Column("firstPlayBits", Integer),
Column("lastPlayDate", String(255)),
Column("lastPlaceId", Integer),
Column("lastPlaceName", String(255)),
Column("lastRegionId", Integer),
Column("lastRegionName", String(255)),
Column("lastClientId", String(255)),
Column("lastCountryCode", String(255)),
Column("eventPoint", Integer),
Column("totalLv", Integer),
Column("lastLoginBonusDay", Integer),
Column("lastSurvivalBonusDay", Integer),
Column("loginBonusLv", Integer),
UniqueConstraint("user", "version", name="maimai_profile_detail_uk"),
mysql_charset="utf8mb4",
)
ghost = Table( ghost = Table(
"mai2_profile_ghost", "mai2_profile_ghost",
metadata, metadata,
@@ -223,6 +285,99 @@ option = Table(
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
option_old = Table(
"maimai_profile_option",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("soudEffect", Integer),
Column("mirrorMode", Integer),
Column("guideSpeed", Integer),
Column("bgInfo", Integer),
Column("brightness", Integer),
Column("isStarRot", Integer),
Column("breakSe", Integer),
Column("slideSe", Integer),
Column("hardJudge", Integer),
Column("isTagJump", Integer),
Column("breakSeVol", Integer),
Column("slideSeVol", Integer),
Column("isUpperDisp", Integer),
Column("trackSkip", Integer),
Column("optionMode", Integer),
Column("simpleOptionParam", Integer),
Column("adjustTiming", Integer),
Column("dispTiming", Integer),
Column("timingPos", Integer),
Column("ansVol", Integer),
Column("noteVol", Integer),
Column("dmgVol", Integer),
Column("appealFlame", Integer),
Column("isFeverDisp", Integer),
Column("dispJudge", Integer),
Column("judgePos", Integer),
Column("ratingGuard", Integer),
Column("selectChara", Integer),
Column("sortType", Integer),
Column("filterGenre", Integer),
Column("filterLevel", Integer),
Column("filterRank", Integer),
Column("filterVersion", Integer),
Column("filterRec", Integer),
Column("filterFullCombo", Integer),
Column("filterAllPerfect", Integer),
Column("filterDifficulty", Integer),
Column("filterFullSync", Integer),
Column("filterReMaster", Integer),
Column("filterMaxFever", Integer),
Column("finalSelectId", Integer),
Column("finalSelectCategory", Integer),
UniqueConstraint("user", "version", name="maimai_profile_option_uk"),
mysql_charset="utf8mb4",
)
web_opt = Table(
"maimai_profile_web_option",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("isNetMember", Boolean),
Column("dispRate", Integer),
Column("dispJudgeStyle", Integer),
Column("dispRank", Integer),
Column("dispHomeRanker", Integer),
Column("dispTotalLv", Integer),
UniqueConstraint("user", "version", name="maimai_profile_web_option_uk"),
mysql_charset="utf8mb4",
)
grade_status = Table(
"maimai_profile_grade_status",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("gradeVersion", Integer),
Column("gradeLevel", Integer),
Column("gradeSubLevel", Integer),
Column("gradeMaxId", Integer),
UniqueConstraint("user", "gradeVersion", name="maimai_profile_grade_status_uk"),
mysql_charset="utf8mb4",
)
rating = Table( rating = Table(
"mai2_profile_rating", "mai2_profile_rating",
metadata, metadata,
@@ -268,43 +423,92 @@ activity = Table(
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False, nullable=False,
), ),
Column("kind", Integer, nullable=False), Column("kind", Integer),
Column("activityId", Integer, nullable=False), Column("activityId", Integer),
Column("param1", Integer, nullable=False), Column("param1", Integer),
Column("param2", Integer, nullable=False), Column("param2", Integer),
Column("param3", Integer, nullable=False), Column("param3", Integer),
Column("param4", Integer, nullable=False), Column("param4", Integer),
Column("sortNumber", Integer, nullable=False), Column("sortNumber", Integer),
UniqueConstraint("user", "kind", "activityId", name="mai2_profile_activity_uk"), UniqueConstraint("user", "kind", "activityId", name="mai2_profile_activity_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
boss = Table(
"maimai_profile_boss",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("pandoraFlagList0", Integer),
Column("pandoraFlagList1", Integer),
Column("pandoraFlagList2", Integer),
Column("pandoraFlagList3", Integer),
Column("pandoraFlagList4", Integer),
Column("pandoraFlagList5", Integer),
Column("pandoraFlagList6", Integer),
Column("emblemFlagList", Integer),
UniqueConstraint("user", name="mai2_profile_boss_uk"),
mysql_charset="utf8mb4",
)
recent_rating = Table(
"maimai_profile_recent_rating",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("userRecentRatingList", JSON),
UniqueConstraint("user", name="mai2_profile_recent_rating_uk"),
mysql_charset="utf8mb4",
)
consec_logins = Table(
"mai2_profile_consec_logins",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("version", Integer, nullable=False),
Column("logins", Integer),
UniqueConstraint("user", "version", name="mai2_profile_consec_logins_uk"),
mysql_charset="utf8mb4",
)
class Mai2ProfileData(BaseData): class Mai2ProfileData(BaseData):
def put_profile_detail( def put_profile_detail(
self, user_id: int, version: int, detail_data: Dict self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True
) -> Optional[Row]: ) -> Optional[Row]:
detail_data["user"] = user_id detail_data["user"] = user_id
detail_data["version"] = version detail_data["version"] = version
if is_dx:
sql = insert(detail).values(**detail_data) sql = insert(detail).values(**detail_data)
else:
sql = insert(detail_old).values(**detail_data)
conflict = sql.on_duplicate_key_update(**detail_data) conflict = sql.on_duplicate_key_update(**detail_data)
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warn(
f"put_profile: Failed to create profile! user_id {user_id}" f"put_profile: Failed to create profile! user_id {user_id} is_dx {is_dx}"
) )
return None return None
return result.lastrowid return result.lastrowid
def get_profile_detail(self, user_id: int, version: int) -> Optional[Row]: def get_profile_detail(self, user_id: int, version: int, is_dx: bool = True) -> Optional[Row]:
if is_dx:
sql = ( sql = (
select(detail) select(detail)
.where(and_(detail.c.user == user_id, detail.c.version <= version)) .where(and_(detail.c.user == user_id, detail.c.version <= version))
.order_by(detail.c.version.desc()) .order_by(detail.c.version.desc())
) )
else:
sql = (
select(detail_old)
.where(and_(detail_old.c.user == user_id, detail_old.c.version <= version))
.order_by(detail_old.c.version.desc())
)
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
return None return None
@@ -365,26 +569,36 @@ class Mai2ProfileData(BaseData):
return result.fetchone() return result.fetchone()
def put_profile_option( def put_profile_option(
self, user_id: int, version: int, option_data: Dict self, user_id: int, version: int, option_data: Dict, is_dx: bool = True
) -> Optional[int]: ) -> Optional[int]:
option_data["user"] = user_id option_data["user"] = user_id
option_data["version"] = version option_data["version"] = version
if is_dx:
sql = insert(option).values(**option_data) sql = insert(option).values(**option_data)
else:
sql = insert(option_old).values(**option_data)
conflict = sql.on_duplicate_key_update(**option_data) conflict = sql.on_duplicate_key_update(**option_data)
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_profile_option: failed to update! {user_id}") self.logger.warn(f"put_profile_option: failed to update! {user_id} is_dx {is_dx}")
return None return None
return result.lastrowid return result.lastrowid
def get_profile_option(self, user_id: int, version: int) -> Optional[Row]: def get_profile_option(self, user_id: int, version: int, is_dx: bool = True) -> Optional[Row]:
if is_dx:
sql = ( sql = (
select(option) select(option)
.where(and_(option.c.user == user_id, option.c.version <= version)) .where(and_(option.c.user == user_id, option.c.version <= version))
.order_by(option.c.version.desc()) .order_by(option.c.version.desc())
) )
else:
sql = (
select(option_old)
.where(and_(option_old.c.user == user_id, option_old.c.version <= version))
.order_by(option_old.c.version.desc())
)
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
@@ -474,3 +688,130 @@ class Mai2ProfileData(BaseData):
if result is None: if result is None:
return None return None
return result.fetchall() return result.fetchall()
def put_web_option(self, user_id: int, version: int, web_opts: Dict) -> Optional[int]:
web_opts["user"] = user_id
web_opts["version"] = version
sql = insert(web_opt).values(**web_opts)
conflict = sql.on_duplicate_key_update(**web_opts)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"put_web_option: failed to update! user_id: {user_id}"
)
return None
return result.lastrowid
def get_web_option(self, user_id: int, version: int) -> Optional[Row]:
sql = web_opt.select(and_(web_opt.c.user == user_id, web_opt.c.version == version))
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_grade_status(self, user_id: int, grade_stat: Dict) -> Optional[int]:
grade_stat["user"] = user_id
sql = insert(grade_status).values(**grade_stat)
conflict = sql.on_duplicate_key_update(**grade_stat)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"put_grade_status: failed to update! user_id: {user_id}"
)
return None
return result.lastrowid
def get_grade_status(self, user_id: int) -> Optional[Row]:
sql = grade_status.select(grade_status.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_boss_list(self, user_id: int, boss_stat: Dict) -> Optional[int]:
boss_stat["user"] = user_id
sql = insert(boss).values(**boss_stat)
conflict = sql.on_duplicate_key_update(**boss_stat)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"put_boss_list: failed to update! user_id: {user_id}"
)
return None
return result.lastrowid
def get_boss_list(self, user_id: int) -> Optional[Row]:
sql = boss.select(boss.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_recent_rating(self, user_id: int, rr: Dict) -> Optional[int]:
sql = insert(recent_rating).values(user=user_id, userRecentRatingList=rr)
conflict = sql.on_duplicate_key_update({'userRecentRatingList': rr})
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"put_recent_rating: failed to update! user_id: {user_id}"
)
return None
return result.lastrowid
def get_recent_rating(self, user_id: int) -> Optional[Row]:
sql = recent_rating.select(recent_rating.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def add_consec_login(self, user_id: int, version: int) -> None:
sql = insert(consec_logins).values(
user=user_id,
version=version,
logins=1
)
conflict = sql.on_duplicate_key_update(
logins=consec_logins.c.logins + 1
)
result = self.execute(conflict)
if result is None:
self.logger.error(f"Failed to update consecutive login count for user {user_id} version {version}")
def get_consec_login(self, user_id: int, version: int) -> Optional[Row]:
sql = select(consec_logins).where(and_(
consec_logins.c.user==user_id,
consec_logins.c.version==version,
))
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def reset_consec_login(self, user_id: int, version: int) -> Optional[Row]:
sql = consec_logins.update(and_(
consec_logins.c.user==user_id,
consec_logins.c.version==version,
)).values(
logins=1
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()

View File

@@ -7,6 +7,7 @@ from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata from core.data.schema import BaseData, metadata
from core.data import cached
best_score = Table( best_score = Table(
"mai2_score_best", "mai2_score_best",
@@ -174,29 +175,137 @@ course = Table(
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
playlog_old = Table(
"maimai_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer),
# Pop access code
Column("orderId", Integer),
Column("sortNumber", Integer),
Column("placeId", Integer),
Column("placeName", String(255)),
Column("country", String(255)),
Column("regionId", Integer),
Column("playDate", String(255)),
Column("userPlayDate", String(255)),
Column("musicId", Integer),
Column("level", Integer),
Column("gameMode", Integer),
Column("rivalNum", Integer),
Column("track", Integer),
Column("eventId", Integer),
Column("isFreeToPlay", Boolean),
Column("playerRating", Integer),
Column("playedUserId1", Integer),
Column("playedUserId2", Integer),
Column("playedUserId3", Integer),
Column("playedUserName1", String(255)),
Column("playedUserName2", String(255)),
Column("playedUserName3", String(255)),
Column("playedMusicLevel1", Integer),
Column("playedMusicLevel2", Integer),
Column("playedMusicLevel3", Integer),
Column("achievement", Integer),
Column("score", Integer),
Column("tapScore", Integer),
Column("holdScore", Integer),
Column("slideScore", Integer),
Column("breakScore", Integer),
Column("syncRate", Integer),
Column("vsWin", Integer),
Column("isAllPerfect", Boolean),
Column("fullCombo", Integer),
Column("maxFever", Integer),
Column("maxCombo", Integer),
Column("tapPerfect", Integer),
Column("tapGreat", Integer),
Column("tapGood", Integer),
Column("tapBad", Integer),
Column("holdPerfect", Integer),
Column("holdGreat", Integer),
Column("holdGood", Integer),
Column("holdBad", Integer),
Column("slidePerfect", Integer),
Column("slideGreat", Integer),
Column("slideGood", Integer),
Column("slideBad", Integer),
Column("breakPerfect", Integer),
Column("breakGreat", Integer),
Column("breakGood", Integer),
Column("breakBad", Integer),
Column("judgeStyle", Integer),
Column("isTrackSkip", Boolean),
Column("isHighScore", Boolean),
Column("isChallengeTrack", Boolean),
Column("challengeLife", Integer),
Column("challengeRemain", Integer),
Column("isAllPerfectPlus", Integer),
mysql_charset="utf8mb4",
)
best_score_old = Table(
"maimai_score_best",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("musicId", Integer),
Column("level", Integer),
Column("playCount", Integer),
Column("achievement", Integer),
Column("scoreMax", Integer),
Column("syncRateMax", Integer),
Column("isAllPerfect", Boolean),
Column("isAllPerfectPlus", Integer),
Column("fullCombo", Integer),
Column("maxFever", Integer),
UniqueConstraint("user", "musicId", "level", name="maimai_score_best_uk"),
mysql_charset="utf8mb4",
)
class Mai2ScoreData(BaseData): class Mai2ScoreData(BaseData):
def put_best_score(self, user_id: int, score_data: Dict) -> Optional[int]: def put_best_score(self, user_id: int, score_data: Dict, is_dx: bool = True) -> Optional[int]:
score_data["user"] = user_id score_data["user"] = user_id
sql = insert(best_score).values(**score_data)
if is_dx:
sql = insert(best_score).values(**score_data)
else:
sql = insert(best_score_old).values(**score_data)
conflict = sql.on_duplicate_key_update(**score_data) conflict = sql.on_duplicate_key_update(**score_data)
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.error( self.logger.error(
f"put_best_score: Failed to insert best score! user_id {user_id}" f"put_best_score: Failed to insert best score! user_id {user_id} is_dx {is_dx}"
) )
return None return None
return result.lastrowid return result.lastrowid
def get_best_scores(self, user_id: int, song_id: int = None) -> Optional[List[Row]]: @cached(2)
def get_best_scores(self, user_id: int, song_id: int = None, is_dx: bool = True) -> Optional[List[Row]]:
if is_dx:
sql = best_score.select( sql = best_score.select(
and_( and_(
best_score.c.user == user_id, best_score.c.user == user_id,
(best_score.c.song_id == song_id) if song_id is not None else True, (best_score.c.song_id == song_id) if song_id is not None else True,
) )
) )
else:
sql = best_score_old.select(
and_(
best_score_old.c.user == user_id,
(best_score_old.c.song_id == song_id) if song_id is not None else True,
)
)
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
@@ -219,15 +328,19 @@ class Mai2ScoreData(BaseData):
return None return None
return result.fetchone() return result.fetchone()
def put_playlog(self, user_id: int, playlog_data: Dict) -> Optional[int]: def put_playlog(self, user_id: int, playlog_data: Dict, is_dx: bool = True) -> Optional[int]:
playlog_data["user"] = user_id playlog_data["user"] = user_id
if is_dx:
sql = insert(playlog).values(**playlog_data) sql = insert(playlog).values(**playlog_data)
else:
sql = insert(playlog_old).values(**playlog_data)
conflict = sql.on_duplicate_key_update(**playlog_data) conflict = sql.on_duplicate_key_update(**playlog_data)
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.error(f"put_playlog: Failed to insert! user_id {user_id}") self.logger.error(f"put_playlog: Failed to insert! user_id {user_id} is_dx {is_dx}")
return None return None
return result.lastrowid return result.lastrowid
@@ -249,4 +362,4 @@ class Mai2ScoreData(BaseData):
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
return None return None
return result.fetchone() return result.fetchall()

View File

@@ -4,12 +4,12 @@ import pytz
import json import json
from core.config import CoreConfig from core.config import CoreConfig
from titles.mai2.base import Mai2Base from titles.mai2.dx import Mai2DX
from titles.mai2.config import Mai2Config from titles.mai2.config import Mai2Config
from titles.mai2.const import Mai2Constants from titles.mai2.const import Mai2Constants
class Mai2Splash(Mai2Base): class Mai2Splash(Mai2DX):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg) super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH

View File

@@ -4,12 +4,12 @@ import pytz
import json import json
from core.config import CoreConfig from core.config import CoreConfig
from titles.mai2.base import Mai2Base from titles.mai2.dx import Mai2DX
from titles.mai2.config import Mai2Config from titles.mai2.config import Mai2Config
from titles.mai2.const import Mai2Constants from titles.mai2.const import Mai2Constants
class Mai2SplashPlus(Mai2Base): class Mai2SplashPlus(Mai2DX):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg) super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS

View File

@@ -5,12 +5,12 @@ import pytz
import json import json
from core.config import CoreConfig from core.config import CoreConfig
from titles.mai2.base import Mai2Base from titles.mai2.dx import Mai2DX
from titles.mai2.const import Mai2Constants from titles.mai2.const import Mai2Constants
from titles.mai2.config import Mai2Config from titles.mai2.config import Mai2Config
class Mai2Universe(Mai2Base): class Mai2Universe(Mai2DX):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg) super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE
@@ -104,8 +104,12 @@ class Mai2Universe(Mai2Base):
tmp.pop("id") tmp.pop("id")
tmp.pop("user") tmp.pop("user")
tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S") tmp["startDate"] = datetime.strftime(
tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S") tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["endDate"] = datetime.strftime(
tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
)
card_list.append(tmp) card_list.append(tmp)
return { return {
@@ -154,6 +158,10 @@ class Mai2Universe(Mai2Base):
# set a random card serial number # set a random card serial number
serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) serial_id = "".join([str(randint(0, 9)) for _ in range(20)])
# calculate start and end date of the card
start_date = datetime.utcnow()
end_date = datetime.utcnow() + timedelta(days=15)
user_card = upsert["userCard"] user_card = upsert["userCard"]
self.data.item.put_card( self.data.item.put_card(
user_id, user_id,
@@ -161,8 +169,26 @@ class Mai2Universe(Mai2Base):
user_card["cardTypeId"], user_card["cardTypeId"],
user_card["charaId"], user_card["charaId"],
user_card["mapId"], user_card["mapId"],
# add the correct start date and also the end date in 15 days
start_date,
end_date,
) )
# get the profile extend to save the new bought card
extend = self.data.profile.get_profile_extend(user_id, self.version)
if extend:
extend = extend._asdict()
# parse the selectedCardList
# 6 = Freedom Pass, 4 = Gold Pass (cardTypeId)
selected_cards: List = extend["selectedCardList"]
# if no pass is already added, add the corresponding pass
if not user_card["cardTypeId"] in selected_cards:
selected_cards.insert(0, user_card["cardTypeId"])
extend["selectedCardList"] = selected_cards
self.data.profile.put_profile_extend(user_id, self.version, extend)
# properly format userPrintDetail for the database # properly format userPrintDetail for the database
upsert.pop("userCard") upsert.pop("userCard")
upsert.pop("serialId") upsert.pop("serialId")
@@ -174,8 +200,8 @@ class Mai2Universe(Mai2Base):
"returnCode": 1, "returnCode": 1,
"orderId": 0, "orderId": 0,
"serialId": serial_id, "serialId": serial_id,
"startDate": "2018-01-01 00:00:00", "startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT),
"endDate": "2038-01-01 00:00:00", "endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT),
} }
def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:

View File

@@ -1,12 +1,12 @@
from typing import Dict from typing import Dict
from core.config import CoreConfig from core.config import CoreConfig
from titles.mai2.universe import Mai2Universe from titles.mai2.dx import Mai2DX
from titles.mai2.const import Mai2Constants from titles.mai2.const import Mai2Constants
from titles.mai2.config import Mai2Config from titles.mai2.config import Mai2Config
class Mai2UniversePlus(Mai2Universe): class Mai2UniversePlus(Mai2DX):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg) super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS

View File

@@ -7,4 +7,4 @@ index = OngekiServlet
database = OngekiData database = OngekiData
reader = OngekiReader reader = OngekiReader
game_codes = [OngekiConstants.GAME_CODE] game_codes = [OngekiConstants.GAME_CODE]
current_schema_version = 4 current_schema_version = 5

View File

@@ -142,7 +142,7 @@ class OngekiBase:
def handle_get_game_point_api_request(self, data: Dict) -> Dict: def handle_get_game_point_api_request(self, data: Dict) -> Dict:
""" """
Sets the GP ammount for A and B sets for 1 - 3 crdits Sets the GP amount for A and B sets for 1 - 3 credits
""" """
return { return {
"length": 6, "length": 6,
@@ -155,13 +155,13 @@ class OngekiBase:
}, },
{ {
"type": 1, "type": 1,
"cost": 200, "cost": 230,
"startDate": "2000-01-01 05:00:00.0", "startDate": "2000-01-01 05:00:00.0",
"endDate": "2099-01-01 05:00:00.0", "endDate": "2099-01-01 05:00:00.0",
}, },
{ {
"type": 2, "type": 2,
"cost": 300, "cost": 370,
"startDate": "2000-01-01 05:00:00.0", "startDate": "2000-01-01 05:00:00.0",
"endDate": "2099-01-01 05:00:00.0", "endDate": "2099-01-01 05:00:00.0",
}, },
@@ -256,7 +256,11 @@ class OngekiBase:
{ {
"type": event["type"], "type": event["type"],
"id": event["eventId"], "id": event["eventId"],
"startDate": "2017-12-05 07:00:00.0", # actually use the startDate from the import so it
# properly shows all the events when new ones are imported
"startDate": datetime.strftime(
event["startDate"], "%Y-%m-%d %H:%M:%S.0"
),
"endDate": "2099-12-31 00:00:00.0", "endDate": "2099-12-31 00:00:00.0",
} }
) )
@@ -268,7 +272,7 @@ class OngekiBase:
} }
def handle_get_game_id_list_api_request(self, data: Dict) -> Dict: def handle_get_game_id_list_api_request(self, data: Dict) -> Dict:
game_idlist: list[str, Any] = [] # 1 to 230 & 8000 to 8050 game_idlist: List[str, Any] = [] # 1 to 230 & 8000 to 8050
if data["type"] == 1: if data["type"] == 1:
for i in range(1, 231): for i in range(1, 231):
@@ -443,7 +447,7 @@ class OngekiBase:
"userItemList": [], "userItemList": [],
} }
items: list[Dict[str, Any]] = [] items: List[Dict[str, Any]] = []
for i in range(data["nextIndex"] % 10000000000, len(p)): for i in range(data["nextIndex"] % 10000000000, len(p)):
if len(items) > data["maxCount"]: if len(items) > data["maxCount"]:
break break
@@ -560,7 +564,11 @@ class OngekiBase:
def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict: def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict:
recent_rating = self.data.profile.get_profile_recent_rating(data["userId"]) recent_rating = self.data.profile.get_profile_recent_rating(data["userId"])
if recent_rating is None: if recent_rating is None:
return {} return {
"userId": data["userId"],
"length": 0,
"userRecentRatingList": [],
}
userRecentRatingList = recent_rating["recentRating"] userRecentRatingList = recent_rating["recentRating"]

View File

@@ -43,15 +43,15 @@ class OngekiBright(OngekiBase):
user_data.pop("user") user_data.pop("user")
user_data.pop("version") user_data.pop("version")
# TODO: replace datetime objects with strings
# add access code that we don't store # add access code that we don't store
user_data["accessCode"] = cards[0]["access_code"] user_data["accessCode"] = cards[0]["access_code"]
# hardcode Card Maker version for now # add the compatible card maker version from config
# Card Maker 1.34.00 = 1.30.01 card_maker_ver = self.game_cfg.version.version(self.version)
# Card Maker 1.36.00 = 1.35.04 if card_maker_ver and card_maker_ver.get("card_maker"):
user_data["compatibleCmVersion"] = "1.30.01" # Card Maker 1.30 = 1.30.01+
# Card Maker 1.35 = 1.35.03+
user_data["compatibleCmVersion"] = card_maker_ver.get("card_maker")
return {"userId": data["userId"], "userData": user_data} return {"userId": data["userId"], "userData": user_data}
@@ -333,6 +333,8 @@ class OngekiBright(OngekiBase):
select_point = data["selectPoint"] select_point = data["selectPoint"]
total_gacha_count, ceiling_gacha_count = 0, 0 total_gacha_count, ceiling_gacha_count = 0, 0
# 0 = can still use Gacha Select, 1 = already used Gacha Select
use_select_point = 0
daily_gacha_cnt, five_gacha_cnt, eleven_gacha_cnt = 0, 0, 0 daily_gacha_cnt, five_gacha_cnt, eleven_gacha_cnt = 0, 0, 0
daily_gacha_date = datetime.strptime("2000-01-01", "%Y-%m-%d") daily_gacha_date = datetime.strptime("2000-01-01", "%Y-%m-%d")
@@ -344,6 +346,9 @@ class OngekiBright(OngekiBase):
daily_gacha_cnt = user_gacha["dailyGachaCnt"] daily_gacha_cnt = user_gacha["dailyGachaCnt"]
five_gacha_cnt = user_gacha["fiveGachaCnt"] five_gacha_cnt = user_gacha["fiveGachaCnt"]
eleven_gacha_cnt = user_gacha["elevenGachaCnt"] eleven_gacha_cnt = user_gacha["elevenGachaCnt"]
# if the Gacha Select has been used, make sure to keep it
if user_gacha["useSelectPoint"] == 1:
use_select_point = 1
# parse just the year, month and date # parse just the year, month and date
daily_gacha_date = user_gacha["dailyGachaDate"] daily_gacha_date = user_gacha["dailyGachaDate"]
@@ -359,7 +364,7 @@ class OngekiBright(OngekiBase):
totalGachaCnt=total_gacha_count + gacha_count, totalGachaCnt=total_gacha_count + gacha_count,
ceilingGachaCnt=ceiling_gacha_count + gacha_count, ceilingGachaCnt=ceiling_gacha_count + gacha_count,
selectPoint=select_point, selectPoint=select_point,
useSelectPoint=0, useSelectPoint=use_select_point,
dailyGachaCnt=daily_gacha_cnt + gacha_count, dailyGachaCnt=daily_gacha_cnt + gacha_count,
fiveGachaCnt=five_gacha_cnt + 1 if gacha_count == 5 else five_gacha_cnt, fiveGachaCnt=five_gacha_cnt + 1 if gacha_count == 5 else five_gacha_cnt,
elevenGachaCnt=eleven_gacha_cnt + 1 elevenGachaCnt=eleven_gacha_cnt + 1

View File

@@ -136,14 +136,3 @@ class OngekiBrightMemory(OngekiBright):
def handle_get_game_music_release_state_api_request(self, data: Dict) -> Dict: def handle_get_game_music_release_state_api_request(self, data: Dict) -> Dict:
return {"techScore": 0, "cardNum": 0} return {"techScore": 0, "cardNum": 0}
def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
# check for a bright memory profile
user_data = super().handle_cm_get_user_data_api_request(data)
# hardcode Card Maker version for now
# Card Maker 1.34 = 1.30.01
# Card Maker 1.35 = 1.35.03
user_data["userData"]["compatibleCmVersion"] = "1.35.03"
return user_data

View File

@@ -1,3 +1,4 @@
from ast import Dict
from typing import List from typing import List
from core.config import CoreConfig from core.config import CoreConfig
@@ -33,7 +34,23 @@ class OngekiGachaConfig:
) )
class OngekiCardMakerVersionConfig:
def __init__(self, parent_config: "OngekiConfig") -> None:
self.__config = parent_config
def version(self, version: int) -> Dict:
"""
in the form of:
<ongeki version>: {"card_maker": <compatible card maker version>}
6: {"card_maker": 1.30.01}
"""
return CoreConfig.get_config_field(
self.__config, "ongeki", "version", default={}
).get(version)
class OngekiConfig(dict): class OngekiConfig(dict):
def __init__(self) -> None: def __init__(self) -> None:
self.server = OngekiServerConfig(self) self.server = OngekiServerConfig(self)
self.gachas = OngekiGachaConfig(self) self.gachas = OngekiGachaConfig(self)
self.version = OngekiCardMakerVersionConfig(self)

View File

@@ -66,13 +66,13 @@ class OngekiConstants:
VERSION_NAMES = ( VERSION_NAMES = (
"ONGEKI", "ONGEKI",
"ONGEKI+", "ONGEKI +",
"ONGEKI Summer", "ONGEKI SUMMER",
"ONGEKI Summer+", "ONGEKI SUMMER +",
"ONGEKI Red", "ONGEKI R.E.D.",
"ONGEKI Red+", "ONGEKI R.E.D. +",
"ONGEKI Bright", "ONGEKI bright",
"ONGEKI Bright Memory", "ONGEKI bright MEMORY",
) )
@classmethod @classmethod

View File

@@ -11,6 +11,7 @@ from os import path
from typing import Tuple from typing import Tuple
from core.config import CoreConfig from core.config import CoreConfig
from core.utils import Utils
from titles.ongeki.config import OngekiConfig from titles.ongeki.config import OngekiConfig
from titles.ongeki.const import OngekiConstants from titles.ongeki.const import OngekiConstants
from titles.ongeki.base import OngekiBase from titles.ongeki.base import OngekiBase
@@ -101,6 +102,7 @@ class OngekiServlet:
url_split = url_path.split("/") url_split = url_path.split("/")
internal_ver = 0 internal_ver = 0
endpoint = url_split[len(url_split) - 1] endpoint = url_split[len(url_split) - 1]
client_ip = Utils.get_ip_addr(request)
if version < 105: # 1.0 if version < 105: # 1.0
internal_ver = OngekiConstants.VER_ONGEKI internal_ver = OngekiConstants.VER_ONGEKI
@@ -137,7 +139,10 @@ class OngekiServlet:
req_data = json.loads(unzip) req_data = json.loads(unzip)
self.logger.info(f"v{version} {endpoint} request - {req_data}") self.logger.info(
f"v{version} {endpoint} request from {client_ip}"
)
self.logger.debug(req_data)
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
@@ -156,6 +161,6 @@ class OngekiServlet:
if resp == None: if resp == None:
resp = {"returnCode": 1} resp = {"returnCode": 1}
self.logger.info(f"Response {resp}") self.logger.debug(f"Response {resp}")
return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))

View File

@@ -316,7 +316,7 @@ class OngekiProfileData(BaseData):
return result.fetchone() return result.fetchone()
def get_profile_rating_log(self, aime_id: int) -> Optional[List[Row]]: def get_profile_rating_log(self, aime_id: int) -> Optional[List[Row]]:
sql = select(rating_log).where(recent_rating.c.user == aime_id) sql = select(rating_log).where(rating_log.c.user == aime_id)
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:

View File

@@ -16,6 +16,7 @@ events = Table(
Column("eventId", Integer), Column("eventId", Integer),
Column("type", Integer), Column("type", Integer),
Column("name", String(255)), Column("name", String(255)),
Column("startDate", TIMESTAMP, server_default=func.now()),
Column("enabled", Boolean, server_default="1"), Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "eventId", "type", name="ongeki_static_events_uk"), UniqueConstraint("version", "eventId", "type", name="ongeki_static_events_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",

View File

@@ -1,6 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
import json, logging import json, logging
from typing import Any, Dict from typing import Any, Dict, List
import random import random
from core.data import Data from core.data import Data
@@ -8,6 +8,7 @@ from core import CoreConfig
from .config import PokkenConfig from .config import PokkenConfig
from .proto import jackal_pb2 from .proto import jackal_pb2
from .database import PokkenData from .database import PokkenData
from .const import PokkenConstants
class PokkenBase: class PokkenBase:
@@ -44,19 +45,19 @@ class PokkenBase:
biwa_setting = { biwa_setting = {
"MatchingServer": { "MatchingServer": {
"host": f"https://{self.game_cfg.server.hostname}", "host": f"https://{self.game_cfg.server.hostname}",
"port": self.game_cfg.server.port, "port": self.game_cfg.ports.game,
"url": "/SDAK/100/matching", "url": "/SDAK/100/matching",
}, },
"StunServer": { "StunServer": {
"addr": self.game_cfg.server.hostname, "addr": self.game_cfg.server.stun_server_host,
"port": self.game_cfg.server.port_stun, "port": self.game_cfg.server.stun_server_port,
}, },
"TurnServer": { "TurnServer": {
"addr": self.game_cfg.server.hostname, "addr": self.game_cfg.server.stun_server_host,
"port": self.game_cfg.server.port_turn, "port": self.game_cfg.server.stun_server_port,
}, },
"AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.server.port_admission}", "AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.ports.admission}",
"locationId": 123, "locationId": 123, # FIXME: Get arcade's ID from the database
"logfilename": "JackalMatchingLibrary.log", "logfilename": "JackalMatchingLibrary.log",
"biwalogfilename": "./biwa.log", "biwalogfilename": "./biwa.log",
} }
@@ -94,6 +95,7 @@ class PokkenBase:
res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS
settings = jackal_pb2.LoadClientSettingsResponseData() settings = jackal_pb2.LoadClientSettingsResponseData()
# TODO: Make configurable
settings.money_magnification = 1 settings.money_magnification = 1
settings.continue_bonus_exp = 100 settings.continue_bonus_exp = 100
settings.continue_fight_money = 100 settings.continue_fight_money = 100
@@ -274,6 +276,100 @@ class PokkenBase:
res.result = 1 res.result = 1
res.type = jackal_pb2.MessageType.SAVE_USER res.type = jackal_pb2.MessageType.SAVE_USER
req = request.save_user
user_id = req.banapass_id
tut_flgs: List[int] = []
ach_flgs: List[int] = []
evt_flgs: List[int] = []
evt_params: List[int] = []
get_rank_pts: int = req.get_trainer_rank_point if req.get_trainer_rank_point else 0
get_money: int = req.get_money
get_score_pts: int = req.get_score_point if req.get_score_point else 0
grade_max: int = req.grade_max_num
extra_counter: int = req.extra_counter
evt_reward_get_flg: int = req.event_reward_get_flag
num_continues: int = req.continue_num
total_play_days: int = req.total_play_days
awake_num: int = req.awake_num # ?
use_support_ct: int = req.use_support_num
beat_num: int = req.beat_num # ?
evt_state: int = req.event_state
aid_skill: int = req.aid_skill
last_evt: int = req.last_play_event_id
battle = req.battle_data
mon = req.pokemon_data
p = self.data.profile.touch_profile(user_id)
if p is None or not p:
self.data.profile.create_profile(user_id)
if req.trainer_name_pending is not None and req.trainer_name_pending: # we're saving for the first time
self.data.profile.set_profile_name(user_id, req.trainer_name_pending, req.avatar_gender if req.avatar_gender else None)
for tut_flg in req.tutorial_progress_flag:
tut_flgs.append(tut_flg)
self.data.profile.update_profile_tutorial_flags(user_id, tut_flgs)
for ach_flg in req.achievement_flag:
ach_flgs.append(ach_flg)
self.data.profile.update_profile_tutorial_flags(user_id, ach_flg)
for evt_flg in req.event_achievement_flag:
evt_flgs.append(evt_flg)
for evt_param in req.event_achievement_param:
evt_params.append(evt_param)
self.data.profile.update_profile_event(user_id, evt_state, evt_flgs, evt_params, )
for reward in req.reward_data:
self.data.item.add_reward(user_id, reward.get_category_id, reward.get_content_id, reward.get_type_id)
self.data.profile.add_profile_points(user_id, get_rank_pts, get_money, get_score_pts, grade_max)
self.data.profile.update_support_team(user_id, 1, req.support_set_1[0], req.support_set_1[1])
self.data.profile.update_support_team(user_id, 2, req.support_set_2[0], req.support_set_2[1])
self.data.profile.update_support_team(user_id, 3, req.support_set_3[0], req.support_set_3[1])
self.data.profile.put_pokemon(user_id, mon.char_id, mon.illustration_book_no, mon.bp_point_atk, mon.bp_point_res, mon.bp_point_def, mon.bp_point_sp)
self.data.profile.add_pokemon_xp(user_id, mon.char_id, mon.get_pokemon_exp)
for x in range(len(battle.play_mode)):
self.data.profile.put_pokemon_battle_result(
user_id,
mon.char_id,
PokkenConstants.BATTLE_TYPE(battle.play_mode[x]),
PokkenConstants.BATTLE_RESULT(battle.result[x])
)
self.data.profile.put_stats(
user_id,
battle.ex_ko_num,
battle.wko_num,
battle.timeup_win_num,
battle.cool_ko_num,
battle.perfect_ko_num,
num_continues
)
self.data.profile.put_extra(
user_id,
extra_counter,
evt_reward_get_flg,
total_play_days,
awake_num,
use_support_ct,
beat_num,
aid_skill,
last_evt
)
return res.SerializeToString() return res.SerializeToString()
def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes: def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes:
@@ -307,11 +403,30 @@ class PokkenBase:
"pcb_id": data["data"]["must"]["pcb_id"], "pcb_id": data["data"]["must"]["pcb_id"],
"gip": client_ip "gip": client_ip
}, },
"list":[]
""" """
return {} return {
"data": {
"sessionId":"12345678",
"A":{
"pcb_id": data["data"]["must"]["pcb_id"],
"gip": client_ip
},
"list":[]
}
}
def handle_matching_stop_matching( def handle_matching_stop_matching(
self, data: Dict = {}, client_ip: str = "127.0.0.1" self, data: Dict = {}, client_ip: str = "127.0.0.1"
) -> Dict: ) -> Dict:
return {} return {}
def handle_admission_noop(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict:
return {}
def handle_admission_joinsession(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict:
self.logger.info(f"Admission: JoinSession from {req_ip}")
return {
'data': {
"id": 12345678
}
}

View File

@@ -25,30 +25,6 @@ class PokkenServerConfig:
) )
) )
@property
def port(self) -> int:
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "port", default=9000
)
@property
def port_stun(self) -> int:
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "port_stun", default=9001
)
@property
def port_turn(self) -> int:
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "port_turn", default=9002
)
@property
def port_admission(self) -> int:
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "port_admission", default=9003
)
@property @property
def auto_register(self) -> bool: def auto_register(self) -> bool:
""" """
@@ -59,7 +35,51 @@ class PokkenServerConfig:
self.__config, "pokken", "server", "auto_register", default=True self.__config, "pokken", "server", "auto_register", default=True
) )
@property
def enable_matching(self) -> bool:
"""
If global matching should happen
"""
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "enable_matching", default=False
)
@property
def stun_server_host(self) -> str:
"""
Hostname of the EXTERNAL stun server the game should connect to. This is not handled by artemis.
"""
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "stun_server_host", default="stunserver.stunprotocol.org"
)
@property
def stun_server_port(self) -> int:
"""
Port of the EXTERNAL stun server the game should connect to. This is not handled by artemis.
"""
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "stun_server_port", default=3478
)
class PokkenPortsConfig:
def __init__(self, parent_config: "PokkenConfig"):
self.__config = parent_config
@property
def game(self) -> int:
return CoreConfig.get_config_field(
self.__config, "pokken", "ports", "game", default=9000
)
@property
def admission(self) -> int:
return CoreConfig.get_config_field(
self.__config, "pokken", "ports", "admission", default=9001
)
class PokkenConfig(dict): class PokkenConfig(dict):
def __init__(self) -> None: def __init__(self) -> None:
self.server = PokkenServerConfig(self) self.server = PokkenServerConfig(self)
self.ports = PokkenPortsConfig(self)

View File

@@ -11,14 +11,14 @@ class PokkenConstants:
VERSION_NAMES = "Pokken Tournament" VERSION_NAMES = "Pokken Tournament"
class BATTLE_TYPE(Enum): class BATTLE_TYPE(Enum):
BATTLE_TYPE_TUTORIAL = 1 TUTORIAL = 1
BATTLE_TYPE_AI = 2 AI = 2
BATTLE_TYPE_LAN = 3 LAN = 3
BATTLE_TYPE_WAN = 4 WAN = 4
class BATTLE_RESULT(Enum): class BATTLE_RESULT(Enum):
BATTLE_RESULT_WIN = 1 WIN = 1
BATTLE_RESULT_LOSS = 2 LOSS = 2
@classmethod @classmethod
def game_ver_to_string(cls, ver: int): def game_ver_to_string(cls, ver: int):

View File

@@ -2,8 +2,9 @@ import yaml
import jinja2 import jinja2
from twisted.web.http import Request from twisted.web.http import Request
from os import path from os import path
from twisted.web.server import Session
from core.frontend import FE_Base from core.frontend import FE_Base, IUserSession
from core.config import CoreConfig from core.config import CoreConfig
from .database import PokkenData from .database import PokkenData
from .config import PokkenConfig from .config import PokkenConfig
@@ -27,7 +28,12 @@ class PokkenFrontend(FE_Base):
template = self.environment.get_template( template = self.environment.get_template(
"titles/pokken/frontend/pokken_index.jinja" "titles/pokken/frontend/pokken_index.jinja"
) )
sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh)
return template.render( return template.render(
title=f"{self.core_config.server.name} | {self.nav_name}", title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"], game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh)
).encode("utf-16") ).encode("utf-16")

View File

@@ -1,6 +1,7 @@
from typing import Tuple from typing import Tuple
from twisted.web.http import Request from twisted.web.http import Request
from twisted.web import resource from twisted.web import resource
from twisted.internet import reactor
import json, ast import json, ast
from datetime import datetime from datetime import datetime
import yaml import yaml
@@ -11,10 +12,11 @@ from os import path
from google.protobuf.message import DecodeError from google.protobuf.message import DecodeError
from core import CoreConfig, Utils from core import CoreConfig, Utils
from titles.pokken.config import PokkenConfig from .config import PokkenConfig
from titles.pokken.base import PokkenBase from .base import PokkenBase
from titles.pokken.const import PokkenConstants from .const import PokkenConstants
from titles.pokken.proto import jackal_pb2 from .proto import jackal_pb2
from .services import PokkenAdmissionFactory
class PokkenServlet(resource.Resource): class PokkenServlet(resource.Resource):
@@ -69,7 +71,7 @@ class PokkenServlet(resource.Resource):
return ( return (
True, True,
f"https://{game_cfg.server.hostname}:{game_cfg.server.port}/{game_code}/$v/", f"https://{game_cfg.server.hostname}:{game_cfg.ports.game}/{game_code}/$v/",
f"{game_cfg.server.hostname}/SDAK/$v/", f"{game_cfg.server.hostname}/SDAK/$v/",
) )
@@ -90,8 +92,10 @@ class PokkenServlet(resource.Resource):
return (True, "PKF1") return (True, "PKF1")
def setup(self) -> None: def setup(self) -> None:
# TODO: Setup stun, turn (UDP) and admission (WSS) servers if self.game_cfg.server.enable_matching:
pass reactor.listenTCP(
self.game_cfg.ports.admission, PokkenAdmissionFactory(self.core_cfg, self.game_cfg)
)
def render_POST( def render_POST(
self, request: Request, version: int = 0, endpoints: str = "" self, request: Request, version: int = 0, endpoints: str = ""
@@ -128,6 +132,9 @@ class PokkenServlet(resource.Resource):
return ret return ret
def handle_matching(self, request: Request) -> bytes: def handle_matching(self, request: Request) -> bytes:
if not self.game_cfg.server.enable_matching:
return b""
content = request.content.getvalue() content = request.content.getvalue()
client_ip = Utils.get_ip_addr(request) client_ip = Utils.get_ip_addr(request)

View File

@@ -31,4 +31,16 @@ class PokkenItemData(BaseData):
Items obtained as rewards Items obtained as rewards
""" """
pass def add_reward(self, user_id: int, category: int, content: int, item_type: int) -> Optional[int]:
sql = insert(item).values(
user=user_id,
category=category,
content=content,
type=item_type,
)
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to insert reward for user {user_id}: {category}-{content}-{item_type}")
return None
return result.lastrowid

View File

@@ -1,4 +1,4 @@
from typing import Optional, Dict, List from typing import Optional, Dict, List, Union
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey from sqlalchemy.schema import ForeignKey
@@ -125,7 +125,7 @@ pokemon_data = Table(
Column("win_vs_lan", Integer), Column("win_vs_lan", Integer),
Column("battle_num_vs_cpu", Integer), # 2 Column("battle_num_vs_cpu", Integer), # 2
Column("win_cpu", Integer), Column("win_cpu", Integer),
Column("battle_all_num_tutorial", Integer), Column("battle_all_num_tutorial", Integer), # ???
Column("battle_num_tutorial", Integer), # 1? Column("battle_num_tutorial", Integer), # 1?
Column("bp_point_atk", Integer), Column("bp_point_atk", Integer),
Column("bp_point_res", Integer), Column("bp_point_res", Integer),
@@ -137,6 +137,14 @@ pokemon_data = Table(
class PokkenProfileData(BaseData): class PokkenProfileData(BaseData):
def touch_profile(self, user_id: int) -> Optional[int]:
sql = select([profile.c.id]).where(profile.c.user == user_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()['id']
def create_profile(self, user_id: int) -> Optional[int]: def create_profile(self, user_id: int) -> Optional[int]:
sql = insert(profile).values(user=user_id) sql = insert(profile).values(user=user_id)
conflict = sql.on_duplicate_key_update(user=user_id) conflict = sql.on_duplicate_key_update(user=user_id)
@@ -147,11 +155,10 @@ class PokkenProfileData(BaseData):
return None return None
return result.lastrowid return result.lastrowid
def set_profile_name(self, user_id: int, new_name: str) -> None: def set_profile_name(self, user_id: int, new_name: str, gender: Union[int, None] = None) -> None:
sql = ( sql = update(profile).where(profile.c.user == user_id).values(
update(profile) trainer_name=new_name,
.where(profile.c.user == user_id) avatar_gender=gender if gender is not None else profile.c.avatar_gender
.values(trainer_name=new_name)
) )
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
@@ -159,13 +166,75 @@ class PokkenProfileData(BaseData):
f"Failed to update pokken profile name for user {user_id}!" f"Failed to update pokken profile name for user {user_id}!"
) )
def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: Dict) -> None: def put_extra(
pass self,
user_id: int,
extra_counter: int,
evt_reward_get_flg: int,
total_play_days: int,
awake_num: int,
use_support_ct: int,
beat_num: int,
aid_skill: int,
last_evt: int
) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
extra_counter=extra_counter,
event_reward_get_flag=evt_reward_get_flg,
total_play_days=total_play_days,
awake_num=awake_num,
use_support_num=use_support_ct,
beat_num=beat_num,
aid_skill=aid_skill,
last_play_event_id=last_evt
)
result = self.execute(sql)
if result is None:
self.logger.error(f"Failed to put extra data for user {user_id}")
def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: List) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
tutorial_progress_flag=tutorial_flags,
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"Failed to update pokken profile tutorial flags for user {user_id}!"
)
def update_profile_achievement_flags(self, user_id: int, achievement_flags: List) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
achievement_flag=achievement_flags,
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"Failed to update pokken profile achievement flags for user {user_id}!"
)
def update_profile_event(self, user_id: int, event_state: List, event_flags: List[int], event_param: List[int], last_evt: int = None) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
event_state=event_state,
event_achievement_flag=event_flags,
event_achievement_param=event_param,
last_play_event_id=last_evt if last_evt is not None else profile.c.last_play_event_id,
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"Failed to update pokken profile event state for user {user_id}!"
)
def add_profile_points( def add_profile_points(
self, user_id: int, rank_pts: int, money: int, score_pts: int self, user_id: int, rank_pts: int, money: int, score_pts: int, grade_max: int
) -> None: ) -> None:
pass sql = update(profile).where(profile.c.user == user_id).values(
trainer_rank_point = profile.c.trainer_rank_point + rank_pts,
fight_money = profile.c.fight_money + money,
score_point = profile.c.score_point + score_pts,
grade_max_num = grade_max
)
def get_profile(self, user_id: int) -> Optional[Row]: def get_profile(self, user_id: int) -> Optional[Row]:
sql = profile.select(profile.c.user == user_id) sql = profile.select(profile.c.user == user_id)
@@ -174,18 +243,53 @@ class PokkenProfileData(BaseData):
return None return None
return result.fetchone() return result.fetchone()
def put_pokemon_data( def put_pokemon(
self, self,
user_id: int, user_id: int,
pokemon_id: int, pokemon_id: int,
illust_no: int, illust_no: int,
get_exp: int,
atk: int, atk: int,
res: int, res: int,
defe: int, defe: int,
sp: int, sp: int
) -> Optional[int]: ) -> Optional[int]:
pass sql = insert(pokemon_data).values(
user=user_id,
char_id=pokemon_id,
illustration_book_no=illust_no,
bp_point_atk=atk,
bp_point_res=res,
bp_point_defe=defe,
bp_point_sp=sp,
)
conflict = sql.on_duplicate_key_update(
illustration_book_no=illust_no,
bp_point_atk=atk,
bp_point_res=res,
bp_point_defe=defe,
bp_point_sp=sp,
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert pokemon ID {pokemon_id} for user {user_id}")
return None
return result.lastrowid
def add_pokemon_xp(
self,
user_id: int,
pokemon_id: int,
xp: int
) -> None:
sql = update(pokemon_data).where(and_(pokemon_data.c.user==user_id, pokemon_data.c.char_id==pokemon_id)).values(
pokemon_exp=pokemon_data.c.pokemon_exp + xp
)
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to add {xp} XP to pokemon ID {pokemon_id} for user {user_id}")
def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]: def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]:
pass pass
@@ -193,13 +297,29 @@ class PokkenProfileData(BaseData):
def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]: def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]:
pass pass
def put_results( def put_pokemon_battle_result(
self, user_id: int, pokemon_id: int, match_type: int, match_result: int self, user_id: int, pokemon_id: int, match_type: PokkenConstants.BATTLE_TYPE, match_result: PokkenConstants.BATTLE_RESULT
) -> None: ) -> None:
""" """
Records the match stats (type and win/loss) for the pokemon and profile Records the match stats (type and win/loss) for the pokemon and profile
""" """
pass sql = update(pokemon_data).where(and_(pokemon_data.c.user==user_id, pokemon_data.c.char_id==pokemon_id)).values(
battle_num_tutorial=pokemon_data.c.battle_num_tutorial + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else pokemon_data.c.battle_num_tutorial,
battle_all_num_tutorial=pokemon_data.c.battle_all_num_tutorial + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else pokemon_data.c.battle_all_num_tutorial,
battle_num_vs_cpu=pokemon_data.c.battle_num_vs_cpu + 1 if match_type==PokkenConstants.BATTLE_TYPE.AI else pokemon_data.c.battle_num_vs_cpu,
win_cpu=pokemon_data.c.win_cpu + 1 if match_type==PokkenConstants.BATTLE_TYPE.AI and match_result==PokkenConstants.BATTLE_RESULT.WIN else pokemon_data.c.win_cpu,
battle_num_vs_lan=pokemon_data.c.battle_num_vs_lan + 1 if match_type==PokkenConstants.BATTLE_TYPE.LAN else pokemon_data.c.battle_num_vs_lan,
win_vs_lan=pokemon_data.c.win_vs_lan + 1 if match_type==PokkenConstants.BATTLE_TYPE.LAN and match_result==PokkenConstants.BATTLE_RESULT.WIN else pokemon_data.c.win_vs_lan,
battle_num_vs_wan=pokemon_data.c.battle_num_vs_wan + 1 if match_type==PokkenConstants.BATTLE_TYPE.WAN else pokemon_data.c.battle_num_vs_wan,
win_vs_wan=pokemon_data.c.win_vs_wan + 1 if match_type==PokkenConstants.BATTLE_TYPE.WAN and match_result==PokkenConstants.BATTLE_RESULT.WIN else pokemon_data.c.win_vs_wan,
)
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to record match stats for user {user_id}'s pokemon {pokemon_id} (type {match_type.name} | result {match_result.name})")
def put_stats( def put_stats(
self, self,
@@ -214,4 +334,29 @@ class PokkenProfileData(BaseData):
""" """
Records profile stats Records profile stats
""" """
pass sql = update(profile).where(profile.c.user==user_id).values(
ex_ko_num=profile.c.ex_ko_num + exkos,
wko_num=profile.c.wko_num + wkos,
timeup_win_num=profile.c.timeup_win_num + timeout_wins,
cool_ko_num=profile.c.cool_ko_num + cool_kos,
perfect_ko_num=profile.c.perfect_ko_num + perfects,
continue_num=continues,
)
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to update stats for user {user_id}")
def update_support_team(self, user_id: int, support_id: int, support1: int = 4294967295, support2: int = 4294967295) -> None:
sql = update(profile).where(profile.c.user==user_id).values(
support_set_1_1=support1 if support_id == 1 else profile.c.support_set_1_1,
support_set_1_2=support2 if support_id == 1 else profile.c.support_set_1_2,
support_set_2_1=support1 if support_id == 2 else profile.c.support_set_2_1,
support_set_2_2=support2 if support_id == 2 else profile.c.support_set_2_2,
support_set_3_1=support1 if support_id == 3 else profile.c.support_set_3_1,
support_set_3_2=support2 if support_id == 3 else profile.c.support_set_3_2,
)
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to update support team {support_id} for user {user_id}")

66
titles/pokken/services.py Normal file
View File

@@ -0,0 +1,66 @@
from twisted.internet.interfaces import IAddress
from twisted.internet.protocol import Protocol
from autobahn.twisted.websocket import WebSocketServerProtocol, WebSocketServerFactory
from autobahn.websocket.types import ConnectionRequest
from typing import Dict
import logging
import json
from core.config import CoreConfig
from .config import PokkenConfig
from .base import PokkenBase
class PokkenAdmissionProtocol(WebSocketServerProtocol):
def __init__(self, cfg: CoreConfig, game_cfg: PokkenConfig):
super().__init__()
self.core_config = cfg
self.game_config = game_cfg
self.logger = logging.getLogger("pokken")
self.base = PokkenBase(cfg, game_cfg)
def onConnect(self, request: ConnectionRequest) -> None:
self.logger.debug(f"Admission: Connection from {request.peer}")
def onClose(self, wasClean: bool, code: int, reason: str) -> None:
self.logger.debug(f"Admission: Connection with {self.transport.getPeer().host} closed {'cleanly ' if wasClean else ''}with code {code} - {reason}")
def onMessage(self, payload, isBinary: bool) -> None:
msg: Dict = json.loads(payload)
self.logger.debug(f"Admission: Message from {self.transport.getPeer().host}:{self.transport.getPeer().port} - {msg}")
api = msg.get("api", "noop")
handler = getattr(self.base, f"handle_admission_{api.lower()}")
resp = handler(msg, self.transport.getPeer().host)
if resp is None:
resp = {}
if "type" not in resp:
resp['type'] = "res"
if "data" not in resp:
resp['data'] = {}
if "api" not in resp:
resp['api'] = api
if "result" not in resp:
resp['result'] = 'true'
self.logger.debug(f"Websocket response: {resp}")
self.sendMessage(json.dumps(resp).encode(), isBinary)
class PokkenAdmissionFactory(WebSocketServerFactory):
protocol = PokkenAdmissionProtocol
def __init__(
self,
cfg: CoreConfig,
game_cfg: PokkenConfig
) -> None:
self.core_config = cfg
self.game_config = game_cfg
super().__init__(f"ws://{self.game_config.server.hostname}:{self.game_config.ports.admission}")
def buildProtocol(self, addr: IAddress) -> Protocol:
p = self.protocol(self.core_config, self.game_config)
p.factory = self
return p

10
titles/sao/__init__.py Normal file
View File

@@ -0,0 +1,10 @@
from .index import SaoServlet
from .const import SaoConstants
from .database import SaoData
from .read import SaoReader
index = SaoServlet
database = SaoData
reader = SaoReader
game_codes = [SaoConstants.GAME_CODE]
current_schema_version = 1

1244
titles/sao/base.py Normal file

File diff suppressed because it is too large Load Diff

47
titles/sao/config.py Normal file
View File

@@ -0,0 +1,47 @@
from core.config import CoreConfig
class SaoServerConfig:
def __init__(self, parent_config: "SaoConfig"):
self.__config = parent_config
@property
def hostname(self) -> str:
return CoreConfig.get_config_field(
self.__config, "sao", "server", "hostname", default="localhost"
)
@property
def enable(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "sao", "server", "enable", default=True
)
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "sao", "server", "loglevel", default="info"
)
)
@property
def port(self) -> int:
return CoreConfig.get_config_field(
self.__config, "sao", "server", "port", default=9000
)
@property
def auto_register(self) -> bool:
"""
Automatically register users in `aime_user` on first carding in with sao
if they don't exist already. Set to false to display an error instead.
"""
return CoreConfig.get_config_field(
self.__config, "sao", "server", "auto_register", default=True
)
class SaoConfig(dict):
def __init__(self) -> None:
self.server = SaoServerConfig(self)

15
titles/sao/const.py Normal file
View File

@@ -0,0 +1,15 @@
from enum import Enum
class SaoConstants:
GAME_CODE = "SDEW"
CONFIG_NAME = "sao.yaml"
VER_SAO = 0
VERSION_NAMES = ("Sword Art Online Arcade")
@classmethod
def game_ver_to_string(cls, ver: int):
return cls.VERSION_NAMES[ver]

View File

@@ -0,0 +1,121 @@
EquipmentLevelId,RequireExp
1,200,
2,400,
3,600,
4,800,
5,1000,
6,1200,
7,1400,
8,1600,
9,1800,
10,2000,
11,2200,
12,2400,
13,2600,
14,2800,
15,3000,
16,3200,
17,3400,
18,3600,
19,3800,
20,4000,
21,4200,
22,4400,
23,4600,
24,4800,
25,5000,
26,5200,
27,5400,
28,5600,
29,5800,
30,6000,
31,6200,
32,6400,
33,6600,
34,6800,
35,7000,
36,7200,
37,7400,
38,7600,
39,7800,
40,8000,
41,8200,
42,8400,
43,8600,
44,8800,
45,9000,
46,9200,
47,9400,
48,9600,
49,9800,
50,10000,
51,10200,
52,10400,
53,10600,
54,10800,
55,11000,
56,11200,
57,11400,
58,11600,
59,11800,
60,12000,
61,12200,
62,12400,
63,12600,
64,12800,
65,13000,
66,13200,
67,13400,
68,13600,
69,13800,
70,14000,
71,14200,
72,14400,
73,14600,
74,14800,
75,15000,
76,15200,
77,15400,
78,15600,
79,15800,
80,16000,
81,16200,
82,16400,
83,16600,
84,16800,
85,17000,
86,17200,
87,17400,
88,17600,
89,17800,
90,18000,
91,18200,
92,18400,
93,18600,
94,18800,
95,19000,
96,19200,
97,19400,
98,19600,
99,19800,
100,100000,
101,150000,
102,200000,
103,250000,
104,300000,
105,350000,
106,400000,
107,450000,
108,500000,
109,550000,
110,600000,
111,650000,
112,700000,
113,750000,
114,800000,
115,850000,
116,900000,
117,950000,
118,1000000,
119,1000000,
120,1000000,
1 EquipmentLevelId,RequireExp
2 1,200,
3 2,400,
4 3,600,
5 4,800,
6 5,1000,
7 6,1200,
8 7,1400,
9 8,1600,
10 9,1800,
11 10,2000,
12 11,2200,
13 12,2400,
14 13,2600,
15 14,2800,
16 15,3000,
17 16,3200,
18 17,3400,
19 18,3600,
20 19,3800,
21 20,4000,
22 21,4200,
23 22,4400,
24 23,4600,
25 24,4800,
26 25,5000,
27 26,5200,
28 27,5400,
29 28,5600,
30 29,5800,
31 30,6000,
32 31,6200,
33 32,6400,
34 33,6600,
35 34,6800,
36 35,7000,
37 36,7200,
38 37,7400,
39 38,7600,
40 39,7800,
41 40,8000,
42 41,8200,
43 42,8400,
44 43,8600,
45 44,8800,
46 45,9000,
47 46,9200,
48 47,9400,
49 48,9600,
50 49,9800,
51 50,10000,
52 51,10200,
53 52,10400,
54 53,10600,
55 54,10800,
56 55,11000,
57 56,11200,
58 57,11400,
59 58,11600,
60 59,11800,
61 60,12000,
62 61,12200,
63 62,12400,
64 63,12600,
65 64,12800,
66 65,13000,
67 66,13200,
68 67,13400,
69 68,13600,
70 69,13800,
71 70,14000,
72 71,14200,
73 72,14400,
74 73,14600,
75 74,14800,
76 75,15000,
77 76,15200,
78 77,15400,
79 78,15600,
80 79,15800,
81 80,16000,
82 81,16200,
83 82,16400,
84 83,16600,
85 84,16800,
86 85,17000,
87 86,17200,
88 87,17400,
89 88,17600,
90 89,17800,
91 90,18000,
92 91,18200,
93 92,18400,
94 93,18600,
95 94,18800,
96 95,19000,
97 96,19200,
98 97,19400,
99 98,19600,
100 99,19800,
101 100,100000,
102 101,150000,
103 102,200000,
104 103,250000,
105 104,300000,
106 105,350000,
107 106,400000,
108 107,450000,
109 108,500000,
110 109,550000,
111 110,600000,
112 111,650000,
113 112,700000,
114 113,750000,
115 114,800000,
116 115,850000,
117 116,900000,
118 117,950000,
119 118,1000000,
120 119,1000000,
121 120,1000000,

View File

@@ -0,0 +1,121 @@
HeroLogLevelId,RequireExp
1,0,
2,1000,
3,1200,
4,1400,
5,1600,
6,1900,
7,2200,
8,2500,
9,2800,
10,3100,
11,3500,
12,3900,
13,4300,
14,4700,
15,5100,
16,5550,
17,6000,
18,6450,
19,6900,
20,7350,
21,7850,
22,8350,
23,8850,
24,9350,
25,9850,
26,10450,
27,11050,
28,11650,
29,12250,
30,12850,
31,13550,
32,14250,
33,14950,
34,15650,
35,16350,
36,17150,
37,17950,
38,18750,
39,19550,
40,20350,
41,21250,
42,22150,
43,23050,
44,23950,
45,24850,
46,25850,
47,26850,
48,27850,
49,28850,
50,29850,
51,30950,
52,32050,
53,33150,
54,34250,
55,35350,
56,36550,
57,37750,
58,38950,
59,40150,
60,41350,
61,42650,
62,43950,
63,45250,
64,46550,
65,47850,
66,49250,
67,50650,
68,52050,
69,53450,
70,54850,
71,56350,
72,57850,
73,59350,
74,60850,
75,62350,
76,63950,
77,65550,
78,67150,
79,68750,
80,70350,
81,72050,
82,73750,
83,75450,
84,77150,
85,78850,
86,80650,
87,82450,
88,84250,
89,86050,
90,87850,
91,89750,
92,91650,
93,93550,
94,95450,
95,97350,
96,99350,
97,101350,
98,103350,
99,105350,
100,107350,
101,200000,
102,300000,
103,450000,
104,600000,
105,800000,
106,1000000,
107,1250000,
108,1500000,
109,1800000,
110,2100000,
111,2100000,
112,2100000,
113,2100000,
114,2100000,
115,2100000,
116,2100000,
117,2100000,
118,2100000,
119,2100000,
120,2100000,
1 HeroLogLevelId,RequireExp
2 1,0,
3 2,1000,
4 3,1200,
5 4,1400,
6 5,1600,
7 6,1900,
8 7,2200,
9 8,2500,
10 9,2800,
11 10,3100,
12 11,3500,
13 12,3900,
14 13,4300,
15 14,4700,
16 15,5100,
17 16,5550,
18 17,6000,
19 18,6450,
20 19,6900,
21 20,7350,
22 21,7850,
23 22,8350,
24 23,8850,
25 24,9350,
26 25,9850,
27 26,10450,
28 27,11050,
29 28,11650,
30 29,12250,
31 30,12850,
32 31,13550,
33 32,14250,
34 33,14950,
35 34,15650,
36 35,16350,
37 36,17150,
38 37,17950,
39 38,18750,
40 39,19550,
41 40,20350,
42 41,21250,
43 42,22150,
44 43,23050,
45 44,23950,
46 45,24850,
47 46,25850,
48 47,26850,
49 48,27850,
50 49,28850,
51 50,29850,
52 51,30950,
53 52,32050,
54 53,33150,
55 54,34250,
56 55,35350,
57 56,36550,
58 57,37750,
59 58,38950,
60 59,40150,
61 60,41350,
62 61,42650,
63 62,43950,
64 63,45250,
65 64,46550,
66 65,47850,
67 66,49250,
68 67,50650,
69 68,52050,
70 69,53450,
71 70,54850,
72 71,56350,
73 72,57850,
74 73,59350,
75 74,60850,
76 75,62350,
77 76,63950,
78 77,65550,
79 78,67150,
80 79,68750,
81 80,70350,
82 81,72050,
83 82,73750,
84 83,75450,
85 84,77150,
86 85,78850,
87 86,80650,
88 87,82450,
89 88,84250,
90 89,86050,
91 90,87850,
92 91,89750,
93 92,91650,
94 93,93550,
95 94,95450,
96 95,97350,
97 96,99350,
98 97,101350,
99 98,103350,
100 99,105350,
101 100,107350,
102 101,200000,
103 102,300000,
104 103,450000,
105 104,600000,
106 105,800000,
107 106,1000000,
108 107,1250000,
109 108,1500000,
110 109,1800000,
111 110,2100000,
112 111,2100000,
113 112,2100000,
114 113,2100000,
115 114,2100000,
116 115,2100000,
117 116,2100000,
118 117,2100000,
119 118,2100000,
120 119,2100000,
121 120,2100000,

View File

@@ -0,0 +1,301 @@
PlayerRankId,TotalExp,
1,0,
2,100,
3,300,
4,500,
5,800,
6,1100,
7,1400,
8,1800,
9,2200,
10,2600,,
11,3000,,
12,3500,,
13,4000,,
14,4500,,
15,5000,,
16,5500,,
17,6100,,
18,6700,,
19,7300,,
20,7900,,
21,8500,,
22,9100,,
23,9800,,
24,10500,
25,11200,
26,11900,
27,12600,
28,13300,
29,14000,
30,14800,
31,15600,
32,16400,
33,17200,
34,18000,
35,18800,
36,19600,
37,20400,
38,21300,
39,22200,
40,23100,
41,24000,
42,24900,
43,25800,
44,26700,
45,27600,
46,28500,
47,29500,
48,30600,
49,31800,
50,33100,
51,34500,
52,36000,
53,37600,
54,39300,
55,41100,
56,43000,
57,45000,
58,47100,
59,49300,
60,51600,
61,54000,
62,56500,
63,59100,
64,61800,
65,64600,
66,67500,
67,70500,
68,73600,
69,76800,
70,80100,
71,83500,
72,87000,
73,90600,
74,94300,
75,98100,
76,102000,
77,106000,
78,110100,
79,114300,
80,118600,
81,123000,
82,127500,
83,132100,
84,136800,
85,141600,
86,146500,
87,151500,
88,156600,
89,161800,
90,167100,
91,172500,
92,178000,
93,183600,
94,189300,
95,195100,
96,201000,
97,207000,
98,213100,
99,219300,
100,225600,
101,232000,
102,238500,
103,245100,
104,251800,
105,258600,
106,265500,
107,272500,
108,279600,
109,286800,
110,294100,
111,301500,
112,309000,
113,316600,
114,324300,
115,332100,
116,340000,
117,348000,
118,356100,
119,364300,
120,372600,
121,381000,
122,389500,
123,398100,
124,406800,
125,415600,
126,424500,
127,433500,
128,442600,
129,451800,
130,461100,
131,470500,
132,480000,
133,489600,
134,499300,
135,509100,
136,519000,
137,529000,
138,539100,
139,549300,
140,559600,
141,570000,
142,580500,
143,591100,
144,601800,
145,612600,
146,623500,
147,634500,
148,645600,
149,656800,
150,668100,
151,679500,
152,691000,
153,702600,
154,714300,
155,726100,
156,738000,
157,750000,
158,762100,
159,774300,
160,786600,
161,799000,
162,811500,
163,824100,
164,836800,
165,849600,
166,862500,
167,875500,
168,888600,
169,901800,
170,915100,
171,928500,
172,942000,
173,955600,
174,969300,
175,983100,
176,997000,
177,1011000,
178,1025100,
179,1039300,
180,1053600,
181,1068000,
182,1082500,
183,1097100,
184,1111800,
185,1126600,
186,1141500,
187,1156500,
188,1171600,
189,1186800,
190,1202100,
191,1217500,
192,1233000,
193,1248600,
194,1264300,
195,1280100,
196,1296000,
197,1312000,
198,1328100,
199,1344300,
200,1360600,
201,1377000,
202,1393500,
203,1410100,
204,1426800,
205,1443600,
206,1460500,
207,1477500,
208,1494600,
209,1511800,
210,1529100,
211,1546500,
212,1564000,
213,1581600,
214,1599300,
215,1617100,
216,1635000,
217,1653000,
218,1671100,
219,1689300,
220,1707600,
221,1726000,
222,1744500,
223,1763100,
224,1781800,
225,1800600,
226,1819500,
227,1838500,
228,1857600,
229,1876800,
230,1896100,
231,1915500,
232,1935000,
233,1954600,
234,1974300,
235,1994100,
236,2014000,
237,2034000,
238,2054100,
239,2074300,
240,2094600,
241,2115000,
242,2135500,
243,2156100,
244,2176800,
245,2197600,
246,2218500,
247,2239500,
248,2260600,
249,2281800,
250,2303100,
251,2324500,
252,2346000,
253,2367600,
254,2389300,
255,2411100,
256,2433000,
257,2455000,
258,2477100,
259,2499300,
260,2521600,
261,2544000,
262,2566500,
263,2589100,
264,2611800,
265,2634600,
266,2657500,
267,2680500,
268,2703600,
269,2726800,
270,2750100,
271,2773500,
272,2797000,
273,2820600,
274,2844300,
275,2868100,
276,2892000,
277,2916000,
278,2940100,
279,2964300,
280,2988600,
281,3013000,
282,3037500,
283,3062100,
284,3086800,
285,3111600,
286,3136500,
287,3161500,
288,3186600,
289,3211800,
290,3237100,
291,3262500,
292,3288000,
293,3313600,
294,3339300,
295,3365100,
296,3391000,
297,3417000,
298,3443100,
299,3469300,
300,3495600,
1 PlayerRankId TotalExp
2 1 0
3 2 100
4 3 300
5 4 500
6 5 800
7 6 1100
8 7 1400
9 8 1800
10 9 2200
11 10 2600
12 11 3000
13 12 3500
14 13 4000
15 14 4500
16 15 5000
17 16 5500
18 17 6100
19 18 6700
20 19 7300
21 20 7900
22 21 8500
23 22 9100
24 23 9800
25 24 10500
26 25 11200
27 26 11900
28 27 12600
29 28 13300
30 29 14000
31 30 14800
32 31 15600
33 32 16400
34 33 17200
35 34 18000
36 35 18800
37 36 19600
38 37 20400
39 38 21300
40 39 22200
41 40 23100
42 41 24000
43 42 24900
44 43 25800
45 44 26700
46 45 27600
47 46 28500
48 47 29500
49 48 30600
50 49 31800
51 50 33100
52 51 34500
53 52 36000
54 53 37600
55 54 39300
56 55 41100
57 56 43000
58 57 45000
59 58 47100
60 59 49300
61 60 51600
62 61 54000
63 62 56500
64 63 59100
65 64 61800
66 65 64600
67 66 67500
68 67 70500
69 68 73600
70 69 76800
71 70 80100
72 71 83500
73 72 87000
74 73 90600
75 74 94300
76 75 98100
77 76 102000
78 77 106000
79 78 110100
80 79 114300
81 80 118600
82 81 123000
83 82 127500
84 83 132100
85 84 136800
86 85 141600
87 86 146500
88 87 151500
89 88 156600
90 89 161800
91 90 167100
92 91 172500
93 92 178000
94 93 183600
95 94 189300
96 95 195100
97 96 201000
98 97 207000
99 98 213100
100 99 219300
101 100 225600
102 101 232000
103 102 238500
104 103 245100
105 104 251800
106 105 258600
107 106 265500
108 107 272500
109 108 279600
110 109 286800
111 110 294100
112 111 301500
113 112 309000
114 113 316600
115 114 324300
116 115 332100
117 116 340000
118 117 348000
119 118 356100
120 119 364300
121 120 372600
122 121 381000
123 122 389500
124 123 398100
125 124 406800
126 125 415600
127 126 424500
128 127 433500
129 128 442600
130 129 451800
131 130 461100
132 131 470500
133 132 480000
134 133 489600
135 134 499300
136 135 509100
137 136 519000
138 137 529000
139 138 539100
140 139 549300
141 140 559600
142 141 570000
143 142 580500
144 143 591100
145 144 601800
146 145 612600
147 146 623500
148 147 634500
149 148 645600
150 149 656800
151 150 668100
152 151 679500
153 152 691000
154 153 702600
155 154 714300
156 155 726100
157 156 738000
158 157 750000
159 158 762100
160 159 774300
161 160 786600
162 161 799000
163 162 811500
164 163 824100
165 164 836800
166 165 849600
167 166 862500
168 167 875500
169 168 888600
170 169 901800
171 170 915100
172 171 928500
173 172 942000
174 173 955600
175 174 969300
176 175 983100
177 176 997000
178 177 1011000
179 178 1025100
180 179 1039300
181 180 1053600
182 181 1068000
183 182 1082500
184 183 1097100
185 184 1111800
186 185 1126600
187 186 1141500
188 187 1156500
189 188 1171600
190 189 1186800
191 190 1202100
192 191 1217500
193 192 1233000
194 193 1248600
195 194 1264300
196 195 1280100
197 196 1296000
198 197 1312000
199 198 1328100
200 199 1344300
201 200 1360600
202 201 1377000
203 202 1393500
204 203 1410100
205 204 1426800
206 205 1443600
207 206 1460500
208 207 1477500
209 208 1494600
210 209 1511800
211 210 1529100
212 211 1546500
213 212 1564000
214 213 1581600
215 214 1599300
216 215 1617100
217 216 1635000
218 217 1653000
219 218 1671100
220 219 1689300
221 220 1707600
222 221 1726000
223 222 1744500
224 223 1763100
225 224 1781800
226 225 1800600
227 226 1819500
228 227 1838500
229 228 1857600
230 229 1876800
231 230 1896100
232 231 1915500
233 232 1935000
234 233 1954600
235 234 1974300
236 235 1994100
237 236 2014000
238 237 2034000
239 238 2054100
240 239 2074300
241 240 2094600
242 241 2115000
243 242 2135500
244 243 2156100
245 244 2176800
246 245 2197600
247 246 2218500
248 247 2239500
249 248 2260600
250 249 2281800
251 250 2303100
252 251 2324500
253 252 2346000
254 253 2367600
255 254 2389300
256 255 2411100
257 256 2433000
258 257 2455000
259 258 2477100
260 259 2499300
261 260 2521600
262 261 2544000
263 262 2566500
264 263 2589100
265 264 2611800
266 265 2634600
267 266 2657500
268 267 2680500
269 268 2703600
270 269 2726800
271 270 2750100
272 271 2773500
273 272 2797000
274 273 2820600
275 274 2844300
276 275 2868100
277 276 2892000
278 277 2916000
279 278 2940100
280 279 2964300
281 280 2988600
282 281 3013000
283 282 3037500
284 283 3062100
285 284 3086800
286 285 3111600
287 286 3136500
288 287 3161500
289 288 3186600
290 289 3211800
291 290 3237100
292 291 3262500
293 292 3288000
294 293 3313600
295 294 3339300
296 295 3365100
297 296 3391000
298 297 3417000
299 298 3443100
300 299 3469300
301 300 3495600

File diff suppressed because it is too large Load Diff

13
titles/sao/database.py Normal file
View File

@@ -0,0 +1,13 @@
from core.data import Data
from core.config import CoreConfig
from .schema import *
class SaoData(Data):
def __init__(self, cfg: CoreConfig) -> None:
super().__init__(cfg)
self.item = SaoItemData(cfg, self.session)
self.profile = SaoProfileData(cfg, self.session)
self.static = SaoStaticData(cfg, self.session)

View File

@@ -0,0 +1 @@
from titles.sao.handlers.base import *

2747
titles/sao/handlers/base.py Normal file

File diff suppressed because it is too large Load Diff

117
titles/sao/index.py Normal file
View File

@@ -0,0 +1,117 @@
from typing import Tuple
from twisted.web.http import Request
from twisted.web import resource
import json, ast
from datetime import datetime
import yaml
import logging, coloredlogs
from logging.handlers import TimedRotatingFileHandler
import inflection
from os import path
from core import CoreConfig, Utils
from titles.sao.config import SaoConfig
from titles.sao.const import SaoConstants
from titles.sao.base import SaoBase
from titles.sao.handlers.base import *
class SaoServlet(resource.Resource):
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
self.isLeaf = True
self.core_cfg = core_cfg
self.config_dir = cfg_dir
self.game_cfg = SaoConfig()
if path.exists(f"{cfg_dir}/sao.yaml"):
self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/sao.yaml")))
self.logger = logging.getLogger("sao")
if not hasattr(self.logger, "inited"):
log_fmt_str = "[%(asctime)s] SAO | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.core_cfg.server.log_dir, "sao"),
encoding="utf8",
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler)
self.logger.setLevel(self.game_cfg.server.loglevel)
coloredlogs.install(
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
)
self.logger.inited = True
self.base = SaoBase(core_cfg, self.game_cfg)
@classmethod
def get_allnet_info(
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
) -> Tuple[bool, str, str]:
game_cfg = SaoConfig()
if path.exists(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}"):
game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}"))
)
if not game_cfg.server.enable:
return (False, "", "")
return (
True,
f"http://{game_cfg.server.hostname}:{game_cfg.server.port}/{game_code}/$v/",
f"{game_cfg.server.hostname}/SDEW/$v/",
)
@classmethod
def get_mucha_info(
cls, core_cfg: CoreConfig, cfg_dir: str
) -> Tuple[bool, str, str]:
game_cfg = SaoConfig()
if path.exists(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}"):
game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}"))
)
if not game_cfg.server.enable:
return (False, "")
return (True, "SAO1")
def setup(self) -> None:
pass
def render_POST(
self, request: Request, version: int = 0, endpoints: str = ""
) -> bytes:
req_url = request.uri.decode()
if req_url == "/matching":
self.logger.info("Matching request")
request.responseHeaders.addRawHeader(b"content-type", b"text/html; charset=utf-8")
sao_request = request.content.getvalue().hex()
handler = getattr(self.base, f"handle_{sao_request[:4]}", None)
if handler is None:
self.logger.info(f"Generic Handler for {req_url} - {sao_request[:4]}")
self.logger.debug(f"Request: {request.content.getvalue().hex()}")
resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(sao_request[:4]), "big")+1)
self.logger.debug(f"Response: {resp.make().hex()}")
return resp.make()
self.logger.info(f"Handler {req_url} - {sao_request[:4]} request")
self.logger.debug(f"Request: {request.content.getvalue().hex()}")
resp = handler(sao_request)
self.logger.debug(f"Response: {resp.hex()}")
return resp

Some files were not shown because too many files have changed in this diff Show More