145 Commits

Author SHA1 Message Date
Dniel97
c51103aaf5 HUGE WebUI overhaul
- Removed ugly styles in header
- Properly implemented Bootstrap's NavBar
- Added username to session
2023-09-09 00:36:22 +02:00
Dniel97
08927db100 Merge branch 'develop' into fork_develop 2023-09-07 15:09:36 +02:00
Kevin Trocolli
238e39f415 adb: properly handle an incorrect adb status value 2023-09-06 22:47:37 -04:00
Hay1tsme
5499d38bb4 mucha: streamline mucha_postprocess (thanks Bottersnike!) 2023-09-05 17:32:51 -04:00
Kevin Trocolli
3a6cfedcca maimai: fix net deliver paths 2023-08-31 01:24:41 -04:00
Hay1tsme
7a6272dcc5 Merge branch 'develop' of https://gitea.tendokyu.moe/Hay1tsme/artemis into develop 2023-08-30 11:19:14 -04:00
Hay1tsme
136e47d1e6 reader: series -> game 2023-08-30 11:19:13 -04:00
Kevin Trocolli
dac655b4ae adb: add catch for uninitialized amlib requests 2023-08-27 19:18:22 -04:00
Kevin Trocolli
37e2da2051 maimai: fix usbdl endpoints 2023-08-27 11:53:29 -04:00
Dniel97
9d74d60c14 Merge remote-tracking branch 'origin/develop' into fork_develop 2023-08-21 10:33:25 +02:00
Kevin Trocolli
d4ea3bc12a billing: float5 hotfix 2023-08-21 01:53:27 -04:00
Kevin Trocolli
d8b0e2ea2a billing: add classes and validation, fix response 2023-08-21 01:50:59 -04:00
Kevin Trocolli
984949d902 allnet: partial DFI implementation 2023-08-21 00:10:25 -04:00
Kevin Trocolli
2e8d99e5fa allnet: add ip check config option 2023-08-20 23:25:50 -04:00
Kevin Trocolli
fd6cadf2da adb: hotfix 2023-08-20 19:56:16 -04:00
Kevin Trocolli
71489c1272 adb: fix log_ex 2023-08-20 19:55:26 -04:00
Dniel97
3773c57de1 Merge remote-tracking branch 'origin/develop' into fork_develop 2023-08-20 14:22:58 +02:00
Kevin Trocolli
8ea82ffe1a adb: add from_req 2023-08-19 01:35:37 -04:00
Kevin Trocolli
904ea10920 cm: remove print, fix default config 2023-08-19 01:35:15 -04:00
Dniel97
8a8c0e023e Merge branch 'develop' into fork_develop 2023-08-16 11:02:22 +02:00
Kevin Trocolli
cf7cc0997a mucha: add other request/response structures 2023-08-15 23:19:48 -04:00
Hay1tsme
92567504f4 adb: fix for felica_lookup_ex, for #32 2023-08-15 10:43:49 -04:00
Hay1tsme
fd50a7ee68 aimedb_redux (#30)
Update AimeDB from new [documentation](https://minori.tendokyu.moe/docs/allnet/aimedb/) of the protocol.
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/30
2023-08-14 03:32:03 +00:00
Hay1tsme
9e3a51a57a allnet: add IP checking 2023-08-08 12:35:38 -04:00
Hay1tsme
4744e8cf5f add ip checking config options 2023-08-08 10:24:28 -04:00
Hay1tsme
88a1462304 logger: change from warn to warning 2023-08-08 10:17:56 -04:00
Hay1tsme
2e277e7791 add arcade ip column 2023-08-08 10:17:38 -04:00
Kevin Trocolli
757fdc5c57 mai2: add check for Mai-Encoding headers 2023-08-01 02:34:40 -04:00
Kevin Trocolli
23bcb5cc13 Merge branch 'develop' of https://gitea.tendokyu.moe/Hay1tsme/artemis into develop 2023-07-25 09:18:47 -04:00
Kevin Trocolli
9f0c181593 add handler for /mucha_front in addition to /mucha 2023-07-25 09:18:45 -04:00
Midorica
5a4baba102 Diva: Adding threading to score loading 2023-07-24 13:19:54 -04:00
Kevin Trocolli
156b4e4ede mai2: fix get user music for dx and up 2023-07-24 00:49:08 -04:00
Kevin Trocolli
6c89a97fe3 frontend: add management pages 2023-07-23 22:21:49 -04:00
Kevin Trocolli
b943807904 core: add columns to machine table, bump to v5 2023-07-23 22:21:41 -04:00
Kevin Trocolli
f417be671b pokken: fix typo 2023-07-23 12:47:37 -04:00
Kevin Trocolli
20335aaebe add download report api 2023-07-23 12:47:10 -04:00
Dniel97
097181008b Merge remote-tracking branch 'origin/develop' into fork_develop 2023-07-16 23:41:38 +02:00
Kevin Trocolli
63d81a2704 changelog: fix typos 2023-07-16 17:24:07 -04:00
Kevin Trocolli
718229b267 Update changelog 2023-07-16 17:21:45 -04:00
Kevin Trocolli
7c78975431 mai2: update example config 2023-07-16 17:00:52 -04:00
Kevin Trocolli
14a315a673 replace except with except Exception 2023-07-16 16:58:34 -04:00
Kevin Trocolli
343fe4357c mai2: add image validation via Pillow 2023-07-16 16:58:18 -04:00
Kevin Trocolli
d0e43140ba mai2: fix ghost saving, add memorial photo upload 2023-07-16 16:06:34 -04:00
Dniel97
859bf4bf5d Merge remote-tracking branch 'origin/develop' into fork_develop 2023-07-16 19:07:56 +02:00
Midorica
c6e7100f51 Merge pull request 'O.N.G.E.K.I.: Card Maker fixes, improvements and bug fixes' (#25) from Dniel97/artemis:cardmaker_ongeki_fix into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/25
2023-07-16 17:06:44 +00:00
Midorica
b0ca37815b Merge pull request 'maimai DX: Fixes' (#27) from Dniel97/artemis:mai2_fixes into develop
Reviewed-on: https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/27
2023-07-16 17:00:32 +00:00
Dniel97
f39317301b mai2: fixed update script, added mai2 heredity, fixed cards import 2023-07-15 22:51:54 +02:00
Dniel97
389784ce82 Merge remote-tracking branch 'origin/develop' into fork_develop 2023-07-15 22:47:05 +02:00
Kevin Trocolli
2f13596885 fix db ignoring port in config, createing database no longer runs over version 2023-07-15 00:15:14 -04:00
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
Kevin Trocolli
85b73e634d mucha: add DownloadState 2023-07-12 00:41:53 -04:00
Midorica
09c4f8cda4 Async request to CXB profile loading 2023-07-08 18:44:02 -04: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
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
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
7bb8c2c80c billing: handle malformed requests 2023-05-03 03:26:39 -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
deeac1d8db add finale handler, pre-dx game codes 2023-04-30 22:19:31 -04:00
121 changed files with 24969 additions and 1449 deletions

View File

@@ -1,6 +1,56 @@
# Changelog # Changelog
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
## 20230716
### General
+ Docker files added (#19)
+ Added support for threading
+ This comes with the caviat that enabling it will not allow you to use Ctrl + C to stop the server.
### Webui
+ Small improvements
+ Add card display
### Allnet
+ Billing format validation
+ Fix naomitest.html endpoint
+ Add event logging for auths and billing
+ LoaderStateRecorder endpoint handler added
### Mucha
+ Fixed log level always being "Info"
+ Add stub handler for DownloadState
### Sword Art Online
+ Support added
### Crossbeats
+ Added threading to profile loading
+ This should cause a noticeable speed-up
### Card Maker
+ DX Passes fixed
+ Various improvements
### Diva
+ Added clear status calculation
+ Various minor fixes and improvements
### Maimai
+ Added support for memorial photo uploads
+ Added support for the following versions
+ Festival
+ FiNALE
+ Various bug fixes and improvements
### Wacca
+ Fixed an error that sometimes occoured when trying to unlock songs (#22)
### Pokken
+ Profile saving added (loading TBA)
+ Use external STUN server for matching by default
+ Matching still not working
## 2023042300 ## 2023042300
### Wacca ### Wacca
+ Time free now works properly + Time free now works properly

View File

@@ -0,0 +1,6 @@
from .base import ADBBaseRequest, ADBBaseResponse, ADBHeader, ADBHeaderException, PortalRegStatus, LogStatus, ADBStatus
from .base import CompanyCodes, ReaderFwVer, CMD_CODE_GOODBYE, HEADER_SIZE
from .lookup import ADBLookupRequest, ADBLookupResponse, ADBLookupExResponse
from .campaign import ADBCampaignClearRequest, ADBCampaignClearResponse, ADBCampaignResponse, ADBOldCampaignRequest, ADBOldCampaignResponse
from .felica import ADBFelicaLookupRequest, ADBFelicaLookupResponse, ADBFelicaLookup2Request, ADBFelicaLookup2Response
from .log import ADBLogExRequest, ADBLogRequest, ADBStatusLogRequest, ADBLogExResponse

170
core/adb_handlers/base.py Normal file
View File

@@ -0,0 +1,170 @@
import struct
from construct import Struct, Int16ul, Int32ul, PaddedString
from enum import Enum
import re
from typing import Union, Final
class LogStatus(Enum):
NONE = 0
START = 1
CONTINUE = 2
END = 3
OTHER = 4
class PortalRegStatus(Enum):
NO_REG = 0
PORTAL = 1
SEGA_ID = 2
class ADBStatus(Enum):
UNKNOWN = 0
GOOD = 1
BAD_AMIE_ID = 2
ALREADY_REG = 3
BAN_SYS_USER = 4
BAN_SYS = 5
BAN_USER = 6
BAN_GEN = 7
LOCK_SYS_USER = 8
LOCK_SYS = 9
LOCK_USER = 10
class CompanyCodes(Enum):
NONE = 0
SEGA = 1
BAMCO = 2
KONAMI = 3
TAITO = 4
class ReaderFwVer(Enum): # Newer readers use a singly byte value
NONE = 0
TN32_10 = 1
TN32_12 = 2
OTHER = 9
def __str__(self) -> str:
if self == self.TN32_10:
return "TN32MSEC003S F/W Ver1.0"
elif self == self.TN32_12:
return "TN32MSEC003S F/W Ver1.2"
elif self == self.NONE:
return "Not Specified"
elif self == self.OTHER:
return "Unknown/Other"
else:
raise ValueError(f"Bad ReaderFwVer value {self.value}")
@classmethod
def from_byte(self, byte: bytes) -> Union["ReaderFwVer", int]:
try:
i = int.from_bytes(byte, 'little')
try:
return ReaderFwVer(i)
except ValueError:
return i
except TypeError:
return 0
class ADBHeaderException(Exception):
pass
HEADER_SIZE: Final[int] = 0x20
CMD_CODE_GOODBYE: Final[int] = 0x66
# everything is LE
class ADBHeader:
def __init__(self, magic: int, protocol_ver: int, cmd: int, length: int, status: int, game_id: Union[str, bytes], store_id: int, keychip_id: Union[str, bytes]) -> None:
self.magic = magic # u16
self.protocol_ver = protocol_ver # u16
self.cmd = cmd # u16
self.length = length # u16
try:
self.status = ADBStatus(status) # u16
except ValueError as e:
raise ADBHeaderException(f"Status is incorrect! {e}")
self.game_id = game_id # 4 char + \x00
self.store_id = store_id # u32
self.keychip_id = keychip_id# 11 char + \x00
if type(self.game_id) == bytes:
self.game_id = self.game_id.decode()
if type(self.keychip_id) == bytes:
self.keychip_id = self.keychip_id.decode()
self.game_id = self.game_id.replace("\0", "")
self.keychip_id = self.keychip_id.replace("\0", "")
if self.cmd != CMD_CODE_GOODBYE: # Games for some reason send no data with goodbye
self.validate()
@classmethod
def from_data(cls, data: bytes) -> "ADBHeader":
magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id = struct.unpack_from("<5H6sI12s", data)
head = cls(magic, protocol_ver, cmd, length, status, game_id, store_id, keychip_id)
if head.length != len(data):
raise ADBHeaderException(f"Length is incorrect! Expect {head.length}, got {len(data)}")
return head
def validate(self) -> bool:
if self.magic != 0xa13e:
raise ADBHeaderException(f"Magic {self.magic} != 0xa13e")
if self.protocol_ver < 0x1000:
raise ADBHeaderException(f"Protocol version {hex(self.protocol_ver)} is invalid!")
if re.fullmatch(r"^S[0-9A-Z]{3}[P]?$", self.game_id) is None:
raise ADBHeaderException(f"Game ID {self.game_id} is invalid!")
if self.store_id == 0:
raise ADBHeaderException(f"Store ID cannot be 0!")
if re.fullmatch(r"^A[0-9]{2}[E|X][0-9]{2}[A-HJ-NP-Z][0-9]{4}$", self.keychip_id) is None:
raise ADBHeaderException(f"Keychip ID {self.keychip_id} is invalid!")
return True
def make(self) -> bytes:
resp_struct = Struct(
"magic" / Int16ul,
"unknown" / Int16ul,
"response_code" / Int16ul,
"length" / Int16ul,
"status" / Int16ul,
"game_id" / PaddedString(6, 'utf_8'),
"store_id" / Int32ul,
"keychip_id" / PaddedString(12, 'utf_8'),
)
return resp_struct.build(dict(
magic=self.magic,
unknown=self.protocol_ver,
response_code=self.cmd,
length=self.length,
status=self.status.value,
game_id = self.game_id,
store_id = self.store_id,
keychip_id = self.keychip_id,
))
class ADBBaseRequest:
def __init__(self, data: bytes) -> None:
self.head = ADBHeader.from_data(data)
class ADBBaseResponse:
def __init__(self, code: int = 0, length: int = 0x20, status: int = 1, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", protocol_ver: int = 0x3087) -> None:
self.head = ADBHeader(0xa13e, protocol_ver, code, length, status, game_id, store_id, keychip_id)
@classmethod
def from_req(cls, req: ADBHeader, cmd: int, length: int = 0x20, status: int = 1) -> "ADBBaseResponse":
return cls(cmd, length, status, req.game_id, req.store_id, req.keychip_id, req.protocol_ver)
def append_padding(self, data: bytes):
"""Appends 0s to the end of the data until it's at the correct size"""
padding_size = self.head.length - len(data)
data += bytes(padding_size)
return data
def make(self) -> bytes:
return self.head.make()

View File

@@ -0,0 +1,132 @@
from construct import Struct, Int16ul, Padding, Bytes, Int32ul, Int32sl
from .base import *
class Campaign:
def __init__(self) -> None:
self.id = 0
self.name = ""
self.announce_date = 0
self.start_date = 0
self.end_date = 0
self.distrib_start_date = 0
self.distrib_end_date = 0
def make(self) -> bytes:
name_padding = bytes(128 - len(self.name))
return Struct(
"id" / Int32ul,
"name" / Bytes(128),
"announce_date" / Int32ul,
"start_date" / Int32ul,
"end_date" / Int32ul,
"distrib_start_date" / Int32ul,
"distrib_end_date" / Int32ul,
Padding(8),
).build(dict(
id = self.id,
name = self.name.encode() + name_padding,
announce_date = self.announce_date,
start_date = self.start_date,
end_date = self.end_date,
distrib_start_date = self.distrib_start_date,
distrib_end_date = self.distrib_end_date,
))
class CampaignClear:
def __init__(self) -> None:
self.id = 0
self.entry_flag = 0
self.clear_flag = 0
def make(self) -> bytes:
return Struct(
"id" / Int32ul,
"entry_flag" / Int32ul,
"clear_flag" / Int32ul,
Padding(4),
).build(dict(
id = self.id,
entry_flag = self.entry_flag,
clear_flag = self.clear_flag,
))
class ADBCampaignResponse(ADBBaseResponse):
def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x0C, length: int = 0x200, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.campaigns = [Campaign(), Campaign(), Campaign()]
@classmethod
def from_req(cls, req: ADBHeader) -> "ADBCampaignResponse":
c = cls(req.game_id, req.store_id, req.keychip_id)
c.head.protocol_ver = req.protocol_ver
return c
def make(self) -> bytes:
body = b""
for c in self.campaigns:
body += c.make()
self.head.length = HEADER_SIZE + len(body)
return self.head.make() + body
class ADBOldCampaignRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
self.campaign_id = struct.unpack_from("<I", data, 0x20)
class ADBOldCampaignResponse(ADBBaseResponse):
def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x0C, length: int = 0x30, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.info0 = 0
self.info1 = 0
self.info2 = 0
self.info3 = 0
@classmethod
def from_req(cls, req: ADBHeader) -> "ADBCampaignResponse":
c = cls(req.game_id, req.store_id, req.keychip_id)
c.head.protocol_ver = req.protocol_ver
return c
def make(self) -> bytes:
resp_struct = Struct(
"info0" / Int32sl,
"info1" / Int32sl,
"info2" / Int32sl,
"info3" / Int32sl,
).build(
info0 = self.info0,
info1 = self.info1,
info2 = self.info2,
info3 = self.info3,
)
self.head.length = HEADER_SIZE + len(resp_struct)
return self.head.make() + resp_struct
class ADBCampaignClearRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
self.aime_id = struct.unpack_from("<i", data, 0x20)
class ADBCampaignClearResponse(ADBBaseResponse):
def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x0E, length: int = 0x50, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.campaign_clear_status = [CampaignClear(), CampaignClear(), CampaignClear()]
@classmethod
def from_req(cls, req: ADBHeader) -> "ADBCampaignResponse":
c = cls(req.game_id, req.store_id, req.keychip_id)
c.head.protocol_ver = req.protocol_ver
return c
def make(self) -> bytes:
body = b""
for c in self.campaign_clear_status:
body += c.make()
self.head.length = HEADER_SIZE + len(body)
return self.head.make() + body

View File

@@ -0,0 +1,84 @@
from construct import Struct, Int32sl, Padding, Int8ub, Int16sl
from typing import Union
from .base import *
class ADBFelicaLookupRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
idm, pmm = struct.unpack_from(">QQ", data, 0x20)
self.idm = hex(idm)[2:].upper()
self.pmm = hex(pmm)[2:].upper()
class ADBFelicaLookupResponse(ADBBaseResponse):
def __init__(self, access_code: str = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x03, length: int = 0x30, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.access_code = access_code if access_code is not None else "00000000000000000000"
@classmethod
def from_req(cls, req: ADBHeader, access_code: str = None) -> "ADBFelicaLookupResponse":
c = cls(access_code, req.game_id, req.store_id, req.keychip_id)
c.head.protocol_ver = req.protocol_ver
return c
def make(self) -> bytes:
resp_struct = Struct(
"felica_idx" / Int32ul,
"access_code" / Int8ub[10],
Padding(2)
).build(dict(
felica_idx = 0,
access_code = bytes.fromhex(self.access_code)
))
self.head.length = HEADER_SIZE + len(resp_struct)
return self.head.make() + resp_struct
class ADBFelicaLookup2Request(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
self.random = struct.unpack_from("<16s", data, 0x20)[0]
idm, pmm = struct.unpack_from(">QQ", data, 0x30)
self.card_key_ver, self.write_ct, self.maca, company, fw_ver, self.dfc = struct.unpack_from("<16s16sQccH", data, 0x40)
self.idm = hex(idm)[2:].upper()
self.pmm = hex(pmm)[2:].upper()
self.company = CompanyCodes(int.from_bytes(company, 'little'))
self.fw_ver = ReaderFwVer.from_byte(fw_ver)
class ADBFelicaLookup2Response(ADBBaseResponse):
def __init__(self, user_id: Union[int, None] = None, access_code: Union[str, None] = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x12, length: int = 0x130, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.user_id = user_id if user_id is not None else -1
self.access_code = access_code if access_code is not None else "00000000000000000000"
self.company = CompanyCodes.SEGA
self.portal_status = PortalRegStatus.NO_REG
@classmethod
def from_req(cls, req: ADBHeader, user_id: Union[int, None] = None, access_code: Union[str, None] = None) -> "ADBFelicaLookup2Response":
c = cls(user_id, access_code, req.game_id, req.store_id, req.keychip_id)
c.head.protocol_ver = req.protocol_ver
return c
def make(self) -> bytes:
resp_struct = Struct(
"user_id" / Int32sl,
"relation1" / Int32sl,
"relation2" / Int32sl,
"access_code" / Int8ub[10],
"portal_status" / Int8ub,
"company_code" / Int8ub,
Padding(8),
"auth_key" / Int8ub[256],
).build(dict(
user_id = self.user_id,
relation1 = -1, # Unsupported
relation2 = -1, # Unsupported
access_code = bytes.fromhex(self.access_code),
portal_status = self.portal_status.value,
company_code = self.company.value,
auth_key = [0] * 256 # Unsupported
))
self.head.length = HEADER_SIZE + len(resp_struct)
return self.head.make() + resp_struct

56
core/adb_handlers/log.py Normal file
View File

@@ -0,0 +1,56 @@
from construct import Struct, Padding, Int8sl
from typing import Final, List
from .base import *
NUM_LOGS: Final[int] = 20
NUM_LEN_LOG_EX: Final[int] = 48
class AmLogEx:
def __init__(self, data: bytes) -> None:
self.aime_id, status, self.user_id, self.credit_ct, self.bet_ct, self.won_ct, self.local_time, \
self.tseq, self.place_id = struct.unpack("<IIQiii4xQiI", data)
self.status = LogStatus(status)
class ADBStatusLogRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
self.aime_id, status = struct.unpack_from("<II", data, 0x20)
self.status = LogStatus(status)
class ADBLogRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
self.aime_id, status, self.user_id, self.credit_ct, self.bet_ct, self.won_ct = struct.unpack_from("<IIQiii", data, 0x20)
self.status = LogStatus(status)
class ADBLogExRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
self.logs: List[AmLogEx] = []
for x in range(NUM_LOGS):
self.logs.append(AmLogEx(data[0x20 + (NUM_LEN_LOG_EX * x): 0x50 + (NUM_LEN_LOG_EX * x)]))
self.num_logs = struct.unpack_from("<I", data, 0x03E0)[0]
class ADBLogExResponse(ADBBaseResponse):
def __init__(self, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", protocol_ver: int = 12423, code: int = 20, length: int = 64, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id, protocol_ver)
@classmethod
def from_req(cls, req: ADBHeader) -> "ADBLogExResponse":
c = cls(req.game_id, req.store_id, req.keychip_id, req.protocol_ver)
return c
def make(self) -> bytes:
resp_struct = Struct(
"log_result" / Int8sl[NUM_LOGS],
Padding(12)
)
body = resp_struct.build(dict(
log_result = [1] * NUM_LOGS
))
self.head.length = HEADER_SIZE + len(body)
return self.head.make() + body

View File

@@ -0,0 +1,81 @@
from construct import Struct, Int32sl, Padding, Int8sl
from typing import Union
from .base import *
class ADBLookupException(Exception):
pass
class ADBLookupRequest(ADBBaseRequest):
def __init__(self, data: bytes) -> None:
super().__init__(data)
self.access_code = data[0x20:0x2A].hex()
company_code, fw_version, self.serial_number = struct.unpack_from("<bbI", data, 0x2A)
try:
self.company_code = CompanyCodes(company_code)
except ValueError as e:
raise ADBLookupException(f"Invalid company code - {e}")
self.fw_version = ReaderFwVer.from_byte(fw_version)
class ADBLookupResponse(ADBBaseResponse):
def __init__(self, user_id: Union[int, None], game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x06, length: int = 0x30, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.user_id = user_id if user_id is not None else -1
self.portal_reg = PortalRegStatus.NO_REG
@classmethod
def from_req(cls, req: ADBHeader, user_id: Union[int, None]) -> "ADBLookupResponse":
c = cls(user_id, req.game_id, req.store_id, req.keychip_id)
c.head.protocol_ver = req.protocol_ver
return c
def make(self):
resp_struct = Struct(
"user_id" / Int32sl,
"portal_reg" / Int8sl,
Padding(11)
)
body = resp_struct.build(dict(
user_id = self.user_id,
portal_reg = self.portal_reg.value
))
self.head.length = HEADER_SIZE + len(body)
return self.head.make() + body
class ADBLookupExResponse(ADBBaseResponse):
def __init__(self, user_id: Union[int, None], game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888",
code: int = 0x10, length: int = 0x130, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.user_id = user_id if user_id is not None else -1
self.portal_reg = PortalRegStatus.NO_REG
@classmethod
def from_req(cls, req: ADBHeader, user_id: Union[int, None]) -> "ADBLookupExResponse":
c = cls(user_id, req.game_id, req.store_id, req.keychip_id)
c.head.protocol_ver = req.protocol_ver
return c
def make(self):
resp_struct = Struct(
"user_id" / Int32sl,
"portal_reg" / Int8sl,
Padding(3),
"auth_key" / Int8sl[256],
"relation1" / Int32sl,
"relation2" / Int32sl,
)
body = resp_struct.build(dict(
user_id = self.user_id,
portal_reg = self.portal_reg.value,
auth_key = [0] * 256,
relation1 = -1,
relation2 = -1
))
self.head.length = HEADER_SIZE + len(body)
return self.head.make() + body

View File

@@ -2,27 +2,17 @@ from twisted.internet.protocol import Factory, Protocol
import logging, coloredlogs import logging, coloredlogs
from Crypto.Cipher import AES from Crypto.Cipher import AES
import struct import struct
from typing import Dict, Any from typing import Dict, Tuple, Callable, Union
from typing_extensions import Final
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from core.config import CoreConfig from core.config import CoreConfig
from core.data import Data from core.data import Data
from .adb_handlers import *
class AimedbProtocol(Protocol): class AimedbProtocol(Protocol):
AIMEDB_RESPONSE_CODES = { request_list: Dict[int, Tuple[Callable[[bytes, int], Union[ADBBaseResponse, bytes]], int, str]] = {}
"felica_lookup": 0x03,
"lookup": 0x06,
"log": 0x0A,
"campaign": 0x0C,
"touch": 0x0E,
"lookup2": 0x10,
"felica_lookup2": 0x12,
"log2": 0x14,
"hello": 0x65,
}
request_list: Dict[int, Any] = {}
def __init__(self, core_cfg: CoreConfig) -> None: def __init__(self, core_cfg: CoreConfig) -> None:
self.logger = logging.getLogger("aimedb") self.logger = logging.getLogger("aimedb")
@@ -32,16 +22,27 @@ class AimedbProtocol(Protocol):
self.logger.error("!!!KEY NOT SET!!!") self.logger.error("!!!KEY NOT SET!!!")
exit(1) exit(1)
self.request_list[0x01] = self.handle_felica_lookup self.register_handler(0x01, 0x03, self.handle_felica_lookup, 'felica_lookup')
self.request_list[0x04] = self.handle_lookup self.register_handler(0x02, 0x03, self.handle_felica_register, 'felica_register')
self.request_list[0x05] = self.handle_register
self.request_list[0x09] = self.handle_log self.register_handler(0x04, 0x06, self.handle_lookup, 'lookup')
self.request_list[0x0B] = self.handle_campaign self.register_handler(0x05, 0x06, self.handle_register, 'register')
self.request_list[0x0D] = self.handle_touch
self.request_list[0x0F] = self.handle_lookup2 self.register_handler(0x07, 0x08, self.handle_status_log, 'status_log')
self.request_list[0x11] = self.handle_felica_lookup2 self.register_handler(0x09, 0x0A, self.handle_log, 'aime_log')
self.request_list[0x13] = self.handle_log2
self.request_list[0x64] = self.handle_hello self.register_handler(0x0B, 0x0C, self.handle_campaign, 'campaign')
self.register_handler(0x0D, 0x0E, self.handle_campaign_clear, 'campaign_clear')
self.register_handler(0x0F, 0x10, self.handle_lookup_ex, 'lookup_ex')
self.register_handler(0x11, 0x12, self.handle_felica_lookup_ex, 'felica_lookup_ex')
self.register_handler(0x13, 0x14, self.handle_log_ex, 'aime_log_ex')
self.register_handler(0x64, 0x65, self.handle_hello, 'hello')
self.register_handler(0x66, 0, self.handle_goodbye, 'goodbye')
def register_handler(self, cmd: int, resp:int, handler: Callable[[bytes, int], Union[ADBBaseResponse, bytes]], name: str) -> None:
self.request_list[cmd] = (handler, resp, name)
def append_padding(self, data: bytes): def append_padding(self, data: bytes):
"""Appends 0s to the end of the data until it's at the correct size""" """Appends 0s to the end of the data until it's at the correct size"""
@@ -63,202 +64,233 @@ class AimedbProtocol(Protocol):
try: try:
decrypted = cipher.decrypt(data) decrypted = cipher.decrypt(data)
except:
self.logger.error(f"Failed to decrypt {data.hex()}") except Exception as e:
self.logger.error(f"Failed to decrypt {data.hex()} because {e}")
return None return None
self.logger.debug(f"{self.transport.getPeer().host} wrote {decrypted.hex()}") self.logger.debug(f"{self.transport.getPeer().host} wrote {decrypted.hex()}")
if not decrypted[1] == 0xA1 and not decrypted[0] == 0x3E:
self.logger.error(f"Bad magic")
return None
req_code = decrypted[4]
if req_code == 0x66:
self.logger.info(f"goodbye from {self.transport.getPeer().host}")
self.transport.loseConnection()
return
try: try:
resp = self.request_list[req_code](decrypted) head = ADBHeader.from_data(decrypted)
encrypted = cipher.encrypt(resp)
self.logger.debug(f"Response {resp.hex()}") except ADBHeaderException as e:
self.logger.error(f"Error parsing ADB header: {e}")
try:
encrypted = cipher.encrypt(ADBBaseResponse().make())
self.transport.write(encrypted) self.transport.write(encrypted)
except KeyError: except Exception as e:
self.logger.error(f"Unknown command code {hex(req_code)}") self.logger.error(f"Failed to encrypt default response because {e}")
return None
except ValueError as e: return
self.logger.error(f"Failed to encrypt {resp.hex()} because {e}")
return None
def handle_campaign(self, data: bytes) -> bytes: if head.keychip_id == "ABCD1234567" or head.store_id == 0xfff0:
self.logger.info(f"campaign from {self.transport.getPeer().host}") self.logger.warning(f"Request from uninitialized AMLib: {vars(head)}")
ret = struct.pack(
"<5H", handler, resp_code, name = self.request_list.get(head.cmd, (self.handle_default, None, 'default'))
0xA13E,
0x3087, if resp_code is None:
self.AIMEDB_RESPONSE_CODES["campaign"], self.logger.warning(f"No handler for cmd {hex(head.cmd)}")
0x0200,
0x0001, elif resp_code > 0:
self.logger.info(f"{name} from {head.keychip_id} ({head.game_id}) @ {self.transport.getPeer().host}")
resp = handler(decrypted, resp_code)
if type(resp) == ADBBaseResponse or issubclass(type(resp), ADBBaseResponse):
resp_bytes = resp.make()
if len(resp_bytes) != resp.head.length:
resp_bytes = self.append_padding(resp_bytes)
elif type(resp) == bytes:
resp_bytes = resp
elif resp is None: # Nothing to send, probably a goodbye
return
else:
raise TypeError(f"Unsupported type returned by ADB handler for {name}: {type(resp)}")
try:
encrypted = cipher.encrypt(resp_bytes)
self.logger.debug(f"Response {resp_bytes.hex()}")
self.transport.write(encrypted)
except Exception as e:
self.logger.error(f"Failed to encrypt {resp_bytes.hex()} because {e}")
def handle_default(self, data: bytes, resp_code: int, length: int = 0x20) -> ADBBaseResponse:
req = ADBHeader.from_data(data)
return ADBBaseResponse(resp_code, length, 1, req.game_id, req.store_id, req.keychip_id, req.protocol_ver)
def handle_hello(self, data: bytes, resp_code: int) -> ADBBaseResponse:
return self.handle_default(data, resp_code)
def handle_campaign(self, data: bytes, resp_code: int) -> ADBBaseResponse:
h = ADBHeader.from_data(data)
if h.protocol_ver >= 0x3030:
req = h
resp = ADBCampaignResponse.from_req(req)
else:
req = ADBOldCampaignRequest(data)
self.logger.info(f"Legacy campaign request for campaign {req.campaign_id} (protocol version {hex(h.protocol_ver)})")
resp = ADBOldCampaignResponse.from_req(req.head)
# We don't currently support campaigns
return resp
def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse:
req = ADBLookupRequest(data)
user_id = self.data.card.get_user_id_from_card(req.access_code)
ret = ADBLookupResponse.from_req(req.head, user_id)
self.logger.info(
f"access_code {req.access_code} -> user_id {ret.user_id}"
) )
return self.append_padding(ret) return ret
def handle_hello(self, data: bytes) -> bytes: def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse:
self.logger.info(f"hello from {self.transport.getPeer().host}") req = ADBLookupRequest(data)
ret = struct.pack( user_id = self.data.card.get_user_id_from_card(req.access_code)
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["hello"], 0x0020, 0x0001
ret = ADBLookupExResponse.from_req(req.head, user_id)
self.logger.info(
f"access_code {req.access_code} -> user_id {ret.user_id}"
) )
return self.append_padding(ret) return ret
def handle_lookup(self, data: bytes) -> bytes: def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes:
luid = data[0x20:0x2A].hex() """
user_id = self.data.card.get_user_id_from_card(access_code=luid) On official, I think a card has to be registered for this to actually work, but
I'm making the executive decision to not implement that and just kick back our
faux generated access code. The real felica IDm -> access code conversion is done
on the ADB server, which we do not and will not ever have access to. Because we can
assure that all IDms will be unique, this basic 0-padded hex -> int conversion will
be fine.
"""
req = ADBFelicaLookupRequest(data)
ac = self.data.card.to_access_code(req.idm)
self.logger.info(
f"idm {req.idm} ipm {req.pmm} -> access_code {ac}"
)
return ADBFelicaLookupResponse.from_req(req.head, ac)
def handle_felica_register(self, data: bytes, resp_code: int) -> bytes:
"""
I've never seen this used.
"""
req = ADBFelicaLookupRequest(data)
ac = self.data.card.to_access_code(req.idm)
if self.config.server.allow_user_registration:
user_id = self.data.user.create_user()
if user_id is None: if user_id is None:
self.logger.error("Failed to register user!")
user_id = -1
else:
card_id = self.data.card.create_card(user_id, ac)
if card_id is None:
self.logger.error("Failed to register card!")
user_id = -1 user_id = -1
self.logger.info( self.logger.info(
f"lookup from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}" f"Register access code {ac} (IDm: {req.idm} PMm: {req.pmm}) -> user_id {user_id}"
) )
ret = struct.pack(
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["lookup"], 0x0130, 0x0001
)
ret += bytes(0x20 - len(ret))
if user_id is None:
ret += struct.pack("<iH", -1, 0)
else: else:
ret += struct.pack("<l", user_id)
return self.append_padding(ret)
def handle_lookup2(self, data: bytes) -> bytes:
self.logger.info(f"lookup2")
ret = bytearray(self.handle_lookup(data))
ret[4] = self.AIMEDB_RESPONSE_CODES["lookup2"]
return bytes(ret)
def handle_felica_lookup(self, data: bytes) -> bytes:
idm = data[0x20:0x28].hex()
pmm = data[0x28:0x30].hex()
access_code = self.data.card.to_access_code(idm)
self.logger.info( self.logger.info(
f"felica_lookup from {self.transport.getPeer().host}: idm {idm} pmm {pmm} -> access_code {access_code}" f"Registration blocked!: access code {ac} (IDm: {req.idm} PMm: {req.pmm})"
) )
ret = struct.pack( return ADBFelicaLookupResponse.from_req(req.head, ac)
"<5H",
0xA13E,
0x3087,
self.AIMEDB_RESPONSE_CODES["felica_lookup"],
0x0030,
0x0001,
)
ret += bytes(26)
ret += bytes.fromhex(access_code)
return self.append_padding(ret) def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes:
req = ADBFelicaLookup2Request(data)
def handle_felica_lookup2(self, data: bytes) -> bytes: access_code = self.data.card.to_access_code(req.idm)
idm = data[0x30:0x38].hex()
pmm = data[0x38:0x40].hex()
access_code = self.data.card.to_access_code(idm)
user_id = self.data.card.get_user_id_from_card(access_code=access_code) user_id = self.data.card.get_user_id_from_card(access_code=access_code)
if user_id is None: if user_id is None:
user_id = -1 user_id = -1
self.logger.info( self.logger.info(
f"felica_lookup2 from {self.transport.getPeer().host}: idm {idm} ipm {pmm} -> access_code {access_code} user_id {user_id}" f"idm {req.idm} ipm {req.pmm} -> access_code {access_code} user_id {user_id}"
) )
ret = struct.pack( return ADBFelicaLookup2Response.from_req(req.head, user_id, access_code)
"<5H",
0xA13E,
0x3087,
self.AIMEDB_RESPONSE_CODES["felica_lookup2"],
0x0140,
0x0001,
)
ret += bytes(22)
ret += struct.pack("<lq", user_id, -1) # first -1 is ext_id, 3rd is access code
ret += bytes.fromhex(access_code)
ret += struct.pack("<l", 1)
return self.append_padding(ret) def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse:
req = ADBCampaignClearRequest(data)
def handle_touch(self, data: bytes) -> bytes: resp = ADBCampaignClearResponse.from_req(req.head)
self.logger.info(f"touch from {self.transport.getPeer().host}")
ret = struct.pack(
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["touch"], 0x0050, 0x0001
)
ret += bytes(5)
ret += struct.pack("<3H", 0x6F, 0, 1)
return self.append_padding(ret) # We don't support campaign stuff
return resp
def handle_register(self, data: bytes, resp_code: int) -> bytes:
req = ADBLookupRequest(data)
user_id = -1
def handle_register(self, data: bytes) -> bytes:
luid = data[0x20:0x2A].hex()
if self.config.server.allow_user_registration: if self.config.server.allow_user_registration:
user_id = self.data.user.create_user() user_id = self.data.user.create_user()
if user_id is None: if user_id is None:
user_id = -1
self.logger.error("Failed to register user!") self.logger.error("Failed to register user!")
user_id = -1
else: else:
card_id = self.data.card.create_card(user_id, luid) card_id = self.data.card.create_card(user_id, req.access_code)
if card_id is None: if card_id is None:
user_id = -1
self.logger.error("Failed to register card!") self.logger.error("Failed to register card!")
user_id = -1
self.logger.info( self.logger.info(
f"register from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}" f"Register access code {req.access_code} -> user_id {user_id}"
) )
else: else:
self.logger.info( self.logger.info(
f"register from {self.transport.getPeer().host} blocked!: luid {luid}" f"Registration blocked!: access code {req.access_code}"
) )
user_id = -1
ret = struct.pack( resp = ADBLookupResponse.from_req(req.head, user_id)
"<5H", if resp.user_id <= 0:
0xA13E, resp.head.status = ADBStatus.BAN_SYS # Closest we can get to a "You cannot register"
0x3087,
self.AIMEDB_RESPONSE_CODES["lookup"],
0x0030,
0x0001 if user_id > -1 else 0,
)
ret += bytes(0x20 - len(ret))
ret += struct.pack("<l", user_id)
return self.append_padding(ret) return resp
def handle_log(self, data: bytes) -> bytes: # TODO: Save these in some capacity, as deemed relevant
# TODO: Save aimedb logs def handle_status_log(self, data: bytes, resp_code: int) -> bytes:
self.logger.info(f"log from {self.transport.getPeer().host}") req = ADBStatusLogRequest(data)
ret = struct.pack( self.logger.info(f"User {req.aime_id} logged {req.status.name} event")
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["log"], 0x0020, 0x0001 return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver)
)
return self.append_padding(ret)
def handle_log2(self, data: bytes) -> bytes: def handle_log(self, data: bytes, resp_code: int) -> bytes:
self.logger.info(f"log2 from {self.transport.getPeer().host}") req = ADBLogRequest(data)
ret = struct.pack( self.logger.info(f"User {req.aime_id} logged {req.status.name} event, credit_ct: {req.credit_ct} bet_ct: {req.bet_ct} won_ct: {req.won_ct}")
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["log2"], 0x0040, 0x0001 return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver)
)
ret += bytes(22)
ret += struct.pack("H", 1)
return self.append_padding(ret) def handle_log_ex(self, data: bytes, resp_code: int) -> bytes:
req = ADBLogExRequest(data)
strs = []
self.logger.info(f"Recieved {req.num_logs} or {len(req.logs)} logs")
for x in range(req.num_logs):
self.logger.debug(f"User {req.logs[x].aime_id} logged {req.logs[x].status.name} event, credit_ct: {req.logs[x].credit_ct} bet_ct: {req.logs[x].bet_ct} won_ct: {req.logs[x].won_ct}")
return ADBLogExResponse.from_req(req.head)
def handle_goodbye(self, data: bytes, resp_code: int) -> None:
self.logger.info(f"goodbye from {self.transport.getPeer().host}")
self.transport.loseConnection()
return
class AimedbFactory(Factory): class AimedbFactory(Factory):
protocol = AimedbProtocol protocol = AimedbProtocol

View File

@@ -1,4 +1,4 @@
from typing import Dict, List, Any, Optional, Tuple from typing import Dict, List, Any, Optional, Tuple, Union, Final
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
@@ -6,17 +6,32 @@ from datetime import datetime
import pytz import pytz
import base64 import base64
import zlib import zlib
import json
from enum import Enum
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from Crypto.Hash import SHA 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
import math
from core.config import CoreConfig from core.config import CoreConfig
from core.utils import Utils from core.utils import Utils
from core.data import Data from core.data import Data
from core.const import * from core.const import *
BILLING_DT_FORMAT: Final[str] = "%Y%m%d%H%M%S"
class DLIMG_TYPE(Enum):
app = 0
opt = 1
class ALLNET_STAT(Enum):
ok = 0
bad_game = -1
bad_machine = -2
bad_shop = -3
class AllnetServlet: class AllnetServlet:
def __init__(self, core_cfg: CoreConfig, cfg_folder: str): def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
@@ -71,15 +86,23 @@ class AllnetServlet:
def handle_poweron(self, request: Request, _: Dict): def handle_poweron(self, request: Request, _: Dict):
request_ip = Utils.get_ip_addr(request) request_ip = Utils.get_ip_addr(request)
pragma_header = request.getHeader('Pragma')
is_dfi = pragma_header is not None and pragma_header == "DFI"
try: try:
req_dict = self.allnet_req_to_dict(request.content.getvalue()) if is_dfi:
req_urlencode = self.from_dfi(request.content.getvalue())
else:
req_urlencode = request.content.getvalue().decode()
req_dict = self.allnet_req_to_dict(req_urlencode)
if req_dict is None: if req_dict is None:
raise AllnetRequestException() raise AllnetRequestException()
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,34 +112,14 @@ 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 not self.config.server.is_develop:
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
self.data.base.log_event(
"allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg
)
self.logger.warn(msg)
resp.stat = 0
return self.dict_to_http_form_string([vars(resp)])
else:
self.logger.info(
f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}"
)
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}"
self.logger.debug(f"Allnet response: {vars(resp)}")
return self.dict_to_http_form_string([vars(resp)])
resp.uri, resp.host = self.uri_registry[req.game_id]
machine = self.data.arcade.get_machine(req.serial) machine = self.data.arcade.get_machine(req.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:
@@ -124,13 +127,38 @@ class AllnetServlet:
self.data.base.log_event( self.data.base.log_event(
"allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg "allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg
) )
self.logger.warn(msg) self.logger.warning(msg)
resp.stat = 0 resp.stat = ALLNET_STAT.bad_machine.value
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"])
if self.config.server.check_arcade_ip:
if arcade["ip"] and arcade["ip"] is not None and arcade["ip"] != req.ip:
msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip} (expected {arcade['ip']})."
self.data.base.log_event(
"allnet", "ALLNET_AUTH_BAD_IP", logging.ERROR, msg
)
self.logger.warning(msg)
resp.stat = ALLNET_STAT.bad_shop.value
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")
elif not arcade["ip"] or arcade["ip"] is None and self.config.server.strict_ip_checking:
msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)."
self.data.base.log_event(
"allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg
)
self.logger.warning(msg)
resp.stat = ALLNET_STAT.bad_shop.value
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")
country = ( country = (
arcade["country"] if machine["country"] is None else machine["country"] arcade["country"] if machine["country"] is None else machine["country"]
) )
@@ -162,6 +190,33 @@ class AllnetServlet:
arcade["timezone"] if arcade["timezone"] is not None else "+0900" arcade["timezone"] if arcade["timezone"] is not None else "+0900"
) )
if req.game_id not in self.uri_registry:
if not self.config.server.is_develop:
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
self.data.base.log_event(
"allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg
)
self.logger.warning(msg)
resp.stat = ALLNET_STAT.bad_game.value
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:
self.logger.info(
f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}"
)
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_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]
int_ver = req.ver.replace(".", "") int_ver = req.ver.replace(".", "")
resp.uri = resp.uri.replace("$v", int_ver) resp.uri = resp.uri.replace("$v", int_ver)
resp.host = resp.host.replace("$v", int_ver) resp.host = resp.host.replace("$v", int_ver)
@@ -169,14 +224,30 @@ 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"
"""if is_dfi:
request.responseHeaders.addRawHeader('Pragma', 'DFI')
return self.to_dfi(resp_str)"""
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)
pragma_header = request.getHeader('Pragma')
is_dfi = pragma_header is not None and pragma_header == "DFI"
try: try:
req_dict = self.allnet_req_to_dict(request.content.getvalue()) if is_dfi:
req_urlencode = self.from_dfi(request.content.getvalue())
else:
req_urlencode = request.content.getvalue().decode()
req_dict = self.allnet_req_to_dict(req_urlencode)
if req_dict is None: if req_dict is None:
raise AllnetRequestException() raise AllnetRequestException()
@@ -196,13 +267,13 @@ 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(
@@ -216,7 +287,15 @@ class AllnetServlet:
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}")
res_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n"
"""if is_dfi:
request.responseHeaders.addRawHeader('Pragma', 'DFI')
return self.to_dfi(res_str)"""
return res_str
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:
@@ -225,6 +304,9 @@ 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()
@@ -233,14 +315,64 @@ class AllnetServlet:
return b"" return b""
def handle_dlorder_report(self, request: Request, match: Dict) -> bytes: def handle_dlorder_report(self, request: Request, match: Dict) -> bytes:
self.logger.info( req_raw = request.content.getvalue()
f"DLI Report from {Utils.get_ip_addr(request)}: {request.content.getvalue()}" try:
) req_dict: Dict = json.loads(req_raw)
return b"" except Exception as e:
self.logger.warning(f"Failed to parse DL Report: {e}")
return "NG"
dl_data_type = DLIMG_TYPE.app
dl_data = req_dict.get("appimage", {})
if dl_data is None or not dl_data:
dl_data_type = DLIMG_TYPE.opt
dl_data = req_dict.get("optimage", {})
if dl_data is None or not dl_data:
self.logger.warning(f"Failed to parse DL Report: Invalid format - contains neither appimage nor optimage")
return "NG"
dl_report_data = DLReport(dl_data, dl_data_type)
if not dl_report_data.validate():
self.logger.warning(f"Failed to parse DL Report: Invalid format - {dl_report_data.err}")
return "NG"
return "OK"
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_raw = request.content.getvalue()
if request.getHeader('Content-Type') == "application/octet-stream":
req_unzip = zlib.decompressobj(-zlib.MAX_WBITS).decompress(req_raw)
else:
req_unzip = req_raw
req_dict = self.billing_req_to_dict(req_unzip)
request_ip = Utils.get_ip_addr(request) request_ip = Utils.get_ip_addr(request)
if req_dict is None: if req_dict is None:
self.logger.error(f"Failed to parse request {request.content.getvalue()}") self.logger.error(f"Failed to parse request {request.content.getvalue()}")
return b"" return b""
@@ -250,45 +382,60 @@ class AllnetServlet:
rsa = RSA.import_key(open(self.config.billing.signing_key, "rb").read()) rsa = RSA.import_key(open(self.config.billing.signing_key, "rb").read())
signer = PKCS1_v1_5.new(rsa) signer = PKCS1_v1_5.new(rsa)
digest = SHA.new() digest = SHA.new()
traces: List[TraceData] = []
try: try:
kc_playlimit = int(req_dict[0]["playlimit"]) for x in range(len(req_dict)):
kc_nearfull = int(req_dict[0]["nearfull"]) if not req_dict[x]:
kc_billigtype = int(req_dict[0]["billingtype"]) continue
kc_playcount = int(req_dict[0]["playcnt"])
kc_serial: str = req_dict[0]["keychipid"] if x == 0:
kc_game: str = req_dict[0]["gameid"] req = BillingInfo(req_dict[x])
kc_date = strptime(req_dict[0]["date"], "%Y%m%d%H%M%S") continue
kc_serial_bytes = kc_serial.encode()
tmp = TraceData(req_dict[x])
if tmp.trace_type == TraceDataType.CHARGE:
tmp = TraceDataCharge(req_dict[x])
elif tmp.trace_type == TraceDataType.EVENT:
tmp = TraceDataEvent(req_dict[x])
elif tmp.trace_type == TraceDataType.CREDIT:
tmp = TraceDataCredit(req_dict[x])
traces.append(tmp)
kc_serial_bytes = req.keychipid.encode()
except KeyError as e: except KeyError as e:
return f"result=5&linelimit=&message={e} field is missing".encode() self.logger.error(f"Billing request failed to parse: {e}")
return f"result=5&linelimit=&message=field is missing or formatting is incorrect\r\n".encode()
machine = self.data.arcade.get_machine(kc_serial) machine = self.data.arcade.get_machine(req.keychipid)
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 {req.keychipid} attempted billing checkin from {request_ip} for {req.gameid} v{req.gamever}."
self.data.base.log_event( self.data.base.log_event(
"allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg "allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg
) )
self.logger.warn(msg) self.logger.warning(msg)
resp = BillingResponse("", "", "", "") return f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n".encode()
resp.result = "1"
return self.dict_to_http_form_string([vars(resp)])
msg = ( msg = (
f"Billing checkin from {request_ip}: game {kc_game} keychip {kc_serial} playcount " f"Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount "
f"{kc_playcount} billing_type {kc_billigtype} nearfull {kc_nearfull} playlimit {kc_playlimit}" f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}"
) )
self.logger.info(msg) self.logger.info(msg)
self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, msg) self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, msg)
if req.traceleft > 0:
self.logger.warn(f"{req.traceleft} unsent tracelogs")
kc_playlimit = req.playlimit
kc_nearfull = req.nearfull
while kc_playcount > kc_playlimit: while req.playcnt > req.playlimit:
kc_playlimit += 1024 kc_playlimit += 1024
kc_nearfull += 1024 kc_nearfull += 1024
playlimit = kc_playlimit playlimit = kc_playlimit
nearfull = kc_nearfull + (kc_billigtype * 0x00010000) nearfull = kc_nearfull + (req.billingtype.value * 0x00010000)
digest.update(playlimit.to_bytes(4, "little") + kc_serial_bytes) digest.update(playlimit.to_bytes(4, "little") + kc_serial_bytes)
playlimit_sig = signer.sign(digest).hex() playlimit_sig = signer.sign(digest).hex()
@@ -299,44 +446,33 @@ class AllnetServlet:
# TODO: playhistory # TODO: playhistory
resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig) #resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig)
resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig, req.requestno, req.protocolver)
resp_str = self.dict_to_http_form_string([vars(resp)], True) resp_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\r\n"
if resp_str is None:
self.logger.error(f"Failed to parse response {vars(resp)}")
self.logger.debug(f"response {vars(resp)}") self.logger.debug(f"response {vars(resp)}")
if req.traceleft > 0:
self.logger.info(f"Requesting 20 more of {req.traceleft} unsent tracelogs")
return f"result=6&waittime=0&linelimit=20\r\n".encode()
return resp_str.encode("utf-8") return resp_str.encode("utf-8")
def handle_naomitest(self, request: Request, _: Dict) -> bytes: def handle_naomitest(self, request: Request, _: Dict) -> bytes:
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
""" """
try: try:
decomp = zlib.decompressobj(-zlib.MAX_WBITS) sections = data.decode("ascii").split("\r\n")
unzipped = decomp.decompress(data)
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}")
@@ -347,67 +483,45 @@ class AllnetServlet:
Parses an allnet request string into a python dictionary Parses an allnet request string into a python dictionary
""" """
try: try:
zipped = base64.b64decode(data) sections = data.split("\r\n")
unzipped = zlib.decompress(zipped)
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}")
return None return None
def dict_to_http_form_string( def from_dfi(self, data: bytes) -> str:
self, zipped = base64.b64decode(data)
data: List[Dict[str, Any]], unzipped = zlib.decompress(zipped)
crlf: bool = False, return unzipped.decode("utf-8")
trailing_newline: bool = True,
) -> Optional[str]:
"""
Takes a python dictionary and parses it into an allnet response string
"""
try:
urlencode = ""
for item in data:
for k, v in item.items():
urlencode += f"{k}={v}&"
if crlf: def to_dfi(self, data: str) -> bytes:
urlencode = urlencode[:-1] + "\r\n" unzipped = data.encode('utf-8')
else: zipped = zlib.compress(unzipped)
urlencode = urlencode[:-1] + "\n" return base64.b64encode(zipped)
if not trailing_newline:
if crlf:
urlencode = urlencode[:-2]
else:
urlencode = urlencode[:-1]
return urlencode
except Exception as e:
self.logger.error(f"dict_to_http_form_string: {e} while parsing {data}")
return None
class AllnetPowerOnRequest: 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) -> None:
def __init__(self, token) -> None:
self.stat = 1 self.stat = 1
self.uri = "" self.uri = ""
self.host = "" self.host = ""
@@ -419,39 +533,44 @@ class AllnetPowerOnResponse3:
self.region_name1 = "" self.region_name1 = ""
self.region_name2 = "" self.region_name2 = ""
self.region_name3 = "" 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.setting = "1"
self.res_ver = "3"
self.token = str(token)
class AllnetPowerOnResponse2:
def __init__(self) -> None:
self.stat = 1
self.uri = ""
self.host = ""
self.place_id = "123"
self.name = "ARTEMiS"
self.nickname = "ARTEMiS"
self.region0 = "1"
self.region_name0 = "W"
self.region_name1 = "X"
self.region_name2 = "Y"
self.region_name3 = "Z"
self.country = "JPN"
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"
@@ -469,6 +588,114 @@ class AllnetDownloadOrderResponse:
self.serial = serial self.serial = serial
self.uri = uri self.uri = uri
class TraceDataType(Enum):
CHARGE = 0
EVENT = 1
CREDIT = 2
class BillingType(Enum):
A = 1
B = 0
class float5:
def __init__(self, n: str = "0") -> None:
nf = float(n)
if nf > 999.9 or nf < 0:
raise ValueError('float5 must be between 0.000 and 999.9 inclusive')
return nf
@classmethod
def to_str(cls, f: float):
return f"%.{2 - int(math.log10(f))+1}f" % f
class BillingInfo:
def __init__(self, data: Dict) -> None:
try:
self.keychipid = str(data.get("keychipid", None))
self.functype = int(data.get("functype", None))
self.gameid = str(data.get("gameid", None))
self.gamever = float(data.get("gamever", None))
self.boardid = str(data.get("boardid", None))
self.tenpoip = str(data.get("tenpoip", None))
self.libalibver = float(data.get("libalibver", None))
self.datamax = int(data.get("datamax", None))
self.billingtype = BillingType(int(data.get("billingtype", None)))
self.protocolver = float(data.get("protocolver", None))
self.operatingfix = bool(data.get("operatingfix", None))
self.traceleft = int(data.get("traceleft", None))
self.requestno = int(data.get("requestno", None))
self.datesync = bool(data.get("datesync", None))
self.timezone = str(data.get("timezone", None))
self.date = datetime.strptime(data.get("date", None), BILLING_DT_FORMAT)
self.crcerrcnt = int(data.get("crcerrcnt", None))
self.memrepair = bool(data.get("memrepair", None))
self.playcnt = int(data.get("playcnt", None))
self.playlimit = int(data.get("playlimit", None))
self.nearfull = int(data.get("nearfull", None))
except Exception as e:
raise KeyError(e)
class TraceData:
def __init__(self, data: Dict) -> None:
try:
self.crc_err_flg = bool(data.get("cs", None))
self.record_number = int(data.get("rn", None))
self.seq_number = int(data.get("sn", None))
self.trace_type = TraceDataType(int(data.get("tt", None)))
self.date_sync_flg = bool(data.get("ds", None))
self.date = datetime.strptime(data.get("dt", None), BILLING_DT_FORMAT)
self.keychip = str(data.get("kn", None))
self.lib_ver = float(data.get("alib", None))
except Exception as e:
raise KeyError(e)
class TraceDataCharge(TraceData):
def __init__(self, data: Dict) -> None:
super().__init__(data)
try:
self.game_id = str(data.get("gi", None))
self.game_version = float(data.get("gv", None))
self.board_serial = str(data.get("bn", None))
self.shop_ip = str(data.get("ti", None))
self.play_count = int(data.get("pc", None))
self.play_limit = int(data.get("pl", None))
self.product_code = int(data.get("ic", None))
self.product_count = int(data.get("in", None))
self.func_type = int(data.get("kk", None))
self.player_number = int(data.get("playerno", None))
except Exception as e:
raise KeyError(e)
class TraceDataEvent(TraceData):
def __init__(self, data: Dict) -> None:
super().__init__(data)
try:
self.message = str(data.get("me", None))
except Exception as e:
raise KeyError(e)
class TraceDataCredit(TraceData):
def __init__(self, data: Dict) -> None:
super().__init__(data)
try:
self.chute_type = int(data.get("cct", None))
self.service_type = int(data.get("cst", None))
self.operation_type = int(data.get("cop", None))
self.coin_rate0 = int(data.get("cr0", None))
self.coin_rate1 = int(data.get("cr1", None))
self.bonus_addition = int(data.get("cba", None))
self.credit_rate = int(data.get("ccr", None))
self.credit0 = int(data.get("cc0", None))
self.credit1 = int(data.get("cc1", None))
self.credit2 = int(data.get("cc2", None))
self.credit3 = int(data.get("cc3", None))
self.credit4 = int(data.get("cc4", None))
self.credit5 = int(data.get("cc5", None))
self.credit6 = int(data.get("cc6", None))
self.credit7 = int(data.get("cc7", None))
except Exception as e:
raise KeyError(e)
class BillingResponse: class BillingResponse:
def __init__( def __init__(
@@ -477,20 +704,22 @@ class BillingResponse:
playlimit_sig: str = "", playlimit_sig: str = "",
nearfull: str = "", nearfull: str = "",
nearfull_sig: str = "", nearfull_sig: str = "",
request_num: int = 1,
protocol_ver: float = 1.000,
playhistory: str = "000000/0:000000/0:000000/0", playhistory: str = "000000/0:000000/0:000000/0",
) -> None: ) -> None:
self.result = "0" self.result = 0
self.waitime = "100" self.requestno = request_num
self.linelimit = "1" self.traceerase = 1
self.message = "" self.fixinterval = 120
self.fixlogcnt = 100
self.playlimit = playlimit self.playlimit = playlimit
self.playlimitsig = playlimit_sig self.playlimitsig = playlimit_sig
self.protocolver = "1.000" self.playhistory = playhistory
self.nearfull = nearfull self.nearfull = nearfull
self.nearfullsig = nearfull_sig self.nearfullsig = nearfull_sig
self.fixlogincnt = "0" self.linelimit = 100
self.fixinterval = "5" self.protocolver = float5.to_str(protocol_ver)
self.playhistory = playhistory
# playhistory -> YYYYMM/C:... # playhistory -> YYYYMM/C:...
# YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period # YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period
@@ -499,3 +728,86 @@ class AllnetRequestException(Exception):
def __init__(self, message="") -> None: def __init__(self, message="") -> None:
self.message = message self.message = message
super().__init__(self.message) super().__init__(self.message)
class DLReport:
def __init__(self, data: Dict, report_type: DLIMG_TYPE) -> None:
self.serial = data.get("serial")
self.dfl = data.get("dfl")
self.wfl = data.get("wfl")
self.tsc = data.get("tsc")
self.tdsc = data.get("tdsc")
self.at = data.get("at")
self.ot = data.get("ot")
self.rt = data.get("rt")
self.as_ = data.get("as")
self.rf_state = data.get("rf_state")
self.gd = data.get("gd")
self.dav = data.get("dav")
self.wdav = data.get("wdav") # app only
self.dov = data.get("dov")
self.wdov = data.get("wdov") # app only
self.__type = report_type
self.err = ""
def validate(self) -> bool:
if self.serial is None:
self.err = "serial not provided"
return False
if self.dfl is None:
self.err = "dfl not provided"
return False
if self.wfl is None:
self.err = "wfl not provided"
return False
if self.tsc is None:
self.err = "tsc not provided"
return False
if self.tdsc is None:
self.err = "tdsc not provided"
return False
if self.at is None:
self.err = "at not provided"
return False
if self.ot is None:
self.err = "ot not provided"
return False
if self.rt is None:
self.err = "rt not provided"
return False
if self.as_ is None:
self.err = "as not provided"
return False
if self.rf_state is None:
self.err = "rf_state not provided"
return False
if self.gd is None:
self.err = "gd not provided"
return False
if self.dav is None:
self.err = "dav not provided"
return False
if self.dov is None:
self.err = "dov not provided"
return False
if (self.wdav is None or self.wdov is None) and self.__type == DLIMG_TYPE.app:
self.err = "wdav or wdov not provided in app image"
return False
if (self.wdav is not None or self.wdov is not None) and self.__type == DLIMG_TYPE.opt:
self.err = "wdav or wdov provided in opt image"
return False
return True

View File

@@ -36,12 +36,30 @@ 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(
self.__config, "core", "server", "log_dir", default="logs" self.__config, "core", "server", "log_dir", default="logs"
) )
@property
def check_arcade_ip(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "core", "server", "check_arcade_ip", default=False
)
@property
def strict_ip_checking(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "core", "server", "strict_ip_checking", default=False
)
class TitleConfig: class TitleConfig:
def __init__(self, parent_config: "CoreConfig") -> None: def __init__(self, parent_config: "CoreConfig") -> None:
@@ -182,6 +200,12 @@ class AllnetConfig:
self.__config, "core", "allnet", "port", default=80 self.__config, "core", "allnet", "port", default=80
) )
@property
def ip_check(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "core", "allnet", "ip_check", default=False
)
@property @property
def allow_online_updates(self) -> int: def allow_online_updates(self) -> int:
return CoreConfig.get_config_field( return CoreConfig.get_config_field(

View File

@@ -15,31 +15,48 @@ from core.utils import Utils
class Data: class Data:
current_schema_version = 6
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
if self.config.database.sha2_password: if self.config.database.sha2_password:
passwd = sha256(self.config.database.password.encode()).digest() passwd = sha256(self.config.database.password.encode()).digest()
self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{passwd.hex()}@{self.config.database.host}/{self.config.database.name}?charset=utf8mb4" self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{passwd.hex()}@{self.config.database.host}:{self.config.database.port}/{self.config.database.name}?charset=utf8mb4"
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.port}/{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",
@@ -77,7 +94,7 @@ class Data:
game_mod.database(self.config) game_mod.database(self.config)
metadata.create_all(self.__engine.connect()) metadata.create_all(self.__engine.connect())
self.base.set_schema_ver( self.base.touch_schema_ver(
game_mod.current_schema_version, game_mod.game_codes[0] game_mod.current_schema_version, game_mod.game_codes[0]
) )
@@ -146,7 +163,7 @@ class Data:
version = mod.current_schema_version version = mod.current_schema_version
else: else:
self.logger.warn( self.logger.warning(
f"current_schema_version not found for {folder}" f"current_schema_version not found for {folder}"
) )
@@ -154,7 +171,7 @@ class Data:
version = self.current_schema_version version = self.current_schema_version
if version is None: if version is None:
self.logger.warn( self.logger.warning(
f"Could not determine latest version for {game}, please specify --version" f"Could not determine latest version for {game}, please specify --version"
) )
@@ -237,7 +254,7 @@ class Data:
self.logger.error(f"Failed to create card for owner with id {user_id}") self.logger.error(f"Failed to create card for owner with id {user_id}")
return return
self.logger.warn( self.logger.warning(
f"Successfully created owner with email {email}, access code 00000000000000000000, and password {pw} Make sure to change this password and assign a real card ASAP!" f"Successfully created owner with email {email}, access code 00000000000000000000, and password {pw} Make sure to change this password and assign a real card ASAP!"
) )
@@ -252,7 +269,7 @@ class Data:
return return
if not should_force: if not should_force:
self.logger.warn( self.logger.warning(
f"Card already exists for access code {new_ac} (id {new_card['id']}). If you wish to continue, rerun with the '--force' flag." f"Card already exists for access code {new_ac} (id {new_card['id']}). If you wish to continue, rerun with the '--force' flag."
f" All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}." f" All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}."
) )
@@ -290,7 +307,7 @@ class Data:
def autoupgrade(self) -> None: def autoupgrade(self) -> None:
all_game_versions = self.base.get_all_schema_vers() all_game_versions = self.base.get_all_schema_vers()
if all_game_versions is None: if all_game_versions is None:
self.logger.warn("Failed to get schema versions") self.logger.warning("Failed to get schema versions")
return return
all_games = Utils.get_all_titles() all_games = Utils.get_all_titles()
@@ -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

@@ -1,9 +1,10 @@
from typing import Optional, Dict from typing import Optional, Dict, List
from sqlalchemy import Table, Column from sqlalchemy import Table, Column, and_, or_
from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint
from sqlalchemy.types import Integer, String, Boolean from sqlalchemy.types import Integer, String, Boolean, JSON
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 sqlalchemy.engine import Row
import re import re
from core.data.schema.base import BaseData, metadata from core.data.schema.base import BaseData, metadata
@@ -21,6 +22,7 @@ arcade = Table(
Column("city", String(255)), Column("city", String(255)),
Column("region_id", Integer), Column("region_id", Integer),
Column("timezone", String(255)), Column("timezone", String(255)),
Column("ip", String(39)),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -40,6 +42,9 @@ machine = Table(
Column("timezone", String(255)), Column("timezone", String(255)),
Column("ota_enable", Boolean), Column("ota_enable", Boolean),
Column("is_cab", Boolean), Column("is_cab", Boolean),
Column("memo", String(255)),
Column("is_cab", Boolean),
Column("data", JSON),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -65,7 +70,7 @@ arcade_owner = Table(
class ArcadeData(BaseData): class ArcadeData(BaseData):
def get_machine(self, serial: str = None, id: int = None) -> Optional[Dict]: def get_machine(self, serial: str = None, id: int = None) -> Optional[Row]:
if serial is not None: if serial is not None:
serial = serial.replace("-", "") serial = serial.replace("-", "")
if len(serial) == 11: if len(serial) == 11:
@@ -130,13 +135,20 @@ class ArcadeData(BaseData):
f"Failed to update board id for machine {machine_id} -> {boardid}" f"Failed to update board id for machine {machine_id} -> {boardid}"
) )
def get_arcade(self, id: int) -> Optional[Dict]: def get_arcade(self, id: int) -> Optional[Row]:
sql = arcade.select(arcade.c.id == id) sql = arcade.select(arcade.c.id == id)
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.fetchone()
def get_arcade_machines(self, id: int) -> Optional[List[Row]]:
sql = machine.select(machine.c.arcade == id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_arcade( def put_arcade(
self, self,
name: str, name: str,
@@ -165,7 +177,21 @@ class ArcadeData(BaseData):
return None return None
return result.lastrowid return result.lastrowid
def get_arcade_owners(self, arcade_id: int) -> Optional[Dict]: def get_arcades_managed_by_user(self, user_id: int) -> Optional[List[Row]]:
sql = select(arcade).join(arcade_owner, arcade_owner.c.arcade == arcade.c.id).where(arcade_owner.c.user == user_id)
result = self.execute(sql)
if result is None:
return False
return result.fetchall()
def get_manager_permissions(self, user_id: int, arcade_id: int) -> Optional[int]:
sql = select(arcade_owner.c.permissions).where(and_(arcade_owner.c.user == user_id, arcade_owner.c.arcade == arcade_id))
result = self.execute(sql)
if result is None:
return False
return result.fetchone()
def get_arcade_owners(self, arcade_id: int) -> Optional[Row]:
sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id) sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id)
result = self.execute(sql) result = self.execute(sql)
@@ -187,33 +213,14 @@ class ArcadeData(BaseData):
return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R
def validate_keychip_format(self, serial: str) -> bool: def validate_keychip_format(self, serial: str) -> bool:
serial = serial.replace("-", "") if re.fullmatch(r"^A[0-9]{2}[E|X][-]?[0-9]{2}[A-HJ-NP-Z][0-9]{4}([0-9]{4})?$", serial) is None:
if len(serial) != 11 or len(serial) != 15:
self.logger.error(
f"Serial validate failed: Incorrect length for {serial} (len {len(serial)})"
)
return False
platform_code = serial[:4]
platform_rev = serial[4:6]
const_a = serial[6]
num = serial[7:11]
append = serial[11:15]
if re.match("A[7|6]\d[E|X][0|1][0|1|2]A\d{4,8}", serial) is None:
self.logger.error(f"Serial validate failed: {serial} failed regex")
return False
if len(append) != 0 or len(append) != 4:
self.logger.error(
f"Serial validate failed: {serial} had malformed append {append}"
)
return False
if len(num) != 4:
self.logger.error(
f"Serial validate failed: {serial} had malformed number {num}"
)
return False return False
return True return True
def find_arcade_by_name(self, name: str) -> List[Row]:
sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%")))
result = self.execute(sql)
if result is None:
return False
return result.fetchall()

View File

@@ -58,7 +58,7 @@ class BaseData:
self.logger.error(f"UnicodeEncodeError error {e}") self.logger.error(f"UnicodeEncodeError error {e}")
return None return None
except: except Exception:
try: try:
res = self.conn.execute(sql, opts) res = self.conn.execute(sql, opts)
@@ -70,7 +70,7 @@ class BaseData:
self.logger.error(f"UnicodeEncodeError error {e}") self.logger.error(f"UnicodeEncodeError error {e}")
return None return None
except: except Exception:
self.logger.error(f"Unknown error") self.logger.error(f"Unknown error")
raise raise
@@ -103,6 +103,18 @@ class BaseData:
return row["version"] return row["version"]
def touch_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]:
sql = insert(schema_ver).values(game=game, version=ver)
conflict = sql.on_duplicate_key_update(version=schema_ver.c.version)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"Failed to update schema version for game {game} (v{ver})"
)
return None
return result.lastrowid
def set_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]: def set_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]:
sql = insert(schema_ver).values(game=game, version=ver) sql = insert(schema_ver).values(game=game, version=ver)
conflict = sql.on_duplicate_key_update(version=ver) conflict = sql.on_duplicate_key_update(version=ver)

View File

@@ -107,3 +107,17 @@ class UserData(BaseData):
if result is None: if result is None:
return None return None
return result.fetchall() return result.fetchall()
def find_user_by_email(self, email: str) -> Row:
sql = select(aime_user).where(aime_user.c.email == email)
result = self.execute(sql)
if result is None:
return False
return result.fetchone()
def find_user_by_username(self, username: str) -> List[Row]:
sql = aime_user.select(aime_user.c.username.like(f"%{username}%"))
result = self.execute(sql)
if result is None:
return False
return result.fetchall()

View File

@@ -0,0 +1,3 @@
ALTER TABLE machine DROP COLUMN memo;
ALTER TABLE machine DROP COLUMN is_blacklisted;
ALTER TABLE machine DROP COLUMN `data`;

View File

@@ -0,0 +1 @@
ALTER TABLE arcade DROP COLUMN 'ip';

View File

@@ -0,0 +1,3 @@
ALTER TABLE machine ADD memo varchar(255) NULL;
ALTER TABLE machine ADD is_blacklisted tinyint(1) NULL;
ALTER TABLE machine ADD `data` longtext NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE arcade ADD ip varchar(39) NULL;

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

@@ -9,21 +9,35 @@ from zope.interface import Interface, Attribute, implementer
from twisted.python.components import registerAdapter from twisted.python.components import registerAdapter
import jinja2 import jinja2
import bcrypt import bcrypt
import re
from enum import Enum
from urllib import parse
from core import CoreConfig, Utils from core import CoreConfig, Utils
from core.data import Data from core.data import Data
class IUserSession(Interface): class IUserSession(Interface):
userId = Attribute("User's ID") user_id = Attribute("User's ID")
username = Attribute("User's username")
current_ip = Attribute("User's current ip address") current_ip = Attribute("User's current ip address")
permissions = Attribute("User's permission level") permissions = Attribute("User's permission level")
class PermissionOffset(Enum):
USER = 0 # Regular user
USERMOD = 1 # Can moderate other users
ACMOD = 2 # Can add arcades and cabs
SYSADMIN = 3 # Can change settings
# 4 - 6 reserved for future use
OWNER = 7 # Can do anything
@implementer(IUserSession) @implementer(IUserSession)
class UserSession(object): class UserSession(object):
def __init__(self, session): def __init__(self, session):
self.userId = 0 self.user_id = 0
self.username = None
self.current_ip = "0.0.0.0" self.current_ip = "0.0.0.0"
self.permissions = 0 self.permissions = 0
@@ -80,6 +94,9 @@ class FrontendServlet(resource.Resource):
self.environment.globals["game_list"] = self.game_list self.environment.globals["game_list"] = self.game_list
self.putChild(b"gate", FE_Gate(cfg, self.environment)) self.putChild(b"gate", FE_Gate(cfg, self.environment))
self.putChild(b"user", FE_User(cfg, self.environment)) self.putChild(b"user", FE_User(cfg, self.environment))
self.putChild(b"sys", FE_System(cfg, self.environment))
self.putChild(b"arcade", FE_Arcade(cfg, self.environment))
self.putChild(b"cab", FE_Machine(cfg, self.environment))
self.putChild(b"game", fe_game) self.putChild(b"game", fe_game)
self.logger.info( self.logger.info(
@@ -93,6 +110,7 @@ class FrontendServlet(resource.Resource):
server_name=self.config.server.name, server_name=self.config.server.name,
title=self.config.server.name, title=self.config.server.name,
game_list=self.game_list, game_list=self.game_list,
active_page="/",
sesh=vars(IUserSession(request.getSession())), sesh=vars(IUserSession(request.getSession())),
).encode("utf-16") ).encode("utf-16")
@@ -121,7 +139,7 @@ class FE_Gate(FE_Base):
sesh = request.getSession() sesh = request.getSession()
usr_sesh = IUserSession(sesh) usr_sesh = IUserSession(sesh)
if usr_sesh.userId > 0: if usr_sesh.user_id > 0:
return redirectTo(b"/user", request) return redirectTo(b"/user", request)
if uri.startswith("/gate/create"): if uri.startswith("/gate/create"):
@@ -130,7 +148,7 @@ class FE_Gate(FE_Base):
if b"e" in request.args: if b"e" in request.args:
try: try:
err = int(request.args[b"e"][0].decode()) err = int(request.args[b"e"][0].decode())
except: except Exception:
err = 0 err = 0
else: else:
@@ -140,6 +158,7 @@ class FE_Gate(FE_Base):
return template.render( return template.render(
title=f"{self.core_config.server.name} | Login Gate", title=f"{self.core_config.server.name} | Login Gate",
error=err, error=err,
active_page="gate",
sesh=vars(usr_sesh), sesh=vars(usr_sesh),
).encode("utf-16") ).encode("utf-16")
@@ -148,12 +167,13 @@ class FE_Gate(FE_Base):
ip = Utils.get_ip_addr(request) ip = Utils.get_ip_addr(request)
if uri == "/gate/gate.login": if uri == "/gate/gate.login":
access_code: str = request.args[b"access_code"][0].decode() access_code: str = request.args[b"access-code"][0].decode()
passwd: bytes = request.args[b"passwd"][0] passwd: bytes = request.args[b"passwd"][0]
if passwd == b"": if passwd == b"":
passwd = None passwd = None
uid = self.data.card.get_user_id_from_card(access_code) uid = self.data.card.get_user_id_from_card(access_code)
user = self.data.user.get_user(uid)
if uid is None: if uid is None:
return redirectTo(b"/gate?e=1", request) return redirectTo(b"/gate?e=1", request)
@@ -173,8 +193,10 @@ class FE_Gate(FE_Base):
sesh = request.getSession() sesh = request.getSession()
usr_sesh = IUserSession(sesh) usr_sesh = IUserSession(sesh)
usr_sesh.userId = uid usr_sesh.user_id = uid
usr_sesh.username = user["username"]
usr_sesh.current_ip = ip usr_sesh.current_ip = ip
usr_sesh.permissions = user["permissions"]
return redirectTo(b"/user", request) return redirectTo(b"/user", request)
@@ -192,7 +214,7 @@ class FE_Gate(FE_Base):
hashed = bcrypt.hashpw(passwd, salt) hashed = bcrypt.hashpw(passwd, salt)
result = self.data.user.create_user( result = self.data.user.create_user(
uid, username, email, hashed.decode(), 1 uid, username, email.lower(), hashed.decode(), 1
) )
if result is None: if result is None:
return redirectTo(b"/gate?e=3", request) return redirectTo(b"/gate?e=3", request)
@@ -210,39 +232,179 @@ class FE_Gate(FE_Base):
return redirectTo(b"/gate?e=2", request) return redirectTo(b"/gate?e=2", request)
ac = request.args[b"ac"][0].decode() ac = request.args[b"ac"][0].decode()
card = self.data.card.get_card_by_access_code(ac)
if card is None:
return redirectTo(b"/gate?e=1", request)
user = self.data.user.get_user(card["user"])
if user is None:
self.logger.warning(
f"Card {ac} exists with no/invalid associated user ID {card['user']}"
)
return redirectTo(b"/gate?e=0", request)
if user["password"] is not None:
return redirectTo(b"/gate?e=1", request)
template = self.environment.get_template("core/frontend/gate/create.jinja") template = self.environment.get_template("core/frontend/gate/create.jinja")
return template.render( return template.render(
title=f"{self.core_config.server.name} | Create User", title=f"{self.core_config.server.name} | Create User",
code=ac, code=ac,
sesh={"userId": 0}, active_page="gate",
sesh={"user_id": 0, "permissions": 0},
).encode("utf-16") ).encode("utf-16")
class FE_User(FE_Base): class FE_User(FE_Base):
def render_GET(self, request: Request): def render_GET(self, request: Request):
uri = request.uri.decode()
template = self.environment.get_template("core/frontend/user/index.jinja") template = self.environment.get_template("core/frontend/user/index.jinja")
sesh: Session = request.getSession() sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh) usr_sesh = IUserSession(sesh)
if usr_sesh.userId == 0: if usr_sesh.user_id == 0:
return redirectTo(b"/gate", request) return redirectTo(b"/gate", request)
cards = self.data.card.get_user_cards(usr_sesh.userId) m = re.match("\/user\/(\d*)", uri)
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}) if m is not None:
usrid = m.group(1)
if (
usr_sesh.permissions < 1 << PermissionOffset.USERMOD.value
or not usrid == usr_sesh.user_id
):
return redirectTo(b"/user", request)
else:
usrid = usr_sesh.user_id
user = self.data.user.get_user(usrid)
if user is None:
return redirectTo(b"/user", request)
cards = self.data.card.get_user_cards(usrid)
arcades = self.data.arcade.get_arcades_managed_by_user(usrid)
card_data = []
arcade_data = []
for i, c in enumerate(cards):
if c["is_locked"]:
status = "Locked"
elif c["is_banned"]:
status = "Banned"
else:
status = "Active"
card_data.append({"index": i+1, "access_code": c["access_code"], "status": status})
for i, a in enumerate(arcades):
arcade_data.append({"index": i+1, "id": a["id"], "name": a["name"]})
return template.render( return template.render(
title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh), cards=card_data, username=user['username'] title=f"{self.core_config.server.name} | Account",
sesh=vars(usr_sesh),
cards=card_data,
arcades=arcade_data,
active_page="user",
).encode("utf-16")
def render_POST(self, request: Request):
pass
class FE_System(FE_Base):
def render_GET(self, request: Request):
uri = request.uri.decode()
template = self.environment.get_template("core/frontend/sys/index.jinja")
usrlist = []
aclist = []
cablist = []
sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh)
if (
usr_sesh.user_id == 0
or usr_sesh.permissions < 1 << PermissionOffset.USERMOD.value
):
return redirectTo(b"/gate", request)
if uri.startswith("/sys/lookup.user?"):
uri_parse = parse.parse_qs(
uri.replace("/sys/lookup.user?", "")
) # lop off the first bit
uid_search = uri_parse.get("usrId")
email_search = uri_parse.get("usrEmail")
uname_search = uri_parse.get("usrName")
if uid_search is not None:
u = self.data.user.get_user(uid_search[0])
if u is not None:
usrlist.append(u._asdict())
elif email_search is not None:
u = self.data.user.find_user_by_email(email_search[0])
if u is not None:
usrlist.append(u._asdict())
elif uname_search is not None:
ul = self.data.user.find_user_by_username(uname_search[0])
for u in ul:
usrlist.append(u._asdict())
elif uri.startswith("/sys/lookup.arcade?"):
uri_parse = parse.parse_qs(
uri.replace("/sys/lookup.arcade?", "")
) # lop off the first bit
ac_id_search = uri_parse.get("arcadeId")
ac_name_search = uri_parse.get("arcadeName")
ac_user_search = uri_parse.get("arcadeUser")
if ac_id_search is not None:
u = self.data.arcade.get_arcade(ac_id_search[0])
if u is not None:
aclist.append(u._asdict())
elif ac_name_search is not None:
ul = self.data.arcade.find_arcade_by_name(ac_name_search[0])
for u in ul:
aclist.append(u._asdict())
elif ac_user_search is not None:
ul = self.data.arcade.get_arcades_managed_by_user(ac_user_search[0])
for u in ul:
aclist.append(u._asdict())
elif uri.startswith("/sys/lookup.cab?"):
uri_parse = parse.parse_qs(
uri.replace("/sys/lookup.cab?", "")
) # lop off the first bit
cab_id_search = uri_parse.get("cabId")
cab_serial_search = uri_parse.get("cabSerial")
cab_acid_search = uri_parse.get("cabAcId")
if cab_id_search is not None:
u = self.data.arcade.get_machine(id=cab_id_search[0])
if u is not None:
cablist.append(u._asdict())
elif cab_serial_search is not None:
u = self.data.arcade.get_machine(serial=cab_serial_search[0])
if u is not None:
cablist.append(u._asdict())
elif cab_acid_search is not None:
ul = self.data.arcade.get_arcade_machines(cab_acid_search[0])
for u in ul:
cablist.append(u._asdict())
return template.render(
title=f"{self.core_config.server.name} | System",
sesh=vars(usr_sesh),
usrlist=usrlist,
aclist=aclist,
cablist=cablist,
active_page="sys",
).encode("utf-16") ).encode("utf-16")
@@ -257,3 +419,56 @@ class FE_Game(FE_Base):
def render_GET(self, request: Request) -> bytes: def render_GET(self, request: Request) -> bytes:
return redirectTo(b"/user", request) return redirectTo(b"/user", request)
class FE_Arcade(FE_Base):
def render_GET(self, request: Request):
uri = request.uri.decode()
template = self.environment.get_template("core/frontend/arcade/index.jinja")
managed = []
sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh)
if usr_sesh.user_id == 0:
return redirectTo(b"/gate", request)
m = re.match("\/arcade\/(\d*)", uri)
if m is not None:
arcadeid = m.group(1)
perms = self.data.arcade.get_manager_permissions(usr_sesh.user_id, arcadeid)
arcade = self.data.arcade.get_arcade(arcadeid)
if perms is None:
perms = 0
else:
return redirectTo(b"/user", request)
return template.render(
title=f"{self.core_config.server.name} | Arcade",
sesh=vars(usr_sesh),
error=0,
perms=perms,
arcade=arcade._asdict(),
active_page="arcade",
).encode("utf-16")
class FE_Machine(FE_Base):
def render_GET(self, request: Request):
uri = request.uri.decode()
template = self.environment.get_template("core/frontend/machine/index.jinja")
sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh)
if usr_sesh.user_id == 0:
return redirectTo(b"/gate", request)
return template.render(
title=f"{self.core_config.server.name} | Machine",
sesh=vars(usr_sesh),
arcade={},
error=0,
active_page="machine",
).encode("utf-16")

View File

@@ -0,0 +1,4 @@
{% extends "core/frontend/index.jinja" %}
{% block content %}
<h1>{{ arcade.name }}</h1>
{% endblock content %}

View File

@@ -1,24 +1,44 @@
{% extends "core/frontend/index.jinja" %} {% extends "core/frontend/index.jinja" %}
{% block content %} {% block content %}
<h1>Create User</h1> <div class="row justify-content-md-center form-signin">
<form id="create" style="max-width: 240px; min-width: 10%;" action="/gate/gate.create" method="post"> <div class="col col-lg-4">
<div class="form-group row"> <form id="create" action="/gate/gate.create" method="post">
<label for="access_code">Card Access Code</label><br> <h1 class="h3 mb-3 fw-normal">Sign-up</h1>
<input class="form-control" name="access_code" id="access_code" type="text" placeholder="00000000000000000000" value={{ code }} maxlength="20" readonly>
<div class="form-floating mb-3">
<input class="form-control" name="access-code" id="access-code" type="text"
placeholder="00000000000000000000" value={{ code }} maxlength="20" required>
<label for="access-code">Access Code*</label>
</div> </div>
<div class="form-group row"> <div class="form-floating mb-3">
<label for="username">Username</label><br> <input type="text" class="form-control" name="username" id="username" placeholder="username">
<input id="username" class="form-control" name="username" type="text" placeholder="username"> <label for="username">Username*</label>
</div> </div>
<div class="form-group row"> <div class="form-floating mb-3">
<label for="email">Email</label><br> <input type="email" class="form-control" name="email" id="email" placeholder="example@example.com">
<input id="email" class="form-control" name="email" type="email" placeholder="example@example.com"> <label for="email">E-Mail*</label>
</div> </div>
<div class="form-group row"> <div class="form-floating mb-3">
<label for="passwd">Password</label><br> <input type="password" class="form-control" name="passwd" id="passwd" placeholder="Password">
<input id="passwd" class="form-control" name="passwd" type="password" placeholder="password"> <label for="passwd">Password*</label>
</div> </div>
<p></p>
<input id="submit" class="btn btn-primary" style="display: block; margin: 0 auto;" type="submit" value="Create"> <!--
</form> <div class="form-check text-start my-3">
<input class="form-check-input" type="checkbox" value="remember-me" id="remember-me">
<label class="form-check-label" for="remember-me">
Remember me
</label>
</div>
-->
<div class="alert alert-warning" role="alert">
If you have not registered a card with this server, you cannot create a WebUI account!
</div>
<button class="btn btn-primary w-100 py-2" type="submit">Sign-up</button>
<!-- <p class="mt-5 mb-3 text-body-secondary">© 2023 ARTEMiS</p>-->
</form>
</div>
</div>
{% endblock content %} {% endblock content %}

View File

@@ -1,32 +1,52 @@
{% extends "core/frontend/index.jinja" %} {% extends "core/frontend/index.jinja" %}
{% block content %} {% block content %}
<h1>Gate</h1>
{% include "core/frontend/widgets/err_banner.jinja" %}
<style> <style>
/* Chrome, Safari, Edge, Opera */ .form-signin input[type="text"] {
input::-webkit-outer-spin-button, margin-bottom: -1px;
input::-webkit-inner-spin-button { border-bottom-right-radius: 0;
-webkit-appearance: none; border-bottom-left-radius: 0;
margin: 0;
} }
/* Firefox */ .form-signin input[type="password"] {
input[type=number] { margin-bottom: 10px;
-moz-appearance: textfield; border-top-left-radius: 0;
border-top-right-radius: 0;
} }
</style> </style>
<form id="login" style="max-width: 240px; min-width: 10%;" action="/gate/gate.login" method="post"> <div class="row justify-content-md-center form-signin">
<div class="form-group row"> <div class="col col-lg-4">
<label for="access_code">Card Access Code</label><br> <form id="login" action="/gate/gate.login" method="post">
<input form="login" class="form-control" name="access_code" id="access_code" type="number" placeholder="00000000000000000000" maxlength="20" required> <h1 class="h3 mb-3 fw-normal">Login</h1>
{% include "core/frontend/widgets/err_banner.jinja" %}
<div class="form-floating">
<input form="login" class="form-control" name="access-code" id="access-code" type="text"
placeholder="00000000000000000000" maxlength="20" required>
<label for="access-code">Access Code</label>
</div> </div>
<div class="form-group row"> <div class="form-floating">
<label for="passwd">Password</label><br> <input form="login" type="password" class="form-control" name="passwd" id="passwd" placeholder="Password">
<input id="passwd" class="form-control" name="passwd" type="password" placeholder="password"> <label for="passwd">Password</label>
</div> </div>
<p></p>
<input id="submit" class="btn btn-primary" style="display: block; margin: 0 auto;" form="login" type="submit" value="Login"> <!--
</form> <div class="form-check text-start my-3">
<h6>*To register for the webui, type in the access code of your card, as shown in a game, and leave the password field blank.</h6> <input class="form-check-input" type="checkbox" value="remember-me" id="remember-me">
<h6>*If you have not registered a card with this server, you cannot create a webui account.</h6> <label class="form-check-label" for="remember-me">
Remember me
</label>
</div>
-->
<div class="alert alert-info" role="alert">
To register for the WebUI, type in the <strong>access code</strong> of your card, as shown in a game, and leave the
password field blank.
</div>
<button class="btn btn-primary w-100 py-2" type="submit">Sign in</button>
<!-- <p class="mt-5 mb-3 text-body-secondary">© 2023 ARTEMiS</p>-->
</form>
</div>
</div>
{% endblock content %} {% endblock content %}

View File

@@ -1,88 +1,25 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en" data-bs-theme="dark">
<head>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title> <title>{{ title }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet"
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script> integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">
<style> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
html { </head>
background-color: #181a1b !important;
margin: 10px; <body>
}
html {
color-scheme: dark !important;
}
html, body, input, textarea, select, button, dialog {
background-color: #181a1b;
}
html, body, input, textarea, select, button {
border-color: #736b5e;
color: #e8e6e3;
}
a {
color: #3391ff;
}
table {
border-color: #545b5e;
}
::placeholder {
color: #b2aba1;
}
input:-webkit-autofill,
textarea:-webkit-autofill,
select:-webkit-autofill {
background-color: #404400 !important;
color: #e8e6e3 !important;
}
::-webkit-scrollbar {
background-color: #202324;
color: #aba499;
}
::-webkit-scrollbar-thumb {
background-color: #454a4d;
}
::-webkit-scrollbar-thumb:hover {
background-color: #575e62;
}
::-webkit-scrollbar-thumb:active {
background-color: #484e51;
}
::-webkit-scrollbar-corner {
background-color: #181a1b;
}
* {
scrollbar-color: #454a4d #202324;
}
::selection {
background-color: #004daa !important;
color: #e8e6e3 !important;
}
::-moz-selection {
background-color: #004daa !important;
color: #e8e6e3 !important;
}
input[type="text"], input[type="text"]:focus, input[type="password"], input[type="password"]:focus, input[type="email"], input[type="email"]:focus {
background-color: #202324 !important;
color: #e8e6e3;
}
form {
outline: 1px solid grey;
padding: 20px;
padding-top: 10px;
padding-bottom: 10px;
}
.err-banner {
background-color: #AA0000;
padding: 20px;
margin-bottom: 10px;
width: 15%;
}
</style>
</head>
<body>
{% include "core/frontend/widgets/topbar.jinja" %} {% include "core/frontend/widgets/topbar.jinja" %}
<div class="container">
{% block content %} {% block content %}
<h1>{{ server_name }}</h1> <h1>{{ server_name }}</h1>
{% endblock content %} {% endblock content %}
</body> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm"
crossorigin="anonymous"></script>
</body>
</html> </html>

View File

@@ -0,0 +1,5 @@
{% extends "core/frontend/index.jinja" %}
{% block content %}
{% include "core/frontend/widgets/err_banner.jinja" %}
<h1>Machine Management</h1>
{% endblock content %}

View File

@@ -0,0 +1,98 @@
{% extends "core/frontend/index.jinja" %}
{% block content %}
<h1>System Management</h1>
<div class="row" id="rowForm">
{% if sesh.permissions >= 2 %}
<div class="col-sm-6" style="max-width: 25%;">
<form id="usrLookup" name="usrLookup" action="/sys/lookup.user" class="form-inline">
<h3>User Search</h3>
<div class="form-group">
<label for="usrEmail">Email address</label>
<input type="email" class="form-control" id="usrEmail" name="usrEmail" aria-describedby="emailHelp">
</div>
OR
<div class="form-group">
<label for="usrName">Username</label>
<input type="text" class="form-control" id="usrName" name="usrName">
</div>
OR
<div class="form-group">
<label for="usrId">User ID</label>
<input type="number" class="form-control" id="usrId" name="usrId">
</div>
<br />
<button type="submit" class="btn btn-primary">Search</button>
</form>
</div>
{% endif %}
{% if sesh.permissions >= 4 %}
<div class="col-sm-6" style="max-width: 25%;">
<form id="arcadeLookup" name="arcadeLookup" action="/sys/lookup.arcade" class="form-inline" >
<h3>Arcade Search</h3>
<div class="form-group">
<label for="arcadeName">Arcade Name</label>
<input type="text" class="form-control" id="arcadeName" name="arcadeName">
</div>
OR
<div class="form-group">
<label for="arcadeId">Arcade ID</label>
<input type="number" class="form-control" id="arcadeId" name="arcadeId">
</div>
OR
<div class="form-group">
<label for="arcadeUser">Owner User ID</label>
<input type="number" class="form-control" id="arcadeUser" name="arcadeUser">
</div>
<br />
<button type="submit" class="btn btn-primary">Search</button>
</form>
</div>
<div class="col-sm-6" style="max-width: 25%;">
<form id="cabLookup" name="cabLookup" action="/sys/lookup.cab" class="form-inline" >
<h3>Machine Search</h3>
<div class="form-group">
<label for="cabSerial">Machine Serial</label>
<input type="text" class="form-control" id="cabSerial" name="cabSerial">
</div>
OR
<div class="form-group">
<label for="cabId">Machine ID</label>
<input type="number" class="form-control" id="cabId" name="cabId">
</div>
OR
<div class="form-group">
<label for="cabAcId">Arcade ID</label>
<input type="number" class="form-control" id="cabAcId" name="cabAcId">
</div>
<br />
<button type="submit" class="btn btn-primary">Search</button>
</form>
</div>
{% endif %}
</div>
<div class="row" id="rowResult" style="margin: 10px;">
{% if sesh.permissions >= 2 %}
<div id="userSearchResult" class="col-sm-6" style="max-width: 25%;">
{% for usr in usrlist %}
<pre><a href=/user/{{ usr.id }}>{{ usr.id }} | {{ usr.username }}</a></pre>
{% endfor %}
</div>
{% endif %}
{% if sesh.permissions >= 4 %}
<div id="arcadeSearchResult" class="col-sm-6" style="max-width: 25%;">
{% for ac in aclist %}
<pre><a href=/arcade/{{ ac.id }}>{{ ac.id }} | {{ ac.name }}</a></pre>
{% endfor %}
</div
><div id="cabSearchResult" class="col-sm-6" style="max-width: 25%;">
{% for cab in cablist %}
<a href=/cab/{{ cab.id }}><pre>{{ cab.id }} | {{ cab.game if cab.game is defined else "ANY " }} | {{ cab.serial }}</pre></a>
{% endfor %}
</div>
{% endif %}
</div>
<div class="row" id="rowAdd">
</div>
{% endblock content %}

View File

@@ -1,28 +1,123 @@
{% extends "core/frontend/index.jinja" %} {% extends "core/frontend/index.jinja" %}
{% block content %} {% block content %}
<h1>Management for {{ username }}</h1> <h1>{{ sesh["username"] }}'s Account</h1>
<h2>Cards <button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#card_add">Add</button></h2>
<ul> <div class="card mb-3">
{% for c in cards %} <div class="card-body">
<li>{{ c.access_code }}: {{ c.status }} <button class="btn-danger btn">Delete</button></li> <h3 class="card-title">Cards</h3>
{% endfor %} <!--<h4 class="card-subtitle mb-2 text-body-secondary">Card subtitle</h4>-->
</ul> <p class="card-text">All aime cards are listed here for the given user.</p>
<div class="modal fade" id="card_add" tabindex="-1" aria-labelledby="card_add_label" aria-hidden="true"> <a href="#" data-bs-toggle="modal" data-bs-target="#card-add" class="btn btn-primary mb-3">Add Card</a>
{% if cards is defined and cards|length > 0 %}
<div class="container">
<div class="row">
<div class="col-12">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Access Code</th>
<th scope="col">Status</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for c in cards %}
<tr class="align-middle">
<th scope="row">{{ c.index }}</th>
<td>{{ c.access_code }}</td>
<td>
{% if c.status == 'Active'%}
<span class="badge rounded-pill text-bg-success">Active</span>
{% elif c.status == 'Locked' %}
<span class="badge rounded-pill text-bg-warning">Locked</span>
{% endif %}
</td>
<td>
{% if c.status == 'Active'%}
<button type="button" class="btn btn-warning btn-sm">Lock</i></button>
{% elif c.status == 'Locked' %}
<button type="button" class="btn btn-success btn-sm">Unlock</i></button>
{% endif %}
<button type="button" class="btn btn-danger btn-sm">Delete</i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="alert alert-warning" role="alert">
You have no cards registered to your account. Please add one.
</div>
{% endif %}
<!--<a href="#" data-bs-toggle="modal" data-bs-target="#card-add" class="card-link">Add Card</a>-->
</div>
</div>
<div class="card">
<div class="card-body">
<h3 class="card-title">Arcades</h3>
<!--<h4 class="card-subtitle mb-2 text-body-secondary">Card subtitle</h4>-->
<p class="card-text">All arcades connected to the given account are listed here.</p>
{% if arcades is defined and arcades|length > 0 %}
<div class="container">
<div class="row">
<div class="col-12">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
</tr>
</thead>
<tbody>
{% for a in arcades %}
<tr class="align-middle clickable-row" data-href=/arcade/{{ a.id }}>
<th scope="row">{{ a.index }}</th>
<td>{{ a.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="alert alert-info" role="alert">
You have no arcades connected to your account.
</div>
{% endif %}
</div>
</div>
<div class="modal fade" id="card-add" tabindex="-1" aria-labelledby="card-add-label" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h1 class="modal-title fs-5" id="card_add_label">Add Card</h1> <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> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
HOW TO:<br> <h3 class="fs-5">How to:</h3>
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> Scan your card on any networked game and press the "View Access Code" button (varies by game) and enter
!!FOR AMUSEIC CARDS: DO NOT ENTER THE CODE SHOWN ON THE BACK OF THE CARD ITSELF OR IT WILL NOT WORK!! the 20
<p /><label for="card_add_frm_access_code">Access Code:&nbsp;</label><input id="card_add_frm_access_code" maxlength="20" type="text" required> digit code below.
<hr>
FOR AMUSE IC CARDS: DO NOT ENTER THE CODE SHOWN ON THE BACK OF THE CARD ITSELF OR IT WILL NOT WORK!
<form>
<div class="form-floating mt-3">
<label for="access-code" class="col-form-label">Access Code</label>
<input maxlength="20" type="text" required pattern="[0-9]{20}" class="form-control"
id="access-code">
</div>
</form>
</div> </div>
<div class="modal-footer"> <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> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Add</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,12 +1,15 @@
{% if error > 0 %} {% if error > 0 %}
<div class="err-banner"> <div class="alert alert-danger" role="alert">
<h3>Error</h3>
{% if error == 1 %} {% if error == 1 %}
Card not registered, or wrong password Card not registered, or wrong password
{% elif error == 2 %} {% elif error == 2 %}
Missing or malformed access code Missing or malformed access code
{% elif error == 3 %} {% elif error == 3 %}
Failed to create user Failed to create user
{% elif error == 4 %}
Arcade not found
{% elif error == 5 %}
Machine not found
{% else %} {% else %}
An unknown error occoured An unknown error occoured
{% endif %} {% endif %}

View File

@@ -1,18 +1,56 @@
<div style="background: #333; color: #f9f9f9; width: 10%; height: 50px; line-height: 50px; text-align: center; float: left;"> <nav class="p-3 mb-3 navbar navbar-expand-lg bg-body-tertiary" aria-label="Thirteenth navbar example">
Navigation <div class="container">
</div> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarsExample11"
<div style="background: #333; color: #f9f9f9; width: 80%; height: 50px; line-height: 50px; padding-left: 10px; float: left;"> aria-controls="navbarsExample11" aria-expanded="false" aria-label="Toggle navigation">
<a href=/><button class="btn btn-primary">Home</button></a>&nbsp; <span class="navbar-toggler-icon"></span>
{% for game in game_list %} </button>
<a href=/game/{{ game.url }}><button class="btn btn-success">{{ game.name }}</button></a>&nbsp;
{% endfor %}
</div>
</div>
<div style="background: #333; color: #f9f9f9; width: 10%; height: 50px; line-height: 50px; text-align: center; float: left;">
{% if sesh is defined and sesh["userId"] > 0 %}
<a href="/user"><button class="btn btn-primary">Account</button></a>
{% else %}
<a href="/gate"><button class="btn btn-primary">Gate</button></a>
{% endif %}
</div> <div class="collapse navbar-collapse d-lg-flex" id="navbarsExample11">
<a class="navbar-brand col-lg-3 me-0" href= />ARTEMiS</a>
<ul class="navbar-nav col-lg-6 justify-content-lg-center nav-pills">
<li class="nav-item">
<a class="nav-link {% if active_page == '/' %}active{% endif %}" aria-current="page" href= />Home</a>
</li>
{% for game in game_list %}
<li class="nav-item">
<a class="nav-link {% if active_page == game.url %}active{% endif %}" href=/game/{{ game.url }}>{{ game.name }}</a>
</li>
{% endfor %}
<!--
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown"
aria-expanded="false">Dropdown</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li>
-->
</ul>
<div class="d-lg-flex col-lg-3 justify-content-lg-end">
{% if sesh is defined and sesh["user_id"] > 0 %}
<div class="btn-group dropdown">
<a href="#" class="d-block link-body-emphasis text-decoration-none dropdown-toggle"
data-bs-toggle="dropdown" aria-expanded="false">
{{ sesh["username"] }}
</a>
<ul class="dropdown-menu text-small">
{% if sesh["permissions"] >= 2 %}
<li><a class="dropdown-item {% if active_page == 'sys' %}active{% endif %}" href="/sys">System</a></li>
{% endif %}
<li><a class="dropdown-item" href="#">Settings</a></li>
<li><a class="dropdown-item {% if active_page == 'user' %}active{% endif %}" href="/user">Account</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li><a class="dropdown-item" href="#">Sign out</a></li>
</ul>
</div>
{% else %}
<a href="/gate"><button class="btn {% if active_page == 'gate' %}btn-primary{% else %}btn-outline-primary{% endif %}">Login</button></a>
{% endif %}
</div>
</div>
</div>
</nav>

View File

@@ -4,6 +4,7 @@ from logging.handlers import TimedRotatingFileHandler
from twisted.web import resource from twisted.web import resource
from twisted.web.http import Request from twisted.web.http import Request
from datetime import datetime from datetime import datetime
from Crypto.Cipher import Blowfish
import pytz import pytz
from core import CoreConfig from core import CoreConfig
@@ -33,8 +34,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()
@@ -56,17 +57,24 @@ class MuchaServlet:
self.logger.error( self.logger.error(
f"Error processing mucha request {request.content.getvalue()}" f"Error processing mucha request {request.content.getvalue()}"
) )
return b"" return b"RESULTS=000"
req = MuchaAuthRequest(req_dict) req = MuchaAuthRequest(req_dict)
self.logger.debug(f"Mucha request {vars(req)}")
self.logger.info(f"Boardauth request from {client_ip} for {req.gameVer}") self.logger.info(f"Boardauth request from {client_ip} for {req.gameVer}")
self.logger.debug(f"Mucha request {vars(req)}")
if req.gameCd not in self.mucha_registry: if req.gameCd not in self.mucha_registry:
self.logger.warn(f"Unknown gameCd {req.gameCd}") self.logger.warning(f"Unknown gameCd {req.gameCd}")
return b"" return b"RESULTS=000"
# TODO: Decrypt S/N # TODO: Decrypt S/N
b_key = b""
for x in range(8):
b_key += req.sendDate[(x - 1) & 7].encode()
cipher = Blowfish.new(b_key, Blowfish.MODE_ECB)
sn_decrypt = cipher.decrypt(bytes.fromhex(req.serialNum))
self.logger.debug(f"Decrypt SN to {sn_decrypt.hex()}")
resp = MuchaAuthResponse( resp = MuchaAuthResponse(
f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}" f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}"
@@ -84,22 +92,37 @@ class MuchaServlet:
self.logger.error( self.logger.error(
f"Error processing mucha request {request.content.getvalue()}" f"Error processing mucha request {request.content.getvalue()}"
) )
return b"" return b"RESULTS=000"
req = MuchaUpdateRequest(req_dict) req = MuchaUpdateRequest(req_dict)
self.logger.debug(f"Mucha request {vars(req)}")
self.logger.info(f"Updatecheck request from {client_ip} for {req.gameVer}") self.logger.info(f"Updatecheck request from {client_ip} for {req.gameVer}")
self.logger.debug(f"Mucha request {vars(req)}")
if req.gameCd not in self.mucha_registry: if req.gameCd not in self.mucha_registry:
self.logger.warn(f"Unknown gameCd {req.gameCd}") self.logger.warning(f"Unknown gameCd {req.gameCd}")
return b"" return b"RESULTS=000"
resp = MuchaUpdateResponseStub(req.gameVer) resp = MuchaUpdateResponse(req.gameVer, f"{self.config.mucha.hostname}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}")
self.logger.debug(f"Mucha response {vars(resp)}") self.logger.debug(f"Mucha response {vars(resp)}")
return self.mucha_postprocess(vars(resp)) return self.mucha_postprocess(vars(resp))
def handle_dlstate(self, request: Request, _: Dict) -> bytes:
req_dict = self.mucha_preprocess(request.content.getvalue())
client_ip = Utils.get_ip_addr(request)
if req_dict is None:
self.logger.error(
f"Error processing mucha request {request.content.getvalue()}"
)
return b""
req = MuchaDownloadStateRequest(req_dict)
self.logger.info(f"DownloadState request from {client_ip} for {req.gameCd} -> {req.updateVer}")
self.logger.debug(f"request {vars(req)}")
return b"RESULTS=001"
def mucha_preprocess(self, data: bytes) -> Optional[Dict]: def mucha_preprocess(self, data: bytes) -> Optional[Dict]:
try: try:
ret: Dict[str, Any] = {} ret: Dict[str, Any] = {}
@@ -111,19 +134,17 @@ class MuchaServlet:
return ret return ret
except: except Exception:
self.logger.error(f"Error processing mucha request {data}") self.logger.error(f"Error processing mucha request {data}")
return None return None
def mucha_postprocess(self, data: dict) -> Optional[bytes]: def mucha_postprocess(self, data: dict) -> Optional[bytes]:
try: try:
urlencode = "" urlencode = "&".join(f"{k}={v}" for k, v in data.items())
for k, v in data.items():
urlencode += f"{k}={v}&"
return urlencode.encode() return urlencode.encode()
except: except Exception:
self.logger.error("Error processing mucha response") self.logger.error("Error processing mucha response")
return None return None
@@ -203,21 +224,120 @@ class MuchaUpdateRequest:
class MuchaUpdateResponse: class MuchaUpdateResponse:
def __init__(self, game_ver: str, mucha_url: str) -> None: def __init__(self, game_ver: str, mucha_url: str) -> None:
self.RESULTS = "001" self.RESULTS = "001"
self.EXE_VER = game_ver
self.UPDATE_VER_1 = game_ver self.UPDATE_VER_1 = game_ver
self.UPDATE_URL_1 = f"https://{mucha_url}/updUrl1/" self.UPDATE_URL_1 = f"http://{mucha_url}/updUrl1/"
self.UPDATE_SIZE_1 = "0" self.UPDATE_SIZE_1 = "20"
self.UPDATE_CRC_1 = "0000000000000000"
self.CHECK_URL_1 = f"https://{mucha_url}/checkUrl/" self.CHECK_CRC_1 = "0000000000000000"
self.EXE_VER_1 = game_ver self.CHECK_URL_1 = f"http://{mucha_url}/checkUrl/"
self.CHECK_SIZE_1 = "20"
self.INFO_SIZE_1 = "0" self.INFO_SIZE_1 = "0"
self.COM_SIZE_1 = "0" self.COM_SIZE_1 = "0"
self.COM_TIME_1 = "0" self.COM_TIME_1 = "0"
self.LAN_INFO_SIZE_1 = "0" self.LAN_INFO_SIZE_1 = "0"
self.USER_ID = "" self.USER_ID = ""
self.PASSWORD = "" self.PASSWORD = ""
"""
RESULTS
EXE_VER
UPDATE_VER_%d
UPDATE_URL_%d
UPDATE_SIZE_%d
CHECK_CRC_%d
CHECK_URL_%d
CHECK_SIZE_%d
INFO_SIZE_1
COM_SIZE_1
COM_TIME_1
LAN_INFO_SIZE_1
USER_ID
PASSWORD
"""
class MuchaUpdateResponseStub: class MuchaUpdateResponseStub:
def __init__(self, game_ver: str) -> None: def __init__(self, game_ver: str) -> None:
self.RESULTS = "001" self.RESULTS = "001"
self.UPDATE_VER_1 = game_ver self.UPDATE_VER_1 = game_ver
class MuchaDownloadStateRequest:
def __init__(self, request: Dict) -> None:
self.gameCd = request.get("gameCd", "")
self.updateVer = request.get("updateVer", "")
self.serialNum = request.get("serialNum", "")
self.fileSize = request.get("fileSize", "")
self.compFileSize = request.get("compFileSize", "")
self.boardId = request.get("boardId", "")
self.placeId = request.get("placeId", "")
self.storeRouterIp = request.get("storeRouterIp", "")
class MuchaDownloadErrorRequest:
def __init__(self, request: Dict) -> None:
self.gameCd = request.get("gameCd", "")
self.updateVer = request.get("updateVer", "")
self.serialNum = request.get("serialNum", "")
self.downloadUrl = request.get("downloadUrl", "")
self.errCd = request.get("errCd", "")
self.errMessage = request.get("errMessage", "")
self.boardId = request.get("boardId", "")
self.placeId = request.get("placeId", "")
self.storeRouterIp = request.get("storeRouterIp", "")
class MuchaRegiAuthRequest:
def __init__(self, request: Dict) -> None:
self.gameCd = request.get("gameCd", "")
self.serialNum = request.get("serialNum", "") # Encrypted
self.countryCd = request.get("countryCd", "")
self.registrationCd = request.get("registrationCd", "")
self.sendDate = request.get("sendDate", "")
self.useToken = request.get("useToken", "")
self.allToken = request.get("allToken", "")
self.placeId = request.get("placeId", "")
self.storeRouterIp = request.get("storeRouterIp", "")
class MuchaRegiAuthResponse:
def __init__(self) -> None:
self.RESULTS = "001" # 001 = success, 099, 098, 097 = fail, others = fail
self.ALL_TOKEN = "0" # Encrypted
self.ADD_TOKEN = "0" # Encrypted
class MuchaTokenStateRequest:
def __init__(self, request: Dict) -> None:
self.gameCd = request.get("gameCd", "")
self.serialNum = request.get("serialNum", "")
self.countryCd = request.get("countryCd", "")
self.useToken = request.get("useToken", "")
self.allToken = request.get("allToken", "")
self.placeId = request.get("placeId", "")
self.storeRouterIp = request.get("storeRouterIp", "")
class MuchaTokenStateResponse:
def __init__(self) -> None:
self.RESULTS = "001"
class MuchaTokenMarginStateRequest:
def __init__(self, request: Dict) -> None:
self.gameCd = request.get("gameCd", "")
self.serialNum = request.get("serialNum", "")
self.countryCd = request.get("countryCd", "")
self.placeId = request.get("placeId", "")
self.limitLowerToken = request.get("limitLowerToken", 0)
self.limitUpperToken = request.get("limitUpperToken", 0)
self.settlementMonth = request.get("settlementMonth", 0)
class MuchaTokenMarginStateResponse:
def __init__(self) -> None:
self.RESULTS = "001"
self.LIMIT_LOWER_TOKEN = 0
self.LIMIT_UPPER_TOKEN = 0
self.LAST_SETTLEMENT_MONTH = 0
self.LAST_LIMIT_LOWER_TOKEN = 0
self.LAST_LIMIT_UPPER_TOKEN = 0
self.SETTLEMENT_MONTH = 0

View File

@@ -62,7 +62,7 @@ class TitleServlet:
self.title_registry[code] = handler_cls self.title_registry[code] = handler_cls
else: else:
self.logger.warn(f"Game {folder} has no get_allnet_info") self.logger.warning(f"Game {folder} has no get_allnet_info")
else: else:
self.logger.error(f"{folder} missing game_code or index in __init__.py") self.logger.error(f"{folder} missing game_code or index in __init__.py")
@@ -74,28 +74,28 @@ class TitleServlet:
def render_GET(self, request: Request, endpoints: dict) -> bytes: def render_GET(self, request: Request, endpoints: dict) -> bytes:
code = endpoints["game"] code = endpoints["game"]
if code not in self.title_registry: if code not in self.title_registry:
self.logger.warn(f"Unknown game code {code}") self.logger.warning(f"Unknown game code {code}")
request.setResponseCode(404) request.setResponseCode(404)
return b"" return b""
index = self.title_registry[code] index = self.title_registry[code]
if not hasattr(index, "render_GET"): if not hasattr(index, "render_GET"):
self.logger.warn(f"{code} does not dispatch GET") self.logger.warning(f"{code} does not dispatch GET")
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"]
if code not in self.title_registry: if code not in self.title_registry:
self.logger.warn(f"Unknown game code {code}") self.logger.warning(f"Unknown game code {code}")
request.setResponseCode(404) request.setResponseCode(404)
return b"" return b""
index = self.title_registry[code] index = self.title_registry[code]
if not hasattr(index, "render_POST"): if not hasattr(index, "render_POST"):
self.logger.warn(f"{code} does not dispatch POST") self.logger.warning(f"{code} does not dispatch POST")
request.setResponseCode(405) request.setResponseCode(405)
return b"" return b""

View File

@@ -56,10 +56,10 @@ if __name__ == "__main__":
elif args.action == "upgrade" or args.action == "rollback": elif args.action == "upgrade" or args.action == "rollback":
if args.version is None: if args.version is None:
data.logger.warn("No version set, upgrading to latest") data.logger.warning("No version set, upgrading to latest")
if args.game is None: if args.game is None:
data.logger.warn("No game set, upgrading core schema") data.logger.warning("No game set, upgrading core schema")
data.migrate_database( data.migrate_database(
"CORE", "CORE",
int(args.version) if args.version is not None else None, int(args.version) if args.version is not None else None,
@@ -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")

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

@@ -54,7 +54,7 @@ Games listed below have been tested and confirmed working.
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:
```shell ```shell
python read.py --series SDBT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder python read.py --game SDBT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
``` ```
The importer for Chunithm will import: Events, Music, Charge Items and Avatar Accesories. The importer for Chunithm will import: Events, Music, Charge Items and Avatar Accesories.
@@ -105,7 +105,7 @@ After a failed Online Battle the room will be deleted. The host is used for the
In order to use the importer you need to use the provided `Export.csv` file: In order to use the importer you need to use the provided `Export.csv` file:
```shell ```shell
python read.py --series SDCA --version <version ID> --binfolder titles/cxb/data python read.py --game SDCA --version <version ID> --binfolder titles/cxb/data
``` ```
The importer for crossbeats REV. will import Music. The importer for crossbeats REV. will import Music.
@@ -127,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 --game <Game Code> --version <Version ID> --binfolder /path/to/StreamingAssets --optfolder /path/to/game/option/folder
```
Pre-DX:
```shell
python read.py --game <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
@@ -157,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
@@ -173,7 +196,7 @@ python dbutils.py --game SDEZ upgrade
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:
```shell ```shell
python read.py --series SBZV --version <version ID> --binfolder /path/to/game/data/diva --optfolder /path/to/game/data/diva/mdata python read.py --game SBZV --version <version ID> --binfolder /path/to/game/data/diva --optfolder /path/to/game/data/diva/mdata
``` ```
The importer for Project Diva Arcade will all required data in order to use The importer for Project Diva Arcade will all required data in order to use
@@ -207,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
@@ -220,7 +243,7 @@ python dbutils.py --game SBZV upgrade
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:
```shell ```shell
python read.py --series SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder python read.py --game SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
``` ```
The importer for O.N.G.E.K.I. will all all Cards, Music and Events. The importer for O.N.G.E.K.I. will all all Cards, Music and Events.
@@ -262,12 +285,12 @@ python dbutils.py --game SDDT upgrade
* Card Maker 1.30: * 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 SUN: Yes (NEW PLUS!! up to A032) * CHUNITHM SUN: Yes (NEW PLUS!! up to A032)
* maimai DX FESTiVAL: Yes (up to A035) (UNiVERSE PLUS up to A031) * 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
@@ -276,19 +299,19 @@ In order to use the importer you need to use the provided `.csv` files (which ar
option folders: option folders:
```shell ```shell
python read.py --series SDED --version <version ID> --binfolder titles/cm/cm_data --optfolder /path/to/cardmaker/option/folder python read.py --game SDED --version <version ID> --binfolder titles/cm/cm_data --optfolder /path/to/cardmaker/option/folder
``` ```
**If you haven't already executed the O.N.G.E.K.I. importer, make sure you import all cards!** **If you haven't already executed the O.N.G.E.K.I. importer, make sure you import all cards!**
```shell ```shell
python read.py --series SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder python read.py --game SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
``` ```
Also make sure to import all maimai DX 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 --game 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 DX/CHUNITHM) and the hardcoded The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai DX/CHUNITHM) and the hardcoded
@@ -324,6 +347,14 @@ version:
ongeki: 1.35.03 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. ### 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
@@ -373,7 +404,7 @@ Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded fo
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:
```shell ```shell
python read.py --series SDFE --version <version ID> --binfolder /path/to/game/WindowsNoEditor/Mercury/Content python read.py --game SDFE --version <version ID> --binfolder /path/to/game/WindowsNoEditor/Mercury/Content
``` ```
The importer for WACCA will import all Music data. The importer for WACCA will import all Music data.
@@ -398,6 +429,41 @@ Always make sure your database (tables) are up-to-date, to do so go to the `core
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 ## SAO
### SDEW ### SDEW
@@ -412,7 +478,7 @@ python dbutils.py --game SDFE upgrade
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:
```shell ```shell
python read.py --series SDEW --version <version ID> --binfolder /path/to/game/extractedassets python read.py --game SDEW --version <version ID> --binfolder /path/to/game/extractedassets
``` ```
The importer for SAO will import all items, heroes, support skills and titles data. The importer for SAO will import all items, heroes, support skills and titles data.
@@ -436,6 +502,13 @@ Always make sure your database (tables) are up-to-date, to do so go to the `core
python dbutils.py --game SDEW upgrade 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: ### Credits for SAO support:
- Midorica - Limited Network Support - Midorica - Limited Network Support

View File

@@ -4,7 +4,10 @@ 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"
check_arcade_ip: False
strict_ip_checking: False
title: title:
loglevel: "info" loglevel: "info"
@@ -31,6 +34,7 @@ frontend:
allnet: allnet:
loglevel: "info" loglevel: "info"
port: 80 port: 80
ip_check: False
allow_online_updates: False allow_online_updates: False
update_cfg_folder: "" update_cfg_folder: ""

View File

@@ -1,3 +1,14 @@
server: server:
enable: True enable: True
loglevel: "info" loglevel: "info"
deliver:
enable: False
udbdl_enable: False
content_folder: ""
uploads:
photos: False
photos_dir: ""
movies: False
movies_dir: ""

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

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):
@@ -36,7 +36,7 @@ class HttpDispatcher(resource.Resource):
self.map_post.connect( self.map_post.connect(
"allnet_downloadorder_report", "allnet_downloadorder_report",
"/dl/report", "/report-api/Report",
controller="allnet", controller="allnet",
action="handle_dlorder_report", action="handle_dlorder_report",
conditions=dict(method=["POST"]), conditions=dict(method=["POST"]),
@@ -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",
@@ -78,6 +99,7 @@ class HttpDispatcher(resource.Resource):
conditions=dict(method=["POST"]), conditions=dict(method=["POST"]),
) )
# Maintain compatability
self.map_post.connect( self.map_post.connect(
"mucha_boardauth", "mucha_boardauth",
"/mucha/boardauth.do", "/mucha/boardauth.do",
@@ -92,6 +114,35 @@ class HttpDispatcher(resource.Resource):
action="handle_updatecheck", action="handle_updatecheck",
conditions=dict(method=["POST"]), conditions=dict(method=["POST"]),
) )
self.map_post.connect(
"mucha_dlstate",
"/mucha/downloadstate.do",
controller="mucha",
action="handle_dlstate",
conditions=dict(method=["POST"]),
)
self.map_post.connect(
"mucha_boardauth",
"/mucha_front/boardauth.do",
controller="mucha",
action="handle_boardauth",
conditions=dict(method=["POST"]),
)
self.map_post.connect(
"mucha_updatacheck",
"/mucha_front/updatacheck.do",
controller="mucha",
action="handle_updatecheck",
conditions=dict(method=["POST"]),
)
self.map_post.connect(
"mucha_dlstate",
"/mucha_front/downloadstate.do",
controller="mucha",
action="handle_dlstate",
conditions=dict(method=["POST"]),
)
self.map_get.connect( self.map_get.connect(
"title_get", "title_get",
@@ -111,7 +162,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 +211,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.warning(f"None returned by controller for {request.uri.decode()} endpoint")
return b""
else: else:
self.logger.warning(f"Unknown data type returned by controller for {request.uri.decode()} endpoint")
return b"" return b""
@@ -256,4 +313,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

@@ -43,11 +43,11 @@ class BaseReader:
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Import Game Information") parser = argparse.ArgumentParser(description="Import Game Information")
parser.add_argument( parser.add_argument(
"--series", "--game",
action="store", action="store",
type=str, type=str,
required=True, required=True,
help="The game series we are importing.", help="The game we are importing.",
) )
parser.add_argument( parser.add_argument(
"--version", "--version",
@@ -109,7 +109,7 @@ if __name__ == "__main__":
logger.setLevel(log_lv) logger.setLevel(log_lv)
coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str) coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str)
if args.series is None or args.version is None: if args.game is None or args.version is None:
logger.error("Game or version not specified") logger.error("Game or version not specified")
parser.print_help() parser.print_help()
exit(1) exit(1)
@@ -134,7 +134,7 @@ if __name__ == "__main__":
titles = Utils.get_all_titles() titles = Utils.get_all_titles()
for dir, mod in titles.items(): for dir, mod in titles.items():
if args.series in mod.game_codes: if args.game in mod.game_codes:
handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra) handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra)
handler.read() handler.read()

View File

@@ -21,7 +21,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
+ 1.35 + 1.35
+ O.N.G.E.K.I. + O.N.G.E.K.I.
+ All versions up to Bright Memory + All versions up to bright MEMORY
+ WACCA + WACCA
+ Lily R + Lily R
@@ -30,6 +30,9 @@ Games listed below have been tested and confirmed working. Only game versions ol
+ POKKÉN TOURNAMENT + 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,5 @@ Routes
bcrypt bcrypt
jinja2 jinja2
protobuf protobuf
autobahn
pillow

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
@@ -73,7 +73,7 @@ class ChuniBase:
# 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.warning(
f"No bonus entries found for bonus preset {preset['presetId']}" f"No bonus entries found for bonus preset {preset['presetId']}"
) )
continue continue
@@ -149,7 +149,7 @@ class ChuniBase:
game_events = self.data.static.get_enabled_events(self.version) game_events = self.data.static.get_enabled_events(self.version)
if game_events is None or len(game_events) == 0: if game_events is None or len(game_events) == 0:
self.logger.warn("No enabled events, did you run the reader?") self.logger.warning("No enabled events, did you run the reader?")
return { return {
"type": data["type"], "type": data["type"],
"length": 0, "length": 0,
@@ -401,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")
@@ -644,7 +644,7 @@ class ChuniBase:
upsert["userData"][0]["userName"] = self.read_wtf8( upsert["userData"][0]["userName"] = self.read_wtf8(
upsert["userData"][0]["userName"] upsert["userData"][0]["userName"]
) )
except: except Exception:
pass pass
self.data.profile.put_profile_data( self.data.profile.put_profile_data(

View File

@@ -67,7 +67,7 @@ class ChuniReader(BaseReader):
if result is not None: if result is not None:
self.logger.info(f"Inserted login bonus preset {id}") self.logger.info(f"Inserted login bonus preset {id}")
else: else:
self.logger.warn(f"Failed to insert login bonus preset {id}") self.logger.warning(f"Failed to insert login bonus preset {id}")
for bonus in xml_root.find("infos").findall("LoginBonusDataInfo"): for bonus in xml_root.find("infos").findall("LoginBonusDataInfo"):
for name in bonus.findall("loginBonusName"): for name in bonus.findall("loginBonusName"):
@@ -113,7 +113,7 @@ class ChuniReader(BaseReader):
if result is not None: if result is not None:
self.logger.info(f"Inserted login bonus {bonus_id}") self.logger.info(f"Inserted login bonus {bonus_id}")
else: else:
self.logger.warn( self.logger.warning(
f"Failed to insert login bonus {bonus_id}" f"Failed to insert login bonus {bonus_id}"
) )
@@ -138,7 +138,7 @@ class ChuniReader(BaseReader):
if result is not None: if result is not None:
self.logger.info(f"Inserted event {id}") self.logger.info(f"Inserted event {id}")
else: else:
self.logger.warn(f"Failed to insert event {id}") self.logger.warning(f"Failed to insert event {id}")
def read_music(self, music_dir: str) -> None: def read_music(self, music_dir: str) -> None:
for root, dirs, files in walk(music_dir): for root, dirs, files in walk(music_dir):
@@ -200,7 +200,7 @@ class ChuniReader(BaseReader):
f"Inserted music {song_id} chart {chart_id}" f"Inserted music {song_id} chart {chart_id}"
) )
else: else:
self.logger.warn( self.logger.warning(
f"Failed to insert music {song_id} chart {chart_id}" f"Failed to insert music {song_id} chart {chart_id}"
) )
@@ -232,7 +232,7 @@ class ChuniReader(BaseReader):
if result is not None: if result is not None:
self.logger.info(f"Inserted charge {id}") self.logger.info(f"Inserted charge {id}")
else: else:
self.logger.warn(f"Failed to insert charge {id}") self.logger.warning(f"Failed to insert charge {id}")
def read_avatar(self, avatar_dir: str) -> None: def read_avatar(self, avatar_dir: str) -> None:
for root, dirs, files in walk(avatar_dir): for root, dirs, files in walk(avatar_dir):
@@ -259,4 +259,4 @@ class ChuniReader(BaseReader):
if result is not None: if result is not None:
self.logger.info(f"Inserted avatarAccessory {id}") self.logger.info(f"Inserted avatarAccessory {id}")
else: else:
self.logger.warn(f"Failed to insert avatarAccessory {id}") self.logger.warning(f"Failed to insert avatarAccessory {id}")

View File

@@ -296,7 +296,7 @@ class ChuniItemData(BaseData):
self, self,
version: int, version: int,
room_id: int, room_id: int,
matching_member_info_list: list, matching_member_info_list: List,
user_id: int = None, user_id: int = None,
rest_sec: int = 60, rest_sec: int = 60,
is_full: bool = False is_full: bool = False
@@ -530,7 +530,7 @@ class ChuniItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_user_gacha: Failed to insert! aime_id: {aime_id}") self.logger.warning(f"put_user_gacha: Failed to insert! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -572,7 +572,7 @@ class ChuniItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_user_print_state: Failed to insert! aime_id: {aime_id}" f"put_user_print_state: Failed to insert! aime_id: {aime_id}"
) )
return None return None
@@ -589,7 +589,7 @@ class ChuniItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_user_print_detail: Failed to insert! aime_id: {aime_id}" f"put_user_print_detail: Failed to insert! aime_id: {aime_id}"
) )
return None return None

View File

@@ -410,7 +410,7 @@ class ChuniProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_profile_data: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_profile_data: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -452,7 +452,7 @@ class ChuniProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_profile_data_ex: Failed to update! aime_id: {aime_id}" f"put_profile_data_ex: Failed to update! aime_id: {aime_id}"
) )
return None return None
@@ -479,7 +479,7 @@ class ChuniProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_profile_option: Failed to update! aime_id: {aime_id}" f"put_profile_option: Failed to update! aime_id: {aime_id}"
) )
return None return None
@@ -503,7 +503,7 @@ class ChuniProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_profile_option_ex: Failed to update! aime_id: {aime_id}" f"put_profile_option_ex: Failed to update! aime_id: {aime_id}"
) )
return None return None
@@ -527,7 +527,7 @@ class ChuniProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_profile_recent_rating: Failed to update! aime_id: {aime_id}" f"put_profile_recent_rating: Failed to update! aime_id: {aime_id}"
) )
return None return None
@@ -552,7 +552,7 @@ class ChuniProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_profile_activity: Failed to update! aime_id: {aime_id}" f"put_profile_activity: Failed to update! aime_id: {aime_id}"
) )
return None return None
@@ -578,7 +578,7 @@ class ChuniProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_profile_charge: Failed to update! aime_id: {aime_id}" f"put_profile_charge: Failed to update! aime_id: {aime_id}"
) )
return None return None

View File

@@ -302,14 +302,14 @@ class ChuniStaticData(BaseData):
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"update_event: failed to update event! version: {version}, event_id: {event_id}, enabled: {enabled}" f"update_event: failed to update event! version: {version}, event_id: {event_id}, enabled: {enabled}"
) )
return None return None
event = self.get_event(version, event_id) event = self.get_event(version, event_id)
if event is None: if event is None:
self.logger.warn( self.logger.warning(
f"update_event: failed to fetch event {event_id} after updating" f"update_event: failed to fetch event {event_id} after updating"
) )
return None return None
@@ -506,7 +506,7 @@ class ChuniStaticData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"Failed to insert gacha! gacha_id {gacha_id}") self.logger.warning(f"Failed to insert gacha! gacha_id {gacha_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -541,7 +541,7 @@ class ChuniStaticData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"Failed to insert gacha card! gacha_id {gacha_id}") self.logger.warning(f"Failed to insert gacha card! gacha_id {gacha_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -577,7 +577,7 @@ class ChuniStaticData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"Failed to insert card! card_id {card_id}") self.logger.warning(f"Failed to insert card! card_id {card_id}")
return None return None
return result.lastrowid return result.lastrowid

View File

@@ -31,7 +31,18 @@ class CardMakerVersionConfig:
1: {"ongeki": 1.30.01, "chuni": 2.00.00, "maimai": 1.20.00} 1: {"ongeki": 1.30.01, "chuni": 2.00.00, "maimai": 1.20.00}
""" """
return CoreConfig.get_config_field( return CoreConfig.get_config_field(
self.__config, "cardmaker", "version", default={} self.__config, "cardmaker", "version", default={
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"
}
}
)[version] )[version]

View File

@@ -85,8 +85,6 @@ class CardMakerServlet:
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)
print(f"version: {version}")
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 < 140: # Card Maker 1.35 elif version >= 135 and version < 140: # Card Maker 1.35
@@ -124,11 +122,12 @@ class CardMakerServlet:
except Exception as e: except Exception as e:
self.logger.error(f"Error handling v{version} method {endpoint} - {e}") self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
raise
return zlib.compress(b'{"stat": "0"}') return zlib.compress(b'{"stat": "0"}')
if resp is None: if resp is 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

@@ -68,7 +68,7 @@ class CardMakerReader(BaseReader):
read_csv = getattr(CardMakerReader, func) read_csv = getattr(CardMakerReader, func)
read_csv(self, f"{self.bin_dir}/MU3/{file}") read_csv(self, f"{self.bin_dir}/MU3/{file}")
else: else:
self.logger.warn( self.logger.warning(
f"Couldn't find {file} file in {self.bin_dir}, skipping" f"Couldn't find {file} file in {self.bin_dir}, skipping"
) )
@@ -89,8 +89,7 @@ class CardMakerReader(BaseReader):
version_ids = { version_ids = {
"v2_00": ChuniConstants.VER_CHUNITHM_NEW, "v2_00": ChuniConstants.VER_CHUNITHM_NEW,
"v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS, "v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS,
# Chunithm SUN, ignore for now "v2_10": ChuniConstants.VER_CHUNITHM_SUN,
"v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1,
} }
for root, dirs, files in os.walk(base_dir): for root, dirs, files in os.walk(base_dir):
@@ -138,8 +137,7 @@ class CardMakerReader(BaseReader):
version_ids = { version_ids = {
"v2_00": ChuniConstants.VER_CHUNITHM_NEW, "v2_00": ChuniConstants.VER_CHUNITHM_NEW,
"v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS, "v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS,
# Chunithm SUN, ignore for now "v2_10": ChuniConstants.VER_CHUNITHM_SUN,
"v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1,
} }
for root, dirs, files in os.walk(base_dir): for root, dirs, files in os.walk(base_dir):
@@ -226,6 +224,12 @@ class CardMakerReader(BaseReader):
True if troot.find("disable").text == "false" else False True if troot.find("disable").text == "false" else False
) )
# check if a date is part of the name and disable the
# card if it is
enabled = (
False if re.search(r"\d{2}/\d{2}/\d{2}", name) else enabled
)
self.mai2_data.static.put_card( self.mai2_data.static.put_card(
version, card_id, name, enabled=enabled version, card_id, name, enabled=enabled
) )

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
@@ -11,6 +11,7 @@ from titles.cxb.config import CxbConfig
from titles.cxb.const import CxbConstants from titles.cxb.const import CxbConstants
from titles.cxb.database import CxbData from titles.cxb.database import CxbData
from threading import Thread
class CxbBase: class CxbBase:
def __init__(self, cfg: CoreConfig, game_cfg: CxbConfig) -> None: def __init__(self, cfg: CoreConfig, game_cfg: CxbConfig) -> None:
@@ -51,9 +52,154 @@ class CxbBase:
self.logger.info(f"Login user {data['login']['authid']}") self.logger.info(f"Login user {data['login']['authid']}")
return {"token": data["login"]["authid"], "uid": data["login"]["authid"]} return {"token": data["login"]["authid"], "uid": data["login"]["authid"]}
self.logger.warn(f"User {data['login']['authid']} does not have a profile") self.logger.warning(f"User {data['login']['authid']} does not have a profile")
return {} return {}
def task_generateCoupon(index, data1):
# Coupons
for i in range(500, 510):
index.append(str(i))
couponid = int(i) - 500
dataValue = [
{
"couponId": str(couponid),
"couponNum": "1",
"couponLog": [],
}
]
data1.append(
b64encode(
bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8")
).decode("utf-8")
)
def task_generateShopListTitle(index, data1):
# ShopList_Title
for i in range(200000, 201451):
index.append(str(i))
shopid = int(i) - 200000
dataValue = [
{
"shopId": shopid,
"shopState": "2",
"isDisable": "t",
"isDeleted": "f",
"isSpecialFlag": "f",
}
]
data1.append(
b64encode(
bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8")
).decode("utf-8")
)
def task_generateShopListIcon(index, data1):
# ShopList_Icon
for i in range(202000, 202264):
index.append(str(i))
shopid = int(i) - 200000
dataValue = [
{
"shopId": shopid,
"shopState": "2",
"isDisable": "t",
"isDeleted": "f",
"isSpecialFlag": "f",
}
]
data1.append(
b64encode(
bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8")
).decode("utf-8")
)
def task_generateStories(index, data1):
# Stories
for i in range(900000, 900003):
index.append(str(i))
storyid = int(i) - 900000
dataValue = [
{
"storyId": storyid,
"unlockState1": ["t"] * 10,
"unlockState2": ["t"] * 10,
"unlockState3": ["t"] * 10,
"unlockState4": ["t"] * 10,
"unlockState5": ["t"] * 10,
"unlockState6": ["t"] * 10,
"unlockState7": ["t"] * 10,
"unlockState8": ["t"] * 10,
"unlockState9": ["t"] * 10,
"unlockState10": ["t"] * 10,
"unlockState11": ["t"] * 10,
"unlockState12": ["t"] * 10,
"unlockState13": ["t"] * 10,
"unlockState14": ["t"] * 10,
"unlockState15": ["t"] * 10,
"unlockState16": ["t"] * 10,
}
]
data1.append(
b64encode(
bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8")
).decode("utf-8")
)
def task_generateScoreData(song, index, data1):
song_data = song["data"]
songCode = []
songCode.append(
{
"mcode": song_data["mcode"],
"musicState": song_data["musicState"],
"playCount": song_data["playCount"],
"totalScore": song_data["totalScore"],
"highScore": song_data["highScore"],
"everHighScore": song_data["everHighScore"]
if "everHighScore" in song_data
else ["0", "0", "0", "0", "0"],
"clearRate": song_data["clearRate"],
"rankPoint": song_data["rankPoint"],
"normalCR": song_data["normalCR"]
if "normalCR" in song_data
else ["0", "0", "0", "0", "0"],
"survivalCR": song_data["survivalCR"]
if "survivalCR" in song_data
else ["0", "0", "0", "0", "0"],
"ultimateCR": song_data["ultimateCR"]
if "ultimateCR" in song_data
else ["0", "0", "0", "0", "0"],
"nohopeCR": song_data["nohopeCR"]
if "nohopeCR" in song_data
else ["0", "0", "0", "0", "0"],
"combo": song_data["combo"],
"coupleUserId": song_data["coupleUserId"],
"difficulty": song_data["difficulty"],
"isFullCombo": song_data["isFullCombo"],
"clearGaugeType": song_data["clearGaugeType"],
"fieldType": song_data["fieldType"],
"gameType": song_data["gameType"],
"grade": song_data["grade"],
"unlockState": song_data["unlockState"],
"extraState": song_data["extraState"],
}
)
index.append(song_data["index"])
data1.append(
b64encode(
bytes(json.dumps(songCode[0], separators=(",", ":")), "utf-8")
).decode("utf-8")
)
def task_generateIndexData(versionindex):
try:
v_profile = self.data.profile.get_profile_index(0, uid, self.version)
v_profile_data = v_profile["data"]
versionindex.append(int(v_profile_data["appVersion"]))
except Exception:
versionindex.append("10400")
def handle_action_loadrange_request(self, data: Dict) -> Dict: def handle_action_loadrange_request(self, data: Dict) -> Dict:
range_start = data["loadrange"]["range"][0] range_start = data["loadrange"]["range"][0]
range_end = data["loadrange"]["range"][1] range_end = data["loadrange"]["range"][1]
@@ -107,146 +253,29 @@ class CxbBase:
900000 = Stories 900000 = Stories
""" """
# Coupons # Async threads to generate the response
for i in range(500, 510): thread_Coupon = Thread(target=CxbBase.task_generateCoupon(index, data1))
index.append(str(i)) thread_ShopListTitle = Thread(target=CxbBase.task_generateShopListTitle(index, data1))
couponid = int(i) - 500 thread_ShopListIcon = Thread(target=CxbBase.task_generateShopListIcon(index, data1))
dataValue = [ thread_Stories = Thread(target=CxbBase.task_generateStories(index, data1))
{
"couponId": str(couponid),
"couponNum": "1",
"couponLog": [],
}
]
data1.append(
b64encode(
bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8")
).decode("utf-8")
)
# ShopList_Title thread_Coupon.start()
for i in range(200000, 201451): thread_ShopListTitle.start()
index.append(str(i)) thread_ShopListIcon.start()
shopid = int(i) - 200000 thread_Stories.start()
dataValue = [
{
"shopId": shopid,
"shopState": "2",
"isDisable": "t",
"isDeleted": "f",
"isSpecialFlag": "f",
}
]
data1.append(
b64encode(
bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8")
).decode("utf-8")
)
# ShopList_Icon thread_Coupon.join()
for i in range(202000, 202264): thread_ShopListTitle.join()
index.append(str(i)) thread_ShopListIcon.join()
shopid = int(i) - 200000 thread_Stories.join()
dataValue = [
{
"shopId": shopid,
"shopState": "2",
"isDisable": "t",
"isDeleted": "f",
"isSpecialFlag": "f",
}
]
data1.append(
b64encode(
bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8")
).decode("utf-8")
)
# Stories
for i in range(900000, 900003):
index.append(str(i))
storyid = int(i) - 900000
dataValue = [
{
"storyId": storyid,
"unlockState1": ["t"] * 10,
"unlockState2": ["t"] * 10,
"unlockState3": ["t"] * 10,
"unlockState4": ["t"] * 10,
"unlockState5": ["t"] * 10,
"unlockState6": ["t"] * 10,
"unlockState7": ["t"] * 10,
"unlockState8": ["t"] * 10,
"unlockState9": ["t"] * 10,
"unlockState10": ["t"] * 10,
"unlockState11": ["t"] * 10,
"unlockState12": ["t"] * 10,
"unlockState13": ["t"] * 10,
"unlockState14": ["t"] * 10,
"unlockState15": ["t"] * 10,
"unlockState16": ["t"] * 10,
}
]
data1.append(
b64encode(
bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8")
).decode("utf-8")
)
for song in songs: for song in songs:
song_data = song["data"] thread_ScoreData = Thread(target=CxbBase.task_generateScoreData(song, index, data1))
songCode = [] thread_ScoreData.start()
songCode.append(
{
"mcode": song_data["mcode"],
"musicState": song_data["musicState"],
"playCount": song_data["playCount"],
"totalScore": song_data["totalScore"],
"highScore": song_data["highScore"],
"everHighScore": song_data["everHighScore"]
if "everHighScore" in song_data
else ["0", "0", "0", "0", "0"],
"clearRate": song_data["clearRate"],
"rankPoint": song_data["rankPoint"],
"normalCR": song_data["normalCR"]
if "normalCR" in song_data
else ["0", "0", "0", "0", "0"],
"survivalCR": song_data["survivalCR"]
if "survivalCR" in song_data
else ["0", "0", "0", "0", "0"],
"ultimateCR": song_data["ultimateCR"]
if "ultimateCR" in song_data
else ["0", "0", "0", "0", "0"],
"nohopeCR": song_data["nohopeCR"]
if "nohopeCR" in song_data
else ["0", "0", "0", "0", "0"],
"combo": song_data["combo"],
"coupleUserId": song_data["coupleUserId"],
"difficulty": song_data["difficulty"],
"isFullCombo": song_data["isFullCombo"],
"clearGaugeType": song_data["clearGaugeType"],
"fieldType": song_data["fieldType"],
"gameType": song_data["gameType"],
"grade": song_data["grade"],
"unlockState": song_data["unlockState"],
"extraState": song_data["extraState"],
}
)
index.append(song_data["index"])
data1.append(
b64encode(
bytes(json.dumps(songCode[0], separators=(",", ":")), "utf-8")
).decode("utf-8")
)
for v in index: for v in index:
try: thread_IndexData = Thread(target=CxbBase.task_generateIndexData(versionindex))
v_profile = self.data.profile.get_profile_index(0, uid, self.version) thread_IndexData.start()
v_profile_data = v_profile["data"]
versionindex.append(int(v_profile_data["appVersion"]))
except:
versionindex.append("10400")
return {"index": index, "data": data1, "version": versionindex} return {"index": index, "data": data1, "version": versionindex}
@@ -257,7 +286,7 @@ class CxbBase:
# REV Omnimix Version Fetcher # REV Omnimix Version Fetcher
gameversion = data["saveindex"]["data"][0][2] gameversion = data["saveindex"]["data"][0][2]
self.logger.warning(f"Game Version is {gameversion}") self.logger.warning(f"Game Version is {gameversion}")
except: except Exception:
pass pass
if "10205" in gameversion: if "10205" in gameversion:
@@ -319,7 +348,7 @@ class CxbBase:
# Sunrise # Sunrise
try: try:
profileIndex = save_data["index"].index("0") profileIndex = save_data["index"].index("0")
except: except Exception:
return {"data": ""} # Maybe return {"data": ""} # Maybe
profile = json.loads(save_data["data"][profileIndex]) profile = json.loads(save_data["data"][profileIndex])
@@ -416,7 +445,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:
@@ -467,7 +496,7 @@ class CxbBase:
score=int(rid["sc"][0]), score=int(rid["sc"][0]),
clear=rid["clear"], clear=rid["clear"],
) )
except: except Exception:
self.data.score.put_ranking( self.data.score.put_ranking(
user_id=uid, user_id=uid,
rev_id=int(rid["rid"]), rev_id=int(rid["rid"]),
@@ -485,7 +514,7 @@ class CxbBase:
score=int(rid["sc"][0]), score=int(rid["sc"][0]),
clear=0, clear=0,
) )
except: except Exception:
self.data.score.put_ranking( self.data.score.put_ranking(
user_id=uid, user_id=uid,
rev_id=int(rid["rid"]), rev_id=int(rid["rid"]),

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 = ""
@@ -123,13 +123,13 @@ class CxbServlet(resource.Resource):
) )
except Exception as f: except Exception as f:
self.logger.warn( self.logger.warning(
f"Error decoding json: {e} / {f} - {req_url} - {req_bytes}" f"Error decoding json: {e} / {f} - {req_url} - {req_bytes}"
) )
return b"" return b""
if req_json == {}: if req_json == {}:
self.logger.warn(f"Empty json request to {req_url}") self.logger.warning(f"Empty json request to {req_url}")
return b"" return b""
cmd = url_split[len(url_split) - 1] cmd = url_split[len(url_split) - 1]
@@ -140,7 +140,7 @@ class CxbServlet(resource.Resource):
not type(req_json["dldate"]) is dict not type(req_json["dldate"]) is dict
or "filetype" not in req_json["dldate"] or "filetype" not in req_json["dldate"]
): ):
self.logger.warn(f"Malformed dldate request: {req_url} {req_json}") self.logger.warning(f"Malformed dldate request: {req_url} {req_json}")
return b"" return b""
filetype = req_json["dldate"]["filetype"] filetype = req_json["dldate"]["filetype"]

View File

@@ -33,7 +33,7 @@ class CxbReader(BaseReader):
pull_bin_ram = True pull_bin_ram = True
if not path.exists(f"{self.bin_dir}"): if not path.exists(f"{self.bin_dir}"):
self.logger.warn(f"Couldn't find csv file in {self.bin_dir}, skipping") self.logger.warning(f"Couldn't find csv file in {self.bin_dir}, skipping")
pull_bin_ram = False pull_bin_ram = False
if pull_bin_ram: if pull_bin_ram:
@@ -123,5 +123,5 @@ class CxbReader(BaseReader):
genre, genre,
int(row["easy"].replace("Easy ", "").replace("N/A", "0")), int(row["easy"].replace("Easy ", "").replace("N/A", "0")),
) )
except: except Exception:
self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") self.logger.warning(f"Couldn't read csv file in {self.bin_dir}, skipping")

View File

@@ -3,6 +3,7 @@ from typing import Any, List, Dict
import logging import logging
import json import json
import urllib import urllib
from threading import Thread
from core.config import CoreConfig from core.config import CoreConfig
from titles.diva.config import DivaConfig from titles.diva.config import DivaConfig
@@ -663,11 +664,8 @@ class DivaBase:
return pv_result return pv_result
def handle_get_pv_pd_request(self, data: Dict) -> Dict: def task_generateScoreData(self, data: Dict, pd_by_pv_id, song):
song_id = data["pd_pv_id_lst"].split(",")
pv = ""
for song in song_id:
if int(song) > 0: if int(song) > 0:
# the request do not send a edition so just perform a query best score and ranking for each edition. # the request do not send a edition so just perform a query best score and ranking for each edition.
# 0=ORIGINAL, 1=EXTRA # 0=ORIGINAL, 1=EXTRA
@@ -702,11 +700,30 @@ class DivaBase:
) )
self.logger.debug(f"pv_result = {pv_result}") self.logger.debug(f"pv_result = {pv_result}")
pd_by_pv_id.append(urllib.parse.quote(pv_result))
pv += urllib.parse.quote(pv_result)
else: else:
pv += urllib.parse.quote(f"{song}***") pd_by_pv_id.append(urllib.parse.quote(f"{song}***"))
pv += "," pd_by_pv_id.append(",")
def handle_get_pv_pd_request(self, data: Dict) -> Dict:
song_id = data["pd_pv_id_lst"].split(",")
pv = ""
threads = []
pd_by_pv_id = []
for song in song_id:
thread_ScoreData = Thread(target=self.task_generateScoreData(data, pd_by_pv_id, song))
threads.append(thread_ScoreData)
for x in threads:
x.start()
for x in threads:
x.join()
for x in pd_by_pv_id:
pv += x
response = "" response = ""
response += f"&pd_by_pv_id={pv[:-1]}" response += f"&pd_by_pv_id={pv[:-1]}"

View File

@@ -34,18 +34,18 @@ class DivaReader(BaseReader):
pull_opt_rom = True pull_opt_rom = True
if not path.exists(f"{self.bin_dir}/ram"): if not path.exists(f"{self.bin_dir}/ram"):
self.logger.warn(f"Couldn't find ram folder in {self.bin_dir}, skipping") self.logger.warning(f"Couldn't find ram folder in {self.bin_dir}, skipping")
pull_bin_ram = False pull_bin_ram = False
if not path.exists(f"{self.bin_dir}/rom"): if not path.exists(f"{self.bin_dir}/rom"):
self.logger.warn(f"Couldn't find rom folder in {self.bin_dir}, skipping") self.logger.warning(f"Couldn't find rom folder in {self.bin_dir}, skipping")
pull_bin_rom = False pull_bin_rom = False
if self.opt_dir is not None: if self.opt_dir is not None:
opt_dirs = self.get_data_directories(self.opt_dir) opt_dirs = self.get_data_directories(self.opt_dir)
else: else:
pull_opt_rom = False pull_opt_rom = False
self.logger.warn("No option directory specified, skipping") self.logger.warning("No option directory specified, skipping")
if pull_bin_ram: if pull_bin_ram:
self.read_ram(f"{self.bin_dir}/ram") self.read_ram(f"{self.bin_dir}/ram")
@@ -139,7 +139,7 @@ class DivaReader(BaseReader):
else: else:
continue continue
else: else:
self.logger.warn(f"Databank folder not found in {ram_root_dir}, skipping") self.logger.warning(f"Databank folder not found in {ram_root_dir}, skipping")
def read_rom(self, rom_root_dir: str) -> None: def read_rom(self, rom_root_dir: str) -> None:
self.logger.info(f"Read ROM from {rom_root_dir}") self.logger.info(f"Read ROM from {rom_root_dir}")
@@ -150,7 +150,7 @@ class DivaReader(BaseReader):
elif path.exists(f"{rom_root_dir}/pv_db.txt"): elif path.exists(f"{rom_root_dir}/pv_db.txt"):
file_path = f"{rom_root_dir}/pv_db.txt" file_path = f"{rom_root_dir}/pv_db.txt"
else: else:
self.logger.warn( self.logger.warning(
f"Cannot find pv_db.txt or mdata_pv_db.txt in {rom_root_dir}, skipping" f"Cannot find pv_db.txt or mdata_pv_db.txt in {rom_root_dir}, skipping"
) )
return return

View File

@@ -114,7 +114,7 @@ class IDZUserDBProtocol(Protocol):
elif self.version == 230: elif self.version == 230:
self.version_internal = IDZConstants.VER_IDZ_230 self.version_internal = IDZConstants.VER_IDZ_230
else: else:
self.logger.warn(f"Bad version v{self.version}") self.logger.warning(f"Bad version v{self.version}")
self.version = None self.version = None
self.version_internal = None self.version_internal = None
@@ -142,7 +142,7 @@ class IDZUserDBProtocol(Protocol):
self.version_internal self.version_internal
].get(cmd, None) ].get(cmd, None)
if handler_cls is None: if handler_cls is None:
self.logger.warn(f"No handler for v{self.version} {hex(cmd)} cmd") self.logger.warning(f"No handler for v{self.version} {hex(cmd)} cmd")
handler_cls = IDZHandlerBase handler_cls = IDZHandlerBase
handler = handler_cls(self.core_config, self.game_config, self.version_internal) handler = handler_cls(self.core_config, self.game_config, self.version_internal)

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 = 5 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,6 +1,9 @@
from datetime import datetime, date, timedelta from datetime import datetime
from typing import Any, Dict from typing import Any, Dict, List
import logging import logging
from base64 import b64decode
from os import path, stat, remove
from PIL import ImageFile
from core.config import CoreConfig from core.config import CoreConfig
from titles.mai2.const import Mai2Constants from titles.mai2.const import Mai2Constants
@@ -12,40 +15,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: 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/100/" self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/197/"
else: else:
self.old_server = f"http://{self.core_config.title.hostname}/SDEY/100/" 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):
# TODO: See if making this epoch 0 breaks things
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": self.old_server, "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:
@@ -58,8 +56,8 @@ 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.warning("No enabled events, did you run the reader?")
return {"type": data["type"], "length": 0, "gameEventList": []} return {"type": data["type"], "length": 0, "gameEventList": []}
for event in events: for event in events:
@@ -94,7 +92,7 @@ class Mai2Base:
for i, charge in enumerate(game_charge_list): for i, charge in enumerate(game_charge_list):
charge_list.append( charge_list.append(
{ {
"orderId": i, "orderId": i + 1,
"chargeId": charge["ticketId"], "chargeId": charge["ticketId"],
"price": charge["price"], "price": charge["price"],
"startDate": "2017-12-05 07:00:00.0", "startDate": "2017-12-05 07:00:00.0",
@@ -117,43 +115,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"]
@@ -165,12 +155,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:
@@ -202,10 +211,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:
@@ -215,11 +247,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:
@@ -228,8 +264,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:
@@ -250,12 +285,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:
@@ -265,7 +297,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:
@@ -291,7 +323,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"]:
@@ -319,7 +351,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
@@ -343,7 +375,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
@@ -413,12 +445,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")
@@ -449,6 +500,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}
@@ -482,6 +535,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:
@@ -645,24 +708,157 @@ 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.warning("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:
if not self.game_config.uploads.photos or not self.game_config.uploads.photos_dir:
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
photo = data.get("userPhoto", {})
if photo is None or not photo:
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
order_id = int(photo.get("orderId", -1))
user_id = int(photo.get("userId", -1))
div_num = int(photo.get("divNumber", -1))
div_len = int(photo.get("divLength", -1))
div_data = photo.get("divData", "")
playlog_id = int(photo.get("playlogId", -1))
track_num = int(photo.get("trackNo", -1))
upload_date = photo.get("uploadDate", "")
if order_id < 0 or user_id <= 0 or div_num < 0 or div_len <= 0 or not div_data or playlog_id < 0 or track_num <= 0 or not upload_date:
self.logger.warning(f"Malformed photo upload request")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
if order_id == 0 and div_num > 0:
self.logger.warning(f"Failed to set orderId properly (still 0 after first chunk)")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
if div_num == 0 and order_id > 0:
self.logger.warning(f"First chuck re-send, Ignore")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
if div_num >= div_len:
self.logger.warning(f"Sent extra chunks ({div_num} >= {div_len})")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
if div_len >= 100:
self.logger.warning(f"Photo too large ({div_len} * 10240 = {div_len * 10240} bytes)")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
ret_code = order_id + 1
photo_chunk = b64decode(div_data)
if len(photo_chunk) > 10240 or (len(photo_chunk) < 10240 and div_num + 1 != div_len):
self.logger.warning(f"Incorrect data size after decoding (Expected 10240, got {len(photo_chunk)})")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
out_name = f"{self.game_config.uploads.photos_dir}/{user_id}_{playlog_id}_{track_num}"
if not path.exists(f"{out_name}.bin") and div_num != 0:
self.logger.warning(f"Out of order photo upload (div_num {div_num})")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
if path.exists(f"{out_name}.bin") and div_num == 0:
self.logger.warning(f"Duplicate file upload")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
elif path.exists(f"{out_name}.bin"):
fstats = stat(f"{out_name}.bin")
if fstats.st_size != 10240 * div_num:
self.logger.warning(f"Out of order photo upload (trying to upload div {div_num}, expected div {fstats.st_size / 10240} for file sized {fstats.st_size} bytes)")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
try:
with open(f"{out_name}.bin", "ab") as f:
f.write(photo_chunk)
except Exception:
self.logger.error(f"Failed writing to {out_name}.bin")
return {'returnCode': 0, 'apiName': 'UploadUserPhotoApi'}
if div_num + 1 == div_len and path.exists(f"{out_name}.bin"):
try:
p = ImageFile.Parser()
with open(f"{out_name}.bin", "rb") as f:
p.feed(f.read())
im = p.close()
im.save(f"{out_name}.jpeg")
except Exception:
self.logger.error(f"File {out_name}.bin failed image validation")
try:
remove(f"{out_name}.bin")
except Exception:
self.logger.error(f"Failed to delete {out_name}.bin, please remove it manually")
return {'returnCode': ret_code, 'apiName': 'UploadUserPhotoApi'}

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", "deliver", "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,19 +20,53 @@ 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",

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

@@ -0,0 +1,591 @@
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_ghost(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_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:
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.warning("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)
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("user")
music_detail_list.append(tmp)
next_index = 0 if len(music_detail_list) < max_ct or num_user_songs == upper_lim else upper_lim
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})")
return {
"userId": data["userId"],
"nextIndex": next_index,
"userMusicList": [{"userMusicDetailList": music_detail_list}],
}
def handle_user_login_api_request(self, data: Dict) -> Dict:
ret = super().handle_user_login_api_request(data)
if ret is None or not ret:
return ret
ret['loginId'] = ret.get('loginCount', 0)
return ret

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

@@ -14,7 +14,7 @@ class Mai2Festival(Mai2UniversePlus):
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.36 # hardcode lastDataVersion for CardMaker 1.35
user_data["lastDataVersion"] = "1.30.00" user_data["lastDataVersion"] = "1.30.00"
return user_data return user_data

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
@@ -6,7 +7,7 @@ import string
import logging, coloredlogs import logging, coloredlogs
import zlib import zlib
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from os import path from os import path, mkdir
from typing import Tuple from typing import Tuple
from core.config import CoreConfig from core.config import CoreConfig
@@ -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,7 +100,7 @@ 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 (
@@ -91,10 +109,27 @@ class Mai2Servlet:
f"{core_cfg.title.hostname}", f"{core_cfg.title.hostname}",
) )
def setup(self):
if self.game_cfg.uploads.photos and self.game_cfg.uploads.photos_dir and not path.exists(self.game_cfg.uploads.photos_dir):
try:
mkdir(self.game_cfg.uploads.photos_dir)
except Exception:
self.logger.error(f"Failed to make photo upload directory at {self.game_cfg.uploads.photos_dir}")
if self.game_cfg.uploads.movies and self.game_cfg.uploads.movies_dir and not path.exists(self.game_cfg.uploads.movies_dir):
try:
mkdir(self.game_cfg.uploads.movies_dir)
except Exception:
self.logger.error(f"Failed to make movie upload directory at {self.game_cfg.uploads.movies_dir}")
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 +137,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,11 +153,42 @@ class Mai2Servlet:
elif version >= 130: # Festival elif version >= 130: # Festival
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: else:
# If we get a 32 character long hex string, it's a hash and we're if version < 110: # 1.0
# doing encrypted. The likelyhood of false positives is low but internal_ver = Mai2Constants.VER_MAIMAI
# technically not 0 elif version >= 110 and version < 120: # Plus
self.logger.error("Encryption not supported at this time") 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 request.getHeader('Mai-Encoding') is not None or request.getHeader('X-Mai-Encoding') is not None:
# The has is some flavor of MD5 of the endpoint with a constant bolted onto the end of it.
# See cake.dll's Obfuscator function for details. Hopefully most DLL edits will remove
# these two(?) headers to not cause issues, but given the general quality of SEGA data...
enc_ver = request.getHeader('Mai-Encoding')
if enc_ver is None:
enc_ver = request.getHeader('X-Mai-Encoding')
self.logger.debug(f"Encryption v{enc_ver} - User-Agent: {request.getHeader('User-Agent')}")
try: try:
unzip = zlib.decompress(req_raw) unzip = zlib.decompress(req_raw)
@@ -159,3 +226,61 @@ 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.debug(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()
else:
request.setResponseCode(404)
return b""
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"{}")
else:
request.setResponseCode(404)
return b""
elif url_split[0] == "usbdl":
if url_split[1] == "CONNECTIONTEST":
self.logger.info(f"v{version} usbdl server test")
return b""
elif self.game_cfg.deliver.udbdl_enable and path.exists(f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}"):
with open(f"{self.game_cfg.deliver.content_folder}/usb/{url_split[-1]}", 'rb') as f:
return f.read()
else:
request.setResponseCode(404)
return b""
elif url_split[0] == "deliver":
file = url_split[len(url_split) - 1]
self.logger.info(f"v{version} {file} deliver inquire")
self.logger.debug(f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}")
if self.game_cfg.deliver.enable and path.exists(f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}"):
with open(f"{self.game_cfg.deliver.content_folder}/net_deliver/{file}", 'rb') as f:
return f.read()
else:
request.setResponseCode(404)
return 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.warning(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.warning(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.warning(f"rindex failed on line {x}")
if is_struct:
self.logger.warning("Struct not formatted properly")
if not struct_def:
self.logger.warning("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.warning(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, nullable=False, server_default=func.now()), Column("startDate", TIMESTAMP, server_default=func.now()),
Column("endDate", TIMESTAMP, nullable=False), 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"),
@@ -203,7 +204,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_item: failed to insert item! user_id: {user_id}, item_kind: {item_kind}, item_id: {item_id}" f"put_item: failed to insert item! user_id: {user_id}, item_kind: {item_kind}, item_id: {item_id}"
) )
return None return None
@@ -260,7 +261,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_login_bonus: failed to insert item! user_id: {user_id}, bonus_id: {bonus_id}, point: {point}" f"put_login_bonus: failed to insert item! user_id: {user_id}, bonus_id: {bonus_id}, point: {point}"
) )
return None return None
@@ -311,7 +312,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_map: failed to insert item! user_id: {user_id}, map_id: {map_id}, distance: {distance}" f"put_map: failed to insert item! user_id: {user_id}, map_id: {map_id}, distance: {distance}"
) )
return None return None
@@ -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.warning(
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,
@@ -357,7 +371,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_character: failed to insert item! user_id: {user_id}, character_id: {character_id}, level: {level}" f"put_character: failed to insert item! user_id: {user_id}, character_id: {character_id}, level: {level}"
) )
return None return None
@@ -400,7 +414,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_friend_season_ranking: failed to insert", f"put_friend_season_ranking: failed to insert",
f"friend_season_ranking! aime_id: {aime_id}", f"friend_season_ranking! aime_id: {aime_id}",
) )
@@ -418,7 +432,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_favorite: failed to insert item! user_id: {user_id}, kind: {kind}" f"put_favorite: failed to insert item! user_id: {user_id}, kind: {kind}"
) )
return None return None
@@ -463,7 +477,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_card: failed to insert card! user_id: {user_id}, kind: {card_kind}" f"put_card: failed to insert card! user_id: {user_id}, kind: {card_kind}"
) )
return None return None
@@ -502,7 +516,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_card: failed to insert charge! user_id: {user_id}, chargeId: {charge_id}" f"put_card: failed to insert charge! user_id: {user_id}, chargeId: {charge_id}"
) )
return None return None
@@ -527,7 +541,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_user_print_detail: Failed to insert! aime_id: {aime_id}" f"put_user_print_detail: Failed to insert! aime_id: {aime_id}"
) )
return None return 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.warning(
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
@@ -321,7 +525,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_profile_ghost: failed to update! {user_id}") self.logger.warning(f"put_profile_ghost: failed to update! {user_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -348,7 +552,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_profile_extend: failed to update! {user_id}") self.logger.warning(f"put_profile_extend: failed to update! {user_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -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.warning(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:
@@ -402,7 +616,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_profile_rating: failed to update! {user_id}") self.logger.warning(f"put_profile_rating: failed to update! {user_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -429,7 +643,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_region: failed to update! {user_id}") self.logger.warning(f"put_region: failed to update! {user_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -454,7 +668,7 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_profile_activity: failed to update! user_id: {user_id}" f"put_profile_activity: failed to update! user_id: {user_id}"
) )
return None return 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.warning(
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.warning(
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.warning(
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.warning(
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

@@ -161,7 +161,7 @@ class Mai2StaticData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"Failed to insert song {song_id} chart {chart_id}") self.logger.warning(f"Failed to insert song {song_id} chart {chart_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -187,7 +187,7 @@ class Mai2StaticData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"Failed to insert charge {ticket_id} type {ticket_type}") self.logger.warning(f"Failed to insert charge {ticket_id} type {ticket_type}")
return None return None
return result.lastrowid return result.lastrowid
@@ -237,7 +237,7 @@ class Mai2StaticData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"Failed to insert card {card_id}") self.logger.warning(f"Failed to insert card {card_id}")
return None return None
return result.lastrowid return result.lastrowid

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.dxplus import Mai2DXPlus
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(Mai2DXPlus):
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.splash import Mai2Splash
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(Mai2Splash):
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.splashplus import Mai2SplashPlus
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(Mai2SplashPlus):
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
@@ -70,13 +70,13 @@ class Mai2Universe(Mai2Base):
tmp.pop("cardName") tmp.pop("cardName")
tmp.pop("enabled") tmp.pop("enabled")
tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S") tmp["startDate"] = datetime.strftime(tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT)
tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S") tmp["endDate"] = datetime.strftime(tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT)
tmp["noticeStartDate"] = datetime.strftime( tmp["noticeStartDate"] = datetime.strftime(
tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S" tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT
) )
tmp["noticeEndDate"] = datetime.strftime( tmp["noticeEndDate"] = datetime.strftime(
tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S" tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT
) )
selling_card_list.append(tmp) selling_card_list.append(tmp)
@@ -180,7 +180,7 @@ class Mai2Universe(Mai2Base):
extend = extend._asdict() extend = extend._asdict()
# parse the selectedCardList # parse the selectedCardList
# 6 = Freedom Pass, 4 = Gold Pass (cardTypeId) # 6 = Freedom Pass, 4 = Gold Pass (cardTypeId)
selected_cards: list = extend["selectedCardList"] selected_cards: List = extend["selectedCardList"]
# if no pass is already added, add the corresponding pass # if no pass is already added, add the corresponding pass
if not user_card["cardTypeId"] in selected_cards: if not user_card["cardTypeId"] in selected_cards:

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

@@ -326,7 +326,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_card: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_card: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -346,7 +346,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_character: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_character: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -366,7 +366,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_deck: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_deck: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -394,7 +394,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_boss: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_boss: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -406,7 +406,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_story: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_story: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -426,7 +426,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_chapter: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_chapter: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -446,7 +446,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_item: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_item: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -479,7 +479,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_music_item: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_music_item: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -499,7 +499,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_login_bonus: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_login_bonus: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -521,7 +521,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_mission_point: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_mission_point: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -541,7 +541,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_event_point: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_event_point: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -561,7 +561,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_scenerio: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_scenerio: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -581,7 +581,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_trade_item: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_trade_item: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -601,7 +601,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_event_music: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_event_music: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -621,7 +621,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_tech_event: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_tech_event: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -651,7 +651,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_memorychapter: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_memorychapter: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -694,7 +694,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_user_gacha: Failed to insert! aime_id: {aime_id}") self.logger.warning(f"put_user_gacha: Failed to insert! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -709,7 +709,7 @@ class OngekiItemData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_user_print_detail: Failed to insert! aime_id: {aime_id}" f"put_user_print_detail: Failed to insert! aime_id: {aime_id}"
) )
return None return None

View File

@@ -63,7 +63,7 @@ class OngekiLogData(BaseData):
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_gp_log: Failed to insert GP log! aime_id: {aime_id} kind {kind} pattern {pattern} current_gp {current_gp}" f"put_gp_log: Failed to insert GP log! aime_id: {aime_id} kind {kind} pattern {pattern} current_gp {current_gp}"
) )
return result.lastrowid return result.lastrowid

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:
@@ -364,7 +364,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_profile_data: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_profile_data: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -376,7 +376,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_profile_options: Failed to update! aime_id: {aime_id}" f"put_profile_options: Failed to update! aime_id: {aime_id}"
) )
return None return None
@@ -393,7 +393,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_profile_recent_rating: failed to update recent rating! aime_id {aime_id}" f"put_profile_recent_rating: failed to update recent rating! aime_id {aime_id}"
) )
return None return None
@@ -415,7 +415,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_profile_rating_log: failed to update rating log! aime_id {aime_id} data_version {data_version} highest_rating {highest_rating}" f"put_profile_rating_log: failed to update rating log! aime_id {aime_id} data_version {data_version} highest_rating {highest_rating}"
) )
return None return None
@@ -449,7 +449,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_profile_activity: failed to put activity! aime_id {aime_id} kind {kind} activity_id {activity_id}" f"put_profile_activity: failed to put activity! aime_id {aime_id} kind {kind} activity_id {activity_id}"
) )
return None return None
@@ -466,7 +466,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_profile_region: failed to update! aime_id {aime_id} region {region}" f"put_profile_region: failed to update! aime_id {aime_id} region {region}"
) )
return None return None
@@ -480,7 +480,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_best_score: Failed to add score! aime_id: {aime_id}") self.logger.warning(f"put_best_score: Failed to add score! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -492,7 +492,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_kop: Failed to add score! aime_id: {aime_id}") self.logger.warning(f"put_kop: Failed to add score! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -503,7 +503,7 @@ class OngekiProfileData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"put_rival: failed to update! aime_id: {aime_id}, rival_id: {rival_id}" f"put_rival: failed to update! aime_id: {aime_id}, rival_id: {rival_id}"
) )
return None return None

View File

@@ -139,7 +139,7 @@ class OngekiScoreData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_tech_count: Failed to update! aime_id: {aime_id}") self.logger.warning(f"put_tech_count: Failed to update! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -164,7 +164,7 @@ class OngekiScoreData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"put_best_score: Failed to add score! aime_id: {aime_id}") self.logger.warning(f"put_best_score: Failed to add score! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -175,6 +175,6 @@ class OngekiScoreData(BaseData):
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
self.logger.warn(f"put_playlog: Failed to add playlog! aime_id: {aime_id}") self.logger.warning(f"put_playlog: Failed to add playlog! aime_id: {aime_id}")
return None return None
return result.lastrowid return result.lastrowid

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",
@@ -104,7 +105,7 @@ class OngekiStaticData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"Failed to insert card! card_id {card_id}") self.logger.warning(f"Failed to insert card! card_id {card_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -179,7 +180,7 @@ class OngekiStaticData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"Failed to insert gacha! gacha_id {gacha_id}") self.logger.warning(f"Failed to insert gacha! gacha_id {gacha_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -214,7 +215,7 @@ class OngekiStaticData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"Failed to insert gacha card! gacha_id {gacha_id}") self.logger.warning(f"Failed to insert gacha card! gacha_id {gacha_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -242,7 +243,7 @@ class OngekiStaticData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn(f"Failed to insert event! event_id {event_id}") self.logger.warning(f"Failed to insert event! event_id {event_id}")
return None return None
return result.lastrowid return result.lastrowid
@@ -303,7 +304,7 @@ class OngekiStaticData(BaseData):
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
self.logger.warn( self.logger.warning(
f"Failed to insert chart! song_id: {song_id}, chart_id: {chart_id}" f"Failed to insert chart! song_id: {song_id}, chart_id: {chart_id}"
) )
return None return None

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,15 @@ 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
TUTORIAL_3 = 7
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

@@ -22,7 +22,7 @@ class PokkenFrontend(FE_Base):
self.game_cfg.update( self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}")) yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"))
) )
self.nav_name = "Pokken" self.nav_name = "Pokkén"
def render_GET(self, request: Request) -> bytes: def render_GET(self, request: Request) -> bytes:
template = self.environment.get_template( template = self.environment.get_template(
@@ -35,5 +35,6 @@ class PokkenFrontend(FE_Base):
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) sesh=vars(usr_sesh),
active_page="pokken",
).encode("utf-16") ).encode("utf-16")

View File

@@ -1,4 +1,4 @@
{% extends "core/frontend/index.jinja" %} {% extends "core/frontend/index.jinja" %}
{% block content %} {% block content %}
<h1>Pokken</h1> <h1>Pokkén</h1>
{% endblock content %} {% endblock content %}

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 = ""
@@ -108,7 +112,7 @@ class PokkenServlet(resource.Resource):
try: try:
pokken_request.ParseFromString(content) pokken_request.ParseFromString(content)
except DecodeError as e: except DecodeError as e:
self.logger.warn(f"{e} {content}") self.logger.warning(f"{e} {content}")
return b"" return b""
endpoint = jackal_pb2.MessageType.DESCRIPTOR.values_by_number[ endpoint = jackal_pb2.MessageType.DESCRIPTOR.values_by_number[
@@ -119,7 +123,7 @@ class PokkenServlet(resource.Resource):
handler = getattr(self.base, f"handle_{endpoint}", None) handler = getattr(self.base, f"handle_{endpoint}", None)
if handler is None: if handler is None:
self.logger.warn(f"No handler found for message type {endpoint}") self.logger.warning(f"No handler found for message type {endpoint}")
return self.base.handle_noop(pokken_request) return self.base.handle_noop(pokken_request)
self.logger.info(f"{endpoint} request from {Utils.get_ip_addr(request)}") self.logger.info(f"{endpoint} request from {Utils.get_ip_addr(request)}")
@@ -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)
@@ -150,7 +157,7 @@ class PokkenServlet(resource.Resource):
None, None,
) )
if handler is None: if handler is None:
self.logger.warn( self.logger.warning(
f"No handler found for message type {json_content['call']}" f"No handler found for message type {json_content['call']}"
) )
return json.dumps(self.base.handle_matching_noop()).encode() return json.dumps(self.base.handle_matching_noop()).encode()

View File

@@ -31,4 +31,20 @@ 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,
)
conflict = sql.on_duplicate_key_update(
content=content,
)
result = self.execute(conflict)
if result is None:
self.logger.warning(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_def=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_def=defe,
bp_point_sp=sp,
)
result = self.execute(conflict)
if result is None:
self.logger.warning(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.warning(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.warning(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,33 @@ 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.warning(f"Failed to update stats for user {user_id}")
def update_support_team(self, user_id: int, support_id: int, support1: int = None, support2: int = None) -> None:
if support1 == 4294967295:
support1 = None
if support2 == 4294967295:
support2 = 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.warning(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

View File

@@ -3,7 +3,9 @@ import json, logging
from typing import Any, Dict from typing import Any, Dict
import random import random
import struct import struct
import csv from csv import *
from random import choice
import random as rand
from core.data import Data from core.data import Data
from core import CoreConfig from core import CoreConfig
@@ -60,9 +62,26 @@ class SaoBase:
def handle_c11e(self, request: Any) -> bytes: def handle_c11e(self, request: Any) -> bytes:
#common/get_auth_card_data #common/get_auth_card_data
req = bytes.fromhex(request)[24:]
req_struct = Struct(
Padding(16),
"cabinet_type" / Int8ub, # cabinet_type is a byte
"auth_type" / Int8ub, # auth_type is a byte
"store_id_size" / Rebuild(Int32ub, len_(this.store_id) * 2), # calculates the length of the store_id
"store_id" / PaddedString(this.store_id_size, "utf_16_le"), # store_id is a (zero) padded string
"serial_no_size" / Rebuild(Int32ub, len_(this.serial_no) * 2), # calculates the length of the serial_no
"serial_no" / PaddedString(this.serial_no_size, "utf_16_le"), # serial_no is a (zero) padded string
"access_code_size" / Rebuild(Int32ub, len_(this.access_code) * 2), # calculates the length of the access_code
"access_code" / PaddedString(this.access_code_size, "utf_16_le"), # access_code is a (zero) padded string
"chip_id_size" / Rebuild(Int32ub, len_(this.chip_id) * 2), # calculates the length of the chip_id
"chip_id" / PaddedString(this.chip_id_size, "utf_16_le"), # chip_id is a (zero) padded string
)
req_data = req_struct.parse(req)
access_code = req_data.access_code
#Check authentication #Check authentication
access_code = bytes.fromhex(request[188:268]).decode("utf-16le")
user_id = self.core_data.card.get_user_id_from_card( access_code ) user_id = self.core_data.card.get_user_id_from_card( access_code )
if not user_id: if not user_id:
@@ -79,6 +98,13 @@ class SaoBase:
self.game_data.item.put_hero_log(user_id, 102000010, 1, 0, 103000006, 0, 30086, 1001, 1002, 1003, 1005) self.game_data.item.put_hero_log(user_id, 102000010, 1, 0, 103000006, 0, 30086, 1001, 1002, 1003, 1005)
self.game_data.item.put_hero_log(user_id, 103000010, 1, 0, 112000009, 0, 30086, 1001, 1002, 1003, 1005) self.game_data.item.put_hero_log(user_id, 103000010, 1, 0, 112000009, 0, 30086, 1001, 1002, 1003, 1005)
self.game_data.item.put_hero_party(user_id, 0, 101000010, 102000010, 103000010) self.game_data.item.put_hero_party(user_id, 0, 101000010, 102000010, 103000010)
self.game_data.item.put_equipment_data(user_id, 101000016, 1, 200, 0, 0, 0)
self.game_data.item.put_equipment_data(user_id, 103000006, 1, 200, 0, 0, 0)
self.game_data.item.put_equipment_data(user_id, 112000009, 1, 200, 0, 0, 0)
self.game_data.item.put_player_quest(user_id, 1001, True, 300, 0, 0, 1)
# Force the tutorial stage to be completed due to potential crash in-game
self.logger.info(f"User Authenticated: { access_code } | { user_id }") self.logger.info(f"User Authenticated: { access_code } | { user_id }")
@@ -87,6 +113,18 @@ class SaoBase:
if user_id and not profile_data: if user_id and not profile_data:
profile_id = self.game_data.profile.create_profile(user_id) profile_id = self.game_data.profile.create_profile(user_id)
self.game_data.item.put_hero_log(user_id, 101000010, 1, 0, 101000016, 0, 30086, 1001, 1002, 1003, 1005)
self.game_data.item.put_hero_log(user_id, 102000010, 1, 0, 103000006, 0, 30086, 1001, 1002, 1003, 1005)
self.game_data.item.put_hero_log(user_id, 103000010, 1, 0, 112000009, 0, 30086, 1001, 1002, 1003, 1005)
self.game_data.item.put_hero_party(user_id, 0, 101000010, 102000010, 103000010)
self.game_data.item.put_equipment_data(user_id, 101000016, 1, 200, 0, 0, 0)
self.game_data.item.put_equipment_data(user_id, 103000006, 1, 200, 0, 0, 0)
self.game_data.item.put_equipment_data(user_id, 112000009, 1, 200, 0, 0, 0)
self.game_data.item.put_player_quest(user_id, 1001, True, 300, 0, 0, 1)
# Force the tutorial stage to be completed due to potential crash in-game
profile_data = self.game_data.profile.get_profile(user_id) profile_data = self.game_data.profile.get_profile(user_id)
resp = SaoGetAuthCardDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) resp = SaoGetAuthCardDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data)
@@ -99,7 +137,28 @@ class SaoBase:
def handle_c104(self, request: Any) -> bytes: def handle_c104(self, request: Any) -> bytes:
#common/login #common/login
access_code = bytes.fromhex(request[228:308]).decode("utf-16le") req = bytes.fromhex(request)[24:]
req_struct = Struct(
Padding(16),
"cabinet_type" / Int8ub, # cabinet_type is a byte
"auth_type" / Int8ub, # auth_type is a byte
"store_id_size" / Rebuild(Int32ub, len_(this.store_id) * 2), # calculates the length of the store_id
"store_id" / PaddedString(this.store_id_size, "utf_16_le"), # store_id is a (zero) padded string
"store_name_size" / Rebuild(Int32ub, len_(this.store_name) * 2), # calculates the length of the store_name
"store_name" / PaddedString(this.store_name_size, "utf_16_le"), # store_name is a (zero) padded string
"serial_no_size" / Rebuild(Int32ub, len_(this.serial_no) * 2), # calculates the length of the serial_no
"serial_no" / PaddedString(this.serial_no_size, "utf_16_le"), # serial_no is a (zero) padded string
"access_code_size" / Rebuild(Int32ub, len_(this.access_code) * 2), # calculates the length of the access_code
"access_code" / PaddedString(this.access_code_size, "utf_16_le"), # access_code is a (zero) padded string
"chip_id_size" / Rebuild(Int32ub, len_(this.chip_id) * 2), # calculates the length of the chip_id
"chip_id" / PaddedString(this.chip_id_size, "utf_16_le"), # chip_id is a (zero) padded string
"free_ticket_distribution_target_flag" / Int8ub, # free_ticket_distribution_target_flag is a byte
)
req_data = req_struct.parse(req)
access_code = req_data.access_code
user_id = self.core_data.card.get_user_id_from_card( access_code ) user_id = self.core_data.card.get_user_id_from_card( access_code )
profile_data = self.game_data.profile.get_profile(user_id) profile_data = self.game_data.profile.get_profile(user_id)
@@ -118,7 +177,17 @@ class SaoBase:
def handle_c500(self, request: Any) -> bytes: def handle_c500(self, request: Any) -> bytes:
#user_info/get_user_basic_data #user_info/get_user_basic_data
user_id = bytes.fromhex(request[88:112]).decode("utf-16le") req = bytes.fromhex(request)[24:]
req_struct = Struct(
Padding(16),
"user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id
"user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string
)
req_data = req_struct.parse(req)
user_id = req_data.user_id
profile_data = self.game_data.profile.get_profile(user_id) profile_data = self.game_data.profile.get_profile(user_id)
resp = SaoGetUserBasicDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) resp = SaoGetUserBasicDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data)
@@ -127,6 +196,7 @@ class SaoBase:
def handle_c600(self, request: Any) -> bytes: def handle_c600(self, request: Any) -> bytes:
#have_object/get_hero_log_user_data_list #have_object/get_hero_log_user_data_list
req = bytes.fromhex(request)[24:] req = bytes.fromhex(request)[24:]
req_struct = Struct( req_struct = Struct(
Padding(16), Padding(16),
"user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id
@@ -143,16 +213,38 @@ class SaoBase:
def handle_c602(self, request: Any) -> bytes: def handle_c602(self, request: Any) -> bytes:
#have_object/get_equipment_user_data_list #have_object/get_equipment_user_data_list
equipmentIdsData = self.game_data.static.get_equipment_ids(0, True) req = bytes.fromhex(request)[24:]
resp = SaoGetEquipmentUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, equipmentIdsData) req_struct = Struct(
Padding(16),
"user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id
"user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string
)
req_data = req_struct.parse(req)
user_id = req_data.user_id
equipment_data = self.game_data.item.get_user_equipments(user_id)
resp = SaoGetEquipmentUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, equipment_data)
return resp.make() return resp.make()
def handle_c604(self, request: Any) -> bytes: def handle_c604(self, request: Any) -> bytes:
#have_object/get_item_user_data_list #have_object/get_item_user_data_list
itemIdsData = self.game_data.static.get_item_ids(0, True) req = bytes.fromhex(request)[24:]
resp = SaoGetItemUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, itemIdsData) req_struct = Struct(
Padding(16),
"user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id
"user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string
)
req_data = req_struct.parse(req)
user_id = req_data.user_id
item_data = self.game_data.item.get_user_items(user_id)
resp = SaoGetItemUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, item_data)
return resp.make() return resp.make()
def handle_c606(self, request: Any) -> bytes: def handle_c606(self, request: Any) -> bytes:
@@ -171,7 +263,17 @@ class SaoBase:
def handle_c608(self, request: Any) -> bytes: def handle_c608(self, request: Any) -> bytes:
#have_object/get_episode_append_data_list #have_object/get_episode_append_data_list
user_id = bytes.fromhex(request[88:112]).decode("utf-16le") req = bytes.fromhex(request)[24:]
req_struct = Struct(
Padding(16),
"user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id
"user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string
)
req_data = req_struct.parse(req)
user_id = req_data.user_id
profile_data = self.game_data.profile.get_profile(user_id) profile_data = self.game_data.profile.get_profile(user_id)
resp = SaoGetEpisodeAppendDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) resp = SaoGetEpisodeAppendDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data)
@@ -179,8 +281,8 @@ class SaoBase:
def handle_c804(self, request: Any) -> bytes: def handle_c804(self, request: Any) -> bytes:
#custom/get_party_data_list #custom/get_party_data_list
req = bytes.fromhex(request)[24:] req = bytes.fromhex(request)[24:]
req_struct = Struct( req_struct = Struct(
Padding(16), Padding(16),
"user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id
@@ -210,8 +312,20 @@ class SaoBase:
def handle_c900(self, request: Any) -> bytes: def handle_c900(self, request: Any) -> bytes:
#quest/get_quest_scene_user_data_list // QuestScene.csv #quest/get_quest_scene_user_data_list // QuestScene.csv
questIdsData = self.game_data.static.get_quests_ids(0, True) req = bytes.fromhex(request)[24:]
resp = SaoGetQuestSceneUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, questIdsData)
req_struct = Struct(
Padding(16),
"user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id
"user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string
)
req_data = req_struct.parse(req)
user_id = req_data.user_id
quest_data = self.game_data.item.get_quest_logs(user_id)
resp = SaoGetQuestSceneUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, quest_data)
return resp.make() return resp.make()
def handle_c400(self, request: Any) -> bytes: def handle_c400(self, request: Any) -> bytes:
@@ -229,6 +343,159 @@ class SaoBase:
resp = SaoCheckProfileCardUsedRewardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) resp = SaoCheckProfileCardUsedRewardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1)
return resp.make() return resp.make()
def handle_c814(self, request: Any) -> bytes:
#custom/synthesize_enhancement_hero_log
req = bytes.fromhex(request)[24:]
req_struct = Struct(
Padding(20),
"ticket_id" / Bytes(1), # needs to be parsed as an int
Padding(1),
"user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id
"user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string
"origin_user_hero_log_id_size" / Rebuild(Int32ub, len_(this.origin_user_hero_log_id) * 2), # calculates the length of the origin_user_hero_log_id
"origin_user_hero_log_id" / PaddedString(this.origin_user_hero_log_id_size, "utf_16_le"), # origin_user_hero_log_id is a (zero) padded string
Padding(3),
"material_common_reward_user_data_list_length" / Rebuild(Int8ub, len_(this.material_common_reward_user_data_list)), # material_common_reward_user_data_list is a byte,
"material_common_reward_user_data_list" / Array(this.material_common_reward_user_data_list_length, Struct(
"common_reward_type" / Int16ub, # team_no is a byte
"user_common_reward_id_size" / Rebuild(Int32ub, len_(this.user_common_reward_id) * 2), # calculates the length of the user_common_reward_id
"user_common_reward_id" / PaddedString(this.user_common_reward_id_size, "utf_16_le"), # user_common_reward_id is a (zero) padded string
)),
)
req_data = req_struct.parse(req)
user_id = req_data.user_id
synthesize_hero_log_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.origin_user_hero_log_id)
for i in range(0,req_data.material_common_reward_user_data_list_length):
itemList = self.game_data.static.get_item_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id)
heroList = self.game_data.static.get_hero_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id)
equipmentList = self.game_data.static.get_equipment_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id)
if itemList:
hero_exp = 2000 + int(synthesize_hero_log_data["log_exp"])
self.game_data.item.remove_item(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id)
if equipmentList:
equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id)
hero_exp = int(equipment_data["enhancement_exp"]) + int(synthesize_hero_log_data["log_exp"])
self.game_data.item.remove_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id)
if heroList:
hero_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id)
hero_exp = int(hero_data["log_exp"]) + int(synthesize_hero_log_data["log_exp"])
self.game_data.item.remove_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id)
self.game_data.item.put_hero_log(
user_id,
int(req_data.origin_user_hero_log_id),
synthesize_hero_log_data["log_level"],
hero_exp,
synthesize_hero_log_data["main_weapon"],
synthesize_hero_log_data["sub_equipment"],
synthesize_hero_log_data["skill_slot1_skill_id"],
synthesize_hero_log_data["skill_slot2_skill_id"],
synthesize_hero_log_data["skill_slot3_skill_id"],
synthesize_hero_log_data["skill_slot4_skill_id"],
synthesize_hero_log_data["skill_slot5_skill_id"]
)
profile = self.game_data.profile.get_profile(req_data.user_id)
new_col = int(profile["own_col"]) - 100
# Update profile
self.game_data.profile.put_profile(
req_data.user_id,
profile["user_type"],
profile["nick_name"],
profile["rank_num"],
profile["rank_exp"],
new_col,
profile["own_vp"],
profile["own_yui_medal"],
profile["setting_title_id"]
)
# Load the item again to push to the response handler
synthesize_hero_log_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.origin_user_hero_log_id)
resp = SaoSynthesizeEnhancementHeroLogResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, synthesize_hero_log_data)
return resp.make()
def handle_c816(self, request: Any) -> bytes:
#custom/synthesize_enhancement_equipment
req = bytes.fromhex(request)[24:]
req_struct = Struct(
Padding(20),
"ticket_id" / Bytes(1), # needs to be parsed as an int
Padding(1),
"user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id
"user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string
"origin_user_equipment_id_size" / Rebuild(Int32ub, len_(this.origin_user_equipment_id) * 2), # calculates the length of the origin_user_equipment_id
"origin_user_equipment_id" / PaddedString(this.origin_user_equipment_id_size, "utf_16_le"), # origin_user_equipment_id is a (zero) padded string
Padding(3),
"material_common_reward_user_data_list_length" / Rebuild(Int8ub, len_(this.material_common_reward_user_data_list)), # material_common_reward_user_data_list is a byte,
"material_common_reward_user_data_list" / Array(this.material_common_reward_user_data_list_length, Struct(
"common_reward_type" / Int16ub, # team_no is a byte
"user_common_reward_id_size" / Rebuild(Int32ub, len_(this.user_common_reward_id) * 2), # calculates the length of the user_common_reward_id
"user_common_reward_id" / PaddedString(this.user_common_reward_id_size, "utf_16_le"), # user_common_reward_id is a (zero) padded string
)),
)
req_data = req_struct.parse(req)
user_id = req_data.user_id
synthesize_equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.origin_user_equipment_id)
for i in range(0,req_data.material_common_reward_user_data_list_length):
itemList = self.game_data.static.get_item_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id)
heroList = self.game_data.static.get_hero_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id)
equipmentList = self.game_data.static.get_equipment_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id)
if itemList:
equipment_exp = 2000 + int(synthesize_equipment_data["enhancement_exp"])
self.game_data.item.remove_item(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id)
if equipmentList:
equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id)
equipment_exp = int(equipment_data["enhancement_exp"]) + int(synthesize_equipment_data["enhancement_exp"])
self.game_data.item.remove_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id)
if heroList:
hero_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id)
equipment_exp = int(hero_data["log_exp"]) + int(synthesize_equipment_data["enhancement_exp"])
self.game_data.item.remove_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id)
self.game_data.item.put_equipment_data(req_data.user_id, int(req_data.origin_user_equipment_id), synthesize_equipment_data["enhancement_value"], equipment_exp, 0, 0, 0)
profile = self.game_data.profile.get_profile(req_data.user_id)
new_col = int(profile["own_col"]) - 100
# Update profile
self.game_data.profile.put_profile(
req_data.user_id,
profile["user_type"],
profile["nick_name"],
profile["rank_num"],
profile["rank_exp"],
new_col,
profile["own_vp"],
profile["own_yui_medal"],
profile["setting_title_id"]
)
# Load the item again to push to the response handler
synthesize_equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.origin_user_equipment_id)
resp = SaoSynthesizeEnhancementEquipmentResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, synthesize_equipment_data)
return resp.make()
def handle_c806(self, request: Any) -> bytes: def handle_c806(self, request: Any) -> bytes:
#custom/change_party #custom/change_party
req = bytes.fromhex(request)[24:] req = bytes.fromhex(request)[24:]
@@ -270,6 +537,7 @@ class SaoBase:
req_data = req_struct.parse(req) req_data = req_struct.parse(req)
user_id = req_data.user_id user_id = req_data.user_id
party_hero_list = []
for party_team in req_data.party_data_list[0].party_team_data_list: for party_team in req_data.party_data_list[0].party_team_data_list:
hero_data = self.game_data.item.get_hero_log(user_id, party_team["user_hero_log_id"]) hero_data = self.game_data.item.get_hero_log(user_id, party_team["user_hero_log_id"])
@@ -294,12 +562,15 @@ class SaoBase:
party_team["skill_slot5_skill_id"] party_team["skill_slot5_skill_id"]
) )
party_hero_list.append(party_team["user_hero_log_id"])
self.game_data.item.put_hero_party(user_id, req_data.party_data_list[0].party_team_data_list[0].user_party_team_id, party_hero_list[0], party_hero_list[1], party_hero_list[2])
resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1)
return resp.make() return resp.make()
def handle_c904(self, request: Any) -> bytes: def handle_c904(self, request: Any) -> bytes:
#quest/episode_play_start #quest/episode_play_start
req = bytes.fromhex(request)[24:] req = bytes.fromhex(request)[24:]
req_struct = Struct( req_struct = Struct(
@@ -432,8 +703,349 @@ class SaoBase:
req_data = req_struct.parse(req) req_data = req_struct.parse(req)
# Add stage progression to database
user_id = req_data.user_id
episode_id = req_data.episode_id
quest_clear_flag = bool(req_data.score_data[0].boss_destroying_num)
clear_time = req_data.score_data[0].clear_time
combo_num = req_data.score_data[0].combo_num
total_damage = req_data.score_data[0].total_damage
concurrent_destroying_num = req_data.score_data[0].concurrent_destroying_num
profile = self.game_data.profile.get_profile(user_id)
vp = int(profile["own_vp"])
exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason
col = int(profile["own_col"]) + int(req_data.base_get_data[0].get_col)
if quest_clear_flag is True:
# Save stage progression - to be revised to avoid saving worse score
# Reference Episode.csv but Chapter 2,3,4 and 5 reports id -1, match using /10 + last digits
if episode_id > 10000 and episode_id < 11000:
# Starts at 1001
episode_id = episode_id - 9000
elif episode_id > 20000:
# Starts at 2001
stage_id = str(episode_id)[-2:]
episode_id = episode_id / 10
episode_id = int(episode_id) + int(stage_id)
# Match episode_id with the questSceneId saved in the DB through sortNo
questId = self.game_data.static.get_quests_id(episode_id)
episode_id = questId[2]
self.game_data.item.put_player_quest(user_id, episode_id, quest_clear_flag, clear_time, combo_num, total_damage, concurrent_destroying_num)
vp = int(profile["own_vp"]) + 10 #always 10 VP per cleared stage
# Calculate level based off experience and the CSV list
with open(r'titles/sao/data/PlayerRank.csv') as csv_file:
csv_reader = csv.reader(csv_file, delimiter=',')
line_count = 0
data = []
rowf = False
for row in csv_reader:
if rowf==False:
rowf=True
else:
data.append(row)
for i in range(0,len(data)):
if exp>=int(data[i][1]) and exp<int(data[i+1][1]):
player_level = int(data[i][0])
break
# Update profile
updated_profile = self.game_data.profile.put_profile(
user_id,
profile["user_type"],
profile["nick_name"],
player_level,
exp,
col,
vp,
profile["own_yui_medal"],
profile["setting_title_id"]
)
# Update heroes from the used party
play_session = self.game_data.item.get_session(user_id)
session_party = self.game_data.item.get_hero_party(user_id, play_session["user_party_team_id"])
hero_list = []
hero_list.append(session_party["user_hero_log_id_1"])
hero_list.append(session_party["user_hero_log_id_2"])
hero_list.append(session_party["user_hero_log_id_3"])
for i in range(0,len(hero_list)):
hero_data = self.game_data.item.get_hero_log(user_id, hero_list[i])
log_exp = int(hero_data["log_exp"]) + int(req_data.base_get_data[0].get_hero_log_exp)
# Calculate hero level based off experience and the CSV list
with open(r'titles/sao/data/HeroLogLevel.csv') as csv_file:
csv_reader = csv.reader(csv_file, delimiter=',')
line_count = 0
data = []
rowf = False
for row in csv_reader:
if rowf==False:
rowf=True
else:
data.append(row)
for e in range(0,len(data)):
if log_exp>=int(data[e][1]) and log_exp<int(data[e+1][1]):
hero_level = int(data[e][0])
break
self.game_data.item.put_hero_log(
user_id,
hero_data["user_hero_log_id"],
hero_level,
log_exp,
hero_data["main_weapon"],
hero_data["sub_equipment"],
hero_data["skill_slot1_skill_id"],
hero_data["skill_slot2_skill_id"],
hero_data["skill_slot3_skill_id"],
hero_data["skill_slot4_skill_id"],
hero_data["skill_slot5_skill_id"]
)
# Grab the rare loot from the table, match it with the right item and then push to the player profile
json_data = {"data": []}
for r in range(0,req_data.get_rare_drop_data_list_length):
rewardList = self.game_data.static.get_rare_drop_id(int(req_data.get_rare_drop_data_list[r].quest_rare_drop_id))
commonRewardId = rewardList["commonRewardId"]
heroList = self.game_data.static.get_hero_id(commonRewardId)
equipmentList = self.game_data.static.get_equipment_id(commonRewardId)
itemList = self.game_data.static.get_item_id(commonRewardId)
if heroList:
self.game_data.item.put_hero_log(user_id, commonRewardId, 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0)
if equipmentList:
self.game_data.item.put_equipment_data(user_id, commonRewardId, 1, 200, 0, 0, 0)
if itemList:
self.game_data.item.put_item(user_id, commonRewardId)
# Generate random hero(es) based off the response
for a in range(0,req_data.get_unanalyzed_log_tmp_reward_data_list_length):
with open('titles/sao/data/RewardTable.csv', 'r') as f:
keys_unanalyzed = next(f).strip().split(',')
data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed))
randomized_unanalyzed_id = choice(data_unanalyzed)
while int(randomized_unanalyzed_id['UnanalyzedLogGradeId']) != req_data.get_unanalyzed_log_tmp_reward_data_list[a].unanalyzed_log_grade_id:
randomized_unanalyzed_id = choice(data_unanalyzed)
heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId'])
equipmentList = self.game_data.static.get_equipment_id(randomized_unanalyzed_id['CommonRewardId'])
itemList = self.game_data.static.get_item_id(randomized_unanalyzed_id['CommonRewardId'])
if heroList:
self.game_data.item.put_hero_log(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0)
if equipmentList:
self.game_data.item.put_equipment_data(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 200, 0, 0, 0)
if itemList:
self.game_data.item.put_item(user_id, randomized_unanalyzed_id['CommonRewardId'])
json_data["data"].append(randomized_unanalyzed_id['CommonRewardId'])
# Send response
self.game_data.item.create_end_session(user_id, episode_id, quest_clear_flag, json_data["data"])
resp = SaoEpisodePlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1)
return resp.make()
def handle_c914(self, request: Any) -> bytes:
#quest/trial_tower_play_start
req = bytes.fromhex(request)[24:]
req_struct = Struct(
Padding(16),
"ticket_id_size" / Rebuild(Int32ub, len_(this.ticket_id) * 2), # calculates the length of the ticket_id
"ticket_id" / PaddedString(this.ticket_id_size, "utf_16_le"), # ticket_id is a (zero) padded string
"user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id
"user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string
"trial_tower_id" / Int32ub, # trial_tower_id is an int
"play_mode" / Int8ub, # play_mode is a byte
Padding(3),
"play_start_request_data_length" / Rebuild(Int8ub, len_(this.play_start_request_data)), # play_start_request_data_length is a byte,
"play_start_request_data" / Array(this.play_start_request_data_length, Struct(
"user_party_id_size" / Rebuild(Int32ub, len_(this.user_party_id) * 2), # calculates the length of the user_party_id
"user_party_id" / PaddedString(this.user_party_id_size, "utf_16_le"), # user_party_id is a (zero) padded string
"appoint_leader_resource_card_code_size" / Rebuild(Int32ub, len_(this.appoint_leader_resource_card_code) * 2), # calculates the length of the total_damage
"appoint_leader_resource_card_code" / PaddedString(this.appoint_leader_resource_card_code_size, "utf_16_le"), # total_damage is a (zero) padded string
"use_profile_card_code_size" / Rebuild(Int32ub, len_(this.use_profile_card_code) * 2), # calculates the length of the total_damage
"use_profile_card_code" / PaddedString(this.use_profile_card_code_size, "utf_16_le"), # use_profile_card_code is a (zero) padded string
"quest_drop_boost_apply_flag" / Int8ub, # quest_drop_boost_apply_flag is a byte
)),
)
req_data = req_struct.parse(req)
user_id = req_data.user_id
floor_id = req_data.trial_tower_id
profile_data = self.game_data.profile.get_profile(user_id)
self.game_data.item.create_session(
user_id,
int(req_data.play_start_request_data[0].user_party_id),
req_data.trial_tower_id,
req_data.play_mode,
req_data.play_start_request_data[0].quest_drop_boost_apply_flag
)
resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data)
return resp.make()
def handle_c918(self, request: Any) -> bytes:
#quest/trial_tower_play_end
req = bytes.fromhex(request)[24:]
req_struct = Struct(
Padding(20),
"ticket_id" / Bytes(1), # needs to be parsed as an int
Padding(1),
"user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id
"user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string
Padding(2),
"trial_tower_id" / Int16ub, # trial_tower_id is a short,
Padding(3),
"play_end_request_data" / Int8ub, # play_end_request_data is a byte
Padding(1),
"play_result_flag" / Int8ub, # play_result_flag is a byte
Padding(2),
"base_get_data_length" / Rebuild(Int8ub, len_(this.base_get_data)), # base_get_data_length is a byte,
"base_get_data" / Array(this.base_get_data_length, Struct(
"get_hero_log_exp" / Int32ub, # get_hero_log_exp is an int
"get_col" / Int32ub, # get_num is a short
)),
Padding(3),
"get_player_trace_data_list_length" / Rebuild(Int8ub, len_(this.get_player_trace_data_list)), # get_player_trace_data_list_length is a byte
"get_player_trace_data_list" / Array(this.get_player_trace_data_list_length, Struct(
"user_quest_scene_player_trace_id" / Int32ub, # user_quest_scene_player_trace_id is an int
)),
Padding(3),
"get_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_rare_drop_data_list)), # get_rare_drop_data_list_length is a byte
"get_rare_drop_data_list" / Array(this.get_rare_drop_data_list_length, Struct(
"quest_rare_drop_id" / Int32ub, # quest_rare_drop_id is an int
)),
Padding(3),
"get_special_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_special_rare_drop_data_list)), # get_special_rare_drop_data_list_length is a byte
"get_special_rare_drop_data_list" / Array(this.get_special_rare_drop_data_list_length, Struct(
"quest_special_rare_drop_id" / Int32ub, # quest_special_rare_drop_id is an int
)),
Padding(3),
"get_unanalyzed_log_tmp_reward_data_list_length" / Rebuild(Int8ub, len_(this.get_unanalyzed_log_tmp_reward_data_list)), # get_unanalyzed_log_tmp_reward_data_list_length is a byte
"get_unanalyzed_log_tmp_reward_data_list" / Array(this.get_unanalyzed_log_tmp_reward_data_list_length, Struct(
"unanalyzed_log_grade_id" / Int32ub, # unanalyzed_log_grade_id is an int,
)),
Padding(3),
"get_event_item_data_list_length" / Rebuild(Int8ub, len_(this.get_event_item_data_list)), # get_event_item_data_list_length is a byte,
"get_event_item_data_list" / Array(this.get_event_item_data_list_length, Struct(
"event_item_id" / Int32ub, # event_item_id is an int
"get_num" / Int16ub, # get_num is a short
)),
Padding(3),
"discovery_enemy_data_list_length" / Rebuild(Int8ub, len_(this.discovery_enemy_data_list)), # discovery_enemy_data_list_length is a byte
"discovery_enemy_data_list" / Array(this.discovery_enemy_data_list_length, Struct(
"enemy_kind_id" / Int32ub, # enemy_kind_id is an int
"destroy_num" / Int16ub, # destroy_num is a short
)),
Padding(3),
"destroy_boss_data_list_length" / Rebuild(Int8ub, len_(this.destroy_boss_data_list)), # destroy_boss_data_list_length is a byte
"destroy_boss_data_list" / Array(this.destroy_boss_data_list_length, Struct(
"boss_type" / Int8ub, # boss_type is a byte
"enemy_kind_id" / Int32ub, # enemy_kind_id is an int
"destroy_num" / Int16ub, # destroy_num is a short
)),
Padding(3),
"mission_data_list_length" / Rebuild(Int8ub, len_(this.mission_data_list)), # mission_data_list_length is a byte
"mission_data_list" / Array(this.mission_data_list_length, Struct(
"mission_id" / Int32ub, # enemy_kind_id is an int
"clear_flag" / Int8ub, # boss_type is a byte
"mission_difficulty_id" / Int16ub, # destroy_num is a short
)),
Padding(3),
"score_data_length" / Rebuild(Int8ub, len_(this.score_data)), # score_data_length is a byte
"score_data" / Array(this.score_data_length, Struct(
"clear_time" / Int32ub, # clear_time is an int
"combo_num" / Int32ub, # boss_type is a int
"total_damage_size" / Rebuild(Int32ub, len_(this.total_damage) * 2), # calculates the length of the total_damage
"total_damage" / PaddedString(this.total_damage_size, "utf_16_le"), # total_damage is a (zero) padded string
"concurrent_destroying_num" / Int16ub, # concurrent_destroying_num is a short
"reaching_skill_level" / Int16ub, # reaching_skill_level is a short
"ko_chara_num" / Int8ub, # ko_chara_num is a byte
"acceleration_invocation_num" / Int16ub, # acceleration_invocation_num is a short
"boss_destroying_num" / Int16ub, # boss_destroying_num is a short
"synchro_skill_used_flag" / Int8ub, # synchro_skill_used_flag is a byte
"used_friend_skill_id" / Int32ub, # used_friend_skill_id is an int
"friend_skill_used_flag" / Int8ub, # friend_skill_used_flag is a byte
"continue_cnt" / Int16ub, # continue_cnt is a short
"total_loss_num" / Int16ub, # total_loss_num is a short
)),
)
req_data = req_struct.parse(req)
# Add tower progression to database
user_id = req_data.user_id
trial_tower_id = req_data.trial_tower_id
next_tower_id = 0
quest_clear_flag = bool(req_data.score_data[0].boss_destroying_num)
clear_time = req_data.score_data[0].clear_time
combo_num = req_data.score_data[0].combo_num
total_damage = req_data.score_data[0].total_damage
concurrent_destroying_num = req_data.score_data[0].concurrent_destroying_num
if quest_clear_flag is True:
# Save tower progression - to be revised to avoid saving worse score
if trial_tower_id == 9:
next_tower_id = 10001
elif trial_tower_id == 10:
trial_tower_id = 10001
next_tower_id = 3011
elif trial_tower_id == 19:
next_tower_id = 10002
elif trial_tower_id == 20:
trial_tower_id = 10002
next_tower_id = 3021
elif trial_tower_id == 29:
next_tower_id = 10003
elif trial_tower_id == 30:
trial_tower_id = 10003
next_tower_id = 3031
elif trial_tower_id == 39:
next_tower_id = 10004
elif trial_tower_id == 40:
trial_tower_id = 10004
next_tower_id = 3041
elif trial_tower_id == 49:
next_tower_id = 10005
elif trial_tower_id == 50:
trial_tower_id = 10005
next_tower_id = 3051
else:
trial_tower_id = trial_tower_id + 3000
next_tower_id = trial_tower_id + 1
self.game_data.item.put_player_quest(user_id, trial_tower_id, quest_clear_flag, clear_time, combo_num, total_damage, concurrent_destroying_num)
# Check if next stage is already done
checkQuest = self.game_data.item.get_quest_log(user_id, next_tower_id)
if not checkQuest:
if next_tower_id != 3101:
self.game_data.item.put_player_quest(user_id, next_tower_id, 0, 0, 0, 0, 0)
# Update the profile # Update the profile
profile = self.game_data.profile.get_profile(req_data.user_id) profile = self.game_data.profile.get_profile(user_id)
exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason
col = int(profile["own_col"]) + int(req_data.base_get_data[0].get_col) col = int(profile["own_col"]) + int(req_data.base_get_data[0].get_col)
@@ -455,9 +1067,8 @@ class SaoBase:
player_level = int(data[i][0]) player_level = int(data[i][0])
break break
# Update profile
updated_profile = self.game_data.profile.put_profile( updated_profile = self.game_data.profile.put_profile(
req_data.user_id, user_id,
profile["user_type"], profile["user_type"],
profile["nick_name"], profile["nick_name"],
player_level, player_level,
@@ -469,8 +1080,8 @@ class SaoBase:
) )
# Update heroes from the used party # Update heroes from the used party
play_session = self.game_data.item.get_session(req_data.user_id) play_session = self.game_data.item.get_session(user_id)
session_party = self.game_data.item.get_hero_party(req_data.user_id, play_session["user_party_team_id"]) session_party = self.game_data.item.get_hero_party(user_id, play_session["user_party_team_id"])
hero_list = [] hero_list = []
hero_list.append(session_party["user_hero_log_id_1"]) hero_list.append(session_party["user_hero_log_id_1"])
@@ -478,14 +1089,31 @@ class SaoBase:
hero_list.append(session_party["user_hero_log_id_3"]) hero_list.append(session_party["user_hero_log_id_3"])
for i in range(0,len(hero_list)): for i in range(0,len(hero_list)):
hero_data = self.game_data.item.get_hero_log(req_data.user_id, hero_list[i]) hero_data = self.game_data.item.get_hero_log(user_id, hero_list[i])
log_exp = int(hero_data["log_exp"]) + int(req_data.base_get_data[0].get_hero_log_exp) log_exp = int(hero_data["log_exp"]) + int(req_data.base_get_data[0].get_hero_log_exp)
# Calculate hero level based off experience and the CSV list
with open(r'titles/sao/data/HeroLogLevel.csv') as csv_file:
csv_reader = csv.reader(csv_file, delimiter=',')
line_count = 0
data = []
rowf = False
for row in csv_reader:
if rowf==False:
rowf=True
else:
data.append(row)
for e in range(0,len(data)):
if log_exp>=int(data[e][1]) and log_exp<int(data[e+1][1]):
hero_level = int(data[e][0])
break
self.game_data.item.put_hero_log( self.game_data.item.put_hero_log(
req_data.user_id, user_id,
hero_data["user_hero_log_id"], hero_data["user_hero_log_id"],
hero_data["log_level"], hero_level,
log_exp, log_exp,
hero_data["main_weapon"], hero_data["main_weapon"],
hero_data["sub_equipment"], hero_data["sub_equipment"],
@@ -496,19 +1124,121 @@ class SaoBase:
hero_data["skill_slot5_skill_id"] hero_data["skill_slot5_skill_id"]
) )
resp = SaoEpisodePlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) json_data = {"data": []}
# Grab the rare loot from the table, match it with the right item and then push to the player profile
for r in range(0,req_data.get_rare_drop_data_list_length):
rewardList = self.game_data.static.get_rare_drop_id(int(req_data.get_rare_drop_data_list[r].quest_rare_drop_id))
commonRewardId = rewardList["commonRewardId"]
heroList = self.game_data.static.get_hero_id(commonRewardId)
equipmentList = self.game_data.static.get_equipment_id(commonRewardId)
itemList = self.game_data.static.get_item_id(commonRewardId)
if heroList:
self.game_data.item.put_hero_log(user_id, commonRewardId, 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0)
if equipmentList:
self.game_data.item.put_equipment_data(user_id, commonRewardId, 1, 200, 0, 0, 0)
if itemList:
self.game_data.item.put_item(user_id, commonRewardId)
# Generate random hero(es) based off the response
for a in range(0,req_data.get_unanalyzed_log_tmp_reward_data_list_length):
with open('titles/sao/data/RewardTable.csv', 'r') as f:
keys_unanalyzed = next(f).strip().split(',')
data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed))
randomized_unanalyzed_id = choice(data_unanalyzed)
while int(randomized_unanalyzed_id['UnanalyzedLogGradeId']) != req_data.get_unanalyzed_log_tmp_reward_data_list[a].unanalyzed_log_grade_id:
randomized_unanalyzed_id = choice(data_unanalyzed)
heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId'])
equipmentList = self.game_data.static.get_equipment_id(randomized_unanalyzed_id['CommonRewardId'])
itemList = self.game_data.static.get_item_id(randomized_unanalyzed_id['CommonRewardId'])
if heroList:
self.game_data.item.put_hero_log(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0)
if equipmentList:
self.game_data.item.put_equipment_data(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 200, 0, 0, 0)
if itemList:
self.game_data.item.put_item(user_id, randomized_unanalyzed_id['CommonRewardId'])
json_data["data"].append(randomized_unanalyzed_id['CommonRewardId'])
# Send response
self.game_data.item.create_end_session(user_id, trial_tower_id, quest_clear_flag, json_data["data"])
resp = SaoTrialTowerPlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1)
return resp.make() return resp.make()
def handle_c914(self, request: Any) -> bytes: def handle_c90a(self, request: Any) -> bytes:
#quest/trial_tower_play_start
user_id = bytes.fromhex(request[100:124]).decode("utf-16le")
floor_id = int(request[130:132], 16) # not required but nice to know
profile_data = self.game_data.profile.get_profile(user_id)
resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data)
return resp.make()
def handle_c90a(self, request: Any) -> bytes: #should be tweaked for proper item unlock
#quest/episode_play_end_unanalyzed_log_fixed #quest/episode_play_end_unanalyzed_log_fixed
resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1)
req = bytes.fromhex(request)[24:]
req_struct = Struct(
Padding(16),
"ticket_id_size" / Rebuild(Int32ub, len_(this.ticket_id) * 2), # calculates the length of the ticket_id
"ticket_id" / PaddedString(this.ticket_id_size, "utf_16_le"), # ticket_id is a (zero) padded string
"user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id
"user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string
)
req_data = req_struct.parse(req)
user_id = req_data.user_id
end_session_data = self.game_data.item.get_end_session(user_id)
resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, end_session_data[4])
return resp.make()
def handle_c91a(self, request: Any) -> bytes: # handler is identical to the episode
#quest/trial_tower_play_end_unanalyzed_log_fixed
req = bytes.fromhex(request)[24:]
req_struct = Struct(
Padding(16),
"ticket_id_size" / Rebuild(Int32ub, len_(this.ticket_id) * 2), # calculates the length of the ticket_id
"ticket_id" / PaddedString(this.ticket_id_size, "utf_16_le"), # ticket_id is a (zero) padded string
"user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id
"user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string
)
req_data = req_struct.parse(req)
user_id = req_data.user_id
end_session_data = self.game_data.item.get_end_session(user_id)
resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, end_session_data[4])
return resp.make()
def handle_cd00(self, request: Any) -> bytes:
#defrag_match/get_defrag_match_basic_data
resp = SaoGetDefragMatchBasicDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1)
return resp.make()
def handle_cd02(self, request: Any) -> bytes:
#defrag_match/get_defrag_match_ranking_user_data
resp = SaoGetDefragMatchRankingUserDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1)
return resp.make()
def handle_cd04(self, request: Any) -> bytes:
#defrag_match/get_defrag_match_league_point_ranking_list
resp = SaoGetDefragMatchLeaguePointRankingListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1)
return resp.make()
def handle_cd06(self, request: Any) -> bytes:
#defrag_match/get_defrag_match_league_score_ranking_list
resp = SaoGetDefragMatchLeagueScoreRankingListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1)
return resp.make()
def handle_d404(self, request: Any) -> bytes:
#other/bnid_serial_code_check
resp = SaoBnidSerialCodeCheckResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1)
return resp.make()
def handle_c306(self, request: Any) -> bytes:
#card/scan_qr_quest_profile_card
resp = SaoScanQrQuestProfileCardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1)
return resp.make() return resp.make()

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