Compare commits
145 Commits
cardmaker_
...
frontend_u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c51103aaf5 | ||
|
|
08927db100 | ||
|
|
238e39f415 | ||
|
|
5499d38bb4 | ||
|
|
3a6cfedcca | ||
|
|
7a6272dcc5 | ||
|
|
136e47d1e6 | ||
|
|
dac655b4ae | ||
|
|
37e2da2051 | ||
|
|
9d74d60c14 | ||
|
|
d4ea3bc12a | ||
|
|
d8b0e2ea2a | ||
|
|
984949d902 | ||
|
|
2e8d99e5fa | ||
|
|
fd6cadf2da | ||
|
|
71489c1272 | ||
|
|
3773c57de1 | ||
|
|
8ea82ffe1a | ||
|
|
904ea10920 | ||
|
|
8a8c0e023e | ||
|
|
cf7cc0997a | ||
|
|
92567504f4 | ||
|
|
fd50a7ee68 | ||
|
|
9e3a51a57a | ||
|
|
4744e8cf5f | ||
|
|
88a1462304 | ||
|
|
2e277e7791 | ||
|
|
757fdc5c57 | ||
|
|
23bcb5cc13 | ||
|
|
9f0c181593 | ||
|
|
5a4baba102 | ||
|
|
156b4e4ede | ||
|
|
6c89a97fe3 | ||
|
|
b943807904 | ||
|
|
f417be671b | ||
|
|
20335aaebe | ||
|
|
097181008b | ||
|
|
63d81a2704 | ||
|
|
718229b267 | ||
|
|
7c78975431 | ||
|
|
14a315a673 | ||
|
|
343fe4357c | ||
|
|
d0e43140ba | ||
|
|
859bf4bf5d | ||
|
|
c6e7100f51 | ||
|
|
b0ca37815b | ||
|
|
f39317301b | ||
|
|
389784ce82 | ||
|
|
2f13596885 | ||
|
|
6a41dac46c | ||
|
|
85b73e634d | ||
|
|
09c4f8cda4 | ||
|
|
36d338e618 | ||
|
|
03cf535ff6 | ||
|
|
6c155a5e48 | ||
|
|
737312ca3d | ||
|
|
1edec7dba2 | ||
|
|
d60f827000 | ||
|
|
da422e602b | ||
|
|
d276ac8598 | ||
|
|
84e880e94f | ||
|
|
432177957a | ||
|
|
a89247cdd6 | ||
|
|
f279adb894 | ||
|
|
a680699939 | ||
|
|
d204954447 | ||
|
|
042440c76e | ||
|
|
c4c0566cd5 | ||
|
|
3e9cec3a20 | ||
|
|
8f9584c3d2 | ||
|
|
b29cb0fbaa | ||
|
|
d9a92f5865 | ||
|
|
9859ab4fdb | ||
|
|
d89eb61e62 | ||
|
|
dc8c27046e | ||
|
|
3e461f4d71 | ||
|
|
2c6902a546 | ||
|
|
318b73dd57 | ||
|
|
9d33091bb8 | ||
|
|
8b43d554fc | ||
|
|
610ef70bad | ||
|
|
60b3bc7750 | ||
|
|
4ea83f6025 | ||
|
|
20389011e9 | ||
|
|
e446816b9a | ||
|
|
9dd2b4d524 | ||
|
|
b60cf6258d | ||
|
|
127e6f8aa8 | ||
|
|
5155353360 | ||
|
|
e3d38dacde | ||
|
|
0c6d9a36ce | ||
|
|
b1968fe320 | ||
|
|
03f91d18c9 | ||
|
|
17508f09b2 | ||
|
|
aae4afe7b8 | ||
|
|
514f786e2d | ||
|
|
ec9ad1ebb0 | ||
|
|
08ebb5c907 | ||
|
|
571b92d0cd | ||
|
|
01b5282899 | ||
|
|
391edd3354 | ||
|
|
d5bff0e891 | ||
|
|
402e753469 | ||
|
|
154ccbdae5 | ||
|
|
858b101a36 | ||
|
|
1d10e798a5 | ||
|
|
3c385f505b | ||
|
|
b12938bcd8 | ||
|
|
1b2f5e3709 | ||
|
|
65686fb615 | ||
|
|
f56332141e | ||
|
|
5a35b1c823 | ||
|
|
5ca16f2067 | ||
|
|
a0b25e2b7b | ||
|
|
84fc002cdb | ||
|
|
3bd03c592e | ||
|
|
cf6cfdbd3b | ||
|
|
db77e61b79 | ||
|
|
ac9e71ee2f | ||
|
|
20865dc495 | ||
|
|
37d24b3b4d | ||
|
|
2418abacce | ||
|
|
5c3f812caf | ||
|
|
4854bcfcad | ||
|
|
bf6d126f8a | ||
|
|
e466ddce55 | ||
|
|
02078080a8 | ||
|
|
61e3a2c930 | ||
|
|
8ae0aba89c | ||
|
|
49166c1a7b | ||
|
|
42ed222095 | ||
|
|
d172e5582b | ||
|
|
9766e3ab78 | ||
|
|
b34b441ba8 | ||
|
|
8149f09a40 | ||
|
|
cad523dfce | ||
|
|
8b9771b5af | ||
|
|
989c080657 | ||
|
|
dcff8adbab | ||
|
|
e3b1addce6 | ||
|
|
b6f43d887a | ||
|
|
7bb8c2c80c | ||
|
|
8d94d25893 | ||
|
|
ae6dcb68df | ||
|
|
deeac1d8db |
50
changelog.md
50
changelog.md
@@ -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
|
||||||
|
|||||||
6
core/adb_handlers/__init__.py
Normal file
6
core/adb_handlers/__init__.py
Normal 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
170
core/adb_handlers/base.py
Normal 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()
|
||||||
132
core/adb_handlers/campaign.py
Normal file
132
core/adb_handlers/campaign.py
Normal 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
|
||||||
84
core/adb_handlers/felica.py
Normal file
84
core/adb_handlers/felica.py
Normal 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
56
core/adb_handlers/log.py
Normal 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
|
||||||
81
core/adb_handlers/lookup.py
Normal file
81
core/adb_handlers/lookup.py
Normal 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
|
||||||
352
core/aimedb.py
352
core/aimedb.py
@@ -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
|
||||||
|
|||||||
640
core/allnet.py
640
core/allnet.py
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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']}")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
3
core/data/schema/versions/CORE_4_rollback.sql
Normal file
3
core/data/schema/versions/CORE_4_rollback.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE machine DROP COLUMN memo;
|
||||||
|
ALTER TABLE machine DROP COLUMN is_blacklisted;
|
||||||
|
ALTER TABLE machine DROP COLUMN `data`;
|
||||||
1
core/data/schema/versions/CORE_5_rollback.sql
Normal file
1
core/data/schema/versions/CORE_5_rollback.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE arcade DROP COLUMN 'ip';
|
||||||
3
core/data/schema/versions/CORE_5_upgrade.sql
Normal file
3
core/data/schema/versions/CORE_5_upgrade.sql
Normal 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;
|
||||||
1
core/data/schema/versions/CORE_6_upgrade.sql
Normal file
1
core/data/schema/versions/CORE_6_upgrade.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE arcade ADD ip varchar(39) NULL;
|
||||||
2
core/data/schema/versions/SDDT_4_rollback.sql
Normal file
2
core/data/schema/versions/SDDT_4_rollback.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE ongeki_static_events
|
||||||
|
DROP COLUMN startDate;
|
||||||
2
core/data/schema/versions/SDDT_5_upgrade.sql
Normal file
2
core/data/schema/versions/SDDT_5_upgrade.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE ongeki_static_events
|
||||||
|
ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp();
|
||||||
78
core/data/schema/versions/SDEZ_5_rollback.sql
Normal file
78
core/data/schema/versions/SDEZ_5_rollback.sql
Normal 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;
|
||||||
1
core/data/schema/versions/SDEZ_6_rollback.sql
Normal file
1
core/data/schema/versions/SDEZ_6_rollback.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE aime.mai2_profile_consec_logins;
|
||||||
62
core/data/schema/versions/SDEZ_6_upgrade.sql
Normal file
62
core/data/schema/versions/SDEZ_6_upgrade.sql
Normal 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;
|
||||||
9
core/data/schema/versions/SDEZ_7_upgrade.sql
Normal file
9
core/data/schema/versions/SDEZ_7_upgrade.sql
Normal 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;
|
||||||
257
core/frontend.py
257
core/frontend.py
@@ -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")
|
||||||
|
|||||||
4
core/frontend/arcade/index.jinja
Normal file
4
core/frontend/arcade/index.jinja
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{% extends "core/frontend/index.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ arcade.name }}</h1>
|
||||||
|
{% endblock content %}
|
||||||
@@ -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">
|
<!--
|
||||||
|
<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>
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
@@ -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">
|
<!--
|
||||||
|
<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-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>
|
</form>
|
||||||
<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>
|
</div>
|
||||||
<h6>*If you have not registered a card with this server, you cannot create a webui account.</h6>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
@@ -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 {
|
|
||||||
background-color: #181a1b !important;
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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 %}
|
||||||
|
</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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
5
core/frontend/machine/index.jinja
Normal file
5
core/frontend/machine/index.jinja
Normal 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 %}
|
||||||
98
core/frontend/sys/index.jinja
Normal file
98
core/frontend/sys/index.jinja
Normal 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 %}
|
||||||
@@ -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">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title">Cards</h3>
|
||||||
|
<!--<h4 class="card-subtitle mb-2 text-body-secondary">Card subtitle</h4>-->
|
||||||
|
<p class="card-text">All aime cards are listed here for the given user.</p>
|
||||||
|
<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 %}
|
{% for c in cards %}
|
||||||
<li>{{ c.access_code }}: {{ c.status }} <button class="btn-danger btn">Delete</button></li>
|
<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 %}
|
{% endfor %}
|
||||||
</ul>
|
</tbody>
|
||||||
<div class="modal fade" id="card_add" tabindex="-1" aria-labelledby="card_add_label" aria-hidden="true">
|
</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: </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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
<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>
|
|
||||||
{% 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 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>
|
</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>
|
||||||
162
core/mucha.py
162
core/mucha.py
@@ -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
|
||||||
|
|||||||
@@ -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""
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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: ""
|
||||||
|
|
||||||
|
|||||||
@@ -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: ""
|
||||||
|
|||||||
@@ -29,3 +29,9 @@ gachas:
|
|||||||
- 1156
|
- 1156
|
||||||
- 1163
|
- 1163
|
||||||
- 1164
|
- 1164
|
||||||
|
|
||||||
|
version:
|
||||||
|
6:
|
||||||
|
card_maker: 1.30.01
|
||||||
|
7:
|
||||||
|
card_maker: 1.35.03
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
70
index.py
70
index.py
@@ -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()
|
||||||
|
|||||||
8
read.py
8
read.py
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -16,3 +16,5 @@ Routes
|
|||||||
bcrypt
|
bcrypt
|
||||||
jinja2
|
jinja2
|
||||||
protobuf
|
protobuf
|
||||||
|
autobahn
|
||||||
|
pillow
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"]),
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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]}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
591
titles/mai2/dx.py
Normal 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
|
||||||
@@ -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
|
||||||
@@ -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
23
titles/mai2/finale.py
Normal 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/"
|
||||||
@@ -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"{}")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -67,12 +67,12 @@ 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
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
66
titles/pokken/services.py
Normal 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
|
||||||
@@ -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
Reference in New Issue
Block a user