forked from Hay1tsme/artemis
Merge branch 'develop' into idac
This commit is contained in:
commit
565dc38e9a
@ -12,6 +12,7 @@ RUN chmod +x entrypoint.sh
|
|||||||
|
|
||||||
COPY index.py index.py
|
COPY index.py index.py
|
||||||
COPY dbutils.py dbutils.py
|
COPY dbutils.py dbutils.py
|
||||||
|
COPY read.py read.py
|
||||||
ADD core core
|
ADD core core
|
||||||
ADD titles titles
|
ADD titles titles
|
||||||
ADD config config
|
ADD config config
|
||||||
|
@ -13,6 +13,13 @@ Documenting updates to ARTEMiS, to be updated every time the master branch is pu
|
|||||||
+ Frontend to download profile added
|
+ Frontend to download profile added
|
||||||
+ Importer to import profiles added
|
+ Importer to import profiles added
|
||||||
|
|
||||||
|
## 20231015
|
||||||
|
### maimai DX
|
||||||
|
+ Added support for FESTiVAL PLUS
|
||||||
|
|
||||||
|
### Card Maker
|
||||||
|
+ Added support for maimai DX FESTiVAL PLUS
|
||||||
|
|
||||||
## 20230716
|
## 20230716
|
||||||
### General
|
### General
|
||||||
+ Docker files added (#19)
|
+ Docker files added (#19)
|
||||||
|
@ -145,7 +145,15 @@ class AimedbProtocol(Protocol):
|
|||||||
def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||||
req = ADBLookupRequest(data)
|
req = ADBLookupRequest(data)
|
||||||
user_id = 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 = self.data.card.get_card_banned(req.access_code)
|
||||||
|
is_locked = self.data.card.get_card_locked(req.access_code)
|
||||||
|
|
||||||
|
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
|
||||||
ret = ADBLookupResponse.from_req(req.head, user_id)
|
ret = ADBLookupResponse.from_req(req.head, user_id)
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
@ -157,7 +165,16 @@ class AimedbProtocol(Protocol):
|
|||||||
req = ADBLookupRequest(data)
|
req = ADBLookupRequest(data)
|
||||||
user_id = 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 = 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)
|
ret = ADBLookupExResponse.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(
|
self.logger.info(
|
||||||
f"access_code {req.access_code} -> user_id {ret.user_id}"
|
f"access_code {req.access_code} -> user_id {ret.user_id}"
|
||||||
|
128
core/allnet.py
128
core/allnet.py
@ -16,10 +16,11 @@ from os import path
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import math
|
import math
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from .config import CoreConfig
|
||||||
from core.utils import Utils
|
from .utils import Utils
|
||||||
from core.data import Data
|
from .data import Data
|
||||||
from core.const import *
|
from .const import *
|
||||||
|
from .title import TitleServlet
|
||||||
|
|
||||||
BILLING_DT_FORMAT: Final[str] = "%Y%m%d%H%M%S"
|
BILLING_DT_FORMAT: Final[str] = "%Y%m%d%H%M%S"
|
||||||
|
|
||||||
@ -33,13 +34,67 @@ class ALLNET_STAT(Enum):
|
|||||||
bad_machine = -2
|
bad_machine = -2
|
||||||
bad_shop = -3
|
bad_shop = -3
|
||||||
|
|
||||||
|
class DLI_STATUS(Enum):
|
||||||
|
START = 0
|
||||||
|
GET_DOWNLOAD_CONFIGURATION = 1
|
||||||
|
WAIT_DOWNLOAD = 2
|
||||||
|
DOWNLOADING = 3
|
||||||
|
|
||||||
|
NOT_SPECIFY_DLI = 100
|
||||||
|
ONLY_POST_REPORT = 101
|
||||||
|
STOPPED_BY_APP_RELEASE = 102
|
||||||
|
STOPPED_BY_OPT_RELEASE = 103
|
||||||
|
|
||||||
|
DOWNLOAD_COMPLETE_RECENTLY = 110
|
||||||
|
|
||||||
|
DOWNLOAD_COMPLETE_WAIT_RELEASE_TIME = 120
|
||||||
|
DOWNLOAD_COMPLETE_BUT_NOT_SYNC_SERVER = 121
|
||||||
|
DOWNLOAD_COMPLETE_BUT_NOT_FIRST_RESUME = 122
|
||||||
|
DOWNLOAD_COMPLETE_BUT_NOT_FIRST_LAUNCH = 123
|
||||||
|
DOWNLOAD_COMPLETE_WAIT_UPDATE = 124
|
||||||
|
|
||||||
|
DOWNLOAD_COMPLETE_AND_ALREADY_UPDATE = 130
|
||||||
|
|
||||||
|
ERROR_AUTH_FAILURE = 200
|
||||||
|
|
||||||
|
ERROR_GET_DLI_HTTP = 300
|
||||||
|
ERROR_GET_DLI = 301
|
||||||
|
ERROR_PARSE_DLI = 302
|
||||||
|
ERROR_INVALID_GAME_ID = 303
|
||||||
|
ERROR_INVALID_IMAGE_LIST = 304
|
||||||
|
ERROR_GET_DLI_APP = 305
|
||||||
|
|
||||||
|
ERROR_GET_BOOT_ID = 400
|
||||||
|
ERROR_ACCESS_SERVER = 401
|
||||||
|
ERROR_NO_IMAGE = 402
|
||||||
|
ERROR_ACCESS_IMAGE = 403
|
||||||
|
|
||||||
|
ERROR_DOWNLOAD_APP = 500
|
||||||
|
ERROR_DOWNLOAD_OPT = 501
|
||||||
|
|
||||||
|
ERROR_DISK_FULL = 600
|
||||||
|
ERROR_UNINSTALL = 601
|
||||||
|
ERROR_INSTALL_APP = 602
|
||||||
|
ERROR_INSTALL_OPT = 603
|
||||||
|
|
||||||
|
ERROR_GET_DLI_INTERNAL = 900
|
||||||
|
ERROR_ICF = 901
|
||||||
|
ERROR_CHECK_RELEASE_INTERNAL = 902
|
||||||
|
UNKNOWN = 999 # Not the actual enum val but it needs to be here as a catch-all
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_int(cls, num: int) -> "DLI_STATUS":
|
||||||
|
try:
|
||||||
|
return cls(num)
|
||||||
|
except ValueError:
|
||||||
|
return cls.UNKNOWN
|
||||||
|
|
||||||
class AllnetServlet:
|
class AllnetServlet:
|
||||||
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.config = core_cfg
|
self.config = core_cfg
|
||||||
self.config_folder = cfg_folder
|
self.config_folder = cfg_folder
|
||||||
self.data = Data(core_cfg)
|
self.data = Data(core_cfg)
|
||||||
self.uri_registry: Dict[str, Tuple[str, str]] = {}
|
|
||||||
|
|
||||||
self.logger = logging.getLogger("allnet")
|
self.logger = logging.getLogger("allnet")
|
||||||
if not hasattr(self.logger, "initialized"):
|
if not hasattr(self.logger, "initialized"):
|
||||||
@ -70,18 +125,8 @@ class AllnetServlet:
|
|||||||
if len(plugins) == 0:
|
if len(plugins) == 0:
|
||||||
self.logger.error("No games detected!")
|
self.logger.error("No games detected!")
|
||||||
|
|
||||||
for _, mod in plugins.items():
|
|
||||||
if hasattr(mod, "index") and hasattr(mod.index, "get_allnet_info"):
|
|
||||||
for code in mod.game_codes:
|
|
||||||
enabled, uri, host = mod.index.get_allnet_info(
|
|
||||||
code, self.config, self.config_folder
|
|
||||||
)
|
|
||||||
|
|
||||||
if enabled:
|
|
||||||
self.uri_registry[code] = (uri, host)
|
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"Serving {len(self.uri_registry)} game codes port {core_cfg.allnet.port}"
|
f"Serving {len(TitleServlet.title_registry)} game codes port {core_cfg.allnet.port}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_poweron(self, request: Request, _: Dict):
|
def handle_poweron(self, request: Request, _: Dict):
|
||||||
@ -147,7 +192,7 @@ class AllnetServlet:
|
|||||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||||
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
|
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
|
||||||
|
|
||||||
elif not arcade["ip"] or arcade["ip"] is None and self.config.server.strict_ip_checking:
|
elif (not arcade["ip"] or arcade["ip"] is None) and self.config.server.strict_ip_checking:
|
||||||
msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)."
|
msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)."
|
||||||
self.data.base.log_event(
|
self.data.base.log_event(
|
||||||
"allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg
|
"allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg
|
||||||
@ -190,7 +235,7 @@ class AllnetServlet:
|
|||||||
arcade["timezone"] if arcade["timezone"] is not None else "+0900" if req.format_ver == 3 else "+09:00"
|
arcade["timezone"] if arcade["timezone"] is not None else "+0900" if req.format_ver == 3 else "+09:00"
|
||||||
)
|
)
|
||||||
|
|
||||||
if req.game_id not in self.uri_registry:
|
if req.game_id not in TitleServlet.title_registry:
|
||||||
if not self.config.server.is_develop:
|
if not self.config.server.is_develop:
|
||||||
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
|
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
|
||||||
self.data.base.log_event(
|
self.data.base.log_event(
|
||||||
@ -215,11 +260,9 @@ class AllnetServlet:
|
|||||||
self.logger.debug(f"Allnet response: {resp_str}")
|
self.logger.debug(f"Allnet response: {resp_str}")
|
||||||
return (resp_str + "\n").encode("utf-8")
|
return (resp_str + "\n").encode("utf-8")
|
||||||
|
|
||||||
resp.uri, resp.host = self.uri_registry[req.game_id]
|
|
||||||
|
|
||||||
int_ver = req.ver.replace(".", "")
|
int_ver = req.ver.replace(".", "")
|
||||||
resp.uri = resp.uri.replace("$v", int_ver)
|
resp.uri, resp.host = TitleServlet.title_registry[req.game_id].get_allnet_info(req.game_id, int(int_ver), req.serial)
|
||||||
resp.host = resp.host.replace("$v", int_ver)
|
|
||||||
|
|
||||||
msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}"
|
msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}"
|
||||||
self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg)
|
self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg)
|
||||||
@ -315,6 +358,7 @@ class AllnetServlet:
|
|||||||
|
|
||||||
def handle_dlorder_report(self, request: Request, match: Dict) -> bytes:
|
def handle_dlorder_report(self, request: Request, match: Dict) -> bytes:
|
||||||
req_raw = request.content.getvalue()
|
req_raw = request.content.getvalue()
|
||||||
|
client_ip = Utils.get_ip_addr(request)
|
||||||
try:
|
try:
|
||||||
req_dict: Dict = json.loads(req_raw)
|
req_dict: Dict = json.loads(req_raw)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -332,12 +376,18 @@ class AllnetServlet:
|
|||||||
self.logger.warning(f"Failed to parse DL Report: Invalid format - contains neither appimage nor optimage")
|
self.logger.warning(f"Failed to parse DL Report: Invalid format - contains neither appimage nor optimage")
|
||||||
return "NG"
|
return "NG"
|
||||||
|
|
||||||
dl_report_data = DLReport(dl_data, dl_data_type)
|
rep = DLReport(dl_data, dl_data_type)
|
||||||
|
|
||||||
if not dl_report_data.validate():
|
if not rep.validate():
|
||||||
self.logger.warning(f"Failed to parse DL Report: Invalid format - {dl_report_data.err}")
|
self.logger.warning(f"Failed to parse DL Report: Invalid format - {rep.err}")
|
||||||
return "NG"
|
return "NG"
|
||||||
|
|
||||||
|
msg = f"{rep.serial} @ {client_ip} reported {rep.rep_type.name} download state {rep.rf_state.name} for {rep.gd} v{rep.dav}:"\
|
||||||
|
f" {rep.tdsc}/{rep.tsc} segments downloaded for working files {rep.wfl} with {rep.dfl if rep.dfl else 'none'} complete."
|
||||||
|
|
||||||
|
self.data.base.log_event("allnet", "DL_REPORT", logging.INFO, msg, dl_data)
|
||||||
|
self.logger.info(msg)
|
||||||
|
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes:
|
def handle_loaderstaterecorder(self, request: Request, match: Dict) -> bytes:
|
||||||
@ -524,7 +574,7 @@ class AllnetPowerOnResponse:
|
|||||||
self.stat = 1
|
self.stat = 1
|
||||||
self.uri = ""
|
self.uri = ""
|
||||||
self.host = ""
|
self.host = ""
|
||||||
self.place_id = "123"
|
self.place_id = "0123"
|
||||||
self.name = "ARTEMiS"
|
self.name = "ARTEMiS"
|
||||||
self.nickname = "ARTEMiS"
|
self.nickname = "ARTEMiS"
|
||||||
self.region0 = "1"
|
self.region0 = "1"
|
||||||
@ -739,13 +789,13 @@ class DLReport:
|
|||||||
self.ot = data.get("ot")
|
self.ot = data.get("ot")
|
||||||
self.rt = data.get("rt")
|
self.rt = data.get("rt")
|
||||||
self.as_ = data.get("as")
|
self.as_ = data.get("as")
|
||||||
self.rf_state = data.get("rf_state")
|
self.rf_state = DLI_STATUS.from_int(data.get("rf_state"))
|
||||||
self.gd = data.get("gd")
|
self.gd = data.get("gd")
|
||||||
self.dav = data.get("dav")
|
self.dav = data.get("dav")
|
||||||
self.wdav = data.get("wdav") # app only
|
self.wdav = data.get("wdav") # app only
|
||||||
self.dov = data.get("dov")
|
self.dov = data.get("dov")
|
||||||
self.wdov = data.get("wdov") # app only
|
self.wdov = data.get("wdov") # app only
|
||||||
self.__type = report_type
|
self.rep_type = report_type
|
||||||
self.err = ""
|
self.err = ""
|
||||||
|
|
||||||
def validate(self) -> bool:
|
def validate(self) -> bool:
|
||||||
@ -753,14 +803,6 @@ class DLReport:
|
|||||||
self.err = "serial not provided"
|
self.err = "serial not provided"
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.dfl is None:
|
|
||||||
self.err = "dfl not provided"
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.wfl is None:
|
|
||||||
self.err = "wfl not provided"
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.tsc is None:
|
if self.tsc is None:
|
||||||
self.err = "tsc not provided"
|
self.err = "tsc not provided"
|
||||||
return False
|
return False
|
||||||
@ -769,18 +811,6 @@ class DLReport:
|
|||||||
self.err = "tdsc not provided"
|
self.err = "tdsc not provided"
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.at is None:
|
|
||||||
self.err = "at not provided"
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.ot is None:
|
|
||||||
self.err = "ot not provided"
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.rt is None:
|
|
||||||
self.err = "rt not provided"
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.as_ is None:
|
if self.as_ is None:
|
||||||
self.err = "as not provided"
|
self.err = "as not provided"
|
||||||
return False
|
return False
|
||||||
@ -801,11 +831,11 @@ class DLReport:
|
|||||||
self.err = "dov not provided"
|
self.err = "dov not provided"
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if (self.wdav is None or self.wdov is None) and self.__type == DLIMG_TYPE.app:
|
if (self.wdav is None or self.wdov is None) and self.rep_type == DLIMG_TYPE.app:
|
||||||
self.err = "wdav or wdov not provided in app image"
|
self.err = "wdav or wdov not provided in app image"
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if (self.wdav is not None or self.wdov is not None) and self.__type == DLIMG_TYPE.opt:
|
if (self.wdav is not None or self.wdov is not None) and self.rep_type == DLIMG_TYPE.opt:
|
||||||
self.err = "wdav or wdov provided in opt image"
|
self.err = "wdav or wdov provided in opt image"
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -36,6 +36,12 @@ class ServerConfig:
|
|||||||
self.__config, "core", "server", "is_develop", default=True
|
self.__config, "core", "server", "is_develop", default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_using_proxy(self) -> bool:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "server", "is_using_proxy", default=False
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def threading(self) -> bool:
|
def threading(self) -> bool:
|
||||||
return CoreConfig.get_config_field(
|
return CoreConfig.get_config_field(
|
||||||
@ -85,6 +91,36 @@ class TitleConfig:
|
|||||||
self.__config, "core", "title", "port", default=8080
|
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(
|
||||||
|
self.__config, "core", "title", "reboot_start_time", default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reboot_end_time(self) -> str:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "title", "reboot_end_time", default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseConfig:
|
class DatabaseConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
@ -150,6 +186,12 @@ class DatabaseConfig:
|
|||||||
default=10000,
|
default=10000,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enable_memcached(self) -> bool:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "database", "enable_memcached", default=True
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def memcached_host(self) -> str:
|
def memcached_host(self) -> str:
|
||||||
return CoreConfig.get_config_field(
|
return CoreConfig.get_config_field(
|
||||||
|
@ -17,7 +17,7 @@ except ModuleNotFoundError:
|
|||||||
|
|
||||||
def cached(lifetime: int = 10, extra_key: Any = None) -> Callable:
|
def cached(lifetime: int = 10, extra_key: Any = None) -> Callable:
|
||||||
def _cached(func: Callable) -> Callable:
|
def _cached(func: Callable) -> Callable:
|
||||||
if has_mc:
|
if has_mc and (cfg and cfg.database.enable_memcached):
|
||||||
hostname = "127.0.0.1"
|
hostname = "127.0.0.1"
|
||||||
if cfg:
|
if cfg:
|
||||||
hostname = cfg.database.memcached_host
|
hostname = cfg.database.memcached_host
|
||||||
|
@ -41,7 +41,6 @@ machine = Table(
|
|||||||
Column("country", String(3)), # overwrites if not null
|
Column("country", String(3)), # overwrites if not null
|
||||||
Column("timezone", String(255)),
|
Column("timezone", String(255)),
|
||||||
Column("ota_enable", Boolean),
|
Column("ota_enable", Boolean),
|
||||||
Column("is_cab", Boolean),
|
|
||||||
Column("memo", String(255)),
|
Column("memo", String(255)),
|
||||||
Column("is_cab", Boolean),
|
Column("is_cab", Boolean),
|
||||||
Column("data", JSON),
|
Column("data", JSON),
|
||||||
|
@ -64,6 +64,27 @@ class CardData(BaseData):
|
|||||||
|
|
||||||
return int(card["user"])
|
return int(card["user"])
|
||||||
|
|
||||||
|
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 = self.get_card_by_access_code(access_code)
|
||||||
|
if card is None:
|
||||||
|
return None
|
||||||
|
if card["is_banned"]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
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 = self.get_card_by_access_code(access_code)
|
||||||
|
if card is None:
|
||||||
|
return None
|
||||||
|
if card["is_locked"]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
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)
|
sql = aime_card.delete(aime_card.c.id == card_id)
|
||||||
|
|
||||||
|
2
core/data/schema/versions/SBZV_5_rollback.sql
Normal file
2
core/data/schema/versions/SBZV_5_rollback.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE diva_profile
|
||||||
|
DROP skn_eqp;
|
2
core/data/schema/versions/SBZV_6_upgrade.sql
Normal file
2
core/data/schema/versions/SBZV_6_upgrade.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE diva_profile
|
||||||
|
ADD skn_eqp INT NOT NULL DEFAULT 0;
|
22
core/data/schema/versions/SDDT_5_rollback.sql
Normal file
22
core/data/schema/versions/SDDT_5_rollback.sql
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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 date;
|
||||||
|
|
||||||
|
ALTER TABLE ongeki_user_tech_event DROP COLUMN version;
|
||||||
|
|
||||||
|
ALTER TABLE ongeki_user_mission_point DROP COLUMN version;
|
||||||
|
|
||||||
|
ALTER TABLE ongeki_static_events DROP COLUMN endDate;
|
||||||
|
|
||||||
|
DROP TABLE ongeki_tech_event_ranking;
|
||||||
|
DROP TABLE ongeki_static_music_ranking_list;
|
||||||
|
DROP TABLE ongeki_static_rewards;
|
||||||
|
DROP TABLE ongeki_static_present_list;
|
||||||
|
DROP TABLE ongeki_static_tech_music;
|
||||||
|
DROP TABLE ongeki_static_client_testmode;
|
||||||
|
DROP TABLE ongeki_static_game_point;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS=1;
|
98
core/data/schema/versions/SDDT_6_upgrade.sql
Normal file
98
core/data/schema/versions/SDDT_6_upgrade.sql
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
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 date VARCHAR(25);
|
||||||
|
|
||||||
|
ALTER TABLE ongeki_user_tech_event ADD COLUMN version INTEGER NOT NULL;
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS=1;
|
10
core/data/schema/versions/SDEZ_7_rollback.sql
Normal file
10
core/data/schema/versions/SDEZ_7_rollback.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
ALTER TABLE mai2_profile_detail
|
||||||
|
DROP COLUMN mapStock;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_profile_extend
|
||||||
|
DROP COLUMN selectResultScoreViewType;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_profile_option
|
||||||
|
DROP COLUMN outFrameType,
|
||||||
|
DROP COLUMN touchVolume,
|
||||||
|
DROP COLUMN breakSlideVolume;
|
10
core/data/schema/versions/SDEZ_8_upgrade.sql
Normal file
10
core/data/schema/versions/SDEZ_8_upgrade.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
ALTER TABLE mai2_profile_detail
|
||||||
|
ADD mapStock INT NULL AFTER playCount;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_profile_extend
|
||||||
|
ADD selectResultScoreViewType INT NULL AFTER selectResultDetails;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_profile_option
|
||||||
|
ADD outFrameType INT NULL AFTER dispCenter,
|
||||||
|
ADD touchVolume INT NULL AFTER slideVolume,
|
||||||
|
ADD breakSlideVolume INT NULL AFTER slideVolume;
|
@ -7,15 +7,15 @@ from datetime import datetime
|
|||||||
from Crypto.Cipher import Blowfish
|
from Crypto.Cipher import Blowfish
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core import CoreConfig
|
from .config import CoreConfig
|
||||||
from core.utils import Utils
|
from .utils import Utils
|
||||||
|
from .title import TitleServlet
|
||||||
|
|
||||||
class MuchaServlet:
|
class MuchaServlet:
|
||||||
|
mucha_registry: List[str] = []
|
||||||
def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None:
|
def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None:
|
||||||
self.config = cfg
|
self.config = cfg
|
||||||
self.config_dir = cfg_dir
|
self.config_dir = cfg_dir
|
||||||
self.mucha_registry: List[str] = []
|
|
||||||
|
|
||||||
self.logger = logging.getLogger("mucha")
|
self.logger = logging.getLogger("mucha")
|
||||||
log_fmt_str = "[%(asctime)s] Mucha | %(levelname)s | %(message)s"
|
log_fmt_str = "[%(asctime)s] Mucha | %(levelname)s | %(message)s"
|
||||||
@ -37,11 +37,9 @@ class MuchaServlet:
|
|||||||
self.logger.setLevel(cfg.mucha.loglevel)
|
self.logger.setLevel(cfg.mucha.loglevel)
|
||||||
coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str)
|
coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str)
|
||||||
|
|
||||||
all_titles = Utils.get_all_titles()
|
for _, mod in TitleServlet.title_registry.items():
|
||||||
|
if hasattr(mod, "get_mucha_info"):
|
||||||
for _, mod in all_titles.items():
|
enabled, game_cd = mod.get_mucha_info(
|
||||||
if hasattr(mod, "index") and hasattr(mod.index, "get_mucha_info"):
|
|
||||||
enabled, game_cd = mod.index.get_mucha_info(
|
|
||||||
self.config, self.config_dir
|
self.config, self.config_dir
|
||||||
)
|
)
|
||||||
if enabled:
|
if enabled:
|
||||||
|
136
core/title.py
136
core/title.py
@ -1,4 +1,4 @@
|
|||||||
from typing import Dict, Any
|
from typing import Dict, List, Tuple
|
||||||
import logging, coloredlogs
|
import logging, coloredlogs
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
from twisted.web.http import Request
|
from twisted.web.http import Request
|
||||||
@ -7,14 +7,88 @@ from core.config import CoreConfig
|
|||||||
from core.data import Data
|
from core.data import Data
|
||||||
from core.utils import Utils
|
from core.utils import Utils
|
||||||
|
|
||||||
|
class BaseServlet:
|
||||||
|
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||||
|
self.core_cfg = core_cfg
|
||||||
|
self.game_cfg = None
|
||||||
|
self.logger = logging.getLogger("title")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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:
|
||||||
|
game_code (str): 4 character game code
|
||||||
|
core_cfg (CoreConfig): CoreConfig class
|
||||||
|
cfg_dir (str): Config directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the game is enabled and set to run, False otherwise
|
||||||
|
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
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:
|
||||||
|
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 (
|
||||||
|
[("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
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
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:
|
||||||
|
game_code (str): 4 character game code
|
||||||
|
game_ver (int): version, expressed as an integer by multiplying by 100 (1.10 -> 110)
|
||||||
|
keychip (str): Keychip serial of the requesting machine, can be used to deliver specific URIs to different machines
|
||||||
|
|
||||||
|
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.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{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, str]:
|
||||||
|
"""Called once during boot to check if this game is a mucha game
|
||||||
|
|
||||||
|
Args:
|
||||||
|
core_cfg (CoreConfig): CoreConfig class
|
||||||
|
cfg_dir (str): Config directory
|
||||||
|
|
||||||
|
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, "")
|
||||||
|
|
||||||
|
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:
|
class TitleServlet:
|
||||||
|
title_registry: Dict[str, BaseServlet] = {}
|
||||||
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.config = core_cfg
|
self.config = core_cfg
|
||||||
self.config_folder = cfg_folder
|
self.config_folder = cfg_folder
|
||||||
self.data = Data(core_cfg)
|
self.data = Data(core_cfg)
|
||||||
self.title_registry: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
self.logger = logging.getLogger("title")
|
self.logger = logging.getLogger("title")
|
||||||
if not hasattr(self.logger, "initialized"):
|
if not hasattr(self.logger, "initialized"):
|
||||||
@ -43,62 +117,62 @@ class TitleServlet:
|
|||||||
plugins = Utils.get_all_titles()
|
plugins = Utils.get_all_titles()
|
||||||
|
|
||||||
for folder, mod in plugins.items():
|
for folder, mod in plugins.items():
|
||||||
if hasattr(mod, "game_codes") and hasattr(mod, "index"):
|
if hasattr(mod, "game_codes") and hasattr(mod, "index") and hasattr(mod.index, "is_game_enabled"):
|
||||||
should_call_setup = True
|
should_call_setup = True
|
||||||
|
game_servlet: BaseServlet = mod.index
|
||||||
|
game_codes: List[str] = mod.game_codes
|
||||||
|
|
||||||
if hasattr(mod.index, "get_allnet_info"):
|
for code in game_codes:
|
||||||
for code in mod.game_codes:
|
if game_servlet.is_game_enabled(code, self.config, self.config_folder):
|
||||||
enabled, _, _ = mod.index.get_allnet_info(
|
handler_cls = game_servlet(self.config, self.config_folder)
|
||||||
code, self.config, self.config_folder
|
|
||||||
)
|
|
||||||
|
|
||||||
if enabled:
|
if hasattr(handler_cls, "setup") and should_call_setup:
|
||||||
handler_cls = mod.index(self.config, self.config_folder)
|
handler_cls.setup()
|
||||||
|
should_call_setup = False
|
||||||
|
|
||||||
if hasattr(handler_cls, "setup") and should_call_setup:
|
self.title_registry[code] = handler_cls
|
||||||
handler_cls.setup()
|
|
||||||
should_call_setup = False
|
|
||||||
|
|
||||||
self.title_registry[code] = handler_cls
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.logger.warning(f"Game {folder} has no get_allnet_info")
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.logger.error(f"{folder} missing game_code or index in __init__.py")
|
self.logger.error(f"{folder} missing game_code or index in __init__.py, or is_game_enabled in index")
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"Serving {len(self.title_registry)} game codes {'on port ' + str(core_cfg.title.port) if core_cfg.title.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:
|
def render_GET(self, request: Request, endpoints: dict) -> bytes:
|
||||||
code = endpoints["game"]
|
code = endpoints["title"]
|
||||||
|
subaction = endpoints['subaction']
|
||||||
|
|
||||||
if code not in self.title_registry:
|
if code not in self.title_registry:
|
||||||
self.logger.warning(f"Unknown game code {code}")
|
self.logger.warning(f"Unknown game code {code}")
|
||||||
request.setResponseCode(404)
|
request.setResponseCode(404)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
index = self.title_registry[code]
|
index = self.title_registry[code]
|
||||||
if not hasattr(index, "render_GET"):
|
handler = getattr(index, f"{subaction}", None)
|
||||||
self.logger.warning(f"{code} does not dispatch GET")
|
if handler is None:
|
||||||
request.setResponseCode(405)
|
self.logger.error(f"{code} does not have handler for GET subaction {subaction}")
|
||||||
|
request.setResponseCode(500)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
return index.render_GET(request, int(endpoints["version"]), endpoints["endpoint"])
|
return handler(request, code, endpoints)
|
||||||
|
|
||||||
def render_POST(self, request: Request, endpoints: dict) -> bytes:
|
def render_POST(self, request: Request, endpoints: dict) -> bytes:
|
||||||
code = endpoints["game"]
|
code = endpoints["title"]
|
||||||
|
subaction = endpoints['subaction']
|
||||||
|
|
||||||
if code not in self.title_registry:
|
if code not in self.title_registry:
|
||||||
self.logger.warning(f"Unknown game code {code}")
|
self.logger.warning(f"Unknown game code {code}")
|
||||||
request.setResponseCode(404)
|
request.setResponseCode(404)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
index = self.title_registry[code]
|
index = self.title_registry[code]
|
||||||
if not hasattr(index, "render_POST"):
|
handler = getattr(index, f"{subaction}", None)
|
||||||
self.logger.warning(f"{code} does not dispatch POST")
|
if handler is None:
|
||||||
request.setResponseCode(405)
|
self.logger.error(f"{code} does not have handler for POST subaction {subaction}")
|
||||||
|
request.setResponseCode(500)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
return index.render_POST(
|
endpoints.pop("title")
|
||||||
request, int(endpoints["version"]), endpoints["endpoint"]
|
endpoints.pop("subaction")
|
||||||
)
|
return handler(request, code, endpoints)
|
||||||
|
@ -5,8 +5,11 @@ import logging
|
|||||||
import importlib
|
import importlib
|
||||||
from os import walk
|
from os import walk
|
||||||
|
|
||||||
|
from .config import CoreConfig
|
||||||
|
|
||||||
class Utils:
|
class Utils:
|
||||||
|
real_title_port = None
|
||||||
|
real_title_port_ssl = None
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_titles(cls) -> Dict[str, ModuleType]:
|
def get_all_titles(cls) -> Dict[str, ModuleType]:
|
||||||
ret: Dict[str, Any] = {}
|
ret: Dict[str, Any] = {}
|
||||||
@ -33,3 +36,27 @@ class Utils:
|
|||||||
if b"x-forwarded-for" in req.getAllHeaders()
|
if b"x-forwarded-for" in req.getAllHeaders()
|
||||||
else req.getClientAddress().host
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -5,22 +5,23 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
volumes:
|
volumes:
|
||||||
- ./aime:/app/aime
|
- ./aime:/app/aime
|
||||||
|
- ./configs/config:/app/config
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
CFG_DEV: 1
|
CFG_DEV: 1
|
||||||
CFG_CORE_SERVER_HOSTNAME: 0.0.0.0
|
CFG_CORE_SERVER_HOSTNAME: 0.0.0.0
|
||||||
CFG_CORE_DATABASE_HOST: ma.db
|
CFG_CORE_DATABASE_HOST: ma.db
|
||||||
CFG_CORE_MEMCACHED_HOSTNAME: ma.memcached
|
CFG_CORE_MEMCACHED_HOSTNAME: ma.memcached
|
||||||
CFG_CORE_AIMEDB_KEY: keyhere
|
CFG_CORE_AIMEDB_KEY: <INSERT AIMEDB KEY HERE>
|
||||||
CFG_CHUNI_SERVER_LOGLEVEL: debug
|
CFG_CHUNI_SERVER_LOGLEVEL: debug
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "8443:8443"
|
- "8443:8443"
|
||||||
- "22345:22345"
|
- "22345:22345"
|
||||||
|
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
- "8090:8090"
|
- "8090:8090"
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
@ -28,21 +29,29 @@ services:
|
|||||||
|
|
||||||
db:
|
db:
|
||||||
hostname: ma.db
|
hostname: ma.db
|
||||||
image: mysql:8.0.31-debian
|
image: yobasystems/alpine-mariadb:10.11.5
|
||||||
environment:
|
environment:
|
||||||
MYSQL_DATABASE: aime
|
MYSQL_DATABASE: aime
|
||||||
MYSQL_USER: aime
|
MYSQL_USER: aime
|
||||||
MYSQL_PASSWORD: aime
|
MYSQL_PASSWORD: aime
|
||||||
MYSQL_ROOT_PASSWORD: AimeRootPassword
|
MYSQL_ROOT_PASSWORD: AimeRootPassword
|
||||||
|
MYSQL_CHARSET: utf8mb4
|
||||||
|
MYSQL_COLLATION: utf8mb4_general_ci
|
||||||
|
##Note: expose port 3306 to allow read.py importer into database, comment out when not needed
|
||||||
|
#ports:
|
||||||
|
# - "3306:3306"
|
||||||
|
##Note: uncomment to allow mysql to create a persistent database, leave commented if you want to rebuild database from scratch often
|
||||||
|
#volumes:
|
||||||
|
# - ./AimeDB:/var/lib/mysql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
|
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "-pAimeRootPassword"]
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
|
||||||
memcached:
|
memcached:
|
||||||
hostname: ma.memcached
|
hostname: ma.memcached
|
||||||
image: memcached:1.6.17-bullseye
|
image: memcached:1.6.22-alpine3.18
|
||||||
|
command: [ "memcached", "-m", "1024", "-I", "128m" ]
|
||||||
|
|
||||||
phpmyadmin:
|
phpmyadmin:
|
||||||
hostname: ma.phpmyadmin
|
hostname: ma.phpmyadmin
|
||||||
@ -53,5 +62,5 @@ services:
|
|||||||
PMA_PASSWORD: AimeRootPassword
|
PMA_PASSWORD: AimeRootPassword
|
||||||
APACHE_PORT: 8080
|
APACHE_PORT: 8080
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "9090:8080"
|
||||||
|
|
||||||
|
246
docs/INSTALL_DOCKER.md
Normal file
246
docs/INSTALL_DOCKER.md
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
# ARTEMiS - Docker Installation Guide
|
||||||
|
|
||||||
|
This step-by-step guide will allow you to install a Contenerized Version of ARTEMiS inside Docker, some steps can be skipped assuming you already have pre-requisite components and modules installed.
|
||||||
|
|
||||||
|
This guide assumes using Debian 12(bookworm-stable) as a Host Operating System for most of packages and modules.
|
||||||
|
|
||||||
|
## Pre-Requisites:
|
||||||
|
|
||||||
|
- Linux-Based Operating System (e.g. Debian, Ubuntu)
|
||||||
|
- Docker (https://get.docker.com)
|
||||||
|
- Python 3.9+
|
||||||
|
- (optional) Git
|
||||||
|
|
||||||
|
## Install Python3.9+ and Docker
|
||||||
|
|
||||||
|
```
|
||||||
|
(if this is a fresh install of the system)
|
||||||
|
sudo apt update && sudo apt upgrade
|
||||||
|
|
||||||
|
(installs python3 and pip)
|
||||||
|
sudo apt install python3 python3-pip
|
||||||
|
|
||||||
|
(installs docker)
|
||||||
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
|
sh get-docker.sh
|
||||||
|
|
||||||
|
(optionally install git)
|
||||||
|
sudo apt install git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Get ARTEMiS
|
||||||
|
|
||||||
|
If you installed git, clone into your choice of ARTEMiS git repository, e.g.:
|
||||||
|
```
|
||||||
|
git clone <ARTEMiS Repo> <folder>
|
||||||
|
```
|
||||||
|
If not, download the source package, and unpack it to the folder of your choice.
|
||||||
|
|
||||||
|
## Prepare development/home configuration
|
||||||
|
|
||||||
|
To build our Docker setup, first we need to create some folders and copy some files around
|
||||||
|
- Create 'aime', 'configs', 'AimeDB', and 'logs' folder in ARTEMiS root folder (where all source files exist)
|
||||||
|
- Inside configs folder, create 'config' folder, and copy all .yaml files from example_config to config (thats all files without nginx_example.conf)
|
||||||
|
- Edit .yaml files inside configs/config to suit your server needs
|
||||||
|
- Edit core.yaml inside configs/config:
|
||||||
|
```
|
||||||
|
set server.listen_address: to "0.0.0.0"
|
||||||
|
set title.hostname: to machine's IP address, e.g. "192.168.x.x", depending on your network, or actual hostname if your configuration is already set for dns resolve
|
||||||
|
set database.host: to "ma.db"
|
||||||
|
set database.memcached_host: to "ma.memcached"
|
||||||
|
set aimedb.key: to "<actual AIMEDB key>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Docker Compose
|
||||||
|
|
||||||
|
After configuring, go to ARTEMiS root folder, and execute:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
("-d" argument means detached or daemon, meaning you will regain control of your terminal and Containers will run in background)
|
||||||
|
|
||||||
|
This will start pulling and building required images from network, after it's done, a development server should be running, with server accessible under machine's IP, frontend with port 8090, and PHPMyAdmin under port 9090.
|
||||||
|
|
||||||
|
- To turn off the server, from ARTEMiS root folder, execute:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
- If you changed some files around, and don't see your changes applied, execute:
|
||||||
|
|
||||||
|
```
|
||||||
|
(turn off the server)
|
||||||
|
docker compose down
|
||||||
|
(rebuild)
|
||||||
|
docker compose build
|
||||||
|
(turn on)
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
- If you need to see logs from containers running, execute:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose logs
|
||||||
|
```
|
||||||
|
|
||||||
|
- add '-f' to the end if you want to follow logs.
|
||||||
|
|
||||||
|
## Running commands
|
||||||
|
|
||||||
|
If you need to execute python scripts supplied with the application, use `docker compose exec app python3 <script> <command>`, for example `docker compose exec app python3 dbutils.py version`
|
||||||
|
|
||||||
|
## Persistent DB
|
||||||
|
|
||||||
|
By default, in development mode, ARTEMiS database is stored temporarily, if you wish to keep your database saved between restarts, we need to bind the database inside the container to actual storage/folder inside our server, to do this we need to make a few changes:
|
||||||
|
|
||||||
|
- First off, edit docker-compose.yml, and uncomment 2 lines:
|
||||||
|
|
||||||
|
```
|
||||||
|
(uncomment these two)
|
||||||
|
#volumes:
|
||||||
|
# - ./AimeDB:/var/lib/mysql
|
||||||
|
```
|
||||||
|
|
||||||
|
- After that, start up the server, this time Database will be saved in AimeDB folder we created in our configuration steps.
|
||||||
|
- If you wish to save it in another folder and/or storage device, change the "./AimeDB" target folder to folder/device of your choice
|
||||||
|
|
||||||
|
NOTE (NEEDS FIX): at the moment running development mode with persistent DB will always run database creation script at the start of application, while it doesn't break database outright, it might create some issues, a temporary fix can be applied:
|
||||||
|
|
||||||
|
- Start up containers with persistent DB already enabled, let application create database
|
||||||
|
- After startup, `docker compose down` the instance
|
||||||
|
- Edit entrypoint.sh and remove the `python3 dbutils.py create` line from Development mode statement
|
||||||
|
- Execute `docker compose build` and `docker compose up -d` to rebuild the app and start the containers back
|
||||||
|
|
||||||
|
## Adding importer data
|
||||||
|
|
||||||
|
To add data using importer, we can do that a few ways:
|
||||||
|
|
||||||
|
### Use importer locally on server
|
||||||
|
|
||||||
|
For that we need actual GameData and Options supplied somehow to the server system, be it wsl2 mounting layer, a pendrive with data, network share, or a direct copy to the server storage
|
||||||
|
With python3 installed on system, install requirements.txt directly to the system, or through python3 virtual-environment (python3-venv)
|
||||||
|
Default mysql/mariadb client development packages will also be required
|
||||||
|
|
||||||
|
- In the system:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install default-libmysqlclient-dev build-essential pkg-config libmemcached-dev
|
||||||
|
sudo apt install mysql-client
|
||||||
|
OR
|
||||||
|
sudo apt install libmariadb-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- In the root ARTEMiS folder
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
- If we wish to layer that with python3 virtual-environment, install required system packages, then:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install python3-venv
|
||||||
|
python3 -m venv /path/to/venv
|
||||||
|
cd /path/to/venv/bin
|
||||||
|
python3 -m pip install -r /path/to/artemis/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
- Depending on how you installed, now you can run read.py using:
|
||||||
|
- For direct installation, from root ARTEMiS folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 read.py <args>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Or from python3 virtual environment, from root ARTEMiS folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
/path/to/python3-venv/bin/python3 /path/to/artemis/read.py <args>
|
||||||
|
```
|
||||||
|
|
||||||
|
- We need to expose database container port, so that read.py can communicate with the database, inside docker-compose.yml, uncomment 2 lines in the database container declaration (db):
|
||||||
|
|
||||||
|
```
|
||||||
|
#ports:
|
||||||
|
# - "3306:3306"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Now, `docker compose down && docker compose build && docker compose up -d` to restart containers
|
||||||
|
|
||||||
|
Now to insert the data, by default, docker doesn't expose container hostnames to root system, when trying to run read.py against a container, it will Error that hostname is not available, to fix that, we can add database hostname by hand to /etc/hosts:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo <editor of your choice> /etc/hosts
|
||||||
|
add '127.0.0.1 ma.db' to the table
|
||||||
|
save and close
|
||||||
|
```
|
||||||
|
|
||||||
|
- You can remove the line in /etc/hosts and de-expose the database port after successful import (this assumes you're using Persistent DB, as restarting the container without it will clear imported data).
|
||||||
|
|
||||||
|
### Use importer on remote Linux system
|
||||||
|
|
||||||
|
Follow the system and python portion of the guide, installing required packages and python3 modules, Download the ARTEMiS source.
|
||||||
|
|
||||||
|
- Edit core.yaml and insert it into config catalog:
|
||||||
|
|
||||||
|
```
|
||||||
|
database:
|
||||||
|
host: "<hostname of target system>"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Expose port 3306 from database docker container to system, and allow port 3306 through system firewall to expose port to the system from which you will be importing data. (Remember to close down the database ports after finishing!)
|
||||||
|
|
||||||
|
- Import data using read.py
|
||||||
|
|
||||||
|
### Use importer on remote Windows system
|
||||||
|
|
||||||
|
Follow the [windows](docs/INSTALL_WINDOWS.md) guide for installing python dependencies, download the ARTEMiS source.
|
||||||
|
|
||||||
|
- Edit core.yaml and insert it into config catalog:
|
||||||
|
|
||||||
|
```
|
||||||
|
database:
|
||||||
|
host: "<hostname of target system>"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Expose port 3306 from database docker container to system, and allow port 3306 through system firewall to expose port to the system from which you will be importing data.
|
||||||
|
- For Windows, also allow port 3306 outside the system so that read.py can communicate with remote database. (Remember to close down the database ports after finishing!)
|
||||||
|
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## Game does not connect to ARTEMiS Allnet Server
|
||||||
|
Double check your core.yaml if all addresses are correct and ports are correctly set and/or opened.
|
||||||
|
|
||||||
|
## Game does not connect to Title Server
|
||||||
|
Title server hostname requires your actual system hostname, from which you set up the Containers, or it's IP address, you can get the IP by using command `ip a` which will list all interfaces, and one of them should be your system IP (typically under eth0).
|
||||||
|
|
||||||
|
## Unhandled command in AimeDB
|
||||||
|
Make sure you have a proper AimeDB Key added to configuration.
|
||||||
|
|
||||||
|
## Memcached Error in ARTEMiS application causes errors in loading data
|
||||||
|
Currently when running ARTEMiS from master branch, there is a small bug that causes app to always configure memcached service to 127.0.0.1, to fix that, locate cache.py file in core/data, and edit:
|
||||||
|
|
||||||
|
```
|
||||||
|
memcache = pylibmc.Client([hostname]), binary=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```
|
||||||
|
memcache = pylibmc.Client(["ma.memcached"], binary=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
And build the containers again.
|
||||||
|
This will fix errors loading data from server.
|
||||||
|
(This is fixed in development branch)
|
||||||
|
|
||||||
|
## read.py "Can't connect to local server through socket '/run/mysqld/mysqld.sock'"
|
||||||
|
|
||||||
|
sqlalchemy by default reads any ip based connection as socket, thus trying to connect locally, please use a hostname (such as ma.db as in guide, and do not localhost) to force it to use a network interface.
|
||||||
|
|
||||||
|
### TODO:
|
||||||
|
|
||||||
|
- Production environment
|
@ -6,11 +6,18 @@
|
|||||||
- `name`: Name for the server, used by some games in their default MOTDs. Default `ARTEMiS`
|
- `name`: Name for the server, used by some games in their default MOTDs. Default `ARTEMiS`
|
||||||
- `is_develop`: Flags that the server is a development instance without a proxy standing in front of it. Setting to `False` tells the server not to listen for SSL, because the proxy should be handling all SSL-related things, among other things. Default `True`
|
- `is_develop`: Flags that the server is a development instance without a proxy standing in front of it. Setting to `False` tells the server not to listen for SSL, because the proxy should be handling all SSL-related things, among other things. Default `True`
|
||||||
- `threading`: Flags that `reactor.run` should be called via the `Thread` standard library. May provide a speed boost, but removes the ability to kill the server via `Ctrl + C`. Default: `False`
|
- `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
|
||||||
- `log_dir`: Directory to store logs. Server MUST have read and write permissions to this directory or you will have issues. Default `logs`
|
- `log_dir`: Directory to store logs. Server MUST have read and write permissions to this directory or you will have issues. Default `logs`
|
||||||
## Title
|
## Title
|
||||||
- `loglevel`: Logging level for the title server. Default `info`
|
- `loglevel`: Logging level for the title server. Default `info`
|
||||||
- `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`
|
- `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`: 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
|
## Database
|
||||||
- `host`: Host of the database. Default `localhost`
|
- `host`: Host of the database. Default `localhost`
|
||||||
- `username`: Username of the account the server should connect to the database with. Default `aime`
|
- `username`: Username of the account the server should connect to the database with. Default `aime`
|
||||||
|
@ -66,6 +66,28 @@ python read.py --game SDBT --version <version ID> --binfolder /path/to/game/fold
|
|||||||
|
|
||||||
The importer for Chunithm will import: Events, Music, Charge Items and Avatar Accesories.
|
The importer for Chunithm will import: Events, Music, Charge Items and Avatar Accesories.
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
Config file is located in `config/chuni.yaml`.
|
||||||
|
|
||||||
|
| Option | Info |
|
||||||
|
|------------------|----------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `news_msg` | If this is set, the news at the top of the main screen will be displayed (up to Chunithm Paradise Lost) |
|
||||||
|
| `name` | If this is set, all players that are not on a team will use this one by default. |
|
||||||
|
| `rank_scale` | Scales the in-game ranking based on the number of teams within the database |
|
||||||
|
| `use_login_bonus`| This is used to enable the login bonuses |
|
||||||
|
| `crypto` | This option is used to enable the TLS Encryption |
|
||||||
|
|
||||||
|
|
||||||
|
**If you would like to use network encryption, the following will be required underneath but key, iv and hash are required:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
crypto:
|
||||||
|
encrypted_only: False
|
||||||
|
keys:
|
||||||
|
13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
|
||||||
|
```
|
||||||
|
|
||||||
### Database upgrade
|
### Database upgrade
|
||||||
|
|
||||||
Always make sure your database (tables) are up-to-date:
|
Always make sure your database (tables) are up-to-date:
|
||||||
@ -93,6 +115,36 @@ After a failed Online Battle the room will be deleted. The host is used for the
|
|||||||
- Timer countdown should be handled globally and not by one user
|
- Timer countdown should be handled globally and not by one user
|
||||||
- Game can freeze or can crash if someone (especially the host) leaves the matchmaking
|
- Game can freeze or can crash if someone (especially the host) leaves the matchmaking
|
||||||
|
|
||||||
|
### Rivals
|
||||||
|
|
||||||
|
You can configure up to 4 rivals in Chunithm on a per-user basis. There is no UI to do this currently, so in the database, you can do this:
|
||||||
|
```sql
|
||||||
|
INSERT INTO aime.chuni_item_favorite (user, version, favId, favKind) VALUES (<user1>, <version>, <user2>, 2);
|
||||||
|
INSERT INTO aime.chuni_item_favorite (user, version, favId, favKind) VALUES (<user2>, <version>, <user1>, 2);
|
||||||
|
```
|
||||||
|
Note that the version **must match**, otherwise song lookup may not work.
|
||||||
|
|
||||||
|
### Teams
|
||||||
|
|
||||||
|
You can also configure teams for users to be on. There is no UI to do this currently, so in the database, you can do this:
|
||||||
|
```sql
|
||||||
|
INSERT INTO aime.chuni_profile_team (teamName) VALUES (<teamName>);
|
||||||
|
```
|
||||||
|
Team names can be regular ASCII, and they will be displayed ingame.
|
||||||
|
|
||||||
|
On smaller installations, you may also wish to enable scaled team rankings. By default, Chunithm determines team ranking within the first 100 teams. This can be configured in the YAML:
|
||||||
|
```yaml
|
||||||
|
team:
|
||||||
|
rank_scale: True # Scales the in-game ranking based on the number of teams within the database, rather than the default scale of ~100 that the game normally uses.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Favorite songs
|
||||||
|
You can set the songs that will be in a user's Favorite Songs category using the following SQL entries:
|
||||||
|
```sql
|
||||||
|
INSERT INTO aime.chuni_item_favorite (user, version, favId, favKind) VALUES (<user>, <version>, <songId>, 1);
|
||||||
|
```
|
||||||
|
The songId is based on the actual ID within your version of Chunithm.
|
||||||
|
|
||||||
|
|
||||||
## crossbeats REV.
|
## crossbeats REV.
|
||||||
|
|
||||||
@ -147,18 +199,19 @@ For versions pre-dx
|
|||||||
| SDBM | 5 | maimai ORANGE PLUS |
|
| SDBM | 5 | maimai ORANGE PLUS |
|
||||||
| SDCQ | 6 | maimai PiNK |
|
| SDCQ | 6 | maimai PiNK |
|
||||||
| SDCQ | 7 | maimai PiNK PLUS |
|
| SDCQ | 7 | maimai PiNK PLUS |
|
||||||
| SDDK | 8 | maimai MURASAKI |
|
| SDDK | 8 | maimai MURASAKi |
|
||||||
| SDDK | 9 | maimai MURASAKI PLUS |
|
| SDDK | 9 | maimai MURASAKi PLUS |
|
||||||
| SDDZ | 10 | maimai MILK |
|
| SDDZ | 10 | maimai MiLK |
|
||||||
| SDDZ | 11 | maimai MILK PLUS |
|
| SDDZ | 11 | maimai MiLK PLUS |
|
||||||
| SDEY | 12 | maimai FiNALE |
|
| SDEY | 12 | maimai FiNALE |
|
||||||
| SDEZ | 13 | maimai DX |
|
| SDEZ | 13 | maimai DX |
|
||||||
| SDEZ | 14 | maimai DX PLUS |
|
| SDEZ | 14 | maimai DX PLUS |
|
||||||
| SDEZ | 15 | maimai DX Splash |
|
| SDEZ | 15 | maimai DX Splash |
|
||||||
| SDEZ | 16 | maimai DX Splash PLUS |
|
| SDEZ | 16 | maimai DX Splash PLUS |
|
||||||
| SDEZ | 17 | maimai DX Universe |
|
| SDEZ | 17 | maimai DX UNiVERSE |
|
||||||
| SDEZ | 18 | maimai DX Universe PLUS |
|
| SDEZ | 18 | maimai DX UNiVERSE PLUS |
|
||||||
| SDEZ | 19 | maimai DX Festival |
|
| SDEZ | 19 | maimai DX FESTiVAL |
|
||||||
|
| SDEZ | 20 | maimai DX FESTiVAL PLUS |
|
||||||
|
|
||||||
### Importer
|
### Importer
|
||||||
|
|
||||||
@ -217,6 +270,9 @@ Config file is located in `config/diva.yaml`.
|
|||||||
| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased |
|
| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased |
|
||||||
| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased |
|
| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased |
|
||||||
|
|
||||||
|
### Custom PV Lists (databanks)
|
||||||
|
|
||||||
|
In order to use custom PV Lists, simply drop in your .dat files inside of /titles/diva/data/ and make sure they are called PvList0.dat, PvList1.dat, PvList2.dat, PvList3.dat and PvList4.dat exactly.
|
||||||
|
|
||||||
### Database upgrade
|
### Database upgrade
|
||||||
|
|
||||||
@ -261,9 +317,19 @@ Config file is located in `config/ongeki.yaml`.
|
|||||||
| Option | Info |
|
| Option | Info |
|
||||||
| ---------------- | -------------------------------------------------------------------------------------------------------------- |
|
| ---------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||||
| `enabled_gachas` | Enter all gacha IDs for Card Maker to work, other than default may not work due to missing cards added to them |
|
| `enabled_gachas` | Enter all gacha IDs for Card Maker to work, other than default may not work due to missing cards added to them |
|
||||||
|
| `crypto` | This option is used to enable the TLS Encryption |
|
||||||
|
|
||||||
Note: 1149 and higher are only for Card Maker 1.35 and higher and will be ignored on lower versions.
|
Note: 1149 and higher are only for Card Maker 1.35 and higher and will be ignored on lower versions.
|
||||||
|
|
||||||
|
**If you would like to use network encryption, the following will be required underneath but key, iv and hash are required:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
crypto:
|
||||||
|
encrypted_only: False
|
||||||
|
keys:
|
||||||
|
7: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
|
||||||
|
```
|
||||||
|
|
||||||
### Database upgrade
|
### Database upgrade
|
||||||
|
|
||||||
Always make sure your database (tables) are up-to-date:
|
Always make sure your database (tables) are up-to-date:
|
||||||
@ -272,6 +338,68 @@ Always make sure your database (tables) are up-to-date:
|
|||||||
python dbutils.py --game SDDT upgrade
|
python dbutils.py --game SDDT upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Controlling Events (Ranking Event, Technical Challenge Event, Mission Event)
|
||||||
|
|
||||||
|
Events are controlled by 2 types of enabled events:
|
||||||
|
- RankingEvent (type 6), TechChallengeEvent (type 17)
|
||||||
|
- AcceptRankingEvent (type 7), AcceptTechChallengeEvent (type 18)
|
||||||
|
|
||||||
|
Both Ranking and Accept must be enabled for event to function properly
|
||||||
|
|
||||||
|
Event will run for the time specified in startDate and endDate
|
||||||
|
|
||||||
|
AcceptRankingEvent and AcceptTechChallengeEvent are reward period for events, which specify from what startDate until endDate you can collect the rewards for attending the event, so the reward period must start in the future, e.g. :
|
||||||
|
|
||||||
|
- RankingEvent startDate 2023-12-01 - endDate 2023-12-30 - period in which whole event is running
|
||||||
|
- AcceptRankingEvent startDate 2023-12-23 - endDate 2023-12-30 - period in which you can collect rewards for the event
|
||||||
|
|
||||||
|
If player misses the AcceptRankingEvent period - ranking will be invalidated and receive lowest reward from the event (typically 500x money)
|
||||||
|
|
||||||
|
Technical Challenge Song List:
|
||||||
|
|
||||||
|
Songs that are used for Technical Challenge are not stored anywhere in data files, so you need to fill the database table by yourself, you can gather all songs that should be in Technical Challenges from ONGEKI japanese wikis, or, you can create your own sets:
|
||||||
|
|
||||||
|
Database table : `ongeki_static_tech_music`
|
||||||
|
|
||||||
|
```
|
||||||
|
id: Id in table, just increment for each entry
|
||||||
|
version: version of the game you want the tech challenge to be in (from RED and up)
|
||||||
|
eventId: Id of the event in ongeki_static_events, insert the Id of the TechChallengeEvent (type 17) you want the song be assigned to
|
||||||
|
musicId: Id of the song you want to add, use songId from ongeki_static_music table
|
||||||
|
level: Difficulty of the song you want to track during the event, from 0(basic) to 3(master)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Current implementation of Ranking and Technical Challenge Events are updated on every profile save to the Network, and Ranked on each player login, in official specification, calculation for current rank on the network should be done in the maintenance window
|
||||||
|
|
||||||
|
Mission Event (type 13) is a monthly type of event, which is used when another event doesn't have it's own Ranking or Technical Challenge Event running, only one Mission Event should be running at a time, so enable only the specific Mission you want to run currently on the Network
|
||||||
|
|
||||||
|
If you're often trying fresh cards, registering new profiles etc., you can also consider disabling all Announcement Events (type 1), as it will disable all the banners that pop up on login (they show up only once though, so if you click through them once they won't show again)
|
||||||
|
|
||||||
|
Event type 2 in Database are Advertisement Movies, enable only 1 you want to currently play, and disable others
|
||||||
|
|
||||||
|
|
||||||
|
Present and Reward List - populate reward list using read.py
|
||||||
|
|
||||||
|
Create present for players by adding an entry in `ongeki_static_present_list`
|
||||||
|
```
|
||||||
|
id: unique for each entry
|
||||||
|
version: game version you want the present be in
|
||||||
|
presentId: id of the present - starts with 1001 and go up from that, must be unique for each reward(don't set multiple rewardIds with same presentId)
|
||||||
|
presentName: present name which will be shown on the bottom when received
|
||||||
|
rewardId: ID of item from ongeki_static_rewards
|
||||||
|
stock: how many you want to give (like 5 copies of same card, or 10000 money, etc.)
|
||||||
|
message: no idea, can be left empty
|
||||||
|
startDate: date when to start giving out
|
||||||
|
endDate: date when ends
|
||||||
|
```
|
||||||
|
|
||||||
|
After inserting present to the table, add the presentId into players `ongeki_static_item`, where itemKind is 9, itemId is the presentId, and stock set 1 and isValid to 1
|
||||||
|
|
||||||
|
After that, on next login the present should be received (or whenever it supposed to happen)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Card Maker
|
## Card Maker
|
||||||
|
|
||||||
### SDED
|
### SDED
|
||||||
@ -284,15 +412,21 @@ python dbutils.py --game SDDT upgrade
|
|||||||
|
|
||||||
### Support status
|
### Support status
|
||||||
|
|
||||||
* Card Maker 1.30:
|
#### Card Maker 1.30:
|
||||||
* CHUNITHM NEW!!: Yes
|
* CHUNITHM NEW!!: Yes
|
||||||
* maimai DX UNiVERSE: Yes
|
* maimai DX UNiVERSE: Yes
|
||||||
* O.N.G.E.K.I. bright: Yes
|
* O.N.G.E.K.I. bright: Yes
|
||||||
|
|
||||||
* Card Maker 1.35:
|
#### Card Maker 1.35:
|
||||||
* CHUNITHM SUN: Yes (NEW PLUS!! up to A032)
|
* CHUNITHM:
|
||||||
* maimai DX FESTiVAL: Yes (up to A035) (UNiVERSE PLUS up to A031)
|
* NEW!!: Yes
|
||||||
* O.N.G.E.K.I. bright MEMORY: Yes
|
* NEW PLUS!!: Yes (added in A028)
|
||||||
|
* SUN: Yes (added in A032)
|
||||||
|
* maimai DX:
|
||||||
|
* UNiVERSE PLUS: Yes
|
||||||
|
* FESTiVAL: Yes (added in A031)
|
||||||
|
* FESTiVAL PLUS: Yes (added in A035)
|
||||||
|
* O.N.G.E.K.I. bright MEMORY: Yes
|
||||||
|
|
||||||
|
|
||||||
### Importer
|
### Importer
|
||||||
@ -511,6 +645,8 @@ python dbutils.py --game SDEW upgrade
|
|||||||
- Player title is currently static and cannot be changed in-game
|
- Player title is currently static and cannot be changed in-game
|
||||||
- QR Card Scanning currently only load a static hero
|
- QR Card Scanning currently only load a static hero
|
||||||
|
|
||||||
|
**Network hashing in GssSite.dll must be disabled**
|
||||||
|
|
||||||
### Credits for SAO support:
|
### Credits for SAO support:
|
||||||
|
|
||||||
- Midorica - Limited Network Support
|
- Midorica - Limited Network Support
|
||||||
@ -646,3 +782,4 @@ python dbutils.py --game SDGT upgrade
|
|||||||
- Kinako: For helping with the timeRelease unlocking of courses and special mode
|
- Kinako: For helping with the timeRelease unlocking of courses and special mode
|
||||||
|
|
||||||
A huge thanks to all people who helped shaping this project to what it is now and don't want to be mentioned here.
|
A huge thanks to all people who helped shaping this project to what it is now and don't want to be mentioned here.
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
server:
|
server:
|
||||||
enable: True
|
enable: True
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
|
news_msg: ""
|
||||||
|
|
||||||
team:
|
team:
|
||||||
name: ARTEMiS
|
name: ARTEMiS # If this is set, all players that are not on a team will use this one by default.
|
||||||
|
|
||||||
mods:
|
mods:
|
||||||
use_login_bonus: True
|
use_login_bonus: True
|
||||||
|
@ -4,6 +4,7 @@ server:
|
|||||||
allow_unregistered_serials: True
|
allow_unregistered_serials: True
|
||||||
name: "ARTEMiS"
|
name: "ARTEMiS"
|
||||||
is_develop: True
|
is_develop: True
|
||||||
|
is_using_proxy: False
|
||||||
threading: False
|
threading: False
|
||||||
log_dir: "logs"
|
log_dir: "logs"
|
||||||
check_arcade_ip: False
|
check_arcade_ip: False
|
||||||
@ -13,6 +14,12 @@ title:
|
|||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
hostname: "localhost"
|
hostname: "localhost"
|
||||||
port: 8080
|
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:
|
database:
|
||||||
host: "localhost"
|
host: "localhost"
|
||||||
@ -24,6 +31,7 @@ database:
|
|||||||
sha2_password: False
|
sha2_password: False
|
||||||
loglevel: "warn"
|
loglevel: "warn"
|
||||||
user_table_autoincrement_start: 10000
|
user_table_autoincrement_start: 10000
|
||||||
|
enable_memcached: True
|
||||||
memcached_host: "localhost"
|
memcached_host: "localhost"
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
|
@ -1,9 +1,3 @@
|
|||||||
server:
|
server:
|
||||||
enable: True
|
enable: True
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
hostname: "localhost"
|
|
||||||
ssl_enable: False
|
|
||||||
port: 8082
|
|
||||||
port_secure: 443
|
|
||||||
ssl_cert: "cert/title.crt"
|
|
||||||
ssl_key: "cert/title.key"
|
|
||||||
|
@ -24,8 +24,7 @@ server {
|
|||||||
|
|
||||||
# SSL titles, comment out if you don't plan on accepting SSL titles
|
# SSL titles, comment out if you don't plan on accepting SSL titles
|
||||||
server {
|
server {
|
||||||
listen 443 ssl default_server;
|
listen 443 ssl;
|
||||||
listen [::]:443 ssl default_server;
|
|
||||||
server_name your.hostname.here;
|
server_name your.hostname.here;
|
||||||
|
|
||||||
ssl_certificate /path/to/cert/title.crt;
|
ssl_certificate /path/to/cert/title.crt;
|
||||||
@ -55,7 +54,7 @@ server {
|
|||||||
ssl_session_tickets off;
|
ssl_session_tickets off;
|
||||||
|
|
||||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers "ALL:@SECLEVEL=1";
|
ssl_ciphers "ALL:@SECLEVEL=0";
|
||||||
ssl_prefer_server_ciphers off;
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@ -75,7 +74,7 @@ server {
|
|||||||
ssl_session_tickets off;
|
ssl_session_tickets off;
|
||||||
|
|
||||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers "ALL:@SECLEVEL=1";
|
ssl_ciphers "ALL:@SECLEVEL=0";
|
||||||
ssl_prefer_server_ciphers off;
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@ -85,28 +84,6 @@ server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# CXB, comment this out if you don't plan on serving crossbeats.
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name cxb.hostname.here;
|
|
||||||
|
|
||||||
ssl_certificate /path/to/cert/cxb.pem;
|
|
||||||
ssl_certificate_key /path/to/cert/cxb.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=1";
|
|
||||||
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://localhost:8080/SDBT/104/;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Frontend, set to redirect to HTTPS. Comment out if you don't intend to use the frontend
|
# Frontend, set to redirect to HTTPS. Comment out if you don't intend to use the frontend
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
server:
|
server:
|
||||||
enable: True
|
enable: True
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
|
use_https: False
|
||||||
|
|
||||||
gachas:
|
gachas:
|
||||||
enabled_gachas:
|
enabled_gachas:
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
server:
|
server:
|
||||||
hostname: "localhost"
|
|
||||||
enable: True
|
enable: True
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
port: 9000
|
|
||||||
auto_register: True
|
auto_register: True
|
||||||
|
|
||||||
|
crypt:
|
||||||
|
enable: False
|
||||||
|
key: ""
|
||||||
|
iv: ""
|
||||||
|
|
||||||
|
hash:
|
||||||
|
verify_hash: False
|
||||||
|
hash_base: ""
|
50
index.py
50
index.py
@ -22,8 +22,8 @@ class HttpDispatcher(resource.Resource):
|
|||||||
self.map_post = Mapper()
|
self.map_post = Mapper()
|
||||||
self.logger = logging.getLogger("core")
|
self.logger = logging.getLogger("core")
|
||||||
|
|
||||||
self.allnet = AllnetServlet(cfg, config_dir)
|
|
||||||
self.title = TitleServlet(cfg, config_dir)
|
self.title = TitleServlet(cfg, config_dir)
|
||||||
|
self.allnet = AllnetServlet(cfg, config_dir)
|
||||||
self.mucha = MuchaServlet(cfg, config_dir)
|
self.mucha = MuchaServlet(cfg, config_dir)
|
||||||
|
|
||||||
self.map_get.connect(
|
self.map_get.connect(
|
||||||
@ -144,22 +144,32 @@ class HttpDispatcher(resource.Resource):
|
|||||||
conditions=dict(method=["POST"]),
|
conditions=dict(method=["POST"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.map_get.connect(
|
for code, game in self.title.title_registry.items():
|
||||||
"title_get",
|
get_matchers, post_matchers = game.get_endpoint_matchers()
|
||||||
"/{game}/{version}/{endpoint:.*?}",
|
|
||||||
controller="title",
|
for m in get_matchers:
|
||||||
action="render_GET",
|
self.map_get.connect(
|
||||||
conditions=dict(method=["GET"]),
|
"title_get",
|
||||||
requirements=dict(game=R"S..."),
|
m[1],
|
||||||
)
|
controller="title",
|
||||||
self.map_post.connect(
|
action="render_GET",
|
||||||
"title_post",
|
title=code,
|
||||||
"/{game}/{version}/{endpoint:.*?}",
|
subaction=m[0],
|
||||||
controller="title",
|
conditions=dict(method=["GET"]),
|
||||||
action="render_POST",
|
requirements=m[2],
|
||||||
conditions=dict(method=["POST"]),
|
)
|
||||||
requirements=dict(game=R"S..."),
|
|
||||||
)
|
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:
|
def render_GET(self, request: Request) -> bytes:
|
||||||
test = self.map_get.match(request.uri.decode())
|
test = self.map_get.match(request.uri.decode())
|
||||||
@ -279,6 +289,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
allnet_server_str = f"tcp:{cfg.allnet.port}:interface={cfg.server.listen_address}"
|
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_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}"
|
adb_server_str = f"tcp:{cfg.aimedb.port}:interface={cfg.server.listen_address}"
|
||||||
frontend_server_str = (
|
frontend_server_str = (
|
||||||
f"tcp:{cfg.frontend.port}:interface={cfg.server.listen_address}"
|
f"tcp:{cfg.frontend.port}:interface={cfg.server.listen_address}"
|
||||||
@ -313,6 +324,11 @@ if __name__ == "__main__":
|
|||||||
server.Site(dispatcher)
|
server.Site(dispatcher)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if cfg.title.port_ssl > 0:
|
||||||
|
endpoints.serverFromString(reactor, title_https_server_str).listen(
|
||||||
|
server.Site(dispatcher)
|
||||||
|
)
|
||||||
|
|
||||||
if cfg.server.threading:
|
if cfg.server.threading:
|
||||||
Thread(target=reactor.run, args=(False,)).start()
|
Thread(target=reactor.run, args=(False,)).start()
|
||||||
else:
|
else:
|
||||||
|
@ -11,7 +11,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
|||||||
+ All versions + omnimix
|
+ All versions + omnimix
|
||||||
|
|
||||||
+ maimai DX
|
+ maimai DX
|
||||||
+ All versions up to FESTiVAL
|
+ All versions up to FESTiVAL PLUS
|
||||||
|
|
||||||
+ Hatsune Miku: Project DIVA Arcade
|
+ Hatsune Miku: Project DIVA Arcade
|
||||||
+ All versions
|
+ All versions
|
||||||
@ -43,7 +43,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
|||||||
- mysql/mariadb server
|
- mysql/mariadb server
|
||||||
|
|
||||||
## Setup guides
|
## Setup guides
|
||||||
Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md) and [ubuntu](docs/INSTALL_UBUNTU.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
|
## Game specific information
|
||||||
Read [Games specific info](docs/game_specific_info.md) for all supported games, importer settings, configuration option and database upgrades.
|
Read [Games specific info](docs/game_specific_info.md) for all supported games, importer settings, configuration option and database upgrades.
|
||||||
|
@ -10,7 +10,7 @@ from core.config import CoreConfig
|
|||||||
from titles.chuni.const import ChuniConstants
|
from titles.chuni.const import ChuniConstants
|
||||||
from titles.chuni.database import ChuniData
|
from titles.chuni.database import ChuniData
|
||||||
from titles.chuni.config import ChuniConfig
|
from titles.chuni.config import ChuniConfig
|
||||||
|
SCORE_BUFFER = {}
|
||||||
|
|
||||||
class ChuniBase:
|
class ChuniBase:
|
||||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||||
@ -181,21 +181,50 @@ class ChuniBase:
|
|||||||
return {"type": data["type"], "length": 0, "gameIdlistList": []}
|
return {"type": data["type"], "length": 0, "gameIdlistList": []}
|
||||||
|
|
||||||
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": "0", "gameMessageList": []}
|
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"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
def handle_get_game_ranking_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_ranking_api_request(self, data: Dict) -> Dict:
|
||||||
return {"type": data["type"], "gameRankingList": []}
|
rankings = self.data.score.get_rankings(self.version)
|
||||||
|
return {"type": data["type"], "gameRankingList": rankings}
|
||||||
|
|
||||||
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": []}
|
return {"type": data["type"], "length": 0, "gameSaleList": []}
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
reboot_start = datetime.strftime(
|
# if reboot start/end time is not defined use the default behavior of being a few hours ago
|
||||||
datetime.now() - timedelta(hours=4), self.date_time_format
|
if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
|
||||||
)
|
reboot_start = datetime.strftime(
|
||||||
reboot_end = datetime.strftime(
|
datetime.utcnow() + timedelta(hours=6), self.date_time_format
|
||||||
datetime.now() - timedelta(hours=3), self.date_time_format
|
)
|
||||||
)
|
reboot_end = datetime.strftime(
|
||||||
|
datetime.utcnow() + timedelta(hours=7), self.date_time_format
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# get current datetime in JST
|
||||||
|
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")
|
||||||
|
|
||||||
|
# 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'))
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"gameSetting": {
|
"gameSetting": {
|
||||||
"dataVersion": "1.00.00",
|
"dataVersion": "1.00.00",
|
||||||
@ -361,11 +390,70 @@ class ChuniBase:
|
|||||||
"userDuelList": duel_list,
|
"userDuelList": duel_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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 = self.data.score.get_rival_music(rival_id)
|
||||||
|
|
||||||
|
# Process the entries based on max_count and nextIndex
|
||||||
|
for music in all_entries[next_index:]:
|
||||||
|
music_id = music["musicId"]
|
||||||
|
level = music["level"]
|
||||||
|
score = music["scoreMax"]
|
||||||
|
rank = music["scoreRank"]
|
||||||
|
|
||||||
|
# Create a music entry for the current music_id
|
||||||
|
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": []
|
||||||
|
}
|
||||||
|
user_rival_music_list.append(music_entry)
|
||||||
|
|
||||||
|
# Create a level entry for the current level
|
||||||
|
level_entry = {
|
||||||
|
"level": level,
|
||||||
|
"scoreMax": score,
|
||||||
|
"scoreRank": rank
|
||||||
|
}
|
||||||
|
music_entry["userRivalMusicDetailList"].append(level_entry)
|
||||||
|
|
||||||
|
# Calculate the length for each "musicId" by counting the levels
|
||||||
|
for music_entry in user_rival_music_list:
|
||||||
|
music_entry["length"] = len(music_entry["userRivalMusicDetailList"])
|
||||||
|
|
||||||
|
# Prepare the result dictionary with user rival music data
|
||||||
|
result = {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"rivalId": data["rivalId"],
|
||||||
|
"nextIndex": str(next_index + len(all_entries) if len(all_entries) <= len(user_rival_music_list) else -1),
|
||||||
|
"userRivalMusicList": user_rival_music_list[:max_count]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
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 = []
|
user_fav_item_list = []
|
||||||
|
|
||||||
# still needs to be implemented on WebUI
|
# still needs to be implemented on WebUI
|
||||||
# 1: Music, 3: Character
|
# 1: Music, 2: User, 3: Character
|
||||||
fav_list = self.data.item.get_all_favorites(
|
fav_list = self.data.item.get_all_favorites(
|
||||||
data["userId"], self.version, fav_kind=int(data["kind"])
|
data["userId"], self.version, fav_kind=int(data["kind"])
|
||||||
)
|
)
|
||||||
@ -490,22 +578,39 @@ class ChuniBase:
|
|||||||
tmp.pop("id")
|
tmp.pop("id")
|
||||||
|
|
||||||
for song in song_list:
|
for song in song_list:
|
||||||
|
score_buf = SCORE_BUFFER.get(str(data["userId"])) or []
|
||||||
if song["userMusicDetailList"][0]["musicId"] == tmp["musicId"]:
|
if song["userMusicDetailList"][0]["musicId"] == tmp["musicId"]:
|
||||||
found = True
|
found = True
|
||||||
song["userMusicDetailList"].append(tmp)
|
song["userMusicDetailList"].append(tmp)
|
||||||
song["length"] = len(song["userMusicDetailList"])
|
song["length"] = len(song["userMusicDetailList"])
|
||||||
|
score_buf.append(tmp["musicId"])
|
||||||
|
SCORE_BUFFER[str(data["userId"])] = score_buf
|
||||||
|
|
||||||
if not found:
|
score_buf = SCORE_BUFFER.get(str(data["userId"])) or []
|
||||||
|
if not found and tmp["musicId"] not in score_buf:
|
||||||
song_list.append({"length": 1, "userMusicDetailList": [tmp]})
|
song_list.append({"length": 1, "userMusicDetailList": [tmp]})
|
||||||
|
score_buf.append(tmp["musicId"])
|
||||||
|
SCORE_BUFFER[str(data["userId"])] = score_buf
|
||||||
|
|
||||||
if len(song_list) >= max_ct:
|
if len(song_list) >= max_ct:
|
||||||
break
|
break
|
||||||
|
|
||||||
if len(song_list) >= next_idx + max_ct:
|
try:
|
||||||
next_idx += max_ct
|
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:
|
else:
|
||||||
next_idx = -1
|
next_idx = -1
|
||||||
|
SCORE_BUFFER[str(data["userId"])] = []
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": len(song_list),
|
"length": len(song_list),
|
||||||
@ -600,39 +705,94 @@ class ChuniBase:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_team_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_team_api_request(self, data: Dict) -> Dict:
|
||||||
# TODO: use the database "chuni_profile_team" with a GUI
|
# Default values
|
||||||
|
team_id = 65535
|
||||||
team_name = self.game_cfg.team.team_name
|
team_name = self.game_cfg.team.team_name
|
||||||
if team_name == "":
|
team_rank = 0
|
||||||
|
|
||||||
|
# Get user profile
|
||||||
|
profile = self.data.profile.get_profile_data(data["userId"], self.version)
|
||||||
|
if profile and profile["teamId"]:
|
||||||
|
# Get team by id
|
||||||
|
team = self.data.profile.get_team_by_id(profile["teamId"])
|
||||||
|
|
||||||
|
if team:
|
||||||
|
team_id = team["id"]
|
||||||
|
team_name = team["teamName"]
|
||||||
|
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 == "":
|
||||||
return {"userId": data["userId"], "teamId": 0}
|
return {"userId": data["userId"], "teamId": 0}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"teamId": 1,
|
"teamId": team_id,
|
||||||
"teamRank": 1,
|
"teamRank": team_rank,
|
||||||
"teamName": team_name,
|
"teamName": team_name,
|
||||||
"userTeamPoint": {
|
"userTeamPoint": {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"teamId": 1,
|
"teamId": team_id,
|
||||||
"orderId": 1,
|
"orderId": 1,
|
||||||
"teamPoint": 1,
|
"teamPoint": 1,
|
||||||
"aggrDate": data["playDate"],
|
"aggrDate": data["playDate"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"nextIndex": 0,
|
"nextIndex": -1,
|
||||||
"teamCourseSettingList": [],
|
"teamCourseSettingList": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def handle_get_team_course_setting_api_request_proto(self, data: Dict) -> Dict:
|
||||||
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"length": 1,
|
||||||
|
"nextIndex": -1,
|
||||||
|
"teamCourseSettingList": [
|
||||||
|
{
|
||||||
|
"orderId": 1,
|
||||||
|
"courseId": 1,
|
||||||
|
"classId": 1,
|
||||||
|
"ruleId": 1,
|
||||||
|
"courseName": "Test",
|
||||||
|
"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}
|
||||||
|
],
|
||||||
|
"teamCourseRankingInfoList": [],
|
||||||
|
"recodeDate": "2099-12-31 11:59:99.0",
|
||||||
|
"isPlayed": False
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"nextIndex": 0,
|
"nextIndex": -1,
|
||||||
"teamCourseRuleList": [],
|
"teamCourseRuleList": []
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_get_team_course_rule_api_request_proto(self, data: Dict) -> Dict:
|
||||||
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"length": 1,
|
||||||
|
"nextIndex": -1,
|
||||||
|
"teamCourseRuleList": [
|
||||||
|
{
|
||||||
|
"recoveryLife": 0,
|
||||||
|
"clearLife": 100,
|
||||||
|
"damageMiss": 1,
|
||||||
|
"damageAttack": 1,
|
||||||
|
"damageJustice": 1,
|
||||||
|
"damageJusticeC": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_upsert_user_all_api_request(self, data: Dict) -> Dict:
|
def handle_upsert_user_all_api_request(self, data: Dict) -> Dict:
|
||||||
@ -706,12 +866,28 @@ class ChuniBase:
|
|||||||
playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"])
|
playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"])
|
||||||
playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"])
|
playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"])
|
||||||
playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"])
|
playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"])
|
||||||
self.data.score.put_playlog(user_id, playlog)
|
self.data.score.put_playlog(user_id, playlog, self.version)
|
||||||
|
|
||||||
if "userTeamPoint" in upsert:
|
if "userTeamPoint" in upsert:
|
||||||
# TODO: team stuff
|
team_points = upsert["userTeamPoint"]
|
||||||
pass
|
try:
|
||||||
|
for tp in team_points:
|
||||||
|
if tp["teamId"] != '65535':
|
||||||
|
# Fetch the current team data
|
||||||
|
current_team = self.data.profile.get_team_by_id(tp["teamId"])
|
||||||
|
|
||||||
|
# Calculate the new teamPoint
|
||||||
|
new_team_point = int(tp["teamPoint"]) + current_team["teamPoint"]
|
||||||
|
|
||||||
|
# Prepare the data to update
|
||||||
|
team_data = {
|
||||||
|
"teamPoint": new_team_point
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update the 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
|
||||||
if "userMapAreaList" in upsert:
|
if "userMapAreaList" in upsert:
|
||||||
for map_area in upsert["userMapAreaList"]:
|
for map_area in upsert["userMapAreaList"]:
|
||||||
self.data.item.put_map_area(user_id, map_area)
|
self.data.item.put_map_area(user_id, map_area)
|
||||||
|
@ -20,6 +20,12 @@ class ChuniServerConfig:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def news_msg(self) -> str:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "chuni", "server", "news_msg", default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChuniTeamConfig:
|
class ChuniTeamConfig:
|
||||||
def __init__(self, parent_config: "ChuniConfig") -> None:
|
def __init__(self, parent_config: "ChuniConfig") -> None:
|
||||||
@ -30,6 +36,11 @@ class ChuniTeamConfig:
|
|||||||
return CoreConfig.get_config_field(
|
return CoreConfig.get_config_field(
|
||||||
self.__config, "chuni", "team", "name", default=""
|
self.__config, "chuni", "team", "name", default=""
|
||||||
)
|
)
|
||||||
|
@property
|
||||||
|
def rank_scale(self) -> str:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "chuni", "team", "rank_scale", default="False"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChuniModsConfig:
|
class ChuniModsConfig:
|
||||||
|
@ -11,30 +11,31 @@ from Crypto.Util.Padding import pad
|
|||||||
from Crypto.Protocol.KDF import PBKDF2
|
from Crypto.Protocol.KDF import PBKDF2
|
||||||
from Crypto.Hash import SHA1
|
from Crypto.Hash import SHA1
|
||||||
from os import path
|
from os import path
|
||||||
from typing import Tuple, Dict
|
from typing import Tuple, Dict, List
|
||||||
|
|
||||||
from core import CoreConfig, Utils
|
from core import CoreConfig, Utils
|
||||||
from titles.chuni.config import ChuniConfig
|
from core.title import BaseServlet
|
||||||
from titles.chuni.const import ChuniConstants
|
from .config import ChuniConfig
|
||||||
from titles.chuni.base import ChuniBase
|
from .const import ChuniConstants
|
||||||
from titles.chuni.plus import ChuniPlus
|
from .base import ChuniBase
|
||||||
from titles.chuni.air import ChuniAir
|
from .plus import ChuniPlus
|
||||||
from titles.chuni.airplus import ChuniAirPlus
|
from .air import ChuniAir
|
||||||
from titles.chuni.star import ChuniStar
|
from .airplus import ChuniAirPlus
|
||||||
from titles.chuni.starplus import ChuniStarPlus
|
from .star import ChuniStar
|
||||||
from titles.chuni.amazon import ChuniAmazon
|
from .starplus import ChuniStarPlus
|
||||||
from titles.chuni.amazonplus import ChuniAmazonPlus
|
from .amazon import ChuniAmazon
|
||||||
from titles.chuni.crystal import ChuniCrystal
|
from .amazonplus import ChuniAmazonPlus
|
||||||
from titles.chuni.crystalplus import ChuniCrystalPlus
|
from .crystal import ChuniCrystal
|
||||||
from titles.chuni.paradise import ChuniParadise
|
from .crystalplus import ChuniCrystalPlus
|
||||||
from titles.chuni.new import ChuniNew
|
from .paradise import ChuniParadise
|
||||||
from titles.chuni.newplus import ChuniNewPlus
|
from .new import ChuniNew
|
||||||
from titles.chuni.sun import ChuniSun
|
from .newplus import ChuniNewPlus
|
||||||
|
from .sun import ChuniSun
|
||||||
|
|
||||||
|
|
||||||
class ChuniServlet:
|
class ChuniServlet(BaseServlet):
|
||||||
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||||
self.core_cfg = core_cfg
|
super().__init__(core_cfg, cfg_dir)
|
||||||
self.game_cfg = ChuniConfig()
|
self.game_cfg = ChuniConfig()
|
||||||
self.hash_table: Dict[Dict[str, str]] = {}
|
self.hash_table: Dict[Dict[str, str]] = {}
|
||||||
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
|
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
|
||||||
@ -115,10 +116,19 @@ class ChuniServlet:
|
|||||||
f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}"
|
f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]:
|
||||||
|
return (
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
("render_POST", "/{version}/ChuniServlet/{endpoint}", {}),
|
||||||
|
("render_POST", "/{version}/ChuniServlet/MatchingServer/{endpoint}", {})
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_allnet_info(
|
def is_game_enabled(
|
||||||
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
|
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
|
||||||
) -> Tuple[bool, str, str]:
|
) -> bool:
|
||||||
game_cfg = ChuniConfig()
|
game_cfg = ChuniConfig()
|
||||||
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
|
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
|
||||||
game_cfg.update(
|
game_cfg.update(
|
||||||
@ -126,26 +136,26 @@ class ChuniServlet:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not game_cfg.server.enable:
|
if not game_cfg.server.enable:
|
||||||
return (False, "", "")
|
return False
|
||||||
|
|
||||||
if core_cfg.server.is_develop:
|
return True
|
||||||
return (
|
|
||||||
True,
|
|
||||||
f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/",
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
|
|
||||||
return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "")
|
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_ver}/", "")
|
||||||
|
|
||||||
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
|
return (f"http://{self.core_cfg.title.hostname}/{game_ver}/", "")
|
||||||
if url_path.lower() == "ping":
|
|
||||||
|
def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes:
|
||||||
|
endpoint = matchers['endpoint']
|
||||||
|
version = int(matchers['version'])
|
||||||
|
|
||||||
|
if endpoint.lower() == "ping":
|
||||||
return zlib.compress(b'{"returnCode": "1"}')
|
return zlib.compress(b'{"returnCode": "1"}')
|
||||||
|
|
||||||
req_raw = request.content.getvalue()
|
req_raw = request.content.getvalue()
|
||||||
url_split = url_path.split("/")
|
|
||||||
encrtped = False
|
encrtped = False
|
||||||
internal_ver = 0
|
internal_ver = 0
|
||||||
endpoint = url_split[len(url_split) - 1]
|
|
||||||
client_ip = Utils.get_ip_addr(request)
|
client_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
if version < 105: # 1.0
|
if version < 105: # 1.0
|
||||||
|
@ -3,6 +3,7 @@ from datetime import datetime, timedelta
|
|||||||
from random import randint
|
from random import randint
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
import pytz
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from titles.chuni.const import ChuniConstants
|
from titles.chuni.const import ChuniConstants
|
||||||
from titles.chuni.database import ChuniData
|
from titles.chuni.database import ChuniData
|
||||||
@ -31,12 +32,29 @@ class ChuniNew(ChuniBase):
|
|||||||
match_end = datetime.strftime(
|
match_end = datetime.strftime(
|
||||||
datetime.utcnow() + timedelta(hours=16), self.date_time_format
|
datetime.utcnow() + timedelta(hours=16), self.date_time_format
|
||||||
)
|
)
|
||||||
reboot_start = datetime.strftime(
|
# if reboot start/end time is not defined use the default behavior of being a few hours ago
|
||||||
datetime.utcnow() + timedelta(hours=6), self.date_time_format
|
if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
|
||||||
)
|
reboot_start = datetime.strftime(
|
||||||
reboot_end = datetime.strftime(
|
datetime.utcnow() + timedelta(hours=6), self.date_time_format
|
||||||
datetime.utcnow() + timedelta(hours=7), self.date_time_format
|
)
|
||||||
)
|
reboot_end = datetime.strftime(
|
||||||
|
datetime.utcnow() + timedelta(hours=7), self.date_time_format
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# get current datetime in JST
|
||||||
|
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")
|
||||||
|
|
||||||
|
# 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'))
|
||||||
|
|
||||||
|
# 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)
|
||||||
return {
|
return {
|
||||||
"gameSetting": {
|
"gameSetting": {
|
||||||
"isMaintenance": False,
|
"isMaintenance": False,
|
||||||
@ -53,11 +71,11 @@ class ChuniNew(ChuniBase):
|
|||||||
"matchErrorLimit": 9999,
|
"matchErrorLimit": 9999,
|
||||||
"romVersion": self.game_cfg.version.version(self.version)["rom"],
|
"romVersion": self.game_cfg.version.version(self.version)["rom"],
|
||||||
"dataVersion": self.game_cfg.version.version(self.version)["data"],
|
"dataVersion": self.game_cfg.version.version(self.version)["data"],
|
||||||
"matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
"matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/200/ChuniServlet/",
|
||||||
"matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
"matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/200/ChuniServlet/",
|
||||||
# might be really important for online battle to connect the cabs via UDP port 50201
|
# might be really important for online battle to connect the cabs via UDP port 50201
|
||||||
"udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
"udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/200/ChuniServlet/",
|
||||||
"reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
"reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/200/ChuniServlet/",
|
||||||
},
|
},
|
||||||
"isDumpUpload": False,
|
"isDumpUpload": False,
|
||||||
"isAou": False,
|
"isAou": False,
|
||||||
|
@ -21,16 +21,16 @@ class ChuniNewPlus(ChuniNew):
|
|||||||
]
|
]
|
||||||
ret["gameSetting"][
|
ret["gameSetting"][
|
||||||
"matchingUri"
|
"matchingUri"
|
||||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/205/ChuniServlet/"
|
||||||
ret["gameSetting"][
|
ret["gameSetting"][
|
||||||
"matchingUriX"
|
"matchingUriX"
|
||||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/205/ChuniServlet/"
|
||||||
ret["gameSetting"][
|
ret["gameSetting"][
|
||||||
"udpHolePunchUri"
|
"udpHolePunchUri"
|
||||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/205/ChuniServlet/"
|
||||||
ret["gameSetting"][
|
ret["gameSetting"][
|
||||||
"reflectorUri"
|
"reflectorUri"
|
||||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/205/ChuniServlet/"
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||||
|
@ -637,3 +637,69 @@ class ChuniProfileData(BaseData):
|
|||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
|
def get_team_by_id(self, team_id: int) -> Optional[Row]:
|
||||||
|
sql = select(team).where(team.c.id == team_id)
|
||||||
|
result = self.execute(sql)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.fetchone()
|
||||||
|
|
||||||
|
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 = 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
|
||||||
|
for i, row in enumerate(result, start=1):
|
||||||
|
if row.id == team_id:
|
||||||
|
rank = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# Return the rank if found, or a default rank otherwise
|
||||||
|
return rank if rank is not None else 0
|
||||||
|
|
||||||
|
# RIP scaled team ranking. Gone, but forgotten
|
||||||
|
# def get_team_rank_scaled(self, team_id: int) -> int:
|
||||||
|
|
||||||
|
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 = 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
|
||||||
|
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))
|
||||||
|
|
||||||
|
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]
|
||||||
|
return {
|
||||||
|
"total_play_count": total_play_count
|
||||||
|
}
|
||||||
|
@ -6,7 +6,7 @@ from sqlalchemy.schema import ForeignKey
|
|||||||
from sqlalchemy.engine import Row
|
from sqlalchemy.engine import Row
|
||||||
from sqlalchemy.sql import func, select
|
from sqlalchemy.sql import func, select
|
||||||
from sqlalchemy.dialects.mysql import insert
|
from sqlalchemy.dialects.mysql import insert
|
||||||
|
from sqlalchemy.sql.expression import exists
|
||||||
from core.data.schema import BaseData, metadata
|
from core.data.schema import BaseData, metadata
|
||||||
|
|
||||||
course = Table(
|
course = Table(
|
||||||
@ -189,9 +189,28 @@ class ChuniScoreData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
def put_playlog(self, aime_id: int, playlog_data: Dict) -> 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
|
||||||
|
romVer = {
|
||||||
|
10: "1.50.0",
|
||||||
|
9: "1.45.0",
|
||||||
|
8: "1.40.0",
|
||||||
|
7: "1.35.0",
|
||||||
|
6: "1.30.0",
|
||||||
|
5: "1.25.0",
|
||||||
|
4: "1.20.0",
|
||||||
|
3: "1.15.0",
|
||||||
|
2: "1.10.0",
|
||||||
|
1: "1.05.0",
|
||||||
|
0: "1.00.0"
|
||||||
|
}
|
||||||
|
|
||||||
playlog_data["user"] = aime_id
|
playlog_data["user"] = aime_id
|
||||||
playlog_data = self.fix_bools(playlog_data)
|
playlog_data = self.fix_bools(playlog_data)
|
||||||
|
if "romVersion" not in playlog_data:
|
||||||
|
playlog_data["romVersion"] = romVer.get(version, "1.00.0")
|
||||||
|
|
||||||
sql = insert(playlog).values(**playlog_data)
|
sql = insert(playlog).values(**playlog_data)
|
||||||
conflict = sql.on_duplicate_key_update(**playlog_data)
|
conflict = sql.on_duplicate_key_update(**playlog_data)
|
||||||
@ -200,3 +219,40 @@ class ChuniScoreData(BaseData):
|
|||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
13: "2.10%",
|
||||||
|
12: "2.05%",
|
||||||
|
11: "2.00%",
|
||||||
|
10: "1.50%",
|
||||||
|
9: "1.45%",
|
||||||
|
8: "1.40%",
|
||||||
|
7: "1.35%",
|
||||||
|
6: "1.30%",
|
||||||
|
5: "1.25%",
|
||||||
|
4: "1.20%",
|
||||||
|
3: "1.15%",
|
||||||
|
2: "1.10%",
|
||||||
|
1: "1.05%",
|
||||||
|
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 = self.execute(sql)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
rows = result.fetchall()
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
def get_rival_music(self, rival_id: int) -> Optional[List[Dict]]:
|
||||||
|
sql = select(best_score).where(best_score.c.user == rival_id)
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.fetchall()
|
||||||
|
|
||||||
|
@ -453,6 +453,15 @@ class ChuniStaticData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.fetchone()
|
return result.fetchone()
|
||||||
|
|
||||||
|
def get_song(self, music_id: int) -> Optional[Row]:
|
||||||
|
sql = music.select(music.c.id == music_id)
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.fetchone()
|
||||||
|
|
||||||
|
|
||||||
def put_avatar(
|
def put_avatar(
|
||||||
self,
|
self,
|
||||||
version: int,
|
version: int,
|
||||||
|
@ -17,16 +17,16 @@ class ChuniSun(ChuniNewPlus):
|
|||||||
ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"]
|
ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"]
|
||||||
ret["gameSetting"][
|
ret["gameSetting"][
|
||||||
"matchingUri"
|
"matchingUri"
|
||||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
|
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/210/ChuniServlet/"
|
||||||
ret["gameSetting"][
|
ret["gameSetting"][
|
||||||
"matchingUriX"
|
"matchingUriX"
|
||||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
|
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/210/ChuniServlet/"
|
||||||
ret["gameSetting"][
|
ret["gameSetting"][
|
||||||
"udpHolePunchUri"
|
"udpHolePunchUri"
|
||||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
|
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/210/ChuniServlet/"
|
||||||
ret["gameSetting"][
|
ret["gameSetting"][
|
||||||
"reflectorUri"
|
"reflectorUri"
|
||||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
|
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/210/ChuniServlet/"
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||||
|
@ -4,7 +4,9 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
import pytz
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
|
from core.utils import Utils
|
||||||
from core.data.cache import cached
|
from core.data.cache import cached
|
||||||
from titles.cm.const import CardMakerConstants
|
from titles.cm.const import CardMakerConstants
|
||||||
from titles.cm.config import CardMakerConfig
|
from titles.cm.config import CardMakerConfig
|
||||||
@ -28,8 +30,8 @@ class CardMakerBase:
|
|||||||
return version.replace(".", "")[:3]
|
return version.replace(".", "")[:3]
|
||||||
|
|
||||||
def handle_get_game_connect_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_connect_api_request(self, data: Dict) -> Dict:
|
||||||
if self.core_cfg.server.is_develop:
|
if 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}:{self.core_cfg.title.port}"
|
uri = f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}"
|
||||||
else:
|
else:
|
||||||
uri = f"http://{self.core_cfg.title.hostname}"
|
uri = f"http://{self.core_cfg.title.hostname}"
|
||||||
|
|
||||||
@ -43,13 +45,13 @@ class CardMakerBase:
|
|||||||
{
|
{
|
||||||
"modelKind": 0,
|
"modelKind": 0,
|
||||||
"type": 1,
|
"type": 1,
|
||||||
"titleUri": f"{uri}/SDHD/{self._parse_int_ver(games_ver['chuni'])}/",
|
"titleUri": f"{uri}/{self._parse_int_ver(games_ver['chuni'])}/ChuniServlet/",
|
||||||
},
|
},
|
||||||
# maimai DX
|
# maimai DX
|
||||||
{
|
{
|
||||||
"modelKind": 1,
|
"modelKind": 1,
|
||||||
"type": 1,
|
"type": 1,
|
||||||
"titleUri": f"{uri}/SDEZ/{self._parse_int_ver(games_ver['maimai'])}/",
|
"titleUri": f"{uri}/{self._parse_int_ver(games_ver['maimai'])}/Maimai2Servlet/",
|
||||||
},
|
},
|
||||||
# ONGEKI
|
# ONGEKI
|
||||||
{
|
{
|
||||||
@ -61,12 +63,29 @@ class CardMakerBase:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||||
reboot_start = date.strftime(
|
# if reboot start/end time is not defined use the default behavior of being a few hours ago
|
||||||
datetime.now() + timedelta(hours=3), self.date_time_format
|
if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
|
||||||
)
|
reboot_start = datetime.strftime(
|
||||||
reboot_end = date.strftime(
|
datetime.utcnow() + timedelta(hours=6), self.date_time_format
|
||||||
datetime.now() + timedelta(hours=4), self.date_time_format
|
)
|
||||||
)
|
reboot_end = datetime.strftime(
|
||||||
|
datetime.utcnow() + timedelta(hours=7), self.date_time_format
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# get current datetime in JST
|
||||||
|
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")
|
||||||
|
|
||||||
|
# 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'))
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
# grab the dict with all games version numbers from user config
|
# grab the dict with all games version numbers from user config
|
||||||
games_ver = self.game_cfg.version.version(self.version)
|
games_ver = self.game_cfg.version.version(self.version)
|
||||||
|
@ -7,21 +7,22 @@ import coloredlogs
|
|||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
from os import path
|
from os import path
|
||||||
from typing import Tuple
|
from typing import Tuple, List, Dict
|
||||||
from twisted.web.http import Request
|
from twisted.web.http import Request
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from core.utils import Utils
|
from core.utils import Utils
|
||||||
from titles.cm.config import CardMakerConfig
|
from core.title import BaseServlet
|
||||||
from titles.cm.const import CardMakerConstants
|
from .config import CardMakerConfig
|
||||||
from titles.cm.base import CardMakerBase
|
from .const import CardMakerConstants
|
||||||
from titles.cm.cm135 import CardMaker135
|
from .base import CardMakerBase
|
||||||
|
from .cm135 import CardMaker135
|
||||||
|
|
||||||
|
|
||||||
class CardMakerServlet:
|
class CardMakerServlet(BaseServlet):
|
||||||
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||||
self.core_cfg = core_cfg
|
super().__init__(core_cfg, cfg_dir)
|
||||||
self.game_cfg = CardMakerConfig()
|
self.game_cfg = CardMakerConfig()
|
||||||
if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"):
|
if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"):
|
||||||
self.game_cfg.update(
|
self.game_cfg.update(
|
||||||
@ -57,9 +58,9 @@ class CardMakerServlet:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_allnet_info(
|
def is_game_enabled(
|
||||||
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
|
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
|
||||||
) -> Tuple[bool, str, str]:
|
) -> bool:
|
||||||
game_cfg = CardMakerConfig()
|
game_cfg = CardMakerConfig()
|
||||||
if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"):
|
if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"):
|
||||||
game_cfg.update(
|
game_cfg.update(
|
||||||
@ -67,22 +68,21 @@ class CardMakerServlet:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not game_cfg.server.enable:
|
if not game_cfg.server.enable:
|
||||||
return (False, "", "")
|
return False
|
||||||
|
|
||||||
if core_cfg.server.is_develop:
|
return True
|
||||||
return (
|
|
||||||
True,
|
|
||||||
f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/",
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
|
|
||||||
return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "")
|
def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]:
|
||||||
|
return (
|
||||||
|
[],
|
||||||
|
[("render_POST", "/SDED/{version}/{endpoint}", {})]
|
||||||
|
)
|
||||||
|
|
||||||
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
|
def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes:
|
||||||
|
version = int(matchers['version'])
|
||||||
|
endpoint = matchers['endpoint']
|
||||||
req_raw = request.content.getvalue()
|
req_raw = request.content.getvalue()
|
||||||
url_split = url_path.split("/")
|
|
||||||
internal_ver = 0
|
internal_ver = 0
|
||||||
endpoint = url_split[len(url_split) - 1]
|
|
||||||
client_ip = Utils.get_ip_addr(request)
|
client_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
if version >= 130 and version < 135: # Card Maker
|
if version >= 130 and version < 135: # Card Maker
|
||||||
|
@ -205,6 +205,7 @@ class CardMakerReader(BaseReader):
|
|||||||
"1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE,
|
"1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE,
|
||||||
"1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS,
|
"1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS,
|
||||||
"1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL,
|
"1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL,
|
||||||
|
"1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS,
|
||||||
}
|
}
|
||||||
|
|
||||||
for root, dirs, files in os.walk(base_dir):
|
for root, dirs, files in os.walk(base_dir):
|
||||||
|
@ -3,13 +3,12 @@ import json
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
from hashlib import md5
|
from os import path
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from titles.cxb.config import CxbConfig
|
from .config import CxbConfig
|
||||||
from titles.cxb.const import CxbConstants
|
from .const import CxbConstants
|
||||||
from titles.cxb.database import CxbData
|
from .database import CxbData
|
||||||
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
@ -22,6 +21,13 @@ class CxbBase:
|
|||||||
self.logger = logging.getLogger("cxb")
|
self.logger = logging.getLogger("cxb")
|
||||||
self.version = CxbConstants.VER_CROSSBEATS_REV
|
self.version = CxbConstants.VER_CROSSBEATS_REV
|
||||||
|
|
||||||
|
def _get_data_contents(self, folder: str, filetype: str, encoding: str = None, subfolder: str = "") -> List[str]:
|
||||||
|
if path.exists(f"titles/cxb/data/{folder}/{subfolder}{filetype}.csv"):
|
||||||
|
with open(f"titles/cxb/data/{folder}/{subfolder}{filetype}.csv", encoding=encoding) as f:
|
||||||
|
return f.readlines()
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
def handle_action_rpreq_request(self, data: Dict) -> Dict:
|
def handle_action_rpreq_request(self, data: Dict) -> Dict:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@ -192,14 +198,6 @@ class CxbBase:
|
|||||||
).decode("utf-8")
|
).decode("utf-8")
|
||||||
)
|
)
|
||||||
|
|
||||||
def task_generateIndexData(versionindex):
|
|
||||||
try:
|
|
||||||
v_profile = self.data.profile.get_profile_index(0, uid, self.version)
|
|
||||||
v_profile_data = v_profile["data"]
|
|
||||||
versionindex.append(int(v_profile_data["appVersion"]))
|
|
||||||
except Exception:
|
|
||||||
versionindex.append("10400")
|
|
||||||
|
|
||||||
def handle_action_loadrange_request(self, data: Dict) -> Dict:
|
def handle_action_loadrange_request(self, data: Dict) -> Dict:
|
||||||
range_start = data["loadrange"]["range"][0]
|
range_start = data["loadrange"]["range"][0]
|
||||||
range_end = data["loadrange"]["range"][1]
|
range_end = data["loadrange"]["range"][1]
|
||||||
@ -273,9 +271,14 @@ class CxbBase:
|
|||||||
thread_ScoreData = Thread(target=CxbBase.task_generateScoreData(song, index, data1))
|
thread_ScoreData = Thread(target=CxbBase.task_generateScoreData(song, index, data1))
|
||||||
thread_ScoreData.start()
|
thread_ScoreData.start()
|
||||||
|
|
||||||
for v in index:
|
v_profile = self.data.profile.get_profile_index(0, uid, self.version)
|
||||||
thread_IndexData = Thread(target=CxbBase.task_generateIndexData(versionindex))
|
v_profile_data = v_profile["data"]
|
||||||
thread_IndexData.start()
|
|
||||||
|
for _, data in enumerate(profile):
|
||||||
|
if v_profile_data:
|
||||||
|
versionindex.append(int(v_profile_data["appVersion"]))
|
||||||
|
else:
|
||||||
|
versionindex.append("10400")
|
||||||
|
|
||||||
return {"index": index, "data": data1, "version": versionindex}
|
return {"index": index, "data": data1, "version": versionindex}
|
||||||
|
|
||||||
@ -530,7 +533,6 @@ class CxbBase:
|
|||||||
profile = self.data.profile.get_profile_index(0, uid, self.version)
|
profile = self.data.profile.get_profile_index(0, uid, self.version)
|
||||||
data1 = profile["data"]
|
data1 = profile["data"]
|
||||||
p = self.data.item.get_energy(uid)
|
p = self.data.item.get_energy(uid)
|
||||||
energy = p["energy"]
|
|
||||||
|
|
||||||
if not p:
|
if not p:
|
||||||
self.data.item.put_energy(uid, 5)
|
self.data.item.put_energy(uid, 5)
|
||||||
@ -543,6 +545,7 @@ class CxbBase:
|
|||||||
}
|
}
|
||||||
|
|
||||||
array = []
|
array = []
|
||||||
|
energy = p["energy"]
|
||||||
|
|
||||||
newenergy = int(energy) + 5
|
newenergy = int(energy) + 5
|
||||||
self.data.item.put_energy(uid, newenergy)
|
self.data.item.put_energy(uid, newenergy)
|
||||||
|
@ -19,42 +19,6 @@ class CxbServerConfig:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def hostname(self) -> str:
|
|
||||||
return CoreConfig.get_config_field(
|
|
||||||
self.__config, "cxb", "server", "hostname", default="localhost"
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ssl_enable(self) -> bool:
|
|
||||||
return CoreConfig.get_config_field(
|
|
||||||
self.__config, "cxb", "server", "ssl_enable", default=False
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def port(self) -> int:
|
|
||||||
return CoreConfig.get_config_field(
|
|
||||||
self.__config, "cxb", "server", "port", default=8082
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def port_secure(self) -> int:
|
|
||||||
return CoreConfig.get_config_field(
|
|
||||||
self.__config, "cxb", "server", "port_secure", default=443
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ssl_cert(self) -> str:
|
|
||||||
return CoreConfig.get_config_field(
|
|
||||||
self.__config, "cxb", "server", "ssl_cert", default="cert/title.crt"
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ssl_key(self) -> str:
|
|
||||||
return CoreConfig.get_config_field(
|
|
||||||
self.__config, "cxb", "server", "ssl_key", default="cert/title.key"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CxbConfig(dict):
|
class CxbConfig(dict):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user