Merge branch 'develop' into diva_handler_classes
This commit is contained in:
commit
572b8ddbe5
@ -1,6 +1,6 @@
|
||||
FROM python:3.9.15-slim-bullseye
|
||||
|
||||
RUN apt update && apt install default-libmysqlclient-dev build-essential libtk nodejs npm -y
|
||||
RUN apt update && apt install default-libmysqlclient-dev build-essential libtk nodejs npm pkg-config -y
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt requirements.txt
|
||||
@ -12,10 +12,10 @@ RUN chmod +x entrypoint.sh
|
||||
|
||||
COPY index.py index.py
|
||||
COPY dbutils.py dbutils.py
|
||||
COPY read.py read.py
|
||||
ADD core core
|
||||
ADD titles titles
|
||||
ADD config config
|
||||
ADD log log
|
||||
ADD logs logs
|
||||
ADD cert cert
|
||||
|
||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||
|
@ -1,6 +1,13 @@
|
||||
# Changelog
|
||||
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
|
||||
|
||||
## 20231015
|
||||
### maimai DX
|
||||
+ Added support for FESTiVAL PLUS
|
||||
|
||||
### Card Maker
|
||||
+ Added support for maimai DX FESTiVAL PLUS
|
||||
|
||||
## 20230716
|
||||
### General
|
||||
+ Docker files added (#19)
|
||||
|
@ -145,7 +145,15 @@ class AimedbProtocol(Protocol):
|
||||
def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
req = ADBLookupRequest(data)
|
||||
user_id = self.data.card.get_user_id_from_card(req.access_code)
|
||||
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)
|
||||
|
||||
self.logger.info(
|
||||
@ -157,8 +165,17 @@ class AimedbProtocol(Protocol):
|
||||
req = ADBLookupRequest(data)
|
||||
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)
|
||||
|
||||
if is_banned and is_locked:
|
||||
ret.head.status = ADBStatus.BAN_SYS_USER
|
||||
elif is_banned:
|
||||
ret.head.status = ADBStatus.BAN_SYS
|
||||
elif is_locked:
|
||||
ret.head.status = ADBStatus.LOCK_USER
|
||||
|
||||
self.logger.info(
|
||||
f"access_code {req.access_code} -> user_id {ret.user_id}"
|
||||
)
|
||||
@ -237,7 +254,7 @@ class AimedbProtocol(Protocol):
|
||||
def handle_register(self, data: bytes, resp_code: int) -> bytes:
|
||||
req = ADBLookupRequest(data)
|
||||
user_id = -1
|
||||
|
||||
|
||||
if self.config.server.allow_user_registration:
|
||||
user_id = self.data.user.create_user()
|
||||
|
||||
|
130
core/allnet.py
130
core/allnet.py
@ -16,10 +16,11 @@ from os import path
|
||||
import urllib.parse
|
||||
import math
|
||||
|
||||
from core.config import CoreConfig
|
||||
from core.utils import Utils
|
||||
from core.data import Data
|
||||
from core.const import *
|
||||
from .config import CoreConfig
|
||||
from .utils import Utils
|
||||
from .data import Data
|
||||
from .const import *
|
||||
from .title import TitleServlet
|
||||
|
||||
BILLING_DT_FORMAT: Final[str] = "%Y%m%d%H%M%S"
|
||||
|
||||
@ -33,13 +34,67 @@ class ALLNET_STAT(Enum):
|
||||
bad_machine = -2
|
||||
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:
|
||||
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
||||
super().__init__()
|
||||
self.config = core_cfg
|
||||
self.config_folder = cfg_folder
|
||||
self.data = Data(core_cfg)
|
||||
self.uri_registry: Dict[str, Tuple[str, str]] = {}
|
||||
|
||||
self.logger = logging.getLogger("allnet")
|
||||
if not hasattr(self.logger, "initialized"):
|
||||
@ -70,18 +125,8 @@ class AllnetServlet:
|
||||
if len(plugins) == 0:
|
||||
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(
|
||||
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):
|
||||
@ -147,7 +192,7 @@ class AllnetServlet:
|
||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8")
|
||||
|
||||
elif not arcade["ip"] or arcade["ip"] is None and self.config.server.strict_ip_checking:
|
||||
elif (not arcade["ip"] or arcade["ip"] is None) and self.config.server.strict_ip_checking:
|
||||
msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)."
|
||||
self.data.base.log_event(
|
||||
"allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg
|
||||
@ -190,7 +235,7 @@ class AllnetServlet:
|
||||
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:
|
||||
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
|
||||
self.data.base.log_event(
|
||||
@ -215,11 +260,9 @@ class AllnetServlet:
|
||||
self.logger.debug(f"Allnet response: {resp_str}")
|
||||
return (resp_str + "\n").encode("utf-8")
|
||||
|
||||
resp.uri, resp.host = self.uri_registry[req.game_id]
|
||||
|
||||
|
||||
int_ver = req.ver.replace(".", "")
|
||||
resp.uri = resp.uri.replace("$v", int_ver)
|
||||
resp.host = resp.host.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)
|
||||
|
||||
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)
|
||||
@ -315,6 +358,7 @@ class AllnetServlet:
|
||||
|
||||
def handle_dlorder_report(self, request: Request, match: Dict) -> bytes:
|
||||
req_raw = request.content.getvalue()
|
||||
client_ip = Utils.get_ip_addr(request)
|
||||
try:
|
||||
req_dict: Dict = json.loads(req_raw)
|
||||
except Exception as e:
|
||||
@ -332,11 +376,17 @@ class AllnetServlet:
|
||||
self.logger.warning(f"Failed to parse DL Report: Invalid format - contains neither appimage nor optimage")
|
||||
return "NG"
|
||||
|
||||
dl_report_data = DLReport(dl_data, dl_data_type)
|
||||
rep = DLReport(dl_data, dl_data_type)
|
||||
|
||||
if not dl_report_data.validate():
|
||||
self.logger.warning(f"Failed to parse DL Report: Invalid format - {dl_report_data.err}")
|
||||
if not rep.validate():
|
||||
self.logger.warning(f"Failed to parse DL Report: Invalid format - {rep.err}")
|
||||
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"
|
||||
|
||||
@ -524,7 +574,7 @@ class AllnetPowerOnResponse:
|
||||
self.stat = 1
|
||||
self.uri = ""
|
||||
self.host = ""
|
||||
self.place_id = "123"
|
||||
self.place_id = "0123"
|
||||
self.name = "ARTEMiS"
|
||||
self.nickname = "ARTEMiS"
|
||||
self.region0 = "1"
|
||||
@ -739,13 +789,13 @@ class DLReport:
|
||||
self.ot = data.get("ot")
|
||||
self.rt = data.get("rt")
|
||||
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.dav = data.get("dav")
|
||||
self.wdav = data.get("wdav") # app only
|
||||
self.dov = data.get("dov")
|
||||
self.wdov = data.get("wdov") # app only
|
||||
self.__type = report_type
|
||||
self.rep_type = report_type
|
||||
self.err = ""
|
||||
|
||||
def validate(self) -> bool:
|
||||
@ -753,14 +803,6 @@ class DLReport:
|
||||
self.err = "serial not provided"
|
||||
return False
|
||||
|
||||
if self.dfl is None:
|
||||
self.err = "dfl not provided"
|
||||
return False
|
||||
|
||||
if self.wfl is None:
|
||||
self.err = "wfl not provided"
|
||||
return False
|
||||
|
||||
if self.tsc is None:
|
||||
self.err = "tsc not provided"
|
||||
return False
|
||||
@ -769,18 +811,6 @@ class DLReport:
|
||||
self.err = "tdsc not provided"
|
||||
return False
|
||||
|
||||
if self.at is None:
|
||||
self.err = "at not provided"
|
||||
return False
|
||||
|
||||
if self.ot is None:
|
||||
self.err = "ot not provided"
|
||||
return False
|
||||
|
||||
if self.rt is None:
|
||||
self.err = "rt not provided"
|
||||
return False
|
||||
|
||||
if self.as_ is None:
|
||||
self.err = "as not provided"
|
||||
return False
|
||||
@ -801,11 +831,11 @@ class DLReport:
|
||||
self.err = "dov not provided"
|
||||
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"
|
||||
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"
|
||||
return False
|
||||
|
||||
|
@ -36,6 +36,12 @@ class ServerConfig:
|
||||
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
|
||||
def threading(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
@ -84,6 +90,36 @@ class TitleConfig:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "title", "port", default=8080
|
||||
)
|
||||
|
||||
@property
|
||||
def port_ssl(self) -> int:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "title", "port_ssl", default=0
|
||||
)
|
||||
|
||||
@property
|
||||
def ssl_key(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "title", "ssl_key", default="cert/title.key"
|
||||
)
|
||||
|
||||
@property
|
||||
def ssl_cert(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "title", "ssl_cert", default="cert/title.pem"
|
||||
)
|
||||
|
||||
@property
|
||||
def reboot_start_time(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
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:
|
||||
@ -150,6 +186,12 @@ class DatabaseConfig:
|
||||
default=10000,
|
||||
)
|
||||
|
||||
@property
|
||||
def enable_memcached(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "database", "enable_memcached", default=True
|
||||
)
|
||||
|
||||
@property
|
||||
def memcached_host(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
|
@ -17,7 +17,7 @@ except ModuleNotFoundError:
|
||||
|
||||
def cached(lifetime: int = 10, extra_key: Any = None) -> Callable:
|
||||
def _cached(func: Callable) -> Callable:
|
||||
if has_mc:
|
||||
if has_mc and (cfg and cfg.database.enable_memcached):
|
||||
hostname = "127.0.0.1"
|
||||
if cfg:
|
||||
hostname = cfg.database.memcached_host
|
||||
|
@ -41,7 +41,6 @@ machine = Table(
|
||||
Column("country", String(3)), # overwrites if not null
|
||||
Column("timezone", String(255)),
|
||||
Column("ota_enable", Boolean),
|
||||
Column("is_cab", Boolean),
|
||||
Column("memo", String(255)),
|
||||
Column("is_cab", Boolean),
|
||||
Column("data", JSON),
|
||||
|
@ -64,6 +64,27 @@ class CardData(BaseData):
|
||||
|
||||
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:
|
||||
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;
|
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
|
||||
import pytz
|
||||
|
||||
from core import CoreConfig
|
||||
from core.utils import Utils
|
||||
|
||||
from .config import CoreConfig
|
||||
from .utils import Utils
|
||||
from .title import TitleServlet
|
||||
|
||||
class MuchaServlet:
|
||||
mucha_registry: List[str] = []
|
||||
def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None:
|
||||
self.config = cfg
|
||||
self.config_dir = cfg_dir
|
||||
self.mucha_registry: List[str] = []
|
||||
|
||||
self.logger = logging.getLogger("mucha")
|
||||
log_fmt_str = "[%(asctime)s] Mucha | %(levelname)s | %(message)s"
|
||||
@ -37,11 +37,9 @@ class MuchaServlet:
|
||||
self.logger.setLevel(cfg.mucha.loglevel)
|
||||
coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str)
|
||||
|
||||
all_titles = Utils.get_all_titles()
|
||||
|
||||
for _, mod in all_titles.items():
|
||||
if hasattr(mod, "index") and hasattr(mod.index, "get_mucha_info"):
|
||||
enabled, game_cd = mod.index.get_mucha_info(
|
||||
for _, mod in TitleServlet.title_registry.items():
|
||||
if hasattr(mod, "get_mucha_info"):
|
||||
enabled, game_cd = mod.get_mucha_info(
|
||||
self.config, self.config_dir
|
||||
)
|
||||
if enabled:
|
||||
|
138
core/title.py
138
core/title.py
@ -1,4 +1,4 @@
|
||||
from typing import Dict, Any
|
||||
from typing import Dict, List, Tuple
|
||||
import logging, coloredlogs
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from twisted.web.http import Request
|
||||
@ -7,14 +7,88 @@ from core.config import CoreConfig
|
||||
from core.data import Data
|
||||
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:
|
||||
title_registry: Dict[str, BaseServlet] = {}
|
||||
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
|
||||
super().__init__()
|
||||
self.config = core_cfg
|
||||
self.config_folder = cfg_folder
|
||||
self.data = Data(core_cfg)
|
||||
self.title_registry: Dict[str, Any] = {}
|
||||
|
||||
self.logger = logging.getLogger("title")
|
||||
if not hasattr(self.logger, "initialized"):
|
||||
@ -43,62 +117,62 @@ class TitleServlet:
|
||||
plugins = Utils.get_all_titles()
|
||||
|
||||
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
|
||||
game_servlet: BaseServlet = mod.index
|
||||
game_codes: List[str] = mod.game_codes
|
||||
|
||||
for code in game_codes:
|
||||
if game_servlet.is_game_enabled(code, self.config, self.config_folder):
|
||||
handler_cls = game_servlet(self.config, self.config_folder)
|
||||
|
||||
if hasattr(mod.index, "get_allnet_info"):
|
||||
for code in mod.game_codes:
|
||||
enabled, _, _ = mod.index.get_allnet_info(
|
||||
code, self.config, self.config_folder
|
||||
)
|
||||
if hasattr(handler_cls, "setup") and should_call_setup:
|
||||
handler_cls.setup()
|
||||
should_call_setup = False
|
||||
|
||||
if enabled:
|
||||
handler_cls = mod.index(self.config, self.config_folder)
|
||||
|
||||
if hasattr(handler_cls, "setup") and should_call_setup:
|
||||
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")
|
||||
self.title_registry[code] = handler_cls
|
||||
|
||||
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(
|
||||
f"Serving {len(self.title_registry)} game codes {'on port ' + str(core_cfg.title.port) if core_cfg.title.port > 0 else ''}"
|
||||
)
|
||||
|
||||
def render_GET(self, request: Request, endpoints: dict) -> bytes:
|
||||
code = endpoints["game"]
|
||||
code = endpoints["title"]
|
||||
subaction = endpoints['subaction']
|
||||
|
||||
if code not in self.title_registry:
|
||||
self.logger.warning(f"Unknown game code {code}")
|
||||
request.setResponseCode(404)
|
||||
return b""
|
||||
|
||||
index = self.title_registry[code]
|
||||
if not hasattr(index, "render_GET"):
|
||||
self.logger.warning(f"{code} does not dispatch GET")
|
||||
request.setResponseCode(405)
|
||||
handler = getattr(index, f"{subaction}", None)
|
||||
if handler is None:
|
||||
self.logger.error(f"{code} does not have handler for GET subaction {subaction}")
|
||||
request.setResponseCode(500)
|
||||
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:
|
||||
code = endpoints["game"]
|
||||
code = endpoints["title"]
|
||||
subaction = endpoints['subaction']
|
||||
|
||||
if code not in self.title_registry:
|
||||
self.logger.warning(f"Unknown game code {code}")
|
||||
request.setResponseCode(404)
|
||||
return b""
|
||||
|
||||
index = self.title_registry[code]
|
||||
if not hasattr(index, "render_POST"):
|
||||
self.logger.warning(f"{code} does not dispatch POST")
|
||||
request.setResponseCode(405)
|
||||
handler = getattr(index, f"{subaction}", None)
|
||||
if handler is None:
|
||||
self.logger.error(f"{code} does not have handler for POST subaction {subaction}")
|
||||
request.setResponseCode(500)
|
||||
return b""
|
||||
|
||||
return index.render_POST(
|
||||
request, int(endpoints["version"]), endpoints["endpoint"]
|
||||
)
|
||||
endpoints.pop("title")
|
||||
endpoints.pop("subaction")
|
||||
return handler(request, code, endpoints)
|
||||
|
@ -5,8 +5,11 @@ import logging
|
||||
import importlib
|
||||
from os import walk
|
||||
|
||||
from .config import CoreConfig
|
||||
|
||||
class Utils:
|
||||
real_title_port = None
|
||||
real_title_port_ssl = None
|
||||
@classmethod
|
||||
def get_all_titles(cls) -> Dict[str, ModuleType]:
|
||||
ret: Dict[str, Any] = {}
|
||||
@ -33,3 +36,27 @@ class Utils:
|
||||
if b"x-forwarded-for" in req.getAllHeaders()
|
||||
else req.getClientAddress().host
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_title_port(cls, cfg: CoreConfig):
|
||||
if cls.real_title_port is not None: return cls.real_title_port
|
||||
|
||||
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: .
|
||||
volumes:
|
||||
- ./aime:/app/aime
|
||||
- ./configs/config:/app/config
|
||||
|
||||
environment:
|
||||
CFG_DEV: 1
|
||||
CFG_CORE_SERVER_HOSTNAME: 0.0.0.0
|
||||
CFG_CORE_DATABASE_HOST: ma.db
|
||||
CFG_CORE_MEMCACHED_HOSTNAME: ma.memcached
|
||||
CFG_CORE_AIMEDB_KEY: keyhere
|
||||
CFG_CORE_AIMEDB_KEY: <INSERT AIMEDB KEY HERE>
|
||||
CFG_CHUNI_SERVER_LOGLEVEL: debug
|
||||
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8443:8443"
|
||||
- "22345:22345"
|
||||
- "80:80"
|
||||
- "8443:8443"
|
||||
- "22345:22345"
|
||||
|
||||
- "8080:8080"
|
||||
- "8090:8090"
|
||||
- "8080:8080"
|
||||
- "8090:8090"
|
||||
|
||||
depends_on:
|
||||
db:
|
||||
@ -28,21 +29,29 @@ services:
|
||||
|
||||
db:
|
||||
hostname: ma.db
|
||||
image: mysql:8.0.31-debian
|
||||
image: yobasystems/alpine-mariadb:10.11.5
|
||||
environment:
|
||||
MYSQL_DATABASE: aime
|
||||
MYSQL_USER: aime
|
||||
MYSQL_PASSWORD: aime
|
||||
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:
|
||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
|
||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "-pAimeRootPassword"]
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
|
||||
memcached:
|
||||
hostname: ma.memcached
|
||||
image: memcached:1.6.17-bullseye
|
||||
image: memcached:1.6.22-alpine3.18
|
||||
command: [ "memcached", "-m", "1024", "-I", "128m" ]
|
||||
|
||||
phpmyadmin:
|
||||
hostname: ma.phpmyadmin
|
||||
@ -53,5 +62,5 @@ services:
|
||||
PMA_PASSWORD: AimeRootPassword
|
||||
APACHE_PORT: 8080
|
||||
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`
|
||||
- `is_develop`: Flags that the server is a development instance without a proxy standing in front of it. Setting to `False` tells the server not to listen for SSL, because the proxy should be handling all SSL-related things, among other things. Default `True`
|
||||
- `threading`: Flags that `reactor.run` should be called via the `Thread` standard library. May provide a speed boost, but removes the ability to kill the server via `Ctrl + C`. Default: `False`
|
||||
- `check_arcade_ip`: Checks IPs against the `arcade` table in the database, if one is defined. Default `False`
|
||||
- `strict_ip_checking`: Rejects clients if there is no IP in the `arcade` table for the respective arcade
|
||||
- `log_dir`: Directory to store logs. Server MUST have read and write permissions to this directory or you will have issues. Default `logs`
|
||||
## Title
|
||||
- `loglevel`: Logging level for the title server. Default `info`
|
||||
- `hostname`: Hostname that gets sent to clients to tell them where to connect. Games must be able to connect to your server via the hostname or IP you spcify here. Note that most games will reject `localhost` or `127.0.0.1`. Default `localhost`
|
||||
- `port`: Port that the title server will listen for connections on. Set to 0 to use the Allnet handler to reduce the port footprint. Default `8080`
|
||||
- `port_ssl`: Port that the secure title server will listen for connections on. Set to 0 to use the Allnet handler to reduce the port footprint. Default `0`
|
||||
- `ssl_key`: Location of the ssl server key for the secure title server. Ignored if `port_ssl` is set to `0` or `is_develop` set to `False`. Default `cert/title.key`
|
||||
- `ssl_cert`: Location of the ssl server certificate for the secure title server. Must not be a self-signed SSL. Ignored if `port_ssl` is set to `0` or `is_develop` is set to `False`. Default `cert/title.pem`
|
||||
- `reboot_start_time`: 24 hour JST time that clients will see as the start of maintenance period. Leave blank for no maintenance time. Default: ""
|
||||
- `reboot_end_time`: 24 hour JST time that clients will see as the end of maintenance period. Leave blank for no maintenance time. Default: ""
|
||||
## Database
|
||||
- `host`: Host of the database. Default `localhost`
|
||||
- `username`: Username of the account the server should connect to the database with. Default `aime`
|
||||
|
@ -59,6 +59,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.
|
||||
|
||||
### 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
|
||||
|
||||
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
|
||||
@ -88,6 +110,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
|
||||
- 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.
|
||||
|
||||
@ -142,18 +194,19 @@ For versions pre-dx
|
||||
| SDBM | 5 | maimai ORANGE PLUS |
|
||||
| SDCQ | 6 | maimai PiNK |
|
||||
| SDCQ | 7 | maimai PiNK PLUS |
|
||||
| SDDK | 8 | maimai MURASAKI |
|
||||
| SDDK | 9 | maimai MURASAKI PLUS |
|
||||
| SDDZ | 10 | maimai MILK |
|
||||
| SDDZ | 11 | maimai MILK PLUS |
|
||||
| SDDK | 8 | maimai MURASAKi |
|
||||
| SDDK | 9 | maimai MURASAKi PLUS |
|
||||
| SDDZ | 10 | maimai MiLK |
|
||||
| SDDZ | 11 | maimai MiLK PLUS |
|
||||
| SDEY | 12 | maimai FiNALE |
|
||||
| SDEZ | 13 | maimai DX |
|
||||
| SDEZ | 14 | maimai DX PLUS |
|
||||
| SDEZ | 15 | maimai DX Splash |
|
||||
| SDEZ | 16 | maimai DX Splash PLUS |
|
||||
| SDEZ | 17 | maimai DX Universe |
|
||||
| SDEZ | 18 | maimai DX Universe PLUS |
|
||||
| SDEZ | 19 | maimai DX Festival |
|
||||
| SDEZ | 17 | maimai DX UNiVERSE |
|
||||
| SDEZ | 18 | maimai DX UNiVERSE PLUS |
|
||||
| SDEZ | 19 | maimai DX FESTiVAL |
|
||||
| SDEZ | 20 | maimai DX FESTiVAL PLUS |
|
||||
|
||||
### Importer
|
||||
|
||||
@ -211,6 +264,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_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
|
||||
|
||||
@ -257,9 +313,19 @@ Config file is located in `config/ongeki.yaml`.
|
||||
| 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 |
|
||||
| `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.
|
||||
|
||||
**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
|
||||
|
||||
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
|
||||
@ -282,15 +348,21 @@ python dbutils.py --game SDDT upgrade
|
||||
|
||||
### Support status
|
||||
|
||||
* Card Maker 1.30:
|
||||
* CHUNITHM NEW!!: Yes
|
||||
* maimai DX UNiVERSE: Yes
|
||||
* O.N.G.E.K.I. bright: Yes
|
||||
#### Card Maker 1.30:
|
||||
* CHUNITHM NEW!!: Yes
|
||||
* maimai DX UNiVERSE: Yes
|
||||
* O.N.G.E.K.I. bright: Yes
|
||||
|
||||
* Card Maker 1.35:
|
||||
* CHUNITHM SUN: Yes (NEW PLUS!! up to A032)
|
||||
* maimai DX FESTiVAL: Yes (up to A035) (UNiVERSE PLUS up to A031)
|
||||
* O.N.G.E.K.I. bright MEMORY: Yes
|
||||
#### Card Maker 1.35:
|
||||
* CHUNITHM:
|
||||
* NEW!!: 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
|
||||
@ -509,6 +581,8 @@ python dbutils.py --game SDEW upgrade
|
||||
- Player title is currently static and cannot be changed in-game
|
||||
- QR Card Scanning currently only load a static hero
|
||||
|
||||
**Network hashing in GssSite.dll must be disabled**
|
||||
|
||||
### Credits for SAO support:
|
||||
|
||||
- Midorica - Limited Network Support
|
||||
|
@ -1,9 +1,10 @@
|
||||
server:
|
||||
enable: True
|
||||
loglevel: "info"
|
||||
news_msg: ""
|
||||
|
||||
team:
|
||||
name: ARTEMiS
|
||||
name: ARTEMiS # If this is set, all players that are not on a team will use this one by default.
|
||||
|
||||
mods:
|
||||
use_login_bonus: True
|
||||
|
@ -4,6 +4,7 @@ server:
|
||||
allow_unregistered_serials: True
|
||||
name: "ARTEMiS"
|
||||
is_develop: True
|
||||
is_using_proxy: False
|
||||
threading: False
|
||||
log_dir: "logs"
|
||||
check_arcade_ip: False
|
||||
@ -13,6 +14,12 @@ title:
|
||||
loglevel: "info"
|
||||
hostname: "localhost"
|
||||
port: 8080
|
||||
port_ssl: 0
|
||||
ssl_cert: "cert/title.crt"
|
||||
ssl_key: "cert/title.key"
|
||||
reboot_start_time: "04:00"
|
||||
reboot_end_time: "05:00"
|
||||
|
||||
|
||||
database:
|
||||
host: "localhost"
|
||||
@ -24,6 +31,7 @@ database:
|
||||
sha2_password: False
|
||||
loglevel: "warn"
|
||||
user_table_autoincrement_start: 10000
|
||||
enable_memcached: True
|
||||
memcached_host: "localhost"
|
||||
|
||||
frontend:
|
||||
|
@ -24,8 +24,7 @@ server {
|
||||
|
||||
# SSL titles, comment out if you don't plan on accepting SSL titles
|
||||
server {
|
||||
listen 443 ssl default_server;
|
||||
listen [::]:443 ssl default_server;
|
||||
listen 443 ssl;
|
||||
server_name your.hostname.here;
|
||||
|
||||
ssl_certificate /path/to/cert/title.crt;
|
||||
@ -55,7 +54,7 @@ server {
|
||||
ssl_session_tickets off;
|
||||
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers "ALL:@SECLEVEL=1";
|
||||
ssl_ciphers "ALL:@SECLEVEL=0";
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
location / {
|
||||
@ -75,7 +74,7 @@ server {
|
||||
ssl_session_tickets off;
|
||||
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers "ALL:@SECLEVEL=1";
|
||||
ssl_ciphers "ALL:@SECLEVEL=0";
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
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
|
||||
server {
|
||||
listen 80;
|
||||
@ -143,4 +120,4 @@ server {
|
||||
proxy_pass_request_headers on;
|
||||
proxy_pass http://localhost:8090/;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
50
index.py
50
index.py
@ -22,8 +22,8 @@ class HttpDispatcher(resource.Resource):
|
||||
self.map_post = Mapper()
|
||||
self.logger = logging.getLogger("core")
|
||||
|
||||
self.allnet = AllnetServlet(cfg, config_dir)
|
||||
self.title = TitleServlet(cfg, config_dir)
|
||||
self.allnet = AllnetServlet(cfg, config_dir)
|
||||
self.mucha = MuchaServlet(cfg, config_dir)
|
||||
|
||||
self.map_get.connect(
|
||||
@ -144,22 +144,32 @@ class HttpDispatcher(resource.Resource):
|
||||
conditions=dict(method=["POST"]),
|
||||
)
|
||||
|
||||
self.map_get.connect(
|
||||
"title_get",
|
||||
"/{game}/{version}/{endpoint:.*?}",
|
||||
controller="title",
|
||||
action="render_GET",
|
||||
conditions=dict(method=["GET"]),
|
||||
requirements=dict(game=R"S..."),
|
||||
)
|
||||
self.map_post.connect(
|
||||
"title_post",
|
||||
"/{game}/{version}/{endpoint:.*?}",
|
||||
controller="title",
|
||||
action="render_POST",
|
||||
conditions=dict(method=["POST"]),
|
||||
requirements=dict(game=R"S..."),
|
||||
)
|
||||
for code, game in self.title.title_registry.items():
|
||||
get_matchers, post_matchers = game.get_endpoint_matchers()
|
||||
|
||||
for m in get_matchers:
|
||||
self.map_get.connect(
|
||||
"title_get",
|
||||
m[1],
|
||||
controller="title",
|
||||
action="render_GET",
|
||||
title=code,
|
||||
subaction=m[0],
|
||||
conditions=dict(method=["GET"]),
|
||||
requirements=m[2],
|
||||
)
|
||||
|
||||
for m in post_matchers:
|
||||
self.map_post.connect(
|
||||
"title_post",
|
||||
m[1],
|
||||
controller="title",
|
||||
action="render_POST",
|
||||
title=code,
|
||||
subaction=m[0],
|
||||
conditions=dict(method=["POST"]),
|
||||
requirements=m[2],
|
||||
)
|
||||
|
||||
def render_GET(self, request: Request) -> bytes:
|
||||
test = self.map_get.match(request.uri.decode())
|
||||
@ -279,6 +289,7 @@ if __name__ == "__main__":
|
||||
|
||||
allnet_server_str = f"tcp:{cfg.allnet.port}:interface={cfg.server.listen_address}"
|
||||
title_server_str = f"tcp:{cfg.title.port}:interface={cfg.server.listen_address}"
|
||||
title_https_server_str = f"ssl:{cfg.title.port_ssl}:interface={cfg.server.listen_address}:privateKey={cfg.title.ssl_key}:certKey={cfg.title.ssl_cert}"
|
||||
adb_server_str = f"tcp:{cfg.aimedb.port}:interface={cfg.server.listen_address}"
|
||||
frontend_server_str = (
|
||||
f"tcp:{cfg.frontend.port}:interface={cfg.server.listen_address}"
|
||||
@ -312,6 +323,11 @@ if __name__ == "__main__":
|
||||
endpoints.serverFromString(reactor, title_server_str).listen(
|
||||
server.Site(dispatcher)
|
||||
)
|
||||
|
||||
if cfg.title.port_ssl > 0:
|
||||
endpoints.serverFromString(reactor, title_https_server_str).listen(
|
||||
server.Site(dispatcher)
|
||||
)
|
||||
|
||||
if cfg.server.threading:
|
||||
Thread(target=reactor.run, args=(False,)).start()
|
||||
|
@ -11,7 +11,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
||||
+ All versions + omnimix
|
||||
|
||||
+ maimai DX
|
||||
+ All versions up to FESTiVAL
|
||||
+ All versions up to FESTiVAL PLUS
|
||||
|
||||
+ Hatsune Miku: Project DIVA Arcade
|
||||
+ All versions
|
||||
@ -40,7 +40,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
||||
- mysql/mariadb server
|
||||
|
||||
## Setup guides
|
||||
Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md) 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
|
||||
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.database import ChuniData
|
||||
from titles.chuni.config import ChuniConfig
|
||||
|
||||
SCORE_BUFFER = {}
|
||||
|
||||
class ChuniBase:
|
||||
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
|
||||
@ -181,21 +181,50 @@ class ChuniBase:
|
||||
return {"type": data["type"], "length": 0, "gameIdlistList": []}
|
||||
|
||||
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:
|
||||
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:
|
||||
return {"type": data["type"], "length": 0, "gameSaleList": []}
|
||||
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
reboot_start = datetime.strftime(
|
||||
datetime.now() - timedelta(hours=4), self.date_time_format
|
||||
)
|
||||
reboot_end = datetime.strftime(
|
||||
datetime.now() - timedelta(hours=3), self.date_time_format
|
||||
)
|
||||
# if reboot start/end time is not defined use the default behavior of being a few hours ago
|
||||
if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
|
||||
reboot_start = datetime.strftime(
|
||||
datetime.utcnow() + timedelta(hours=6), 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 {
|
||||
"gameSetting": {
|
||||
"dataVersion": "1.00.00",
|
||||
@ -361,11 +390,70 @@ class ChuniBase:
|
||||
"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:
|
||||
user_fav_item_list = []
|
||||
|
||||
# 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(
|
||||
data["userId"], self.version, fav_kind=int(data["kind"])
|
||||
)
|
||||
@ -490,22 +578,39 @@ class ChuniBase:
|
||||
tmp.pop("id")
|
||||
|
||||
for song in song_list:
|
||||
score_buf = SCORE_BUFFER.get(str(data["userId"])) or []
|
||||
if song["userMusicDetailList"][0]["musicId"] == tmp["musicId"]:
|
||||
found = True
|
||||
song["userMusicDetailList"].append(tmp)
|
||||
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]})
|
||||
score_buf.append(tmp["musicId"])
|
||||
SCORE_BUFFER[str(data["userId"])] = score_buf
|
||||
|
||||
if len(song_list) >= max_ct:
|
||||
break
|
||||
|
||||
if len(song_list) >= next_idx + max_ct:
|
||||
next_idx += max_ct
|
||||
|
||||
try:
|
||||
while song_list[-1]["userMusicDetailList"][0]["musicId"] == music_detail[x + 1]["musicId"]:
|
||||
music = music_detail[x + 1]._asdict()
|
||||
music.pop("user")
|
||||
music.pop("id")
|
||||
song_list[-1]["userMusicDetailList"].append(music)
|
||||
song_list[-1]["length"] += 1
|
||||
x += 1
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
if len(song_list) >= max_ct:
|
||||
next_idx += len(song_list)
|
||||
else:
|
||||
next_idx = -1
|
||||
|
||||
SCORE_BUFFER[str(data["userId"])] = []
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": len(song_list),
|
||||
@ -600,39 +705,94 @@ class ChuniBase:
|
||||
}
|
||||
|
||||
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
|
||||
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": 1,
|
||||
"teamRank": 1,
|
||||
"teamId": team_id,
|
||||
"teamRank": team_rank,
|
||||
"teamName": team_name,
|
||||
"userTeamPoint": {
|
||||
"userId": data["userId"],
|
||||
"teamId": 1,
|
||||
"teamId": team_id,
|
||||
"orderId": 1,
|
||||
"teamPoint": 1,
|
||||
"aggrDate": data["playDate"],
|
||||
},
|
||||
}
|
||||
|
||||
def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": 0,
|
||||
"nextIndex": 0,
|
||||
"nextIndex": -1,
|
||||
"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:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": 0,
|
||||
"nextIndex": 0,
|
||||
"teamCourseRuleList": [],
|
||||
"nextIndex": -1,
|
||||
"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:
|
||||
@ -706,12 +866,28 @@ class ChuniBase:
|
||||
playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"])
|
||||
playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"])
|
||||
playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"])
|
||||
self.data.score.put_playlog(user_id, playlog)
|
||||
self.data.score.put_playlog(user_id, playlog, self.version)
|
||||
|
||||
if "userTeamPoint" in upsert:
|
||||
# TODO: team stuff
|
||||
pass
|
||||
team_points = upsert["userTeamPoint"]
|
||||
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:
|
||||
for map_area in upsert["userMapAreaList"]:
|
||||
self.data.item.put_map_area(user_id, map_area)
|
||||
|
@ -19,6 +19,12 @@ class ChuniServerConfig:
|
||||
self.__config, "chuni", "server", "loglevel", default="info"
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def news_msg(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "chuni", "server", "news_msg", default=""
|
||||
)
|
||||
|
||||
|
||||
class ChuniTeamConfig:
|
||||
@ -30,6 +36,11 @@ class ChuniTeamConfig:
|
||||
return CoreConfig.get_config_field(
|
||||
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:
|
||||
|
@ -11,30 +11,31 @@ from Crypto.Util.Padding import pad
|
||||
from Crypto.Protocol.KDF import PBKDF2
|
||||
from Crypto.Hash import SHA1
|
||||
from os import path
|
||||
from typing import Tuple, Dict
|
||||
from typing import Tuple, Dict, List
|
||||
|
||||
from core import CoreConfig, Utils
|
||||
from titles.chuni.config import ChuniConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.base import ChuniBase
|
||||
from titles.chuni.plus import ChuniPlus
|
||||
from titles.chuni.air import ChuniAir
|
||||
from titles.chuni.airplus import ChuniAirPlus
|
||||
from titles.chuni.star import ChuniStar
|
||||
from titles.chuni.starplus import ChuniStarPlus
|
||||
from titles.chuni.amazon import ChuniAmazon
|
||||
from titles.chuni.amazonplus import ChuniAmazonPlus
|
||||
from titles.chuni.crystal import ChuniCrystal
|
||||
from titles.chuni.crystalplus import ChuniCrystalPlus
|
||||
from titles.chuni.paradise import ChuniParadise
|
||||
from titles.chuni.new import ChuniNew
|
||||
from titles.chuni.newplus import ChuniNewPlus
|
||||
from titles.chuni.sun import ChuniSun
|
||||
from core.title import BaseServlet
|
||||
from .config import ChuniConfig
|
||||
from .const import ChuniConstants
|
||||
from .base import ChuniBase
|
||||
from .plus import ChuniPlus
|
||||
from .air import ChuniAir
|
||||
from .airplus import ChuniAirPlus
|
||||
from .star import ChuniStar
|
||||
from .starplus import ChuniStarPlus
|
||||
from .amazon import ChuniAmazon
|
||||
from .amazonplus import ChuniAmazonPlus
|
||||
from .crystal import ChuniCrystal
|
||||
from .crystalplus import ChuniCrystalPlus
|
||||
from .paradise import ChuniParadise
|
||||
from .new import ChuniNew
|
||||
from .newplus import ChuniNewPlus
|
||||
from .sun import ChuniSun
|
||||
|
||||
|
||||
class ChuniServlet:
|
||||
class ChuniServlet(BaseServlet):
|
||||
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.hash_table: Dict[Dict[str, str]] = {}
|
||||
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()}"
|
||||
)
|
||||
|
||||
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
|
||||
def get_allnet_info(
|
||||
def is_game_enabled(
|
||||
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
|
||||
) -> Tuple[bool, str, str]:
|
||||
) -> bool:
|
||||
game_cfg = ChuniConfig()
|
||||
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
|
||||
game_cfg.update(
|
||||
@ -126,26 +136,26 @@ class ChuniServlet:
|
||||
)
|
||||
|
||||
if not game_cfg.server.enable:
|
||||
return (False, "", "")
|
||||
return False
|
||||
|
||||
if core_cfg.server.is_develop:
|
||||
return (
|
||||
True,
|
||||
f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/",
|
||||
"",
|
||||
)
|
||||
return True
|
||||
|
||||
def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]:
|
||||
if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80:
|
||||
return (f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_ver}/", "")
|
||||
|
||||
return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "")
|
||||
return (f"http://{self.core_cfg.title.hostname}/{game_ver}/", "")
|
||||
|
||||
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
|
||||
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"}')
|
||||
|
||||
req_raw = request.content.getvalue()
|
||||
url_split = url_path.split("/")
|
||||
encrtped = False
|
||||
internal_ver = 0
|
||||
endpoint = url_split[len(url_split) - 1]
|
||||
client_ip = Utils.get_ip_addr(request)
|
||||
|
||||
if version < 105: # 1.0
|
||||
|
@ -3,6 +3,7 @@ from datetime import datetime, timedelta
|
||||
from random import randint
|
||||
from typing import Dict
|
||||
|
||||
import pytz
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.const import ChuniConstants
|
||||
from titles.chuni.database import ChuniData
|
||||
@ -31,12 +32,29 @@ class ChuniNew(ChuniBase):
|
||||
match_end = datetime.strftime(
|
||||
datetime.utcnow() + timedelta(hours=16), self.date_time_format
|
||||
)
|
||||
reboot_start = datetime.strftime(
|
||||
datetime.utcnow() + timedelta(hours=6), self.date_time_format
|
||||
)
|
||||
reboot_end = datetime.strftime(
|
||||
datetime.utcnow() + timedelta(hours=7), self.date_time_format
|
||||
)
|
||||
# if reboot start/end time is not defined use the default behavior of being a few hours ago
|
||||
if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
|
||||
reboot_start = datetime.strftime(
|
||||
datetime.utcnow() + timedelta(hours=6), 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 {
|
||||
"gameSetting": {
|
||||
"isMaintenance": False,
|
||||
@ -53,11 +71,11 @@ class ChuniNew(ChuniBase):
|
||||
"matchErrorLimit": 9999,
|
||||
"romVersion": self.game_cfg.version.version(self.version)["rom"],
|
||||
"dataVersion": self.game_cfg.version.version(self.version)["data"],
|
||||
"matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
||||
"matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
||||
"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}/200/ChuniServlet/",
|
||||
# might be really important for online battle to connect the cabs via UDP port 50201
|
||||
"udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
||||
"reflectorUri": 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}/200/ChuniServlet/",
|
||||
},
|
||||
"isDumpUpload": False,
|
||||
"isAou": False,
|
||||
|
@ -21,16 +21,16 @@ class ChuniNewPlus(ChuniNew):
|
||||
]
|
||||
ret["gameSetting"][
|
||||
"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"][
|
||||
"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"][
|
||||
"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"][
|
||||
"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
|
||||
|
||||
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
|
@ -637,3 +637,69 @@ class ChuniProfileData(BaseData):
|
||||
if result is None:
|
||||
return None
|
||||
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.sql import func, select
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
|
||||
from sqlalchemy.sql.expression import exists
|
||||
from core.data.schema import BaseData, metadata
|
||||
|
||||
course = Table(
|
||||
@ -189,9 +189,28 @@ class ChuniScoreData(BaseData):
|
||||
return None
|
||||
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 = 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)
|
||||
conflict = sql.on_duplicate_key_update(**playlog_data)
|
||||
@ -200,3 +219,40 @@ class ChuniScoreData(BaseData):
|
||||
if result is None:
|
||||
return None
|
||||
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 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(
|
||||
self,
|
||||
version: int,
|
||||
@ -587,4 +596,4 @@ class ChuniStaticData(BaseData):
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
return result.fetchone()
|
@ -17,16 +17,16 @@ class ChuniSun(ChuniNewPlus):
|
||||
ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"]
|
||||
ret["gameSetting"][
|
||||
"matchingUri"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/210/ChuniServlet/"
|
||||
ret["gameSetting"][
|
||||
"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"][
|
||||
"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"][
|
||||
"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
|
||||
|
||||
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
|
@ -4,7 +4,9 @@ import json
|
||||
import logging
|
||||
from enum import Enum
|
||||
|
||||
import pytz
|
||||
from core.config import CoreConfig
|
||||
from core.utils import Utils
|
||||
from core.data.cache import cached
|
||||
from titles.cm.const import CardMakerConstants
|
||||
from titles.cm.config import CardMakerConfig
|
||||
@ -28,8 +30,8 @@ class CardMakerBase:
|
||||
return version.replace(".", "")[:3]
|
||||
|
||||
def handle_get_game_connect_api_request(self, data: Dict) -> Dict:
|
||||
if self.core_cfg.server.is_develop:
|
||||
uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}"
|
||||
if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80:
|
||||
uri = f"http://{self.core_cfg.title.hostname}:{Utils.get_title_port(self.core_cfg)}"
|
||||
else:
|
||||
uri = f"http://{self.core_cfg.title.hostname}"
|
||||
|
||||
@ -43,13 +45,13 @@ class CardMakerBase:
|
||||
{
|
||||
"modelKind": 0,
|
||||
"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
|
||||
{
|
||||
"modelKind": 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
|
||||
{
|
||||
@ -61,12 +63,29 @@ class CardMakerBase:
|
||||
}
|
||||
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
reboot_start = date.strftime(
|
||||
datetime.now() + timedelta(hours=3), self.date_time_format
|
||||
)
|
||||
reboot_end = date.strftime(
|
||||
datetime.now() + timedelta(hours=4), self.date_time_format
|
||||
)
|
||||
# if reboot start/end time is not defined use the default behavior of being a few hours ago
|
||||
if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
|
||||
reboot_start = datetime.strftime(
|
||||
datetime.utcnow() + timedelta(hours=6), 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
|
||||
games_ver = self.game_cfg.version.version(self.version)
|
||||
|
@ -7,21 +7,22 @@ import coloredlogs
|
||||
import zlib
|
||||
|
||||
from os import path
|
||||
from typing import Tuple
|
||||
from typing import Tuple, List, Dict
|
||||
from twisted.web.http import Request
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
from core.config import CoreConfig
|
||||
from core.utils import Utils
|
||||
from titles.cm.config import CardMakerConfig
|
||||
from titles.cm.const import CardMakerConstants
|
||||
from titles.cm.base import CardMakerBase
|
||||
from titles.cm.cm135 import CardMaker135
|
||||
from core.title import BaseServlet
|
||||
from .config import CardMakerConfig
|
||||
from .const import CardMakerConstants
|
||||
from .base import CardMakerBase
|
||||
from .cm135 import CardMaker135
|
||||
|
||||
|
||||
class CardMakerServlet:
|
||||
class CardMakerServlet(BaseServlet):
|
||||
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()
|
||||
if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"):
|
||||
self.game_cfg.update(
|
||||
@ -55,11 +56,11 @@ class CardMakerServlet:
|
||||
coloredlogs.install(
|
||||
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_allnet_info(
|
||||
def is_game_enabled(
|
||||
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
|
||||
) -> Tuple[bool, str, str]:
|
||||
) -> bool:
|
||||
game_cfg = CardMakerConfig()
|
||||
if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"):
|
||||
game_cfg.update(
|
||||
@ -67,22 +68,21 @@ class CardMakerServlet:
|
||||
)
|
||||
|
||||
if not game_cfg.server.enable:
|
||||
return (False, "", "")
|
||||
return False
|
||||
|
||||
if core_cfg.server.is_develop:
|
||||
return (
|
||||
True,
|
||||
f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/",
|
||||
"",
|
||||
)
|
||||
return True
|
||||
|
||||
def get_endpoint_matchers(self) -> Tuple[List[Tuple[str, str, Dict]], List[Tuple[str, str, Dict]]]:
|
||||
return (
|
||||
[],
|
||||
[("render_POST", "/SDED/{version}/{endpoint}", {})]
|
||||
)
|
||||
|
||||
return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "")
|
||||
|
||||
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()
|
||||
url_split = url_path.split("/")
|
||||
internal_ver = 0
|
||||
endpoint = url_split[len(url_split) - 1]
|
||||
client_ip = Utils.get_ip_addr(request)
|
||||
|
||||
if version >= 130 and version < 135: # Card Maker
|
||||
|
@ -205,6 +205,7 @@ class CardMakerReader(BaseReader):
|
||||
"1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE,
|
||||
"1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS,
|
||||
"1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL,
|
||||
"1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS,
|
||||
}
|
||||
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
|
@ -3,13 +3,12 @@ import json
|
||||
from decimal import Decimal
|
||||
from base64 import b64encode
|
||||
from typing import Any, Dict, List
|
||||
from hashlib import md5
|
||||
from datetime import datetime
|
||||
from os import path
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.cxb.config import CxbConfig
|
||||
from titles.cxb.const import CxbConstants
|
||||
from titles.cxb.database import CxbData
|
||||
from .config import CxbConfig
|
||||
from .const import CxbConstants
|
||||
from .database import CxbData
|
||||
|
||||
from threading import Thread
|
||||
|
||||
@ -22,6 +21,13 @@ class CxbBase:
|
||||
self.logger = logging.getLogger("cxb")
|
||||
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:
|
||||
return {}
|
||||
|
||||
@ -192,7 +198,7 @@ class CxbBase:
|
||||
).decode("utf-8")
|
||||
)
|
||||
|
||||
def task_generateIndexData(versionindex):
|
||||
def task_generateIndexData(self, versionindex: List[str], uid: int):
|
||||
try:
|
||||
v_profile = self.data.profile.get_profile_index(0, uid, self.version)
|
||||
v_profile_data = v_profile["data"]
|
||||
@ -274,7 +280,7 @@ class CxbBase:
|
||||
thread_ScoreData.start()
|
||||
|
||||
for v in index:
|
||||
thread_IndexData = Thread(target=CxbBase.task_generateIndexData(versionindex))
|
||||
thread_IndexData = Thread(target=CxbBase.task_generateIndexData(self, versionindex, uid))
|
||||
thread_IndexData.start()
|
||||
|
||||
return {"index": index, "data": data1, "version": versionindex}
|
||||
@ -530,7 +536,6 @@ class CxbBase:
|
||||
profile = self.data.profile.get_profile_index(0, uid, self.version)
|
||||
data1 = profile["data"]
|
||||
p = self.data.item.get_energy(uid)
|
||||
energy = p["energy"]
|
||||
|
||||
if not p:
|
||||
self.data.item.put_energy(uid, 5)
|
||||
@ -543,6 +548,7 @@ class CxbBase:
|
||||
}
|
||||
|
||||
array = []
|
||||
energy = p["energy"]
|
||||
|
||||
newenergy = int(energy) + 5
|
||||
self.data.item.put_energy(uid, newenergy)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user