parent
3b153d4184
commit
130daf0db6
|
@ -15,6 +15,7 @@ COPY dbutils.py dbutils.py
|
|||
COPY read.py read.py
|
||||
ADD core core
|
||||
ADD titles titles
|
||||
ADD config config
|
||||
ADD logs logs
|
||||
ADD cert cert
|
||||
|
||||
|
|
13
LICENSE.txt
13
LICENSE.txt
|
@ -1,13 +0,0 @@
|
|||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
109
changelog.md
109
changelog.md
|
@ -1,74 +1,14 @@
|
|||
# Changelog
|
||||
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
|
||||
|
||||
## 20240318
|
||||
### CXB
|
||||
+ Fixing handle_data_shop_list_detail_request for Sunrise S1
|
||||
|
||||
## 20240302
|
||||
### SAO
|
||||
+ Fixing new profile creation with right heroes and start VP
|
||||
+ Fix to the Unanalyzed Log responses returning the wrong rewards
|
||||
+ Documentation revised
|
||||
|
||||
## 20240226
|
||||
### CXB
|
||||
+ Fixing paths for rev.py
|
||||
+ Changed encoding for handle_data_item_list_icon_request
|
||||
|
||||
## 20240202
|
||||
### SAO
|
||||
+ Added reader assets and edited the game specific documentation
|
||||
|
||||
## 20240118
|
||||
### System
|
||||
+ Added game version names to the readme
|
||||
|
||||
## 20240109
|
||||
### System
|
||||
+ Removed `ADD config config` from dockerfile [#83](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/83) (Thanks zaphkito!)
|
||||
|
||||
### Aimedb
|
||||
+ Fixed an error that resulted from trying to scan a banned or locked card
|
||||
|
||||
## 20240108
|
||||
### System
|
||||
+ Change how the underlying system handles URLs
|
||||
+ This can now allow for things like version-specific, or even keychip-specific URLs
|
||||
+ Specific changes to games are noted below
|
||||
+ Fix docker files [#60](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/60) (Thanks Rylie!)
|
||||
+ Fix support for python 3.8 - 3.10
|
||||
|
||||
### Aimedb
|
||||
+ Add support for SegaAuth key in games that support it (for now only Chunithm)
|
||||
+ This is a JWT that is sent to games, by Aimedb, that the games send to their game server, to verify that the access code the game is sending to the server was obtained via aimedb.
|
||||
+ Requires a base64-encoded secret to be set in the `core.yaml`
|
||||
|
||||
### Chunithm
|
||||
+ Fix Air support
|
||||
+ Add saving for userRecentPlayerList
|
||||
+ Add support for SegaAuthKey
|
||||
+ Fix a bug arising if a user set their name to be 'true' or 'false'
|
||||
+ Add support for Sun+ [#78](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/78) (Thanks EmmyHeart!)
|
||||
+ Add `matching` section to `chuni.yaml`
|
||||
+ ~~Change `udpHolePunchUri` and `reflectorUri` to be STUN and TURN servers~~ Reverted
|
||||
+ Imrpove `GetGameSetting` request handling for different versions
|
||||
+ Fix issue where songs would not always return all scores [#92](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/92) (Thanks Kumubou!)
|
||||
|
||||
## 20231015
|
||||
### maimai DX
|
||||
+ Fix user charges failing to save
|
||||
+ Added support for FESTiVAL PLUS
|
||||
|
||||
### maimai
|
||||
+ Made it functional
|
||||
|
||||
### CXB
|
||||
+ Improvements to request dispatching
|
||||
+ Add support for non-omnimix music lists
|
||||
|
||||
|
||||
### IDZ
|
||||
+ Fix news urls in accordance with the system change to URLs
|
||||
### Card Maker
|
||||
+ Added support for maimai DX FESTiVAL PLUS
|
||||
|
||||
## 20231001
|
||||
### Initial D THE ARCADE
|
||||
+ Added support for Initial D THE ARCADE S2
|
||||
+ Story mode progress added
|
||||
|
@ -80,45 +20,6 @@ Documenting updates to ARTEMiS, to be updated every time the master branch is pu
|
|||
+ Frontend to download profile added
|
||||
+ Importer to import profiles added
|
||||
|
||||
### ONGEKI
|
||||
+ Now supports HTTPS on a per-version basis
|
||||
+ Merg PR [#61](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/61) (Thanks phantomlan!)
|
||||
+ Add Ranking Event Support
|
||||
+ Add reward list support
|
||||
+ Add version segregation to Event Ranking, Tech Challenge, and Music Ranking
|
||||
+ Now stores ClientTestmode and ClientSetting data
|
||||
+ Fix mission points not adding correctly [#68](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/68) (Thanks phantomlan!)
|
||||
+ Fix tech challenge [#70](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/70) (Thanks phantomlan!)
|
||||
|
||||
### SAO
|
||||
+ Change endpoint in accordance with the system change to URLs
|
||||
+ Update request header class to be more accurate
|
||||
+ Encrypted requests are now supported
|
||||
+ Change to using handler classes instead of raw structs for simplicity
|
||||
|
||||
### Wacca
|
||||
+ Fix a server error causing a seperate error that casued issues
|
||||
+ Add better error printing
|
||||
+ Add better request validation
|
||||
+ Fix HousingStartV2
|
||||
+ Fix Lily's housing/get handler
|
||||
|
||||
## 20231107
|
||||
### CXB
|
||||
+ Hotfix `render_POST` sometimes failing to read the request body on large requests
|
||||
|
||||
## 20231106
|
||||
### CXB
|
||||
+ Hotfix `render_POST` function signature signature
|
||||
+ Hotfix `handle_action_addenergy_request` hard failing if `get_energy` returns None
|
||||
|
||||
## 20231015
|
||||
### maimai DX
|
||||
+ Added support for FESTiVAL PLUS
|
||||
|
||||
### Card Maker
|
||||
+ Added support for maimai DX FESTiVAL PLUS
|
||||
|
||||
## 20230716
|
||||
### General
|
||||
+ Docker files added (#19)
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# ruff: noqa: F401, I001, I002
|
||||
# Import ordering is important here!
|
||||
from core.config import CoreConfig
|
||||
from core.allnet import AllnetServlet, BillingServlet
|
||||
from core.aimedb import AimedbServlette
|
||||
from core.allnet import AllnetServlet
|
||||
from core.aimedb import AimedbFactory
|
||||
from core.title import TitleServlet
|
||||
from core.utils import Utils
|
||||
from core.mucha import MuchaServlet
|
||||
|
|
|
@ -1,29 +1,6 @@
|
|||
# ruff: noqa: F401
|
||||
from .base import (
|
||||
CMD_CODE_GOODBYE,
|
||||
HEADER_SIZE,
|
||||
ADBBaseRequest,
|
||||
ADBBaseResponse,
|
||||
ADBHeader,
|
||||
ADBHeaderException,
|
||||
ADBStatus,
|
||||
CompanyCodes,
|
||||
LogStatus,
|
||||
PortalRegStatus,
|
||||
ReaderFwVer,
|
||||
)
|
||||
from .campaign import (
|
||||
ADBCampaignClearRequest,
|
||||
ADBCampaignClearResponse,
|
||||
ADBCampaignResponse,
|
||||
ADBOldCampaignRequest,
|
||||
ADBOldCampaignResponse,
|
||||
)
|
||||
from .felica import (
|
||||
ADBFelicaLookup2Request,
|
||||
ADBFelicaLookup2Response,
|
||||
ADBFelicaLookupRequest,
|
||||
ADBFelicaLookupResponse,
|
||||
)
|
||||
from .log import ADBLogExRequest, ADBLogExResponse, ADBLogRequest, ADBStatusLogRequest
|
||||
from .lookup import ADBLookupExResponse, ADBLookupRequest, ADBLookupResponse
|
||||
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
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import re
|
||||
import struct
|
||||
from construct import Struct, Int16ul, Int32ul, PaddedString
|
||||
from enum import Enum
|
||||
from typing import Final, Union
|
||||
|
||||
from construct import Int16ul, Int32ul, PaddedString, Struct
|
||||
|
||||
import re
|
||||
from typing import Union, Final
|
||||
|
||||
class LogStatus(Enum):
|
||||
NONE = 0
|
||||
|
@ -13,13 +11,11 @@ class LogStatus(Enum):
|
|||
END = 3
|
||||
OTHER = 4
|
||||
|
||||
|
||||
class PortalRegStatus(Enum):
|
||||
NO_REG = 0
|
||||
PORTAL = 1
|
||||
SEGA_ID = 2
|
||||
|
||||
|
||||
class ADBStatus(Enum):
|
||||
UNKNOWN = 0
|
||||
GOOD = 1
|
||||
|
@ -33,7 +29,6 @@ class ADBStatus(Enum):
|
|||
LOCK_SYS = 9
|
||||
LOCK_USER = 10
|
||||
|
||||
|
||||
class CompanyCodes(Enum):
|
||||
NONE = 0
|
||||
SEGA = 1
|
||||
|
@ -41,13 +36,12 @@ class CompanyCodes(Enum):
|
|||
KONAMI = 3
|
||||
TAITO = 4
|
||||
|
||||
|
||||
class ReaderFwVer(Enum): # Newer readers use a singly byte value
|
||||
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"
|
||||
|
@ -59,11 +53,11 @@ class ReaderFwVer(Enum): # Newer readers use a singly byte value
|
|||
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")
|
||||
i = int.from_bytes(byte, 'little')
|
||||
try:
|
||||
return ReaderFwVer(i)
|
||||
except ValueError:
|
||||
|
@ -71,92 +65,64 @@ class ReaderFwVer(Enum): # Newer readers use a singly byte value
|
|||
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
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
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)}"
|
||||
)
|
||||
|
||||
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:
|
||||
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!"
|
||||
)
|
||||
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
|
||||
):
|
||||
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:
|
||||
|
@ -166,58 +132,33 @@ class ADBHeader:
|
|||
"response_code" / Int16ul,
|
||||
"length" / Int16ul,
|
||||
"status" / Int16ul,
|
||||
"game_id" / PaddedString(6, "utf_8"),
|
||||
"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,
|
||||
)
|
||||
"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
|
||||
)
|
||||
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 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"""
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
from construct import Bytes, Int32sl, Int32ul, Padding, Struct
|
||||
from construct import Struct, Int16ul, Padding, Bytes, Int32ul, Int32sl
|
||||
|
||||
from .base import *
|
||||
|
||||
|
||||
class Campaign:
|
||||
def __init__(self) -> None:
|
||||
self.id = 0
|
||||
|
@ -12,7 +11,7 @@ class Campaign:
|
|||
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(
|
||||
|
@ -24,53 +23,39 @@ class Campaign:
|
|||
"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,
|
||||
)
|
||||
)
|
||||
|
||||
).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,
|
||||
)
|
||||
)
|
||||
|
||||
).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:
|
||||
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)
|
||||
|
@ -79,42 +64,32 @@ class ADBCampaignResponse(ADBBaseResponse):
|
|||
|
||||
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:
|
||||
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,
|
||||
|
@ -122,46 +97,36 @@ class ADBOldCampaignResponse(ADBBaseResponse):
|
|||
"info2" / Int32sl,
|
||||
"info3" / Int32sl,
|
||||
).build(
|
||||
info0=self.info0,
|
||||
info1=self.info1,
|
||||
info2=self.info2,
|
||||
info3=self.info3,
|
||||
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:
|
||||
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
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
from construct import Struct, Int32sl, Padding, Int8ub, Int16sl
|
||||
from typing import Union
|
||||
|
||||
from construct import Int8ub, Int32sl, Padding, Struct
|
||||
|
||||
from .base import *
|
||||
|
||||
|
||||
class ADBFelicaLookupRequest(ADBBaseRequest):
|
||||
def __init__(self, data: bytes) -> None:
|
||||
super().__init__(data)
|
||||
|
@ -12,88 +9,58 @@ class ADBFelicaLookupRequest(ADBBaseRequest):
|
|||
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:
|
||||
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"
|
||||
)
|
||||
|
||||
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":
|
||||
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:
|
||||
|
||||
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)))
|
||||
"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.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.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:
|
||||
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.access_code = access_code if access_code is not None else "00000000000000000000"
|
||||
self.company = CompanyCodes.SEGA
|
||||
self.portal_status = PortalRegStatus.NO_REG
|
||||
self.auth_key = [0] * 256
|
||||
|
||||
@classmethod
|
||||
def from_req(
|
||||
cls,
|
||||
req: ADBHeader,
|
||||
user_id: Union[int, None] = None,
|
||||
access_code: Union[str, None] = None,
|
||||
) -> "ADBFelicaLookup2Response":
|
||||
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:
|
||||
def make(self) -> bytes:
|
||||
resp_struct = Struct(
|
||||
"user_id" / Int32sl,
|
||||
"relation1" / Int32sl,
|
||||
|
@ -103,17 +70,15 @@ class ADBFelicaLookup2Response(ADBBaseResponse):
|
|||
"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=self.auth_key,
|
||||
)
|
||||
)
|
||||
).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 = self.auth_key
|
||||
))
|
||||
|
||||
self.head.length = HEADER_SIZE + len(resp_struct)
|
||||
|
||||
|
|
|
@ -1,82 +1,56 @@
|
|||
from construct import Struct, Padding, Int8sl
|
||||
from typing import Final, List
|
||||
|
||||
from construct import Int8sl, Padding, Struct
|
||||
|
||||
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.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.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.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
|
||||
)
|
||||
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))
|
||||
resp_struct = Struct(
|
||||
"log_result" / Int8sl[NUM_LOGS],
|
||||
Padding(12)
|
||||
)
|
||||
|
||||
body = resp_struct.build(dict(log_result=[1] * NUM_LOGS))
|
||||
body = resp_struct.build(dict(
|
||||
log_result = [1] * NUM_LOGS
|
||||
))
|
||||
|
||||
self.head.length = HEADER_SIZE + len(body)
|
||||
return self.head.make() + body
|
||||
|
|
|
@ -1,41 +1,26 @@
|
|||
from construct import Struct, Int32sl, Padding, Int8sl
|
||||
from typing import Union
|
||||
|
||||
from construct import Int8sl, Int32sl, Padding, Struct
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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:
|
||||
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
|
||||
|
@ -47,36 +32,30 @@ class ADBLookupResponse(ADBBaseResponse):
|
|||
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)
|
||||
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:
|
||||
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
|
||||
self.auth_key = [0] * 256
|
||||
|
||||
@classmethod
|
||||
def from_req(
|
||||
cls, req: ADBHeader, user_id: Union[int, None]
|
||||
) -> "ADBLookupExResponse":
|
||||
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
|
||||
|
@ -91,15 +70,13 @@ class ADBLookupExResponse(ADBBaseResponse):
|
|||
"relation2" / Int32sl,
|
||||
)
|
||||
|
||||
body = resp_struct.build(
|
||||
dict(
|
||||
user_id=self.user_id,
|
||||
portal_reg=self.portal_reg.value,
|
||||
auth_key=self.auth_key,
|
||||
relation1=-1,
|
||||
relation2=-1,
|
||||
)
|
||||
)
|
||||
body = resp_struct.build(dict(
|
||||
user_id = self.user_id,
|
||||
portal_reg = self.portal_reg.value,
|
||||
auth_key = self.auth_key,
|
||||
relation1 = -1,
|
||||
relation2 = -1
|
||||
))
|
||||
|
||||
self.head.length = HEADER_SIZE + len(body)
|
||||
return self.head.make() + body
|
||||
|
|
387
core/aimedb.py
387
core/aimedb.py
|
@ -1,210 +1,134 @@
|
|||
import asyncio
|
||||
import logging
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from typing import Callable, Dict, Optional, Tuple, Union
|
||||
|
||||
import coloredlogs
|
||||
from twisted.internet.protocol import Factory, Protocol
|
||||
import logging, coloredlogs
|
||||
from Crypto.Cipher import AES
|
||||
import struct
|
||||
from typing import Dict, Tuple, Callable, Union
|
||||
from typing_extensions import Final
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
from core.config import CoreConfig
|
||||
from core.data import Data
|
||||
from core.utils import create_sega_auth_key
|
||||
|
||||
from core.data import Data
|
||||
from .adb_handlers import *
|
||||
|
||||
|
||||
class AimedbServlette:
|
||||
request_list: Dict[
|
||||
int, Tuple[Callable[[bytes, int], Union[ADBBaseResponse, bytes]], int, str]
|
||||
] = {}
|
||||
class AimedbProtocol(Protocol):
|
||||
request_list: Dict[int, Tuple[Callable[[bytes, int], Union[ADBBaseResponse, bytes]], int, str]] = {}
|
||||
|
||||
def __init__(self, core_cfg: CoreConfig) -> None:
|
||||
self.logger = logging.getLogger("aimedb")
|
||||
self.config = core_cfg
|
||||
self.data = Data(core_cfg)
|
||||
|
||||
self.logger = logging.getLogger("aimedb")
|
||||
if not hasattr(self.logger, "initted"):
|
||||
log_fmt_str = "[%(asctime)s] Aimedb | %(levelname)s | %(message)s"
|
||||
log_fmt = logging.Formatter(log_fmt_str)
|
||||
|
||||
fileHandler = TimedRotatingFileHandler(
|
||||
"{0}/{1}.log".format(self.config.server.log_dir, "aimedb"),
|
||||
when="d",
|
||||
backupCount=10,
|
||||
)
|
||||
fileHandler.setFormatter(log_fmt)
|
||||
|
||||
consoleHandler = logging.StreamHandler()
|
||||
consoleHandler.setFormatter(log_fmt)
|
||||
|
||||
self.logger.addHandler(fileHandler)
|
||||
self.logger.addHandler(consoleHandler)
|
||||
|
||||
self.logger.setLevel(self.config.aimedb.loglevel)
|
||||
coloredlogs.install(
|
||||
level=core_cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||
)
|
||||
self.logger.initted = True
|
||||
|
||||
if not core_cfg.aimedb.key:
|
||||
if core_cfg.aimedb.key == "":
|
||||
self.logger.error("!!!KEY NOT SET!!!")
|
||||
exit(1)
|
||||
|
||||
self.register_handler(0x01, 0x03, self.handle_felica_lookup, "felica_lookup")
|
||||
self.register_handler(
|
||||
0x02, 0x03, self.handle_felica_register, "felica_register"
|
||||
)
|
||||
self.register_handler(0x01, 0x03, self.handle_felica_lookup, 'felica_lookup')
|
||||
self.register_handler(0x02, 0x03, self.handle_felica_register, 'felica_register')
|
||||
|
||||
self.register_handler(0x04, 0x06, self.handle_lookup, "lookup")
|
||||
self.register_handler(0x05, 0x06, self.handle_register, "register")
|
||||
self.register_handler(0x04, 0x06, self.handle_lookup, 'lookup')
|
||||
self.register_handler(0x05, 0x06, self.handle_register, 'register')
|
||||
|
||||
self.register_handler(0x07, 0x08, self.handle_status_log, "status_log")
|
||||
self.register_handler(0x09, 0x0A, self.handle_log, "aime_log")
|
||||
self.register_handler(0x07, 0x08, self.handle_status_log, 'status_log')
|
||||
self.register_handler(0x09, 0x0A, self.handle_log, 'aime_log')
|
||||
|
||||
self.register_handler(0x0B, 0x0C, self.handle_campaign, "campaign")
|
||||
self.register_handler(0x0D, 0x0E, self.handle_campaign_clear, "campaign_clear")
|
||||
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(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")
|
||||
|
||||
def register_handler(
|
||||
self,
|
||||
cmd: int,
|
||||
resp: int,
|
||||
handler: Callable[[bytes, int], Union[ADBBaseResponse, bytes]],
|
||||
name: str,
|
||||
) -> None:
|
||||
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 start(self) -> None:
|
||||
self.logger.info(f"Start on port {self.config.aimedb.port}")
|
||||
addr = (
|
||||
self.config.aimedb.listen_address
|
||||
if self.config.aimedb.listen_address
|
||||
else self.config.server.listen_address
|
||||
)
|
||||
asyncio.create_task(
|
||||
asyncio.start_server(self.dataReceived, addr, self.config.aimedb.port)
|
||||
)
|
||||
def append_padding(self, data: bytes):
|
||||
"""Appends 0s to the end of the data until it's at the correct size"""
|
||||
length = struct.unpack_from("<H", data, 6)
|
||||
padding_size = length[0] - len(data)
|
||||
data += bytes(padding_size)
|
||||
return data
|
||||
|
||||
async def dataReceived(
|
||||
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
||||
):
|
||||
def connectionMade(self) -> None:
|
||||
self.logger.debug(f"{self.transport.getPeer().host} Connected")
|
||||
|
||||
def connectionLost(self, reason) -> None:
|
||||
self.logger.debug(
|
||||
f"Connection made from {writer.get_extra_info('peername')[0]}"
|
||||
f"{self.transport.getPeer().host} Disconnected - {reason.value}"
|
||||
)
|
||||
while True:
|
||||
try:
|
||||
data: bytes = await reader.read(4096)
|
||||
if len(data) == 0:
|
||||
self.logger.debug("Connection closed")
|
||||
return
|
||||
await self.process_data(data, reader, writer)
|
||||
await writer.drain()
|
||||
except ConnectionResetError as e:
|
||||
self.logger.debug("Connection reset, disconnecting")
|
||||
return
|
||||
|
||||
async def process_data(
|
||||
self, data: bytes, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
||||
) -> Optional[bytes]:
|
||||
addr = writer.get_extra_info("peername")[0]
|
||||
def dataReceived(self, data: bytes) -> None:
|
||||
cipher = AES.new(self.config.aimedb.key.encode(), AES.MODE_ECB)
|
||||
|
||||
try:
|
||||
decrypted = cipher.decrypt(data)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to decrypt {data.hex()} because {e}")
|
||||
return
|
||||
return None
|
||||
|
||||
self.logger.debug(f"{addr} wrote {decrypted.hex()}")
|
||||
self.logger.debug(f"{self.transport.getPeer().host} wrote {decrypted.hex()}")
|
||||
|
||||
try:
|
||||
head = ADBHeader.from_data(decrypted)
|
||||
|
||||
|
||||
except ADBHeaderException as e:
|
||||
self.logger.error(f"Error parsing ADB header: {e}")
|
||||
try:
|
||||
try:
|
||||
encrypted = cipher.encrypt(ADBBaseResponse().make())
|
||||
writer.write(encrypted)
|
||||
await writer.drain()
|
||||
return
|
||||
self.transport.write(encrypted)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to encrypt default response because {e}")
|
||||
|
||||
|
||||
return
|
||||
|
||||
if head.keychip_id == "ABCD1234567" or head.store_id == 0xFFF0:
|
||||
if head.keychip_id == "ABCD1234567" or head.store_id == 0xfff0:
|
||||
self.logger.warning(f"Request from uninitialized AMLib: {vars(head)}")
|
||||
|
||||
if head.cmd == 0x66:
|
||||
self.logger.info("Goodbye")
|
||||
writer.close()
|
||||
return
|
||||
|
||||
handler, resp_code, name = self.request_list.get(
|
||||
head.cmd, (self.handle_default, None, "default")
|
||||
)
|
||||
handler, resp_code, name = self.request_list.get(head.cmd, (self.handle_default, None, 'default'))
|
||||
|
||||
if resp_code is None:
|
||||
self.logger.warning(f"No handler for cmd {hex(head.cmd)}")
|
||||
|
||||
|
||||
elif resp_code > 0:
|
||||
self.logger.info(f"{name} from {head.keychip_id} ({head.game_id}) @ {addr}")
|
||||
|
||||
resp = await handler(decrypted, resp_code)
|
||||
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
|
||||
self.logger.warn(f"None return by handler for {name}")
|
||||
|
||||
elif resp is None: # Nothing to send, probably a goodbye
|
||||
return
|
||||
|
||||
|
||||
else:
|
||||
self.logger.error(
|
||||
f"Unsupported type returned by ADB handler for {name}: {type(resp)}"
|
||||
)
|
||||
raise TypeError(
|
||||
f"Unsupported type returned by ADB handler for {name}: {type(resp)}"
|
||||
)
|
||||
raise TypeError(f"Unsupported type returned by ADB handler for {name}: {type(resp)}")
|
||||
|
||||
try:
|
||||
try:
|
||||
encrypted = cipher.encrypt(resp_bytes)
|
||||
self.logger.debug(f"Response {resp_bytes.hex()}")
|
||||
writer.write(encrypted)
|
||||
self.transport.write(encrypted)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to encrypt {resp_bytes.hex()} because {e}")
|
||||
|
||||
async def handle_default(
|
||||
self, data: bytes, resp_code: int, length: int = 0x20
|
||||
) -> ADBBaseResponse:
|
||||
|
||||
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,
|
||||
)
|
||||
return ADBBaseResponse(resp_code, length, 1, req.game_id, req.store_id, req.keychip_id, req.protocol_ver)
|
||||
|
||||
async def handle_hello(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
return await self.handle_default(data, resp_code)
|
||||
def handle_hello(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
return self.handle_default(data, resp_code)
|
||||
|
||||
async def handle_campaign(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
def handle_campaign(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
h = ADBHeader.from_data(data)
|
||||
if h.protocol_ver >= 0x3030:
|
||||
req = h
|
||||
|
@ -212,41 +136,38 @@ class AimedbServlette:
|
|||
|
||||
else:
|
||||
req = ADBOldCampaignRequest(data)
|
||||
|
||||
self.logger.info(
|
||||
f"Legacy campaign request for campaign {req.campaign_id} (protocol version {hex(h.protocol_ver)})"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
async def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
req = ADBLookupRequest(data)
|
||||
user_id = await self.data.card.get_user_id_from_card(req.access_code)
|
||||
is_banned = await self.data.card.get_card_banned(req.access_code)
|
||||
is_locked = await self.data.card.get_card_locked(req.access_code)
|
||||
user_id = self.data.card.get_user_id_from_card(req.access_code)
|
||||
is_banned = self.data.card.get_card_banned(req.access_code)
|
||||
is_locked = self.data.card.get_card_locked(req.access_code)
|
||||
|
||||
ret = ADBLookupResponse.from_req(req.head, user_id)
|
||||
if is_banned and is_locked:
|
||||
ret.head.status = ADBStatus.BAN_SYS_USER
|
||||
elif is_banned:
|
||||
ret.head.status = ADBStatus.BAN_SYS
|
||||
elif is_locked:
|
||||
ret.head.status = ADBStatus.LOCK_USER
|
||||
|
||||
self.logger.info(f"access_code {req.access_code} -> user_id {ret.user_id}")
|
||||
|
||||
if user_id and user_id > 0:
|
||||
await self.data.card.update_card_last_login(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 ret
|
||||
|
||||
async def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
req = ADBLookupRequest(data)
|
||||
user_id = await self.data.card.get_user_id_from_card(req.access_code)
|
||||
user_id = self.data.card.get_user_id_from_card(req.access_code)
|
||||
|
||||
is_banned = await self.data.card.get_card_banned(req.access_code)
|
||||
is_locked = await self.data.card.get_card_locked(req.access_code)
|
||||
is_banned = self.data.card.get_card_banned(req.access_code)
|
||||
is_locked = self.data.card.get_card_locked(req.access_code)
|
||||
|
||||
ret = ADBLookupExResponse.from_req(req.head, user_id)
|
||||
if is_banned and is_locked:
|
||||
|
@ -256,30 +177,23 @@ class AimedbServlette:
|
|||
elif is_locked:
|
||||
ret.head.status = ADBStatus.LOCK_USER
|
||||
|
||||
self.logger.info(f"access_code {req.access_code} -> user_id {ret.user_id}")
|
||||
self.logger.info(
|
||||
f"access_code {req.access_code} -> user_id {ret.user_id}"
|
||||
)
|
||||
|
||||
if user_id and user_id > 0 and self.config.aimedb.id_secret:
|
||||
auth_key = create_sega_auth_key(
|
||||
user_id,
|
||||
req.head.game_id,
|
||||
req.head.store_id,
|
||||
req.head.keychip_id,
|
||||
self.config.aimedb.id_secret,
|
||||
self.config.aimedb.id_lifetime_seconds,
|
||||
)
|
||||
auth_key = create_sega_auth_key(user_id, req.head.game_id, req.head.store_id, req.head.keychip_id, self.config.aimedb.id_secret, self.config.aimedb.id_lifetime_seconds)
|
||||
if auth_key is not None:
|
||||
auth_key_extra_len = 256 - len(auth_key)
|
||||
auth_key_full = auth_key.encode() + (b"\0" * auth_key_extra_len)
|
||||
self.logger.debug(f"Generated auth token {auth_key}")
|
||||
ret.auth_key = auth_key_full
|
||||
|
||||
if user_id and user_id > 0:
|
||||
await self.data.card.update_card_last_login(req.access_code)
|
||||
return ret
|
||||
|
||||
async def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes:
|
||||
def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes:
|
||||
"""
|
||||
On official, I think a card has to be registered for this to actually work, but
|
||||
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
|
||||
|
@ -288,25 +202,27 @@ class AimedbServlette:
|
|||
"""
|
||||
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}")
|
||||
self.logger.info(
|
||||
f"idm {req.idm} ipm {req.pmm} -> access_code {ac}"
|
||||
)
|
||||
return ADBFelicaLookupResponse.from_req(req.head, ac)
|
||||
|
||||
async def handle_felica_register(self, data: bytes, resp_code: int) -> bytes:
|
||||
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 = await self.data.user.create_user()
|
||||
user_id = self.data.user.create_user()
|
||||
|
||||
if user_id is None:
|
||||
self.logger.error("Failed to register user!")
|
||||
user_id = -1
|
||||
|
||||
else:
|
||||
card_id = await self.data.card.create_card(user_id, ac)
|
||||
card_id = self.data.card.create_card(user_id, ac)
|
||||
|
||||
if card_id is None:
|
||||
self.logger.error("Failed to register card!")
|
||||
|
@ -321,14 +237,12 @@ class AimedbServlette:
|
|||
f"Registration blocked!: access code {ac} (IDm: {req.idm} PMm: {req.pmm})"
|
||||
)
|
||||
|
||||
if user_id > 0:
|
||||
await self.data.card.update_card_last_login(ac)
|
||||
return ADBFelicaLookupResponse.from_req(req.head, ac)
|
||||
|
||||
async def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes:
|
||||
def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes:
|
||||
req = ADBFelicaLookup2Request(data)
|
||||
access_code = self.data.card.to_access_code(req.idm)
|
||||
user_id = await self.data.card.get_user_id_from_card(access_code=access_code)
|
||||
user_id = self.data.card.get_user_id_from_card(access_code=access_code)
|
||||
|
||||
if user_id is None:
|
||||
user_id = -1
|
||||
|
@ -340,27 +254,16 @@ class AimedbServlette:
|
|||
resp = ADBFelicaLookup2Response.from_req(req.head, user_id, access_code)
|
||||
|
||||
if user_id and user_id > 0 and self.config.aimedb.id_secret:
|
||||
auth_key = create_sega_auth_key(
|
||||
user_id,
|
||||
req.head.game_id,
|
||||
req.head.store_id,
|
||||
req.head.keychip_id,
|
||||
self.config.aimedb.id_secret,
|
||||
self.config.aimedb.id_lifetime_seconds,
|
||||
)
|
||||
auth_key = create_sega_auth_key(user_id, req.head.game_id, req.head.store_id, req.head.keychip_id, self.config.aimedb.id_secret, self.config.aimedb.id_lifetime_seconds)
|
||||
if auth_key is not None:
|
||||
auth_key_extra_len = 256 - len(auth_key)
|
||||
auth_key_full = auth_key.encode() + (b"\0" * auth_key_extra_len)
|
||||
self.logger.debug(f"Generated auth token {auth_key}")
|
||||
resp.auth_key = auth_key_full
|
||||
|
||||
if user_id and user_id > 0:
|
||||
await self.data.card.update_card_last_login(access_code)
|
||||
return resp
|
||||
|
||||
async def handle_campaign_clear(
|
||||
self, data: bytes, resp_code: int
|
||||
) -> ADBBaseResponse:
|
||||
def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
req = ADBCampaignClearRequest(data)
|
||||
|
||||
resp = ADBCampaignClearResponse.from_req(req.head)
|
||||
|
@ -368,19 +271,19 @@ class AimedbServlette:
|
|||
# We don't support campaign stuff
|
||||
return resp
|
||||
|
||||
async def handle_register(self, data: bytes, resp_code: int) -> bytes:
|
||||
def handle_register(self, data: bytes, resp_code: int) -> bytes:
|
||||
req = ADBLookupRequest(data)
|
||||
user_id = -1
|
||||
|
||||
if self.config.server.allow_user_registration:
|
||||
user_id = await self.data.user.create_user()
|
||||
user_id = self.data.user.create_user()
|
||||
|
||||
if user_id is None:
|
||||
self.logger.error("Failed to register user!")
|
||||
user_id = -1
|
||||
|
||||
else:
|
||||
card_id = await self.data.card.create_card(user_id, req.access_code)
|
||||
card_id = self.data.card.create_card(user_id, req.access_code)
|
||||
|
||||
if card_id is None:
|
||||
self.logger.error("Failed to register card!")
|
||||
|
@ -391,55 +294,73 @@ class AimedbServlette:
|
|||
)
|
||||
|
||||
else:
|
||||
self.logger.info(f"Registration blocked!: access code {req.access_code}")
|
||||
self.logger.info(
|
||||
f"Registration blocked!: access code {req.access_code}"
|
||||
)
|
||||
|
||||
resp = ADBLookupResponse.from_req(req.head, user_id)
|
||||
if resp.user_id <= 0:
|
||||
resp.head.status = (
|
||||
ADBStatus.BAN_SYS
|
||||
) # Closest we can get to a "You cannot register"
|
||||
|
||||
else:
|
||||
await self.data.card.update_card_last_login(req.access_code)
|
||||
resp.head.status = ADBStatus.BAN_SYS # Closest we can get to a "You cannot register"
|
||||
|
||||
return resp
|
||||
|
||||
# TODO: Save these in some capacity, as deemed relevant
|
||||
async def handle_status_log(self, data: bytes, resp_code: int) -> bytes:
|
||||
def handle_status_log(self, data: bytes, resp_code: int) -> bytes:
|
||||
req = ADBStatusLogRequest(data)
|
||||
self.logger.info(f"User {req.aime_id} logged {req.status.name} event")
|
||||
return ADBBaseResponse(
|
||||
resp_code,
|
||||
0x20,
|
||||
1,
|
||||
req.head.game_id,
|
||||
req.head.store_id,
|
||||
req.head.keychip_id,
|
||||
req.head.protocol_ver,
|
||||
)
|
||||
return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver)
|
||||
|
||||
async def handle_log(self, data: bytes, resp_code: int) -> bytes:
|
||||
def handle_log(self, data: bytes, resp_code: int) -> bytes:
|
||||
req = ADBLogRequest(data)
|
||||
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}"
|
||||
)
|
||||
return ADBBaseResponse(
|
||||
resp_code,
|
||||
0x20,
|
||||
1,
|
||||
req.head.game_id,
|
||||
req.head.store_id,
|
||||
req.head.keychip_id,
|
||||
req.head.protocol_ver,
|
||||
)
|
||||
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}")
|
||||
return ADBBaseResponse(resp_code, 0x20, 1, req.head.game_id, req.head.store_id, req.head.keychip_id, req.head.protocol_ver)
|
||||
|
||||
async def handle_log_ex(self, data: bytes, resp_code: int) -> bytes:
|
||||
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}"
|
||||
)
|
||||
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):
|
||||
protocol = AimedbProtocol
|
||||
|
||||
def __init__(self, cfg: CoreConfig) -> None:
|
||||
self.config = cfg
|
||||
log_fmt_str = "[%(asctime)s] Aimedb | %(levelname)s | %(message)s"
|
||||
log_fmt = logging.Formatter(log_fmt_str)
|
||||
self.logger = logging.getLogger("aimedb")
|
||||
|
||||
fileHandler = TimedRotatingFileHandler(
|
||||
"{0}/{1}.log".format(self.config.server.log_dir, "aimedb"),
|
||||
when="d",
|
||||
backupCount=10,
|
||||
)
|
||||
fileHandler.setFormatter(log_fmt)
|
||||
|
||||
consoleHandler = logging.StreamHandler()
|
||||
consoleHandler.setFormatter(log_fmt)
|
||||
|
||||
self.logger.addHandler(fileHandler)
|
||||
self.logger.addHandler(consoleHandler)
|
||||
|
||||
self.logger.setLevel(self.config.aimedb.loglevel)
|
||||
coloredlogs.install(
|
||||
level=cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||
)
|
||||
|
||||
if self.config.aimedb.key == "":
|
||||
self.logger.error("Please set 'key' field in your config file.")
|
||||
exit(1)
|
||||
|
||||
self.logger.info(f"Ready on port {self.config.aimedb.port}")
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
return AimedbProtocol(self.config)
|
||||
|
|
667
core/allnet.py
667
core/allnet.py
File diff suppressed because it is too large
Load Diff
107
core/app.py
107
core/app.py
|
@ -1,107 +0,0 @@
|
|||
import logging
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from os import W_OK, access, environ, mkdir, path
|
||||
from typing import List
|
||||
|
||||
import coloredlogs
|
||||
import yaml
|
||||
from starlette.applications import Starlette
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import PlainTextResponse
|
||||
from starlette.routing import Route
|
||||
|
||||
from core import (
|
||||
AllnetServlet,
|
||||
BillingServlet,
|
||||
CoreConfig,
|
||||
MuchaServlet,
|
||||
TitleServlet,
|
||||
)
|
||||
from core.frontend import FrontendServlet
|
||||
|
||||
|
||||
async def dummy_rt(request: Request):
|
||||
return PlainTextResponse("Service OK")
|
||||
|
||||
|
||||
cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config")
|
||||
cfg: CoreConfig = CoreConfig()
|
||||
if path.exists(f"{cfg_dir}/core.yaml"):
|
||||
cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml")))
|
||||
|
||||
if not path.exists(cfg.server.log_dir):
|
||||
mkdir(cfg.server.log_dir)
|
||||
|
||||
if not access(cfg.server.log_dir, W_OK):
|
||||
print(f"Log directory {cfg.server.log_dir} NOT writable, please check permissions")
|
||||
exit(1)
|
||||
|
||||
logger = logging.getLogger("core")
|
||||
log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s"
|
||||
log_fmt = logging.Formatter(log_fmt_str)
|
||||
|
||||
fileHandler = TimedRotatingFileHandler(
|
||||
"{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10
|
||||
)
|
||||
fileHandler.setFormatter(log_fmt)
|
||||
|
||||
consoleHandler = logging.StreamHandler()
|
||||
consoleHandler.setFormatter(log_fmt)
|
||||
|
||||
logger.addHandler(fileHandler)
|
||||
logger.addHandler(consoleHandler)
|
||||
|
||||
log_lv = logging.DEBUG if cfg.server.is_develop else logging.INFO
|
||||
logger.setLevel(log_lv)
|
||||
coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str)
|
||||
|
||||
logger.info(
|
||||
f"Artemis starting in {'develop' if cfg.server.is_develop else 'production'} mode"
|
||||
)
|
||||
|
||||
title = TitleServlet(cfg, cfg_dir) # This has to be loaded first to load plugins
|
||||
mucha = MuchaServlet(cfg, cfg_dir)
|
||||
|
||||
route_lst: List[Route] = [
|
||||
# Mucha
|
||||
Route("/mucha_front/boardauth.do", mucha.handle_boardauth, methods=["POST"]),
|
||||
Route("/mucha_front/updatacheck.do", mucha.handle_updatecheck, methods=["POST"]),
|
||||
Route("/mucha_front/downloadstate.do", mucha.handle_dlstate, methods=["POST"]),
|
||||
# General
|
||||
Route("/", dummy_rt),
|
||||
Route("/robots.txt", FrontendServlet.robots),
|
||||
]
|
||||
|
||||
if not cfg.billing.standalone:
|
||||
billing = BillingServlet(cfg, cfg_dir)
|
||||
route_lst += [
|
||||
Route("/request", billing.handle_billing_request, methods=["POST"]),
|
||||
Route("/request/", billing.handle_billing_request, methods=["POST"]),
|
||||
]
|
||||
|
||||
if not cfg.allnet.standalone:
|
||||
allnet = AllnetServlet(cfg, cfg_dir)
|
||||
route_lst += [
|
||||
Route("/sys/servlet/PowerOn", allnet.handle_poweron, methods=["GET", "POST"]),
|
||||
Route(
|
||||
"/sys/servlet/DownloadOrder", allnet.handle_dlorder, methods=["GET", "POST"]
|
||||
),
|
||||
Route(
|
||||
"/sys/servlet/LoaderStateRecorder",
|
||||
allnet.handle_loaderstaterecorder,
|
||||
methods=["GET", "POST"],
|
||||
),
|
||||
Route("/sys/servlet/Alive", allnet.handle_alive, methods=["GET", "POST"]),
|
||||
Route("/naomitest.html", allnet.handle_naomitest),
|
||||
]
|
||||
|
||||
if cfg.allnet.allow_online_updates:
|
||||
route_lst += [
|
||||
Route("/report-api/Report", allnet.handle_dlorder_report, methods=["POST"]),
|
||||
Route("/dl/ini/{file:str}", allnet.handle_dlorder_ini),
|
||||
]
|
||||
|
||||
for code, game in title.title_registry.items():
|
||||
route_lst += game.get_routes()
|
||||
|
||||
app = Starlette(cfg.server.is_develop, route_lst)
|
173
core/config.py
173
core/config.py
|
@ -1,5 +1,4 @@
|
|||
import logging
|
||||
import os
|
||||
import logging, os
|
||||
from typing import Any
|
||||
|
||||
|
||||
|
@ -9,43 +8,10 @@ class ServerConfig:
|
|||
|
||||
@property
|
||||
def listen_address(self) -> str:
|
||||
"""
|
||||
Address Artemis will bind to and listen on
|
||||
"""
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "server", "listen_address", default="127.0.0.1"
|
||||
)
|
||||
|
||||
@property
|
||||
def hostname(self) -> str:
|
||||
"""
|
||||
Hostname sent to games
|
||||
"""
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "server", "hostname", default="localhost"
|
||||
)
|
||||
|
||||
@property
|
||||
def port(self) -> int:
|
||||
"""
|
||||
Port the game will listen on
|
||||
"""
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "server", "port", default=80
|
||||
)
|
||||
|
||||
@property
|
||||
def ssl_key(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "server", "ssl_key", default="cert/title.key"
|
||||
)
|
||||
|
||||
@property
|
||||
def ssl_cert(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "title", "ssl_cert", default="cert/title.pem"
|
||||
)
|
||||
|
||||
@property
|
||||
def allow_user_registration(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
|
@ -77,23 +43,9 @@ class ServerConfig:
|
|||
)
|
||||
|
||||
@property
|
||||
def proxy_port(self) -> int:
|
||||
"""
|
||||
What port the proxy is listening on. This will be sent instead of 'port' if
|
||||
is_using_proxy is True and this value is non-zero
|
||||
"""
|
||||
def threading(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "server", "proxy_port", default=0
|
||||
)
|
||||
|
||||
@property
|
||||
def proxy_port_ssl(self) -> int:
|
||||
"""
|
||||
What port the proxy is listening for secure connections on. This will be sent
|
||||
instead of 'port' if is_using_proxy is True and this value is non-zero
|
||||
"""
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "server", "proxy_port_ssl", default=0
|
||||
self.__config, "core", "server", "threading", default=False
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -127,6 +79,36 @@ class TitleConfig:
|
|||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def hostname(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "title", "hostname", default="localhost"
|
||||
)
|
||||
|
||||
@property
|
||||
def port(self) -> int:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "title", "port", default=8080
|
||||
)
|
||||
|
||||
@property
|
||||
def port_ssl(self) -> int:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "title", "port_ssl", default=0
|
||||
)
|
||||
|
||||
@property
|
||||
def ssl_key(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "title", "ssl_key", default="cert/title.key"
|
||||
)
|
||||
|
||||
@property
|
||||
def ssl_cert(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "title", "ssl_cert", default="cert/title.pem"
|
||||
)
|
||||
|
||||
@property
|
||||
def reboot_start_time(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
|
@ -177,7 +159,7 @@ class DatabaseConfig:
|
|||
@property
|
||||
def protocol(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "database", "protocol", default="mysql"
|
||||
self.__config, "core", "database", "type", default="mysql"
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -194,6 +176,16 @@ class DatabaseConfig:
|
|||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def user_table_autoincrement_start(self) -> int:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config,
|
||||
"core",
|
||||
"database",
|
||||
"user_table_autoincrement_start",
|
||||
default=10000,
|
||||
)
|
||||
|
||||
@property
|
||||
def enable_memcached(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
|
@ -212,7 +204,7 @@ class FrontendConfig:
|
|||
self.__config = parent_config
|
||||
|
||||
@property
|
||||
def enable(self) -> bool:
|
||||
def enable(self) -> int:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "frontend", "enable", default=False
|
||||
)
|
||||
|
@ -220,7 +212,7 @@ class FrontendConfig:
|
|||
@property
|
||||
def port(self) -> int:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "frontend", "port", default=8080
|
||||
self.__config, "core", "frontend", "port", default=8090
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -231,21 +223,17 @@ class FrontendConfig:
|
|||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def secret(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "frontend", "secret", default=""
|
||||
)
|
||||
|
||||
|
||||
class AllnetConfig:
|
||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||
self.__config = parent_config
|
||||
|
||||
@property
|
||||
def standalone(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "allnet", "standalone", default=False
|
||||
def loglevel(self) -> int:
|
||||
return CoreConfig.str_to_loglevel(
|
||||
CoreConfig.get_config_field(
|
||||
self.__config, "core", "allnet", "loglevel", default="info"
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -255,11 +243,9 @@ class AllnetConfig:
|
|||
)
|
||||
|
||||
@property
|
||||
def loglevel(self) -> int:
|
||||
return CoreConfig.str_to_loglevel(
|
||||
CoreConfig.get_config_field(
|
||||
self.__config, "core", "allnet", "loglevel", default="info"
|
||||
)
|
||||
def ip_check(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "allnet", "ip_check", default=False
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -279,20 +265,6 @@ class BillingConfig:
|
|||
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||
self.__config = parent_config
|
||||
|
||||
@property
|
||||
def standalone(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "billing", "standalone", default=True
|
||||
)
|
||||
|
||||
@property
|
||||
def loglevel(self) -> int:
|
||||
return CoreConfig.str_to_loglevel(
|
||||
CoreConfig.get_config_field(
|
||||
self.__config, "core", "billing", "loglevel", default="info"
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def port(self) -> int:
|
||||
return CoreConfig.get_config_field(
|
||||
|
@ -322,18 +294,6 @@ class AimedbConfig:
|
|||
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||
self.__config = parent_config
|
||||
|
||||
@property
|
||||
def enable(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "aimedb", "enable", default=True
|
||||
)
|
||||
|
||||
@property
|
||||
def listen_address(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "aimedb", "listen_address", default=""
|
||||
)
|
||||
|
||||
@property
|
||||
def loglevel(self) -> int:
|
||||
return CoreConfig.str_to_loglevel(
|
||||
|
@ -371,6 +331,12 @@ class MuchaConfig:
|
|||
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||
self.__config = parent_config
|
||||
|
||||
@property
|
||||
def enable(self) -> int:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "mucha", "enable", default=False
|
||||
)
|
||||
|
||||
@property
|
||||
def loglevel(self) -> int:
|
||||
return CoreConfig.str_to_loglevel(
|
||||
|
@ -379,6 +345,12 @@ class MuchaConfig:
|
|||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def hostname(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "mucha", "hostname", default="localhost"
|
||||
)
|
||||
|
||||
|
||||
class CoreConfig(dict):
|
||||
def __init__(self) -> None:
|
||||
|
@ -402,19 +374,6 @@ class CoreConfig(dict):
|
|||
else:
|
||||
return logging.INFO
|
||||
|
||||
@classmethod
|
||||
def loglevel_to_str(cls, level: int) -> str:
|
||||
if level == logging.ERROR:
|
||||
return "error"
|
||||
elif level == logging.WARN:
|
||||
return "warn"
|
||||
elif level == logging.INFO:
|
||||
return "info"
|
||||
elif level == logging.DEBUG:
|
||||
return "debug"
|
||||
else:
|
||||
return "notset"
|
||||
|
||||
@classmethod
|
||||
def get_config_field(
|
||||
cls, __config: dict, module, *path: str, default: Any = ""
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
# ruff: noqa: F401
|
||||
from .cache import cached
|
||||
from .database import Data
|
||||
from core.data.database import Data
|
||||
from core.data.cache import cached
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Generic single-database configuration.
|
|
@ -1,64 +0,0 @@
|
|||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
script_location=.
|
||||
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
#truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; this defaults
|
||||
# to migrations//versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path
|
||||
# version_locations = %(here)s/bar %(here)s/bat migrations//versions
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
|
@ -1,81 +0,0 @@
|
|||
from __future__ import with_statement
|
||||
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from core.data.schema.base import metadata
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
raise Exception("Not implemented or configured!")
|
||||
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
ini_section = config.get_section(config.config_ini_section)
|
||||
overrides = context.get_x_argument(as_dictionary=True)
|
||||
for override in overrides:
|
||||
ini_section[override] = overrides[override]
|
||||
|
||||
connectable = engine_from_config(
|
||||
ini_section, prefix="sqlalchemy.", poolclass=pool.NullPool
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
compare_type=True,
|
||||
compare_server_default=True,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
|
@ -1,24 +0,0 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
|
@ -1,56 +0,0 @@
|
|||
"""GekiChu rating tables
|
||||
|
||||
Revision ID: 6a7e8277763b
|
||||
Revises: d8950c7ce2fc
|
||||
Create Date: 2024-03-13 12:18:53.210018
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
from sqlalchemy import Column, Integer, String
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '6a7e8277763b'
|
||||
down_revision = 'd8950c7ce2fc'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
GEKICHU_RATING_TABLE_NAMES = [
|
||||
"chuni_profile_rating",
|
||||
"ongeki_profile_rating",
|
||||
]
|
||||
|
||||
def upgrade():
|
||||
for table_name in GEKICHU_RATING_TABLE_NAMES:
|
||||
op.create_table(
|
||||
table_name,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("user", Integer, nullable=False),
|
||||
Column("version", Integer, nullable=False),
|
||||
Column("type", String(255), nullable=False),
|
||||
Column("index", Integer, nullable=False),
|
||||
Column("musicId", Integer),
|
||||
Column("difficultId", Integer),
|
||||
Column("romVersionCode", Integer),
|
||||
Column("score", Integer),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
op.create_foreign_key(
|
||||
None,
|
||||
table_name,
|
||||
"aime_user",
|
||||
["user"],
|
||||
["id"],
|
||||
ondelete="cascade",
|
||||
onupdate="cascade",
|
||||
)
|
||||
op.create_unique_constraint(
|
||||
f"{table_name}_uk",
|
||||
table_name,
|
||||
["user", "version", "type", "index"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
for table_name in GEKICHU_RATING_TABLE_NAMES:
|
||||
op.drop_table(table_name)
|
|
@ -1,21 +0,0 @@
|
|||
"""Initial Migration
|
||||
|
||||
Revision ID: 835b862f9bf0
|
||||
Revises:
|
||||
Create Date: 2024-01-09 13:06:10.787432
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "835b862f9bf0"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
pass
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
|
@ -1,29 +0,0 @@
|
|||
"""Remove old db mgmt system
|
||||
|
||||
Revision ID: d8950c7ce2fc
|
||||
Revises: 835b862f9bf0
|
||||
Create Date: 2024-01-09 13:43:51.381175
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "d8950c7ce2fc"
|
||||
down_revision = "835b862f9bf0"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.drop_table("schema_versions")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.create_table(
|
||||
"schema_versions",
|
||||
sa.Column("game", sa.String(4), primary_key=True, nullable=False),
|
||||
sa.Column("version", sa.Integer, nullable=False, server_default="1"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
|
@ -1,88 +0,0 @@
|
|||
"""CHUNITHM Luminous
|
||||
|
||||
Revision ID: ead361541998
|
||||
Revises: d8950c7ce2fc
|
||||
Create Date: 2024-03-05 08:39:14.630558
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import Column, UniqueConstraint
|
||||
from sqlalchemy.types import Boolean, Integer
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "ead361541998"
|
||||
down_revision = "d8950c7ce2fc"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"chuni_profile_net_battle",
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("user", Integer, nullable=False),
|
||||
Column("isRankUpChallengeFailed", Boolean),
|
||||
Column("highestBattleRankId", Integer),
|
||||
Column("battleIconId", Integer),
|
||||
Column("battleIconNum", Integer),
|
||||
Column("avatarEffectPoint", Integer),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
op.create_foreign_key(
|
||||
None,
|
||||
"chuni_profile_net_battle",
|
||||
"aime_user",
|
||||
["user"],
|
||||
["id"],
|
||||
ondelete="cascade",
|
||||
onupdate="cascade",
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"chuni_item_cmission",
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("user", Integer, nullable=False),
|
||||
Column("missionId", Integer, nullable=False),
|
||||
Column("point", Integer),
|
||||
UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
op.create_foreign_key(
|
||||
None,
|
||||
"chuni_item_cmission",
|
||||
"aime_user",
|
||||
["user"],
|
||||
["id"],
|
||||
ondelete="cascade",
|
||||
onupdate="cascade",
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"chuni_item_cmission_progress",
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("user", Integer, nullable=False),
|
||||
Column("missionId", Integer, nullable=False),
|
||||
Column("order", Integer),
|
||||
Column("stage", Integer),
|
||||
Column("progress", Integer),
|
||||
UniqueConstraint(
|
||||
"user", "missionId", "order", name="chuni_item_cmission_progress_uk"
|
||||
),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
op.create_foreign_key(
|
||||
None,
|
||||
"chuni_item_cmission_progress",
|
||||
"aime_user",
|
||||
["user"],
|
||||
["id"],
|
||||
ondelete="cascade",
|
||||
onupdate="cascade",
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table("chuni_profile_net_battle")
|
||||
op.drop_table("chuni_item_cmission")
|
||||
op.drop_table("chuni_item_cmission_progress")
|
|
@ -1,9 +1,8 @@
|
|||
import hashlib
|
||||
import logging
|
||||
import pickle
|
||||
from functools import wraps
|
||||
from typing import Any, Callable
|
||||
|
||||
from functools import wraps
|
||||
import hashlib
|
||||
import pickle
|
||||
import logging
|
||||
from core.config import CoreConfig
|
||||
|
||||
cfg: CoreConfig = None # type: ignore
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import string
|
||||
from hashlib import sha256
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from typing import Optional
|
||||
|
||||
import alembic.config
|
||||
import bcrypt
|
||||
import coloredlogs
|
||||
from sqlalchemy import create_engine
|
||||
import logging, coloredlogs
|
||||
from typing import Optional, Dict, List
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy import create_engine
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
import importlib, os
|
||||
import secrets, string
|
||||
import bcrypt
|
||||
from hashlib import sha256
|
||||
|
||||
from core.config import CoreConfig
|
||||
from core.data.schema import *
|
||||
|
@ -18,13 +15,13 @@ from core.utils import Utils
|
|||
|
||||
|
||||
class Data:
|
||||
current_schema_version = 6
|
||||
engine = None
|
||||
session = None
|
||||
user = None
|
||||
arcade = None
|
||||
card = None
|
||||
base = None
|
||||
|
||||
def __init__(self, cfg: CoreConfig) -> None:
|
||||
self.config = cfg
|
||||
|
||||
|
@ -44,20 +41,20 @@ class Data:
|
|||
|
||||
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)
|
||||
|
||||
self.logger = logging.getLogger("database")
|
||||
|
||||
# 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(
|
||||
|
@ -80,207 +77,281 @@ class Data:
|
|||
)
|
||||
self.logger.handler_set = True # type: ignore
|
||||
|
||||
def __alembic_cmd(self, command: str, *args: str) -> None:
|
||||
old_dir = os.path.abspath(os.path.curdir)
|
||||
base_dir = os.path.join(
|
||||
os.path.abspath(os.path.curdir), "core", "data", "alembic"
|
||||
)
|
||||
alembicArgs = [
|
||||
"-c",
|
||||
os.path.join(base_dir, "alembic.ini"),
|
||||
"-x",
|
||||
f"script_location={base_dir}",
|
||||
"-x",
|
||||
f"sqlalchemy.url={self.__url}",
|
||||
command,
|
||||
]
|
||||
alembicArgs.extend(args)
|
||||
os.chdir(base_dir)
|
||||
alembic.config.main(argv=alembicArgs)
|
||||
os.chdir(old_dir)
|
||||
|
||||
def create_database(self):
|
||||
self.logger.info("Creating databases...")
|
||||
metadata.create_all(
|
||||
self.engine,
|
||||
checkfirst=True,
|
||||
)
|
||||
try:
|
||||
metadata.create_all(self.__engine.connect())
|
||||
except SQLAlchemyError as e:
|
||||
self.logger.error(f"Failed to create databases! {e}")
|
||||
return
|
||||
|
||||
for _, mod in Utils.get_all_titles().items():
|
||||
if hasattr(mod, "database"):
|
||||
mod.database(self.config)
|
||||
metadata.create_all(
|
||||
self.engine,
|
||||
checkfirst=True,
|
||||
games = Utils.get_all_titles()
|
||||
for game_dir, game_mod in games.items():
|
||||
try:
|
||||
if hasattr(game_mod, "database") and hasattr(
|
||||
game_mod, "current_schema_version"
|
||||
):
|
||||
game_mod.database(self.config)
|
||||
metadata.create_all(self.__engine.connect())
|
||||
|
||||
self.base.touch_schema_ver(
|
||||
game_mod.current_schema_version, game_mod.game_codes[0]
|
||||
)
|
||||
|
||||
# Stamp the end revision as if alembic had created it, so it can take off after this.
|
||||
self.__alembic_cmd(
|
||||
"stamp",
|
||||
"head",
|
||||
except Exception as e:
|
||||
self.logger.warning(
|
||||
f"Could not load database schema from {game_dir} - {e}"
|
||||
)
|
||||
|
||||
self.logger.info(f"Setting base_schema_ver to {self.current_schema_version}")
|
||||
self.base.set_schema_ver(self.current_schema_version)
|
||||
|
||||
self.logger.info(
|
||||
f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}"
|
||||
)
|
||||
self.user.reset_autoincrement(
|
||||
self.config.database.user_table_autoincrement_start
|
||||
)
|
||||
|
||||
def schema_upgrade(self, ver: str = None):
|
||||
self.__alembic_cmd(
|
||||
"upgrade",
|
||||
"head" if not ver else ver,
|
||||
)
|
||||
def recreate_database(self):
|
||||
self.logger.info("Dropping all databases...")
|
||||
self.base.execute("SET FOREIGN_KEY_CHECKS=0")
|
||||
try:
|
||||
metadata.drop_all(self.__engine.connect())
|
||||
except SQLAlchemyError as e:
|
||||
self.logger.error(f"Failed to drop databases! {e}")
|
||||
return
|
||||
|
||||
def schema_downgrade(self, ver: str):
|
||||
self.__alembic_cmd(
|
||||
"downgrade",
|
||||
ver,
|
||||
)
|
||||
for root, dirs, files in os.walk("./titles"):
|
||||
for dir in dirs:
|
||||
if not dir.startswith("__"):
|
||||
try:
|
||||
mod = importlib.import_module(f"titles.{dir}")
|
||||
|
||||
async def create_owner(
|
||||
self, email: Optional[str] = None, code: Optional[str] = "00000000000000000000"
|
||||
) -> None:
|
||||
try:
|
||||
if hasattr(mod, "database"):
|
||||
mod.database(self.config)
|
||||
metadata.drop_all(self.__engine.connect())
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(
|
||||
f"Could not load database schema from {dir} - {e}"
|
||||
)
|
||||
|
||||
except ImportError as e:
|
||||
self.logger.warning(
|
||||
f"Failed to load database schema dir {dir} - {e}"
|
||||
)
|
||||
break
|
||||
|
||||
self.base.execute("SET FOREIGN_KEY_CHECKS=1")
|
||||
|
||||
self.create_database()
|
||||
|
||||
def migrate_database(self, game: str, version: Optional[int], action: str) -> None:
|
||||
old_ver = self.base.get_schema_ver(game)
|
||||
sql = ""
|
||||
if version is None:
|
||||
if not game == "CORE":
|
||||
titles = Utils.get_all_titles()
|
||||
|
||||
for folder, mod in titles.items():
|
||||
if not mod.game_codes[0] == game:
|
||||
continue
|
||||
|
||||
if hasattr(mod, "current_schema_version"):
|
||||
version = mod.current_schema_version
|
||||
|
||||
else:
|
||||
self.logger.warning(
|
||||
f"current_schema_version not found for {folder}"
|
||||
)
|
||||
|
||||
else:
|
||||
version = self.current_schema_version
|
||||
|
||||
if version is None:
|
||||
self.logger.warning(
|
||||
f"Could not determine latest version for {game}, please specify --version"
|
||||
)
|
||||
|
||||
if old_ver is None:
|
||||
self.logger.error(
|
||||
f"Schema for game {game} does not exist, did you run the creation script?"
|
||||
)
|
||||
return
|
||||
|
||||
if old_ver == version:
|
||||
self.logger.info(
|
||||
f"Schema for game {game} is already version {old_ver}, nothing to do"
|
||||
)
|
||||
return
|
||||
|
||||
if action == "upgrade":
|
||||
for x in range(old_ver, version):
|
||||
if not os.path.exists(
|
||||
f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql"
|
||||
):
|
||||
self.logger.error(
|
||||
f"Could not find {action} script {game.upper()}_{x + 1}_{action}.sql in core/data/schema/versions folder"
|
||||
)
|
||||
return
|
||||
|
||||
with open(
|
||||
f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql",
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
sql = f.read()
|
||||
|
||||
result = self.base.execute(sql)
|
||||
if result is None:
|
||||
self.logger.error("Error execuing sql script!")
|
||||
return None
|
||||
|
||||
else:
|
||||
for x in range(old_ver, version, -1):
|
||||
if not os.path.exists(
|
||||
f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql"
|
||||
):
|
||||
self.logger.error(
|
||||
f"Could not find {action} script {game.upper()}_{x - 1}_{action}.sql in core/data/schema/versions folder"
|
||||
)
|
||||
return
|
||||
|
||||
with open(
|
||||
f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql",
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
sql = f.read()
|
||||
|
||||
result = self.base.execute(sql)
|
||||
if result is None:
|
||||
self.logger.error("Error execuing sql script!")
|
||||
return None
|
||||
|
||||
result = self.base.set_schema_ver(version, game)
|
||||
if result is None:
|
||||
self.logger.error("Error setting version in schema_version table!")
|
||||
return None
|
||||
|
||||
self.logger.info(f"Successfully migrated {game} to schema version {version}")
|
||||
|
||||
def create_owner(self, email: Optional[str] = None) -> None:
|
||||
pw = "".join(
|
||||
secrets.choice(string.ascii_letters + string.digits) for i in range(20)
|
||||
)
|
||||
hash = bcrypt.hashpw(pw.encode(), bcrypt.gensalt())
|
||||
|
||||
user_id = await self.user.create_user(
|
||||
username="sysowner", email=email, password=hash.decode(), permission=255
|
||||
)
|
||||
user_id = self.user.create_user(email=email, permission=255, password=hash)
|
||||
if user_id is None:
|
||||
self.logger.error(f"Failed to create owner with email {email}")
|
||||
return
|
||||
|
||||
card_id = await self.card.create_card(user_id, code)
|
||||
card_id = self.card.create_card(user_id, "00000000000000000000")
|
||||
if card_id is None:
|
||||
self.logger.error(f"Failed to create card for owner with id {user_id}")
|
||||
return
|
||||
|
||||
self.logger.warning(
|
||||
f"Successfully created owner with email {email}, access code {code}, 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!"
|
||||
)
|
||||
|
||||
async def migrate(self) -> None:
|
||||
exist = await self.base.execute("SELECT * FROM alembic_version")
|
||||
if exist is not None:
|
||||
self.logger.warn(
|
||||
"No need to migrate as you have already migrated to alembic. If you are trying to upgrade the schema, use `upgrade` instead!"
|
||||
def migrate_card(self, old_ac: str, new_ac: str, should_force: bool) -> None:
|
||||
if old_ac == new_ac:
|
||||
self.logger.error("Both access codes are the same!")
|
||||
return
|
||||
|
||||
new_card = self.card.get_card_by_access_code(new_ac)
|
||||
if new_card is None:
|
||||
self.card.update_access_code(old_ac, new_ac)
|
||||
return
|
||||
|
||||
if not should_force:
|
||||
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" All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}."
|
||||
)
|
||||
return
|
||||
|
||||
self.logger.info("Upgrading to latest with legacy system")
|
||||
if not await self.legacy_upgrade():
|
||||
self.logger.warn(
|
||||
"No need to migrate as you have already deleted the old schema_versions system. If you are trying to upgrade the schema, use `upgrade` instead!"
|
||||
)
|
||||
self.logger.info(
|
||||
f"All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}."
|
||||
)
|
||||
self.card.delete_card(new_card["id"])
|
||||
self.card.update_access_code(old_ac, new_ac)
|
||||
|
||||
hanging_user = self.user.get_user(new_card["user"])
|
||||
if hanging_user["password"] is None:
|
||||
self.logger.info(f"Delete hanging user {hanging_user['id']}")
|
||||
self.user.delete_user(hanging_user["id"])
|
||||
|
||||
def delete_hanging_users(self) -> None:
|
||||
"""
|
||||
Finds and deletes users that have not registered for the webui that have no cards assocated with them.
|
||||
"""
|
||||
unreg_users = self.user.get_unregistered_users()
|
||||
if unreg_users is None:
|
||||
self.logger.error("Error occoured finding unregistered users")
|
||||
|
||||
for user in unreg_users:
|
||||
cards = self.card.get_user_cards(user["id"])
|
||||
if cards is None:
|
||||
self.logger.error(f"Error getting cards for user {user['id']}")
|
||||
continue
|
||||
|
||||
if not cards:
|
||||
self.logger.info(f"Delete hanging user {user['id']}")
|
||||
self.user.delete_user(user["id"])
|
||||
|
||||
def autoupgrade(self) -> None:
|
||||
all_game_versions = self.base.get_all_schema_vers()
|
||||
if all_game_versions is None:
|
||||
self.logger.warning("Failed to get schema versions")
|
||||
return
|
||||
self.logger.info("Done")
|
||||
|
||||
self.logger.info("Stamp with initial revision")
|
||||
self.__alembic_cmd(
|
||||
"stamp",
|
||||
"835b862f9bf0",
|
||||
)
|
||||
all_games = Utils.get_all_titles()
|
||||
all_games_list: Dict[str, int] = {}
|
||||
for _, mod in all_games.items():
|
||||
if hasattr(mod, "current_schema_version"):
|
||||
all_games_list[mod.game_codes[0]] = mod.current_schema_version
|
||||
|
||||
self.logger.info("Upgrade")
|
||||
self.__alembic_cmd(
|
||||
"upgrade",
|
||||
"head",
|
||||
)
|
||||
for x in all_game_versions:
|
||||
failed = False
|
||||
game = x["game"].upper()
|
||||
update_ver = int(x["version"])
|
||||
latest_ver = all_games_list.get(game, 1)
|
||||
if game == "CORE":
|
||||
latest_ver = self.current_schema_version
|
||||
|
||||
async def legacy_upgrade(self) -> bool:
|
||||
vers = await self.base.execute("SELECT * FROM schema_versions")
|
||||
if vers is None:
|
||||
self.logger.warn(
|
||||
"Cannot legacy upgrade, schema_versions table unavailable!"
|
||||
)
|
||||
return False
|
||||
if update_ver == latest_ver:
|
||||
self.logger.info(f"{game} is already latest version")
|
||||
continue
|
||||
|
||||
db_vers = {}
|
||||
vers_list = vers.fetchall()
|
||||
for x in vers_list:
|
||||
db_vers[x["game"]] = x["version"]
|
||||
|
||||
core_now_ver = int(db_vers["CORE"]) + 1
|
||||
while os.path.exists(
|
||||
f"core/data/schema/versions/CORE_{core_now_ver}_upgrade.sql"
|
||||
):
|
||||
with open(
|
||||
f"core/data/schema/versions/CORE_{core_now_ver}_upgrade.sql", "r"
|
||||
) as f:
|
||||
result = await self.base.execute(f.read())
|
||||
|
||||
if result is None:
|
||||
self.logger.error(
|
||||
f"Invalid upgrade script CORE_{core_now_ver}_upgrade.sql"
|
||||
)
|
||||
break
|
||||
|
||||
result = await self.base.execute(
|
||||
f"UPDATE schema_versions SET version = {core_now_ver} WHERE game = 'CORE'"
|
||||
)
|
||||
if result is None:
|
||||
self.logger.error(
|
||||
f"Failed to update schema version for CORE to {core_now_ver}"
|
||||
)
|
||||
break
|
||||
|
||||
self.logger.info(f"Upgrade CORE to version {core_now_ver}")
|
||||
core_now_ver += 1
|
||||
|
||||
for _, mod in Utils.get_all_titles().items():
|
||||
game_codes = getattr(mod, "game_codes", [])
|
||||
for game in game_codes:
|
||||
if game not in db_vers:
|
||||
self.logger.warn(
|
||||
f"{game} does not have an antry in schema_versions, skipping"
|
||||
)
|
||||
continue
|
||||
|
||||
now_ver = int(db_vers[game]) + 1
|
||||
while os.path.exists(
|
||||
f"core/data/schema/versions/{game}_{now_ver}_upgrade.sql"
|
||||
):
|
||||
for y in range(update_ver + 1, latest_ver + 1):
|
||||
if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"):
|
||||
with open(
|
||||
f"core/data/schema/versions/{game}_{now_ver}_upgrade.sql", "r"
|
||||
f"core/data/schema/versions/{game}_{y}_upgrade.sql",
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as f:
|
||||
result = await self.base.execute(f.read())
|
||||
sql = f.read()
|
||||
|
||||
if result is None:
|
||||
self.logger.error(
|
||||
f"Invalid upgrade script {game}_{now_ver}_upgrade.sql"
|
||||
)
|
||||
break
|
||||
|
||||
result = await self.base.execute(
|
||||
f"UPDATE schema_versions SET version = {now_ver} WHERE game = '{game}'"
|
||||
result = self.base.execute(sql)
|
||||
if result is None:
|
||||
self.logger.error(
|
||||
f"Error execuing sql script for game {game} v{y}!"
|
||||
)
|
||||
if result is None:
|
||||
self.logger.error(
|
||||
f"Failed to update schema version for {game} to {now_ver}"
|
||||
)
|
||||
break
|
||||
failed = True
|
||||
break
|
||||
else:
|
||||
self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql")
|
||||
failed = True
|
||||
|
||||
self.logger.info(f"Upgrade {game} to version {now_ver}")
|
||||
now_ver += 1
|
||||
|
||||
return True
|
||||
|
||||
async def create_revision(self, message: str) -> None:
|
||||
if not message:
|
||||
self.logger.info("Message is required for create-revision")
|
||||
return
|
||||
|
||||
self.__alembic_cmd(
|
||||
"revision",
|
||||
"-m",
|
||||
message,
|
||||
)
|
||||
|
||||
async def create_revision_auto(self, message: str) -> None:
|
||||
if not message:
|
||||
self.logger.info("Message is required for create-revision")
|
||||
return
|
||||
|
||||
self.__alembic_cmd(
|
||||
"revision",
|
||||
"--autogenerate",
|
||||
"-m",
|
||||
message,
|
||||
)
|
||||
if not failed:
|
||||
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,6 +1,6 @@
|
|||
from core.data.schema.arcade import ArcadeData
|
||||
from core.data.schema.base import BaseData, metadata
|
||||
from core.data.schema.card import CardData
|
||||
from core.data.schema.user import UserData
|
||||
from core.data.schema.card import CardData
|
||||
from core.data.schema.base import BaseData, metadata
|
||||
from core.data.schema.arcade import ArcadeData
|
||||
|
||||
__all__ = ["UserData", "CardData", "BaseData", "metadata", "ArcadeData"]
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import re
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import Column, Table, and_, or_
|
||||
from typing import Optional, Dict, List
|
||||
from sqlalchemy import Table, Column, and_, or_
|
||||
from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint
|
||||
from sqlalchemy.types import Integer, String, Boolean, JSON
|
||||
from sqlalchemy.sql import func, select
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
from sqlalchemy.engine import Row
|
||||
from sqlalchemy.sql import select
|
||||
from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint
|
||||
from sqlalchemy.types import JSON, Boolean, Integer, String
|
||||
import re
|
||||
|
||||
from core.const import *
|
||||
from core.data.schema.base import BaseData, metadata
|
||||
from core.const import *
|
||||
|
||||
arcade = Table(
|
||||
"arcade",
|
||||
|
@ -70,7 +69,7 @@ arcade_owner = Table(
|
|||
|
||||
|
||||
class ArcadeData(BaseData):
|
||||
async def get_machine(self, serial: str = None, id: int = None) -> Optional[Row]:
|
||||
def get_machine(self, serial: str = None, id: int = None) -> Optional[Row]:
|
||||
if serial is not None:
|
||||
serial = serial.replace("-", "")
|
||||
if len(serial) == 11:
|
||||
|
@ -90,12 +89,12 @@ class ArcadeData(BaseData):
|
|||
self.logger.error(f"{__name__ }: Need either serial or ID to look up!")
|
||||
return None
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def put_machine(
|
||||
def put_machine(
|
||||
self,
|
||||
arcade_id: int,
|
||||
serial: str = "",
|
||||
|
@ -111,13 +110,13 @@ class ArcadeData(BaseData):
|
|||
arcade=arcade_id, keychip=serial, board=board, game=game, is_cab=is_cab
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def set_machine_serial(self, machine_id: int, serial: str) -> None:
|
||||
result = await self.execute(
|
||||
def set_machine_serial(self, machine_id: int, serial: str) -> None:
|
||||
result = self.execute(
|
||||
machine.update(machine.c.id == machine_id).values(keychip=serial)
|
||||
)
|
||||
if result is None:
|
||||
|
@ -126,8 +125,8 @@ class ArcadeData(BaseData):
|
|||
)
|
||||
return result.lastrowid
|
||||
|
||||
async def set_machine_boardid(self, machine_id: int, boardid: str) -> None:
|
||||
result = await self.execute(
|
||||
def set_machine_boardid(self, machine_id: int, boardid: str) -> None:
|
||||
result = self.execute(
|
||||
machine.update(machine.c.id == machine_id).values(board=boardid)
|
||||
)
|
||||
if result is None:
|
||||
|
@ -135,21 +134,21 @@ class ArcadeData(BaseData):
|
|||
f"Failed to update board id for machine {machine_id} -> {boardid}"
|
||||
)
|
||||
|
||||
async def get_arcade(self, id: int) -> Optional[Row]:
|
||||
def get_arcade(self, id: int) -> Optional[Row]:
|
||||
sql = arcade.select(arcade.c.id == id)
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def get_arcade_machines(self, id: int) -> Optional[List[Row]]:
|
||||
|
||||
def get_arcade_machines(self, id: int) -> Optional[List[Row]]:
|
||||
sql = machine.select(machine.c.arcade == id)
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_arcade(
|
||||
def put_arcade(
|
||||
self,
|
||||
name: str,
|
||||
nickname: str = None,
|
||||
|
@ -172,77 +171,62 @@ class ArcadeData(BaseData):
|
|||
regional_id=regional_id,
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async 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 = await self.execute(sql)
|
||||
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()
|
||||
|
||||
async 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 = await self.execute(sql)
|
||||
|
||||
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()
|
||||
|
||||
async def get_arcade_owners(self, arcade_id: int) -> Optional[Row]:
|
||||
def get_arcade_owners(self, arcade_id: int) -> Optional[Row]:
|
||||
sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def add_arcade_owner(self, arcade_id: int, user_id: int) -> None:
|
||||
def add_arcade_owner(self, arcade_id: int, user_id: int) -> None:
|
||||
sql = insert(arcade_owner).values(arcade=arcade_id, user=user_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def format_serial(
|
||||
def format_serial(
|
||||
self, platform_code: str, platform_rev: int, serial_num: int, append: int = 4152
|
||||
) -> str:
|
||||
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:
|
||||
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 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:
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
|
||||
async def get_arcade_by_name(self, name: str) -> Optional[List[Row]]:
|
||||
sql = arcade.select(
|
||||
or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%"))
|
||||
)
|
||||
result = await self.execute(sql)
|
||||
def get_arcade_by_name(self, name: str) -> Optional[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 None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_arcades_by_ip(self, ip: str) -> Optional[List[Row]]:
|
||||
def get_arcades_by_ip(self, ip: str) -> Optional[List[Row]]:
|
||||
sql = arcade.select().where(arcade.c.ip == ip)
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
|
|
@ -1,19 +1,28 @@
|
|||
import json
|
||||
import logging
|
||||
from random import randrange
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import Column, MetaData, Table
|
||||
from sqlalchemy.engine.base import Connection
|
||||
from typing import Any, Optional, Dict, List
|
||||
from sqlalchemy.engine import Row
|
||||
from sqlalchemy.engine.cursor import CursorResult
|
||||
from sqlalchemy.engine.base import Connection
|
||||
from sqlalchemy.sql import text, func, select
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.sql import func, text
|
||||
from sqlalchemy.types import JSON, TIMESTAMP, Integer, String
|
||||
from sqlalchemy import MetaData, Table, Column
|
||||
from sqlalchemy.types import Integer, String, TIMESTAMP, JSON
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
|
||||
from core.config import CoreConfig
|
||||
|
||||
metadata = MetaData()
|
||||
|
||||
schema_ver = Table(
|
||||
"schema_versions",
|
||||
metadata,
|
||||
Column("game", String(4), primary_key=True, nullable=False),
|
||||
Column("version", Integer, nullable=False, server_default="1"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
event_log = Table(
|
||||
"event_log",
|
||||
metadata,
|
||||
|
@ -34,13 +43,11 @@ class BaseData:
|
|||
self.conn = conn
|
||||
self.logger = logging.getLogger("database")
|
||||
|
||||
async def execute(
|
||||
self, sql: str, opts: Dict[str, Any] = {}
|
||||
) -> Optional[CursorResult]:
|
||||
def execute(self, sql: str, opts: Dict[str, Any] = {}) -> Optional[CursorResult]:
|
||||
res = None
|
||||
|
||||
try:
|
||||
self.logger.debug(f"SQL Execute: {''.join(str(sql).splitlines())}")
|
||||
self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())}")
|
||||
res = self.conn.execute(text(sql), opts)
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
|
@ -75,7 +82,52 @@ class BaseData:
|
|||
"""
|
||||
return randrange(10000, 9999999)
|
||||
|
||||
async def log_event(
|
||||
def get_all_schema_vers(self) -> Optional[List[Row]]:
|
||||
sql = select(schema_ver)
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
def get_schema_ver(self, game: str) -> Optional[int]:
|
||||
sql = select(schema_ver).where(schema_ver.c.game == game)
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
row = result.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
|
||||
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]:
|
||||
sql = insert(schema_ver).values(game=game, version=ver)
|
||||
conflict = sql.on_duplicate_key_update(version=ver)
|
||||
|
||||
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 log_event(
|
||||
self, system: str, type: str, severity: int, message: str, details: Dict = {}
|
||||
) -> Optional[int]:
|
||||
sql = event_log.insert().values(
|
||||
|
@ -85,7 +137,7 @@ class BaseData:
|
|||
message=message,
|
||||
details=json.dumps(details),
|
||||
)
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
|
||||
if result is None:
|
||||
self.logger.error(
|
||||
|
@ -95,9 +147,9 @@ class BaseData:
|
|||
|
||||
return result.lastrowid
|
||||
|
||||
async def get_event_log(self, entries: int = 100) -> Optional[List[Dict]]:
|
||||
def get_event_log(self, entries: int = 100) -> Optional[List[Dict]]:
|
||||
sql = event_log.select().limit(entries).all()
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import Column, Table, UniqueConstraint
|
||||
from sqlalchemy.engine import Row
|
||||
from sqlalchemy.sql import func
|
||||
from typing import Dict, List, Optional
|
||||
from sqlalchemy import Table, Column, UniqueConstraint
|
||||
from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP
|
||||
from sqlalchemy.sql.schema import ForeignKey
|
||||
from sqlalchemy.types import TIMESTAMP, Boolean, Integer, String
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.engine import Row
|
||||
|
||||
from core.data.schema.base import BaseData, metadata
|
||||
|
||||
|
@ -28,101 +27,91 @@ aime_card = Table(
|
|||
|
||||
|
||||
class CardData(BaseData):
|
||||
async def get_card_by_access_code(self, access_code: str) -> Optional[Row]:
|
||||
def get_card_by_access_code(self, access_code: str) -> Optional[Row]:
|
||||
sql = aime_card.select(aime_card.c.access_code == access_code)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def get_card_by_id(self, card_id: int) -> Optional[Row]:
|
||||
def get_card_by_id(self, card_id: int) -> Optional[Row]:
|
||||
sql = aime_card.select(aime_card.c.id == card_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def update_access_code(self, old_ac: str, new_ac: str) -> None:
|
||||
def update_access_code(self, old_ac: str, new_ac: str) -> None:
|
||||
sql = aime_card.update(aime_card.c.access_code == old_ac).values(
|
||||
access_code=new_ac
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
self.logger.error(
|
||||
f"Failed to change card access code from {old_ac} to {new_ac}"
|
||||
)
|
||||
|
||||
async def get_user_id_from_card(self, access_code: str) -> Optional[int]:
|
||||
def get_user_id_from_card(self, access_code: str) -> Optional[int]:
|
||||
"""
|
||||
Given a 20 digit access code as a string, get the user id associated with that card
|
||||
"""
|
||||
card = await self.get_card_by_access_code(access_code)
|
||||
card = self.get_card_by_access_code(access_code)
|
||||
if card is None:
|
||||
return None
|
||||
|
||||
return int(card["user"])
|
||||
|
||||
async def get_card_banned(self, access_code: str) -> Optional[bool]:
|
||||
def get_card_banned(self, access_code: str) -> Optional[bool]:
|
||||
"""
|
||||
Given a 20 digit access code as a string, check if the card is banned
|
||||
"""
|
||||
card = await self.get_card_by_access_code(access_code)
|
||||
card = self.get_card_by_access_code(access_code)
|
||||
if card is None:
|
||||
return None
|
||||
if card["is_banned"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def get_card_locked(self, access_code: str) -> Optional[bool]:
|
||||
def get_card_locked(self, access_code: str) -> Optional[bool]:
|
||||
"""
|
||||
Given a 20 digit access code as a string, check if the card is locked
|
||||
"""
|
||||
card = await self.get_card_by_access_code(access_code)
|
||||
card = self.get_card_by_access_code(access_code)
|
||||
if card is None:
|
||||
return None
|
||||
if card["is_locked"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def delete_card(self, card_id: int) -> None:
|
||||
def delete_card(self, card_id: int) -> None:
|
||||
sql = aime_card.delete(aime_card.c.id == card_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
self.logger.error(f"Failed to delete card with id {card_id}")
|
||||
|
||||
async def get_user_cards(self, aime_id: int) -> Optional[List[Row]]:
|
||||
def get_user_cards(self, aime_id: int) -> Optional[List[Row]]:
|
||||
"""
|
||||
Returns all cards owned by a user
|
||||
"""
|
||||
sql = aime_card.select(aime_card.c.user == aime_id)
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def create_card(self, user_id: int, access_code: str) -> Optional[int]:
|
||||
def create_card(self, user_id: int, access_code: str) -> Optional[int]:
|
||||
"""
|
||||
Given a aime_user id and a 20 digit access code as a string, create a card and return the ID if successful
|
||||
"""
|
||||
sql = aime_card.insert().values(user=user_id, access_code=access_code)
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def update_card_last_login(self, access_code: str) -> None:
|
||||
sql = aime_card.update(aime_card.c.access_code == access_code).values(
|
||||
last_login_date=func.now()
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
self.logger.warn(f"Failed to update last login time for {access_code}")
|
||||
|
||||
def to_access_code(self, luid: str) -> str:
|
||||
"""
|
||||
Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from typing import List, Optional
|
||||
|
||||
import bcrypt
|
||||
from sqlalchemy import Column, Table
|
||||
from enum import Enum
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import Table, Column
|
||||
from sqlalchemy.types import Integer, String, TIMESTAMP
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
from sqlalchemy.engine import Row
|
||||
from sqlalchemy.sql import func, select
|
||||
from sqlalchemy.types import TIMESTAMP, Integer, String
|
||||
from sqlalchemy.engine import Row
|
||||
import bcrypt
|
||||
|
||||
from core.data.schema.base import BaseData, metadata
|
||||
|
||||
|
@ -24,8 +25,14 @@ aime_user = Table(
|
|||
)
|
||||
|
||||
|
||||
class PermissionBits(Enum):
|
||||
PermUser = 1
|
||||
PermMod = 2
|
||||
PermSysAdmin = 4
|
||||
|
||||
|
||||
class UserData(BaseData):
|
||||
async def create_user(
|
||||
def create_user(
|
||||
self,
|
||||
id: int = None,
|
||||
username: str = None,
|
||||
|
@ -53,71 +60,64 @@ class UserData(BaseData):
|
|||
username=username, email=email, password=password, permissions=permission
|
||||
)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_user(self, user_id: int) -> Optional[Row]:
|
||||
def get_user(self, user_id: int) -> Optional[Row]:
|
||||
sql = select(aime_user).where(aime_user.c.id == user_id)
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return False
|
||||
return result.fetchone()
|
||||
|
||||
async def check_password(self, user_id: int, passwd: bytes = None) -> bool:
|
||||
usr = await self.get_user(user_id)
|
||||
def check_password(self, user_id: int, passwd: bytes = None) -> bool:
|
||||
usr = self.get_user(user_id)
|
||||
if usr is None:
|
||||
return False
|
||||
|
||||
if usr["password"] is None:
|
||||
return False
|
||||
|
||||
|
||||
if passwd is None or not passwd:
|
||||
return False
|
||||
|
||||
return bcrypt.checkpw(passwd, usr["password"].encode())
|
||||
|
||||
async def delete_user(self, user_id: int) -> None:
|
||||
def reset_autoincrement(self, ai_value: int) -> None:
|
||||
# ALTER TABLE isn't in sqlalchemy so we do this the ugly way
|
||||
sql = f"ALTER TABLE aime_user AUTO_INCREMENT={ai_value}"
|
||||
self.execute(sql)
|
||||
|
||||
def delete_user(self, user_id: int) -> None:
|
||||
sql = aime_user.delete(aime_user.c.id == user_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
self.logger.error(f"Failed to delete user with id {user_id}")
|
||||
|
||||
async def get_unregistered_users(self) -> List[Row]:
|
||||
def get_unregistered_users(self) -> List[Row]:
|
||||
"""
|
||||
Returns a list of users who have not registered with the webui. They may or may not have cards.
|
||||
"""
|
||||
sql = select(aime_user).where(aime_user.c.password == None)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def find_user_by_email(self, email: str) -> Row:
|
||||
def find_user_by_email(self, email: str) -> Row:
|
||||
sql = select(aime_user).where(aime_user.c.email == email)
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return False
|
||||
return result.fetchone()
|
||||
|
||||
async def find_user_by_username(self, username: str) -> List[Row]:
|
||||
def find_user_by_username(self, username: str) -> List[Row]:
|
||||
sql = aime_user.select(aime_user.c.username.like(f"%{username}%"))
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return False
|
||||
return result.fetchall()
|
||||
|
||||
async def change_password(self, user_id: int, new_passwd: str) -> bool:
|
||||
sql = aime_user.update(aime_user.c.id == user_id).values(password=new_passwd)
|
||||
|
||||
result = await self.execute(sql)
|
||||
return result is not None
|
||||
|
||||
async def change_username(self, user_id: int, new_name: str) -> bool:
|
||||
sql = aime_user.update(aime_user.c.id == user_id).values(username=new_name)
|
||||
|
||||
result = await self.execute(sql)
|
||||
return result is not None
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
ALTER TABLE ongeki_user_event_point DROP COLUMN version;
|
||||
ALTER TABLE ongeki_user_event_point DROP COLUMN `rank`;
|
||||
ALTER TABLE ongeki_user_event_point DROP COLUMN `type`;
|
||||
ALTER TABLE ongeki_user_event_point DROP COLUMN rank;
|
||||
ALTER TABLE ongeki_user_event_point DROP COLUMN type;
|
||||
ALTER TABLE ongeki_user_event_point DROP COLUMN date;
|
||||
|
||||
ALTER TABLE ongeki_user_tech_event DROP COLUMN version;
|
||||
|
@ -19,4 +19,4 @@ DROP TABLE ongeki_static_tech_music;
|
|||
DROP TABLE ongeki_static_client_testmode;
|
||||
DROP TABLE ongeki_static_game_point;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
ALTER TABLE ongeki_user_event_point ADD COLUMN version INTEGER NOT NULL;
|
||||
ALTER TABLE ongeki_user_event_point ADD COLUMN `rank` INTEGER;
|
||||
ALTER TABLE ongeki_user_event_point ADD COLUMN `type` INTEGER NOT NULL;
|
||||
ALTER TABLE ongeki_user_event_point ADD COLUMN rank INTEGER;
|
||||
ALTER TABLE ongeki_user_event_point ADD COLUMN type INTEGER NOT NULL;
|
||||
ALTER TABLE ongeki_user_event_point ADD COLUMN date VARCHAR(25);
|
||||
|
||||
ALTER TABLE ongeki_user_tech_event ADD COLUMN version INTEGER NOT NULL;
|
||||
|
@ -12,87 +12,87 @@ ALTER TABLE ongeki_user_mission_point ADD COLUMN version INTEGER NOT NULL;
|
|||
ALTER TABLE ongeki_static_events ADD COLUMN endDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
CREATE TABLE ongeki_tech_event_ranking (
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
user INT NOT NULL,
|
||||
version INT NOT NULL,
|
||||
date VARCHAR(25),
|
||||
eventId INT NOT NULL,
|
||||
`rank` INT,
|
||||
totalPlatinumScore INT NOT NULL,
|
||||
totalTechScore INT NOT NULL,
|
||||
UNIQUE KEY ongeki_tech_event_ranking_uk (user, eventId),
|
||||
CONSTRAINT ongeki_tech_event_ranking_ibfk1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
user INT NOT NULL,
|
||||
version INT NOT NULL,
|
||||
date VARCHAR(25),
|
||||
eventId INT NOT NULL,
|
||||
rank INT,
|
||||
totalPlatinumScore INT NOT NULL,
|
||||
totalTechScore INT NOT NULL,
|
||||
UNIQUE KEY ongeki_tech_event_ranking_uk (user, eventId),
|
||||
CONSTRAINT ongeki_tech_event_ranking_ibfk1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE ongeki_static_music_ranking_list (
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
version INT NOT NULL,
|
||||
musicId INT NOT NULL,
|
||||
point INT NOT NULL,
|
||||
userName VARCHAR(255),
|
||||
UNIQUE KEY ongeki_static_music_ranking_list_uk (version, musicId)
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
version INT NOT NULL,
|
||||
musicId INT NOT NULL,
|
||||
point INT NOT NULL,
|
||||
userName VARCHAR(255),
|
||||
UNIQUE KEY ongeki_static_music_ranking_list_uk (version, musicId)
|
||||
);
|
||||
|
||||
CREATE TABLE ongeki_static_rewards (
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
version INT NOT NULL,
|
||||
rewardId INT NOT NULL,
|
||||
rewardName VARCHAR(255) NOT NULL,
|
||||
itemKind INT NOT NULL,
|
||||
itemId INT NOT NULL,
|
||||
UNIQUE KEY ongeki_tech_event_ranking_uk (version, rewardId)
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
version INT NOT NULL,
|
||||
rewardId INT NOT NULL,
|
||||
rewardName VARCHAR(255) NOT NULL,
|
||||
itemKind INT NOT NULL,
|
||||
itemId INT NOT NULL,
|
||||
UNIQUE KEY ongeki_tech_event_ranking_uk (version, rewardId)
|
||||
);
|
||||
|
||||
CREATE TABLE ongeki_static_present_list (
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
version INT NOT NULL,
|
||||
presentId INT NOT NULL,
|
||||
presentName VARCHAR(255) NOT NULL,
|
||||
rewardId INT NOT NULL,
|
||||
stock INT NOT NULL,
|
||||
message VARCHAR(255),
|
||||
startDate VARCHAR(25) NOT NULL,
|
||||
endDate VARCHAR(25) NOT NULL,
|
||||
UNIQUE KEY ongeki_static_present_list_uk (version, presentId, rewardId)
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
version INT NOT NULL,
|
||||
presentId INT NOT NULL,
|
||||
presentName VARCHAR(255) NOT NULL,
|
||||
rewardId INT NOT NULL,
|
||||
stock INT NOT NULL,
|
||||
message VARCHAR(255),
|
||||
startDate VARCHAR(25) NOT NULL,
|
||||
endDate VARCHAR(25) NOT NULL,
|
||||
UNIQUE KEY ongeki_static_present_list_uk (version, presentId, rewardId)
|
||||
);
|
||||
|
||||
CREATE TABLE ongeki_static_tech_music (
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
version INT NOT NULL,
|
||||
eventId INT NOT NULL,
|
||||
musicId INT NOT NULL,
|
||||
level INT NOT NULL,
|
||||
UNIQUE KEY ongeki_static_tech_music_uk (version, musicId, eventId)
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
version INT NOT NULL,
|
||||
eventId INT NOT NULL,
|
||||
musicId INT NOT NULL,
|
||||
level INT NOT NULL,
|
||||
UNIQUE KEY ongeki_static_tech_music_uk (version, musicId, eventId)
|
||||
);
|
||||
|
||||
CREATE TABLE ongeki_static_client_testmode (
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
regionId INT NOT NULL,
|
||||
placeId INT NOT NULL,
|
||||
clientId VARCHAR(11) NOT NULL,
|
||||
updateDate TIMESTAMP NOT NULL,
|
||||
isDelivery BOOLEAN NOT NULL,
|
||||
groupId INT NOT NULL,
|
||||
groupRole INT NOT NULL,
|
||||
continueMode INT NOT NULL,
|
||||
selectMusicTime INT NOT NULL,
|
||||
advertiseVolume INT NOT NULL,
|
||||
eventMode INT NOT NULL,
|
||||
eventMusicNum INT NOT NULL,
|
||||
patternGp INT NOT NULL,
|
||||
limitGp INT NOT NULL,
|
||||
maxLeverMovable INT NOT NULL,
|
||||
minLeverMovable INT NOT NULL,
|
||||
UNIQUE KEY ongeki_static_client_testmode_uk (clientId)
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
regionId INT NOT NULL,
|
||||
placeId INT NOT NULL,
|
||||
clientId VARCHAR(11) NOT NULL,
|
||||
updateDate TIMESTAMP NOT NULL,
|
||||
isDelivery BOOLEAN NOT NULL,
|
||||
groupId INT NOT NULL,
|
||||
groupRole INT NOT NULL,
|
||||
continueMode INT NOT NULL,
|
||||
selectMusicTime INT NOT NULL,
|
||||
advertiseVolume INT NOT NULL,
|
||||
eventMode INT NOT NULL,
|
||||
eventMusicNum INT NOT NULL,
|
||||
patternGp INT NOT NULL,
|
||||
limitGp INT NOT NULL,
|
||||
maxLeverMovable INT NOT NULL,
|
||||
minLeverMovable INT NOT NULL,
|
||||
UNIQUE KEY ongeki_static_client_testmode_uk (clientId)
|
||||
);
|
||||
|
||||
CREATE TABLE ongeki_static_game_point (
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
`type` INT NOT NULL,
|
||||
cost INT NOT NULL,
|
||||
startDate VARCHAR(25) NOT NULL DEFAULT "2000-01-01 05:00:00.0",
|
||||
endDate VARCHAR(25) NOT NULL DEFAULT "2099-01-01 05:00:00.0",
|
||||
UNIQUE KEY ongeki_static_game_point_uk (`type`)
|
||||
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
||||
type INT NOT NULL,
|
||||
cost INT NOT NULL,
|
||||
startDate VARCHAR(25) NOT NULL DEFAULT "2000-01-01 05:00:00.0",
|
||||
endDate VARCHAR(25) NOT NULL DEFAULT "2099-01-01 05:00:00.0",
|
||||
UNIQUE KEY ongeki_static_game_point_uk (type)
|
||||
);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
ALTER TABLE mai2_profile_detail
|
||||
DROP COLUMN currentPlayCount,
|
||||
DROP COLUMN renameCredit;
|
||||
ALTER TABLE mai2_profile_detail DROP COLUMN currentPlayCount;
|
||||
ALTER TABLE mai2_profile_detail DROP COLUMN renameCredit;
|
||||
|
||||
ALTER TABLE mai2_playlog
|
||||
DROP COLUMN extBool1;
|
||||
|
||||
DROP TABLE IF EXISTS `mai2_playlog_2p`;
|
||||
ALTER TABLE mai2_playlog DROP COLUMN extBool1;
|
||||
|
|
|
@ -1,20 +1,4 @@
|
|||
ALTER TABLE mai2_profile_detail
|
||||
ADD currentPlayCount INT NULL AFTER playCount,
|
||||
ADD renameCredit INT NULL AFTER banState;
|
||||
ALTER TABLE mai2_profile_detail ADD currentPlayCount INT NULL;
|
||||
ALTER TABLE mai2_profile_detail ADD renameCredit INT NULL;
|
||||
|
||||
ALTER TABLE mai2_playlog
|
||||
ADD extBool1 BOOLEAN NULL AFTER extNum4;
|
||||
|
||||
CREATE TABLE `mai2_playlog_2p` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`user` INT NOT NULL,
|
||||
`userId1` BIGINT,
|
||||
`userId2` BIGINT,
|
||||
`userName1` VARCHAR(255),
|
||||
`userName2` VARCHAR(255),
|
||||
`regionId` INT,
|
||||
`placeId` INT,
|
||||
`user2pPlaylogDetailList` JSON,
|
||||
PRIMARY KEY (`id`),
|
||||
FOREIGN KEY (`user`) REFERENCES `aime_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
ALTER TABLE mai2_playlog ADD extBool1 BOOLEAN NULL;
|
||||
|
|
1129
core/frontend.py
1129
core/frontend.py
File diff suppressed because it is too large
Load Diff
256
core/mucha.py
256
core/mucha.py
|
@ -1,24 +1,18 @@
|
|||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, List
|
||||
import logging, coloredlogs
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import coloredlogs
|
||||
import pytz
|
||||
from twisted.web import resource
|
||||
from twisted.web.http import Request
|
||||
from datetime import datetime
|
||||
from Crypto.Cipher import Blowfish
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import PlainTextResponse
|
||||
import pytz
|
||||
|
||||
from .config import CoreConfig
|
||||
from .const import *
|
||||
from .data import Data
|
||||
from .title import TitleServlet
|
||||
from .utils import Utils
|
||||
|
||||
from .title import TitleServlet
|
||||
|
||||
class MuchaServlet:
|
||||
mucha_registry: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
mucha_registry: List[str] = []
|
||||
def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None:
|
||||
self.config = cfg
|
||||
self.config_dir = cfg_dir
|
||||
|
@ -41,174 +35,91 @@ class MuchaServlet:
|
|||
self.logger.addHandler(consoleHandler)
|
||||
|
||||
self.logger.setLevel(cfg.mucha.loglevel)
|
||||
coloredlogs.install(
|
||||
level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||
)
|
||||
|
||||
self.data = Data(cfg)
|
||||
coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str)
|
||||
|
||||
for _, mod in TitleServlet.title_registry.items():
|
||||
enabled, game_cds, netids = mod.get_mucha_info(self.config, self.config_dir)
|
||||
if enabled:
|
||||
for x in range(len(game_cds)):
|
||||
self.mucha_registry[game_cds[x]] = {"netid_prefix": netids[x]}
|
||||
if hasattr(mod, "get_mucha_info"):
|
||||
enabled, game_cd = mod.get_mucha_info(
|
||||
self.config, self.config_dir
|
||||
)
|
||||
if enabled:
|
||||
self.mucha_registry.append(game_cd)
|
||||
|
||||
self.logger.info(f"Serving {len(self.mucha_registry)} games")
|
||||
|
||||
async def handle_boardauth(self, request: Request) -> bytes:
|
||||
bod = await request.body()
|
||||
req_dict = self.mucha_preprocess(bod)
|
||||
def handle_boardauth(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 {bod}")
|
||||
return PlainTextResponse("RESULTS=000")
|
||||
self.logger.error(
|
||||
f"Error processing mucha request {request.content.getvalue()}"
|
||||
)
|
||||
return b"RESULTS=000"
|
||||
|
||||
req = MuchaAuthRequest(req_dict)
|
||||
self.logger.debug(f"Mucha request {vars(req)}")
|
||||
|
||||
if (
|
||||
not req.gameCd
|
||||
or not req.gameVer
|
||||
or not req.sendDate
|
||||
or not req.countryCd
|
||||
or not req.serialNum
|
||||
):
|
||||
self.logger.warn(f"Missing required fields - {vars(req)}")
|
||||
return PlainTextResponse("RESULTS=000")
|
||||
|
||||
minfo = self.mucha_registry.get(req.gameCd, {})
|
||||
|
||||
if not minfo:
|
||||
self.logger.warning(f"Unknown gameCd {req.gameCd} from {client_ip}")
|
||||
return PlainTextResponse("RESULTS=000")
|
||||
|
||||
b_key = b""
|
||||
for x in range(8):
|
||||
b_key += req.sendDate[(x - 1) & 7].encode()
|
||||
|
||||
b_iv = b_key # what the fuck namco
|
||||
|
||||
cipher = Blowfish.new(b_key, Blowfish.MODE_CBC, b_iv)
|
||||
try:
|
||||
sn_decrypt = cipher.decrypt(bytes.fromhex(req.serialNum))[:12].decode()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Decrypt SN {req.serialNum} failed! - {e}")
|
||||
return PlainTextResponse("RESULTS=000")
|
||||
|
||||
self.logger.info(
|
||||
f"Boardauth request from {sn_decrypt} ({client_ip}) for {req.gameVer}"
|
||||
)
|
||||
|
||||
resp = MuchaAuthResponse(
|
||||
f"{self.config.server.hostname}{':' + str(self.config.server.port) if not self.config.server.is_using_proxy else ''}"
|
||||
)
|
||||
|
||||
netid = minfo.get("netid_prefix", "ABxN") + sn_decrypt[5:]
|
||||
|
||||
cab = await self.data.arcade.get_machine(netid)
|
||||
if cab:
|
||||
arcade = await self.data.arcade.get_arcade(cab["id"])
|
||||
if not arcade:
|
||||
self.logger.error(f"Failed to get arcade with id {cab['id']}")
|
||||
return PlainTextResponse("RESULTS=000")
|
||||
|
||||
resp.AREA_0 = arcade["region_id"] or AllnetJapanRegionId.AICHI.name
|
||||
resp.AREA_0_EN = arcade["region_id"] or AllnetJapanRegionId.AICHI.name
|
||||
resp.AREA_FULL_0 = arcade["region_id"] or AllnetJapanRegionId.AICHI.name
|
||||
resp.AREA_FULL_0_EN = arcade["region_id"] or AllnetJapanRegionId.AICHI.name
|
||||
|
||||
resp.AREA_1 = (
|
||||
arcade["country"] or cab["country"] or AllnetCountryCode.JAPAN.value
|
||||
)
|
||||
resp.AREA_1_EN = (
|
||||
arcade["country"] or cab["country"] or AllnetCountryCode.JAPAN.value
|
||||
)
|
||||
resp.AREA_FULL_1 = (
|
||||
arcade["country"] or cab["country"] or AllnetCountryCode.JAPAN.value
|
||||
)
|
||||
resp.AREA_FULL_1_EN = (
|
||||
arcade["country"] or cab["country"] or AllnetCountryCode.JAPAN.value
|
||||
)
|
||||
|
||||
resp.AREA_2 = arcade["city"] if arcade["city"] else ""
|
||||
resp.AREA_2_EN = arcade["city"] if arcade["city"] else ""
|
||||
resp.AREA_FULL_2 = arcade["city"] if arcade["city"] else ""
|
||||
resp.AREA_FULL_2_EN = arcade["city"] if arcade["city"] else ""
|
||||
|
||||
resp.AREA_3 = ""
|
||||
resp.AREA_3_EN = ""
|
||||
resp.AREA_FULL_3 = ""
|
||||
resp.AREA_FULL_3_EN = ""
|
||||
|
||||
resp.PREFECTURE_ID = arcade["region_id"]
|
||||
resp.COUNTRY_CD = (
|
||||
arcade["country"] or cab["country"] or AllnetCountryCode.JAPAN.value
|
||||
)
|
||||
resp.PLACE_ID = (
|
||||
req.placeId
|
||||
if req.placeId
|
||||
else f"{arcade['country'] or cab['country'] or AllnetCountryCode.JAPAN.value}{arcade['id']:04X}"
|
||||
)
|
||||
resp.SHOP_NAME = arcade["name"]
|
||||
resp.SHOP_NAME_EN = arcade["name"]
|
||||
resp.SHOP_NICKNAME = arcade["nickname"]
|
||||
resp.SHOP_NICKNAME_EN = arcade["nickname"]
|
||||
|
||||
elif self.config.server.allow_unregistered_serials:
|
||||
self.logger.info(f"Allow unknown serial {netid} ({sn_decrypt}) to auth")
|
||||
|
||||
else:
|
||||
self.logger.warn(f"Auth failed for NetID {netid}")
|
||||
return PlainTextResponse("RESULTS=000")
|
||||
|
||||
self.logger.debug(f"Mucha response {vars(resp)}")
|
||||
|
||||
return PlainTextResponse(self.mucha_postprocess(vars(resp)))
|
||||
|
||||
async def handle_updatecheck(self, request: Request) -> bytes:
|
||||
bod = await request.body()
|
||||
req_dict = self.mucha_preprocess(bod)
|
||||
client_ip = Utils.get_ip_addr(request)
|
||||
|
||||
if req_dict is None:
|
||||
self.logger.error(f"Error processing mucha request {bod}")
|
||||
return PlainTextResponse("RESULTS=000")
|
||||
|
||||
req = MuchaUpdateRequest(req_dict)
|
||||
self.logger.info(
|
||||
f"Updatecheck request from {req.serialNum} ({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:
|
||||
self.logger.warning(f"Unknown gameCd {req.gameCd}")
|
||||
return PlainTextResponse("RESULTS=000")
|
||||
return b"RESULTS=000"
|
||||
|
||||
resp = MuchaUpdateResponse(
|
||||
req.gameVer,
|
||||
f"{self.config.server.hostname}{':' + str(self.config.server.port) if not self.config.server.is_using_proxy else ''}",
|
||||
# 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(
|
||||
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)}")
|
||||
|
||||
return PlainTextResponse(self.mucha_postprocess(vars(resp)))
|
||||
return self.mucha_postprocess(vars(resp))
|
||||
|
||||
async def handle_dlstate(self, request: Request) -> bytes:
|
||||
bod = await request.body()
|
||||
req_dict = self.mucha_preprocess(bod)
|
||||
def handle_updatecheck(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 {bod}")
|
||||
return PlainTextResponse("RESULTS=000")
|
||||
self.logger.error(
|
||||
f"Error processing mucha request {request.content.getvalue()}"
|
||||
)
|
||||
return b"RESULTS=000"
|
||||
|
||||
req = MuchaUpdateRequest(req_dict)
|
||||
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:
|
||||
self.logger.warning(f"Unknown gameCd {req.gameCd}")
|
||||
return b"RESULTS=000"
|
||||
|
||||
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)}")
|
||||
|
||||
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 {req.serialNum} ({client_ip}) for {req.gameCd} -> {req.updateVer}"
|
||||
)
|
||||
self.logger.info(f"DownloadState request from {client_ip} for {req.gameCd} -> {req.updateVer}")
|
||||
self.logger.debug(f"request {vars(req)}")
|
||||
return PlainTextResponse("RESULTS=001")
|
||||
return b"RESULTS=001"
|
||||
|
||||
def mucha_preprocess(self, data: bytes) -> Optional[Dict]:
|
||||
try:
|
||||
|
@ -258,7 +169,7 @@ class MuchaAuthResponse:
|
|||
self.RESULTS = "001"
|
||||
self.AUTH_INTERVAL = "86400"
|
||||
self.SERVER_TIME = datetime.strftime(datetime.now(), "%Y%m%d%H%M")
|
||||
self.SERVER_TIME_UTC = datetime.strftime(datetime.now(pytz.UTC), "%Y%m%d%H%M")
|
||||
self.UTC_SERVER_TIME = datetime.strftime(datetime.now(pytz.UTC), "%Y%m%d%H%M")
|
||||
|
||||
self.CHARGE_URL = f"https://{mucha_url}/charge/"
|
||||
self.FILE_URL = f"https://{mucha_url}/file/"
|
||||
|
@ -310,7 +221,7 @@ class MuchaUpdateRequest:
|
|||
|
||||
class MuchaUpdateResponse:
|
||||
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
|
||||
|
@ -329,7 +240,6 @@ class MuchaUpdateResponse:
|
|||
self.USER_ID = ""
|
||||
self.PASSWORD = ""
|
||||
|
||||
|
||||
"""
|
||||
RESULTS
|
||||
EXE_VER
|
||||
|
@ -350,16 +260,13 @@ LAN_INFO_SIZE_1
|
|||
USER_ID
|
||||
PASSWORD
|
||||
"""
|
||||
|
||||
|
||||
class MuchaUpdateResponseStub:
|
||||
def __init__(self, game_ver: str) -> None:
|
||||
self.RESULTS = "001"
|
||||
self.UPDATE_VER_1 = game_ver
|
||||
|
||||
|
||||
class MuchaDownloadStateRequest:
|
||||
def __init__(self, request: Dict) -> None:
|
||||
def __init__(self, request: Dict) -> None:
|
||||
self.gameCd = request.get("gameCd", "")
|
||||
self.updateVer = request.get("updateVer", "")
|
||||
self.serialNum = request.get("serialNum", "")
|
||||
|
@ -369,9 +276,8 @@ class MuchaDownloadStateRequest:
|
|||
self.placeId = request.get("placeId", "")
|
||||
self.storeRouterIp = request.get("storeRouterIp", "")
|
||||
|
||||
|
||||
class MuchaDownloadErrorRequest:
|
||||
def __init__(self, request: Dict) -> None:
|
||||
def __init__(self, request: Dict) -> None:
|
||||
self.gameCd = request.get("gameCd", "")
|
||||
self.updateVer = request.get("updateVer", "")
|
||||
self.serialNum = request.get("serialNum", "")
|
||||
|
@ -382,11 +288,10 @@ class MuchaDownloadErrorRequest:
|
|||
self.placeId = request.get("placeId", "")
|
||||
self.storeRouterIp = request.get("storeRouterIp", "")
|
||||
|
||||
|
||||
class MuchaRegiAuthRequest:
|
||||
def __init__(self, request: Dict) -> None:
|
||||
def __init__(self, request: Dict) -> None:
|
||||
self.gameCd = request.get("gameCd", "")
|
||||
self.serialNum = request.get("serialNum", "") # Encrypted
|
||||
self.serialNum = request.get("serialNum", "") # Encrypted
|
||||
self.countryCd = request.get("countryCd", "")
|
||||
self.registrationCd = request.get("registrationCd", "")
|
||||
self.sendDate = request.get("sendDate", "")
|
||||
|
@ -395,16 +300,14 @@ class MuchaRegiAuthRequest:
|
|||
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
|
||||
|
||||
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:
|
||||
def __init__(self, request: Dict) -> None:
|
||||
self.gameCd = request.get("gameCd", "")
|
||||
self.serialNum = request.get("serialNum", "")
|
||||
self.countryCd = request.get("countryCd", "")
|
||||
|
@ -413,14 +316,12 @@ class MuchaTokenStateRequest:
|
|||
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:
|
||||
def __init__(self, request: Dict) -> None:
|
||||
self.gameCd = request.get("gameCd", "")
|
||||
self.serialNum = request.get("serialNum", "")
|
||||
self.countryCd = request.get("countryCd", "")
|
||||
|
@ -429,7 +330,6 @@ class MuchaTokenMarginStateRequest:
|
|||
self.limitUpperToken = request.get("limitUpperToken", 0)
|
||||
self.settlementMonth = request.get("settlementMonth", 0)
|
||||
|
||||
|
||||
class MuchaTokenMarginStateResponse:
|
||||
def __init__(self) -> None:
|
||||
self.RESULTS = "001"
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
{% extends "core/templates/index.jinja" %}
|
||||
{% block content %}
|
||||
{% if arcade is defined %}
|
||||
<h1>{{ arcade.name }}</h1>
|
||||
<h2>PCBs assigned to this arcade <button class="btn btn-success" id="btn_add_cab" onclick="toggle_add_cab_form()">Add</button></h2>
|
||||
{% if success is defined and success == 3 %}
|
||||
<div style="background-color: #00AA00; padding: 20px; margin-bottom: 10px; width: 15%;">
|
||||
Cab added successfully
|
||||
</div>
|
||||
{% endif %}
|
||||
<ul style="font-size: 20px;">
|
||||
{% for c in arcade.cabs %}
|
||||
<li><a href="/cab/{{ c.id }}">{{ c.serial }} ({{ c.game }})</a> <button class="btn btn-secondary" onclick="prep_edit_form()">Edit</button> <button class="btn-danger btn">Delete</button></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<h3>Arcade Not Found</h3>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
|
@ -1,24 +0,0 @@
|
|||
{% extends "core/templates/index.jinja" %}
|
||||
{% block content %}
|
||||
<h1>Create User</h1>
|
||||
<form id="create" style="max-width: 240px; min-width: 10%;" action="/gate/gate.create" method="post">
|
||||
<div class="form-group row">
|
||||
<label for="access_code">Card Access Code</label><br>
|
||||
<input class="form-control" name="access_code" id="access_code" type="text" placeholder="00000000000000000000" value={{ code }} maxlength="20" readonly>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="username">Username</label><br>
|
||||
<input id="username" class="form-control" name="username" type="text" placeholder="username">
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="email">Email</label><br>
|
||||
<input id="email" class="form-control" name="email" type="email" placeholder="example@example.com">
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="passwd">Password</label><br>
|
||||
<input id="passwd" class="form-control" name="passwd" type="password" placeholder="password">
|
||||
</div>
|
||||
<p></p>
|
||||
<input id="submit" class="btn btn-primary" style="display: block; margin: 0 auto;" type="submit" value="Create">
|
||||
</form>
|
||||
{% endblock content %}
|
|
@ -1,32 +0,0 @@
|
|||
{% extends "core/templates/index.jinja" %}
|
||||
{% block content %}
|
||||
<h1>Gate</h1>
|
||||
{% include "core/templates/widgets/err_banner.jinja" %}
|
||||
<style>
|
||||
/* Chrome, Safari, Edge, Opera */
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
</style>
|
||||
<form id="login" style="max-width: 240px; min-width: 10%;" action="/gate/gate.login" method="post">
|
||||
<div class="form-group row">
|
||||
<label for="access_code">Card Access Code</label><br>
|
||||
<input form="login" class="form-control" name="access_code" id="access_code" type="number" placeholder="00000000000000000000" maxlength="20" required>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="passwd">Password</label><br>
|
||||
<input id="passwd" class="form-control" name="passwd" type="password" placeholder="password">
|
||||
</div>
|
||||
<p></p>
|
||||
<input id="submit" class="btn btn-primary" style="display: block; margin: 0 auto;" form="login" type="submit" value="Login">
|
||||
</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>
|
||||
<h6>*If you have not registered a card with this server, you cannot create a webui account.</h6>
|
||||
{% endblock content %}
|
|
@ -1,92 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<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">
|
||||
<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>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
|
||||
<style>
|
||||
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%;
|
||||
}
|
||||
.modal-content {
|
||||
background-color: #181a1b;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% include "core/templates/widgets/topbar.jinja" %}
|
||||
{% block content %}
|
||||
<h1>{{ server_name }}</h1>
|
||||
{% endblock content %}
|
||||
</body>
|
||||
</html>
|
|
@ -1,4 +0,0 @@
|
|||
{% extends "core/templates/index.jinja" %}
|
||||
{% block content %}
|
||||
<h1>Machine Management</h1>
|
||||
{% endblock content %}
|
|
@ -1,69 +0,0 @@
|
|||
{% extends "core/templates/index.jinja" %}
|
||||
{% block content %}
|
||||
<h1>System Management</h1>
|
||||
{% if error is defined %}
|
||||
{% include "core/templates/widgets/err_banner.jinja" %}
|
||||
{% endif %}
|
||||
<div class="row" id="rowForm">
|
||||
{% if "{:08b}".format(sesh.permissions)[6] == "1" %}
|
||||
<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="usrId">User ID</label>
|
||||
<input type="number" class="form-control" id="usrId" name="usrId">
|
||||
</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="usrEmail">Email address</label>
|
||||
<input type="email" class="form-control" id="usrEmail" name="usrEmail" aria-describedby="emailHelp">
|
||||
</div>
|
||||
<br />
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if "{:08b}".format(sesh.permissions)[5] == "1" %}
|
||||
<div class="col-sm-6" style="max-width: 25%;">
|
||||
<form id="shopLookup" name="shopLookup" action="/sys/lookup.shop" class="form-inline">
|
||||
<h3>Shop search</h3>
|
||||
<div class="form-group">
|
||||
<label for="shopId">Shop ID</label>
|
||||
<input type="number" class="form-control" id="shopId" name="shopId">
|
||||
</div>
|
||||
OR
|
||||
<div class="form-group">
|
||||
<label for="serialNum">Serial Number</label>
|
||||
<input type="text" class="form-control" id="serialNum" name="serialNum" maxlength="15">
|
||||
</div>
|
||||
<br />
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row" id="rowResult" style="margin: 10px;">
|
||||
{% if "{:08b}".format(sesh.permissions)[6] == "1" %}
|
||||
<div id="userSearchResult" class="col-sm-6" style="max-width: 25%;">
|
||||
{% for usr in usrlist %}
|
||||
<a href=/user/{{ usr.id }}><pre>{{ usr.username if usr.username is not none else "<i>No Name Set</i>"}}</pre></a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if "{:08b}".format(sesh.permissions)[5] == "1" %}
|
||||
<div id="shopSearchResult" class="col-sm-6" style="max-width: 25%;">
|
||||
{% for shop in shoplist %}
|
||||
<a href="/shop/{{ shop.id }}"><pre>{{ shop.name if shop.name else "<i>No Name Set</i>"}}</pre></a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row" id="rowAdd">
|
||||
|
||||
</div>
|
||||
{% endblock content %}
|
|
@ -1,175 +0,0 @@
|
|||
{% extends "core/templates/index.jinja" %}
|
||||
{% block content %}
|
||||
<script type="text/javascript">
|
||||
function toggle_new_name_form() {
|
||||
let frm = document.getElementById("new_name_form");
|
||||
let btn = document.getElementById("btn_toggle_form");
|
||||
|
||||
if (frm.style['display'] != "") {
|
||||
frm.style['display'] = "";
|
||||
frm.style['max-height'] = "";
|
||||
btn.innerText = "Cancel";
|
||||
} else {
|
||||
frm.style['display'] = "none";
|
||||
frm.style['max-height'] = "0px";
|
||||
btn.innerText = "Edit";
|
||||
}
|
||||
}
|
||||
function toggle_add_card_form() {
|
||||
let btn = document.getElementById("btn_add_card");
|
||||
let dv = document.getElementById("add_card_container")
|
||||
|
||||
if (dv.style['display'] != "") {
|
||||
btn.innerText = "Cancel";
|
||||
dv.style['display'] = "";
|
||||
} else {
|
||||
btn.innerText = "Add";
|
||||
dv.style['display'] = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function prep_edit_form(access_code, chip_id, idm, card_type, u_memo) {
|
||||
ac = document.getElementById("card_edit_frm_access_code");
|
||||
cid = document.getElementById("card_edit_frm_chip_id");
|
||||
fidm = document.getElementById("card_edit_frm_idm");
|
||||
memo = document.getElementById("card_edit_frm_memo");
|
||||
|
||||
if (chip_id == "None" || chip_id == undefined) {
|
||||
chip_id = ""
|
||||
}
|
||||
if (idm == "None" || idm == undefined) {
|
||||
idm = ""
|
||||
}
|
||||
if (u_memo == "None" || u_memo == undefined) {
|
||||
u_memo = ""
|
||||
}
|
||||
|
||||
ac.value = access_code;
|
||||
cid.value = chip_id;
|
||||
fidm.value = idm;
|
||||
memo.value = u_memo;
|
||||
|
||||
if (card_type == "AmusementIC") {
|
||||
cid.disabled = true;
|
||||
fidm.disabled = false;
|
||||
} else {
|
||||
cid.disabled = false;
|
||||
fidm.disabled = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<h1>Management for {{ username }} <button onclick="toggle_new_name_form()" class="btn btn-secondary" id="btn_toggle_form">Edit</button></h1>
|
||||
{% if error is defined %}
|
||||
{% include "core/templates/widgets/err_banner.jinja" %}
|
||||
{% endif %}
|
||||
{% if success is defined and success == 2 %}
|
||||
<div style="background-color: #00AA00; padding: 20px; margin-bottom: 10px; width: 15%;">
|
||||
Update successful
|
||||
</div>
|
||||
{% endif %}
|
||||
<form style="max-width: 33%; display: none; max-height: 0px;" action="/user/update.name" method="post" id="new_name_form">
|
||||
<div class="mb-3">
|
||||
<label for="new_name" class="form-label">New Nickname</label>
|
||||
<input type="text" class="form-control" id="new_name" name="new_name" aria-describedby="new_name_help">
|
||||
<div id="new_name_help" class="form-text">Must be 10 characters or less</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
<p></p>
|
||||
<h2>Cards <button class="btn btn-success" id="btn_add_card" onclick="toggle_add_card_form()">Add</button></h2>
|
||||
{% if success is defined and success == 3 %}
|
||||
<div style="background-color: #00AA00; padding: 20px; margin-bottom: 10px; width: 15%;">
|
||||
Card added successfully
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="add_card_container" style="display: none; max-width: 33%;">
|
||||
<form action="/user/add.card" method="post", id="frm_add_card">
|
||||
<label class="form-label" for="card_add_frm_access_code">Access Code:</label>
|
||||
<input class="form-control" name="add_access_code" id="card_add_frm_access_code" maxlength="20" type="text" required aria-describedby="ac_help">
|
||||
<div id="ac_help" class="form-text">20 digit code on the back of the card.</div>
|
||||
<button type="submit" class="btn btn-primary">Add</button>
|
||||
</form>
|
||||
<br>
|
||||
</div>
|
||||
<ul style="font-size: 20px;">
|
||||
{% for c in cards %}
|
||||
<li>{{ c.access_code }} ({{ c.type}}): {{ c.status }} <button onclick="prep_edit_form('{{ c.access_code }}', '{{ c.chip_id}}', '{{ c.idm }}', '{{ c.type }}', '{{ c.memo }}')" data-bs-toggle="modal" data-bs-target="#card_edit" class="btn btn-secondary" id="btn_edit_card_{{ c.access_code }}">Edit</button> {% if c.status == 'Active'%}<button class="btn-warning btn">Lock</button>{% elif c.status == 'Locked' %}<button class="btn-warning btn">Unlock</button>{% endif %} <button class="btn-danger btn">Delete</button></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>Reset Password</h2>
|
||||
{% if success is defined and success == 1 %}
|
||||
<div style="background-color: #00AA00; padding: 20px; margin-bottom: 10px; width: 15%;">
|
||||
Update successful
|
||||
</div>
|
||||
{% endif %}
|
||||
<form style="max-width: 33%;" action="/user/update.pw" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="current_pw" class="form-label">Current Password</label>
|
||||
<input type="password" class="form-control" id="current_pw" name="current_pw">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password1" class="form-label">New Password</label>
|
||||
<input type="password" class="form-control" id="password1" name="password1" aria-describedby="password_help">
|
||||
<div id="password_help" class="form-text">Password must be at least 10 characters long, contain an upper and lowercase character, number, and special character</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password2" class="form-label">Retype New Password</label>
|
||||
<input type="password" class="form-control" id="password2" name="password2">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
|
||||
{% if arcades is defined and arcades|length > 0 %}
|
||||
<h2>Arcades</h2>
|
||||
<ul>
|
||||
{% for a in arcades %}
|
||||
<li><h3>{{ a.name }}</h3>
|
||||
{% if a.machines|length > 0 %}
|
||||
<table>
|
||||
<tr><th>Serial</th><th>Game</th><th>Last Seen</th></tr>
|
||||
{% for m in a.machines %}
|
||||
<tr><td>{{ m.serial }}</td><td>{{ m.game }}</td><td>{{ m.last_seen }}</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<div class="modal fade" id="card_edit" tabindex="-1" aria-labelledby="card_edit_label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="card_edit_label">Edit Card</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="/user/edit.card" method="post" id="frm_edit_card">
|
||||
<label class="form-label" for="card_edit_frm_access_code">Access Code:</label>
|
||||
<input class="form-control" readonly name="add_access_code" id="card_edit_frm_access_code" maxlength="20" type="text" required aria-describedby="ac_help">
|
||||
<div id="ac_help" class="form-text">20 digit code on the back of the card. If this is incorrect, contact a sysadmin.</div>
|
||||
|
||||
<label class="form-label" for="card_edit_frm_memo" id="card_edit_frm_memo_lbl">Memo:</label>
|
||||
<input class="form-control" aria-describedby="memo_help" name="add_memo" id="card_edit_frm_memo" maxlength="16" type="text">
|
||||
<div id="memo_help" class="form-text">Must be 16 characters or less.</div>
|
||||
|
||||
<label class="form-label" for="card_edit_frm_idm" id="card_edit_frm_idm_lbl">FeliCa IDm:</label>
|
||||
<input class="form-control" aria-describedby="idm_help" name="add_felica_idm" id="card_edit_frm_idm" maxlength="16" type="text">
|
||||
<div id="idm_help" class="form-text">8 bytes that uniquly idenfites a FeliCa card. Obtained by reading the card with an NFC reader.</div>
|
||||
|
||||
<label class="form-label" for="card_edit_frm_chip_id" id="card_edit_frm_chip_id_lbl">Mifare UID:</label>
|
||||
<input class="form-control" aria-describedby="chip_id_help" name="add_mifare_chip_id" id="card_edit_frm_chip_id" maxlength="8" type="text">
|
||||
<div id="chip_id_help" class="form-text">4 byte integer that uniquly identifies a Mifare card. Obtained by reading the card with an NFC reader.</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary" form="frm_edit_card">Edit</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
|
@ -1,28 +0,0 @@
|
|||
{% if error > 0 %}
|
||||
<div class="err-banner">
|
||||
<h3>Error</h3>
|
||||
{% if error == 1 %}
|
||||
Card not registered, or wrong password
|
||||
{% elif error == 2 %}
|
||||
Missing or malformed access code
|
||||
{% elif error == 3 %}
|
||||
Failed to create user
|
||||
{% elif error == 4 %}
|
||||
Required field not filled or invalid
|
||||
{% elif error == 5 %}
|
||||
Incorrect old password
|
||||
{% elif error == 6 %}
|
||||
Passwords don't match
|
||||
{% elif error == 7 %}
|
||||
New password not acceptable
|
||||
{% elif error == 8 %}
|
||||
New Nickname too long
|
||||
{% elif error == 9 %}
|
||||
You must be logged in to preform this action
|
||||
{% elif error == 10 %}
|
||||
Invalid serial number
|
||||
{% else %}
|
||||
An unknown error occoured
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
|
@ -1,22 +0,0 @@
|
|||
<div style="background: #333; color: #f9f9f9; width: 10%; height: 50px; line-height: 50px; text-align: center; float: left;">
|
||||
Navigation
|
||||
</div>
|
||||
<div style="background: #333; color: #f9f9f9; width: 80%; height: 50px; line-height: 50px; padding-left: 10px; float: left;">
|
||||
<a href=/><button class="btn btn-primary">Home</button></a>
|
||||
{% for game, data in game_list|items %}
|
||||
<a href=/game{{ data.url }}/><button class="btn btn-success">{{ game }}</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["permissions"] >= 2 %}
|
||||
<a href="/sys/"><button class="btn btn-primary">System</button></a>
|
||||
{% endif %}
|
||||
{% if sesh is defined and sesh["user_id"] > 0 %}
|
||||
<a href="/user/"><button class="btn btn-primary">Account</button></a>
|
||||
<a href="/user/logout"><button class="btn btn-danger">Logout</button></a>
|
||||
{% else %}
|
||||
<a href="/gate/"><button class="btn btn-primary">Gate</button></a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
117
core/title.py
117
core/title.py
|
@ -1,28 +1,12 @@
|
|||
import json
|
||||
import logging
|
||||
from typing import Dict, List, Tuple
|
||||
import logging, coloredlogs
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
import coloredlogs
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
from starlette.routing import Route
|
||||
from twisted.web.http import Request
|
||||
|
||||
from core.config import CoreConfig
|
||||
from core.data import Data
|
||||
from core.utils import Utils
|
||||
|
||||
|
||||
class JSONResponseNoASCII(Response):
|
||||
media_type = "application/json"
|
||||
|
||||
def render(self, content: Any) -> bytes:
|
||||
return json.dumps(
|
||||
content,
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
class BaseServlet:
|
||||
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||
self.core_cfg = core_cfg
|
||||
|
@ -30,9 +14,7 @@ class BaseServlet:
|
|||
self.logger = logging.getLogger("title")
|
||||
|
||||
@classmethod
|
||||
def is_game_enabled(
|
||||
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
|
||||
) -> bool:
|
||||
def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool:
|
||||
"""Called during boot to check if a specific game code should load.
|
||||
|
||||
Args:
|
||||
|
@ -42,28 +24,29 @@ class BaseServlet:
|
|||
|
||||
Returns:
|
||||
bool: True if the game is enabled and set to run, False otherwise
|
||||
|
||||
|
||||
"""
|
||||
return False
|
||||
|
||||
def get_routes(self) -> List[Route]:
|
||||
|
||||
def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]:
|
||||
"""Called during boot to get all matcher endpoints this title servlet handles
|
||||
|
||||
Returns:
|
||||
List[Route]: A list of Routes, WebSocketRoutes, or similar classes
|
||||
Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]: A 2-length tuple where offset 0 is GET and offset 1 is POST,
|
||||
containing a list of 3-length tuples where offset 0 is the name of the function in the handler that should be called, offset 1
|
||||
is the matching string, and offset 2 is a dict containing rules for the matcher.
|
||||
"""
|
||||
return [
|
||||
Route("/{game}/{version}/{endpoint}", self.render_POST, methods=["POST"]),
|
||||
Route("/{game}/{version}/{endpoint}", self.render_GET, methods=["GET"]),
|
||||
]
|
||||
|
||||
return (
|
||||
[("render_GET", "/{game}/{version}/{endpoint}", {'game': R'S...'})],
|
||||
[("render_POST", "/{game}/{version}/{endpoint}", {'game': R'S...'})]
|
||||
)
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Called once during boot, should contain any additional setup the handler must do, such as starting any sub-services"""
|
||||
"""Called once during boot, should contain any additional setup the handler must do, such as starting any sub-services
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_allnet_info(
|
||||
self, game_code: str, game_ver: int, keychip: str
|
||||
) -> Tuple[str, str]:
|
||||
def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]:
|
||||
"""Called any time a request to PowerOn is made to retrieve the url/host strings to be sent back to the game
|
||||
|
||||
Args:
|
||||
|
@ -74,20 +57,12 @@ class BaseServlet:
|
|||
Returns:
|
||||
Tuple[str, str]: A tuple where offset 0 is the allnet uri field, and offset 1 is the allnet host field
|
||||
"""
|
||||
if (
|
||||
not self.core_cfg.server.is_using_proxy
|
||||
and Utils.get_title_port(self.core_cfg) != 80
|
||||
):
|
||||
return (
|
||||
f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/",
|
||||
"",
|
||||
)
|
||||
if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80:
|
||||
return (f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", "")
|
||||
|
||||
return (f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/", "")
|
||||
return (f"http://{self.core_cfg.title.hostname}/{game_code}/{game_ver}/", "")
|
||||
|
||||
def get_mucha_info(
|
||||
self, core_cfg: CoreConfig, cfg_dir: str
|
||||
) -> Tuple[bool, List[str], List[str]]:
|
||||
def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str]:
|
||||
"""Called once during boot to check if this game is a mucha game
|
||||
|
||||
Args:
|
||||
|
@ -97,20 +72,18 @@ class BaseServlet:
|
|||
Returns:
|
||||
Tuple[bool, str]: Tuple where offset 0 is true if the game is enabled, false otherwise, and offset 1 is the game CD
|
||||
"""
|
||||
return (False, [], [])
|
||||
return (False, "")
|
||||
|
||||
async def render_POST(self, request: Request) -> bytes:
|
||||
self.logger.warn(f"Game Does not dispatch POST")
|
||||
return Response()
|
||||
|
||||
async def render_GET(self, request: Request) -> bytes:
|
||||
self.logger.warn(f"Game Does not dispatch GET")
|
||||
return Response()
|
||||
def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes:
|
||||
self.logger.warn(f"{game_code} Does not dispatch POST")
|
||||
return None
|
||||
|
||||
def render_GET(self, request: Request, game_code: str, matchers: Dict) -> bytes:
|
||||
self.logger.warn(f"{game_code} Does not dispatch GET")
|
||||
return None
|
||||
|
||||
class TitleServlet:
|
||||
title_registry: Dict[str, BaseServlet] = {}
|
||||
|
||||
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
||||
super().__init__()
|
||||
self.config = core_cfg
|
||||
|
@ -144,19 +117,13 @@ class TitleServlet:
|
|||
plugins = Utils.get_all_titles()
|
||||
|
||||
for folder, mod in plugins.items():
|
||||
if (
|
||||
hasattr(mod, "game_codes")
|
||||
and hasattr(mod, "index")
|
||||
and hasattr(mod.index, "is_game_enabled")
|
||||
):
|
||||
if hasattr(mod, "game_codes") and hasattr(mod, "index") and hasattr(mod.index, "is_game_enabled"):
|
||||
should_call_setup = True
|
||||
game_servlet: BaseServlet = mod.index
|
||||
game_codes: List[str] = mod.game_codes
|
||||
|
||||
|
||||
for code in game_codes:
|
||||
if game_servlet.is_game_enabled(
|
||||
code, self.config, self.config_folder
|
||||
):
|
||||
if game_servlet.is_game_enabled(code, self.config, self.config_folder):
|
||||
handler_cls = game_servlet(self.config, self.config_folder)
|
||||
|
||||
if hasattr(handler_cls, "setup") and should_call_setup:
|
||||
|
@ -166,18 +133,16 @@ class TitleServlet:
|
|||
self.title_registry[code] = handler_cls
|
||||
|
||||
else:
|
||||
self.logger.error(
|
||||
f"{folder} missing game_code or index in __init__.py, or is_game_enabled in index"
|
||||
)
|
||||
self.logger.error(f"{folder} missing game_code or index in __init__.py, or is_game_enabled in index")
|
||||
|
||||
self.logger.info(
|
||||
f"Serving {len(self.title_registry)} game codes {'on port ' + str(core_cfg.server.port) if core_cfg.server.port > 0 else ''}"
|
||||
f"Serving {len(self.title_registry)} game codes {'on port ' + str(core_cfg.title.port) if core_cfg.title.port > 0 else ''}"
|
||||
)
|
||||
|
||||
def render_GET(self, request: Request, endpoints: dict) -> bytes:
|
||||
code = endpoints["title"]
|
||||
subaction = endpoints["subaction"]
|
||||
|
||||
subaction = endpoints['subaction']
|
||||
|
||||
if code not in self.title_registry:
|
||||
self.logger.warning(f"Unknown game code {code}")
|
||||
request.setResponseCode(404)
|
||||
|
@ -186,9 +151,7 @@ class TitleServlet:
|
|||
index = self.title_registry[code]
|
||||
handler = getattr(index, f"{subaction}", None)
|
||||
if handler is None:
|
||||
self.logger.error(
|
||||
f"{code} does not have handler for GET subaction {subaction}"
|
||||
)
|
||||
self.logger.error(f"{code} does not have handler for GET subaction {subaction}")
|
||||
request.setResponseCode(500)
|
||||
return b""
|
||||
|
||||
|
@ -196,7 +159,7 @@ class TitleServlet:
|
|||
|
||||
def render_POST(self, request: Request, endpoints: dict) -> bytes:
|
||||
code = endpoints["title"]
|
||||
subaction = endpoints["subaction"]
|
||||
subaction = endpoints['subaction']
|
||||
|
||||
if code not in self.title_registry:
|
||||
self.logger.warning(f"Unknown game code {code}")
|
||||
|
@ -206,9 +169,7 @@ class TitleServlet:
|
|||
index = self.title_registry[code]
|
||||
handler = getattr(index, f"{subaction}", None)
|
||||
if handler is None:
|
||||
self.logger.error(
|
||||
f"{code} does not have handler for POST subaction {subaction}"
|
||||
)
|
||||
self.logger.error(f"{code} does not have handler for POST subaction {subaction}")
|
||||
request.setResponseCode(500)
|
||||
return b""
|
||||
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
import importlib
|
||||
from typing import Dict, Any, Optional
|
||||
from types import ModuleType
|
||||
from twisted.web.http import Request
|
||||
import logging
|
||||
import importlib
|
||||
from os import walk
|
||||
import jwt
|
||||
from base64 import b64decode
|
||||
from datetime import datetime, timezone
|
||||
from os import walk
|
||||
from types import ModuleType
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import jwt
|
||||
from starlette.requests import Request
|
||||
|
||||
from .config import CoreConfig
|
||||
|
||||
|
||||
class Utils:
|
||||
real_title_port = None
|
||||
real_title_port_ssl = None
|
||||
|
||||
@classmethod
|
||||
def get_all_titles(cls) -> Dict[str, ModuleType]:
|
||||
ret: Dict[str, Any] = {}
|
||||
|
@ -37,57 +34,40 @@ class Utils:
|
|||
|
||||
@classmethod
|
||||
def get_ip_addr(cls, req: Request) -> str:
|
||||
return req.headers.get("x-forwarded-for", req.client.host)
|
||||
|
||||
return (
|
||||
req.getAllHeaders()[b"x-forwarded-for"].decode()
|
||||
if b"x-forwarded-for" in req.getAllHeaders()
|
||||
else req.getClientAddress().host
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_title_port(cls, cfg: CoreConfig):
|
||||
if cls.real_title_port is not None:
|
||||
return cls.real_title_port
|
||||
|
||||
cls.real_title_port = (
|
||||
cfg.server.proxy_port
|
||||
if cfg.server.is_using_proxy and cfg.server.proxy_port
|
||||
else cfg.server.port
|
||||
)
|
||||
if cls.real_title_port is not None: return cls.real_title_port
|
||||
|
||||
if cfg.title.port == 0:
|
||||
cls.real_title_port = cfg.allnet.port
|
||||
|
||||
else:
|
||||
cls.real_title_port = cfg.title.port
|
||||
|
||||
return cls.real_title_port
|
||||
|
||||
@classmethod
|
||||
def get_title_port_ssl(cls, cfg: CoreConfig):
|
||||
if cls.real_title_port_ssl is not None:
|
||||
return cls.real_title_port_ssl
|
||||
|
||||
cls.real_title_port_ssl = (
|
||||
cfg.server.proxy_port_ssl
|
||||
if cfg.server.is_using_proxy and cfg.server.proxy_port_ssl
|
||||
else Utils.get_title_port(cfg)
|
||||
)
|
||||
if cls.real_title_port_ssl is not None: return cls.real_title_port_ssl
|
||||
|
||||
if cfg.title.port_ssl == 0:
|
||||
cls.real_title_port_ssl = 443
|
||||
|
||||
else:
|
||||
cls.real_title_port_ssl = cfg.title.port_ssl
|
||||
|
||||
return cls.real_title_port_ssl
|
||||
|
||||
|
||||
def create_sega_auth_key(
|
||||
aime_id: int,
|
||||
game: str,
|
||||
place_id: int,
|
||||
keychip_id: str,
|
||||
b64_secret: str,
|
||||
exp_seconds: int = 86400,
|
||||
err_logger: str = "aimedb",
|
||||
) -> Optional[str]:
|
||||
def create_sega_auth_key(aime_id: int, game: str, place_id: int, keychip_id: str, b64_secret: str, exp_seconds: int = 86400, err_logger: str = 'aimedb') -> Optional[str]:
|
||||
logger = logging.getLogger(err_logger)
|
||||
try:
|
||||
return jwt.encode(
|
||||
{
|
||||
"aime_id": aime_id,
|
||||
"game": game,
|
||||
"place_id": place_id,
|
||||
"keychip_id": keychip_id,
|
||||
"exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds,
|
||||
},
|
||||
b64decode(b64_secret),
|
||||
algorithm="HS256",
|
||||
)
|
||||
return jwt.encode({ "aime_id": aime_id, "game": game, "place_id": place_id, "keychip_id": keychip_id, "exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds }, b64decode(b64_secret), algorithm="HS256")
|
||||
except jwt.InvalidKeyError:
|
||||
logger.error("Failed to encode Sega Auth Key because the secret is invalid!")
|
||||
return None
|
||||
|
@ -95,19 +75,10 @@ def create_sega_auth_key(
|
|||
logger.error(f"Unknown exception occoured when encoding Sega Auth Key! {e}")
|
||||
return None
|
||||
|
||||
|
||||
def decode_sega_auth_key(
|
||||
token: str, b64_secret: str, err_logger: str = "aimedb"
|
||||
) -> Optional[Dict]:
|
||||
def decode_sega_auth_key(token: str, b64_secret: str, err_logger: str = 'aimedb') -> Optional[Dict]:
|
||||
logger = logging.getLogger(err_logger)
|
||||
try:
|
||||
return jwt.decode(
|
||||
token,
|
||||
"secret",
|
||||
b64decode(b64_secret),
|
||||
algorithms=["HS256"],
|
||||
options={"verify_signature": True},
|
||||
)
|
||||
return jwt.decode(token, "secret", b64decode(b64_secret), algorithms=["HS256"], options={"verify_signature": True})
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.error("Sega Auth Key failed to validate due to an expired signature!")
|
||||
return None
|
||||
|
@ -123,3 +94,4 @@ def decode_sega_auth_key(
|
|||
except Exception as e:
|
||||
logger.error(f"Unknown exception occoured when decoding Sega Auth Key! {e}")
|
||||
return None
|
||||
|
87
dbutils.py
87
dbutils.py
|
@ -1,12 +1,9 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
from os import W_OK, access, mkdir, path
|
||||
|
||||
import yaml
|
||||
import argparse
|
||||
import logging
|
||||
from core.config import CoreConfig
|
||||
from core.data import Data
|
||||
from os import path, mkdir, access, W_OK
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Database utilities")
|
||||
|
@ -19,24 +16,19 @@ if __name__ == "__main__":
|
|||
type=str,
|
||||
help="Version of the database to upgrade/rollback to",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--game",
|
||||
"-g",
|
||||
type=str,
|
||||
help="Game code of the game who's schema will be updated/rolled back. Ex. SDFE",
|
||||
)
|
||||
parser.add_argument("--email", "-e", type=str, help="Email for the new user")
|
||||
parser.add_argument("--old_ac", "-o", type=str, help="Access code to transfer from")
|
||||
parser.add_argument("--new_ac", "-n", type=str, help="Access code to transfer to")
|
||||
parser.add_argument("--force", "-f", type=bool, help="Force the action to happen")
|
||||
parser.add_argument(
|
||||
"--access_code",
|
||||
"-a",
|
||||
type=str,
|
||||
help="Access code for new/transfer user",
|
||||
default="00000000000000000000",
|
||||
"action", type=str, help="DB Action, create, recreate, upgrade, or rollback"
|
||||
)
|
||||
parser.add_argument("--message", "-m", type=str, help="Revision message")
|
||||
<<<<<<< Updated upstream
|
||||
parser.add_argument("action", type=str, help="create, upgrade, downgrade, create-owner, migrate, create-revision, create-autorevision")
|
||||
=======
|
||||
parser.add_argument(
|
||||
"action",
|
||||
type=str,
|
||||
help="create, upgrade, create-owner, migrate, create-revision",
|
||||
)
|
||||
>>>>>>> Stashed changes
|
||||
args = parser.parse_args()
|
||||
|
||||
cfg = CoreConfig()
|
||||
|
@ -59,32 +51,41 @@ if __name__ == "__main__":
|
|||
if args.action == "create":
|
||||
data.create_database()
|
||||
|
||||
elif args.action == "upgrade":
|
||||
data.schema_upgrade(args.version)
|
||||
elif args.action == "recreate":
|
||||
data.recreate_database()
|
||||
|
||||
elif args.action == "downgrade":
|
||||
if not args.version:
|
||||
logging.getLogger("database").error(
|
||||
f"Version argument required for downgrade"
|
||||
elif args.action == "upgrade" or args.action == "rollback":
|
||||
if args.version is None:
|
||||
data.logger.warning("No version set, upgrading to latest")
|
||||
|
||||
if args.game is None:
|
||||
data.logger.warning("No game set, upgrading core schema")
|
||||
data.migrate_database(
|
||||
"CORE",
|
||||
int(args.version) if args.version is not None else None,
|
||||
args.action,
|
||||
)
|
||||
exit(1)
|
||||
data.schema_downgrade(args.version)
|
||||
|
||||
else:
|
||||
data.migrate_database(
|
||||
args.game,
|
||||
int(args.version) if args.version is not None else None,
|
||||
args.action,
|
||||
)
|
||||
|
||||
elif args.action == "autoupgrade":
|
||||
data.autoupgrade()
|
||||
|
||||
elif args.action == "create-owner":
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(data.create_owner(args.email, args.access_code))
|
||||
data.create_owner(args.email)
|
||||
|
||||
elif args.action == "migrate":
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(data.migrate())
|
||||
elif args.action == "migrate-card":
|
||||
data.migrate_card(args.old_ac, args.new_ac, args.force)
|
||||
|
||||
elif args.action == "create-revision":
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(data.create_revision(args.message))
|
||||
elif args.action == "cleanup":
|
||||
data.delete_hanging_users()
|
||||
|
||||
elif args.action == "version":
|
||||
data.show_versions()
|
||||
|
||||
elif args.action == "create-autorevision":
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(data.create_revision_auto(args.message))
|
||||
|
||||
else:
|
||||
logging.getLogger("database").info(f"Unknown action {args.action}")
|
||||
data.logger.info("Done")
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
# Installing ARTEMiS on Linux
|
||||
This guide assumes a fresh install of Debian 12 or Rasperry Pi OS. If you're using a different distrubution, your package manager commands and package names may be different then what's listed below. Please check with your repository's package manager for package names.
|
||||
|
||||
## Install prerequisits
|
||||
### Python
|
||||
Some installs may come with python already installed. You can verify this by trying the following commands:
|
||||
- `python --version`
|
||||
- `python3 --version`
|
||||
- `python3.<minor version> --version` where `<minor version>` is a python 3 release (eg 11, 10)
|
||||
|
||||
If your python version is at least 3.7, you can move to the next step
|
||||
|
||||
### Libraries and other software
|
||||
ARTEMiS depends on mysql and memcached. As stated above, package names may vary by distrubution, but this is generally what you should expect to install.
|
||||
#### Rasperry Pi OS
|
||||
`sudo apt install git mariadb-server python3-pip memcached libmemcached-dev `
|
||||
|
||||
#### Debian 12
|
||||
`sudo apt install git mariadb-server python3-pip memcached libmemcached-dev default-libmysqlclient-dev pkg-config`
|
||||
|
||||
### Optional: Install proxy
|
||||
If you intend to use a proxy (recomended for public-facing production setups), we recomend nginx
|
||||
`sudo apt install nginx`
|
||||
|
||||
## Database setup
|
||||
### mysql_secure_installation
|
||||
If you already have your database installed and configured, and are able to log in, skip down to the [Creating the database](#creating-the-database) section below. Otherwise, setup your newly installed database.
|
||||
|
||||
`sudo mysql_secure_installation`
|
||||
|
||||
Leave the root password blank, do not switch to unix socket, do reset the root password to something secure, and answer yes to the rest of the prompts. You can then log into your database with `sudo mysql`
|
||||
|
||||
### Creating the database
|
||||
Once you're logged in, run the following commands, as root, to set up our database. Make sure you note down whatever you decide to make the password for the aime account, as you will need it to configure artemis.
|
||||
|
||||
```sql
|
||||
CREATE USER 'aime'@'localhost' IDENTIFIED BY '<password>';
|
||||
CREATE DATABASE aime;
|
||||
GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost';
|
||||
quit
|
||||
```
|
||||
We have now set up our new user, `aime`, created a database called `aime` and given our user all the permissions it needs on every table of that database.
|
||||
|
||||
### Configure memcached
|
||||
Under the file /etc/memcached.conf, please make sure the following parameters are set:
|
||||
|
||||
```
|
||||
# Start with a cap of 64 megs of memory. It's reasonable, and the daemon default
|
||||
# Note that the daemon will grow to this size, but does not start out holding this much
|
||||
# memory
|
||||
|
||||
-I 128m
|
||||
-m 1024
|
||||
```
|
||||
|
||||
** This is mandatory to avoid memcached overload caused by Crossbeats or by massive profiles
|
||||
|
||||
Restart memcached using: sudo systemctl restart memcached
|
||||
|
||||
## Getting ARTEMiS
|
||||
### Clone from gitea
|
||||
use `git clone https://gitea.tendokyu.moe/Hay1tsme/artemis.git` to pull down ARTEMiS into a folder called `artemis` created at wherever your current working directory is. `cd` into `artemis`.
|
||||
|
||||
### Optional: Create a venv
|
||||
Python venvs are a way to install and manage packages on a per-project basis and are recomended on systems that will have multiple python scripts running on them to avoid dependancy issues. If this server will be running ARTEMiS and ONLY ARTEMiS, then it is possible to get away without creating one. If you do want to create one, you will have to install an additional package:
|
||||
|
||||
`sudo apt install python3-venv` (like above, package name may vary depending on distro and python version)
|
||||
|
||||
Now, simply run `python -m venv .venv` (may have to use python3 or python 3.11 instead of python) to create your virtual environment in the folder `.venv`. In order to install packages and run scripts in this environment, you have to 'activate' it by running `source .venv/bin/activate`. Your terminal should now have (venv) appended to it.
|
||||
|
||||
### Optional: Use the develop branch
|
||||
By default, pulling down ARTEMiS from gitea will pull the `master` branch. This branch is updated less frequently, but is considered stable and ready for production use. If you'd rather have more updates, but a possibility for instability or bugs, you can switch to the develop branch by running `git checkout develop`. You can run `git checkout master` to switch back to stable.
|
||||
|
||||
## Install python libraries
|
||||
Run `pip install -r requirements.txt` to install all of ARTEMiS' dependencies. If any installs fail, you may have missed a step in the [Install prerequisits](#install-prerequisits) section above. If you're absolutly sure you didn't, submit an issue on gitea.
|
||||
|
||||
## Configuration
|
||||
### Copy example configs
|
||||
From the `artemis` directory, run `cp -r example_config config` to copy the example configuration files to a new folder called `config`. All of the config changes you make will be done in the `config` folder.
|
||||
|
||||
### Optional: Generate AimeDB and Frontend JWT Secrets
|
||||
AimeDB and the frontend utalize JSON Web Tokens (JWT) for card authentication and session cookies respectivly. While generating a secret for AimeDB is optional, if you intend to run the frontend, a secret is required. You can generate a secret easily by running:
|
||||
|
||||
`openssl rand --base64 64`
|
||||
|
||||
With 64 being the number of bytes. You shouldn't need to go higher then 64, but you can if desired. **NOTE: When pasting secrets into the config file, make sure you remove any newlines!**
|
||||
|
||||
### Edit `core.yaml`
|
||||
Before editing `core.yaml`, you should familiarize yourself with the name and function of each of the config options. You can find a full list in [config.md](config.md)
|
||||
|
||||
Open `core.yaml` in the `config` folder in your prefered text editor. The only configuration option that it is absolutly mandatory to change is `aimedb`->`key`. This key must be set for the server to start, and the key must be correct, otherwise you will not be able to process aimedb requests. The correct key is floating around online, and finding it is left as an excersie to the reader.
|
||||
|
||||
Another option that should be changed is `database`->`password` to be the password you set when you created your database user. You did write it down somewhere, right?
|
||||
|
||||
Since you are presumably not running the games on the same computer you're installing this server on, you're going to want to change `server`->`hostname` to be whatever hostname or IP address other PCs can reach this server by. Note that some games reject IPs and require hostnames, so setting a hostname is always recomended over an IP.
|
||||
|
||||
### Edit game configs
|
||||
Every game has their own yaml file with settings that you may want to tweek. `InitialD Zero` and `Pokken` both have `hostname` fields in their config file that you should edit, and some games support encryption, if supplied with proper keys.
|
||||
|
||||
### A note about IDZ
|
||||
InitialD Zero is currently the only game where it is required to specify encryption information (the AES key and at least one RSA key) for the game to start. These keys are, like the aimedb key, floating around online and will not be provided. If you don't have the keys, and don't plan on anybody connecting to your server playing InitialD Zero, it's best to set `enabled` to `False` in idz.yaml to disable the game.
|
||||
|
||||
## Create database tables
|
||||
ARTEMiS uses alembic to manage datbase versioning. `dbutils.py` acts as a wrapper for alembic, and can execute some necessassary database functions. To create the database tables, run `python dbutils.py create`. Confirm that there are no errors, and you're good to go. If you intend to use the frontend, you may also want to run `python dbutils.py create-owner -a <your 20 digit access code here>` to create a superuser account to log in with.
|
||||
|
||||
## Run ARTEMiS
|
||||
Once you have everything configured properly, simply run `python index.py` to start ARTEMiS. Verify that clients can connect to all services (allnet, billing, aimedb, and game servers) and setup is complete.
|
|
@ -1,129 +0,0 @@
|
|||
# ARTEMiS - Ubuntu 20.04 LTS Guide
|
||||
This step-by-step guide assumes that you are using a fresh install of Ubuntu 20.04 LTS, some of the steps can be skipped if you already have an installation with MySQL 5.7 or even some of the modules already present on your environment
|
||||
|
||||
# Setup
|
||||
## Install memcached module
|
||||
1. sudo apt-get install memcached
|
||||
2. Under the file /etc/memcached.conf, please make sure the following parameters are set:
|
||||
|
||||
```
|
||||
# Start with a cap of 64 megs of memory. It's reasonable, and the daemon default
|
||||
# Note that the daemon will grow to this size, but does not start out holding this much
|
||||
# memory
|
||||
|
||||
-I 128m
|
||||
-m 1024
|
||||
```
|
||||
|
||||
** This is mandatory to avoid memcached overload caused by Crossbeats or by massive profiles
|
||||
|
||||
3. Restart memcached using: sudo systemctl restart memcached
|
||||
|
||||
## Install MySQL 5.7
|
||||
```
|
||||
sudo apt update
|
||||
sudo apt install wget -y
|
||||
wget https://dev.mysql.com/get/mysql-apt-config_0.8.12-1_all.deb
|
||||
sudo dpkg -i mysql-apt-config_0.8.12-1_all.deb
|
||||
```
|
||||
1. During the first prompt, select Ubuntu Bionic
|
||||
2. Select the default option
|
||||
3. Select MySQL 5.7
|
||||
4. Select the last option
|
||||
```
|
||||
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29
|
||||
sudo apt-get update
|
||||
sudo apt-cache policy mysql-server
|
||||
sudo apt install -f mysql-client=5.7* mysql-community-server=5.7* mysql-server=5.7*
|
||||
```
|
||||
|
||||
## Default Configuration for MySQL Server
|
||||
1. sudo mysql_secure_installation
|
||||
> Make sure to follow the steps that will be prompted such as changing the mysql root password and such
|
||||
|
||||
2. Test your MySQL Server login by doing the following command :
|
||||
> mysql -u root -p
|
||||
|
||||
## Create the default ARTEMiS database and user
|
||||
1. mysql -u root -p
|
||||
2. Please change the password indicated in the next line for a custom secure one and continue with the next commands
|
||||
|
||||
```
|
||||
CREATE USER 'aime'@'localhost' IDENTIFIED BY 'MyStrongPass.';
|
||||
CREATE DATABASE aime;
|
||||
GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
exit;
|
||||
```
|
||||
|
||||
3. sudo systemctl restart mysql
|
||||
|
||||
## Install Python modules
|
||||
```
|
||||
sudo apt-get install python3-dev default-libmysqlclient-dev build-essential mysql-client libmysqlclient-dev libmemcached-dev
|
||||
sudo apt install libpython3.8-dev
|
||||
sudo apt-get install python3-software-properties
|
||||
sudo apt install python3-pip
|
||||
sudo pip3 install --upgrade pip testresources
|
||||
sudo pip3 install --upgrade pip setuptools
|
||||
sudo apt-get install python3-tk
|
||||
```
|
||||
7. Change your work path to the ARTEMiS root folder using 'cd' and install the requirements:
|
||||
> sudo python3 -m pip install -r requirements.txt
|
||||
|
||||
## Copy/Rename the folder example_config to config
|
||||
|
||||
## Adjust /config/core.yaml
|
||||
1. Make sure to change the server listen_address to be set to your local machine IP (ex.: 192.168.1.xxx)
|
||||
2. Adjust the proper MySQL information you created earlier
|
||||
3. Add the AimeDB key at the bottom of the file
|
||||
|
||||
## Create the database tables for ARTEMiS
|
||||
1. sudo python3 dbutils.py create
|
||||
|
||||
2. If you get "No module named Crypto", run the following command:
|
||||
```
|
||||
sudo pip uninstall crypto
|
||||
sudo pip uninstall pycrypto
|
||||
sudo pip install pycrypto
|
||||
```
|
||||
|
||||
## Firewall Adjustements
|
||||
```
|
||||
sudo ufw allow 80
|
||||
sudo ufw allow 443
|
||||
sudo ufw allow 8443
|
||||
sudo ufw allow 22345
|
||||
sudo ufw allow 8090
|
||||
sudo ufw allow 8444
|
||||
sudo ufw allow 8080
|
||||
```
|
||||
|
||||
## Running the ARTEMiS instance
|
||||
1. sudo python3 index.py
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## Game does not connect to ARTEMiS Allnet server
|
||||
1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened
|
||||
|
||||
## Game does not connect to Title Server
|
||||
1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname
|
||||
2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox.
|
||||
3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml.
|
||||
|
||||
## Unhandled command under AimeDB
|
||||
1. Double check your AimeDB key under core.yaml, it is incorrect.
|
||||
|
||||
## Memcache failed, error 3
|
||||
1. Make sure memcached is properly installed and running. You can check the status of the service using the following command:
|
||||
> sudo systemctl status memcached
|
||||
2. If it is failing, double check the /etc/memcached.conf file, it may have duplicated arguments like the -I and -m
|
||||
3. If it is still not working afterward, you can proceed with a workaround by manually editing the /core/data/cache.py file.
|
||||
```
|
||||
# Make memcache optional
|
||||
try:
|
||||
has_mc = False
|
||||
except ModuleNotFoundError:
|
||||
has_mc = False
|
||||
```
|
|
@ -1,77 +1,102 @@
|
|||
# Installing ARTEMiS on Windows
|
||||
This guide assumes a fresh install of Windows 10. Please be aware that due to the lack of memcached and the general woes of running a server on Windows, this is only recommended for local setups or small hosting-for-the-homies type servers.
|
||||
# ARTEMiS - Windows 10/11 Guide
|
||||
This step-by-step guide assumes that you are using a fresh install of Windows 10/11 without MySQL installed, some of the steps can be skipped if you already have an installation with MySQL 8.0 or even some of the modules already present on your environment
|
||||
|
||||
## Install prerequisites
|
||||
### Python
|
||||
- Python versions from 3.8 to 3.11 work with ARTEMiS. We recommend 3.11.
|
||||
- https://www.python.org/ftp/python/3.11.7/python-3.11.7-amd64.exe
|
||||
- Install using whichever options best suit your environment, making sure that the Python executable is on path, such that you can open CMD, type `python --version` and see the version of Python you have installed.
|
||||
- If you already have a working version of Python installed, skip this step.
|
||||
# Setup
|
||||
## Install Python Python 3.9 (recommended) or 3.10
|
||||
1. Download Python 3.9 : [Link](https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe)
|
||||
2. Install python-3.9.13-amd64.exe
|
||||
1. Select Customize installation
|
||||
2. Make sure that pip, tcl/tk, and the for all users are checked and hit Next
|
||||
3. Make sure that you enable "Create shortcuts for installed applications" and "Add Python to environment variables" and hit Install
|
||||
|
||||
### MariaDB
|
||||
- It is always recommended to use MariaDB over MySQL because Oracle is a terrible company.
|
||||
- While the latest release of v10 is recommended, as it is an LTS release, v11 should work fine.
|
||||
- https://ftp.osuosl.org/pub/mariadb//mariadb-10.11.6/winx64-packages/mariadb-10.11.6-winx64.msi
|
||||
- REMEMBER YOUR ROOT PASSWORD SO YOU CAN LOG IN IN FUTURE STEPS.
|
||||
## Install MySQL 8.0
|
||||
1. Download MySQL 8.0 Server : [Link](https://dev.mysql.com/get/Downloads/MySQLInstaller/mysql-installer-community-8.0.34.0.msi)
|
||||
2. Install mysql-installer-web-community-8.0.34.0.msi
|
||||
1. Click on "Add ..." on the side
|
||||
2. Click on the "+" next to MySQL Servers
|
||||
3. Make sure MySQL Server 8.0.34 - X64 is under the products to be installed.
|
||||
4. Hit Next and Next once installed
|
||||
5. Select the configuration type "Development Computer"
|
||||
6. Hit Next
|
||||
7. Select "Use Legacy Authentication Method (Retain MySQL 5.x compatibility)" and hit Next
|
||||
8. Enter a root password and then hit Next >
|
||||
9. Leave everything under Windows Service as default and hit Next >
|
||||
10. Click on Execute and for it to finish and hit Next> and then Finish
|
||||
3. Open MySQL 8.0 Command Line Client and login as your root user
|
||||
4. Change `<Enter Password Here>` to a new password for the user aime, type those commands to create your user and the database
|
||||
|
||||
### Git
|
||||
- While technically optional, it is strongly recommended to obtain ARTEMiS via git clone instead of just downloading it.
|
||||
- https://git-scm.com/download/win
|
||||
- It is recommended to use Notepad++ as the default editor (if you have it installed), other than that, the default settings should be fine.
|
||||
|
||||
### Optional: GUI database viewer
|
||||
- Having a GUI database editor is recommended but not required.
|
||||
- MariaDB will try to install HeidiSQL, but we recommend DBeaver.
|
||||
- https://dbeaver.io/download/
|
||||
|
||||
## Obtain ARTEMiS
|
||||
### Via git (recommended)
|
||||
- `git clone https://gitea.tendokyu.moe/Hay1tsme/artemis.git` via cmd in whatever folder you want to install ARTEMiS.
|
||||
- You can switch to the develop branch for latest changes via `git checkout develop`.
|
||||
|
||||
### Via http download
|
||||
- Download [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/archive/master.zip).
|
||||
- Develop branch can be found [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/archive/develop.zip).
|
||||
- Extract the zip file somewhere.
|
||||
|
||||
## Database setup
|
||||
- Log into your server as root, either via GUI (recommended) or CMD
|
||||
- Create the `aime` user, replace `<password>` with a password you choose. Remember it!
|
||||
```
|
||||
CREATE USER 'aime'@'localhost' IDENTIFIED BY '<password>';
|
||||
```sql
|
||||
CREATE USER 'aime'@'localhost' IDENTIFIED BY '<Enter Password Here>';
|
||||
CREATE DATABASE aime;
|
||||
GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
exit;
|
||||
```
|
||||
- If you create the database via a GUI, make sure you grant all the above permissions.
|
||||
|
||||
## Create a venv
|
||||
- Python virtual environments are a good way to manage packages and make dealing with python and pip easier.
|
||||
- `python -m pip venv venv`
|
||||
- `venv\Scripts\activate.bat` to activate the venv whenever you need to interact with ARTEMiS.
|
||||
- All the rest of the steps assume your venv is activated.
|
||||
## Install Python modules
|
||||
1. Change your work path to the artemis-master folder using 'cd' and install the requirements:
|
||||
|
||||
## Install pip modules
|
||||
- `pip install -r requirements.txt`
|
||||
```shell
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Setup configuration
|
||||
- Create a new `config` folder and copy the files in `example_config` over.
|
||||
- edit `core.yaml`
|
||||
- Put the password you created for the aime user into the `database` section.
|
||||
- Put in the aimedb key (YOU DO NOT GENERATE THIS KEY, FIND IT SOMEWHERE).
|
||||
- Set your hostname to be whatever hostname or IP address games can reach your server at (many games reject localhost and 127.0.0.1).
|
||||
- Optional: generate base64-encoded secrets for aimedb and frontend.
|
||||
- See [config.md](docs/config.md) for a full list of options.
|
||||
- edit `idz.yaml`
|
||||
- If you don't plan on anyone using your server to play Initial D Zero, it is best to disable it to cut down on console spam on boot.
|
||||
- Edit other game yamls
|
||||
- Add keys, set hostnames, ports, etc. Specific settings will depend on the game. See [game_specific_info](docs/game_specific_info.md).
|
||||
## Copy/Rename the folder `example_config` to `config`
|
||||
|
||||
## Create Database Tables
|
||||
- `python dbutils.py create`
|
||||
## Adjust `config/core.yaml`
|
||||
|
||||
## Firewall
|
||||
- If you're planning on serving games not on your PC, open at least ports 80, 8443, and 22345 in windows firewall
|
||||
- Also set `listen_address` to either your local IP to serve on your LAN, or `0.0.0.0` for all interfaces, to accept connections from other places.
|
||||
1. Make sure to change the server `hostname` to be set to your local machine IP (ex.: 192.168.xxx.xxx)
|
||||
- In case you want to run this only locally, set the following values:
|
||||
|
||||
## Start ARTEMiS
|
||||
- `python index.py`
|
||||
```yaml
|
||||
server:
|
||||
listen_address: 0.0.0.0
|
||||
title:
|
||||
hostname: 192.168.xxx.xxx
|
||||
```
|
||||
|
||||
1. Adjust the proper MySQL information you created earlier
|
||||
```yaml
|
||||
database:
|
||||
host: "localhost"
|
||||
username: "aime"
|
||||
password: "<Enter Password Here>"
|
||||
name: "aime"
|
||||
```
|
||||
3. Add the AimeDB key at the bottom of the file
|
||||
4. If the webui is needed, change the flag from False to True
|
||||
|
||||
## Create the database tables for ARTEMiS
|
||||
|
||||
```shell
|
||||
python dbutils.py create
|
||||
```
|
||||
|
||||
## Firewall Adjustements
|
||||
Make sure the following ports are open both on your router and local Windows firewall in case you want to use this for public use (NOT recommended):
|
||||
> Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8080 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha
|
||||
|
||||
## Running the ARTEMiS instance
|
||||
```shell
|
||||
python index.py
|
||||
```
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## Game does not connect to ARTEMiS Allnet server
|
||||
1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened
|
||||
|
||||
## Game does not connect to Title Server
|
||||
1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname
|
||||
2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox.
|
||||
3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml.
|
||||
|
||||
## Unhandled command under AimeDB
|
||||
1. Double check your AimeDB key under core.yaml, it is incorrect.
|
||||
|
||||
## AttributeError: module 'collections' has no attribute 'Hashable'
|
||||
1. This means the pyYAML module is obsolete, simply rerun pip with the -U (force update) flag, as shown below.
|
||||
- Change your work path to the artemis-master (or artemis-develop) folder using 'cd' and run the following commands:
|
||||
|
||||
```shell
|
||||
pip install -r requirements.txt -U
|
||||
```
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
# ARTEMiS - Windows 10/11 Guide
|
||||
This step-by-step guide assumes that you are using a fresh install of Windows 10/11 without MySQL installed, some of the steps can be skipped if you already have an installation with MySQL 8.0 or even some of the modules already present on your environment
|
||||
|
||||
# Setup
|
||||
## Install Python Python 3.9 (recommended) or 3.10
|
||||
1. Download Python 3.9 : [Link](https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe)
|
||||
2. Install python-3.9.13-amd64.exe
|
||||
1. Select Customize installation
|
||||
2. Make sure that pip, tcl/tk, and the for all users are checked and hit Next
|
||||
3. Make sure that you enable "Create shortcuts for installed applications" and "Add Python to environment variables" and hit Install
|
||||
|
||||
## Install MySQL 8.0
|
||||
1. Download MySQL 8.0 Server : [Link](https://dev.mysql.com/get/Downloads/MySQLInstaller/mysql-installer-community-8.0.34.0.msi)
|
||||
2. Install mysql-installer-web-community-8.0.34.0.msi
|
||||
1. Click on "Add ..." on the side
|
||||
2. Click on the "+" next to MySQL Servers
|
||||
3. Make sure MySQL Server 8.0.34 - X64 is under the products to be installed.
|
||||
4. Hit Next and Next once installed
|
||||
5. Select the configuration type "Development Computer"
|
||||
6. Hit Next
|
||||
7. Select "Use Legacy Authentication Method (Retain MySQL 5.x compatibility)" and hit Next
|
||||
8. Enter a root password and then hit Next >
|
||||
9. Leave everything under Windows Service as default and hit Next >
|
||||
10. Click on Execute and for it to finish and hit Next> and then Finish
|
||||
3. Open MySQL 8.0 Command Line Client and login as your root user
|
||||
4. Change `<Enter Password Here>` to a new password for the user aime, type those commands to create your user and the database
|
||||
|
||||
```sql
|
||||
CREATE USER 'aime'@'localhost' IDENTIFIED BY '<Enter Password Here>';
|
||||
CREATE DATABASE aime;
|
||||
GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
exit;
|
||||
```
|
||||
|
||||
## Install Python modules
|
||||
1. Change your work path to the artemis-master folder using 'cd' and install the requirements:
|
||||
|
||||
```shell
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Copy/Rename the folder `example_config` to `config`
|
||||
|
||||
## Adjust `config/core.yaml`
|
||||
|
||||
1. Make sure to change the server `hostname` to be set to your local machine IP (ex.: 192.168.xxx.xxx)
|
||||
- In case you want to run this only locally, set the following values:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
listen_address: 0.0.0.0
|
||||
title:
|
||||
hostname: 192.168.xxx.xxx
|
||||
```
|
||||
|
||||
1. Adjust the proper MySQL information you created earlier
|
||||
```yaml
|
||||
database:
|
||||
host: "localhost"
|
||||
username: "aime"
|
||||
password: "<Enter Password Here>"
|
||||
name: "aime"
|
||||
```
|
||||
3. Add the AimeDB key at the bottom of the file
|
||||
4. If the webui is needed, change the flag from False to True
|
||||
|
||||
## Create the database tables for ARTEMiS
|
||||
|
||||
```shell
|
||||
python dbutils.py create
|
||||
```
|
||||
|
||||
## Firewall Adjustements
|
||||
Make sure the following ports are open both on your router and local Windows firewall in case you want to use this for public use (NOT recommended):
|
||||
> Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8080 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha
|
||||
|
||||
## Running the ARTEMiS instance
|
||||
```shell
|
||||
python index.py
|
||||
```
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## Game does not connect to ARTEMiS Allnet server
|
||||
1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened
|
||||
|
||||
## Game does not connect to Title Server
|
||||
1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname
|
||||
2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox.
|
||||
3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml.
|
||||
|
||||
## Unhandled command under AimeDB
|
||||
1. Double check your AimeDB key under core.yaml, it is incorrect.
|
||||
|
||||
## AttributeError: module 'collections' has no attribute 'Hashable'
|
||||
1. This means the pyYAML module is obsolete, simply rerun pip with the -U (force update) flag, as shown below.
|
||||
- Change your work path to the artemis-master (or artemis-develop) folder using 'cd' and run the following commands:
|
||||
|
||||
```shell
|
||||
pip install -r requirements.txt -U
|
||||
```
|
|
@ -1,24 +1,23 @@
|
|||
# ARTEMiS Configuration
|
||||
## Server
|
||||
- `listen_address`: IP Address or hostname that the server will listen for connections on. Set to 127.0.0.1 for local only, or 0.0.0.0 for all interfaces. Default `127.0.0.1`
|
||||
- `hostname`: Hostname that gets sent to clients to tell them where to connect. Games must be able to connect to your server via the hostname or IP you spcify here. Note that most games will reject `localhost` or `127.0.0.1`. Default `localhost`
|
||||
- `port`: Port that the server will listen for connections on. Default `80`
|
||||
- `ssl_key`: Location of the ssl server key for the secure title server. Ignored if you don't use SSL. Default `cert/title.key`
|
||||
- `ssl_cert`: Location of the ssl server certificate for the secure title server. Must not be a self-signed SSL. Ignored if you don't use SSL. Default `cert/title.pem`
|
||||
- `allow_user_registration`: Allows users to register in-game via the AimeDB `register` function. Disable to be able to control who can use cards on your server. 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`
|
||||
- `is_develop`: Flags that the server is a development instance, and enables some useful development features. Disable for production setups. Default `True`.
|
||||
- `is_using_proxy`: Flags that you'll be using some other software, such as nginx, to proxy requests, and to send `proxy_port` or `proxy_port_ssl` to games instead of `port`. Default `False`
|
||||
- `proxy_port`: Which port your front-facing proxy will be listening on. Ignored if `is_using_proxy` is `False` or if set to `0`. Default `0`
|
||||
- `proxy_port`: Which port your front-facing proxy will be listening for ssl connections on. Ignored if `is_using_proxy` is `False` or if set to `0`. Default `0`
|
||||
- `log_dir`: Directory to store logs. Server MUST have read and write permissions to this directory or you will have issues. Default `logs`
|
||||
- `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`
|
||||
- `check_arcade_ip`: Checks IPs against the `arcade` table in the database, if one is defined. Default `False`
|
||||
- `strict_ip_checking`: Rejects clients if there is no IP in the `arcade` table for the respective arcade. Default `False`
|
||||
- `strict_ip_checking`: Rejects clients if there is no IP in the `arcade` table for the respective arcade
|
||||
- `log_dir`: Directory to store logs. Server MUST have read and write permissions to this directory or you will have issues. Default `logs`
|
||||
## Title
|
||||
- `loglevel`: Logging level for the title server. Default `info`
|
||||
- `reboot_start_time`: 24 hour JST time that clients will see as the start of maintenance period, ex `04:00`. Leave blank for no maintenance time. Default: `""`
|
||||
- `reboot_end_time`: 24 hour JST time that clients will see as the end of maintenance period, ex `05:00`. Leave blank for no maintenance time. Default: `""`
|
||||
- `hostname`: Hostname that gets sent to clients to tell them where to connect. Games must be able to connect to your server via the hostname or IP you spcify here. Note that most games will reject `localhost` or `127.0.0.1`. Default `localhost`
|
||||
- `port`: Port that the title server will listen for connections on. Set to 0 to use the Allnet handler to reduce the port footprint. Default `8080`
|
||||
- `port_ssl`: Port that the secure title server will listen for connections on. Set to 0 to use the Allnet handler to reduce the port footprint. Default `0`
|
||||
- `ssl_key`: Location of the ssl server key for the secure title server. Ignored if `port_ssl` is set to `0` or `is_develop` set to `False`. Default `cert/title.key`
|
||||
- `ssl_cert`: Location of the ssl server certificate for the secure title server. Must not be a self-signed SSL. Ignored if `port_ssl` is set to `0` or `is_develop` is set to `False`. Default `cert/title.pem`
|
||||
- `reboot_start_time`: 24 hour JST time that clients will see as the start of maintenance period. Leave blank for no maintenance time. Default: ""
|
||||
- `reboot_end_time`: 24 hour JST time that clients will see as the end of maintenance period. Leave blank for no maintenance time. Default: ""
|
||||
## Database
|
||||
- `host`: Host of the database. Default `localhost`
|
||||
- `username`: Username of the account the server should connect to the database with. Default `aime`
|
||||
|
@ -26,32 +25,24 @@
|
|||
- `name`: Name of the database the server should expect. Default `aime`
|
||||
- `port`: Port the database server is listening on. Default `3306`
|
||||
- `protocol`: Protocol used in the connection string, e.i `mysql` would result in `mysql://...`. Default `mysql`
|
||||
- `sha2_password`: Whether or not the password in the connection string should be hashed via SHA2. Default `False`
|
||||
- `loglevel`: Logging level for the database. Default `info`
|
||||
- `sha2_password`: Weather or not the password in the connection string should be hashed via SHA2. Default `False`
|
||||
- `loglevel`: Logging level for the database. Default `warn`
|
||||
- `user_table_autoincrement_start`: What the `aime_user` table ID autoincrememnt should start with. Default `10000`
|
||||
- `memcached_host`: Host of the memcached server. Default `localhost`
|
||||
## Frontend
|
||||
- `enable`: Whether or not the frontend servlet should run. Frontend can still be run via `python -m uvicorn core.frontend:app` even if this is set to `False`. Default `False`
|
||||
- `port`: Port the frontend should listen on. Default `8080`
|
||||
- `enable`: Weather or not the frontend should be enabled. Default `False`
|
||||
- `port`: Port the frontend should listen for connections on. Default `8090`
|
||||
- `loglevel`: Logging level for the frontend server. Default `info`
|
||||
- `secret`: Base64-encoded JWT secret for session cookies, generated by you. Default `""`
|
||||
## Allnet
|
||||
- `standalone`: Whether allnet should launch it's own servlet on it's own port, or be part of the main servlet on the default port. Disable if you either have something proxying `naominet.jp` requests to port 80, or have port 80 set in `server` -> `port`
|
||||
- `port`: Port the allnet server should listen for connections on if it's running standalone. Games are hardcoded to ask for port `80` so only change if you have a proxy redirecting properly. Ignored if `standalone` is `False`. Default `80`
|
||||
- `loglevel`: Logging level for the allnet server. Default `info`
|
||||
- `port`: Port the allnet server should listen for connections on. Games are hardcoded to ask for port `80` so only change if you have a proxy redirecting properly. Default `80`
|
||||
- `allow_online_updates`: Allow allnet to distribute online updates via DownloadOrders. This system is currently non-functional, so leave it disabled. Default `False`
|
||||
- `update_cfg_folder`: Folder where delivery INI files will be checked for. Ignored if `allow_online_updates` is `False`. Default `""`
|
||||
## Billing
|
||||
- `standalone`: Whether the billing server should launch it's own servlet on it's own port, or be part of the main servlet on the default port. Setting this to `True` requires that you have `ssl_key` and `ssl_cert` set. Default `False`
|
||||
- `loglevel`: Logging level for the billing server. Default `info`
|
||||
- `port`: Port the billing server should listen for connections on. Games are hardcoded to ask for port `8443` so only change if you have a proxy redirecting properly. Ignored if `standalone` is `False`. Default `8443`
|
||||
- `ssl_key`: Location of the ssl server key for the billing server. Ignored if `standalone` is `False`. Default `cert/server.key`
|
||||
- `ssl_cert`: Location of the ssl server certificate for the billing server. Ignored if `standalone` is `False`. Must match the CA distributed to users or the billing server will not connect. Default `cert/server.pem`
|
||||
- `port`: Port the billing server should listen for connections on. Games are hardcoded to ask for port `8443` so only change if you have a proxy redirecting properly. Set to 0 to use the allnet handler to reduce the number of ports the server eats up. Default `8443`
|
||||
- `ssl_key`: Location of the ssl server key for the billing server. Ignored if `port` is set to `0` or `is_develop` set to `False`. Default `cert/server.key`
|
||||
- `ssl_cert`: Location of the ssl server certificate for the billing server. Must match the CA distributed to users or the billing server will not connect. Ignored if `port` is set to `0` or `is_develop` is set to `False`. Default `cert/server.pem`
|
||||
- `signing_key`: Location of the RSA Private key used to sign billing requests. Must match the public key distributed to users or the billing server will not connect. Default `cert/billing.key`
|
||||
## Aimedb
|
||||
- `enable`: Whether or not aimedb should run. Default `True`
|
||||
- `listen_address`: IP Address or hostname that the aimedb server will listen for connections on. Leave this blank to use the listen address under `server`. Default `""`
|
||||
- `loglevel`: Logging level for the aimedb server. Default `info`
|
||||
- `port`: Port the aimedb server should listen for connections on. Games are hardcoded to ask for port `22345` so only change if you have a proxy redirecting properly. Default `22345`
|
||||
- `key`: Key to encrypt/decrypt aimedb requests and responses. MUST be set or the server will not start. If set incorrectly, your server will not properly handle aimedb requests. Default `""`
|
||||
- `id_secret`: Base64-encoded JWT secret for Sega Auth IDs. Leaving this blank disables this feature. Default `""`
|
||||
- `id_lifetime_seconds`: Number of secons a JWT generated should be valid for. Default `86400` (1 day)
|
||||
- `key`: Key to encrypt/decrypt aimedb requests and responses. MUST be set or the server will not start. If set incorrectly, your server will not properly handle aimedb requests. Default `""`
|
|
@ -165,6 +165,15 @@ The importer for crossbeats REV. will import Music.
|
|||
|
||||
Config file is located in `config/cxb.yaml`.
|
||||
|
||||
| Option | Info |
|
||||
| --------------------- | ---------------------------------------------------------- |
|
||||
| `hostname` | Requires a proper `hostname` (not localhost!) to run |
|
||||
| `ssl_enable` | Enables/Disables the use of the `ssl_cert` and `ssl_key` |
|
||||
| `port` | Set your unsecure port number |
|
||||
| `port_secure` | Set your secure/SSL port number |
|
||||
| `ssl_cert`, `ssl_key` | Enter your SSL certificate (requires not self signed cert) |
|
||||
|
||||
|
||||
## maimai DX
|
||||
|
||||
### Versions
|
||||
|
@ -192,6 +201,7 @@ Config file is located in `config/cxb.yaml`.
|
|||
| SDEZ | 18 | maimai DX UNiVERSE PLUS |
|
||||
| SDEZ | 19 | maimai DX FESTiVAL |
|
||||
| SDEZ | 20 | maimai DX FESTiVAL PLUS |
|
||||
| SDEZ | 21 | maimai DX BUDDiES |
|
||||
|
||||
### Importer
|
||||
|
||||
|
@ -406,6 +416,7 @@ After that, on next login the present should be received (or whenever it suppose
|
|||
* UNiVERSE PLUS: Yes
|
||||
* FESTiVAL: Yes (added in A031)
|
||||
* FESTiVAL PLUS: Yes (added in A035)
|
||||
* BUDDiES: Yes
|
||||
* O.N.G.E.K.I. bright MEMORY: Yes
|
||||
|
||||
|
||||
|
@ -594,7 +605,7 @@ Below is a list of VIP rewards. Currently, VIP is not implemented, and thus thes
|
|||
In order to use the importer locate your game installation folder and execute:
|
||||
|
||||
```shell
|
||||
python read.py --game SDEW --version 0 --binfolder /titles/sao/data/
|
||||
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.
|
||||
|
@ -621,17 +632,15 @@ python dbutils.py --game SDEW upgrade
|
|||
### Notes
|
||||
- Defrag Match will crash at loading
|
||||
- Co-Op Online is not supported
|
||||
- Shop is displayed but cannot purchase heroes or items
|
||||
- Shop is not functionnal
|
||||
- Player title is currently static and cannot be changed in-game
|
||||
- QR Card Scanning currently only load a static hero
|
||||
- Ex-quests progression not supported yet
|
||||
- Daily Missions not implemented
|
||||
- EX TOWER 1,2 & 3 are not yet supported
|
||||
- Daily Yui coin not yet fixed
|
||||
|
||||
**Network hashing in GssSite.dll must be disabled**
|
||||
|
||||
### Credits for SAO support:
|
||||
|
||||
- Midorica - Network Support
|
||||
- Midorica - Limited Network Support
|
||||
- Dniel97 - Helping with network base
|
||||
- tungnotpunk - Source
|
||||
|
||||
|
|
67
docs/prod.md
67
docs/prod.md
|
@ -1,34 +1,41 @@
|
|||
# ARTEMiS Production mode
|
||||
ARTEMiS is designed to run in one of two ways. Developmen/local mode, which assumes you're just trying to set up something to save your scores and make the games work, and have patched your games to disable SSL and cert checks and encryption and the like, and production mode. In production mode, artemis assumes you have a proxy server, such as nginx or apache, standing in front of artemis doing HTTPS and port management. This document will cover how to properly set up a production instance of ARTEMiS.
|
||||
|
||||
## ARTEMiS configuration
|
||||
Step 1 is to edit your artemis configuration. Some recomended changes:
|
||||
### `server`
|
||||
- `listen_address` -> `127.0.0.1`
|
||||
- `is_develop` -> `False`
|
||||
- `is_using_proxy` -> `True`
|
||||
- `port` -> The port nginx will send proxied requests to. If you're using the example config, set this to 8080.
|
||||
- `proxy_port` -> The port your proxy will be accepting title server connections on. If you're using the example config, set this to 80.
|
||||
- `proxy_port_ssl` -> The port your proxy will be accepting secure title server connections on. If you're using the example config, set this to 443.
|
||||
- `allow_unregistered_serials` -> `False`
|
||||
### `billing`
|
||||
- `standalone` -> `False`
|
||||
### `allnet`
|
||||
- `standalone` -> `False`
|
||||
### `frontend`
|
||||
- `enable` -> `True` if you want the frontend
|
||||
- `port` -> `8090` if you're using the default nginx config, otherwise whatever port your proxy will be sending requests to
|
||||
### `aimedb`
|
||||
- `listen_address` -> `0.0.0.0` unless you're proxying aimedb requests (not recomended at this time), in which case, leave this option unchanged
|
||||
|
||||
If you plan to serve artemis behind a VPN, these additional settings are also recomended
|
||||
- `check_arcade_ip` -> `True`
|
||||
- `strict_ip_checking` -> `True`
|
||||
Production mode is a configuration option that changes how the server listens to be more friendly to a production environment. This mode assumes that a proxy (for this guide, nginx) is standing in front of the server to handle port mapping and TLS. In order to activate production mode, simply change `is_develop` to `False` in `core.yaml`. Next time you start the server, you should see "Starting server in production mode".
|
||||
|
||||
## Nginx Configuration
|
||||
For most cases, the config in `example_config` will suffice. It makes the following assumptions
|
||||
- ARTEMiS is running on port 8080
|
||||
- Billing is set to not be standalone
|
||||
- You're not using cloudflare in front of your frontend
|
||||
### Port forwarding
|
||||
Artemis requires that the following ports be forwarded to allow internet traffic to access the server. This will not change regardless of what you set in the config, as many of these ports are hard-coded in the games.
|
||||
`tcp:80` all.net, non-ssl titles
|
||||
`tcp:8443` billing
|
||||
`tcp:22345` aimedb
|
||||
`tcp:443` frontend, SSL titles
|
||||
|
||||
If this describes you, your only configuration needs are to edit the `server_name` and `certificate_*` directives. Otherwise, please see nginx configuration documentation to configure it to best suit your setup.
|
||||
### A note about external proxy services (cloudflare, etc)
|
||||
Due to the way that artemis functions, it is currently not possible to put the server behind something like Cloudflare. Cloudflare only proxies web traffic on the standard ports (80, 443) and, as shown above, this does not work with artemis. Server administrators should seek other means to protect their network (VPS hosting, VPN, etc)
|
||||
|
||||
### SSL Certificates
|
||||
You will need to generate SSL certificates for some games. The certificates vary in security and validity requirements. Please see the general guide below
|
||||
- General Title: The certificate for the general title server should be valid, not self-signed and match the CN that the game will be reaching out to (e.i if your games are reaching out to titles.hostname.here, your ssl certificate should be valid for titles.hostname.here, or *.hostname.here)
|
||||
- CXB: Same requires as the title server. It must not be self-signed, and CN must match. Recomended to get a wildcard cert if possible, and use it for both Title and CXB
|
||||
- Pokken: Pokken can be self-signed, and the CN doesn't have to match, but it MUST use 2048-bit RSA. Due to the games age, andthing stronger then that will be rejected.
|
||||
|
||||
### Port mappings
|
||||
An example config is provided in the `config` folder called `nginx_example.conf`. It is set up for the following:
|
||||
`naominet.jp:tcp:80` -> `localhost:tcp:8000` for allnet
|
||||
`ib.naominet.jp:ssl:8443` -> `localhost:tcp:8444` for the billing server
|
||||
`your.hostname.here:ssl:443` -> `localhost:tcp:8080` for the SSL title server
|
||||
`your.hostname.here:tcp:80` -> `localhost:tcp:8080` for the non-SSL title server
|
||||
`cxb.hostname.here:ssl:443` -> `localhost:tcp:8080` for crossbeats (appends /SDCA/104/ to the request)
|
||||
`pokken.hostname.here:ssl:443` -> `localhost:tcp:8080` for pokken
|
||||
`frontend.hostname.here:ssl:443` -> `localhost:tcp:8090` for the frontend, includes https redirection
|
||||
|
||||
If you're using this as a guide, be sure to replace your.hostname.here with the hostname you specified in core.yaml under `titles->hostname`. Do *not* change naominet.jp, or allnet/billing will fail. Also remember to specifiy certificate paths correctly, as in the example they are simply placeholders.
|
||||
|
||||
### Multi-service ports
|
||||
It is possible to use nginx to redirect billing and title server requests to the same port that all.net uses. By setting `port` to 0 under billing and title server, you can change the nginx config to serve the following (entries not shown here should be the same)
|
||||
`ib.naominet.jp:ssl:8443` -> `localhost:tcp:8000` for the billing server
|
||||
`your.hostname.here:ssl:443` -> `localhost:tcp:8000` for the SSL title server
|
||||
`your.hostname.here:tcp:80` -> `localhost:tcp:8000` for the non-SSL title server
|
||||
`cxb.hostname.here:ssl:443` -> `localhost:tcp:8000` for crossbeats (appends /SDCA/104/ to the request)
|
||||
`pokken.hostname.here:ssl:443` -> `localhost:tcp:8000` for pokken
|
||||
|
||||
This will allow you to only use 3 ports locally, but you will still need to forward the same internet-facing ports as before.
|
|
@ -22,14 +22,6 @@ version:
|
|||
14:
|
||||
rom: 2.15.00
|
||||
data: 2.15.00
|
||||
15:
|
||||
rom: 2.20.00
|
||||
data: 2.20.00
|
||||
|
||||
crypto:
|
||||
encrypted_only: False
|
||||
|
||||
matching:
|
||||
enable: False
|
||||
match_time_limit: 60
|
||||
match_error_limit: 9999
|
||||
encrypted_only: False
|
|
@ -1,25 +1,26 @@
|
|||
server:
|
||||
listen_address: "127.0.0.1"
|
||||
hostname: "localhost"
|
||||
port: 80
|
||||
ssl_key: "cert/title.key"
|
||||
ssl_cert: "cert/title.crt"
|
||||
listen_address: "127.0.0.1"
|
||||
allow_user_registration: True
|
||||
allow_unregistered_serials: True
|
||||
name: "ARTEMiS"
|
||||
is_develop: True
|
||||
is_using_proxy: False
|
||||
proxy_port: 0
|
||||
proxy_port_ssl: 0
|
||||
threading: False
|
||||
log_dir: "logs"
|
||||
check_arcade_ip: False
|
||||
strict_ip_checking: False
|
||||
|
||||
title:
|
||||
loglevel: "info"
|
||||
hostname: "localhost"
|
||||
port: 8080
|
||||
port_ssl: 0
|
||||
ssl_cert: "cert/title.crt"
|
||||
ssl_key: "cert/title.key"
|
||||
reboot_start_time: "04:00"
|
||||
reboot_end_time: "05:00"
|
||||
|
||||
|
||||
database:
|
||||
host: "localhost"
|
||||
username: "aime"
|
||||
|
@ -28,34 +29,30 @@ database:
|
|||
port: 3306
|
||||
protocol: "mysql"
|
||||
sha2_password: False
|
||||
loglevel: "info"
|
||||
loglevel: "warn"
|
||||
user_table_autoincrement_start: 10000
|
||||
enable_memcached: True
|
||||
memcached_host: "localhost"
|
||||
|
||||
frontend:
|
||||
enable: False
|
||||
port: 8080
|
||||
port: 8090
|
||||
loglevel: "info"
|
||||
secret: ""
|
||||
|
||||
allnet:
|
||||
standalone: False
|
||||
port: 80
|
||||
loglevel: "info"
|
||||
port: 80
|
||||
ip_check: False
|
||||
allow_online_updates: False
|
||||
update_cfg_folder: ""
|
||||
|
||||
billing:
|
||||
standalone: True
|
||||
loglevel: "info"
|
||||
port: 8443
|
||||
ssl_key: "cert/server.key"
|
||||
ssl_cert: "cert/server.pem"
|
||||
signing_key: "cert/billing.key"
|
||||
|
||||
aimedb:
|
||||
enable: True
|
||||
listen_address: ""
|
||||
loglevel: "info"
|
||||
port: 22345
|
||||
key: ""
|
||||
|
@ -63,4 +60,6 @@ aimedb:
|
|||
id_lifetime_seconds: 86400
|
||||
|
||||
mucha:
|
||||
enable: False
|
||||
hostname: "localhost"
|
||||
loglevel: "info"
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
server:
|
||||
enable: True
|
||||
loglevel: "info"
|
||||
use:https: True
|
||||
loglevel: "info"
|
|
@ -6,7 +6,7 @@ server {
|
|||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass_request_headers on;
|
||||
proxy_pass http://127.0.0.1:8080/;
|
||||
proxy_pass http://localhost:8000/;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ server {
|
|||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass_request_headers on;
|
||||
proxy_pass http://127.0.0.1:8080/;
|
||||
proxy_pass http://localhost:8080/;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,13 +38,11 @@ server {
|
|||
ssl_prefer_server_ciphers off;
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass_request_headers on;
|
||||
proxy_pass http://127.0.0.1:8080/;
|
||||
proxy_pass http://localhost:8080/;
|
||||
}
|
||||
}
|
||||
|
||||
# Billing, comment this out if running billing standalone
|
||||
# Billing
|
||||
server {
|
||||
listen 8443 ssl;
|
||||
server_name ib.naominet.jp;
|
||||
|
@ -59,10 +57,30 @@ server {
|
|||
ssl_ciphers "ALL:@SECLEVEL=0";
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8444/;
|
||||
}
|
||||
}
|
||||
|
||||
# Pokken, comment this out if you don't plan on serving pokken.
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name pokken.hostname.here;
|
||||
|
||||
ssl_certificate /path/to/cert/pokken.pem;
|
||||
ssl_certificate_key /path/to/cert/pokken.key;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:MozSSL:10m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers "ALL:@SECLEVEL=0";
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass_request_headers on;
|
||||
proxy_pass http://127.0.0.1:8080/;
|
||||
proxy_pass http://localhost:8080/;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,12 +91,12 @@ server {
|
|||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
# If you don't want https redirection, or are using something like cloudflare to manage HTTPS, comment out the line above and uncomment the line below
|
||||
# proxy_pass http://127.0.0.1:8090/;
|
||||
# If you don't want https redirection, comment the line above and uncomment the line below
|
||||
# proxy_pass http://localhost:8090/;
|
||||
}
|
||||
}
|
||||
|
||||
# Frontend HTTPS. Comment out if you on't intend to use the frontend, or have cloudflare or something managing https for you.
|
||||
# Frontend HTTPS. Comment out if you on't intend to use the frontend
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name frontend.hostname.here;
|
||||
|
@ -100,6 +118,6 @@ server {
|
|||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass_request_headers on;
|
||||
proxy_pass http://127.0.0.1:8090/;
|
||||
proxy_pass http://localhost:8090/;
|
||||
}
|
||||
}
|
||||
|
|
383
index.py
383
index.py
|
@ -1,118 +1,335 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
from os import environ, path
|
||||
|
||||
import uvicorn
|
||||
import logging, coloredlogs
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from typing import Dict
|
||||
import yaml
|
||||
from core import AimedbServlette, CoreConfig
|
||||
from os import path, mkdir, access, W_OK
|
||||
from core import *
|
||||
|
||||
from twisted.web import server, resource
|
||||
from twisted.internet import reactor, endpoints
|
||||
from twisted.web.http import Request
|
||||
from routes import Mapper
|
||||
from threading import Thread
|
||||
|
||||
async def launch_main(cfg: CoreConfig, ssl: bool) -> None:
|
||||
if ssl:
|
||||
server_cfg = uvicorn.Config(
|
||||
"core.app:app",
|
||||
host=cfg.server.listen_address,
|
||||
port=cfg.server.port if args.port == 0 else args.port,
|
||||
reload=cfg.server.is_develop,
|
||||
log_level="info" if cfg.server.is_develop else "critical",
|
||||
ssl_version=3,
|
||||
ssl_certfile=cfg.server.ssl_cert,
|
||||
ssl_keyfile=cfg.server.ssl_key,
|
||||
class HttpDispatcher(resource.Resource):
|
||||
def __init__(self, cfg: CoreConfig, config_dir: str):
|
||||
super().__init__()
|
||||
self.config = cfg
|
||||
self.isLeaf = True
|
||||
self.map_get = Mapper()
|
||||
self.map_post = Mapper()
|
||||
self.logger = logging.getLogger("core")
|
||||
|
||||
self.title = TitleServlet(cfg, config_dir)
|
||||
self.allnet = AllnetServlet(cfg, config_dir)
|
||||
self.mucha = MuchaServlet(cfg, config_dir)
|
||||
|
||||
self.map_get.connect(
|
||||
"allnet_downloadorder_ini",
|
||||
"/dl/ini/{file}",
|
||||
controller="allnet",
|
||||
action="handle_dlorder_ini",
|
||||
conditions=dict(method=["GET"]),
|
||||
)
|
||||
else:
|
||||
server_cfg = uvicorn.Config(
|
||||
"core.app:app",
|
||||
host=cfg.server.listen_address,
|
||||
port=cfg.server.port if args.port == 0 else args.port,
|
||||
reload=cfg.server.is_develop,
|
||||
log_level="info" if cfg.server.is_develop else "critical",
|
||||
|
||||
self.map_post.connect(
|
||||
"allnet_downloadorder_report",
|
||||
"/report-api/Report",
|
||||
controller="allnet",
|
||||
action="handle_dlorder_report",
|
||||
conditions=dict(method=["POST"]),
|
||||
)
|
||||
server = uvicorn.Server(server_cfg)
|
||||
await server.serve()
|
||||
|
||||
self.map_get.connect(
|
||||
"allnet_ping",
|
||||
"/naomitest.html",
|
||||
controller="allnet",
|
||||
action="handle_naomitest",
|
||||
conditions=dict(method=["GET"]),
|
||||
)
|
||||
self.map_post.connect(
|
||||
"allnet_poweron",
|
||||
"/sys/servlet/PowerOn",
|
||||
controller="allnet",
|
||||
action="handle_poweron",
|
||||
conditions=dict(method=["POST"]),
|
||||
)
|
||||
self.map_post.connect(
|
||||
"allnet_downloadorder",
|
||||
"/sys/servlet/DownloadOrder",
|
||||
controller="allnet",
|
||||
action="handle_dlorder",
|
||||
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(
|
||||
"allnet_billing",
|
||||
"/request",
|
||||
controller="allnet",
|
||||
action="handle_billing_request",
|
||||
conditions=dict(method=["POST"]),
|
||||
)
|
||||
self.map_post.connect(
|
||||
"allnet_billing",
|
||||
"/request/",
|
||||
controller="allnet",
|
||||
action="handle_billing_request",
|
||||
conditions=dict(method=["POST"]),
|
||||
)
|
||||
|
||||
async def launch_billing(cfg: CoreConfig) -> None:
|
||||
server_cfg = uvicorn.Config(
|
||||
"core.allnet:app_billing",
|
||||
host=cfg.server.listen_address,
|
||||
port=cfg.billing.port,
|
||||
reload=cfg.server.is_develop,
|
||||
log_level="info" if cfg.server.is_develop else "critical",
|
||||
ssl_version=3,
|
||||
ssl_certfile=cfg.billing.ssl_cert,
|
||||
ssl_keyfile=cfg.billing.ssl_key,
|
||||
ssl_ciphers="DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK",
|
||||
)
|
||||
server = uvicorn.Server(server_cfg)
|
||||
await server.serve()
|
||||
# Maintain compatability
|
||||
self.map_post.connect(
|
||||
"mucha_boardauth",
|
||||
"/mucha/boardauth.do",
|
||||
controller="mucha",
|
||||
action="handle_boardauth",
|
||||
conditions=dict(method=["POST"]),
|
||||
)
|
||||
self.map_post.connect(
|
||||
"mucha_updatacheck",
|
||||
"/mucha/updatacheck.do",
|
||||
controller="mucha",
|
||||
action="handle_updatecheck",
|
||||
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"]),
|
||||
)
|
||||
|
||||
async def launch_frontend(cfg: CoreConfig) -> None:
|
||||
server_cfg = uvicorn.Config(
|
||||
"core.frontend:app",
|
||||
host=cfg.server.listen_address,
|
||||
port=cfg.frontend.port,
|
||||
reload=cfg.server.is_develop,
|
||||
log_level="info" if cfg.server.is_develop else "critical",
|
||||
)
|
||||
server = uvicorn.Server(server_cfg)
|
||||
await server.serve()
|
||||
for code, game in self.title.title_registry.items():
|
||||
get_matchers, post_matchers = game.get_endpoint_matchers()
|
||||
|
||||
for m in get_matchers:
|
||||
self.map_get.connect(
|
||||
"title_get",
|
||||
m[1],
|
||||
controller="title",
|
||||
action="render_GET",
|
||||
title=code,
|
||||
subaction=m[0],
|
||||
conditions=dict(method=["GET"]),
|
||||
requirements=m[2],
|
||||
)
|
||||
|
||||
for m in post_matchers:
|
||||
self.map_post.connect(
|
||||
"title_post",
|
||||
m[1],
|
||||
controller="title",
|
||||
action="render_POST",
|
||||
title=code,
|
||||
subaction=m[0],
|
||||
conditions=dict(method=["POST"]),
|
||||
requirements=m[2],
|
||||
)
|
||||
|
||||
def render_GET(self, request: Request) -> bytes:
|
||||
test = self.map_get.match(request.uri.decode())
|
||||
client_ip = Utils.get_ip_addr(request)
|
||||
|
||||
async def launch_allnet(cfg: CoreConfig) -> None:
|
||||
server_cfg = uvicorn.Config(
|
||||
"core.allnet:app_allnet",
|
||||
host=cfg.server.listen_address,
|
||||
port=cfg.allnet.port,
|
||||
reload=cfg.server.is_develop,
|
||||
log_level="info" if cfg.server.is_develop else "critical",
|
||||
)
|
||||
server = uvicorn.Server(server_cfg)
|
||||
await server.serve()
|
||||
if test is None:
|
||||
self.logger.debug(
|
||||
f"Unknown GET endpoint {request.uri.decode()} from {client_ip} to port {request.getHost().port}"
|
||||
)
|
||||
request.setResponseCode(404)
|
||||
return b"Endpoint not found."
|
||||
|
||||
return self.dispatch(test, request)
|
||||
|
||||
async def launcher(cfg: CoreConfig, ssl: bool) -> None:
|
||||
task_list = [asyncio.create_task(launch_main(cfg, ssl))]
|
||||
def render_POST(self, request: Request) -> bytes:
|
||||
test = self.map_post.match(request.uri.decode())
|
||||
client_ip = Utils.get_ip_addr(request)
|
||||
|
||||
if cfg.billing.standalone:
|
||||
task_list.append(asyncio.create_task(launch_billing(cfg)))
|
||||
if cfg.frontend.enable:
|
||||
task_list.append(asyncio.create_task(launch_frontend(cfg)))
|
||||
if cfg.allnet.standalone:
|
||||
task_list.append(asyncio.create_task(launch_allnet(cfg)))
|
||||
if cfg.aimedb.enable:
|
||||
AimedbServlette(cfg).start()
|
||||
if test is None:
|
||||
self.logger.debug(
|
||||
f"Unknown POST endpoint {request.uri.decode()} from {client_ip} to port {request.getHost().port}"
|
||||
)
|
||||
request.setResponseCode(404)
|
||||
return b"Endpoint not found."
|
||||
|
||||
done, pending = await asyncio.wait(
|
||||
task_list,
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
return self.dispatch(test, request)
|
||||
|
||||
logging.getLogger("core").info("Shutdown")
|
||||
for pending_task in pending:
|
||||
pending_task.cancel("Another service died, server is shutting down")
|
||||
def dispatch(self, matcher: Dict, request: Request) -> bytes:
|
||||
controller = getattr(self, matcher["controller"], None)
|
||||
if controller is None:
|
||||
self.logger.error(
|
||||
f"Controller {matcher['controller']} not found via endpoint {request.uri.decode()}"
|
||||
)
|
||||
request.setResponseCode(404)
|
||||
return b"Endpoint not found."
|
||||
|
||||
handler = getattr(controller, matcher["action"], None)
|
||||
if handler is None:
|
||||
self.logger.error(
|
||||
f"Action {matcher['action']} not found in controller {matcher['controller']} via endpoint {request.uri.decode()}"
|
||||
)
|
||||
request.setResponseCode(404)
|
||||
return b"Endpoint not found."
|
||||
|
||||
url_vars = matcher
|
||||
url_vars.pop("controller")
|
||||
url_vars.pop("action")
|
||||
ret = handler(request, url_vars)
|
||||
|
||||
if type(ret) == str:
|
||||
return ret.encode()
|
||||
|
||||
elif type(ret) == bytes or type(ret) == tuple: # allow for bytes or tuple (data, response code) responses
|
||||
return ret
|
||||
|
||||
elif ret is None:
|
||||
self.logger.warning(f"None returned by controller for {request.uri.decode()} endpoint")
|
||||
return b""
|
||||
|
||||
else:
|
||||
self.logger.warning(f"Unknown data type returned by controller for {request.uri.decode()} endpoint")
|
||||
return b""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Artemis main entry point")
|
||||
parser = argparse.ArgumentParser(description="ARTEMiS main entry point")
|
||||
parser.add_argument(
|
||||
"--config", "-c", type=str, default="config", help="Configuration folder"
|
||||
)
|
||||
parser.add_argument("--port", "-p", type=int, default=0, help="Port override")
|
||||
parser.add_argument("--ssl", "-s", type=bool, help="Launch with SSL")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not path.exists(f"{args.config}/core.yaml"):
|
||||
print(
|
||||
f"The config folder you specified ({args.config}) does not exist or does not contain core.yaml. Defaults will be used.\nDid you copy the example folder?"
|
||||
f"The config folder you specified ({args.config}) does not exist or does not contain core.yaml.\nDid you copy the example folder?"
|
||||
)
|
||||
exit(1)
|
||||
|
||||
cfg: CoreConfig = CoreConfig()
|
||||
if path.exists(f"{args.config}/core.yaml"):
|
||||
cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
|
||||
|
||||
environ["ARTEMIS_CFG_DIR"] = args.config
|
||||
if not path.exists(cfg.server.log_dir):
|
||||
mkdir(cfg.server.log_dir)
|
||||
|
||||
asyncio.run(launcher(cfg, args.ssl))
|
||||
if not access(cfg.server.log_dir, W_OK):
|
||||
print(
|
||||
f"Log directory {cfg.server.log_dir} NOT writable, please check permissions"
|
||||
)
|
||||
exit(1)
|
||||
|
||||
logger = logging.getLogger("core")
|
||||
log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s"
|
||||
log_fmt = logging.Formatter(log_fmt_str)
|
||||
|
||||
fileHandler = TimedRotatingFileHandler(
|
||||
"{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10
|
||||
)
|
||||
fileHandler.setFormatter(log_fmt)
|
||||
|
||||
consoleHandler = logging.StreamHandler()
|
||||
consoleHandler.setFormatter(log_fmt)
|
||||
|
||||
logger.addHandler(fileHandler)
|
||||
logger.addHandler(consoleHandler)
|
||||
|
||||
log_lv = logging.DEBUG if cfg.server.is_develop else logging.INFO
|
||||
logger.setLevel(log_lv)
|
||||
coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str)
|
||||
|
||||
if not cfg.aimedb.key:
|
||||
logger.error("!!AIMEDB KEY BLANK, SET KEY IN CORE.YAML!!")
|
||||
exit(1)
|
||||
|
||||
logger.info(
|
||||
f"ARTEMiS starting in {'develop' if cfg.server.is_develop else 'production'} mode"
|
||||
)
|
||||
|
||||
allnet_server_str = f"tcp:{cfg.allnet.port}:interface={cfg.server.listen_address}"
|
||||
title_server_str = f"tcp:{cfg.title.port}:interface={cfg.server.listen_address}"
|
||||
title_https_server_str = f"ssl:{cfg.title.port_ssl}:interface={cfg.server.listen_address}:privateKey={cfg.title.ssl_key}:certKey={cfg.title.ssl_cert}"
|
||||
adb_server_str = f"tcp:{cfg.aimedb.port}:interface={cfg.server.listen_address}"
|
||||
frontend_server_str = (
|
||||
f"tcp:{cfg.frontend.port}:interface={cfg.server.listen_address}"
|
||||
)
|
||||
|
||||
billing_server_str = f"tcp:{cfg.billing.port}:interface={cfg.server.listen_address}"
|
||||
if cfg.server.is_develop:
|
||||
billing_server_str = (
|
||||
f"ssl:{cfg.billing.port}:interface={cfg.server.listen_address}"
|
||||
f":privateKey={cfg.billing.ssl_key}:certKey={cfg.billing.ssl_cert}"
|
||||
)
|
||||
|
||||
dispatcher = HttpDispatcher(cfg, args.config)
|
||||
|
||||
endpoints.serverFromString(reactor, allnet_server_str).listen(
|
||||
server.Site(dispatcher)
|
||||
)
|
||||
endpoints.serverFromString(reactor, adb_server_str).listen(AimedbFactory(cfg))
|
||||
|
||||
if cfg.frontend.enable:
|
||||
endpoints.serverFromString(reactor, frontend_server_str).listen(
|
||||
server.Site(FrontendServlet(cfg, args.config))
|
||||
)
|
||||
|
||||
if cfg.billing.port > 0:
|
||||
endpoints.serverFromString(reactor, billing_server_str).listen(
|
||||
server.Site(dispatcher)
|
||||
)
|
||||
|
||||
if cfg.title.port > 0:
|
||||
endpoints.serverFromString(reactor, title_server_str).listen(
|
||||
server.Site(dispatcher)
|
||||
)
|
||||
|
||||
if cfg.title.port_ssl > 0:
|
||||
endpoints.serverFromString(reactor, title_https_server_str).listen(
|
||||
server.Site(dispatcher)
|
||||
)
|
||||
|
||||
if cfg.server.threading:
|
||||
Thread(target=reactor.run, args=(False,)).start()
|
||||
else:
|
||||
reactor.run()
|
||||
|
|
20
read.py
20
read.py
|
@ -1,15 +1,15 @@
|
|||
#!/usr/bin/env python3
|
||||
# vim: set fileencoding=utf-8
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
import os
|
||||
import yaml
|
||||
from os import path
|
||||
import logging
|
||||
import coloredlogs
|
||||
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from typing import List, Optional
|
||||
|
||||
import coloredlogs
|
||||
import yaml
|
||||
from core import CoreConfig, Utils
|
||||
|
||||
|
||||
|
@ -39,9 +39,6 @@ class BaseReader:
|
|||
|
||||
return ret
|
||||
|
||||
async def read(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Import Game Information")
|
||||
|
@ -139,7 +136,6 @@ if __name__ == "__main__":
|
|||
for dir, mod in titles.items():
|
||||
if args.game in mod.game_codes:
|
||||
handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(handler.read())
|
||||
handler.read()
|
||||
|
||||
logger.info("Done")
|
||||
|
|
76
readme.md
76
readme.md
|
@ -4,68 +4,38 @@ A network service emulator for games running SEGA'S ALL.NET service, and similar
|
|||
# Supported games
|
||||
Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported.
|
||||
|
||||
+ CHUNITHM
|
||||
+ All versions up to SUN PLUS
|
||||
|
||||
+ crossbeats REV.
|
||||
+ All versions + omnimix
|
||||
|
||||
+ maimai DX
|
||||
+ All versions up to BUDDiES
|
||||
|
||||
+ Hatsune Miku: Project DIVA Arcade
|
||||
+ All versions
|
||||
|
||||
+ Card Maker
|
||||
+ 1.30
|
||||
+ 1.35
|
||||
|
||||
+ CHUNITHM INTL
|
||||
+ SUPERSTAR
|
||||
+ NEW
|
||||
+ NEW PLUS
|
||||
+ SUN
|
||||
+ SUN PLUS
|
||||
|
||||
+ CHUNITHM JP
|
||||
+ AIR
|
||||
+ AIR PLUS
|
||||
+ AMAZON
|
||||
+ AMAZON PLUS
|
||||
+ CRYSTAL
|
||||
+ CRYSTAL PLUS
|
||||
+ PARADISE
|
||||
+ PARADISE LOST
|
||||
+ NEW
|
||||
+ NEW PLUS
|
||||
+ SUN
|
||||
+ SUN PLUS
|
||||
|
||||
+ crossbeats REV.
|
||||
+ Crossbeats REV.
|
||||
+ Crossbeats REV. SUNRiSE S1
|
||||
+ Crossbeats REV. SUNRiSE S2 + omnimix
|
||||
|
||||
+ Hatsune Miku: Project DIVA Arcade
|
||||
+ Future Tone Arcade - All versions
|
||||
|
||||
+ Initial D THE ARCADE
|
||||
+ Season 2
|
||||
|
||||
+ maimai DX
|
||||
+ Splash
|
||||
+ Splash Plus
|
||||
+ UNiVERSE
|
||||
+ UNiVERSE PLUS
|
||||
+ FESTiVAL
|
||||
+ FESTiVAL PLUS
|
||||
|
||||
+ O.N.G.E.K.I.
|
||||
+ SUMMER
|
||||
+ SUMMER PLUS
|
||||
+ R.E.D.
|
||||
+ R.E.D. PLUS
|
||||
+ bright
|
||||
+ bright MEMORY
|
||||
|
||||
+ POKKÉN TOURNAMENT
|
||||
+ Final Online
|
||||
|
||||
+ Sword Art Online Arcade
|
||||
+ Final (Single player only)
|
||||
+ All versions up to bright MEMORY
|
||||
|
||||
+ WACCA
|
||||
+ Lily R
|
||||
+ Reverse
|
||||
|
||||
+ POKKÉN TOURNAMENT
|
||||
+ Final Online
|
||||
|
||||
+ Sword Art Online Arcade (partial support)
|
||||
+ Final
|
||||
|
||||
+ Initial D THE ARCADE
|
||||
+ Season 2
|
||||
|
||||
## Requirements
|
||||
- python 3 (tested working with 3.9 and 3.10, other versions YMMV)
|
||||
- pip
|
||||
|
@ -73,7 +43,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
|||
- mysql/mariadb server
|
||||
|
||||
## Setup guides
|
||||
Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md), [linux (Debian 12 or Rasperry Pi OS recomended, but anything works)](docs/INSTALL_LINUX.md) or [docker](docs/INSTALL_DOCKER.md) to setup and run the server.
|
||||
Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md), [ubuntu](docs/INSTALL_UBUNTU.md) or [docker](docs/INSTALL_DOCKER.md) to setup and run the server.
|
||||
|
||||
## Game specific information
|
||||
Read [Games specific info](docs/game_specific_info.md) for all supported games, importer settings, configuration option and database upgrades.
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
mypy
|
||||
wheel
|
||||
pytz
|
||||
pyyaml
|
||||
sqlalchemy==1.4.46
|
||||
mysqlclient
|
||||
pyopenssl
|
||||
service_identity
|
||||
PyCryptodome
|
||||
inflection
|
||||
coloredlogs
|
||||
pylibmc; platform_system != "Windows"
|
||||
wacky
|
||||
bcrypt
|
||||
jinja2
|
||||
protobuf
|
||||
pillow
|
||||
pyjwt==2.8.0
|
||||
websockets
|
||||
starlette
|
||||
asyncio
|
||||
uvicorn
|
||||
alembic
|
||||
python-multipart
|
||||
mypy
|
||||
wheel
|
||||
twisted
|
||||
pytz
|
||||
pyyaml
|
||||
sqlalchemy==1.4.46
|
||||
mysqlclient
|
||||
pyopenssl
|
||||
service_identity
|
||||
PyCryptodome
|
||||
inflection
|
||||
coloredlogs
|
||||
pylibmc; platform_system != "Windows"
|
||||
wacky
|
||||
Routes
|
||||
bcrypt
|
||||
jinja2
|
||||
protobuf
|
||||
autobahn
|
||||
pillow
|
||||
pyjwt
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
from .const import ChuniConstants
|
||||
from .database import ChuniData
|
||||
from .frontend import ChuniFrontend
|
||||
from .index import ChuniServlet
|
||||
from .read import ChuniReader
|
||||
from titles.chuni.index import ChuniServlet
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.database import ChuniData
|
||||
from titles.chuni.read import ChuniReader
|
||||
|
||||
index = ChuniServlet
|
||||
database = ChuniData
|
||||
reader = ChuniReader
|
||||
frontend = ChuniFrontend
|
||||
game_codes = [
|
||||
ChuniConstants.GAME_CODE,
|
||||
ChuniConstants.GAME_CODE_NEW,
|
||||
ChuniConstants.GAME_CODE_INT,
|
||||
]
|
||||
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW, ChuniConstants.GAME_CODE_INT]
|
||||
current_schema_version = 5
|
|
@ -2,8 +2,8 @@ from typing import Dict
|
|||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.base import ChuniBase
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.config import ChuniConfig
|
||||
|
||||
|
||||
class ChuniAir(ChuniBase):
|
||||
|
@ -11,7 +11,7 @@ class ChuniAir(ChuniBase):
|
|||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = ChuniConstants.VER_CHUNITHM_AIR
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = await super().handle_get_game_setting_api_request(data)
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["dataVersion"] = "1.10.00"
|
||||
return ret
|
||||
|
|
|
@ -2,8 +2,8 @@ from typing import Dict
|
|||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.base import ChuniBase
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.config import ChuniConfig
|
||||
|
||||
|
||||
class ChuniAirPlus(ChuniBase):
|
||||
|
@ -11,7 +11,7 @@ class ChuniAirPlus(ChuniBase):
|
|||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = ChuniConstants.VER_CHUNITHM_AIR_PLUS
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = await super().handle_get_game_setting_api_request(data)
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["dataVersion"] = "1.15.00"
|
||||
return ret
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
from typing import Dict
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any
|
||||
import pytz
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.base import ChuniBase
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.config import ChuniConfig
|
||||
|
||||
|
||||
class ChuniAmazon(ChuniBase):
|
||||
|
@ -11,7 +13,7 @@ class ChuniAmazon(ChuniBase):
|
|||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = ChuniConstants.VER_CHUNITHM_AMAZON
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = await super().handle_get_game_setting_api_request(data)
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["dataVersion"] = "1.30.00"
|
||||
return ret
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
from typing import Dict
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any
|
||||
import pytz
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.base import ChuniBase
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.config import ChuniConfig
|
||||
|
||||
|
||||
class ChuniAmazonPlus(ChuniBase):
|
||||
|
@ -11,7 +13,7 @@ class ChuniAmazonPlus(ChuniBase):
|
|||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = await super().handle_get_game_setting_api_request(data)
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["dataVersion"] = "1.35.00"
|
||||
return ret
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import logging
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List
|
||||
from time import strftime
|
||||
|
||||
import pytz
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.database import ChuniData
|
||||
|
||||
from titles.chuni.config import ChuniConfig
|
||||
SCORE_BUFFER = {}
|
||||
|
||||
|
||||
class ChuniBase:
|
||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||
self.core_cfg = core_cfg
|
||||
|
@ -21,7 +22,7 @@ class ChuniBase:
|
|||
self.game = ChuniConstants.GAME_CODE
|
||||
self.version = ChuniConstants.VER_CHUNITHM
|
||||
|
||||
async def handle_game_login_api_request(self, data: Dict) -> Dict:
|
||||
def handle_game_login_api_request(self, data: Dict) -> Dict:
|
||||
"""
|
||||
Handles the login bonus logic, required for the game because
|
||||
getUserLoginBonus gets called after getUserItem and therefore the
|
||||
|
@ -37,22 +38,20 @@ class ChuniBase:
|
|||
return {"returnCode": 1}
|
||||
|
||||
user_id = data["userId"]
|
||||
login_bonus_presets = await self.data.static.get_login_bonus_presets(
|
||||
self.version
|
||||
)
|
||||
login_bonus_presets = self.data.static.get_login_bonus_presets(self.version)
|
||||
|
||||
for preset in login_bonus_presets:
|
||||
# check if a user already has some pogress and if not add the
|
||||
# login bonus entry
|
||||
user_login_bonus = await self.data.item.get_login_bonus(
|
||||
user_login_bonus = self.data.item.get_login_bonus(
|
||||
user_id, self.version, preset["presetId"]
|
||||
)
|
||||
if user_login_bonus is None:
|
||||
await self.data.item.put_login_bonus(
|
||||
self.data.item.put_login_bonus(
|
||||
user_id, self.version, preset["presetId"]
|
||||
)
|
||||
# yeah i'm lazy
|
||||
user_login_bonus = await self.data.item.get_login_bonus(
|
||||
user_login_bonus = self.data.item.get_login_bonus(
|
||||
user_id, self.version, preset["presetId"]
|
||||
)
|
||||
|
||||
|
@ -68,7 +67,7 @@ class ChuniBase:
|
|||
bonus_count = user_login_bonus["bonusCount"] + 1
|
||||
last_update_date = datetime.now()
|
||||
|
||||
all_login_boni = await self.data.static.get_login_bonus(
|
||||
all_login_boni = self.data.static.get_login_bonus(
|
||||
self.version, preset["presetId"]
|
||||
)
|
||||
|
||||
|
@ -92,13 +91,13 @@ class ChuniBase:
|
|||
is_finished = True
|
||||
|
||||
# grab the item for the corresponding day
|
||||
login_item = await self.data.static.get_login_bonus_by_required_days(
|
||||
login_item = self.data.static.get_login_bonus_by_required_days(
|
||||
self.version, preset["presetId"], bonus_count
|
||||
)
|
||||
if login_item is not None:
|
||||
# now add the present to the database so the
|
||||
# handle_get_user_item_api_request can grab them
|
||||
await self.data.item.put_item(
|
||||
self.data.item.put_item(
|
||||
user_id,
|
||||
{
|
||||
"itemId": login_item["presentId"],
|
||||
|
@ -108,7 +107,7 @@ class ChuniBase:
|
|||
},
|
||||
)
|
||||
|
||||
await self.data.item.put_login_bonus(
|
||||
self.data.item.put_login_bonus(
|
||||
user_id,
|
||||
self.version,
|
||||
preset["presetId"],
|
||||
|
@ -120,12 +119,12 @@ class ChuniBase:
|
|||
|
||||
return {"returnCode": 1}
|
||||
|
||||
async def handle_game_logout_api_request(self, data: Dict) -> Dict:
|
||||
def handle_game_logout_api_request(self, data: Dict) -> Dict:
|
||||
# self.data.base.log_event("chuni", "logout", logging.INFO, {"version": self.version, "user": data["userId"]})
|
||||
return {"returnCode": 1}
|
||||
|
||||
async def handle_get_game_charge_api_request(self, data: Dict) -> Dict:
|
||||
game_charge_list = await self.data.static.get_enabled_charges(self.version)
|
||||
def handle_get_game_charge_api_request(self, data: Dict) -> Dict:
|
||||
game_charge_list = self.data.static.get_enabled_charges(self.version)
|
||||
|
||||
if game_charge_list is None or len(game_charge_list) == 0:
|
||||
return {"length": 0, "gameChargeList": []}
|
||||
|
@ -146,8 +145,8 @@ class ChuniBase:
|
|||
)
|
||||
return {"length": len(charges), "gameChargeList": charges}
|
||||
|
||||
async def handle_get_game_event_api_request(self, data: Dict) -> Dict:
|
||||
game_events = await self.data.static.get_enabled_events(self.version)
|
||||
def handle_get_game_event_api_request(self, data: Dict) -> Dict:
|
||||
game_events = self.data.static.get_enabled_events(self.version)
|
||||
|
||||
if game_events is None or len(game_events) == 0:
|
||||
self.logger.warning("No enabled events, did you run the reader?")
|
||||
|
@ -178,39 +177,32 @@ class ChuniBase:
|
|||
"gameEventList": event_list,
|
||||
}
|
||||
|
||||
async def handle_get_game_idlist_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_game_idlist_api_request(self, data: Dict) -> Dict:
|
||||
return {"type": data["type"], "length": 0, "gameIdlistList": []}
|
||||
|
||||
async def handle_get_game_message_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_game_message_api_request(self, data: Dict) -> Dict:
|
||||
return {
|
||||
"type": data["type"],
|
||||
"length": 1,
|
||||
"gameMessageList": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": 1,
|
||||
"message": f"Welcome to {self.core_cfg.server.name} network!"
|
||||
if not self.game_cfg.server.news_msg
|
||||
else self.game_cfg.server.news_msg,
|
||||
"startDate": "2017-12-05 07:00:00.0",
|
||||
"endDate": "2099-12-31 00:00:00.0",
|
||||
}
|
||||
],
|
||||
"type": data["type"],
|
||||
"length": 1,
|
||||
"gameMessageList": [{
|
||||
"id": 1,
|
||||
"type": 1,
|
||||
"message": f"Welcome to {self.core_cfg.server.name} network!" if not self.game_cfg.server.news_msg else self.game_cfg.server.news_msg,
|
||||
"startDate": "2017-12-05 07:00:00.0",
|
||||
"endDate": "2099-12-31 00:00:00.0"
|
||||
}]
|
||||
}
|
||||
|
||||
async def handle_get_game_ranking_api_request(self, data: Dict) -> Dict:
|
||||
rankings = await self.data.score.get_rankings(self.version)
|
||||
def handle_get_game_ranking_api_request(self, data: Dict) -> Dict:
|
||||
rankings = self.data.score.get_rankings(self.version)
|
||||
return {"type": data["type"], "gameRankingList": rankings}
|
||||
|
||||
async def handle_get_game_sale_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_game_sale_api_request(self, data: Dict) -> Dict:
|
||||
return {"type": data["type"], "length": 0, "gameSaleList": []}
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
# if reboot start/end time is not defined use the default behavior of being a few hours ago
|
||||
if (
|
||||
self.core_cfg.title.reboot_start_time == ""
|
||||
or self.core_cfg.title.reboot_end_time == ""
|
||||
):
|
||||
if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
|
||||
reboot_start = datetime.strftime(
|
||||
datetime.utcnow() + timedelta(hours=6), self.date_time_format
|
||||
)
|
||||
|
@ -219,29 +211,15 @@ class ChuniBase:
|
|||
)
|
||||
else:
|
||||
# get current datetime in JST
|
||||
current_jst = datetime.now(pytz.timezone("Asia/Tokyo")).date()
|
||||
current_jst = datetime.now(pytz.timezone('Asia/Tokyo')).date()
|
||||
|
||||
# parse config start/end times into datetime
|
||||
reboot_start_time = datetime.strptime(
|
||||
self.core_cfg.title.reboot_start_time, "%H:%M"
|
||||
)
|
||||
reboot_end_time = datetime.strptime(
|
||||
self.core_cfg.title.reboot_end_time, "%H:%M"
|
||||
)
|
||||
reboot_start_time = datetime.strptime(self.core_cfg.title.reboot_start_time, "%H:%M")
|
||||
reboot_end_time = datetime.strptime(self.core_cfg.title.reboot_end_time, "%H:%M")
|
||||
|
||||
# offset datetimes with current date/time
|
||||
reboot_start_time = reboot_start_time.replace(
|
||||
year=current_jst.year,
|
||||
month=current_jst.month,
|
||||
day=current_jst.day,
|
||||
tzinfo=pytz.timezone("Asia/Tokyo"),
|
||||
)
|
||||
reboot_end_time = reboot_end_time.replace(
|
||||
year=current_jst.year,
|
||||
month=current_jst.month,
|
||||
day=current_jst.day,
|
||||
tzinfo=pytz.timezone("Asia/Tokyo"),
|
||||
)
|
||||
reboot_start_time = reboot_start_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo'))
|
||||
reboot_end_time = reboot_end_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo'))
|
||||
|
||||
# create strings for use in gameSetting
|
||||
reboot_start = reboot_start_time.strftime(self.date_time_format)
|
||||
|
@ -262,9 +240,8 @@ class ChuniBase:
|
|||
"isDumpUpload": "false",
|
||||
"isAou": "false",
|
||||
}
|
||||
|
||||
async def handle_get_user_activity_api_request(self, data: Dict) -> Dict:
|
||||
user_activity_list = await self.data.profile.get_profile_activity(
|
||||
def handle_get_user_activity_api_request(self, data: Dict) -> Dict:
|
||||
user_activity_list = self.data.profile.get_profile_activity(
|
||||
data["userId"], data["kind"]
|
||||
)
|
||||
|
||||
|
@ -284,8 +261,8 @@ class ChuniBase:
|
|||
"userActivityList": activity_list,
|
||||
}
|
||||
|
||||
async def handle_get_user_character_api_request(self, data: Dict) -> Dict:
|
||||
characters = await self.data.item.get_characters(data["userId"])
|
||||
def handle_get_user_character_api_request(self, data: Dict) -> Dict:
|
||||
characters = self.data.item.get_characters(data["userId"])
|
||||
if characters is None:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
|
@ -319,8 +296,8 @@ class ChuniBase:
|
|||
"userCharacterList": character_list,
|
||||
}
|
||||
|
||||
async def handle_get_user_charge_api_request(self, data: Dict) -> Dict:
|
||||
user_charge_list = await self.data.profile.get_profile_charge(data["userId"])
|
||||
def handle_get_user_charge_api_request(self, data: Dict) -> Dict:
|
||||
user_charge_list = self.data.profile.get_profile_charge(data["userId"])
|
||||
|
||||
charge_list = []
|
||||
for charge in user_charge_list:
|
||||
|
@ -335,15 +312,15 @@ class ChuniBase:
|
|||
"userChargeList": charge_list,
|
||||
}
|
||||
|
||||
async def handle_get_user_recent_player_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_user_recent_player_api_request(self, data: Dict) -> Dict:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": 0,
|
||||
"userRecentPlayerList": [], # playUserId, playUserName, playDate, friendPoint
|
||||
"userRecentPlayerList": [], # playUserId, playUserName, playDate, friendPoint
|
||||
}
|
||||
|
||||
async def handle_get_user_course_api_request(self, data: Dict) -> Dict:
|
||||
user_course_list = await self.data.score.get_courses(data["userId"])
|
||||
def handle_get_user_course_api_request(self, data: Dict) -> Dict:
|
||||
user_course_list = self.data.score.get_courses(data["userId"])
|
||||
if user_course_list is None:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
|
@ -377,8 +354,8 @@ class ChuniBase:
|
|||
"userCourseList": course_list,
|
||||
}
|
||||
|
||||
async def handle_get_user_data_api_request(self, data: Dict) -> Dict:
|
||||
p = await self.data.profile.get_profile_data(data["userId"], self.version)
|
||||
def handle_get_user_data_api_request(self, data: Dict) -> Dict:
|
||||
p = self.data.profile.get_profile_data(data["userId"], self.version)
|
||||
if p is None:
|
||||
return {}
|
||||
|
||||
|
@ -389,8 +366,8 @@ class ChuniBase:
|
|||
|
||||
return {"userId": data["userId"], "userData": profile}
|
||||
|
||||
async def handle_get_user_data_ex_api_request(self, data: Dict) -> Dict:
|
||||
p = await self.data.profile.get_profile_data_ex(data["userId"], self.version)
|
||||
def handle_get_user_data_ex_api_request(self, data: Dict) -> Dict:
|
||||
p = self.data.profile.get_profile_data_ex(data["userId"], self.version)
|
||||
if p is None:
|
||||
return {}
|
||||
|
||||
|
@ -401,8 +378,8 @@ class ChuniBase:
|
|||
|
||||
return {"userId": data["userId"], "userDataEx": profile}
|
||||
|
||||
async def handle_get_user_duel_api_request(self, data: Dict) -> Dict:
|
||||
user_duel_list = await self.data.item.get_duels(data["userId"])
|
||||
def handle_get_user_duel_api_request(self, data: Dict) -> Dict:
|
||||
user_duel_list = self.data.item.get_duels(data["userId"])
|
||||
if user_duel_list is None:
|
||||
return {}
|
||||
|
||||
|
@ -419,21 +396,27 @@ class ChuniBase:
|
|||
"userDuelList": duel_list,
|
||||
}
|
||||
|
||||
async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict:
|
||||
p = await self.data.profile.get_rival(data["rivalId"])
|
||||
def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict:
|
||||
p = self.data.profile.get_rival(data["rivalId"])
|
||||
if p is None:
|
||||
return {}
|
||||
userRivalData = {"rivalId": p.user, "rivalName": p.userName}
|
||||
return {"userId": data["userId"], "userRivalData": userRivalData}
|
||||
|
||||
async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict:
|
||||
userRivalData = {
|
||||
"rivalId": p.user,
|
||||
"rivalName": p.userName
|
||||
}
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"userRivalData": userRivalData
|
||||
}
|
||||
|
||||
def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict:
|
||||
rival_id = data["rivalId"]
|
||||
next_index = int(data["nextIndex"])
|
||||
max_count = int(data["maxCount"])
|
||||
user_rival_music_list = []
|
||||
|
||||
# Fetch all the rival music entries for the user
|
||||
all_entries = await self.data.score.get_rival_music(rival_id)
|
||||
all_entries = self.data.score.get_rival_music(rival_id)
|
||||
|
||||
# Process the entries based on max_count and nextIndex
|
||||
for music in all_entries:
|
||||
|
@ -443,33 +426,23 @@ class ChuniBase:
|
|||
rank = music["scoreRank"]
|
||||
|
||||
# Create a music entry for the current music_id if it's unique
|
||||
music_entry = next(
|
||||
(
|
||||
entry
|
||||
for entry in user_rival_music_list
|
||||
if entry["musicId"] == music_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
music_entry = next((entry for entry in user_rival_music_list if entry["musicId"] == music_id), None)
|
||||
if music_entry is None:
|
||||
music_entry = {
|
||||
"musicId": music_id,
|
||||
"length": 0,
|
||||
"userRivalMusicDetailList": [],
|
||||
"userRivalMusicDetailList": []
|
||||
}
|
||||
user_rival_music_list.append(music_entry)
|
||||
|
||||
# Create a level entry for the current level if it's unique or has a higher score
|
||||
level_entry = next(
|
||||
(
|
||||
entry
|
||||
for entry in music_entry["userRivalMusicDetailList"]
|
||||
if entry["level"] == level
|
||||
),
|
||||
None,
|
||||
)
|
||||
level_entry = next((entry for entry in music_entry["userRivalMusicDetailList"] if entry["level"] == level), None)
|
||||
if level_entry is None:
|
||||
level_entry = {"level": level, "scoreMax": score, "scoreRank": rank}
|
||||
level_entry = {
|
||||
"level": level,
|
||||
"scoreMax": score,
|
||||
"scoreRank": rank
|
||||
}
|
||||
music_entry["userRivalMusicDetailList"].append(level_entry)
|
||||
elif score > level_entry["scoreMax"]:
|
||||
level_entry["scoreMax"] = score
|
||||
|
@ -483,25 +456,18 @@ class ChuniBase:
|
|||
result = {
|
||||
"userId": data["userId"],
|
||||
"rivalId": data["rivalId"],
|
||||
"nextIndex": str(
|
||||
next_index
|
||||
+ len(user_rival_music_list[next_index : next_index + max_count])
|
||||
if max_count
|
||||
<= len(user_rival_music_list[next_index : next_index + max_count])
|
||||
else -1
|
||||
),
|
||||
"userRivalMusicList": user_rival_music_list[
|
||||
next_index : next_index + max_count
|
||||
],
|
||||
"nextIndex": str(next_index + len(user_rival_music_list[next_index: next_index + max_count]) if max_count <= len(user_rival_music_list[next_index: next_index + max_count]) else -1),
|
||||
"userRivalMusicList": user_rival_music_list[next_index: next_index + max_count]
|
||||
}
|
||||
return result
|
||||
|
||||
async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
|
||||
|
||||
def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
|
||||
user_fav_item_list = []
|
||||
|
||||
# still needs to be implemented on WebUI
|
||||
# 1: Music, 2: User, 3: Character
|
||||
fav_list = await self.data.item.get_all_favorites(
|
||||
fav_list = self.data.item.get_all_favorites(
|
||||
data["userId"], self.version, fav_kind=int(data["kind"])
|
||||
)
|
||||
if fav_list is not None:
|
||||
|
@ -516,17 +482,17 @@ class ChuniBase:
|
|||
"userFavoriteItemList": user_fav_item_list,
|
||||
}
|
||||
|
||||
async def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict:
|
||||
"""
|
||||
This is handled via the webui, which we don't have right now
|
||||
"""
|
||||
|
||||
return {"userId": data["userId"], "length": 0, "userFavoriteMusicList": []}
|
||||
|
||||
async def handle_get_user_item_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_user_item_api_request(self, data: Dict) -> Dict:
|
||||
kind = int(int(data["nextIndex"]) / 10000000000)
|
||||
next_idx = int(int(data["nextIndex"]) % 10000000000)
|
||||
user_item_list = await self.data.item.get_items(data["userId"], kind)
|
||||
user_item_list = self.data.item.get_items(data["userId"], kind)
|
||||
|
||||
if user_item_list is None or len(user_item_list) == 0:
|
||||
return {
|
||||
|
@ -560,11 +526,9 @@ class ChuniBase:
|
|||
"userItemList": items,
|
||||
}
|
||||
|
||||
async def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
|
||||
user_id = data["userId"]
|
||||
user_login_bonus = await self.data.item.get_all_login_bonus(
|
||||
user_id, self.version
|
||||
)
|
||||
user_login_bonus = self.data.item.get_all_login_bonus(user_id, self.version)
|
||||
# ignore the loginBonus request if its disabled in config
|
||||
if user_login_bonus is None or not self.game_cfg.mods.use_login_bonus:
|
||||
return {"userId": user_id, "length": 0, "userLoginBonusList": []}
|
||||
|
@ -588,8 +552,8 @@ class ChuniBase:
|
|||
"userLoginBonusList": user_login_list,
|
||||
}
|
||||
|
||||
async def handle_get_user_map_api_request(self, data: Dict) -> Dict:
|
||||
user_map_list = await self.data.item.get_maps(data["userId"])
|
||||
def handle_get_user_map_api_request(self, data: Dict) -> Dict:
|
||||
user_map_list = self.data.item.get_maps(data["userId"])
|
||||
if user_map_list is None:
|
||||
return {}
|
||||
|
||||
|
@ -606,8 +570,8 @@ class ChuniBase:
|
|||
"userMapList": map_list,
|
||||
}
|
||||
|
||||
async def handle_get_user_music_api_request(self, data: Dict) -> Dict:
|
||||
music_detail = await self.data.score.get_scores(data["userId"])
|
||||
def handle_get_user_music_api_request(self, data: Dict) -> Dict:
|
||||
music_detail = self.data.score.get_scores(data["userId"])
|
||||
if music_detail is None:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
|
@ -643,19 +607,18 @@ class ChuniBase:
|
|||
|
||||
if len(song_list) >= max_ct:
|
||||
break
|
||||
|
||||
for songIdx in range(len(song_list)):
|
||||
for recordIdx in range(x + 1, len(music_detail)):
|
||||
if (
|
||||
song_list[songIdx]["userMusicDetailList"][0]["musicId"]
|
||||
== music_detail[recordIdx]["musicId"]
|
||||
):
|
||||
music = music_detail[recordIdx]._asdict()
|
||||
music.pop("user")
|
||||
music.pop("id")
|
||||
song_list[songIdx]["userMusicDetailList"].append(music)
|
||||
song_list[songIdx]["length"] += 1
|
||||
|
||||
|
||||
try:
|
||||
while song_list[-1]["userMusicDetailList"][0]["musicId"] == music_detail[x + 1]["musicId"]:
|
||||
music = music_detail[x + 1]._asdict()
|
||||
music.pop("user")
|
||||
music.pop("id")
|
||||
song_list[-1]["userMusicDetailList"].append(music)
|
||||
song_list[-1]["length"] += 1
|
||||
x += 1
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
if len(song_list) >= max_ct:
|
||||
next_idx += len(song_list)
|
||||
else:
|
||||
|
@ -668,8 +631,8 @@ class ChuniBase:
|
|||
"userMusicList": song_list, # 240
|
||||
}
|
||||
|
||||
async def handle_get_user_option_api_request(self, data: Dict) -> Dict:
|
||||
p = await self.data.profile.get_profile_option(data["userId"])
|
||||
def handle_get_user_option_api_request(self, data: Dict) -> Dict:
|
||||
p = self.data.profile.get_profile_option(data["userId"])
|
||||
|
||||
option = p._asdict()
|
||||
option.pop("id")
|
||||
|
@ -677,8 +640,8 @@ class ChuniBase:
|
|||
|
||||
return {"userId": data["userId"], "userGameOption": option}
|
||||
|
||||
async def handle_get_user_option_ex_api_request(self, data: Dict) -> Dict:
|
||||
p = await self.data.profile.get_profile_option_ex(data["userId"])
|
||||
def handle_get_user_option_ex_api_request(self, data: Dict) -> Dict:
|
||||
p = self.data.profile.get_profile_option_ex(data["userId"])
|
||||
|
||||
option = p._asdict()
|
||||
option.pop("id")
|
||||
|
@ -689,13 +652,11 @@ class ChuniBase:
|
|||
def read_wtf8(self, src):
|
||||
return bytes([ord(c) for c in src]).decode("utf-8")
|
||||
|
||||
async def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
profile = await self.data.profile.get_profile_preview(
|
||||
data["userId"], self.version
|
||||
)
|
||||
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
profile = self.data.profile.get_profile_preview(data["userId"], self.version)
|
||||
if profile is None:
|
||||
return None
|
||||
profile_character = await self.data.item.get_character(
|
||||
profile_character = self.data.item.get_character(
|
||||
data["userId"], profile["characterId"]
|
||||
)
|
||||
|
||||
|
@ -733,10 +694,8 @@ class ChuniBase:
|
|||
"userNameEx": profile["userName"],
|
||||
}
|
||||
|
||||
async def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict:
|
||||
recent_rating_list = await self.data.profile.get_profile_recent_rating(
|
||||
data["userId"]
|
||||
)
|
||||
def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict:
|
||||
recent_rating_list = self.data.profile.get_profile_recent_rating(data["userId"])
|
||||
if recent_rating_list is None:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
|
@ -750,7 +709,7 @@ class ChuniBase:
|
|||
"userRecentRatingList": recent_rating_list["recentRating"],
|
||||
}
|
||||
|
||||
async def handle_get_user_region_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_user_region_api_request(self, data: Dict) -> Dict:
|
||||
# TODO: Region
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
|
@ -758,22 +717,22 @@ class ChuniBase:
|
|||
"userRegionList": [],
|
||||
}
|
||||
|
||||
async def handle_get_user_team_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_user_team_api_request(self, data: Dict) -> Dict:
|
||||
# Default values
|
||||
team_id = 65535
|
||||
team_name = self.game_cfg.team.team_name
|
||||
team_rank = 0
|
||||
|
||||
# Get user profile
|
||||
profile = await self.data.profile.get_profile_data(data["userId"], self.version)
|
||||
profile = self.data.profile.get_profile_data(data["userId"], self.version)
|
||||
if profile and profile["teamId"]:
|
||||
# Get team by id
|
||||
team = await self.data.profile.get_team_by_id(profile["teamId"])
|
||||
team = self.data.profile.get_team_by_id(profile["teamId"])
|
||||
|
||||
if team:
|
||||
team_id = team["id"]
|
||||
team_name = team["teamName"]
|
||||
team_rank = await self.data.profile.get_team_rank(team["id"])
|
||||
team_rank = self.data.profile.get_team_rank(team["id"])
|
||||
|
||||
# Don't return anything if no team name has been defined for defaults and there is no team set for the player
|
||||
if not profile["teamId"] and team_name == "":
|
||||
|
@ -792,8 +751,8 @@ class ChuniBase:
|
|||
"aggrDate": data["playDate"],
|
||||
},
|
||||
}
|
||||
|
||||
async def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict:
|
||||
|
||||
def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": 0,
|
||||
|
@ -801,9 +760,7 @@ class ChuniBase:
|
|||
"teamCourseSettingList": [],
|
||||
}
|
||||
|
||||
async def handle_get_team_course_setting_api_request_proto(
|
||||
self, data: Dict
|
||||
) -> Dict:
|
||||
def handle_get_team_course_setting_api_request_proto(self, data: Dict) -> Dict:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": 1,
|
||||
|
@ -818,24 +775,24 @@ class ChuniBase:
|
|||
"teamCourseMusicList": [
|
||||
{"track": 184, "type": 1, "level": 3, "selectLevel": -1},
|
||||
{"track": 184, "type": 1, "level": 3, "selectLevel": -1},
|
||||
{"track": 184, "type": 1, "level": 3, "selectLevel": -1},
|
||||
{"track": 184, "type": 1, "level": 3, "selectLevel": -1}
|
||||
],
|
||||
"teamCourseRankingInfoList": [],
|
||||
"recodeDate": "2099-12-31 11:59:99.0",
|
||||
"isPlayed": False,
|
||||
"isPlayed": False
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
async def handle_get_team_course_rule_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_team_course_rule_api_request(self, data: Dict) -> Dict:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": 0,
|
||||
"nextIndex": -1,
|
||||
"teamCourseRuleList": [],
|
||||
"teamCourseRuleList": []
|
||||
}
|
||||
|
||||
async def handle_get_team_course_rule_api_request_proto(self, data: Dict) -> Dict:
|
||||
def handle_get_team_course_rule_api_request_proto(self, data: Dict) -> Dict:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": 1,
|
||||
|
@ -847,12 +804,12 @@ class ChuniBase:
|
|||
"damageMiss": 1,
|
||||
"damageAttack": 1,
|
||||
"damageJustice": 1,
|
||||
"damageJusticeC": 1,
|
||||
"damageJusticeC": 1
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
async def handle_upsert_user_all_api_request(self, data: Dict) -> Dict:
|
||||
def handle_upsert_user_all_api_request(self, data: Dict) -> Dict:
|
||||
upsert = data["upsertUserAll"]
|
||||
user_id = data["userId"]
|
||||
|
||||
|
@ -864,189 +821,137 @@ class ChuniBase:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
await self.data.profile.put_profile_data(
|
||||
self.data.profile.put_profile_data(
|
||||
user_id, self.version, upsert["userData"][0]
|
||||
)
|
||||
|
||||
if "userDataEx" in upsert:
|
||||
await self.data.profile.put_profile_data_ex(
|
||||
self.data.profile.put_profile_data_ex(
|
||||
user_id, self.version, upsert["userDataEx"][0]
|
||||
)
|
||||
|
||||
if "userGameOption" in upsert:
|
||||
await self.data.profile.put_profile_option(
|
||||
user_id, upsert["userGameOption"][0]
|
||||
)
|
||||
self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0])
|
||||
|
||||
if "userGameOptionEx" in upsert:
|
||||
await self.data.profile.put_profile_option_ex(
|
||||
self.data.profile.put_profile_option_ex(
|
||||
user_id, upsert["userGameOptionEx"][0]
|
||||
)
|
||||
if "userRecentRatingList" in upsert:
|
||||
await self.data.profile.put_profile_recent_rating(
|
||||
self.data.profile.put_profile_recent_rating(
|
||||
user_id, upsert["userRecentRatingList"]
|
||||
)
|
||||
|
||||
if "userCharacterList" in upsert:
|
||||
for character in upsert["userCharacterList"]:
|
||||
await self.data.item.put_character(user_id, character)
|
||||
self.data.item.put_character(user_id, character)
|
||||
|
||||
if "userMapList" in upsert:
|
||||
for map in upsert["userMapList"]:
|
||||
await self.data.item.put_map(user_id, map)
|
||||
self.data.item.put_map(user_id, map)
|
||||
|
||||
if "userCourseList" in upsert:
|
||||
for course in upsert["userCourseList"]:
|
||||
await self.data.score.put_course(user_id, course)
|
||||
self.data.score.put_course(user_id, course)
|
||||
|
||||
if "userDuelList" in upsert:
|
||||
for duel in upsert["userDuelList"]:
|
||||
await self.data.item.put_duel(user_id, duel)
|
||||
self.data.item.put_duel(user_id, duel)
|
||||
|
||||
if "userItemList" in upsert:
|
||||
for item in upsert["userItemList"]:
|
||||
await self.data.item.put_item(user_id, item)
|
||||
self.data.item.put_item(user_id, item)
|
||||
|
||||
if "userActivityList" in upsert:
|
||||
for activity in upsert["userActivityList"]:
|
||||
await self.data.profile.put_profile_activity(user_id, activity)
|
||||
self.data.profile.put_profile_activity(user_id, activity)
|
||||
|
||||
if "userChargeList" in upsert:
|
||||
for charge in upsert["userChargeList"]:
|
||||
await self.data.profile.put_profile_charge(user_id, charge)
|
||||
self.data.profile.put_profile_charge(user_id, charge)
|
||||
|
||||
if "userMusicDetailList" in upsert:
|
||||
for song in upsert["userMusicDetailList"]:
|
||||
await self.data.score.put_score(user_id, song)
|
||||
self.data.score.put_score(user_id, song)
|
||||
|
||||
if "userPlaylogList" in upsert:
|
||||
for playlog in upsert["userPlaylogList"]:
|
||||
# convert the player names to utf-8
|
||||
if playlog["playedUserName1"] is not None:
|
||||
playlog["playedUserName1"] = self.read_wtf8(
|
||||
playlog["playedUserName1"]
|
||||
)
|
||||
playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"])
|
||||
if playlog["playedUserName2"] is not None:
|
||||
playlog["playedUserName2"] = self.read_wtf8(
|
||||
playlog["playedUserName2"]
|
||||
)
|
||||
playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"])
|
||||
if playlog["playedUserName3"] is not None:
|
||||
playlog["playedUserName3"] = self.read_wtf8(
|
||||
playlog["playedUserName3"]
|
||||
)
|
||||
await self.data.score.put_playlog(user_id, playlog, self.version)
|
||||
playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"])
|
||||
self.data.score.put_playlog(user_id, playlog, self.version)
|
||||
|
||||
if "userTeamPoint" in upsert:
|
||||
team_points = upsert["userTeamPoint"]
|
||||
try:
|
||||
for tp in team_points:
|
||||
if tp["teamId"] != "65535":
|
||||
if tp["teamId"] != '65535':
|
||||
# Fetch the current team data
|
||||
current_team = await self.data.profile.get_team_by_id(
|
||||
tp["teamId"]
|
||||
)
|
||||
current_team = self.data.profile.get_team_by_id(tp["teamId"])
|
||||
|
||||
# Calculate the new teamPoint
|
||||
new_team_point = (
|
||||
int(tp["teamPoint"]) + current_team["teamPoint"]
|
||||
)
|
||||
new_team_point = int(tp["teamPoint"]) + current_team["teamPoint"]
|
||||
|
||||
# Prepare the data to update
|
||||
team_data = {"teamPoint": new_team_point}
|
||||
team_data = {
|
||||
"teamPoint": new_team_point
|
||||
}
|
||||
|
||||
# Update the team data
|
||||
await self.data.profile.update_team(tp["teamId"], team_data)
|
||||
self.data.profile.update_team(tp["teamId"], team_data)
|
||||
except:
|
||||
pass # Probably a better way to catch if the team is not set yet (new profiles), but let's just pass
|
||||
pass # Probably a better way to catch if the team is not set yet (new profiles), but let's just pass
|
||||
if "userMapAreaList" in upsert:
|
||||
for map_area in upsert["userMapAreaList"]:
|
||||
await self.data.item.put_map_area(user_id, map_area)
|
||||
self.data.item.put_map_area(user_id, map_area)
|
||||
|
||||
if "userOverPowerList" in upsert:
|
||||
for overpower in upsert["userOverPowerList"]:
|
||||
await self.data.profile.put_profile_overpower(user_id, overpower)
|
||||
self.data.profile.put_profile_overpower(user_id, overpower)
|
||||
|
||||
if "userEmoneyList" in upsert:
|
||||
for emoney in upsert["userEmoneyList"]:
|
||||
await self.data.profile.put_profile_emoney(user_id, emoney)
|
||||
self.data.profile.put_profile_emoney(user_id, emoney)
|
||||
|
||||
if "userLoginBonusList" in upsert:
|
||||
for login in upsert["userLoginBonusList"]:
|
||||
await self.data.item.put_login_bonus(
|
||||
self.data.item.put_login_bonus(
|
||||
user_id, self.version, login["presetId"], isWatched=True
|
||||
)
|
||||
|
||||
if (
|
||||
"userRecentPlayerList" in upsert
|
||||
): # TODO: Seen in Air, maybe implement sometime
|
||||
|
||||
if "userRecentPlayerList" in upsert: # TODO: Seen in Air, maybe implement sometime
|
||||
for rp in upsert["userRecentPlayerList"]:
|
||||
pass
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
for rating_type in {"userRatingBaseList", "userRatingBaseHotList", "userRatingBaseNextList"}:
|
||||
if rating_type not in upsert:
|
||||
continue
|
||||
|
||||
await self.data.profile.put_profile_rating(
|
||||
user_id,
|
||||
self.version,
|
||||
rating_type,
|
||||
upsert[rating_type],
|
||||
)
|
||||
=======
|
||||
# added in LUMINOUS
|
||||
if "userCMissionList" in upsert:
|
||||
for cmission in upsert["userCMissionList"]:
|
||||
mission_id = cmission["missionId"]
|
||||
|
||||
await self.data.item.put_cmission(
|
||||
user_id,
|
||||
{
|
||||
"missionId": mission_id,
|
||||
"point": cmission["point"],
|
||||
},
|
||||
)
|
||||
|
||||
for progress in cmission["userCMissionProgressList"]:
|
||||
await self.data.item.put_cmission_progress(
|
||||
user_id, mission_id, progress
|
||||
)
|
||||
|
||||
if "userNetBattleData" in upsert:
|
||||
net_battle = upsert["userNetBattleData"][0]
|
||||
|
||||
# fix the boolean
|
||||
net_battle["isRankUpChallengeFailed"] = (
|
||||
False if net_battle["isRankUpChallengeFailed"] == "false" else True
|
||||
)
|
||||
await self.data.profile.put_net_battle(user_id, net_battle)
|
||||
>>>>>>> Stashed changes
|
||||
|
||||
return {"returnCode": "1"}
|
||||
|
||||
async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:
|
||||
def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:
|
||||
# add tickets after they got bought, this makes sure the tickets are
|
||||
# still valid after an unsuccessful logout
|
||||
await self.data.profile.put_profile_charge(data["userId"], data["userCharge"])
|
||||
self.data.profile.put_profile_charge(data["userId"], data["userCharge"])
|
||||
return {"returnCode": "1"}
|
||||
|
||||
async def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict:
|
||||
def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": "1"}
|
||||
|
||||
async def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict:
|
||||
def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": "1"}
|
||||
|
||||
async def handle_upsert_client_error_api_request(self, data: Dict) -> Dict:
|
||||
def handle_upsert_client_error_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": "1"}
|
||||
|
||||
async def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict:
|
||||
def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": "1"}
|
||||
|
||||
async def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict:
|
||||
def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": "1"}
|
||||
|
||||
async def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"userNetBattleData": {"recentNBSelectMusicList": []},
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
from typing import Dict
|
||||
|
||||
from core.config import CoreConfig
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class ChuniServerConfig:
|
||||
|
@ -20,7 +19,7 @@ class ChuniServerConfig:
|
|||
self.__config, "chuni", "server", "loglevel", default="info"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def news_msg(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
|
@ -37,7 +36,6 @@ class ChuniTeamConfig:
|
|||
return CoreConfig.get_config_field(
|
||||
self.__config, "chuni", "team", "name", default=""
|
||||
)
|
||||
|
||||
@property
|
||||
def rank_scale(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
|
@ -92,29 +90,6 @@ class ChuniCryptoConfig:
|
|||
)
|
||||
|
||||
|
||||
class ChuniMatchingConfig:
|
||||
def __init__(self, parent_config: "ChuniConfig") -> None:
|
||||
self.__config = parent_config
|
||||
|
||||
@property
|
||||
def enable(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "chuni", "matching", "enable", default=False
|
||||
)
|
||||
|
||||
@property
|
||||
def match_time_limit(self) -> int:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "chuni", "matching", "match_time_limit", default=60
|
||||
)
|
||||
|
||||
@property
|
||||
def match_error_limit(self) -> int:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "chuni", "matching", "match_error_limit", default=9999
|
||||
)
|
||||
|
||||
|
||||
class ChuniConfig(dict):
|
||||
def __init__(self) -> None:
|
||||
self.server = ChuniServerConfig(self)
|
||||
|
@ -122,4 +97,3 @@ class ChuniConfig(dict):
|
|||
self.mods = ChuniModsConfig(self)
|
||||
self.version = ChuniVersionConfig(self)
|
||||
self.crypto = ChuniCryptoConfig(self)
|
||||
self.matching = ChuniMatchingConfig(self)
|
||||
|
|
|
@ -20,7 +20,6 @@ class ChuniConstants:
|
|||
VER_CHUNITHM_NEW_PLUS = 12
|
||||
VER_CHUNITHM_SUN = 13
|
||||
VER_CHUNITHM_SUN_PLUS = 14
|
||||
VER_CHUNITHM_LUMINOUS = 15
|
||||
VERSION_NAMES = [
|
||||
"CHUNITHM",
|
||||
"CHUNITHM PLUS",
|
||||
|
@ -36,10 +35,9 @@ class ChuniConstants:
|
|||
"CHUNITHM NEW!!",
|
||||
"CHUNITHM NEW PLUS!!",
|
||||
"CHUNITHM SUN",
|
||||
"CHUNITHM SUN PLUS",
|
||||
"CHUNITHM LUMINOUS",
|
||||
"CHUNITHM SUN PLUS"
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def game_ver_to_string(cls, ver: int):
|
||||
return cls.VERSION_NAMES[ver]
|
||||
return cls.VERSION_NAMES[ver]
|
|
@ -1,9 +1,11 @@
|
|||
from typing import Dict
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any
|
||||
import pytz
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.base import ChuniBase
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.config import ChuniConfig
|
||||
|
||||
|
||||
class ChuniCrystal(ChuniBase):
|
||||
|
@ -11,7 +13,7 @@ class ChuniCrystal(ChuniBase):
|
|||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = await super().handle_get_game_setting_api_request(data)
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["dataVersion"] = "1.40.00"
|
||||
return ret
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
from typing import Dict
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any
|
||||
import pytz
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.base import ChuniBase
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.config import ChuniConfig
|
||||
|
||||
|
||||
class ChuniCrystalPlus(ChuniBase):
|
||||
|
@ -11,7 +13,7 @@ class ChuniCrystalPlus(ChuniBase):
|
|||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = await super().handle_get_game_setting_api_request(data)
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["dataVersion"] = "1.45.00"
|
||||
return ret
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from core.config import CoreConfig
|
||||
from core.data import Data
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.schema import *
|
||||
|
||||
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
from os import path
|
||||
from typing import List
|
||||
|
||||
import jinja2
|
||||
import yaml
|
||||
from core.config import CoreConfig
|
||||
from core.frontend import FE_Base, UserSession
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import RedirectResponse, Response
|
||||
from starlette.routing import Route
|
||||
|
||||
from .config import ChuniConfig
|
||||
from .const import ChuniConstants
|
||||
from .database import ChuniData
|
||||
|
||||
|
||||
class ChuniFrontend(FE_Base):
|
||||
def __init__(
|
||||
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
|
||||
) -> None:
|
||||
super().__init__(cfg, environment)
|
||||
self.data = ChuniData(cfg)
|
||||
self.game_cfg = ChuniConfig()
|
||||
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
|
||||
self.game_cfg.update(
|
||||
yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"))
|
||||
)
|
||||
self.nav_name = "Chunithm"
|
||||
|
||||
def get_routes(self) -> List[Route]:
|
||||
return [
|
||||
Route("/", self.render_GET, methods=["GET"]),
|
||||
Route("/update.name", self.update_name, methods=["POST"]),
|
||||
]
|
||||
|
||||
async def render_GET(self, request: Request) -> bytes:
|
||||
template = self.environment.get_template(
|
||||
"titles/chuni/templates/chuni_index.jinja"
|
||||
)
|
||||
usr_sesh = self.validate_session(request)
|
||||
if not usr_sesh:
|
||||
usr_sesh = UserSession()
|
||||
|
||||
return Response(
|
||||
template.render(
|
||||
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||
game_list=self.environment.globals["game_list"],
|
||||
sesh=vars(usr_sesh),
|
||||
),
|
||||
media_type="text/html; charset=utf-8",
|
||||
)
|
||||
|
||||
async def update_name(self, request: Request) -> bytes:
|
||||
usr_sesh = self.validate_session(request)
|
||||
if not usr_sesh:
|
||||
return RedirectResponse("/gate/", 303)
|
||||
|
||||
new_name: str = request.query_params.get("new_name", "")
|
||||
new_name_full = ""
|
||||
|
||||
if not new_name:
|
||||
return RedirectResponse("/gate/?e=4", 303)
|
||||
|
||||
if len(new_name) > 8:
|
||||
return RedirectResponse("/gate/?e=8", 303)
|
||||
|
||||
for x in new_name: # FIXME: This will let some invalid characters through atm
|
||||
o = ord(x)
|
||||
try:
|
||||
if o == 0x20:
|
||||
new_name_full += chr(0x3000)
|
||||
elif o < 0x7F and o > 0x20:
|
||||
new_name_full += chr(o + 0xFEE0)
|
||||
elif o <= 0x7F:
|
||||
self.logger.warn(f"Invalid ascii character {o:02X}")
|
||||
return RedirectResponse("/gate/?e=4", 303)
|
||||
else:
|
||||
new_name_full += x
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Something went wrong parsing character {o:04X} - {e}"
|
||||
)
|
||||
return RedirectResponse("/gate/?e=4", 303)
|
||||
|
||||
if not await self.data.profile.update_name(usr_sesh, new_name_full):
|
||||
return RedirectResponse("/gate/?e=999", 303)
|
||||
|
||||
return RedirectResponse("/gate/?s=1", 303)
|
|
@ -1,40 +1,35 @@
|
|||
import json
|
||||
import logging
|
||||
import string
|
||||
import zlib
|
||||
from twisted.web.http import Request
|
||||
import logging, coloredlogs
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from os import path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import coloredlogs
|
||||
import inflection
|
||||
import zlib
|
||||
import yaml
|
||||
import json
|
||||
import inflection
|
||||
import string
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad
|
||||
from Crypto.Protocol.KDF import PBKDF2
|
||||
from Crypto.Hash import SHA1
|
||||
from os import path
|
||||
from typing import Tuple, Dict, List
|
||||
|
||||
from core import CoreConfig, Utils
|
||||
from core.title import BaseServlet
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Hash import SHA1
|
||||
from Crypto.Protocol.KDF import PBKDF2
|
||||
from Crypto.Util.Padding import pad
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
from starlette.routing import Route
|
||||
|
||||
from .air import ChuniAir
|
||||
from .airplus import ChuniAirPlus
|
||||
from .amazon import ChuniAmazon
|
||||
from .amazonplus import ChuniAmazonPlus
|
||||
from .base import ChuniBase
|
||||
from .config import ChuniConfig
|
||||
from .const import ChuniConstants
|
||||
from .crystal import ChuniCrystal
|
||||
from .crystalplus import ChuniCrystalPlus
|
||||
from .luminous import ChuniLuminous
|
||||
from .new import ChuniNew
|
||||
from .newplus import ChuniNewPlus
|
||||
from .paradise import ChuniParadise
|
||||
from .base import ChuniBase
|
||||
from .plus import ChuniPlus
|
||||
from .air import ChuniAir
|
||||
from .airplus import ChuniAirPlus
|
||||
from .star import ChuniStar
|
||||
from .starplus import ChuniStarPlus
|
||||
from .amazon import ChuniAmazon
|
||||
from .amazonplus import ChuniAmazonPlus
|
||||
from .crystal import ChuniCrystal
|
||||
from .crystalplus import ChuniCrystalPlus
|
||||
from .paradise import ChuniParadise
|
||||
from .new import ChuniNew
|
||||
from .newplus import ChuniNewPlus
|
||||
from .sun import ChuniSun
|
||||
from .sunplus import ChuniSunPlus
|
||||
|
||||
|
@ -65,7 +60,6 @@ class ChuniServlet(BaseServlet):
|
|||
ChuniNewPlus,
|
||||
ChuniSun,
|
||||
ChuniSunPlus,
|
||||
ChuniLuminous,
|
||||
]
|
||||
|
||||
self.logger = logging.getLogger("chuni")
|
||||
|
@ -108,15 +102,13 @@ class ChuniServlet(BaseServlet):
|
|||
for method in method_list:
|
||||
method_fixed = inflection.camelize(method)[6:-7]
|
||||
# number of iterations was changed to 70 in SUN and then to 36
|
||||
if version == ChuniConstants.VER_CHUNITHM_LUMINOUS:
|
||||
iter_count = 8
|
||||
elif version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
|
||||
if version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
|
||||
iter_count = 36
|
||||
elif version == ChuniConstants.VER_CHUNITHM_SUN:
|
||||
iter_count = 70
|
||||
else:
|
||||
iter_count = 44
|
||||
|
||||
|
||||
hash = PBKDF2(
|
||||
method_fixed,
|
||||
bytes.fromhex(keys[2]),
|
||||
|
@ -125,15 +117,22 @@ class ChuniServlet(BaseServlet):
|
|||
hmac_hash_module=SHA1,
|
||||
)
|
||||
|
||||
hashed_name = hash.hex()[
|
||||
:32
|
||||
] # truncate unused bytes like the game does
|
||||
hashed_name = hash.hex()[:32] # truncate unused bytes like the game does
|
||||
self.hash_table[version][hashed_name] = method_fixed
|
||||
|
||||
self.logger.debug(
|
||||
f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}"
|
||||
)
|
||||
|
||||
def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]:
|
||||
return (
|
||||
[],
|
||||
[
|
||||
("render_POST", "/{game}/{version}/ChuniServlet/{endpoint}", {}),
|
||||
("render_POST", "/{game}/{version}/ChuniServlet/MatchingServer/{endpoint}", {})
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_game_enabled(
|
||||
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
|
||||
|
@ -149,119 +148,88 @@ class ChuniServlet(BaseServlet):
|
|||
|
||||
return True
|
||||
|
||||
def get_allnet_info(
|
||||
self, game_code: str, game_ver: int, keychip: str
|
||||
) -> Tuple[str, str]:
|
||||
if (
|
||||
not self.core_cfg.server.is_using_proxy
|
||||
and Utils.get_title_port(self.core_cfg) != 80
|
||||
):
|
||||
return (
|
||||
f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/",
|
||||
self.core_cfg.server.hostname,
|
||||
)
|
||||
def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]:
|
||||
if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80:
|
||||
return (f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/", self.core_cfg.title.hostname)
|
||||
|
||||
return (
|
||||
f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/",
|
||||
self.core_cfg.server.hostname,
|
||||
)
|
||||
return (f"http://{self.core_cfg.title.hostname}/{game_code}/{game_ver}/", self.core_cfg.title.hostname)
|
||||
|
||||
def get_routes(self) -> List[Route]:
|
||||
return [
|
||||
Route(
|
||||
"/{game:str}/{version:int}/ChuniServlet/{endpoint:str}",
|
||||
self.render_POST,
|
||||
methods=["POST"],
|
||||
),
|
||||
Route(
|
||||
"/{game:str}/{version:int}/ChuniServlet/MatchingServer/{endpoint:str}",
|
||||
self.render_POST,
|
||||
methods=["POST"],
|
||||
),
|
||||
]
|
||||
|
||||
async def render_POST(self, request: Request) -> bytes:
|
||||
endpoint: str = request.path_params.get("endpoint")
|
||||
version: int = request.path_params.get("version")
|
||||
game_code: str = request.path_params.get("game")
|
||||
def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes:
|
||||
endpoint = matchers['endpoint']
|
||||
version = int(matchers['version'])
|
||||
game_code = matchers['game']
|
||||
|
||||
if endpoint.lower() == "ping":
|
||||
return Response(zlib.compress(b'{"returnCode": "1"}'))
|
||||
return zlib.compress(b'{"returnCode": "1"}')
|
||||
|
||||
req_raw = await request.body()
|
||||
req_raw = request.content.getvalue()
|
||||
|
||||
encrtped = False
|
||||
internal_ver = 0
|
||||
client_ip = Utils.get_ip_addr(request)
|
||||
|
||||
if game_code == "SDHD" or game_code == "SDBT": # JP
|
||||
if version < 105: # 1.0
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM
|
||||
elif version >= 105 and version < 110: # PLUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_PLUS
|
||||
elif version >= 110 and version < 115: # AIR
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_AIR
|
||||
elif version >= 115 and version < 120: # AIR PLUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS
|
||||
elif version >= 120 and version < 125: # STAR
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_STAR
|
||||
elif version >= 125 and version < 130: # STAR PLUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS
|
||||
elif version >= 130 and version < 135: # AMAZON
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON
|
||||
elif version >= 135 and version < 140: # AMAZON PLUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
|
||||
elif version >= 140 and version < 145: # CRYSTAL
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL
|
||||
elif version >= 145 and version < 150: # CRYSTAL PLUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
|
||||
elif version >= 150 and version < 200: # PARADISE
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE
|
||||
elif version >= 200 and version < 205: # NEW!!
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
|
||||
elif version >= 205 and version < 210: # NEW PLUS!!
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||
elif version >= 210 and version < 215: # SUN
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
|
||||
elif 215 <= version < 220: # SUN
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
||||
elif version >= 220: # LUMINOUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
||||
elif game_code == "SDGS": # Int
|
||||
if version < 110: # SUPERSTAR
|
||||
internal_ver = (
|
||||
ChuniConstants.VER_CHUNITHM_PARADISE
|
||||
) # FIXME: Not sure what was intended to go here? was just "PARADISE"
|
||||
elif version >= 110 and version < 115: # NEW
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
|
||||
elif version >= 115 and version < 120: # NEW PLUS!!
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||
elif version >= 120 and version < 125: # SUN
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
|
||||
elif 125 <= version < 130: # SUN PLUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
||||
elif version >= 130: # LUMINOUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
||||
if game_code == "SDHD" or game_code == "SDBT": # JP
|
||||
if version < 105: # 1.0
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM
|
||||
elif version >= 105 and version < 110: # PLUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_PLUS
|
||||
elif version >= 110 and version < 115: # AIR
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_AIR
|
||||
elif version >= 115 and version < 120: # AIR PLUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS
|
||||
elif version >= 120 and version < 125: # STAR
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_STAR
|
||||
elif version >= 125 and version < 130: # STAR PLUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS
|
||||
elif version >= 130 and version < 135: # AMAZON
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON
|
||||
elif version >= 135 and version < 140: # AMAZON PLUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
|
||||
elif version >= 140 and version < 145: # CRYSTAL
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL
|
||||
elif version >= 145 and version < 150: # CRYSTAL PLUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
|
||||
elif version >= 150 and version < 200: # PARADISE
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE
|
||||
elif version >= 200 and version < 205: # NEW!!
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
|
||||
elif version >= 205 and version < 210: # NEW PLUS!!
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||
elif version >= 210 and version < 215: # SUN
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
|
||||
elif version >= 215: # SUN
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
||||
elif game_code == "SDGS": # Int
|
||||
if version < 110: # SUPERSTAR
|
||||
internal_ver = ChuniConstants.PARADISE
|
||||
elif version >= 110 and version < 115: # NEW
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
|
||||
elif version >= 115 and version < 120: # NEW PLUS!!
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||
elif version >= 120 and version < 125: # SUN
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
|
||||
elif version >= 125: # SUN PLUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
||||
|
||||
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
||||
# If we get a 32 character long hex string, it's a hash and we're
|
||||
# doing encrypted. The likelyhood of false positives is low but
|
||||
# technically not 0
|
||||
if internal_ver < ChuniConstants.VER_CHUNITHM_NEW:
|
||||
endpoint = request.headers.get("User-Agent").split("#")[0]
|
||||
endpoint = request.getHeader("User-Agent").split("#")[0]
|
||||
|
||||
else:
|
||||
if internal_ver not in self.hash_table:
|
||||
self.logger.error(
|
||||
f"v{version} does not support encryption or no keys entered"
|
||||
)
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
return zlib.compress(b'{"stat": "0"}')
|
||||
|
||||
elif endpoint.lower() not in self.hash_table[internal_ver]:
|
||||
self.logger.error(
|
||||
f"No hash found for v{version} endpoint {endpoint}"
|
||||
)
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
return zlib.compress(b'{"stat": "0"}')
|
||||
|
||||
endpoint = self.hash_table[internal_ver][endpoint.lower()]
|
||||
|
||||
|
@ -278,7 +246,7 @@ class ChuniServlet(BaseServlet):
|
|||
self.logger.error(
|
||||
f"Failed to decrypt v{version} request to {endpoint} -> {e}"
|
||||
)
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
return zlib.compress(b'{"stat": "0"}')
|
||||
|
||||
encrtped = True
|
||||
|
||||
|
@ -290,7 +258,7 @@ class ChuniServlet(BaseServlet):
|
|||
self.logger.error(
|
||||
f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}"
|
||||
)
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
return zlib.compress(b'{"stat": "0"}')
|
||||
|
||||
try:
|
||||
unzip = zlib.decompress(req_raw)
|
||||
|
@ -299,7 +267,7 @@ class ChuniServlet(BaseServlet):
|
|||
self.logger.error(
|
||||
f"Failed to decompress v{version} {endpoint} request -> {e}"
|
||||
)
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
return b""
|
||||
|
||||
req_data = json.loads(unzip)
|
||||
|
||||
|
@ -317,13 +285,13 @@ class ChuniServlet(BaseServlet):
|
|||
else:
|
||||
try:
|
||||
handler = getattr(handler_cls, func_to_find)
|
||||
resp = await handler(req_data)
|
||||
resp = handler(req_data)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
return zlib.compress(b'{"stat": "0"}')
|
||||
|
||||
if resp is None:
|
||||
if resp == None:
|
||||
resp = {"returnCode": 1}
|
||||
|
||||
self.logger.debug(f"Response {resp}")
|
||||
|
@ -331,7 +299,7 @@ class ChuniServlet(BaseServlet):
|
|||
zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
|
||||
|
||||
if not encrtped:
|
||||
return Response(zipped)
|
||||
return zipped
|
||||
|
||||
padded = pad(zipped, 16)
|
||||
|
||||
|
@ -341,4 +309,4 @@ class ChuniServlet(BaseServlet):
|
|||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
|
||||
)
|
||||
|
||||
return Response(crypt.encrypt(padded))
|
||||
return crypt.encrypt(padded)
|
|
@ -1,72 +0,0 @@
|
|||
from typing import Dict
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.sunplus import ChuniSunPlus
|
||||
|
||||
|
||||
class ChuniLuminous(ChuniSunPlus):
|
||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
||||
|
||||
async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
user_data = await super().handle_cm_get_user_preview_api_request(data)
|
||||
|
||||
user_data["lastDataVersion"] = "2.20.00"
|
||||
return user_data
|
||||
|
||||
async def handle_get_game_map_area_condition_api_request(self, data: Dict) -> Dict:
|
||||
return {"length": 0, "gameMapAreaConditionList": []}
|
||||
|
||||
async def handle_get_user_c_mission_api_request(self, data: Dict) -> Dict:
|
||||
user_id = data["userId"]
|
||||
mission_id = data["missionId"]
|
||||
|
||||
progress_list = []
|
||||
point = 0
|
||||
|
||||
mission_data = await self.data.item.get_cmission(user_id, mission_id)
|
||||
progress_data = await self.data.item.get_cmission_progress(user_id, mission_id)
|
||||
|
||||
if mission_data and progress_data:
|
||||
point = mission_data["point"]
|
||||
|
||||
for progress in progress_data:
|
||||
progress_list.append(
|
||||
{
|
||||
"order": progress["order"],
|
||||
"stage": progress["stage"],
|
||||
"progress": progress["progress"],
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"userId": user_id,
|
||||
"missionId": mission_id,
|
||||
"point": point,
|
||||
"userCMissionProgressList": progress_list,
|
||||
}
|
||||
|
||||
async def handle_get_user_net_battle_ranking_info_api_request(
|
||||
self, data: Dict
|
||||
) -> Dict:
|
||||
user_id = data["userId"]
|
||||
|
||||
net_battle = {}
|
||||
net_battle_data = await self.data.profile.get_net_battle(user_id)
|
||||
|
||||
if net_battle_data:
|
||||
net_battle = {
|
||||
"isRankUpChallengeFailed": net_battle_data["isRankUpChallengeFailed"],
|
||||
"highestBattleRankId": net_battle_data["highestBattleRankId"],
|
||||
"battleIconId": net_battle_data["battleIconId"],
|
||||
"battleIconNum": net_battle_data["battleIconNum"],
|
||||
"avatarEffectPoint": net_battle_data["avatarEffectPoint"],
|
||||
}
|
||||
|
||||
return {
|
||||
"userId": user_id,
|
||||
"userNetBattleData": net_battle,
|
||||
}
|
|
@ -5,11 +5,10 @@ from typing import Dict
|
|||
|
||||
import pytz
|
||||
from core.config import CoreConfig
|
||||
from core.utils import Utils
|
||||
from titles.chuni.base import ChuniBase
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.database import ChuniData
|
||||
from titles.chuni.base import ChuniBase
|
||||
from titles.chuni.config import ChuniConfig
|
||||
|
||||
|
||||
class ChuniNew(ChuniBase):
|
||||
|
@ -24,19 +23,7 @@ class ChuniNew(ChuniBase):
|
|||
self.game = ChuniConstants.GAME_CODE
|
||||
self.version = ChuniConstants.VER_CHUNITHM_NEW
|
||||
|
||||
def _interal_ver_to_intver(self) -> str:
|
||||
if self.version == ChuniConstants.VER_CHUNITHM_NEW:
|
||||
return "200"
|
||||
if self.version == ChuniConstants.VER_CHUNITHM_NEW_PLUS:
|
||||
return "205"
|
||||
if self.version == ChuniConstants.VER_CHUNITHM_SUN:
|
||||
return "210"
|
||||
if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
|
||||
return "215"
|
||||
if self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS:
|
||||
return "220"
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
# use UTC time and convert it to JST time by adding +9
|
||||
# matching therefore starts one hour before and lasts for 8 hours
|
||||
match_start = datetime.strftime(
|
||||
|
@ -46,10 +33,7 @@ class ChuniNew(ChuniBase):
|
|||
datetime.utcnow() + timedelta(hours=16), self.date_time_format
|
||||
)
|
||||
# if reboot start/end time is not defined use the default behavior of being a few hours ago
|
||||
if (
|
||||
self.core_cfg.title.reboot_start_time == ""
|
||||
or self.core_cfg.title.reboot_end_time == ""
|
||||
):
|
||||
if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
|
||||
reboot_start = datetime.strftime(
|
||||
datetime.utcnow() + timedelta(hours=6), self.date_time_format
|
||||
)
|
||||
|
@ -58,44 +42,19 @@ class ChuniNew(ChuniBase):
|
|||
)
|
||||
else:
|
||||
# get current datetime in JST
|
||||
current_jst = datetime.now(pytz.timezone("Asia/Tokyo")).date()
|
||||
current_jst = datetime.now(pytz.timezone('Asia/Tokyo')).date()
|
||||
|
||||
# parse config start/end times into datetime
|
||||
reboot_start_time = datetime.strptime(
|
||||
self.core_cfg.title.reboot_start_time, "%H:%M"
|
||||
)
|
||||
reboot_end_time = datetime.strptime(
|
||||
self.core_cfg.title.reboot_end_time, "%H:%M"
|
||||
)
|
||||
reboot_start_time = datetime.strptime(self.core_cfg.title.reboot_start_time, "%H:%M")
|
||||
reboot_end_time = datetime.strptime(self.core_cfg.title.reboot_end_time, "%H:%M")
|
||||
|
||||
# offset datetimes with current date/time
|
||||
reboot_start_time = reboot_start_time.replace(
|
||||
year=current_jst.year,
|
||||
month=current_jst.month,
|
||||
day=current_jst.day,
|
||||
tzinfo=pytz.timezone("Asia/Tokyo"),
|
||||
)
|
||||
reboot_end_time = reboot_end_time.replace(
|
||||
year=current_jst.year,
|
||||
month=current_jst.month,
|
||||
day=current_jst.day,
|
||||
tzinfo=pytz.timezone("Asia/Tokyo"),
|
||||
)
|
||||
reboot_start_time = reboot_start_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo'))
|
||||
reboot_end_time = reboot_end_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo'))
|
||||
|
||||
# create strings for use in gameSetting
|
||||
reboot_start = reboot_start_time.strftime(self.date_time_format)
|
||||
reboot_end = reboot_end_time.strftime(self.date_time_format)
|
||||
|
||||
t_port = (
|
||||
f":{self.core_cfg.server.port}"
|
||||
if (
|
||||
not self.core_cfg.server.is_using_proxy
|
||||
and Utils.get_title_port(self.core_cfg) != 80
|
||||
)
|
||||
else ""
|
||||
)
|
||||
version = self.game_cfg.version.version(self.version)
|
||||
|
||||
return {
|
||||
"gameSetting": {
|
||||
"isMaintenance": False,
|
||||
|
@ -108,31 +67,31 @@ class ChuniNew(ChuniBase):
|
|||
"maxCountMusic": 300,
|
||||
"matchStartTime": match_start,
|
||||
"matchEndTime": match_end,
|
||||
"matchTimeLimit": self.game_cfg.matching.match_time_limit,
|
||||
"matchErrorLimit": self.game_cfg.matching.match_error_limit,
|
||||
"romVersion": version["rom"],
|
||||
"dataVersion": version["data"],
|
||||
"matchingUri": f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
|
||||
"matchingUriX": f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
|
||||
"matchTimeLimit": 60,
|
||||
"matchErrorLimit": 9999,
|
||||
"romVersion": self.game_cfg.version.version(self.version)["rom"],
|
||||
"dataVersion": self.game_cfg.version.version(self.version)["data"],
|
||||
"matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
||||
"matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
||||
# might be really important for online battle to connect the cabs via UDP port 50201
|
||||
"udpHolePunchUri": f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
|
||||
"reflectorUri": f"http://{self.core_cfg.server.hostname}{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
|
||||
"udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
||||
"reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
||||
},
|
||||
"isDumpUpload": False,
|
||||
"isAou": False,
|
||||
}
|
||||
|
||||
async def handle_remove_token_api_request(self, data: Dict) -> Dict:
|
||||
def handle_remove_token_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": "1"}
|
||||
|
||||
async def handle_delete_token_api_request(self, data: Dict) -> Dict:
|
||||
def handle_delete_token_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": "1"}
|
||||
|
||||
async def handle_create_token_api_request(self, data: Dict) -> Dict:
|
||||
def handle_create_token_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": "1"}
|
||||
|
||||
async def handle_get_user_map_area_api_request(self, data: Dict) -> Dict:
|
||||
user_map_areas = await self.data.item.get_map_areas(data["userId"])
|
||||
def handle_get_user_map_area_api_request(self, data: Dict) -> Dict:
|
||||
user_map_areas = self.data.item.get_map_areas(data["userId"])
|
||||
|
||||
map_areas = []
|
||||
for map_area in user_map_areas:
|
||||
|
@ -143,16 +102,14 @@ class ChuniNew(ChuniBase):
|
|||
|
||||
return {"userId": data["userId"], "userMapAreaList": map_areas}
|
||||
|
||||
async def handle_get_user_symbol_chat_setting_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_user_symbol_chat_setting_api_request(self, data: Dict) -> Dict:
|
||||
return {"userId": data["userId"], "symbolCharInfoList": []}
|
||||
|
||||
async def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
profile = await self.data.profile.get_profile_preview(
|
||||
data["userId"], self.version
|
||||
)
|
||||
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
profile = self.data.profile.get_profile_preview(data["userId"], self.version)
|
||||
if profile is None:
|
||||
return None
|
||||
profile_character = await self.data.item.get_character(
|
||||
profile_character = self.data.item.get_character(
|
||||
data["userId"], profile["characterId"]
|
||||
)
|
||||
|
||||
|
@ -196,8 +153,8 @@ class ChuniNew(ChuniBase):
|
|||
}
|
||||
return data1
|
||||
|
||||
async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
p = await self.data.profile.get_profile_data(data["userId"], self.version)
|
||||
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
p = self.data.profile.get_profile_data(data["userId"], self.version)
|
||||
if p is None:
|
||||
return {}
|
||||
|
||||
|
@ -209,17 +166,17 @@ class ChuniNew(ChuniBase):
|
|||
"isLogin": False,
|
||||
}
|
||||
|
||||
async def handle_printer_login_api_request(self, data: Dict) -> Dict:
|
||||
def handle_printer_login_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": 1}
|
||||
|
||||
async def handle_printer_logout_api_request(self, data: Dict) -> Dict:
|
||||
def handle_printer_logout_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": 1}
|
||||
|
||||
async def handle_get_game_gacha_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_game_gacha_api_request(self, data: Dict) -> Dict:
|
||||
"""
|
||||
returns all current active banners (gachas)
|
||||
"""
|
||||
game_gachas = await self.data.static.get_gachas(self.version)
|
||||
game_gachas = self.data.static.get_gachas(self.version)
|
||||
|
||||
# clean the database rows
|
||||
game_gacha_list = []
|
||||
|
@ -245,11 +202,11 @@ class ChuniNew(ChuniBase):
|
|||
"registIdList": [],
|
||||
}
|
||||
|
||||
async def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict:
|
||||
"""
|
||||
returns all valid cards for a given gachaId
|
||||
"""
|
||||
game_gacha_cards = await self.data.static.get_gacha_cards(data["gachaId"])
|
||||
game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"])
|
||||
|
||||
game_gacha_card_list = []
|
||||
for gacha_card in game_gacha_cards:
|
||||
|
@ -269,8 +226,8 @@ class ChuniNew(ChuniBase):
|
|||
"ssrBookCalcList": [],
|
||||
}
|
||||
|
||||
async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
|
||||
p = await self.data.profile.get_profile_data(data["userId"], self.version)
|
||||
def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
|
||||
p = self.data.profile.get_profile_data(data["userId"], self.version)
|
||||
if p is None:
|
||||
return {}
|
||||
|
||||
|
@ -294,8 +251,8 @@ class ChuniNew(ChuniBase):
|
|||
],
|
||||
}
|
||||
|
||||
async def handle_get_user_gacha_api_request(self, data: Dict) -> Dict:
|
||||
user_gachas = await self.data.item.get_user_gachas(data["userId"])
|
||||
def handle_get_user_gacha_api_request(self, data: Dict) -> Dict:
|
||||
user_gachas = self.data.item.get_user_gachas(data["userId"])
|
||||
if user_gachas is None:
|
||||
return {"userId": data["userId"], "length": 0, "userGachaList": []}
|
||||
|
||||
|
@ -313,8 +270,8 @@ class ChuniNew(ChuniBase):
|
|||
"userGachaList": user_gacha_list,
|
||||
}
|
||||
|
||||
async def handle_get_user_printed_card_api_request(self, data: Dict) -> Dict:
|
||||
user_print_list = await self.data.item.get_user_print_states(
|
||||
def handle_get_user_printed_card_api_request(self, data: Dict) -> Dict:
|
||||
user_print_list = self.data.item.get_user_print_states(
|
||||
data["userId"], has_completed=True
|
||||
)
|
||||
if user_print_list is None:
|
||||
|
@ -348,10 +305,10 @@ class ChuniNew(ChuniBase):
|
|||
"userPrintedCardList": print_list,
|
||||
}
|
||||
|
||||
async def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
|
||||
user_id = data["userId"]
|
||||
|
||||
user_print_states = await self.data.item.get_user_print_states(
|
||||
user_print_states = self.data.item.get_user_print_states(
|
||||
user_id, has_completed=False
|
||||
)
|
||||
|
||||
|
@ -370,13 +327,13 @@ class ChuniNew(ChuniBase):
|
|||
"userCardPrintStateList": card_print_state_list,
|
||||
}
|
||||
|
||||
async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
|
||||
return await super().handle_get_user_character_api_request(data)
|
||||
def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
|
||||
return super().handle_get_user_character_api_request(data)
|
||||
|
||||
async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
|
||||
return await super().handle_get_user_item_api_request(data)
|
||||
def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
|
||||
return super().handle_get_user_item_api_request(data)
|
||||
|
||||
async def handle_roll_gacha_api_request(self, data: Dict) -> Dict:
|
||||
def handle_roll_gacha_api_request(self, data: Dict) -> Dict:
|
||||
"""
|
||||
Handle a gacha roll API request, with:
|
||||
gachaId: the gachaId where the cards should be pulled from
|
||||
|
@ -394,16 +351,14 @@ class ChuniNew(ChuniBase):
|
|||
# characterId should be returned
|
||||
if chara_id != -1:
|
||||
# get the
|
||||
card = await self.data.static.get_gacha_card_by_character(
|
||||
gacha_id, chara_id
|
||||
)
|
||||
card = self.data.static.get_gacha_card_by_character(gacha_id, chara_id)
|
||||
|
||||
tmp = card._asdict()
|
||||
tmp.pop("id")
|
||||
|
||||
rolled_cards.append(tmp)
|
||||
else:
|
||||
gacha_cards = await self.data.static.get_gacha_cards(gacha_id)
|
||||
gacha_cards = self.data.static.get_gacha_cards(gacha_id)
|
||||
|
||||
# get the card id for each roll
|
||||
for _ in range(num_rolls):
|
||||
|
@ -420,7 +375,7 @@ class ChuniNew(ChuniBase):
|
|||
|
||||
return {"length": len(rolled_cards), "gameGachaCardList": rolled_cards}
|
||||
|
||||
async def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict:
|
||||
def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict:
|
||||
upsert = data["cmUpsertUserGacha"]
|
||||
user_id = data["userId"]
|
||||
place_id = data["placeId"]
|
||||
|
@ -430,7 +385,7 @@ class ChuniNew(ChuniBase):
|
|||
user_data.pop("rankUpChallengeResults")
|
||||
user_data.pop("userEmoney")
|
||||
|
||||
await self.data.profile.put_profile_data(user_id, self.version, user_data)
|
||||
self.data.profile.put_profile_data(user_id, self.version, user_data)
|
||||
|
||||
# save the user gacha
|
||||
user_gacha = upsert["userGacha"]
|
||||
|
@ -438,16 +393,16 @@ class ChuniNew(ChuniBase):
|
|||
user_gacha.pop("gachaId")
|
||||
user_gacha.pop("dailyGachaDate")
|
||||
|
||||
await self.data.item.put_user_gacha(user_id, gacha_id, user_gacha)
|
||||
self.data.item.put_user_gacha(user_id, gacha_id, user_gacha)
|
||||
|
||||
# save all user items
|
||||
if "userItemList" in upsert:
|
||||
for item in upsert["userItemList"]:
|
||||
await self.data.item.put_item(user_id, item)
|
||||
self.data.item.put_item(user_id, item)
|
||||
|
||||
# add every gamegachaCard to database
|
||||
for card in upsert["gameGachaCardList"]:
|
||||
await self.data.item.put_user_print_state(
|
||||
self.data.item.put_user_print_state(
|
||||
user_id,
|
||||
hasCompleted=False,
|
||||
placeId=place_id,
|
||||
|
@ -457,7 +412,7 @@ class ChuniNew(ChuniBase):
|
|||
|
||||
# retrieve every game gacha card which has been added in order to get
|
||||
# the orderId for the next request
|
||||
user_print_states = await self.data.item.get_user_print_states_by_gacha(
|
||||
user_print_states = self.data.item.get_user_print_states_by_gacha(
|
||||
user_id, gacha_id, has_completed=False
|
||||
)
|
||||
card_print_state_list = []
|
||||
|
@ -475,7 +430,7 @@ class ChuniNew(ChuniBase):
|
|||
"userCardPrintStateList": card_print_state_list,
|
||||
}
|
||||
|
||||
async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
|
||||
def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
|
||||
return {
|
||||
"returnCode": 1,
|
||||
"orderId": 0,
|
||||
|
@ -483,7 +438,7 @@ class ChuniNew(ChuniBase):
|
|||
"apiName": "CMUpsertUserPrintlogApi",
|
||||
}
|
||||
|
||||
async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
|
||||
def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
|
||||
user_print_detail = data["userPrintDetail"]
|
||||
user_id = data["userId"]
|
||||
|
||||
|
@ -499,9 +454,7 @@ class ChuniNew(ChuniBase):
|
|||
)
|
||||
|
||||
# add the entry to the user print table with the random serialId
|
||||
await self.data.item.put_user_print_detail(
|
||||
user_id, serial_id, user_print_detail
|
||||
)
|
||||
self.data.item.put_user_print_detail(user_id, serial_id, user_print_detail)
|
||||
|
||||
return {
|
||||
"returnCode": 1,
|
||||
|
@ -510,9 +463,7 @@ class ChuniNew(ChuniBase):
|
|||
"apiName": "CMUpsertUserPrintApi",
|
||||
}
|
||||
|
||||
async def handle_cm_upsert_user_print_subtract_api_request(
|
||||
self, data: Dict
|
||||
) -> Dict:
|
||||
def handle_cm_upsert_user_print_subtract_api_request(self, data: Dict) -> Dict:
|
||||
upsert = data["userCardPrintState"]
|
||||
user_id = data["userId"]
|
||||
place_id = data["placeId"]
|
||||
|
@ -520,39 +471,37 @@ class ChuniNew(ChuniBase):
|
|||
# save all user items
|
||||
if "userItemList" in data:
|
||||
for item in data["userItemList"]:
|
||||
await self.data.item.put_item(user_id, item)
|
||||
self.data.item.put_item(user_id, item)
|
||||
|
||||
# set the card print state to success and use the orderId as the key
|
||||
await self.data.item.put_user_print_state(
|
||||
self.data.item.put_user_print_state(
|
||||
user_id, id=upsert["orderId"], hasCompleted=True
|
||||
)
|
||||
|
||||
return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"}
|
||||
|
||||
async def handle_cm_upsert_user_print_cancel_api_request(self, data: Dict) -> Dict:
|
||||
def handle_cm_upsert_user_print_cancel_api_request(self, data: Dict) -> Dict:
|
||||
order_ids = data["orderIdList"]
|
||||
user_id = data["userId"]
|
||||
|
||||
# set the card print state to success and use the orderId as the key
|
||||
for order_id in order_ids:
|
||||
await self.data.item.put_user_print_state(
|
||||
user_id, id=order_id, hasCompleted=True
|
||||
)
|
||||
self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True)
|
||||
|
||||
return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"}
|
||||
|
||||
async def handle_ping_request(self, data: Dict) -> Dict:
|
||||
def handle_ping_request(self, data: Dict) -> Dict:
|
||||
# matchmaking ping request
|
||||
return {"returnCode": "1"}
|
||||
|
||||
async def handle_begin_matching_api_request(self, data: Dict) -> Dict:
|
||||
def handle_begin_matching_api_request(self, data: Dict) -> Dict:
|
||||
room_id = 1
|
||||
# check if there is a free matching room
|
||||
matching_room = await self.data.item.get_oldest_free_matching(self.version)
|
||||
matching_room = self.data.item.get_oldest_free_matching(self.version)
|
||||
|
||||
if matching_room is None:
|
||||
# grab the latest roomId and add 1 for the new room
|
||||
newest_matching = await self.data.item.get_newest_matching(self.version)
|
||||
newest_matching = self.data.item.get_newest_matching(self.version)
|
||||
if newest_matching is not None:
|
||||
room_id = newest_matching["roomId"] + 1
|
||||
|
||||
|
@ -562,12 +511,12 @@ class ChuniNew(ChuniBase):
|
|||
|
||||
# create the new room with room_id and the current user id (host)
|
||||
# user id is required for the countdown later on
|
||||
await self.data.item.put_matching(
|
||||
self.data.item.put_matching(
|
||||
self.version, room_id, [new_member], user_id=new_member["userId"]
|
||||
)
|
||||
|
||||
# get the newly created matching room
|
||||
matching_room = await self.data.item.get_matching(self.version, room_id)
|
||||
matching_room = self.data.item.get_matching(self.version, room_id)
|
||||
else:
|
||||
# a room already exists, so just add the new member to it
|
||||
matching_member_list = matching_room["matchingMemberInfoList"]
|
||||
|
@ -577,7 +526,7 @@ class ChuniNew(ChuniBase):
|
|||
matching_member_list.append(new_member)
|
||||
|
||||
# add the updated room to the database, make sure to set isFull correctly!
|
||||
await self.data.item.put_matching(
|
||||
self.data.item.put_matching(
|
||||
self.version,
|
||||
matching_room["roomId"],
|
||||
matching_member_list,
|
||||
|
@ -594,8 +543,8 @@ class ChuniNew(ChuniBase):
|
|||
|
||||
return {"roomId": 1, "matchingWaitState": matching_wait}
|
||||
|
||||
async def handle_end_matching_api_request(self, data: Dict) -> Dict:
|
||||
matching_room = await self.data.item.get_matching(self.version, data["roomId"])
|
||||
def handle_end_matching_api_request(self, data: Dict) -> Dict:
|
||||
matching_room = self.data.item.get_matching(self.version, data["roomId"])
|
||||
members = matching_room["matchingMemberInfoList"]
|
||||
|
||||
# only set the host user to role 1 every other to 0?
|
||||
|
@ -604,7 +553,7 @@ class ChuniNew(ChuniBase):
|
|||
for m in members
|
||||
]
|
||||
|
||||
await self.data.item.put_matching(
|
||||
self.data.item.put_matching(
|
||||
self.version,
|
||||
matching_room["roomId"],
|
||||
members,
|
||||
|
@ -619,13 +568,13 @@ class ChuniNew(ChuniBase):
|
|||
# no idea, maybe to differentiate between CPUs and real players?
|
||||
"matchingMemberRoleList": role_list,
|
||||
# TCP/UDP connection?
|
||||
"reflectorUri": f"{self.core_cfg.server.hostname}",
|
||||
"reflectorUri": f"{self.core_cfg.title.hostname}",
|
||||
}
|
||||
|
||||
async def handle_remove_matching_member_api_request(self, data: Dict) -> Dict:
|
||||
def handle_remove_matching_member_api_request(self, data: Dict) -> Dict:
|
||||
# get all matching rooms, because Chuni only returns the userId
|
||||
# not the actual roomId
|
||||
matching_rooms = await self.data.item.get_all_matchings(self.version)
|
||||
matching_rooms = self.data.item.get_all_matchings(self.version)
|
||||
if matching_rooms is None:
|
||||
return {"returnCode": "1"}
|
||||
|
||||
|
@ -639,10 +588,10 @@ class ChuniNew(ChuniBase):
|
|||
|
||||
# if the last user got removed, delete the matching room
|
||||
if len(new_members) <= 0:
|
||||
await self.data.item.delete_matching(self.version, room["roomId"])
|
||||
self.data.item.delete_matching(self.version, room["roomId"])
|
||||
else:
|
||||
# remove the user from the room
|
||||
await self.data.item.put_matching(
|
||||
self.data.item.put_matching(
|
||||
self.version,
|
||||
room["roomId"],
|
||||
new_members,
|
||||
|
@ -652,10 +601,10 @@ class ChuniNew(ChuniBase):
|
|||
|
||||
return {"returnCode": "1"}
|
||||
|
||||
async def handle_get_matching_state_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_matching_state_api_request(self, data: Dict) -> Dict:
|
||||
polling_interval = 1
|
||||
# get the current active room
|
||||
matching_room = await self.data.item.get_matching(self.version, data["roomId"])
|
||||
matching_room = self.data.item.get_matching(self.version, data["roomId"])
|
||||
members = matching_room["matchingMemberInfoList"]
|
||||
rest_sec = matching_room["restMSec"]
|
||||
|
||||
|
@ -678,7 +627,7 @@ class ChuniNew(ChuniBase):
|
|||
current_member["userName"] = self.read_wtf8(current_member["userName"])
|
||||
members[i] = current_member
|
||||
|
||||
await self.data.item.put_matching(
|
||||
self.data.item.put_matching(
|
||||
self.version,
|
||||
data["roomId"],
|
||||
members,
|
||||
|
@ -698,4 +647,4 @@ class ChuniNew(ChuniBase):
|
|||
"matchingMemberInfoList": [current_member] + diff_members,
|
||||
}
|
||||
|
||||
return {"roomId": data["roomId"], "matchingWaitState": matching_wait}
|
||||
return {"matchingWaitState": matching_wait}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
from typing import Dict
|
||||
from typing import Dict, Any
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.new import ChuniNew
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.config import ChuniConfig
|
||||
|
||||
|
||||
class ChuniNewPlus(ChuniNew):
|
||||
|
@ -11,9 +11,31 @@ class ChuniNewPlus(ChuniNew):
|
|||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||
|
||||
async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
user_data = await super().handle_cm_get_user_preview_api_request(data)
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)[
|
||||
"rom"
|
||||
]
|
||||
ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)[
|
||||
"data"
|
||||
]
|
||||
ret["gameSetting"][
|
||||
"matchingUri"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
||||
ret["gameSetting"][
|
||||
"matchingUriX"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
||||
ret["gameSetting"][
|
||||
"udpHolePunchUri"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
||||
ret["gameSetting"][
|
||||
"reflectorUri"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
||||
return ret
|
||||
|
||||
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
user_data = super().handle_cm_get_user_preview_api_request(data)
|
||||
|
||||
# hardcode lastDataVersion for CardMaker 1.35 A028
|
||||
user_data["lastDataVersion"] = "2.05.00"
|
||||
return user_data
|
||||
return user_data
|
|
@ -1,9 +1,11 @@
|
|||
from typing import Dict
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any
|
||||
import pytz
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.base import ChuniBase
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.config import ChuniConfig
|
||||
|
||||
|
||||
class ChuniParadise(ChuniBase):
|
||||
|
@ -11,7 +13,7 @@ class ChuniParadise(ChuniBase):
|
|||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = ChuniConstants.VER_CHUNITHM_PARADISE
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = await super().handle_get_game_setting_api_request(data)
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["dataVersion"] = "1.50.00"
|
||||
return ret
|
||||
|
|
|
@ -2,8 +2,8 @@ from typing import Dict
|
|||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.base import ChuniBase
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.config import ChuniConfig
|
||||
|
||||
|
||||
class ChuniPlus(ChuniBase):
|
||||
|
@ -11,7 +11,7 @@ class ChuniPlus(ChuniBase):
|
|||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = ChuniConstants.VER_CHUNITHM_PLUS
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = await super().handle_get_game_setting_api_request(data)
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["dataVersion"] = "1.05.00"
|
||||
return ret
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import xml.etree.ElementTree as ET
|
||||
from os import path, walk
|
||||
from typing import Optional
|
||||
from os import walk, path
|
||||
import xml.etree.ElementTree as ET
|
||||
from read import BaseReader
|
||||
|
||||
from core.config import CoreConfig
|
||||
from read import BaseReader
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.database import ChuniData
|
||||
from titles.chuni.const import ChuniConstants
|
||||
|
||||
|
||||
class ChuniReader(BaseReader):
|
||||
|
@ -28,7 +28,7 @@ class ChuniReader(BaseReader):
|
|||
self.logger.error(f"Invalid chunithm version {version}")
|
||||
exit(1)
|
||||
|
||||
async def read(self) -> None:
|
||||
def read(self) -> None:
|
||||
data_dirs = []
|
||||
if self.bin_dir is not None:
|
||||
data_dirs += self.get_data_directories(self.bin_dir)
|
||||
|
@ -38,13 +38,13 @@ class ChuniReader(BaseReader):
|
|||
|
||||
for dir in data_dirs:
|
||||
self.logger.info(f"Read from {dir}")
|
||||
await self.read_events(f"{dir}/event")
|
||||
await self.read_music(f"{dir}/music")
|
||||
await self.read_charges(f"{dir}/chargeItem")
|
||||
await self.read_avatar(f"{dir}/avatarAccessory")
|
||||
await self.read_login_bonus(f"{dir}/")
|
||||
self.read_events(f"{dir}/event")
|
||||
self.read_music(f"{dir}/music")
|
||||
self.read_charges(f"{dir}/chargeItem")
|
||||
self.read_avatar(f"{dir}/avatarAccessory")
|
||||
self.read_login_bonus(f"{dir}/")
|
||||
|
||||
async def read_login_bonus(self, root_dir: str) -> None:
|
||||
def read_login_bonus(self, root_dir: str) -> None:
|
||||
for root, dirs, files in walk(f"{root_dir}loginBonusPreset"):
|
||||
for dir in dirs:
|
||||
if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"):
|
||||
|
@ -60,7 +60,7 @@ class ChuniReader(BaseReader):
|
|||
True if xml_root.find("disableFlag").text == "false" else False
|
||||
)
|
||||
|
||||
result = await self.data.static.put_login_bonus_preset(
|
||||
result = self.data.static.put_login_bonus_preset(
|
||||
self.version, id, name, is_enabled
|
||||
)
|
||||
|
||||
|
@ -98,7 +98,7 @@ class ChuniReader(BaseReader):
|
|||
bonus_root.find("loginBonusCategoryType").text
|
||||
)
|
||||
|
||||
result = await self.data.static.put_login_bonus(
|
||||
result = self.data.static.put_login_bonus(
|
||||
self.version,
|
||||
id,
|
||||
bonus_id,
|
||||
|
@ -117,7 +117,7 @@ class ChuniReader(BaseReader):
|
|||
f"Failed to insert login bonus {bonus_id}"
|
||||
)
|
||||
|
||||
async def read_events(self, evt_dir: str) -> None:
|
||||
def read_events(self, evt_dir: str) -> None:
|
||||
for root, dirs, files in walk(evt_dir):
|
||||
for dir in dirs:
|
||||
if path.exists(f"{root}/{dir}/Event.xml"):
|
||||
|
@ -132,7 +132,7 @@ class ChuniReader(BaseReader):
|
|||
for substances in xml_root.findall("substances"):
|
||||
event_type = substances.find("type").text
|
||||
|
||||
result = await self.data.static.put_event(
|
||||
result = self.data.static.put_event(
|
||||
self.version, id, event_type, name
|
||||
)
|
||||
if result is not None:
|
||||
|
@ -140,7 +140,7 @@ class ChuniReader(BaseReader):
|
|||
else:
|
||||
self.logger.warning(f"Failed to insert event {id}")
|
||||
|
||||
async 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 dir in dirs:
|
||||
if path.exists(f"{root}/{dir}/Music.xml"):
|
||||
|
@ -172,9 +172,7 @@ class ChuniReader(BaseReader):
|
|||
chart_type = MusicFumenData.find("type")
|
||||
chart_id = chart_type.find("id").text
|
||||
chart_diff = chart_type.find("str").text
|
||||
if chart_diff == "WorldsEnd" and (
|
||||
chart_id == "4" or chart_id == "5"
|
||||
): # 4 in SDBT, 5 in SDHD
|
||||
if chart_diff == "WorldsEnd" and (chart_id == "4" or chart_id == "5"): # 4 in SDBT, 5 in SDHD
|
||||
level = float(xml_root.find("starDifType").text)
|
||||
we_chara = (
|
||||
xml_root.find("worldsEndTagName")
|
||||
|
@ -187,7 +185,7 @@ class ChuniReader(BaseReader):
|
|||
)
|
||||
we_chara = None
|
||||
|
||||
result = await self.data.static.put_music(
|
||||
result = self.data.static.put_music(
|
||||
self.version,
|
||||
song_id,
|
||||
chart_id,
|
||||
|
@ -208,7 +206,7 @@ class ChuniReader(BaseReader):
|
|||
f"Failed to insert music {song_id} chart {chart_id}"
|
||||
)
|
||||
|
||||
async def read_charges(self, charge_dir: str) -> None:
|
||||
def read_charges(self, charge_dir: str) -> None:
|
||||
for root, dirs, files in walk(charge_dir):
|
||||
for dir in dirs:
|
||||
if path.exists(f"{root}/{dir}/ChargeItem.xml"):
|
||||
|
@ -224,7 +222,7 @@ class ChuniReader(BaseReader):
|
|||
consumeType = xml_root.find("consumeType").text
|
||||
sellingAppeal = bool(xml_root.find("sellingAppeal").text)
|
||||
|
||||
result = await self.data.static.put_charge(
|
||||
result = self.data.static.put_charge(
|
||||
self.version,
|
||||
id,
|
||||
name,
|
||||
|
@ -238,7 +236,7 @@ class ChuniReader(BaseReader):
|
|||
else:
|
||||
self.logger.warning(f"Failed to insert charge {id}")
|
||||
|
||||
async 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 dir in dirs:
|
||||
if path.exists(f"{root}/{dir}/AvatarAccessory.xml"):
|
||||
|
@ -256,7 +254,7 @@ class ChuniReader(BaseReader):
|
|||
for texture in xml_root.findall("texture"):
|
||||
texturePath = texture.find("path").text
|
||||
|
||||
result = await self.data.static.put_avatar(
|
||||
result = self.data.static.put_avatar(
|
||||
self.version, id, name, category, iconPath, texturePath
|
||||
)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from titles.chuni.schema.item import ChuniItemData
|
||||
from titles.chuni.schema.profile import ChuniProfileData
|
||||
from titles.chuni.schema.score import ChuniScoreData
|
||||
from titles.chuni.schema.item import ChuniItemData
|
||||
from titles.chuni.schema.static import ChuniStaticData
|
||||
|
||||
__all__ = ["ChuniProfileData", "ChuniScoreData", "ChuniItemData", "ChuniStaticData"]
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
from typing import Dict, List, Optional
|
||||
|
||||
from core.data.schema import BaseData, metadata
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
PrimaryKeyConstraint,
|
||||
Table,
|
||||
Column,
|
||||
UniqueConstraint,
|
||||
PrimaryKeyConstraint,
|
||||
and_,
|
||||
delete,
|
||||
)
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
from sqlalchemy.engine import Row
|
||||
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
|
||||
from sqlalchemy.engine.base import Connection
|
||||
from sqlalchemy.schema import ForeignKey
|
||||
from sqlalchemy.sql import func, select
|
||||
from sqlalchemy.types import JSON, TIMESTAMP, Boolean, Integer, String
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
from sqlalchemy.engine import Row
|
||||
|
||||
from core.data.schema import BaseData, metadata
|
||||
|
||||
character = Table(
|
||||
"chuni_item_character",
|
||||
|
@ -242,84 +243,63 @@ matching = Table(
|
|||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
cmission = Table(
|
||||
"chuni_item_cmission",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column(
|
||||
"user",
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("missionId", Integer, nullable=False),
|
||||
Column("point", Integer),
|
||||
UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
cmission_progress = Table(
|
||||
"chuni_item_cmission_progress",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
|
||||
Column("missionId", Integer, nullable=False),
|
||||
Column("order", Integer),
|
||||
Column("stage", Integer),
|
||||
Column("progress", Integer),
|
||||
UniqueConstraint(
|
||||
"user", "missionId", "order", name="chuni_item_cmission_progress_uk"
|
||||
),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
|
||||
class ChuniItemData(BaseData):
|
||||
async def get_oldest_free_matching(self, version: int) -> Optional[Row]:
|
||||
def get_oldest_free_matching(self, version: int) -> Optional[Row]:
|
||||
sql = matching.select(
|
||||
and_(matching.c.version == version, matching.c.isFull == False)
|
||||
and_(
|
||||
matching.c.version == version,
|
||||
matching.c.isFull == False
|
||||
)
|
||||
).order_by(matching.c.roomId.asc())
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def get_newest_matching(self, version: int) -> Optional[Row]:
|
||||
sql = matching.select(and_(matching.c.version == version)).order_by(
|
||||
matching.c.roomId.desc()
|
||||
def get_newest_matching(self, version: int) -> Optional[Row]:
|
||||
sql = matching.select(
|
||||
and_(
|
||||
matching.c.version == version
|
||||
)
|
||||
).order_by(matching.c.roomId.desc())
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
def get_all_matchings(self, version: int) -> Optional[List[Row]]:
|
||||
sql = matching.select(
|
||||
and_(
|
||||
matching.c.version == version
|
||||
)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def get_all_matchings(self, version: int) -> Optional[List[Row]]:
|
||||
sql = matching.select(and_(matching.c.version == version))
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_matching(self, version: int, room_id: int) -> Optional[Row]:
|
||||
def get_matching(self, version: int, room_id: int) -> Optional[Row]:
|
||||
sql = matching.select(
|
||||
and_(matching.c.version == version, matching.c.roomId == room_id)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def put_matching(
|
||||
def put_matching(
|
||||
self,
|
||||
version: int,
|
||||
room_id: int,
|
||||
matching_member_info_list: List,
|
||||
user_id: int = None,
|
||||
rest_sec: int = 60,
|
||||
is_full: bool = False,
|
||||
is_full: bool = False
|
||||
) -> Optional[int]:
|
||||
sql = insert(matching).values(
|
||||
roomId=room_id,
|
||||
|
@ -334,22 +314,22 @@ class ChuniItemData(BaseData):
|
|||
restMSec=rest_sec, matchingMemberInfoList=matching_member_info_list
|
||||
)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def delete_matching(self, version: int, room_id: int):
|
||||
def delete_matching(self, version: int, room_id: int):
|
||||
sql = delete(matching).where(
|
||||
and_(matching.c.roomId == room_id, matching.c.version == version)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_all_favorites(
|
||||
def get_all_favorites(
|
||||
self, user_id: int, version: int, fav_kind: int = 1
|
||||
) -> Optional[List[Row]]:
|
||||
sql = favorite.select(
|
||||
|
@ -360,12 +340,12 @@ class ChuniItemData(BaseData):
|
|||
)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_login_bonus(
|
||||
def put_login_bonus(
|
||||
self, user_id: int, version: int, preset_id: int, **login_bonus_data
|
||||
) -> Optional[int]:
|
||||
sql = insert(login_bonus).values(
|
||||
|
@ -374,12 +354,12 @@ class ChuniItemData(BaseData):
|
|||
|
||||
conflict = sql.on_duplicate_key_update(presetId=preset_id, **login_bonus_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_all_login_bonus(
|
||||
def get_all_login_bonus(
|
||||
self, user_id: int, version: int, is_finished: bool = False
|
||||
) -> Optional[List[Row]]:
|
||||
sql = login_bonus.select(
|
||||
|
@ -390,12 +370,12 @@ class ChuniItemData(BaseData):
|
|||
)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_login_bonus(
|
||||
def get_login_bonus(
|
||||
self, user_id: int, version: int, preset_id: int
|
||||
) -> Optional[Row]:
|
||||
sql = login_bonus.select(
|
||||
|
@ -406,12 +386,12 @@ class ChuniItemData(BaseData):
|
|||
)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def put_character(self, user_id: int, character_data: Dict) -> Optional[int]:
|
||||
def put_character(self, user_id: int, character_data: Dict) -> Optional[int]:
|
||||
character_data["user"] = user_id
|
||||
|
||||
character_data = self.fix_bools(character_data)
|
||||
|
@ -419,30 +399,30 @@ class ChuniItemData(BaseData):
|
|||
sql = insert(character).values(**character_data)
|
||||
conflict = sql.on_duplicate_key_update(**character_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_character(self, user_id: int, character_id: int) -> Optional[Dict]:
|
||||
def get_character(self, user_id: int, character_id: int) -> Optional[Dict]:
|
||||
sql = select(character).where(
|
||||
and_(character.c.user == user_id, character.c.characterId == character_id)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def get_characters(self, user_id: int) -> Optional[List[Row]]:
|
||||
def get_characters(self, user_id: int) -> Optional[List[Row]]:
|
||||
sql = select(character).where(character.c.user == user_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_item(self, user_id: int, item_data: Dict) -> Optional[int]:
|
||||
def put_item(self, user_id: int, item_data: Dict) -> Optional[int]:
|
||||
item_data["user"] = user_id
|
||||
|
||||
item_data = self.fix_bools(item_data)
|
||||
|
@ -450,12 +430,12 @@ class ChuniItemData(BaseData):
|
|||
sql = insert(item).values(**item_data)
|
||||
conflict = sql.on_duplicate_key_update(**item_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_items(self, user_id: int, kind: int = None) -> Optional[List[Row]]:
|
||||
def get_items(self, user_id: int, kind: int = None) -> Optional[List[Row]]:
|
||||
if kind is None:
|
||||
sql = select(item).where(item.c.user == user_id)
|
||||
else:
|
||||
|
@ -463,12 +443,12 @@ class ChuniItemData(BaseData):
|
|||
and_(item.c.user == user_id, item.c.itemKind == kind)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_duel(self, user_id: int, duel_data: Dict) -> Optional[int]:
|
||||
def put_duel(self, user_id: int, duel_data: Dict) -> Optional[int]:
|
||||
duel_data["user"] = user_id
|
||||
|
||||
duel_data = self.fix_bools(duel_data)
|
||||
|
@ -476,20 +456,20 @@ class ChuniItemData(BaseData):
|
|||
sql = insert(duel).values(**duel_data)
|
||||
conflict = sql.on_duplicate_key_update(**duel_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_duels(self, user_id: int) -> Optional[List[Row]]:
|
||||
def get_duels(self, user_id: int) -> Optional[List[Row]]:
|
||||
sql = select(duel).where(duel.c.user == user_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_map(self, user_id: int, map_data: Dict) -> Optional[int]:
|
||||
def put_map(self, user_id: int, map_data: Dict) -> Optional[int]:
|
||||
map_data["user"] = user_id
|
||||
|
||||
map_data = self.fix_bools(map_data)
|
||||
|
@ -497,20 +477,20 @@ class ChuniItemData(BaseData):
|
|||
sql = insert(map).values(**map_data)
|
||||
conflict = sql.on_duplicate_key_update(**map_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_maps(self, user_id: int) -> Optional[List[Row]]:
|
||||
def get_maps(self, user_id: int) -> Optional[List[Row]]:
|
||||
sql = select(map).where(map.c.user == user_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_map_area(self, user_id: int, map_area_data: Dict) -> Optional[int]:
|
||||
def put_map_area(self, user_id: int, map_area_data: Dict) -> Optional[int]:
|
||||
map_area_data["user"] = user_id
|
||||
|
||||
map_area_data = self.fix_bools(map_area_data)
|
||||
|
@ -518,28 +498,28 @@ class ChuniItemData(BaseData):
|
|||
sql = insert(map_area).values(**map_area_data)
|
||||
conflict = sql.on_duplicate_key_update(**map_area_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_map_areas(self, user_id: int) -> Optional[List[Row]]:
|
||||
def get_map_areas(self, user_id: int) -> Optional[List[Row]]:
|
||||
sql = select(map_area).where(map_area.c.user == user_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]:
|
||||
def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]:
|
||||
sql = gacha.select(gacha.c.user == aime_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_user_gacha(
|
||||
def put_user_gacha(
|
||||
self, aime_id: int, gacha_id: int, gacha_data: Dict
|
||||
) -> Optional[int]:
|
||||
sql = insert(gacha).values(user=aime_id, gachaId=gacha_id, **gacha_data)
|
||||
|
@ -547,14 +527,14 @@ class ChuniItemData(BaseData):
|
|||
conflict = sql.on_duplicate_key_update(
|
||||
user=aime_id, gachaId=gacha_id, **gacha_data
|
||||
)
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
self.logger.warning(f"put_user_gacha: Failed to insert! aime_id: {aime_id}")
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_user_print_states(
|
||||
def get_user_print_states(
|
||||
self, aime_id: int, has_completed: bool = False
|
||||
) -> Optional[List[Row]]:
|
||||
sql = print_state.select(
|
||||
|
@ -564,12 +544,12 @@ class ChuniItemData(BaseData):
|
|||
)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_user_print_states_by_gacha(
|
||||
def get_user_print_states_by_gacha(
|
||||
self, aime_id: int, gacha_id: int, has_completed: bool = False
|
||||
) -> Optional[List[Row]]:
|
||||
sql = print_state.select(
|
||||
|
@ -580,16 +560,16 @@ class ChuniItemData(BaseData):
|
|||
)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_user_print_state(self, aime_id: int, **print_data) -> Optional[int]:
|
||||
def put_user_print_state(self, aime_id: int, **print_data) -> Optional[int]:
|
||||
sql = insert(print_state).values(user=aime_id, **print_data)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(user=aime_id, **print_data)
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
self.logger.warning(
|
||||
|
@ -598,7 +578,7 @@ class ChuniItemData(BaseData):
|
|||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def put_user_print_detail(
|
||||
def put_user_print_detail(
|
||||
self, aime_id: int, serial_id: str, user_print_data: Dict
|
||||
) -> Optional[int]:
|
||||
sql = insert(print_detail).values(
|
||||
|
@ -606,7 +586,7 @@ class ChuniItemData(BaseData):
|
|||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data)
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
self.logger.warning(
|
||||
|
@ -614,61 +594,3 @@ class ChuniItemData(BaseData):
|
|||
)
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def put_cmission_progress(
|
||||
self, user_id: int, mission_id: int, progress_data: Dict
|
||||
) -> Optional[int]:
|
||||
progress_data["user"] = user_id
|
||||
progress_data["missionId"] = mission_id
|
||||
|
||||
sql = insert(cmission_progress).values(**progress_data)
|
||||
conflict = sql.on_duplicate_key_update(**progress_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_cmission_progress(
|
||||
self, user_id: int, mission_id: int
|
||||
) -> Optional[List[Row]]:
|
||||
sql = cmission_progress.select(
|
||||
and_(
|
||||
cmission_progress.c.user == user_id,
|
||||
cmission_progress.c.missionId == mission_id,
|
||||
)
|
||||
).order_by(cmission_progress.c.order.asc())
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_cmission(self, user_id: int, mission_data: Dict) -> Optional[int]:
|
||||
mission_data["user"] = user_id
|
||||
|
||||
sql = insert(cmission).values(**mission_data)
|
||||
conflict = sql.on_duplicate_key_update(**mission_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_cmissions(self, user_id: int) -> Optional[List[Row]]:
|
||||
sql = cmission.select(cmission.c.user == user_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_cmission(self, user_id: int, mission_id: int) -> Optional[Row]:
|
||||
sql = cmission.select(
|
||||
and_(cmission.c.user == user_id, cmission.c.missionId == mission_id)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
|
|
@ -1,21 +1,13 @@
|
|||
from typing import Dict, List, Optional
|
||||
<<<<<<< Updated upstream
|
||||
from sqlalchemy import Table, Column, UniqueConstraint, and_
|
||||
from sqlalchemy.types import Integer, String, Boolean, JSON, BigInteger
|
||||
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
|
||||
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger
|
||||
from sqlalchemy.engine.base import Connection
|
||||
from sqlalchemy.schema import ForeignKey
|
||||
from sqlalchemy.engine import Row
|
||||
from sqlalchemy.sql import select, delete
|
||||
from sqlalchemy.sql import func, select
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
=======
|
||||
>>>>>>> Stashed changes
|
||||
|
||||
from core.data.schema import BaseData, metadata
|
||||
from sqlalchemy import Column, Table, UniqueConstraint, and_
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
from sqlalchemy.engine import Row
|
||||
from sqlalchemy.schema import ForeignKey
|
||||
from sqlalchemy.sql import select
|
||||
from sqlalchemy.types import JSON, BigInteger, Boolean, Integer, String
|
||||
|
||||
profile = Table(
|
||||
"chuni_profile_data",
|
||||
|
@ -401,51 +393,9 @@ team = Table(
|
|||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
rating = Table(
|
||||
"chuni_profile_rating",
|
||||
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("type", String(255), nullable=False),
|
||||
Column("index", Integer, nullable=False),
|
||||
Column("musicId", Integer),
|
||||
Column("difficultId", Integer),
|
||||
Column("romVersionCode", Integer),
|
||||
Column("score", Integer),
|
||||
UniqueConstraint("user", "version", "type", "index", name="chuni_profile_rating_best_uk"),
|
||||
=======
|
||||
net_battle = Table(
|
||||
"chuni_profile_net_battle",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
|
||||
Column("isRankUpChallengeFailed", Boolean),
|
||||
Column("highestBattleRankId", Integer),
|
||||
Column("battleIconId", Integer),
|
||||
Column("battleIconNum", Integer),
|
||||
Column("avatarEffectPoint", Integer),
|
||||
>>>>>>> Stashed changes
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
|
||||
class ChuniProfileData(BaseData):
|
||||
async def update_name(self, user_id: int, new_name: str) -> bool:
|
||||
sql = profile.update(profile.c.user == user_id).values(userName=new_name)
|
||||
result = await self.execute(sql)
|
||||
|
||||
if result is None:
|
||||
self.logger.warning(f"Failed to set user {user_id} name to {new_name}")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def put_profile_data(
|
||||
def put_profile_data(
|
||||
self, aime_id: int, version: int, profile_data: Dict
|
||||
) -> Optional[int]:
|
||||
profile_data["user"] = aime_id
|
||||
|
@ -457,45 +407,39 @@ class ChuniProfileData(BaseData):
|
|||
|
||||
sql = insert(profile).values(**profile_data)
|
||||
conflict = sql.on_duplicate_key_update(**profile_data)
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
self.logger.warning(
|
||||
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 result.lastrowid
|
||||
|
||||
async def get_profile_preview(self, aime_id: int, version: int) -> Optional[Row]:
|
||||
def get_profile_preview(self, aime_id: int, version: int) -> Optional[Row]:
|
||||
sql = (
|
||||
select([profile, option])
|
||||
.join(option, profile.c.user == option.c.user)
|
||||
.filter(and_(profile.c.user == aime_id, profile.c.version <= version))
|
||||
).order_by(profile.c.version.desc())
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def get_profile_data(self, aime_id: int, version: int) -> Optional[Row]:
|
||||
sql = (
|
||||
select(profile)
|
||||
.where(
|
||||
and_(
|
||||
profile.c.user == aime_id,
|
||||
profile.c.version <= version,
|
||||
)
|
||||
def get_profile_data(self, aime_id: int, version: int) -> Optional[Row]:
|
||||
sql = select(profile).where(
|
||||
and_(
|
||||
profile.c.user == aime_id,
|
||||
profile.c.version <= version,
|
||||
)
|
||||
.order_by(profile.c.version.desc())
|
||||
)
|
||||
).order_by(profile.c.version.desc())
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def put_profile_data_ex(
|
||||
def put_profile_data_ex(
|
||||
self, aime_id: int, version: int, profile_ex_data: Dict
|
||||
) -> Optional[int]:
|
||||
profile_ex_data["user"] = aime_id
|
||||
|
@ -505,7 +449,7 @@ class ChuniProfileData(BaseData):
|
|||
|
||||
sql = insert(profile_ex).values(**profile_ex_data)
|
||||
conflict = sql.on_duplicate_key_update(**profile_ex_data)
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
self.logger.warning(
|
||||
|
@ -514,31 +458,25 @@ class ChuniProfileData(BaseData):
|
|||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_profile_data_ex(self, aime_id: int, version: int) -> Optional[Row]:
|
||||
sql = (
|
||||
select(profile_ex)
|
||||
.where(
|
||||
and_(
|
||||
profile_ex.c.user == aime_id,
|
||||
profile_ex.c.version <= version,
|
||||
)
|
||||
def get_profile_data_ex(self, aime_id: int, version: int) -> Optional[Row]:
|
||||
sql = select(profile_ex).where(
|
||||
and_(
|
||||
profile_ex.c.user == aime_id,
|
||||
profile_ex.c.version <= version,
|
||||
)
|
||||
.order_by(profile_ex.c.version.desc())
|
||||
)
|
||||
).order_by(profile_ex.c.version.desc())
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def put_profile_option(
|
||||
self, aime_id: int, option_data: Dict
|
||||
) -> Optional[int]:
|
||||
def put_profile_option(self, aime_id: int, option_data: Dict) -> Optional[int]:
|
||||
option_data["user"] = aime_id
|
||||
|
||||
sql = insert(option).values(**option_data)
|
||||
conflict = sql.on_duplicate_key_update(**option_data)
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
self.logger.warning(
|
||||
|
@ -547,22 +485,22 @@ class ChuniProfileData(BaseData):
|
|||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_profile_option(self, aime_id: int) -> Optional[Row]:
|
||||
def get_profile_option(self, aime_id: int) -> Optional[Row]:
|
||||
sql = select(option).where(option.c.user == aime_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def put_profile_option_ex(
|
||||
def put_profile_option_ex(
|
||||
self, aime_id: int, option_ex_data: Dict
|
||||
) -> Optional[int]:
|
||||
option_ex_data["user"] = aime_id
|
||||
|
||||
sql = insert(option_ex).values(**option_ex_data)
|
||||
conflict = sql.on_duplicate_key_update(**option_ex_data)
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
self.logger.warning(
|
||||
|
@ -571,15 +509,15 @@ class ChuniProfileData(BaseData):
|
|||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_profile_option_ex(self, aime_id: int) -> Optional[Row]:
|
||||
def get_profile_option_ex(self, aime_id: int) -> Optional[Row]:
|
||||
sql = select(option_ex).where(option_ex.c.user == aime_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def put_profile_recent_rating(
|
||||
def put_profile_recent_rating(
|
||||
self, aime_id: int, recent_rating_data: List[Dict]
|
||||
) -> Optional[int]:
|
||||
sql = insert(recent_rating).values(
|
||||
|
@ -587,7 +525,7 @@ class ChuniProfileData(BaseData):
|
|||
)
|
||||
conflict = sql.on_duplicate_key_update(recentRating=recent_rating_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
self.logger.warning(
|
||||
f"put_profile_recent_rating: Failed to update! aime_id: {aime_id}"
|
||||
|
@ -595,17 +533,15 @@ class ChuniProfileData(BaseData):
|
|||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_profile_recent_rating(self, aime_id: int) -> Optional[Row]:
|
||||
def get_profile_recent_rating(self, aime_id: int) -> Optional[Row]:
|
||||
sql = select(recent_rating).where(recent_rating.c.user == aime_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def put_profile_activity(
|
||||
self, aime_id: int, activity_data: Dict
|
||||
) -> Optional[int]:
|
||||
def put_profile_activity(self, aime_id: int, activity_data: Dict) -> Optional[int]:
|
||||
# The game just uses "id" but we need to distinguish that from the db column "id"
|
||||
activity_data["user"] = aime_id
|
||||
activity_data["activityId"] = activity_data["id"]
|
||||
|
@ -613,7 +549,7 @@ class ChuniProfileData(BaseData):
|
|||
|
||||
sql = insert(activity).values(**activity_data)
|
||||
conflict = sql.on_duplicate_key_update(**activity_data)
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
self.logger.warning(
|
||||
|
@ -622,28 +558,24 @@ class ChuniProfileData(BaseData):
|
|||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_profile_activity(
|
||||
self, aime_id: int, kind: int
|
||||
) -> Optional[List[Row]]:
|
||||
def get_profile_activity(self, aime_id: int, kind: int) -> Optional[List[Row]]:
|
||||
sql = (
|
||||
select(activity)
|
||||
.where(and_(activity.c.user == aime_id, activity.c.kind == kind))
|
||||
.order_by(activity.c.sortNumber.desc()) # to get the last played track
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_profile_charge(
|
||||
self, aime_id: int, charge_data: Dict
|
||||
) -> Optional[int]:
|
||||
def put_profile_charge(self, aime_id: int, charge_data: Dict) -> Optional[int]:
|
||||
charge_data["user"] = aime_id
|
||||
|
||||
sql = insert(charge).values(**charge_data)
|
||||
conflict = sql.on_duplicate_key_update(**charge_data)
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
self.logger.warning(
|
||||
|
@ -652,42 +584,40 @@ class ChuniProfileData(BaseData):
|
|||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_profile_charge(self, aime_id: int) -> Optional[List[Row]]:
|
||||
def get_profile_charge(self, aime_id: int) -> Optional[List[Row]]:
|
||||
sql = select(charge).where(charge.c.user == aime_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def add_profile_region(self, aime_id: int, region_id: int) -> Optional[int]:
|
||||
def add_profile_region(self, aime_id: int, region_id: int) -> Optional[int]:
|
||||
pass
|
||||
|
||||
async def get_profile_regions(self, aime_id: int) -> Optional[List[Row]]:
|
||||
def get_profile_regions(self, aime_id: int) -> Optional[List[Row]]:
|
||||
pass
|
||||
|
||||
async def put_profile_emoney(
|
||||
self, aime_id: int, emoney_data: Dict
|
||||
) -> Optional[int]:
|
||||
def put_profile_emoney(self, aime_id: int, emoney_data: Dict) -> Optional[int]:
|
||||
emoney_data["user"] = aime_id
|
||||
|
||||
sql = insert(emoney).values(**emoney_data)
|
||||
conflict = sql.on_duplicate_key_update(**emoney_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_profile_emoney(self, aime_id: int) -> Optional[List[Row]]:
|
||||
def get_profile_emoney(self, aime_id: int) -> Optional[List[Row]]:
|
||||
sql = select(emoney).where(emoney.c.user == aime_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_profile_overpower(
|
||||
def put_profile_overpower(
|
||||
self, aime_id: int, overpower_data: Dict
|
||||
) -> Optional[int]:
|
||||
overpower_data["user"] = aime_id
|
||||
|
@ -695,31 +625,33 @@ class ChuniProfileData(BaseData):
|
|||
sql = insert(overpower).values(**overpower_data)
|
||||
conflict = sql.on_duplicate_key_update(**overpower_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_profile_overpower(self, aime_id: int) -> Optional[List[Row]]:
|
||||
def get_profile_overpower(self, aime_id: int) -> Optional[List[Row]]:
|
||||
sql = select(overpower).where(overpower.c.user == aime_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_team_by_id(self, team_id: int) -> Optional[Row]:
|
||||
def get_team_by_id(self, team_id: int) -> Optional[Row]:
|
||||
sql = select(team).where(team.c.id == team_id)
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def get_team_rank(self, team_id: int) -> int:
|
||||
def get_team_rank(self, team_id: int) -> int:
|
||||
# Normal ranking system, likely the one used in the real servers
|
||||
# Query all teams sorted by 'teamPoint'
|
||||
result = await self.execute(select(team.c.id).order_by(team.c.teamPoint.desc()))
|
||||
result = self.execute(
|
||||
select(team.c.id).order_by(team.c.teamPoint.desc())
|
||||
)
|
||||
|
||||
# Get the rank of the team with the given team_id
|
||||
rank = None
|
||||
|
@ -734,86 +666,40 @@ class ChuniProfileData(BaseData):
|
|||
# RIP scaled team ranking. Gone, but forgotten
|
||||
# def get_team_rank_scaled(self, team_id: int) -> int:
|
||||
|
||||
async def update_team(self, team_id: int, team_data: Dict) -> bool:
|
||||
def update_team(self, team_id: int, team_data: Dict) -> bool:
|
||||
team_data["id"] = team_id
|
||||
|
||||
sql = insert(team).values(**team_data)
|
||||
conflict = sql.on_duplicate_key_update(**team_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
self.logger.warn(f"update_team: Failed to update team! team id: {team_id}")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def get_rival(self, rival_id: int) -> Optional[Row]:
|
||||
sql = select(profile).where(profile.c.user == rival_id)
|
||||
result = await self.execute(sql)
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def get_overview(self) -> Dict:
|
||||
# Fetch and add up all the playcounts
|
||||
playcount_sql = await self.execute(select(profile.c.playCount))
|
||||
|
||||
if playcount_sql is None:
|
||||
self.logger.warn(f"get_overview: Couldn't pull playcounts")
|
||||
return 0
|
||||
|
||||
total_play_count = 0
|
||||
for row in playcount_sql:
|
||||
total_play_count += row[0]
|
||||
<<<<<<< Updated upstream
|
||||
return {
|
||||
"total_play_count": total_play_count
|
||||
}
|
||||
|
||||
async def put_profile_rating(
|
||||
self,
|
||||
aime_id: int,
|
||||
version: int,
|
||||
rating_type: str,
|
||||
rating_data: List[Dict],
|
||||
):
|
||||
inserted_values = [
|
||||
{"user": aime_id, "version": version, "type": rating_type, "index": i, **x}
|
||||
for (i, x) in enumerate(rating_data)
|
||||
]
|
||||
sql = insert(rating).values(inserted_values)
|
||||
update_dict = {x.name: x for x in sql.inserted if x.name != "id"}
|
||||
sql = sql.on_duplicate_key_update(**update_dict)
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
self.logger.warn(
|
||||
f"put_profile_rating: Could not insert {rating_type}, aime_id: {aime_id}",
|
||||
f"update_team: Failed to update team! team id: {team_id}"
|
||||
)
|
||||
return
|
||||
|
||||
=======
|
||||
return {"total_play_count": total_play_count}
|
||||
|
||||
async def get_net_battle(self, aime_id: int) -> Optional[Row]:
|
||||
sql = select(net_battle).where(net_battle.c.user == aime_id)
|
||||
result = await self.execute(sql)
|
||||
return False
|
||||
return True
|
||||
def get_rival(self, rival_id: int) -> Optional[Row]:
|
||||
sql = select(profile).where(profile.c.user == rival_id)
|
||||
result = self.execute(sql)
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
def get_overview(self) -> Dict:
|
||||
# Fetch and add up all the playcounts
|
||||
playcount_sql = self.execute(select(profile.c.playCount))
|
||||
|
||||
async def put_net_battle(
|
||||
self, aime_id: int, net_battle_data: Dict
|
||||
) -> Optional[int]:
|
||||
net_battle_data["user"] = aime_id
|
||||
if playcount_sql is None:
|
||||
self.logger.warn(
|
||||
f"get_overview: Couldn't pull playcounts"
|
||||
)
|
||||
return 0
|
||||
|
||||
sql = insert(net_battle).values(**net_battle_data)
|
||||
conflict = sql.on_duplicate_key_update(**net_battle_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
>>>>>>> Stashed changes
|
||||
return result.lastrowid
|
||||
total_play_count = 0;
|
||||
for row in playcount_sql:
|
||||
total_play_count += row[0]
|
||||
return {
|
||||
"total_play_count": total_play_count
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
from typing import Dict, List, Optional
|
||||
|
||||
from core.data.schema import BaseData, metadata
|
||||
from sqlalchemy import Column, Table, UniqueConstraint
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
from sqlalchemy.engine import Row
|
||||
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
|
||||
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger
|
||||
from sqlalchemy.engine.base import Connection
|
||||
from sqlalchemy.schema import ForeignKey
|
||||
from sqlalchemy.engine import Row
|
||||
from sqlalchemy.sql import func, select
|
||||
from sqlalchemy.types import Boolean, Integer, String
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
from sqlalchemy.sql.expression import exists
|
||||
from core.data.schema import BaseData, metadata
|
||||
|
||||
course = Table(
|
||||
"chuni_score_course",
|
||||
|
@ -136,62 +137,60 @@ playlog = Table(
|
|||
Column("regionId", Integer),
|
||||
Column("machineType", Integer),
|
||||
Column("ticketId", Integer),
|
||||
mysql_charset="utf8mb4",
|
||||
mysql_charset="utf8mb4"
|
||||
)
|
||||
|
||||
|
||||
class ChuniScoreData(BaseData):
|
||||
async def get_courses(self, aime_id: int) -> Optional[Row]:
|
||||
def get_courses(self, aime_id: int) -> Optional[Row]:
|
||||
sql = select(course).where(course.c.user == aime_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]:
|
||||
def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]:
|
||||
course_data["user"] = aime_id
|
||||
course_data = self.fix_bools(course_data)
|
||||
|
||||
sql = insert(course).values(**course_data)
|
||||
conflict = sql.on_duplicate_key_update(**course_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_scores(self, aime_id: int) -> Optional[Row]:
|
||||
def get_scores(self, aime_id: int) -> Optional[Row]:
|
||||
sql = select(best_score).where(best_score.c.user == aime_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_score(self, aime_id: int, score_data: Dict) -> Optional[int]:
|
||||
def put_score(self, aime_id: int, score_data: Dict) -> Optional[int]:
|
||||
score_data["user"] = aime_id
|
||||
score_data = self.fix_bools(score_data)
|
||||
|
||||
sql = insert(best_score).values(**score_data)
|
||||
conflict = sql.on_duplicate_key_update(**score_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_playlogs(self, aime_id: int) -> Optional[Row]:
|
||||
def get_playlogs(self, aime_id: int) -> Optional[Row]:
|
||||
sql = select(playlog).where(playlog.c.user == aime_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_playlog(
|
||||
self, aime_id: int, playlog_data: Dict, version: int
|
||||
) -> Optional[int]:
|
||||
def put_playlog(self, aime_id: int, playlog_data: Dict, version: int) -> Optional[int]:
|
||||
# Calculate the ROM version that should be inserted into the DB, based on the version of the ggame being inserted
|
||||
# We only need from Version 10 (Plost) and back, as newer versions include romVersion in their upsert
|
||||
# This matters both for gameRankings, as well as a future DB update to keep version data separate
|
||||
|
@ -206,7 +205,7 @@ class ChuniScoreData(BaseData):
|
|||
3: "1.15.0",
|
||||
2: "1.10.0",
|
||||
1: "1.05.0",
|
||||
0: "1.00.0",
|
||||
0: "1.00.0"
|
||||
}
|
||||
|
||||
playlog_data["user"] = aime_id
|
||||
|
@ -217,12 +216,12 @@ class ChuniScoreData(BaseData):
|
|||
sql = insert(playlog).values(**playlog_data)
|
||||
conflict = sql.on_duplicate_key_update(**playlog_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_rankings(self, version: int) -> Optional[List[Dict]]:
|
||||
def get_rankings(self, version: int) -> Optional[List[Dict]]:
|
||||
# Calculates the ROM version that should be fetched for rankings, based on the game version being retrieved
|
||||
# This prevents tracks that are not accessible in your version from counting towards the 10 results
|
||||
romVer = {
|
||||
|
@ -239,24 +238,10 @@ class ChuniScoreData(BaseData):
|
|||
3: "1.15%",
|
||||
2: "1.10%",
|
||||
1: "1.05%",
|
||||
0: "1.00%",
|
||||
0: "1.00%"
|
||||
}
|
||||
sql = (
|
||||
select(
|
||||
[
|
||||
playlog.c.musicId.label("id"),
|
||||
func.count(playlog.c.musicId).label("point"),
|
||||
]
|
||||
)
|
||||
.where(
|
||||
(playlog.c.level != 4)
|
||||
& (playlog.c.romVersion.like(romVer.get(version, "%")))
|
||||
)
|
||||
.group_by(playlog.c.musicId)
|
||||
.order_by(func.count(playlog.c.musicId).desc())
|
||||
.limit(10)
|
||||
)
|
||||
result = await self.execute(sql)
|
||||
sql = select([playlog.c.musicId.label('id'), func.count(playlog.c.musicId).label('point')]).where((playlog.c.level != 4) & (playlog.c.romVersion.like(romVer.get(version, "%")))).group_by(playlog.c.musicId).order_by(func.count(playlog.c.musicId).desc()).limit(10)
|
||||
result = self.execute(sql)
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
|
@ -264,10 +249,10 @@ class ChuniScoreData(BaseData):
|
|||
rows = result.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
async def get_rival_music(self, rival_id: int) -> Optional[List[Dict]]:
|
||||
def get_rival_music(self, rival_id: int) -> Optional[List[Dict]]:
|
||||
sql = select(best_score).where(best_score.c.user == rival_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
from typing import Dict, List, Optional
|
||||
|
||||
from core.data.schema import BaseData, metadata
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
ForeignKeyConstraint,
|
||||
PrimaryKeyConstraint,
|
||||
Table,
|
||||
Column,
|
||||
UniqueConstraint,
|
||||
PrimaryKeyConstraint,
|
||||
and_,
|
||||
)
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, Float
|
||||
from sqlalchemy.engine.base import Connection
|
||||
from sqlalchemy.engine import Row
|
||||
from sqlalchemy.schema import ForeignKey
|
||||
from sqlalchemy.sql import func, select
|
||||
from sqlalchemy.types import TIMESTAMP, Boolean, Float, Integer, String
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
from datetime import datetime
|
||||
|
||||
from core.data.schema import BaseData, metadata
|
||||
|
||||
events = Table(
|
||||
"chuni_static_events",
|
||||
|
@ -172,7 +175,7 @@ login_bonus = Table(
|
|||
|
||||
|
||||
class ChuniStaticData(BaseData):
|
||||
async def put_login_bonus(
|
||||
def put_login_bonus(
|
||||
self,
|
||||
version: int,
|
||||
preset_id: int,
|
||||
|
@ -204,12 +207,12 @@ class ChuniStaticData(BaseData):
|
|||
loginBonusCategoryType=login_bonus_category_type,
|
||||
)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_login_bonus(
|
||||
def get_login_bonus(
|
||||
self,
|
||||
version: int,
|
||||
preset_id: int,
|
||||
|
@ -221,12 +224,12 @@ class ChuniStaticData(BaseData):
|
|||
)
|
||||
).order_by(login_bonus.c.needLoginDayCount.desc())
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_login_bonus_by_required_days(
|
||||
def get_login_bonus_by_required_days(
|
||||
self, version: int, preset_id: int, need_login_day_count: int
|
||||
) -> Optional[Row]:
|
||||
sql = login_bonus.select(
|
||||
|
@ -237,12 +240,12 @@ class ChuniStaticData(BaseData):
|
|||
)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def put_login_bonus_preset(
|
||||
def put_login_bonus_preset(
|
||||
self, version: int, preset_id: int, preset_name: str, is_enabled: bool
|
||||
) -> Optional[int]:
|
||||
sql = insert(login_bonus_preset).values(
|
||||
|
@ -256,12 +259,12 @@ class ChuniStaticData(BaseData):
|
|||
presetName=preset_name, isEnabled=is_enabled
|
||||
)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_login_bonus_presets(
|
||||
def get_login_bonus_presets(
|
||||
self, version: int, is_enabled: bool = True
|
||||
) -> Optional[List[Row]]:
|
||||
sql = login_bonus_preset.select(
|
||||
|
@ -271,12 +274,12 @@ class ChuniStaticData(BaseData):
|
|||
)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_event(
|
||||
def put_event(
|
||||
self, version: int, event_id: int, type: int, name: str
|
||||
) -> Optional[int]:
|
||||
sql = insert(events).values(
|
||||
|
@ -285,19 +288,19 @@ class ChuniStaticData(BaseData):
|
|||
|
||||
conflict = sql.on_duplicate_key_update(name=name)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def update_event(
|
||||
def update_event(
|
||||
self, version: int, event_id: int, enabled: bool
|
||||
) -> Optional[bool]:
|
||||
sql = events.update(
|
||||
and_(events.c.version == version, events.c.eventId == event_id)
|
||||
).values(enabled=enabled)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
self.logger.warning(
|
||||
f"update_event: failed to update event! version: {version}, event_id: {event_id}, enabled: {enabled}"
|
||||
|
@ -312,35 +315,35 @@ class ChuniStaticData(BaseData):
|
|||
return None
|
||||
return event["enabled"]
|
||||
|
||||
async def get_event(self, version: int, event_id: int) -> Optional[Row]:
|
||||
def get_event(self, version: int, event_id: int) -> Optional[Row]:
|
||||
sql = select(events).where(
|
||||
and_(events.c.version == version, events.c.eventId == event_id)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def get_enabled_events(self, version: int) -> Optional[List[Row]]:
|
||||
def get_enabled_events(self, version: int) -> Optional[List[Row]]:
|
||||
sql = select(events).where(
|
||||
and_(events.c.version == version, events.c.enabled == True)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_events(self, version: int) -> Optional[List[Row]]:
|
||||
def get_events(self, version: int) -> Optional[List[Row]]:
|
||||
sql = select(events).where(events.c.version == version)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def put_music(
|
||||
def put_music(
|
||||
self,
|
||||
version: int,
|
||||
song_id: int,
|
||||
|
@ -373,12 +376,12 @@ class ChuniStaticData(BaseData):
|
|||
worldsEndTag=we_tag,
|
||||
)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def put_charge(
|
||||
def put_charge(
|
||||
self,
|
||||
version: int,
|
||||
charge_id: int,
|
||||
|
@ -403,38 +406,38 @@ class ChuniStaticData(BaseData):
|
|||
sellingAppeal=selling_appeal,
|
||||
)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_enabled_charges(self, version: int) -> Optional[List[Row]]:
|
||||
def get_enabled_charges(self, version: int) -> Optional[List[Row]]:
|
||||
sql = select(charge).where(
|
||||
and_(charge.c.version == version, charge.c.enabled == True)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_charges(self, version: int) -> Optional[List[Row]]:
|
||||
def get_charges(self, version: int) -> Optional[List[Row]]:
|
||||
sql = select(charge).where(charge.c.version == version)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_music(self, version: int) -> Optional[List[Row]]:
|
||||
def get_music(self, version: int) -> Optional[List[Row]]:
|
||||
sql = music.select(music.c.version <= version)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_music_chart(
|
||||
def get_music_chart(
|
||||
self, version: int, song_id: int, chart_id: int
|
||||
) -> Optional[List[Row]]:
|
||||
sql = select(music).where(
|
||||
|
@ -445,20 +448,21 @@ class ChuniStaticData(BaseData):
|
|||
)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def get_song(self, music_id: int) -> Optional[Row]:
|
||||
def get_song(self, music_id: int) -> Optional[Row]:
|
||||
sql = music.select(music.c.id == music_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def put_avatar(
|
||||
|
||||
def put_avatar(
|
||||
self,
|
||||
version: int,
|
||||
avatarAccessoryId: int,
|
||||
|
@ -483,12 +487,12 @@ class ChuniStaticData(BaseData):
|
|||
texturePath=texturePath,
|
||||
)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def put_gacha(
|
||||
def put_gacha(
|
||||
self,
|
||||
version: int,
|
||||
gacha_id: int,
|
||||
|
@ -509,33 +513,33 @@ class ChuniStaticData(BaseData):
|
|||
**gacha_data,
|
||||
)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
self.logger.warning(f"Failed to insert gacha! gacha_id {gacha_id}")
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_gachas(self, version: int) -> Optional[List[Dict]]:
|
||||
def get_gachas(self, version: int) -> Optional[List[Dict]]:
|
||||
sql = gachas.select(gachas.c.version <= version).order_by(
|
||||
gachas.c.gachaId.asc()
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]:
|
||||
def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]:
|
||||
sql = gachas.select(
|
||||
and_(gachas.c.version <= version, gachas.c.gachaId == gacha_id)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def put_gacha_card(
|
||||
def put_gacha_card(
|
||||
self, gacha_id: int, card_id: int, **gacha_card
|
||||
) -> Optional[int]:
|
||||
sql = insert(gacha_cards).values(gachaId=gacha_id, cardId=card_id, **gacha_card)
|
||||
|
@ -544,21 +548,21 @@ class ChuniStaticData(BaseData):
|
|||
gachaId=gacha_id, cardId=card_id, **gacha_card
|
||||
)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
self.logger.warning(f"Failed to insert gacha card! gacha_id {gacha_id}")
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]:
|
||||
def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]:
|
||||
sql = gacha_cards.select(gacha_cards.c.gachaId == gacha_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_gacha_card_by_character(
|
||||
def get_gacha_card_by_character(
|
||||
self, gacha_id: int, chara_id: int
|
||||
) -> Optional[Dict]:
|
||||
sql_sub = (
|
||||
|
@ -570,26 +574,26 @@ class ChuniStaticData(BaseData):
|
|||
and_(gacha_cards.c.gachaId == gacha_id, gacha_cards.c.cardId == sql_sub)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]:
|
||||
def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]:
|
||||
sql = insert(cards).values(version=version, cardId=card_id, **card_data)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(**card_data)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
result = self.execute(conflict)
|
||||
if result is None:
|
||||
self.logger.warning(f"Failed to insert card! card_id {card_id}")
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def get_card(self, version: int, card_id: int) -> Optional[Dict]:
|
||||
def get_card(self, version: int, card_id: int) -> Optional[Dict]:
|
||||
sql = cards.select(and_(cards.c.version <= version, cards.c.cardId == card_id))
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
return result.fetchone()
|
|
@ -2,8 +2,8 @@ from typing import Dict
|
|||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.base import ChuniBase
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.config import ChuniConfig
|
||||
|
||||
|
||||
class ChuniStar(ChuniBase):
|
||||
|
@ -11,7 +11,7 @@ class ChuniStar(ChuniBase):
|
|||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = ChuniConstants.VER_CHUNITHM_STAR
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = await super().handle_get_game_setting_api_request(data)
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["dataVersion"] = "1.20.00"
|
||||
return ret
|
||||
|
|
|
@ -2,8 +2,8 @@ from typing import Dict
|
|||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.base import ChuniBase
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.config import ChuniConfig
|
||||
|
||||
|
||||
class ChuniStarPlus(ChuniBase):
|
||||
|
@ -11,7 +11,7 @@ class ChuniStarPlus(ChuniBase):
|
|||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = ChuniConstants.VER_CHUNITHM_STAR_PLUS
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = await super().handle_get_game_setting_api_request(data)
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["dataVersion"] = "1.25.00"
|
||||
return ret
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
from typing import Dict
|
||||
from typing import Dict, Any
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.newplus import ChuniNewPlus
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.config import ChuniConfig
|
||||
|
||||
|
||||
class ChuniSun(ChuniNewPlus):
|
||||
|
@ -11,9 +11,27 @@ class ChuniSun(ChuniNewPlus):
|
|||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = ChuniConstants.VER_CHUNITHM_SUN
|
||||
|
||||
async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
user_data = await super().handle_cm_get_user_preview_api_request(data)
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)["rom"]
|
||||
ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"]
|
||||
ret["gameSetting"][
|
||||
"matchingUri"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
|
||||
ret["gameSetting"][
|
||||
"matchingUriX"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
|
||||
ret["gameSetting"][
|
||||
"udpHolePunchUri"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
|
||||
ret["gameSetting"][
|
||||
"reflectorUri"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
|
||||
return ret
|
||||
|
||||
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
user_data = super().handle_cm_get_user_preview_api_request(data)
|
||||
|
||||
# hardcode lastDataVersion for CardMaker 1.35 A032
|
||||
user_data["lastDataVersion"] = "2.10.00"
|
||||
return user_data
|
||||
return user_data
|
|
@ -1,9 +1,9 @@
|
|||
from typing import Dict
|
||||
from typing import Dict, Any
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.sun import ChuniSun
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.config import ChuniConfig
|
||||
|
||||
|
||||
class ChuniSunPlus(ChuniSun):
|
||||
|
@ -11,8 +11,26 @@ class ChuniSunPlus(ChuniSun):
|
|||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
||||
|
||||
async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
user_data = await super().handle_cm_get_user_preview_api_request(data)
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)["rom"]
|
||||
ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"]
|
||||
ret["gameSetting"][
|
||||
"matchingUri"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/215/ChuniServlet/"
|
||||
ret["gameSetting"][
|
||||
"matchingUriX"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/215/ChuniServlet/"
|
||||
ret["gameSetting"][
|
||||
"udpHolePunchUri"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/215/ChuniServlet/"
|
||||
ret["gameSetting"][
|
||||
"reflectorUri"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/215/ChuniServlet/"
|
||||
return ret
|
||||
|
||||
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
user_data = super().handle_cm_get_user_preview_api_request(data)
|
||||
|
||||
# I don't know if lastDataVersion is going to matter, I don't think CardMaker 1.35 works this far up
|
||||
user_data["lastDataVersion"] = "2.15.00"
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
{% extends "core/templates/index.jinja" %}
|
||||
{% block content %}
|
||||
<h1>Chunithm</h1>
|
||||
{% if profile is defined and profile is not none and profile.id > 0 %}
|
||||
<script type="text/javascript">
|
||||
function toggle_new_name_form() {
|
||||
let frm = document.getElementById("new_name_form");
|
||||
let btn = document.getElementById("btn_toggle_form");
|
||||
|
||||
if (frm.style['display'] != "") {
|
||||
frm.style['display'] = "";
|
||||
frm.style['max-height'] = "";
|
||||
btn.innerText = "Cancel";
|
||||
} else {
|
||||
frm.style['display'] = "none";
|
||||
frm.style['max-height'] = "0px";
|
||||
btn.innerText = "Edit";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<h3>Profile for {{ profile.userName }} <button onclick="toggle_new_name_form()" class="btn btn-secondary" id="btn_toggle_form">Edit</button></h3>
|
||||
{% if error is defined %}
|
||||
{% include "core/templates/widgets/err_banner.jinja" %}
|
||||
{% endif %}
|
||||
{% if success is defined and success == 1 %}
|
||||
<div style="background-color: #00AA00; padding: 20px; margin-bottom: 10px; width: 15%;">
|
||||
Update successful
|
||||
</div>
|
||||
{% endif %}
|
||||
<form style="max-width: 33%; display: none; max-height: 0px;" action="/game/chuni/update.name" method="post" id="new_name_form">
|
||||
<div class="mb-3">
|
||||
<label for="new_name" class="form-label">New Trainer Name</label>
|
||||
<input type="text" class="form-control" id="new_name" name="new_name" aria-describedby="new_name_help" maxlength="14">
|
||||
<div id="new_name_help" class="form-text">Must be 14 characters or less</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
{% elif sesh is defined and sesh is not none and sesh.user_id > 0 %}
|
||||
No profile information found for this account.
|
||||
{% else %}
|
||||
Login to view profile information.
|
||||
{% endif %}
|
||||
{% endblock content %}
|
|
@ -1,9 +1,12 @@
|
|||
from titles.cm.const import CardMakerConstants
|
||||
from titles.cm.database import CardMakerData
|
||||
from titles.cm.index import CardMakerServlet
|
||||
from titles.cm.const import CardMakerConstants
|
||||
from titles.cm.read import CardMakerReader
|
||||
from titles.cm.database import CardMakerData
|
||||
|
||||
index = CardMakerServlet
|
||||
reader = CardMakerReader
|
||||
database = CardMakerData
|
||||
|
||||
game_codes = [CardMakerConstants.GAME_CODE]
|
||||
|
||||
current_schema_version = 1
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
from datetime import date, datetime, timedelta
|
||||
from typing import Any, Dict, List
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict
|
||||
from enum import Enum
|
||||
|
||||
import pytz
|
||||
from core.config import CoreConfig
|
||||
from core.utils import Utils
|
||||
from titles.cm.config import CardMakerConfig
|
||||
from core.data.cache import cached
|
||||
from titles.cm.const import CardMakerConstants
|
||||
from titles.cm.config import CardMakerConfig
|
||||
|
||||
|
||||
class CardMakerBase:
|
||||
|
@ -26,14 +29,11 @@ class CardMakerBase:
|
|||
def _parse_int_ver(version: str) -> str:
|
||||
return version.replace(".", "")[:3]
|
||||
|
||||
async def handle_get_game_connect_api_request(self, data: Dict) -> Dict:
|
||||
if (
|
||||
not self.core_cfg.server.is_using_proxy
|
||||
and Utils.get_title_port(self.core_cfg) != 80
|
||||
):
|
||||
uri = f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}"
|
||||
def handle_get_game_connect_api_request(self, data: Dict) -> Dict:
|
||||
if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80:
|
||||
uri = f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}"
|
||||
else:
|
||||
uri = f"http://{self.core_cfg.server.hostname}"
|
||||
uri = f"http://{self.core_cfg.title.hostname}"
|
||||
|
||||
# grab the dict with all games version numbers from user config
|
||||
games_ver = self.game_cfg.version.version(self.version)
|
||||
|
@ -51,7 +51,7 @@ class CardMakerBase:
|
|||
{
|
||||
"modelKind": 1,
|
||||
"type": 1,
|
||||
"titleUri": f"{uri}/SDEZ/{self._parse_int_ver(games_ver['maimai'])}/Maimai2Servlet/",
|
||||
"titleUri": f"{uri}/{self._parse_int_ver(games_ver['maimai'])}/Maimai2Servlet/",
|
||||
},
|
||||
# ONGEKI
|
||||
{
|
||||
|
@ -62,12 +62,9 @@ class CardMakerBase:
|
|||
],
|
||||
}
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
# if reboot start/end time is not defined use the default behavior of being a few hours ago
|
||||
if (
|
||||
self.core_cfg.title.reboot_start_time == ""
|
||||
or self.core_cfg.title.reboot_end_time == ""
|
||||
):
|
||||
if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
|
||||
reboot_start = datetime.strftime(
|
||||
datetime.utcnow() + timedelta(hours=6), self.date_time_format
|
||||
)
|
||||
|
@ -76,29 +73,15 @@ class CardMakerBase:
|
|||
)
|
||||
else:
|
||||
# get current datetime in JST
|
||||
current_jst = datetime.now(pytz.timezone("Asia/Tokyo")).date()
|
||||
current_jst = datetime.now(pytz.timezone('Asia/Tokyo')).date()
|
||||
|
||||
# parse config start/end times into datetime
|
||||
reboot_start_time = datetime.strptime(
|
||||
self.core_cfg.title.reboot_start_time, "%H:%M"
|
||||
)
|
||||
reboot_end_time = datetime.strptime(
|
||||
self.core_cfg.title.reboot_end_time, "%H:%M"
|
||||
)
|
||||
reboot_start_time = datetime.strptime(self.core_cfg.title.reboot_start_time, "%H:%M")
|
||||
reboot_end_time = datetime.strptime(self.core_cfg.title.reboot_end_time, "%H:%M")
|
||||
|
||||
# offset datetimes with current date/time
|
||||
reboot_start_time = reboot_start_time.replace(
|
||||
year=current_jst.year,
|
||||
month=current_jst.month,
|
||||
day=current_jst.day,
|
||||
tzinfo=pytz.timezone("Asia/Tokyo"),
|
||||
)
|
||||
reboot_end_time = reboot_end_time.replace(
|
||||
year=current_jst.year,
|
||||
month=current_jst.month,
|
||||
day=current_jst.day,
|
||||
tzinfo=pytz.timezone("Asia/Tokyo"),
|
||||
)
|
||||
reboot_start_time = reboot_start_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo'))
|
||||
reboot_end_time = reboot_end_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo'))
|
||||
|
||||
# create strings for use in gameSetting
|
||||
reboot_start = reboot_start_time.strftime(self.date_time_format)
|
||||
|
@ -127,11 +110,11 @@ class CardMakerBase:
|
|||
"isAou": False,
|
||||
}
|
||||
|
||||
async def handle_get_client_bookkeeping_api_request(self, data: Dict) -> Dict:
|
||||
def handle_get_client_bookkeeping_api_request(self, data: Dict) -> Dict:
|
||||
return {"placeId": data["placeId"], "length": 0, "clientBookkeepingList": []}
|
||||
|
||||
async def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict:
|
||||
def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": 1, "apiName": "UpsertClientSettingApi"}
|
||||
|
||||
async def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict:
|
||||
def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": 1, "apiName": "UpsertClientBookkeepingApi"}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from typing import Dict
|
||||
|
||||
from core.config import CoreConfig
|
||||
from core.data.cache import cached
|
||||
from titles.cm.base import CardMakerBase
|
||||
from titles.cm.config import CardMakerConfig
|
||||
from titles.cm.const import CardMakerConstants
|
||||
from titles.cm.config import CardMakerConfig
|
||||
|
||||
|
||||
class CardMaker135(CardMakerBase):
|
||||
|
@ -11,7 +12,7 @@ class CardMaker135(CardMakerBase):
|
|||
super().__init__(core_cfg, game_cfg)
|
||||
self.version = CardMakerConstants.VER_CARD_MAKER_135
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = await super().handle_get_game_setting_api_request(data)
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["dataVersion"] = "1.35.00"
|
||||
return ret
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from typing import Dict
|
||||
|
||||
from core.config import CoreConfig
|
||||
|
||||
|
||||
|
@ -32,13 +31,18 @@ class CardMakerVersionConfig:
|
|||
1: {"ongeki": 1.30.01, "chuni": 2.00.00, "maimai": 1.20.00}
|
||||
"""
|
||||
return CoreConfig.get_config_field(
|
||||
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"},
|
||||
},
|
||||
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]
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from core.config import CoreConfig
|
||||
from core.data import Data
|
||||
from core.config import CoreConfig
|
||||
|
||||
|
||||
class CardMakerData(Data):
|
||||
|
|
|
@ -1,25 +1,23 @@
|
|||
import json
|
||||
import logging
|
||||
import string
|
||||
import zlib
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from os import path
|
||||
from typing import List
|
||||
|
||||
import coloredlogs
|
||||
import inflection
|
||||
import yaml
|
||||
from core.config import CoreConfig
|
||||
from core.title import BaseServlet
|
||||
from core.utils import Utils
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
from starlette.routing import Route
|
||||
import string
|
||||
import logging
|
||||
import coloredlogs
|
||||
import zlib
|
||||
|
||||
from .base import CardMakerBase
|
||||
from .cm135 import CardMaker135
|
||||
from os import path
|
||||
from typing import Tuple, List, Dict
|
||||
from twisted.web.http import Request
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
from core.config import CoreConfig
|
||||
from core.utils import Utils
|
||||
from core.title import BaseServlet
|
||||
from .config import CardMakerConfig
|
||||
from .const import CardMakerConstants
|
||||
from .base import CardMakerBase
|
||||
from .cm135 import CardMaker135
|
||||
|
||||
|
||||
class CardMakerServlet(BaseServlet):
|
||||
|
@ -33,7 +31,7 @@ class CardMakerServlet(BaseServlet):
|
|||
|
||||
self.versions = [
|
||||
CardMakerBase(core_cfg, self.game_cfg),
|
||||
CardMaker135(core_cfg, self.game_cfg),
|
||||
CardMaker135(core_cfg, self.game_cfg)
|
||||
]
|
||||
|
||||
self.logger = logging.getLogger("cardmaker")
|
||||
|
@ -58,7 +56,7 @@ class CardMakerServlet(BaseServlet):
|
|||
coloredlogs.install(
|
||||
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def is_game_enabled(
|
||||
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
|
||||
|
@ -73,18 +71,17 @@ class CardMakerServlet(BaseServlet):
|
|||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]:
|
||||
return (
|
||||
[],
|
||||
[("render_POST", "/SDED/{version}/{endpoint}", {})]
|
||||
)
|
||||
|
||||
def get_routes(self) -> List[Route]:
|
||||
return [
|
||||
Route(
|
||||
"/SDED/{version:int}/{endpoint:str}", self.render_POST, methods=["POST"]
|
||||
)
|
||||
]
|
||||
|
||||
async def render_POST(self, request: Request) -> bytes:
|
||||
version: int = request.path_params.get("version")
|
||||
endpoint: str = request.path_params.get("endpoint")
|
||||
req_raw = await request.body()
|
||||
def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes:
|
||||
version = int(matchers['version'])
|
||||
endpoint = matchers['endpoint']
|
||||
req_raw = request.content.getvalue()
|
||||
internal_ver = 0
|
||||
client_ip = Utils.get_ip_addr(request)
|
||||
|
||||
|
@ -106,7 +103,7 @@ class CardMakerServlet(BaseServlet):
|
|||
self.logger.error(
|
||||
f"Failed to decompress v{version} {endpoint} request -> {e}"
|
||||
)
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
return zlib.compress(b'{"stat": "0"}')
|
||||
|
||||
req_data = json.loads(unzip)
|
||||
|
||||
|
@ -117,22 +114,20 @@ class CardMakerServlet(BaseServlet):
|
|||
|
||||
if not hasattr(self.versions[internal_ver], func_to_find):
|
||||
self.logger.warning(f"Unhandled v{version} request {endpoint}")
|
||||
return Response(zlib.compress(b'{"returnCode": 1}'))
|
||||
return zlib.compress(b'{"returnCode": 1}')
|
||||
|
||||
try:
|
||||
handler = getattr(self.versions[internal_ver], func_to_find)
|
||||
resp = await handler(req_data)
|
||||
resp = handler(req_data)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
|
||||
raise
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
return zlib.compress(b'{"stat": "0"}')
|
||||
|
||||
if resp is None:
|
||||
resp = {"returnCode": 1}
|
||||
|
||||
self.logger.debug(f"Response {resp}")
|
||||
|
||||
return Response(
|
||||
zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
|
||||
)
|
||||
return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import csv
|
||||
from decimal import Decimal
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import csv
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from core.config import CoreConfig
|
||||
from read import BaseReader
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.database import ChuniData
|
||||
from titles.cm.const import CardMakerConstants
|
||||
from titles.mai2.const import Mai2Constants
|
||||
from titles.mai2.database import Mai2Data
|
||||
from titles.ongeki.const import OngekiConstants
|
||||
from core.config import CoreConfig
|
||||
from titles.ongeki.database import OngekiData
|
||||
from titles.cm.const import CardMakerConstants
|
||||
from titles.ongeki.const import OngekiConstants
|
||||
from titles.ongeki.config import OngekiConfig
|
||||
from titles.mai2.database import Mai2Data
|
||||
from titles.mai2.const import Mai2Constants
|
||||
from titles.chuni.database import ChuniData
|
||||
from titles.chuni.const import ChuniConstants
|
||||
|
||||
|
||||
class CardMakerReader(BaseReader):
|
||||
|
@ -47,7 +50,7 @@ class CardMakerReader(BaseReader):
|
|||
):
|
||||
return f"{root}/{dir}"
|
||||
|
||||
async def read(self) -> None:
|
||||
def read(self) -> None:
|
||||
static_datas = {
|
||||
"static_gachas.csv": "read_ongeki_gacha_csv",
|
||||
"static_gacha_cards.csv": "read_ongeki_gacha_card_csv",
|
||||
|
@ -56,14 +59,14 @@ class CardMakerReader(BaseReader):
|
|||
if self.bin_dir is not None:
|
||||
data_dir = self._get_card_maker_directory(self.bin_dir)
|
||||
|
||||
await self.read_chuni_card(f"{data_dir}/CHU/Data/A000/card")
|
||||
await self.read_chuni_gacha(f"{data_dir}/CHU/Data/A000/gacha")
|
||||
self.read_chuni_card(f"{data_dir}/CHU/Data/A000/card")
|
||||
self.read_chuni_gacha(f"{data_dir}/CHU/Data/A000/gacha")
|
||||
|
||||
await self.read_mai2_card(f"{data_dir}/MAI/Data/A000/card")
|
||||
self.read_mai2_card(f"{data_dir}/MAI/Data/A000/card")
|
||||
for file, func in static_datas.items():
|
||||
if os.path.exists(f"{self.bin_dir}/MU3/{file}"):
|
||||
read_csv = getattr(CardMakerReader, func)
|
||||
await read_csv(self, f"{self.bin_dir}/MU3/{file}")
|
||||
read_csv(self, f"{self.bin_dir}/MU3/{file}")
|
||||
else:
|
||||
self.logger.warning(
|
||||
f"Couldn't find {file} file in {self.bin_dir}, skipping"
|
||||
|
@ -75,12 +78,12 @@ class CardMakerReader(BaseReader):
|
|||
# ONGEKI (MU3) cnnot easily access the bin data(A000.pac)
|
||||
# so only opt_dir will work for now
|
||||
for dir in data_dirs:
|
||||
await self.read_chuni_card(f"{dir}/CHU/card")
|
||||
await self.read_chuni_gacha(f"{dir}/CHU/gacha")
|
||||
await self.read_mai2_card(f"{dir}/MAI/card")
|
||||
await self.read_ongeki_gacha(f"{dir}/MU3/gacha")
|
||||
self.read_chuni_card(f"{dir}/CHU/card")
|
||||
self.read_chuni_gacha(f"{dir}/CHU/gacha")
|
||||
self.read_mai2_card(f"{dir}/MAI/card")
|
||||
self.read_ongeki_gacha(f"{dir}/MU3/gacha")
|
||||
|
||||
async def read_chuni_card(self, base_dir: str) -> None:
|
||||
def read_chuni_card(self, base_dir: str) -> None:
|
||||
self.logger.info(f"Reading cards from {base_dir}...")
|
||||
|
||||
version_ids = {
|
||||
|
@ -111,7 +114,7 @@ class CardMakerReader(BaseReader):
|
|||
chain = int(troot.find("chain").text)
|
||||
skill_name = troot.find("skillName").text
|
||||
|
||||
await self.chuni_data.static.put_card(
|
||||
self.chuni_data.static.put_card(
|
||||
version,
|
||||
card_id,
|
||||
charaName=chara_name,
|
||||
|
@ -128,7 +131,7 @@ class CardMakerReader(BaseReader):
|
|||
|
||||
self.logger.info(f"Added chuni card {card_id}")
|
||||
|
||||
async def read_chuni_gacha(self, base_dir: str) -> None:
|
||||
def read_chuni_gacha(self, base_dir: str) -> None:
|
||||
self.logger.info(f"Reading gachas from {base_dir}...")
|
||||
|
||||
version_ids = {
|
||||
|
@ -155,7 +158,7 @@ class CardMakerReader(BaseReader):
|
|||
True if troot.find("ceilingType").text == "1" else False
|
||||
)
|
||||
|
||||
await self.chuni_data.static.put_gacha(
|
||||
self.chuni_data.static.put_gacha(
|
||||
version,
|
||||
gacha_id,
|
||||
name,
|
||||
|
@ -178,7 +181,7 @@ class CardMakerReader(BaseReader):
|
|||
True if gacha_card.find("pickup").text == "1" else False
|
||||
)
|
||||
|
||||
await self.chuni_data.static.put_gacha_card(
|
||||
self.chuni_data.static.put_gacha_card(
|
||||
gacha_id,
|
||||
card_id,
|
||||
weight=weight,
|
||||
|
@ -190,7 +193,7 @@ class CardMakerReader(BaseReader):
|
|||
f"Added chuni card {card_id} to gacha {gacha_id}"
|
||||
)
|
||||
|
||||
async def read_mai2_card(self, base_dir: str) -> None:
|
||||
def read_mai2_card(self, base_dir: str) -> None:
|
||||
self.logger.info(f"Reading cards from {base_dir}...")
|
||||
|
||||
version_ids = {
|
||||
|
@ -229,18 +232,18 @@ class CardMakerReader(BaseReader):
|
|||
False if re.search(r"\d{2}/\d{2}/\d{2}", name) else enabled
|
||||
)
|
||||
|
||||
await self.mai2_data.static.put_card(
|
||||
self.mai2_data.static.put_card(
|
||||
version, card_id, name, enabled=enabled
|
||||
)
|
||||
self.logger.info(f"Added mai2 card {card_id}")
|
||||
|
||||
async def read_ongeki_gacha_csv(self, file_path: str) -> None:
|
||||
def read_ongeki_gacha_csv(self, file_path: str) -> None:
|
||||
self.logger.info(f"Reading gachas from {file_path}...")
|
||||
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
await self.ongeki_data.static.put_gacha(
|
||||
self.ongeki_data.static.put_gacha(
|
||||
row["version"],
|
||||
row["gachaId"],
|
||||
row["gachaName"],
|
||||
|
@ -252,13 +255,13 @@ class CardMakerReader(BaseReader):
|
|||
|
||||
self.logger.info(f"Added ongeki gacha {row['gachaId']}")
|
||||
|
||||
async def read_ongeki_gacha_card_csv(self, file_path: str) -> None:
|
||||
def read_ongeki_gacha_card_csv(self, file_path: str) -> None:
|
||||
self.logger.info(f"Reading gacha cards from {file_path}...")
|
||||
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
await self.ongeki_data.static.put_gacha_card(
|
||||
self.ongeki_data.static.put_gacha_card(
|
||||
row["gachaId"],
|
||||
row["cardId"],
|
||||
rarity=row["rarity"],
|
||||
|
@ -269,7 +272,7 @@ class CardMakerReader(BaseReader):
|
|||
|
||||
self.logger.info(f"Added ongeki card {row['cardId']} to gacha")
|
||||
|
||||
async def read_ongeki_gacha(self, base_dir: str) -> None:
|
||||
def read_ongeki_gacha(self, base_dir: str) -> None:
|
||||
self.logger.info(f"Reading gachas from {base_dir}...")
|
||||
|
||||
# assuming some GachaKinds based on the GachaType
|
||||
|
@ -292,7 +295,7 @@ class CardMakerReader(BaseReader):
|
|||
|
||||
# skip already existing gachas
|
||||
if (
|
||||
await self.ongeki_data.static.get_gacha(
|
||||
self.ongeki_data.static.get_gacha(
|
||||
OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, gacha_id
|
||||
)
|
||||
is not None
|
||||
|
@ -318,7 +321,7 @@ class CardMakerReader(BaseReader):
|
|||
is_ceiling = 1
|
||||
max_select_point = 33
|
||||
|
||||
await self.ongeki_data.static.put_gacha(
|
||||
self.ongeki_data.static.put_gacha(
|
||||
version,
|
||||
gacha_id,
|
||||
name,
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from titles.cxb.index import CxbServlet
|
||||
from titles.cxb.const import CxbConstants
|
||||
from titles.cxb.database import CxbData
|
||||
from titles.cxb.index import CxbServlet
|
||||
from titles.cxb.read import CxbReader
|
||||
|
||||
index = CxbServlet
|
||||
database = CxbData
|
||||
reader = CxbReader
|
||||
game_codes = [CxbConstants.GAME_CODE]
|
||||
current_schema_version = 1
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user