Merge branch 'develop' into fork_develop

This commit is contained in:
Dniel97 2023-03-12 01:05:31 +01:00
commit 3a234244d4
Signed by untrusted user: Dniel97
GPG Key ID: 6180B3C768FB2E08
151 changed files with 9046 additions and 4953 deletions

View File

@ -4,4 +4,4 @@ from core.aimedb import AimedbFactory
from core.title import TitleServlet
from core.utils import Utils
from core.mucha import MuchaServlet
from core.frontend import FrontendServlet
from core.frontend import FrontendServlet

View File

@ -8,17 +8,18 @@ from logging.handlers import TimedRotatingFileHandler
from core.config import CoreConfig
from core.data import Data
class AimedbProtocol(Protocol):
AIMEDB_RESPONSE_CODES = {
"felica_lookup": 0x03,
"lookup": 0x06,
"log": 0x0a,
"campaign": 0x0c,
"touch": 0x0e,
"log": 0x0A,
"campaign": 0x0C,
"touch": 0x0E,
"lookup2": 0x10,
"felica_lookup2": 0x12,
"log2": 0x14,
"hello": 0x65
"hello": 0x65,
}
request_list: Dict[int, Any] = {}
@ -30,14 +31,14 @@ class AimedbProtocol(Protocol):
if core_cfg.aimedb.key == "":
self.logger.error("!!!KEY NOT SET!!!")
exit(1)
self.request_list[0x01] = self.handle_felica_lookup
self.request_list[0x04] = self.handle_lookup
self.request_list[0x05] = self.handle_register
self.request_list[0x09] = self.handle_log
self.request_list[0x0b] = self.handle_campaign
self.request_list[0x0d] = self.handle_touch
self.request_list[0x0f] = self.handle_lookup2
self.request_list[0x09] = self.handle_log
self.request_list[0x0B] = self.handle_campaign
self.request_list[0x0D] = self.handle_touch
self.request_list[0x0F] = self.handle_lookup2
self.request_list[0x11] = self.handle_felica_lookup2
self.request_list[0x13] = self.handle_log2
self.request_list[0x64] = self.handle_hello
@ -53,8 +54,10 @@ class AimedbProtocol(Protocol):
self.logger.debug(f"{self.transport.getPeer().host} Connected")
def connectionLost(self, reason) -> None:
self.logger.debug(f"{self.transport.getPeer().host} Disconnected - {reason.value}")
self.logger.debug(
f"{self.transport.getPeer().host} Disconnected - {reason.value}"
)
def dataReceived(self, data: bytes) -> None:
cipher = AES.new(self.config.aimedb.key.encode(), AES.MODE_ECB)
@ -66,7 +69,7 @@ class AimedbProtocol(Protocol):
self.logger.debug(f"{self.transport.getPeer().host} wrote {decrypted.hex()}")
if not decrypted[1] == 0xa1 and not decrypted[0] == 0x3e:
if not decrypted[1] == 0xA1 and not decrypted[0] == 0x3E:
self.logger.error(f"Bad magic")
return None
@ -90,30 +93,46 @@ class AimedbProtocol(Protocol):
except ValueError as e:
self.logger.error(f"Failed to encrypt {resp.hex()} because {e}")
return None
def handle_campaign(self, data: bytes) -> bytes:
self.logger.info(f"campaign from {self.transport.getPeer().host}")
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["campaign"], 0x0200, 0x0001)
ret = struct.pack(
"<5H",
0xA13E,
0x3087,
self.AIMEDB_RESPONSE_CODES["campaign"],
0x0200,
0x0001,
)
return self.append_padding(ret)
def handle_hello(self, data: bytes) -> bytes:
self.logger.info(f"hello from {self.transport.getPeer().host}")
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["hello"], 0x0020, 0x0001)
ret = struct.pack(
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["hello"], 0x0020, 0x0001
)
return self.append_padding(ret)
def handle_lookup(self, data: bytes) -> bytes:
luid = data[0x20: 0x2a].hex()
luid = data[0x20:0x2A].hex()
user_id = self.data.card.get_user_id_from_card(access_code=luid)
if user_id is None: user_id = -1
if user_id is None:
user_id = -1
self.logger.info(f"lookup from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}")
self.logger.info(
f"lookup from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}"
)
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["lookup"], 0x0130, 0x0001)
ret = struct.pack(
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["lookup"], 0x0130, 0x0001
)
ret += bytes(0x20 - len(ret))
if user_id is None: ret += struct.pack("<iH", -1, 0)
else: ret += struct.pack("<l", user_id)
if user_id is None:
ret += struct.pack("<iH", -1, 0)
else:
ret += struct.pack("<l", user_id)
return self.append_padding(ret)
def handle_lookup2(self, data: bytes) -> bytes:
@ -125,66 +144,98 @@ class AimedbProtocol(Protocol):
return bytes(ret)
def handle_felica_lookup(self, data: bytes) -> bytes:
idm = data[0x20: 0x28].hex()
pmm = data[0x28: 0x30].hex()
idm = data[0x20:0x28].hex()
pmm = data[0x28:0x30].hex()
access_code = self.data.card.to_access_code(idm)
self.logger.info(f"felica_lookup from {self.transport.getPeer().host}: idm {idm} pmm {pmm} -> access_code {access_code}")
self.logger.info(
f"felica_lookup from {self.transport.getPeer().host}: idm {idm} pmm {pmm} -> access_code {access_code}"
)
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["felica_lookup"], 0x0030, 0x0001)
ret = struct.pack(
"<5H",
0xA13E,
0x3087,
self.AIMEDB_RESPONSE_CODES["felica_lookup"],
0x0030,
0x0001,
)
ret += bytes(26)
ret += bytes.fromhex(access_code)
return self.append_padding(ret)
def handle_felica_lookup2(self, data: bytes) -> bytes:
idm = data[0x30: 0x38].hex()
pmm = data[0x38: 0x40].hex()
idm = data[0x30:0x38].hex()
pmm = data[0x38:0x40].hex()
access_code = self.data.card.to_access_code(idm)
user_id = self.data.card.get_user_id_from_card(access_code=access_code)
if user_id is None: user_id = -1
if user_id is None:
user_id = -1
self.logger.info(f"felica_lookup2 from {self.transport.getPeer().host}: idm {idm} ipm {pmm} -> access_code {access_code} user_id {user_id}")
self.logger.info(
f"felica_lookup2 from {self.transport.getPeer().host}: idm {idm} ipm {pmm} -> access_code {access_code} user_id {user_id}"
)
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["felica_lookup2"], 0x0140, 0x0001)
ret = struct.pack(
"<5H",
0xA13E,
0x3087,
self.AIMEDB_RESPONSE_CODES["felica_lookup2"],
0x0140,
0x0001,
)
ret += bytes(22)
ret += struct.pack("<lq", user_id, -1) # first -1 is ext_id, 3rd is access code
ret += struct.pack("<lq", user_id, -1) # first -1 is ext_id, 3rd is access code
ret += bytes.fromhex(access_code)
ret += struct.pack("<l", 1)
return self.append_padding(ret)
def handle_touch(self, data: bytes) -> bytes:
self.logger.info(f"touch from {self.transport.getPeer().host}")
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["touch"], 0x0050, 0x0001)
ret = struct.pack(
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["touch"], 0x0050, 0x0001
)
ret += bytes(5)
ret += struct.pack("<3H", 0x6f, 0, 1)
ret += struct.pack("<3H", 0x6F, 0, 1)
return self.append_padding(ret)
def handle_register(self, data: bytes) -> bytes:
luid = data[0x20: 0x2a].hex()
def handle_register(self, data: bytes) -> bytes:
luid = data[0x20:0x2A].hex()
if self.config.server.allow_user_registration:
user_id = self.data.user.create_user()
if user_id is None:
if user_id is None:
user_id = -1
self.logger.error("Failed to register user!")
else:
card_id = self.data.card.create_card(user_id, luid)
if card_id is None:
if card_id is None:
user_id = -1
self.logger.error("Failed to register card!")
self.logger.info(f"register from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}")
self.logger.info(
f"register from {self.transport.getPeer().host}: luid {luid} -> user_id {user_id}"
)
else:
self.logger.info(f"register from {self.transport.getPeer().host} blocked!: luid {luid}")
self.logger.info(
f"register from {self.transport.getPeer().host} blocked!: luid {luid}"
)
user_id = -1
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["lookup"], 0x0030, 0x0001 if user_id > -1 else 0)
ret = struct.pack(
"<5H",
0xA13E,
0x3087,
self.AIMEDB_RESPONSE_CODES["lookup"],
0x0030,
0x0001 if user_id > -1 else 0,
)
ret += bytes(0x20 - len(ret))
ret += struct.pack("<l", user_id)
@ -193,42 +244,54 @@ class AimedbProtocol(Protocol):
def handle_log(self, data: bytes) -> bytes:
# TODO: Save aimedb logs
self.logger.info(f"log from {self.transport.getPeer().host}")
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["log"], 0x0020, 0x0001)
ret = struct.pack(
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["log"], 0x0020, 0x0001
)
return self.append_padding(ret)
def handle_log2(self, data: bytes) -> bytes:
self.logger.info(f"log2 from {self.transport.getPeer().host}")
ret = struct.pack("<5H", 0xa13e, 0x3087, self.AIMEDB_RESPONSE_CODES["log2"], 0x0040, 0x0001)
ret = struct.pack(
"<5H", 0xA13E, 0x3087, self.AIMEDB_RESPONSE_CODES["log2"], 0x0040, 0x0001
)
ret += bytes(22)
ret += struct.pack("H", 1)
return self.append_padding(ret)
class AimedbFactory(Factory):
protocol = AimedbProtocol
def __init__(self, cfg: CoreConfig) -> None:
self.config = cfg
log_fmt_str = "[%(asctime)s] Aimedb | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
self.logger = logging.getLogger("aimedb")
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "aimedb"), when="d", backupCount=10)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.config.server.log_dir, "aimedb"),
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler)
self.logger.setLevel(self.config.aimedb.loglevel)
coloredlogs.install(level=cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str)
coloredlogs.install(
level=cfg.aimedb.loglevel, logger=self.logger, fmt=log_fmt_str
)
if self.config.aimedb.key == "":
self.logger.error("Please set 'key' field in your config file.")
exit(1)
self.logger.info(f"Ready on port {self.config.aimedb.port}")
def buildProtocol(self, addr):
return AimedbProtocol(self.config)

View File

@ -16,8 +16,9 @@ from core.data import Data
from core.utils import Utils
from core.const import *
class AllnetServlet:
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
super().__init__()
self.config = core_cfg
self.config_folder = cfg_folder
@ -27,35 +28,45 @@ class AllnetServlet:
self.logger = logging.getLogger("allnet")
if not hasattr(self.logger, "initialized"):
log_fmt_str = "[%(asctime)s] Allnet | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "allnet"), when="d", backupCount=10)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.config.server.log_dir, "allnet"),
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler)
self.logger.setLevel(core_cfg.allnet.loglevel)
coloredlogs.install(level=core_cfg.allnet.loglevel, logger=self.logger, fmt=log_fmt_str)
coloredlogs.install(
level=core_cfg.allnet.loglevel, logger=self.logger, fmt=log_fmt_str
)
self.logger.initialized = True
plugins = Utils.get_all_titles()
if len(plugins) == 0:
self.logger.error("No games detected!")
for _, mod in plugins.items():
if 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)
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"Allnet serving {len(self.uri_registry)} games on port {core_cfg.allnet.port}")
self.logger.info(
f"Allnet serving {len(self.uri_registry)} games on port {core_cfg.allnet.port}"
)
def handle_poweron(self, request: Request, _: Dict):
request_ip = request.getClientAddress().host
@ -67,15 +78,17 @@ class AllnetServlet:
req = AllnetPowerOnRequest(req_dict[0])
# Validate the request. Currently we only validate the fields we plan on using
if not req.game_id or not req.ver or not req.token or not req.serial or not req.ip:
raise AllnetRequestException(f"Bad auth request params from {request_ip} - {vars(req)}")
if not req.game_id or not req.ver or not req.serial or not req.ip:
raise AllnetRequestException(
f"Bad auth request params from {request_ip} - {vars(req)}"
)
except AllnetRequestException as e:
if e.message != "":
self.logger.error(e)
return b""
if req.format_ver == 3:
if req.format_ver == "3":
resp = AllnetPowerOnResponse3(req.token)
else:
resp = AllnetPowerOnResponse2()
@ -83,26 +96,32 @@ class AllnetServlet:
self.logger.debug(f"Allnet request: {vars(req)}")
if req.game_id not in self.uri_registry:
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
self.data.base.log_event("allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg)
self.data.base.log_event(
"allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg
)
self.logger.warn(msg)
resp.stat = 0
return self.dict_to_http_form_string([vars(resp)])
resp.uri, resp.host = self.uri_registry[req.game_id]
machine = self.data.arcade.get_machine(req.serial)
if machine is None and not self.config.server.allow_unregistered_serials:
msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}."
self.data.base.log_event("allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg)
self.data.base.log_event(
"allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg
)
self.logger.warn(msg)
resp.stat = 0
return self.dict_to_http_form_string([vars(resp)])
if machine is not None:
arcade = self.data.arcade.get_arcade(machine["arcade"])
country = arcade["country"] if machine["country"] is None else machine["country"]
country = (
arcade["country"] if machine["country"] is None else machine["country"]
)
if country is None:
country = AllnetCountryCode.JAPAN.value
@ -111,16 +130,30 @@ class AllnetServlet:
resp.allnet_id = machine["id"]
resp.name = arcade["name"] if arcade["name"] is not None else ""
resp.nickname = arcade["nickname"] if arcade["nickname"] is not None else ""
resp.region0 = arcade["region_id"] if arcade["region_id"] is not None else AllnetJapanRegionId.AICHI.value
resp.region_name0 = arcade["country"] if arcade["country"] is not None else AllnetCountryCode.JAPAN.value
resp.region_name1 = arcade["state"] if arcade["state"] is not None else AllnetJapanRegionId.AICHI.name
resp.region0 = (
arcade["region_id"]
if arcade["region_id"] is not None
else AllnetJapanRegionId.AICHI.value
)
resp.region_name0 = (
arcade["country"]
if arcade["country"] is not None
else AllnetCountryCode.JAPAN.value
)
resp.region_name1 = (
arcade["state"]
if arcade["state"] is not None
else AllnetJapanRegionId.AICHI.name
)
resp.region_name2 = arcade["city"] if arcade["city"] is not None else ""
resp.client_timezone = arcade["timezone"] if arcade["timezone"] is not None else "+0900"
resp.client_timezone = (
arcade["timezone"] if arcade["timezone"] is not None else "+0900"
)
int_ver = req.ver.replace(".", "")
resp.uri = resp.uri.replace("$v", int_ver)
resp.host = resp.host.replace("$v", int_ver)
msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}"
self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg)
self.logger.info(msg)
@ -139,8 +172,10 @@ class AllnetServlet:
# Validate the request. Currently we only validate the fields we plan on using
if not req.game_id or not req.ver or not req.serial:
raise AllnetRequestException(f"Bad download request params from {request_ip} - {vars(req)}")
raise AllnetRequestException(
f"Bad download request params from {request_ip} - {vars(req)}"
)
except AllnetRequestException as e:
if e.message != "":
self.logger.error(e)
@ -149,8 +184,8 @@ class AllnetServlet:
resp = AllnetDownloadOrderResponse()
if not self.config.allnet.allow_online_updates:
return self.dict_to_http_form_string([vars(resp)])
else: # TODO: Actual dlorder response
else: # TODO: Actual dlorder response
return self.dict_to_http_form_string([vars(resp)])
def handle_billing_request(self, request: Request, _: Dict):
@ -159,10 +194,10 @@ class AllnetServlet:
if req_dict is None:
self.logger.error(f"Failed to parse request {request.content.getvalue()}")
return b""
self.logger.debug(f"request {req_dict}")
rsa = RSA.import_key(open(self.config.billing.signing_key, 'rb').read())
rsa = RSA.import_key(open(self.config.billing.signing_key, "rb").read())
signer = PKCS1_v1_5.new(rsa)
digest = SHA.new()
@ -178,30 +213,34 @@ class AllnetServlet:
machine = self.data.arcade.get_machine(kc_serial)
if machine is None and not self.config.server.allow_unregistered_serials:
msg = f"Unrecognised serial {kc_serial} attempted billing checkin from {request_ip} for game {kc_game}."
self.data.base.log_event("allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg)
self.data.base.log_event(
"allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg
)
self.logger.warn(msg)
resp = BillingResponse("", "", "", "")
resp.result = "1"
return self.dict_to_http_form_string([vars(resp)])
msg = f"Billing checkin from {request.getClientIP()}: game {kc_game} keychip {kc_serial} playcount " \
msg = (
f"Billing checkin from {request.getClientIP()}: game {kc_game} keychip {kc_serial} playcount "
f"{kc_playcount} billing_type {kc_billigtype} nearfull {kc_nearfull} playlimit {kc_playlimit}"
)
self.logger.info(msg)
self.data.base.log_event('billing', 'BILLING_CHECKIN_OK', logging.INFO, msg)
self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, msg)
while kc_playcount > kc_playlimit:
kc_playlimit += 1024
kc_nearfull += 1024
playlimit = kc_playlimit
nearfull = kc_nearfull + (kc_billigtype * 0x00010000)
digest.update(playlimit.to_bytes(4, 'little') + kc_serial_bytes)
digest.update(playlimit.to_bytes(4, "little") + kc_serial_bytes)
playlimit_sig = signer.sign(digest).hex()
digest = SHA.new()
digest.update(nearfull.to_bytes(4, 'little') + kc_serial_bytes)
digest.update(nearfull.to_bytes(4, "little") + kc_serial_bytes)
nearfull_sig = signer.sign(digest).hex()
# TODO: playhistory
@ -222,16 +261,16 @@ class AllnetServlet:
def kvp_to_dict(self, kvp: List[str]) -> List[Dict[str, Any]]:
ret: List[Dict[str, Any]] = []
for x in kvp:
items = x.split('&')
items = x.split("&")
tmp = {}
for item in items:
kvp = item.split('=')
kvp = item.split("=")
if len(kvp) == 2:
tmp[kvp[0]] = kvp[1]
ret.append(tmp)
return ret
def billing_req_to_dict(self, data: bytes):
@ -241,8 +280,8 @@ class AllnetServlet:
try:
decomp = zlib.decompressobj(-zlib.MAX_WBITS)
unzipped = decomp.decompress(data)
sections = unzipped.decode('ascii').split('\r\n')
sections = unzipped.decode("ascii").split("\r\n")
return self.kvp_to_dict(sections)
except Exception as e:
@ -252,33 +291,38 @@ class AllnetServlet:
def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]:
"""
Parses an allnet request string into a python dictionary
"""
"""
try:
zipped = base64.b64decode(data)
unzipped = zlib.decompress(zipped)
sections = unzipped.decode('utf-8').split('\r\n')
sections = unzipped.decode("utf-8").split("\r\n")
return self.kvp_to_dict(sections)
except Exception as e:
self.logger.error(f"allnet_req_to_dict: {e} while parsing {data}")
return None
def dict_to_http_form_string(self, data:List[Dict[str, Any]], crlf: bool = False, trailing_newline: bool = True) -> Optional[str]:
def dict_to_http_form_string(
self,
data: List[Dict[str, Any]],
crlf: bool = False,
trailing_newline: bool = True,
) -> Optional[str]:
"""
Takes a python dictionary and parses it into an allnet response string
"""
try:
urlencode = ""
for item in data:
for k,v in item.items():
for k, v in item.items():
urlencode += f"{k}={v}&"
if crlf:
urlencode = urlencode[:-1] + "\r\n"
else:
urlencode = urlencode[:-1] + "\n"
if not trailing_newline:
if crlf:
urlencode = urlencode[:-2]
@ -286,31 +330,29 @@ class AllnetServlet:
urlencode = urlencode[:-1]
return urlencode
except Exception as e:
self.logger.error(f"dict_to_http_form_string: {e} while parsing {data}")
return None
class AllnetPowerOnRequest():
class AllnetPowerOnRequest:
def __init__(self, req: Dict) -> None:
if req is None:
raise AllnetRequestException("Request processing failed")
self.game_id: str = req["game_id"] if "game_id" in req else ""
self.ver: str = req["ver"] if "ver" in req else ""
self.serial: str = req["serial"] if "serial" in req else ""
self.ip: str = req["ip"] if "ip" in req else ""
self.firm_ver: str = req["firm_ver"] if "firm_ver" in req else ""
self.boot_ver: str = req["boot_ver"] if "boot_ver" in req else ""
self.encode: str = req["encode"] if "encode" in req else ""
try:
self.hops = int(req["hops"]) if "hops" in req else 0
self.format_ver = int(req["format_ver"]) if "format_ver" in req else 2
self.token = int(req["token"]) if "token" in req else 0
except ValueError as e:
raise AllnetRequestException(f"Failed to parse int: {e}")
self.game_id: str = req.get("game_id", "")
self.ver: str = req.get("ver", "")
self.serial: str = req.get("serial", "")
self.ip: str = req.get("ip", "")
self.firm_ver: str = req.get("firm_ver", "")
self.boot_ver: str = req.get("boot_ver", "")
self.encode: str = req.get("encode", "")
self.hops = int(req.get("hops", "0"))
self.format_ver = req.get("format_ver", "2")
self.token = int(req.get("token", "0"))
class AllnetPowerOnResponse3():
class AllnetPowerOnResponse3:
def __init__(self, token) -> None:
self.stat = 1
self.uri = ""
@ -326,12 +368,15 @@ class AllnetPowerOnResponse3():
self.country = "JPN"
self.allnet_id = "123"
self.client_timezone = "+0900"
self.utc_time = datetime.now(tz=pytz.timezone('UTC')).strftime("%Y-%m-%dT%H:%M:%SZ")
self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
self.setting = ""
self.res_ver = "3"
self.token = str(token)
class AllnetPowerOnResponse2():
class AllnetPowerOnResponse2:
def __init__(self) -> None:
self.stat = 1
self.uri = ""
@ -355,23 +400,31 @@ class AllnetPowerOnResponse2():
self.timezone = "+0900"
self.res_class = "PowerOnResponseV2"
class AllnetDownloadOrderRequest():
def __init__(self, req: Dict) -> None:
self.game_id = req["game_id"] if "game_id" in req else ""
self.ver = req["ver"] if "ver" in req else ""
self.serial = req["serial"] if "serial" in req else ""
self.encode = req["encode"] if "encode" in req else ""
class AllnetDownloadOrderResponse():
class AllnetDownloadOrderRequest:
def __init__(self, req: Dict) -> None:
self.game_id = req.get("game_id", "")
self.ver = req.get("ver", "")
self.serial = req.get("serial", "")
self.encode = req.get("encode", "")
class AllnetDownloadOrderResponse:
def __init__(self, stat: int = 1, serial: str = "", uri: str = "null") -> None:
self.stat = stat
self.serial = serial
self.uri = uri
class BillingResponse():
def __init__(self, playlimit: str = "", playlimit_sig: str = "", nearfull: str = "", nearfull_sig: str = "",
playhistory: str = "000000/0:000000/0:000000/0") -> None:
class BillingResponse:
def __init__(
self,
playlimit: str = "",
playlimit_sig: str = "",
nearfull: str = "",
nearfull_sig: str = "",
playhistory: str = "000000/0:000000/0:000000/0",
) -> None:
self.result = "0"
self.waitime = "100"
self.linelimit = "1"
@ -383,10 +436,11 @@ class BillingResponse():
self.nearfullsig = nearfull_sig
self.fixlogincnt = "0"
self.fixinterval = "5"
self.playhistory = playhistory
self.playhistory = playhistory
# playhistory -> YYYYMM/C:...
# YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period
class AllnetRequestException(Exception):
def __init__(self, message="") -> None:
self.message = message

View File

@ -1,33 +1,47 @@
import logging, os
from typing import Any
class ServerConfig:
def __init__(self, parent_config: "CoreConfig") -> None:
self.__config = parent_config
@property
def listen_address(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'server', 'listen_address', default='127.0.0.1')
return CoreConfig.get_config_field(
self.__config, "core", "server", "listen_address", default="127.0.0.1"
)
@property
def allow_user_registration(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'core', 'server', 'allow_user_registration', default=True)
return CoreConfig.get_config_field(
self.__config, "core", "server", "allow_user_registration", default=True
)
@property
def allow_unregistered_serials(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'core', 'server', 'allow_unregistered_serials', default=True)
return CoreConfig.get_config_field(
self.__config, "core", "server", "allow_unregistered_serials", default=True
)
@property
def name(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'server', 'name', default="ARTEMiS")
return CoreConfig.get_config_field(
self.__config, "core", "server", "name", default="ARTEMiS"
)
@property
def is_develop(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'core', 'server', 'is_develop', default=True)
return CoreConfig.get_config_field(
self.__config, "core", "server", "is_develop", default=True
)
@property
def log_dir(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'server', 'log_dir', default='logs')
return CoreConfig.get_config_field(
self.__config, "core", "server", "log_dir", default="logs"
)
class TitleConfig:
def __init__(self, parent_config: "CoreConfig") -> None:
@ -35,15 +49,24 @@ class TitleConfig:
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'title', 'loglevel', default="info"))
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "core", "title", "loglevel", default="info"
)
)
@property
def hostname(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'title', 'hostname', default="localhost")
return CoreConfig.get_config_field(
self.__config, "core", "title", "hostname", default="localhost"
)
@property
def port(self) -> int:
return CoreConfig.get_config_field(self.__config, 'core', 'title', 'port', default=8080)
return CoreConfig.get_config_field(
self.__config, "core", "title", "port", default=8080
)
class DatabaseConfig:
def __init__(self, parent_config: "CoreConfig") -> None:
@ -51,43 +74,70 @@ class DatabaseConfig:
@property
def host(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'host', default="localhost")
return CoreConfig.get_config_field(
self.__config, "core", "database", "host", default="localhost"
)
@property
def username(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'username', default='aime')
return CoreConfig.get_config_field(
self.__config, "core", "database", "username", default="aime"
)
@property
def password(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'password', default='aime')
return CoreConfig.get_config_field(
self.__config, "core", "database", "password", default="aime"
)
@property
def name(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'name', default='aime')
return CoreConfig.get_config_field(
self.__config, "core", "database", "name", default="aime"
)
@property
def port(self) -> int:
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'port', default=3306)
return CoreConfig.get_config_field(
self.__config, "core", "database", "port", default=3306
)
@property
def protocol(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'type', default="mysql")
return CoreConfig.get_config_field(
self.__config, "core", "database", "type", default="mysql"
)
@property
def sha2_password(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'sha2_password', default=False)
return CoreConfig.get_config_field(
self.__config, "core", "database", "sha2_password", default=False
)
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'database', 'loglevel', default="info"))
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "core", "database", "loglevel", default="info"
)
)
@property
def user_table_autoincrement_start(self) -> int:
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'user_table_autoincrement_start', default=10000)
return CoreConfig.get_config_field(
self.__config,
"core",
"database",
"user_table_autoincrement_start",
default=10000,
)
@property
def memcached_host(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'database', 'memcached_host', default="localhost")
return CoreConfig.get_config_field(
self.__config, "core", "database", "memcached_host", default="localhost"
)
class FrontendConfig:
def __init__(self, parent_config: "CoreConfig") -> None:
@ -95,15 +145,24 @@ class FrontendConfig:
@property
def enable(self) -> int:
return CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'enable', default=False)
return CoreConfig.get_config_field(
self.__config, "core", "frontend", "enable", default=False
)
@property
def port(self) -> int:
return CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'port', default=8090)
return CoreConfig.get_config_field(
self.__config, "core", "frontend", "port", default=8090
)
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'frontend', 'loglevel', default="info"))
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "core", "frontend", "loglevel", default="info"
)
)
class AllnetConfig:
def __init__(self, parent_config: "CoreConfig") -> None:
@ -111,15 +170,24 @@ class AllnetConfig:
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'allnet', 'loglevel', default="info"))
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "core", "allnet", "loglevel", default="info"
)
)
@property
def port(self) -> int:
return CoreConfig.get_config_field(self.__config, 'core', 'allnet', 'port', default=80)
return CoreConfig.get_config_field(
self.__config, "core", "allnet", "port", default=80
)
@property
def allow_online_updates(self) -> int:
return CoreConfig.get_config_field(self.__config, 'core', 'allnet', 'allow_online_updates', default=False)
return CoreConfig.get_config_field(
self.__config, "core", "allnet", "allow_online_updates", default=False
)
class BillingConfig:
def __init__(self, parent_config: "CoreConfig") -> None:
@ -127,35 +195,53 @@ class BillingConfig:
@property
def port(self) -> int:
return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'port', default=8443)
return CoreConfig.get_config_field(
self.__config, "core", "billing", "port", default=8443
)
@property
def ssl_key(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'ssl_key', default="cert/server.key")
return CoreConfig.get_config_field(
self.__config, "core", "billing", "ssl_key", default="cert/server.key"
)
@property
def ssl_cert(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'ssl_cert', default="cert/server.pem")
return CoreConfig.get_config_field(
self.__config, "core", "billing", "ssl_cert", default="cert/server.pem"
)
@property
def signing_key(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'billing', 'signing_key', default="cert/billing.key")
return CoreConfig.get_config_field(
self.__config, "core", "billing", "signing_key", default="cert/billing.key"
)
class AimedbConfig:
def __init__(self, parent_config: "CoreConfig") -> None:
self.__config = parent_config
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'aimedb', 'loglevel', default="info"))
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "core", "aimedb", "loglevel", default="info"
)
)
@property
def port(self) -> int:
return CoreConfig.get_config_field(self.__config, 'core', 'aimedb', 'port', default=22345)
return CoreConfig.get_config_field(
self.__config, "core", "aimedb", "port", default=22345
)
@property
def key(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'aimedb', 'key', default="")
return CoreConfig.get_config_field(
self.__config, "core", "aimedb", "key", default=""
)
class MuchaConfig:
def __init__(self, parent_config: "CoreConfig") -> None:
@ -163,27 +249,42 @@ class MuchaConfig:
@property
def enable(self) -> int:
return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'enable', default=False)
return CoreConfig.get_config_field(
self.__config, "core", "mucha", "enable", default=False
)
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'loglevel', default="info"))
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "core", "mucha", "loglevel", default="info"
)
)
@property
def hostname(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'hostname', default="localhost")
return CoreConfig.get_config_field(
self.__config, "core", "mucha", "hostname", default="localhost"
)
@property
def port(self) -> int:
return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'port', default=8444)
return CoreConfig.get_config_field(
self.__config, "core", "mucha", "port", default=8444
)
@property
def ssl_cert(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'ssl_cert', default="cert/server.pem")
return CoreConfig.get_config_field(
self.__config, "core", "mucha", "ssl_cert", default="cert/server.pem"
)
@property
def signing_key(self) -> str:
return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'signing_key', default="cert/billing.key")
return CoreConfig.get_config_field(
self.__config, "core", "mucha", "signing_key", default="cert/billing.key"
)
class CoreConfig(dict):
def __init__(self) -> None:
@ -200,20 +301,22 @@ class CoreConfig(dict):
def str_to_loglevel(cls, level_str: str):
if level_str.lower() == "error":
return logging.ERROR
elif level_str.lower().startswith("warn"): # Fits warn or warning
elif level_str.lower().startswith("warn"): # Fits warn or warning
return logging.WARN
elif level_str.lower() == "debug":
return logging.DEBUG
else:
return logging.INFO
return logging.INFO
@classmethod
def get_config_field(cls, __config: dict, module, *path: str, default: Any = "") -> Any:
envKey = f'CFG_{module}_'
def get_config_field(
cls, __config: dict, module, *path: str, default: Any = ""
) -> Any:
envKey = f"CFG_{module}_"
for arg in path:
envKey += arg + '_'
if envKey.endswith('_'):
envKey += arg + "_"
if envKey.endswith("_"):
envKey = envKey[:-1]
if envKey in os.environ:

View File

@ -1,6 +1,7 @@
from enum import Enum
class MainboardPlatformCodes():
class MainboardPlatformCodes:
RINGEDGE = "AALE"
RINGWIDE = "AAML"
NU = "AAVE"
@ -8,7 +9,8 @@ class MainboardPlatformCodes():
ALLS_UX = "ACAE"
ALLS_HX = "ACAX"
class MainboardRevisions():
class MainboardRevisions:
RINGEDGE = 1
RINGEDGE2 = 2
@ -26,12 +28,14 @@ class MainboardRevisions():
ALLS_UX2 = 2
ALLS_HX2 = 12
class KeychipPlatformsCodes():
class KeychipPlatformsCodes:
RING = "A72E"
NU = ("A60E", "A60E", "A60E")
NUSX = ("A61X", "A69X")
ALLS = "A63E"
class AllnetCountryCode(Enum):
JAPAN = "JPN"
UNITED_STATES = "USA"
@ -41,6 +45,7 @@ class AllnetCountryCode(Enum):
TAIWAN = "TWN"
CHINA = "CHN"
class AllnetJapanRegionId(Enum):
NONE = 0
AICHI = 1

View File

@ -1,2 +1,2 @@
from core.data.database import Data
from core.data.cache import cached
from core.data.cache import cached

View File

@ -1,4 +1,3 @@
from typing import Any, Callable
from functools import wraps
import hashlib
@ -6,15 +5,17 @@ import pickle
import logging
from core.config import CoreConfig
cfg:CoreConfig = None # type: ignore
cfg: CoreConfig = None # type: ignore
# Make memcache optional
try:
import pylibmc # type: ignore
has_mc = True
except ModuleNotFoundError:
has_mc = False
def cached(lifetime: int=10, extra_key: Any=None) -> Callable:
def cached(lifetime: int = 10, extra_key: Any = None) -> Callable:
def _cached(func: Callable) -> Callable:
if has_mc:
hostname = "127.0.0.1"
@ -22,11 +23,10 @@ def cached(lifetime: int=10, extra_key: Any=None) -> Callable:
hostname = cfg.database.memcached_host
memcache = pylibmc.Client([hostname], binary=True)
memcache.behaviors = {"tcp_nodelay": True, "ketama": True}
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
if lifetime is not None:
# Hash function args
items = kwargs.items()
hashable_args = (args[1:], sorted(list(items)))
@ -41,7 +41,7 @@ def cached(lifetime: int=10, extra_key: Any=None) -> Callable:
except pylibmc.Error as e:
logging.getLogger("database").error(f"Memcache failed: {e}")
result = None
if result is not None:
logging.getLogger("database").debug(f"Cache hit: {result}")
return result
@ -55,7 +55,9 @@ def cached(lifetime: int=10, extra_key: Any=None) -> Callable:
memcache.set(cache_key, result, lifetime)
return result
else:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
return func(*args, **kwargs)

View File

@ -13,6 +13,7 @@ from core.config import CoreConfig
from core.data.schema import *
from core.utils import Utils
class Data:
def __init__(self, cfg: CoreConfig) -> None:
self.config = cfg
@ -22,7 +23,7 @@ class Data:
self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{passwd.hex()}@{self.config.database.host}/{self.config.database.name}?charset=utf8mb4"
else:
self.__url = f"{self.config.database.protocol}://{self.config.database.username}:{self.config.database.password}@{self.config.database.host}/{self.config.database.name}?charset=utf8mb4"
self.__engine = create_engine(self.__url, pool_recycle=3600)
session = sessionmaker(bind=self.__engine, autoflush=True, autocommit=True)
self.session = scoped_session(session)
@ -38,11 +39,15 @@ class Data:
self.logger = logging.getLogger("database")
# Prevent the logger from adding handlers multiple times
if not getattr(self.logger, 'handler_set', None):
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "db"), encoding="utf-8",
when="d", backupCount=10)
if not getattr(self.logger, "handler_set", None):
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.config.server.log_dir, "db"),
encoding="utf-8",
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
@ -50,8 +55,10 @@ class Data:
self.logger.addHandler(consoleHandler)
self.logger.setLevel(self.config.database.loglevel)
coloredlogs.install(cfg.database.loglevel, logger=self.logger, fmt=log_fmt_str)
self.logger.handler_set = True # type: ignore
coloredlogs.install(
cfg.database.loglevel, logger=self.logger, fmt=log_fmt_str
)
self.logger.handler_set = True # type: ignore
def create_database(self):
self.logger.info("Creating databases...")
@ -60,24 +67,32 @@ class Data:
except SQLAlchemyError as e:
self.logger.error(f"Failed to create databases! {e}")
return
games = Utils.get_all_titles()
for game_dir, game_mod in games.items():
try:
title_db = game_mod.database(self.config)
metadata.create_all(self.__engine.connect())
self.base.set_schema_ver(game_mod.current_schema_version, game_mod.game_codes[0])
self.base.set_schema_ver(
game_mod.current_schema_version, game_mod.game_codes[0]
)
except Exception as e:
self.logger.warning(f"Could not load database schema from {game_dir} - {e}")
self.logger.warning(
f"Could not load database schema from {game_dir} - {e}"
)
self.logger.info(f"Setting base_schema_ver to {self.schema_ver_latest}")
self.base.set_schema_ver(self.schema_ver_latest)
self.logger.info(f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}")
self.user.reset_autoincrement(self.config.database.user_table_autoincrement_start)
self.logger.info(
f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}"
)
self.user.reset_autoincrement(
self.config.database.user_table_autoincrement_start
)
def recreate_database(self):
self.logger.info("Dropping all databases...")
self.base.execute("SET FOREIGN_KEY_CHECKS=0")
@ -86,61 +101,79 @@ class Data:
except SQLAlchemyError as e:
self.logger.error(f"Failed to drop databases! {e}")
return
for root, dirs, files in os.walk("./titles"):
for dir in dirs:
for dir in dirs:
if not dir.startswith("__"):
try:
mod = importlib.import_module(f"titles.{dir}")
try:
title_db = mod.database(self.config)
metadata.drop_all(self.__engine.connect())
except Exception as e:
self.logger.warning(f"Could not load database schema from {dir} - {e}")
self.logger.warning(
f"Could not load database schema from {dir} - {e}"
)
except ImportError as e:
self.logger.warning(f"Failed to load database schema dir {dir} - {e}")
self.logger.warning(
f"Failed to load database schema dir {dir} - {e}"
)
break
self.base.execute("SET FOREIGN_KEY_CHECKS=1")
self.create_database()
def migrate_database(self, game: str, version: int, action: str) -> None:
old_ver = self.base.get_schema_ver(game)
sql = ""
if old_ver is None:
self.logger.error(f"Schema for game {game} does not exist, did you run the creation script?")
return
if old_ver == version:
self.logger.info(f"Schema for game {game} is already version {old_ver}, nothing to do")
return
if not os.path.exists(f"core/data/schema/versions/{game.upper()}_{version}_{action}.sql"):
self.logger.error(f"Could not find {action} script {game.upper()}_{version}_{action}.sql in core/data/schema/versions folder")
self.logger.error(
f"Schema for game {game} does not exist, did you run the creation script?"
)
return
with open(f"core/data/schema/versions/{game.upper()}_{version}_{action}.sql", "r", encoding="utf-8") as f:
if old_ver == version:
self.logger.info(
f"Schema for game {game} is already version {old_ver}, nothing to do"
)
return
if not os.path.exists(
f"core/data/schema/versions/{game.upper()}_{version}_{action}.sql"
):
self.logger.error(
f"Could not find {action} script {game.upper()}_{version}_{action}.sql in core/data/schema/versions folder"
)
return
with open(
f"core/data/schema/versions/{game.upper()}_{version}_{action}.sql",
"r",
encoding="utf-8",
) as f:
sql = f.read()
result = self.base.execute(sql)
if result is None:
self.logger.error("Error execuing sql script!")
return None
result = self.base.set_schema_ver(version, game)
if result is None:
self.logger.error("Error setting version in schema_version table!")
return None
self.logger.info(f"Successfully migrated {game} to schema version {version}")
def create_owner(self, email: Optional[str] = None) -> None:
pw = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(20))
pw = "".join(
secrets.choice(string.ascii_letters + string.digits) for i in range(20)
)
hash = bcrypt.hashpw(pw.encode(), bcrypt.gensalt())
user_id = self.user.create_user(email=email, permission=255, password=hash)
@ -153,32 +186,38 @@ class Data:
self.logger.error(f"Failed to create card for owner with id {user_id}")
return
self.logger.warn(f"Successfully created owner with email {email}, access code 00000000000000000000, and password {pw} Make sure to change this password and assign a real card ASAP!")
self.logger.warn(
f"Successfully created owner with email {email}, access code 00000000000000000000, and password {pw} Make sure to change this password and assign a real card ASAP!"
)
def migrate_card(self, old_ac: str, new_ac: str, should_force: bool) -> None:
if old_ac == new_ac:
self.logger.error("Both access codes are the same!")
return
new_card = self.card.get_card_by_access_code(new_ac)
if new_card is None:
self.card.update_access_code(old_ac, new_ac)
return
if not should_force:
self.logger.warn(f"Card already exists for access code {new_ac} (id {new_card['id']}). If you wish to continue, rerun with the '--force' flag."\
f" All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}.")
self.logger.warn(
f"Card already exists for access code {new_ac} (id {new_card['id']}). If you wish to continue, rerun with the '--force' flag."
f" All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}."
)
return
self.logger.info(f"All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}.")
self.logger.info(
f"All exiting data on the target card {new_ac} will be perminently erased and replaced with data from card {old_ac}."
)
self.card.delete_card(new_card["id"])
self.card.update_access_code(old_ac, new_ac)
hanging_user = self.user.get_user(new_card["user"])
if hanging_user["password"] is None:
self.logger.info(f"Delete hanging user {hanging_user['id']}")
self.user.delete_user(hanging_user['id'])
self.user.delete_user(hanging_user["id"])
def delete_hanging_users(self) -> None:
"""
Finds and deletes users that have not registered for the webui that have no cards assocated with them.
@ -186,13 +225,13 @@ class Data:
unreg_users = self.user.get_unregistered_users()
if unreg_users is None:
self.logger.error("Error occoured finding unregistered users")
for user in unreg_users:
cards = self.card.get_user_cards(user['id'])
cards = self.card.get_user_cards(user["id"])
if cards is None:
self.logger.error(f"Error getting cards for user {user['id']}")
continue
if not cards:
self.logger.info(f"Delete hanging user {user['id']}")
self.user.delete_user(user['id'])
self.user.delete_user(user["id"])

View File

@ -3,4 +3,4 @@ from core.data.schema.card import CardData
from core.data.schema.base import BaseData, metadata
from core.data.schema.arcade import ArcadeData
__all__ = ["UserData", "CardData", "BaseData", "metadata", "ArcadeData"]
__all__ = ["UserData", "CardData", "BaseData", "metadata", "ArcadeData"]

View File

@ -14,131 +14,186 @@ arcade = Table(
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("name", String(255)),
Column("nickname", String(255)),
Column("nickname", String(255)),
Column("country", String(3)),
Column("country_id", Integer),
Column("state", String(255)),
Column("city", String(255)),
Column("region_id", Integer),
Column("timezone", String(255)),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
machine = Table(
"machine",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("arcade", ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"arcade",
ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("serial", String(15), nullable=False),
Column("board", String(15)),
Column("game", String(4)),
Column("country", String(3)), # overwrites if not null
Column("country", String(3)), # overwrites if not null
Column("timezone", String(255)),
Column("ota_enable", Boolean),
Column("is_cab", Boolean),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
arcade_owner = Table(
'arcade_owner',
"arcade_owner",
metadata,
Column('user', Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column('arcade', Integer, ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column('permissions', Integer, nullable=False),
PrimaryKeyConstraint('user', 'arcade', name='arcade_owner_pk'),
mysql_charset='utf8mb4'
Column(
"user",
Integer,
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column(
"arcade",
Integer,
ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("permissions", Integer, nullable=False),
PrimaryKeyConstraint("user", "arcade", name="arcade_owner_pk"),
mysql_charset="utf8mb4",
)
class ArcadeData(BaseData):
def get_machine(self, serial: str = None, id: int = None) -> Optional[Dict]:
if serial is not None:
serial = serial.replace("-", "")
if len(serial) == 11:
sql = machine.select(machine.c.serial.like(f"{serial}%"))
elif len(serial) == 15:
sql = machine.select(machine.c.serial == serial)
else:
self.logger.error(f"{__name__ }: Malformed serial {serial}")
return None
elif id is not None:
sql = machine.select(machine.c.id == id)
else:
else:
self.logger.error(f"{__name__ }: Need either serial or ID to look up!")
return None
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def put_machine(self, arcade_id: int, serial: str = "", board: str = None, game: str = None, is_cab: bool = False) -> Optional[int]:
def put_machine(
self,
arcade_id: int,
serial: str = "",
board: str = None,
game: str = None,
is_cab: bool = False,
) -> Optional[int]:
if arcade_id:
self.logger.error(f"{__name__ }: Need arcade id!")
return None
sql = machine.insert().values(arcade = arcade_id, keychip = serial, board = board, game = game, is_cab = is_cab)
result = self.execute(sql)
if result is None: return None
return result.lastrowid
def set_machine_serial(self, machine_id: int, serial: str) -> None:
result = self.execute(machine.update(machine.c.id == machine_id).values(keychip = serial))
if result is None:
self.logger.error(f"Failed to update serial for machine {machine_id} -> {serial}")
return result.lastrowid
def set_machine_boardid(self, machine_id: int, boardid: str) -> None:
result = self.execute(machine.update(machine.c.id == machine_id).values(board = boardid))
if result is None:
self.logger.error(f"Failed to update board id for machine {machine_id} -> {boardid}")
def get_arcade(self, id: int) -> Optional[Dict]:
sql = arcade.select(arcade.c.id == id)
result = self.execute(sql)
if result is None: return None
return result.fetchone()
def put_arcade(self, name: str, nickname: str = None, country: str = "JPN", country_id: int = 1,
state: str = "", city: str = "", regional_id: int = 1) -> Optional[int]:
if nickname is None: nickname = name
sql = arcade.insert().values(name = name, nickname = nickname, country = country, country_id = country_id,
state = state, city = city, regional_id = regional_id)
result = self.execute(sql)
if result is None: return None
return result.lastrowid
def get_arcade_owners(self, arcade_id: int) -> Optional[Dict]:
sql = select(arcade_owner).where(arcade_owner.c.arcade==arcade_id)
result = self.execute(sql)
if result is None: return None
return result.fetchall()
def add_arcade_owner(self, arcade_id: int, user_id: int) -> None:
sql = insert(arcade_owner).values(
arcade=arcade_id,
user=user_id
sql = machine.insert().values(
arcade=arcade_id, keychip=serial, board=board, game=game, is_cab=is_cab
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.lastrowid
def format_serial(self, platform_code: str, platform_rev: int, serial_num: int, append: int = 4152) -> str:
return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R
def set_machine_serial(self, machine_id: int, serial: str) -> None:
result = self.execute(
machine.update(machine.c.id == machine_id).values(keychip=serial)
)
if result is None:
self.logger.error(
f"Failed to update serial for machine {machine_id} -> {serial}"
)
return result.lastrowid
def set_machine_boardid(self, machine_id: int, boardid: str) -> None:
result = self.execute(
machine.update(machine.c.id == machine_id).values(board=boardid)
)
if result is None:
self.logger.error(
f"Failed to update board id for machine {machine_id} -> {boardid}"
)
def get_arcade(self, id: int) -> Optional[Dict]:
sql = arcade.select(arcade.c.id == id)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_arcade(
self,
name: str,
nickname: str = None,
country: str = "JPN",
country_id: int = 1,
state: str = "",
city: str = "",
regional_id: int = 1,
) -> Optional[int]:
if nickname is None:
nickname = name
sql = arcade.insert().values(
name=name,
nickname=nickname,
country=country,
country_id=country_id,
state=state,
city=city,
regional_id=regional_id,
)
result = self.execute(sql)
if result is None:
return None
return result.lastrowid
def get_arcade_owners(self, arcade_id: int) -> Optional[Dict]:
sql = select(arcade_owner).where(arcade_owner.c.arcade == arcade_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def add_arcade_owner(self, arcade_id: int, user_id: int) -> None:
sql = insert(arcade_owner).values(arcade=arcade_id, user=user_id)
result = self.execute(sql)
if result is None:
return None
return result.lastrowid
def format_serial(
self, platform_code: str, platform_rev: int, serial_num: int, append: int = 4152
) -> str:
return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R
def validate_keychip_format(self, serial: str) -> bool:
serial = serial.replace("-", "")
if len(serial) != 11 or len(serial) != 15:
self.logger.error(f"Serial validate failed: Incorrect length for {serial} (len {len(serial)})")
self.logger.error(
f"Serial validate failed: Incorrect length for {serial} (len {len(serial)})"
)
return False
platform_code = serial[:4]
platform_rev = serial[4:6]
const_a = serial[6]
@ -150,11 +205,15 @@ class ArcadeData(BaseData):
return False
if len(append) != 0 or len(append) != 4:
self.logger.error(f"Serial validate failed: {serial} had malformed append {append}")
self.logger.error(
f"Serial validate failed: {serial} had malformed append {append}"
)
return False
if len(num) != 4:
self.logger.error(f"Serial validate failed: {serial} had malformed number {num}")
self.logger.error(
f"Serial validate failed: {serial} had malformed number {num}"
)
return False
return True

View File

@ -19,7 +19,7 @@ schema_ver = Table(
metadata,
Column("game", String(4), primary_key=True, nullable=False),
Column("version", Integer, nullable=False, server_default="1"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
event_log = Table(
@ -32,16 +32,17 @@ event_log = Table(
Column("message", String(1000), nullable=False),
Column("details", JSON, nullable=False),
Column("when_logged", TIMESTAMP, nullable=False, server_default=func.now()),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class BaseData():
class BaseData:
def __init__(self, cfg: CoreConfig, conn: Connection) -> None:
self.config = cfg
self.conn = conn
self.logger = logging.getLogger("database")
def execute(self, sql: str, opts: Dict[str, Any]={}) -> Optional[CursorResult]:
def execute(self, sql: str, opts: Dict[str, Any] = {}) -> Optional[CursorResult]:
res = None
try:
@ -51,7 +52,7 @@ class BaseData():
except SQLAlchemyError as e:
self.logger.error(f"SQLAlchemy error {e}")
return None
except UnicodeEncodeError as e:
self.logger.error(f"UnicodeEncodeError error {e}")
return None
@ -63,7 +64,7 @@ class BaseData():
except SQLAlchemyError as e:
self.logger.error(f"SQLAlchemy error {e}")
return None
except UnicodeEncodeError as e:
self.logger.error(f"UnicodeEncodeError error {e}")
return None
@ -73,58 +74,71 @@ class BaseData():
raise
return res
def generate_id(self) -> int:
"""
Generate a random 5-7 digit id
"""
return randrange(10000, 9999999)
def get_schema_ver(self, game: str) -> Optional[int]:
sql = select(schema_ver).where(schema_ver.c.game == game)
result = self.execute(sql)
if result is None:
return None
row = result.fetchone()
if row is None:
return None
return row["version"]
def set_schema_ver(self, ver: int, game: str = "CORE") -> Optional[int]:
sql = insert(schema_ver).values(game = game, version = ver)
conflict = sql.on_duplicate_key_update(version = ver)
sql = insert(schema_ver).values(game=game, version=ver)
conflict = sql.on_duplicate_key_update(version=ver)
result = self.execute(conflict)
if result is None:
self.logger.error(f"Failed to update schema version for game {game} (v{ver})")
self.logger.error(
f"Failed to update schema version for game {game} (v{ver})"
)
return None
return result.lastrowid
def log_event(self, system: str, type: str, severity: int, message: str, details: Dict = {}) -> Optional[int]:
sql = event_log.insert().values(system = system, type = type, severity = severity, message = message, details = json.dumps(details))
def log_event(
self, system: str, type: str, severity: int, message: str, details: Dict = {}
) -> Optional[int]:
sql = event_log.insert().values(
system=system,
type=type,
severity=severity,
message=message,
details=json.dumps(details),
)
result = self.execute(sql)
if result is None:
self.logger.error(f"{__name__}: Failed to insert event into event log! system = {system}, type = {type}, severity = {severity}, message = {message}")
self.logger.error(
f"{__name__}: Failed to insert event into event log! system = {system}, type = {type}, severity = {severity}, message = {message}"
)
return None
return result.lastrowid
def get_event_log(self, entries: int = 100) -> Optional[List[Dict]]:
sql = event_log.select().limit(entries).all()
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def fix_bools(self, data: Dict) -> Dict:
for k,v in data.items():
for k, v in data.items():
if type(v) == str and v.lower() == "true":
data[k] = True
elif type(v) == str and v.lower() == "false":
data[k] = False
return data

View File

@ -8,55 +8,67 @@ from sqlalchemy.engine import Row
from core.data.schema.base import BaseData, metadata
aime_card = Table(
'aime_card',
"aime_card",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("access_code", String(20)),
Column("created_date", TIMESTAMP, server_default=func.now()),
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
Column("is_locked", Boolean, server_default="0"),
Column("is_banned", Boolean, server_default="0"),
UniqueConstraint("user", "access_code", name="aime_card_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class CardData(BaseData):
def get_card_by_access_code(self, access_code: str) -> Optional[Row]:
sql = aime_card.select(aime_card.c.access_code == access_code)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def get_card_by_id(self, card_id: int) -> Optional[Row]:
sql = aime_card.select(aime_card.c.id == card_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def update_access_code(self, old_ac: str, new_ac: str) -> None:
sql = aime_card.update(aime_card.c.access_code == old_ac).values(access_code = new_ac)
sql = aime_card.update(aime_card.c.access_code == old_ac).values(
access_code=new_ac
)
result = self.execute(sql)
if result is None:
self.logger.error(f"Failed to change card access code from {old_ac} to {new_ac}")
if result is None:
self.logger.error(
f"Failed to change card access code from {old_ac} to {new_ac}"
)
def get_user_id_from_card(self, access_code: str) -> Optional[int]:
"""
Given a 20 digit access code as a string, get the user id associated with that card
"""
card = self.get_card_by_access_code(access_code)
if card is None: return None
if card is None:
return None
return int(card["user"])
def delete_card(self, card_id: int) -> None:
sql = aime_card.delete(aime_card.c.id == card_id)
result = self.execute(sql)
if result is None:
if result is None:
self.logger.error(f"Failed to delete card with id {card_id}")
def get_user_cards(self, aime_id: int) -> Optional[List[Row]]:
@ -65,17 +77,18 @@ class CardData(BaseData):
"""
sql = aime_card.select(aime_card.c.user == aime_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def create_card(self, user_id: int, access_code: str) -> Optional[int]:
"""
Given a aime_user id and a 20 digit access code as a string, create a card and return the ID if successful
"""
sql = aime_card.insert().values(user=user_id, access_code=access_code)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.lastrowid
def to_access_code(self, luid: str) -> str:
@ -88,4 +101,4 @@ class CardData(BaseData):
"""
Given a 20 digit access code as a string, return the 16 hex character luid
"""
return f'{int(access_code):0{16}x}'
return f"{int(access_code):0{16}x}"

View File

@ -17,72 +17,81 @@ aime_user = Table(
Column("username", String(25), unique=True),
Column("email", String(255), unique=True),
Column("password", String(255)),
Column("permissions", Integer),
Column("permissions", Integer),
Column("created_date", TIMESTAMP, server_default=func.now()),
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
Column("suspend_expire_time", TIMESTAMP),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class PermissionBits(Enum):
PermUser = 1
PermMod = 2
PermSysAdmin = 4
class UserData(BaseData):
def create_user(self, id: int = None, username: str = None, email: str = None, password: str = None, permission: int = 1) -> Optional[int]:
def create_user(
self,
id: int = None,
username: str = None,
email: str = None,
password: str = None,
permission: int = 1,
) -> Optional[int]:
if id is None:
sql = insert(aime_user).values(
username=username,
email=email,
password=password,
permissions=permission
username=username,
email=email,
password=password,
permissions=permission,
)
else:
sql = insert(aime_user).values(
id=id,
username=username,
email=email,
password=password,
permissions=permission
id=id,
username=username,
email=email,
password=password,
permissions=permission,
)
conflict = sql.on_duplicate_key_update(
username=username,
email=email,
password=password,
permissions=permission
username=username, email=email, password=password, permissions=permission
)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_user(self, user_id: int) -> Optional[Row]:
sql = select(aime_user).where(aime_user.c.id == user_id)
result = self.execute(sql)
if result is None: return False
if result is None:
return False
return result.fetchone()
def check_password(self, user_id: int, passwd: bytes = None) -> bool:
usr = self.get_user(user_id)
if usr is None: return False
if usr['password'] is None:
if usr is None:
return False
return bcrypt.checkpw(passwd, usr['password'].encode())
if usr["password"] is None:
return False
return bcrypt.checkpw(passwd, usr["password"].encode())
def reset_autoincrement(self, ai_value: int) -> None:
# ALTER TABLE isn't in sqlalchemy so we do this the ugly way
sql = f"ALTER TABLE aime_user AUTO_INCREMENT={ai_value}"
self.execute(sql)
def delete_user(self, user_id: int) -> None:
sql = aime_user.delete(aime_user.c.id == user_id)
result = self.execute(sql)
if result is None:
if result is None:
self.logger.error(f"Failed to delete user with id {user_id}")
def get_unregistered_users(self) -> List[Row]:
@ -92,6 +101,6 @@ class UserData(BaseData):
sql = select(aime_user).where(aime_user.c.password == None)
result = self.execute(sql)
if result is None:
if result is None:
return None
return result.fetchall()
return result.fetchall()

View File

@ -14,11 +14,13 @@ from core.config import CoreConfig
from core.data import Data
from core.utils import Utils
class IUserSession(Interface):
userId = Attribute("User's ID")
current_ip = Attribute("User's current ip address")
permissions = Attribute("User's permission level")
@implementer(IUserSession)
class UserSession(object):
def __init__(self, session):
@ -26,10 +28,11 @@ class UserSession(object):
self.current_ip = "0.0.0.0"
self.permissions = 0
class FrontendServlet(resource.Resource):
def getChild(self, name: bytes, request: Request):
self.logger.debug(f"{request.getClientIP()} -> {name.decode()}")
if name == b'':
if name == b"":
return self
return resource.Resource.getChild(self, name, request)
@ -42,17 +45,23 @@ class FrontendServlet(resource.Resource):
self.game_list: List[Dict[str, str]] = []
self.children: Dict[str, Any] = {}
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "frontend"), when="d", backupCount=10)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.config.server.log_dir, "frontend"),
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler)
self.logger.setLevel(cfg.frontend.loglevel)
coloredlogs.install(level=cfg.frontend.loglevel, logger=self.logger, fmt=log_fmt_str)
coloredlogs.install(
level=cfg.frontend.loglevel, logger=self.logger, fmt=log_fmt_str
)
registerAdapter(UserSession, Session, IUserSession)
fe_game = FE_Game(cfg, self.environment)
@ -65,18 +74,26 @@ class FrontendServlet(resource.Resource):
fe_game.putChild(game_dir.encode(), game_fe)
except:
raise
self.environment.globals["game_list"] = self.game_list
self.putChild(b"gate", FE_Gate(cfg, self.environment))
self.putChild(b"user", FE_User(cfg, self.environment))
self.putChild(b"game", fe_game)
self.logger.info(f"Ready on port {self.config.frontend.port} serving {len(fe_game.children)} games")
self.logger.info(
f"Ready on port {self.config.frontend.port} serving {len(fe_game.children)} games"
)
def render_GET(self, request):
self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}")
template = self.environment.get_template("core/frontend/index.jinja")
return template.render(server_name=self.config.server.name, title=self.config.server.name, game_list=self.game_list, sesh=vars(IUserSession(request.getSession()))).encode("utf-16")
return template.render(
server_name=self.config.server.name,
title=self.config.server.name,
game_list=self.game_list,
sesh=vars(IUserSession(request.getSession())),
).encode("utf-16")
class FE_Base(resource.Resource):
"""
@ -84,43 +101,51 @@ class FE_Base(resource.Resource):
Initializes the environment, data, logger, config, and sets isLeaf to true
It is expected that game implementations of this class overwrite many of these
"""
isLeaf = True
isLeaf = True
def __init__(self, cfg: CoreConfig, environment: jinja2.Environment) -> None:
self.core_config = cfg
self.data = Data(cfg)
self.logger = logging.getLogger('frontend')
self.logger = logging.getLogger("frontend")
self.environment = environment
self.nav_name = "nav_name"
class FE_Gate(FE_Base):
def render_GET(self, request: Request):
def render_GET(self, request: Request):
self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}")
uri: str = request.uri.decode()
sesh = request.getSession()
usr_sesh = IUserSession(sesh)
if usr_sesh.userId > 0:
return redirectTo(b"/user", request)
if uri.startswith("/gate/create"):
return self.create_user(request)
if b'e' in request.args:
if b"e" in request.args:
try:
err = int(request.args[b'e'][0].decode())
err = int(request.args[b"e"][0].decode())
except:
err = 0
else: err = 0
else:
err = 0
template = self.environment.get_template("core/frontend/gate/gate.jinja")
return template.render(
title=f"{self.core_config.server.name} | Login Gate",
error=err,
sesh=vars(usr_sesh),
).encode("utf-16")
template = self.environment.get_template("core/frontend/gate/gate.jinja")
return template.render(title=f"{self.core_config.server.name} | Login Gate", error=err, sesh=vars(usr_sesh)).encode("utf-16")
def render_POST(self, request: Request):
uri = request.uri.decode()
ip = request.getClientAddress().host
if uri == "/gate/gate.login":
if uri == "/gate/gate.login":
access_code: str = request.args[b"access_code"][0].decode()
passwd: bytes = request.args[b"passwd"][0]
if passwd == b"":
@ -129,26 +154,28 @@ class FE_Gate(FE_Base):
uid = self.data.card.get_user_id_from_card(access_code)
if uid is None:
return redirectTo(b"/gate?e=1", request)
if passwd is None:
sesh = self.data.user.check_password(uid)
if sesh is not None:
return redirectTo(f"/gate/create?ac={access_code}".encode(), request)
return redirectTo(
f"/gate/create?ac={access_code}".encode(), request
)
return redirectTo(b"/gate?e=1", request)
if not self.data.user.check_password(uid, passwd):
return redirectTo(b"/gate?e=1", request)
self.logger.info(f"Successful login of user {uid} at {ip}")
sesh = request.getSession()
usr_sesh = IUserSession(sesh)
usr_sesh.userId = uid
usr_sesh.current_ip = ip
return redirectTo(b"/user", request)
elif uri == "/gate/gate.create":
access_code: str = request.args[b"access_code"][0].decode()
username: str = request.args[b"username"][0]
@ -162,26 +189,33 @@ class FE_Gate(FE_Base):
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(passwd, salt)
result = self.data.user.create_user(uid, username, email, hashed.decode(), 1)
result = self.data.user.create_user(
uid, username, email, hashed.decode(), 1
)
if result is None:
return redirectTo(b"/gate?e=3", request)
if not self.data.user.check_password(uid, passwd.encode()):
return redirectTo(b"/gate", request)
return redirectTo(b"/user", request)
else:
return b""
def create_user(self, request: Request):
if b'ac' not in request.args or len(request.args[b'ac'][0].decode()) != 20:
if b"ac" not in request.args or len(request.args[b"ac"][0].decode()) != 20:
return redirectTo(b"/gate?e=2", request)
ac = request.args[b'ac'][0].decode()
template = self.environment.get_template("core/frontend/gate/create.jinja")
return template.render(title=f"{self.core_config.server.name} | Create User", code=ac, sesh={"userId": 0}).encode("utf-16")
ac = request.args[b"ac"][0].decode()
template = self.environment.get_template("core/frontend/gate/create.jinja")
return template.render(
title=f"{self.core_config.server.name} | Create User",
code=ac,
sesh={"userId": 0},
).encode("utf-16")
class FE_User(FE_Base):
def render_GET(self, request: Request):
@ -191,17 +225,20 @@ class FE_User(FE_Base):
usr_sesh = IUserSession(sesh)
if usr_sesh.userId == 0:
return redirectTo(b"/gate", request)
return template.render(title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh)).encode("utf-16")
return template.render(
title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh)
).encode("utf-16")
class FE_Game(FE_Base):
isLeaf = False
children: Dict[str, Any] = {}
def getChild(self, name: bytes, request: Request):
if name == b'':
if name == b"":
return self
return resource.Resource.getChild(self, name, request)
def render_GET(self, request: Request) -> bytes:
return redirectTo(b"/user", request)
return redirectTo(b"/user", request)

View File

@ -9,25 +9,30 @@ import pytz
from core.config import CoreConfig
from core.utils import Utils
class MuchaServlet:
def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None:
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')
self.logger = logging.getLogger("mucha")
log_fmt_str = "[%(asctime)s] Mucha | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "mucha"), when="d", backupCount=10)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.config.server.log_dir, "mucha"),
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler)
self.logger.setLevel(logging.INFO)
coloredlogs.install(level=logging.INFO, logger=self.logger, fmt=log_fmt_str)
@ -35,43 +40,57 @@ class MuchaServlet:
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(self.config, self.config_dir)
enabled, game_cd = mod.index.get_mucha_info(
self.config, self.config_dir
)
if enabled:
self.mucha_registry.append(game_cd)
self.logger.info(f"Serving {len(self.mucha_registry)} games on port {self.config.mucha.port}")
self.logger.info(
f"Serving {len(self.mucha_registry)} games on port {self.config.mucha.port}"
)
def handle_boardauth(self, request: Request, _: Dict) -> bytes:
req_dict = self.mucha_preprocess(request.content.getvalue())
if req_dict is None:
self.logger.error(f"Error processing mucha request {request.content.getvalue()}")
self.logger.error(
f"Error processing mucha request {request.content.getvalue()}"
)
return b""
req = MuchaAuthRequest(req_dict)
self.logger.debug(f"Mucha request {vars(req)}")
self.logger.info(f"Boardauth request from {request.getClientAddress().host} for {req.gameVer}")
self.logger.info(
f"Boardauth request from {request.getClientAddress().host} for {req.gameVer}"
)
if req.gameCd not in self.mucha_registry:
self.logger.warn(f"Unknown gameCd {req.gameCd}")
return b""
# TODO: Decrypt S/N
resp = MuchaAuthResponse(f"{self.config.mucha.hostname}{':' + self.config.mucha.port if self.config.server.is_develop else ''}")
resp = MuchaAuthResponse(
f"{self.config.mucha.hostname}{':' + str(self.config.mucha.port) if self.config.server.is_develop else ''}"
)
self.logger.debug(f"Mucha response {vars(resp)}")
return self.mucha_postprocess(vars(resp))
def handle_updatecheck(self, request: Request, _: Dict) -> bytes:
req_dict = self.mucha_preprocess(request.content.getvalue())
if req_dict is None:
self.logger.error(f"Error processing mucha request {request.content.getvalue()}")
self.logger.error(
f"Error processing mucha request {request.content.getvalue()}"
)
return b""
req = MuchaUpdateRequest(req_dict)
self.logger.debug(f"Mucha request {vars(req)}")
self.logger.info(f"Updatecheck request from {request.getClientAddress().host} for {req.gameVer}")
self.logger.info(
f"Updatecheck request from {request.getClientAddress().host} for {req.gameVer}"
)
if req.gameCd not in self.mucha_registry:
self.logger.warn(f"Unknown gameCd {req.gameCd}")
@ -86,14 +105,14 @@ class MuchaServlet:
def mucha_preprocess(self, data: bytes) -> Optional[Dict]:
try:
ret: Dict[str, Any] = {}
for x in data.decode().split('&'):
kvp = x.split('=')
for x in data.decode().split("&"):
kvp = x.split("=")
if len(kvp) == 2:
ret[kvp[0]] = kvp[1]
return ret
except:
self.logger.error(f"Error processing mucha request {data}")
return None
@ -101,7 +120,7 @@ class MuchaServlet:
def mucha_postprocess(self, data: dict) -> Optional[bytes]:
try:
urlencode = ""
for k,v in data.items():
for k, v in data.items():
urlencode += f"{k}={v}&"
return urlencode.encode()
@ -110,36 +129,39 @@ class MuchaServlet:
self.logger.error("Error processing mucha response")
return None
class MuchaAuthRequest():
def __init__(self, request: Dict) -> None:
self.gameVer = "" if "gameVer" not in request else request["gameVer"] # gameCd + boardType + countryCd + version
self.sendDate = "" if "sendDate" not in request else request["sendDate"] # %Y%m%d
self.serialNum = "" if "serialNum" not in request else request["serialNum"]
self.gameCd = "" if "gameCd" not in request else request["gameCd"]
self.boardType = "" if "boardType" not in request else request["boardType"]
self.boardId = "" if "boardId" not in request else request["boardId"]
self.mac = "" if "mac" not in request else request["mac"]
self.placeId = "" if "placeId" not in request else request["placeId"]
self.storeRouterIp = "" if "storeRouterIp" not in request else request["storeRouterIp"]
self.countryCd = "" if "countryCd" not in request else request["countryCd"]
self.useToken = "" if "useToken" not in request else request["useToken"]
self.allToken = "" if "allToken" not in request else request["allToken"]
class MuchaAuthResponse():
class MuchaAuthRequest:
def __init__(self, request: Dict) -> None:
# gameCd + boardType + countryCd + version
self.gameVer = request.get("gameVer", "")
self.sendDate = request.get("sendDate", "") # %Y%m%d
self.serialNum = request.get("serialNum", "")
self.gameCd = request.get("gameCd", "")
self.boardType = request.get("boardType", "")
self.boardId = request.get("boardId", "")
self.mac = request.get("mac", "")
self.placeId = request.get("placeId", "")
self.storeRouterIp = request.get("storeRouterIp", "")
self.countryCd = request.get("countryCd", "")
self.useToken = request.get("useToken", "")
self.allToken = request.get("allToken", "")
class MuchaAuthResponse:
def __init__(self, mucha_url: str) -> None:
self.RESULTS = "001"
self.RESULTS = "001"
self.AUTH_INTERVAL = "86400"
self.SERVER_TIME = datetime.strftime(datetime.now(), "%Y%m%d%H%M")
self.UTC_SERVER_TIME = datetime.strftime(datetime.now(pytz.UTC), "%Y%m%d%H%M")
self.CHARGE_URL = f"https://{mucha_url}/charge/"
self.CHARGE_URL = f"https://{mucha_url}/charge/"
self.FILE_URL = f"https://{mucha_url}/file/"
self.URL_1 = f"https://{mucha_url}/url1/"
self.URL_2 = f"https://{mucha_url}/url2/"
self.URL_3 = f"https://{mucha_url}/url3/"
self.PLACE_ID = "JPN123"
self.COUNTRY_CD = "JPN"
self.PLACE_ID = "JPN123"
self.COUNTRY_CD = "JPN"
self.SHOP_NAME = "TestShop!"
self.SHOP_NICKNAME = "TestShop"
self.AREA_0 = "008"
@ -150,7 +172,7 @@ class MuchaAuthResponse():
self.AREA_FULL_1 = ""
self.AREA_FULL_2 = ""
self.AREA_FULL_3 = ""
self.SHOP_NAME_EN = "TestShop!"
self.SHOP_NICKNAME_EN = "TestShop"
self.AREA_0_EN = "008"
@ -162,23 +184,25 @@ class MuchaAuthResponse():
self.AREA_FULL_2_EN = ""
self.AREA_FULL_3_EN = ""
self.PREFECTURE_ID = "1"
self.PREFECTURE_ID = "1"
self.EXPIRATION_DATE = "null"
self.USE_TOKEN = "0"
self.CONSUME_TOKEN = "0"
self.DONGLE_FLG = "1"
self.FORCE_BOOT = "0"
class MuchaUpdateRequest():
def __init__(self, request: Dict) -> None:
self.gameVer = "" if "gameVer" not in request else request["gameVer"]
self.gameCd = "" if "gameCd" not in request else request["gameCd"]
self.serialNum = "" if "serialNum" not in request else request["serialNum"]
self.countryCd = "" if "countryCd" not in request else request["countryCd"]
self.placeId = "" if "placeId" not in request else request["placeId"]
self.storeRouterIp = "" if "storeRouterIp" not in request else request["storeRouterIp"]
class MuchaUpdateResponse():
class MuchaUpdateRequest:
def __init__(self, request: Dict) -> None:
self.gameVer = request.get("gameVer", "")
self.gameCd = request.get("gameCd", "")
self.serialNum = request.get("serialNum", "")
self.countryCd = request.get("countryCd", "")
self.placeId = request.get("placeId", "")
self.storeRouterIp = request.get("storeRouterIp", "")
class MuchaUpdateResponse:
def __init__(self, game_ver: str, mucha_url: str) -> None:
self.RESULTS = "001"
self.UPDATE_VER_1 = game_ver
@ -194,7 +218,8 @@ class MuchaUpdateResponse():
self.USER_ID = ""
self.PASSWORD = ""
class MuchaUpdateResponseStub():
class MuchaUpdateResponseStub:
def __init__(self, game_ver: str) -> None:
self.RESULTS = "001"
self.UPDATE_VER_1 = game_ver

View File

@ -7,8 +7,9 @@ from core.config import CoreConfig
from core.data import Data
from core.utils import Utils
class TitleServlet():
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
class TitleServlet:
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
super().__init__()
self.config = core_cfg
self.config_folder = cfg_folder
@ -18,47 +19,57 @@ class TitleServlet():
self.logger = logging.getLogger("title")
if not hasattr(self.logger, "initialized"):
log_fmt_str = "[%(asctime)s] Title | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.config.server.log_dir, "title"), when="d", backupCount=10)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.config.server.log_dir, "title"),
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler)
self.logger.setLevel(core_cfg.title.loglevel)
coloredlogs.install(level=core_cfg.title.loglevel, logger=self.logger, fmt=log_fmt_str)
coloredlogs.install(
level=core_cfg.title.loglevel, logger=self.logger, fmt=log_fmt_str
)
self.logger.initialized = True
plugins = Utils.get_all_titles()
for folder, mod in plugins.items():
if hasattr(mod, "game_codes") and hasattr(mod, "index"):
should_call_setup = True
if hasattr(mod.index, "get_allnet_info"):
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)
enabled, _, _ = mod.index.get_allnet_info(
code, self.config, self.config_folder
)
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.warn(f"Game {folder} has no get_allnet_info")
else:
self.logger.error(f"{folder} missing game_code or index in __init__.py")
self.logger.info(f"Serving {len(self.title_registry)} game codes on port {core_cfg.title.port}")
self.logger.info(
f"Serving {len(self.title_registry)} game codes on port {core_cfg.title.port}"
)
def render_GET(self, request: Request, endpoints: dict) -> bytes:
code = endpoints["game"]
@ -66,7 +77,7 @@ class TitleServlet():
self.logger.warn(f"Unknown game code {code}")
request.setResponseCode(404)
return b""
index = self.title_registry[code]
if not hasattr(index, "render_GET"):
self.logger.warn(f"{code} does not dispatch GET")
@ -74,18 +85,20 @@ class TitleServlet():
return b""
return index.render_GET(request, endpoints["version"], endpoints["endpoint"])
def render_POST(self, request: Request, endpoints: dict) -> bytes:
code = endpoints["game"]
if code not in self.title_registry:
self.logger.warn(f"Unknown game code {code}")
request.setResponseCode(404)
return b""
index = self.title_registry[code]
if not hasattr(index, "render_POST"):
self.logger.warn(f"{code} does not dispatch POST")
request.setResponseCode(405)
return b""
return index.render_POST(request, int(endpoints["version"]), endpoints["endpoint"])
return index.render_POST(
request, int(endpoints["version"]), endpoints["endpoint"]
)

View File

@ -4,13 +4,14 @@ import logging
import importlib
from os import walk
class Utils:
@classmethod
def get_all_titles(cls) -> Dict[str, ModuleType]:
ret: Dict[str, Any] = {}
for root, dirs, files in walk("titles"):
for dir in dirs:
for dir in dirs:
if not dir.startswith("__"):
try:
mod = importlib.import_module(f"titles.{dir}")

View File

@ -4,16 +4,30 @@ from core.config import CoreConfig
from core.data import Data
from os import path
if __name__=='__main__':
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Database utilities")
parser.add_argument("--config", "-c", type=str, help="Config folder to use", default="config")
parser.add_argument("--version", "-v", type=str, help="Version of the database to upgrade/rollback to")
parser.add_argument("--game", "-g", type=str, help="Game code of the game who's schema will be updated/rolled back. Ex. SDFE")
parser.add_argument(
"--config", "-c", type=str, help="Config folder to use", default="config"
)
parser.add_argument(
"--version",
"-v",
type=str,
help="Version of the database to upgrade/rollback to",
)
parser.add_argument(
"--game",
"-g",
type=str,
help="Game code of the game who's schema will be updated/rolled back. Ex. SDFE",
)
parser.add_argument("--email", "-e", type=str, help="Email for the new user")
parser.add_argument("--old_ac", "-o", type=str, help="Access code to transfer from")
parser.add_argument("--new_ac", "-n", type=str, help="Access code to transfer to")
parser.add_argument("--force", "-f", type=bool, help="Force the action to happen")
parser.add_argument("action", type=str, help="DB Action, create, recreate, upgrade, or rollback")
parser.add_argument(
"action", type=str, help="DB Action, create, recreate, upgrade, or rollback"
)
args = parser.parse_args()
cfg = CoreConfig()
@ -23,7 +37,7 @@ if __name__=='__main__':
if args.action == "create":
data.create_database()
elif args.action == "recreate":
data.recreate_database()

178
index.py
View File

@ -12,6 +12,7 @@ from twisted.internet import reactor, endpoints
from twisted.web.http import Request
from routes import Mapper
class HttpDispatcher(resource.Resource):
def __init__(self, cfg: CoreConfig, config_dir: str):
super().__init__()
@ -20,131 +21,218 @@ class HttpDispatcher(resource.Resource):
self.map_get = Mapper()
self.map_post = Mapper()
self.logger = logging.getLogger("core")
self.allnet = AllnetServlet(cfg, config_dir)
self.title = TitleServlet(cfg, config_dir)
self.mucha = MuchaServlet(cfg, config_dir)
self.map_post.connect('allnet_ping', '/naomitest.html', controller="allnet", action='handle_naomitest', conditions=dict(method=['GET']))
self.map_post.connect('allnet_poweron', '/sys/servlet/PowerOn', controller="allnet", action='handle_poweron', conditions=dict(method=['POST']))
self.map_post.connect('allnet_downloadorder', '/sys/servlet/DownloadOrder', controller="allnet", action='handle_dlorder', conditions=dict(method=['POST']))
self.map_post.connect('allnet_billing', '/request', controller="allnet", action='handle_billing_request', conditions=dict(method=['POST']))
self.map_post.connect('allnet_billing', '/request/', controller="allnet", action='handle_billing_request', conditions=dict(method=['POST']))
self.map_post.connect(
"allnet_ping",
"/naomitest.html",
controller="allnet",
action="handle_naomitest",
conditions=dict(method=["GET"]),
)
self.map_post.connect(
"allnet_poweron",
"/sys/servlet/PowerOn",
controller="allnet",
action="handle_poweron",
conditions=dict(method=["POST"]),
)
self.map_post.connect(
"allnet_downloadorder",
"/sys/servlet/DownloadOrder",
controller="allnet",
action="handle_dlorder",
conditions=dict(method=["POST"]),
)
self.map_post.connect(
"allnet_billing",
"/request",
controller="allnet",
action="handle_billing_request",
conditions=dict(method=["POST"]),
)
self.map_post.connect(
"allnet_billing",
"/request/",
controller="allnet",
action="handle_billing_request",
conditions=dict(method=["POST"]),
)
self.map_post.connect('mucha_boardauth', '/mucha/boardauth.do', controller="mucha", action='handle_boardauth', conditions=dict(method=['POST']))
self.map_post.connect('mucha_updatacheck', '/mucha/updatacheck.do', controller="mucha", action='handle_updatacheck', conditions=dict(method=['POST']))
self.map_post.connect(
"mucha_boardauth",
"/mucha/boardauth.do",
controller="mucha",
action="handle_boardauth",
conditions=dict(method=["POST"]),
)
self.map_post.connect(
"mucha_updatacheck",
"/mucha/updatacheck.do",
controller="mucha",
action="handle_updatecheck",
conditions=dict(method=["POST"]),
)
self.map_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..."))
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..."),
)
def render_GET(self, request: Request) -> bytes:
def render_GET(self, request: Request) -> bytes:
test = self.map_get.match(request.uri.decode())
if test is None:
self.logger.debug(f"Unknown GET endpoint {request.uri.decode()} from {request.getClientAddress().host} to port {request.getHost().port}")
self.logger.debug(
f"Unknown GET endpoint {request.uri.decode()} from {request.getClientAddress().host} to port {request.getHost().port}"
)
request.setResponseCode(404)
return b"Endpoint not found."
return self.dispatch(test, request)
def render_POST(self, request: Request) -> bytes:
def render_POST(self, request: Request) -> bytes:
test = self.map_post.match(request.uri.decode())
if test is None:
self.logger.debug(f"Unknown POST endpoint {request.uri.decode()} from {request.getClientAddress().host} to port {request.getHost().port}")
self.logger.debug(
f"Unknown POST endpoint {request.uri.decode()} from {request.getClientAddress().host} to port {request.getHost().port}"
)
request.setResponseCode(404)
return b"Endpoint not found."
return self.dispatch(test, request)
def dispatch(self, matcher: Dict, request: Request) -> bytes:
controller = getattr(self, matcher["controller"], None)
if controller is None:
self.logger.error(f"Controller {matcher['controller']} not found via endpoint {request.uri.decode()}")
self.logger.error(
f"Controller {matcher['controller']} not found via endpoint {request.uri.decode()}"
)
request.setResponseCode(404)
return b"Endpoint not found."
handler = getattr(controller, matcher["action"], None)
if handler is None:
self.logger.error(f"Action {matcher['action']} not found in controller {matcher['controller']} via endpoint {request.uri.decode()}")
self.logger.error(
f"Action {matcher['action']} not found in controller {matcher['controller']} via endpoint {request.uri.decode()}"
)
request.setResponseCode(404)
return b"Endpoint not found."
url_vars = matcher
url_vars.pop("controller")
url_vars.pop("action")
ret = handler(request, url_vars)
if type(ret) == str:
return ret.encode()
elif type(ret) == bytes:
return ret
else:
return b""
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="ARTEMiS main entry point")
parser.add_argument("--config", "-c", type=str, default="config", help="Configuration folder")
parser.add_argument(
"--config", "-c", type=str, default="config", help="Configuration folder"
)
args = parser.parse_args()
if not path.exists(f"{args.config}/core.yaml"):
print(f"The config folder you specified ({args.config}) does not exist or does not contain core.yaml.\nDid you copy the example folder?")
print(
f"The config folder you specified ({args.config}) does not exist or does not contain core.yaml.\nDid you copy the example folder?"
)
exit(1)
cfg: CoreConfig = CoreConfig()
if path.exists(f"{args.config}/core.yaml"):
cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
if not path.exists(cfg.server.log_dir):
mkdir(cfg.server.log_dir)
logger = logging.getLogger("core")
log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(cfg.server.log_dir, "core"), when="d", backupCount=10
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
logger.addHandler(fileHandler)
logger.addHandler(consoleHandler)
log_lv = logging.DEBUG if cfg.server.is_develop else logging.INFO
logger.setLevel(log_lv)
coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str)
if not path.exists(cfg.server.log_dir):
mkdir(cfg.server.log_dir)
if not access(cfg.server.log_dir, W_OK):
logger.error(f"Log directory {cfg.server.log_dir} NOT writable, please check permissions")
logger.error(
f"Log directory {cfg.server.log_dir} NOT writable, please check permissions"
)
exit(1)
if not cfg.aimedb.key:
logger.error("!!AIMEDB KEY BLANK, SET KEY IN CORE.YAML!!")
exit(1)
logger.info(f"ARTEMiS starting in {'develop' if cfg.server.is_develop else 'production'} mode")
allnet_server_str = f"tcp:{cfg.allnet.port}:interface={cfg.server.listen_address}"
logger.info(
f"ARTEMiS starting in {'develop' if cfg.server.is_develop else 'production'} mode"
)
allnet_server_str = f"tcp:{cfg.allnet.port}:interface={cfg.server.listen_address}"
title_server_str = f"tcp:{cfg.title.port}:interface={cfg.server.listen_address}"
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}"
frontend_server_str = (
f"tcp:{cfg.frontend.port}:interface={cfg.server.listen_address}"
)
billing_server_str = f"tcp:{cfg.billing.port}:interface={cfg.server.listen_address}"
if cfg.server.is_develop:
billing_server_str = f"ssl:{cfg.billing.port}:interface={cfg.server.listen_address}"\
billing_server_str = (
f"ssl:{cfg.billing.port}:interface={cfg.server.listen_address}"
f":privateKey={cfg.billing.ssl_key}:certKey={cfg.billing.ssl_cert}"
)
dispatcher = HttpDispatcher(cfg, args.config)
endpoints.serverFromString(reactor, allnet_server_str).listen(server.Site(dispatcher))
endpoints.serverFromString(reactor, allnet_server_str).listen(
server.Site(dispatcher)
)
endpoints.serverFromString(reactor, adb_server_str).listen(AimedbFactory(cfg))
if cfg.frontend.enable:
endpoints.serverFromString(reactor, frontend_server_str).listen(server.Site(FrontendServlet(cfg, args.config)))
endpoints.serverFromString(reactor, frontend_server_str).listen(
server.Site(FrontendServlet(cfg, args.config))
)
if cfg.billing.port > 0:
endpoints.serverFromString(reactor, billing_server_str).listen(server.Site(dispatcher))
if cfg.title.port > 0:
endpoints.serverFromString(reactor, title_server_str).listen(server.Site(dispatcher))
reactor.run() # type: ignore
endpoints.serverFromString(reactor, billing_server_str).listen(
server.Site(dispatcher)
)
if cfg.title.port > 0:
endpoints.serverFromString(reactor, title_server_str).listen(
server.Site(dispatcher)
)
reactor.run() # type: ignore

72
read.py
View File

@ -12,56 +12,64 @@ from typing import List, Optional
from core import CoreConfig
from core.utils import Utils
class BaseReader():
def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None:
class BaseReader:
def __init__(
self,
config: CoreConfig,
version: int,
bin_dir: Optional[str],
opt_dir: Optional[str],
extra: Optional[str],
) -> None:
self.logger = logging.getLogger("reader")
self.config = config
self.bin_dir = bin_dir
self.opt_dir = opt_dir
self.version = version
self.extra = extra
def get_data_directories(self, directory: str) -> List[str]:
ret: List[str] = []
for root, dirs, files in os.walk(directory):
for dir in dirs:
if re.fullmatch("[A-Z0-9]{4,4}", dir) is not None:
ret.append(f"{root}/{dir}")
for dir in dirs:
if re.fullmatch("[A-Z0-9]{4,4}", dir) is not None:
ret.append(f"{root}/{dir}")
return ret
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Import Game Information')
parser = argparse.ArgumentParser(description="Import Game Information")
parser.add_argument(
'--series',
action='store',
"--series",
action="store",
type=str,
required=True,
help='The game series we are importing.',
help="The game series we are importing.",
)
parser.add_argument(
'--version',
dest='version',
action='store',
"--version",
dest="version",
action="store",
type=int,
required=True,
help='The game version we are importing.',
help="The game version we are importing.",
)
parser.add_argument(
'--binfolder',
dest='bin',
action='store',
"--binfolder",
dest="bin",
action="store",
type=str,
help='Folder containing A000 base data',
help="Folder containing A000 base data",
)
parser.add_argument(
'--optfolder',
dest='opt',
action='store',
"--optfolder",
dest="opt",
action="store",
type=str,
help='Folder containing Option data folders',
help="Folder containing Option data folders",
)
parser.add_argument(
"--config",
@ -86,15 +94,17 @@ if __name__ == "__main__":
log_fmt = logging.Formatter(log_fmt_str)
logger = logging.getLogger("reader")
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(config.server.log_dir, "reader"), when="d", backupCount=10)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(config.server.log_dir, "reader"), when="d", backupCount=10
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
logger.addHandler(fileHandler)
logger.addHandler(consoleHandler)
log_lv = logging.DEBUG if config.server.is_develop else logging.INFO
logger.setLevel(log_lv)
coloredlogs.install(level=log_lv, logger=logger, fmt=log_fmt_str)
@ -102,8 +112,8 @@ if __name__ == "__main__":
if args.series is None or args.version is None:
logger.error("Game or version not specified")
parser.print_help()
exit(1)
exit(1)
if args.bin is None and args.opt is None:
logger.error("Must specify either bin or opt directory")
parser.print_help()
@ -113,7 +123,7 @@ if __name__ == "__main__":
bin_arg = args.bin[:-1]
else:
bin_arg = args.bin
if args.opt is not None and (args.opt.endswith("\\") or args.opt.endswith("/")):
opt_arg = args.opt[:-1]
else:
@ -127,5 +137,5 @@ if __name__ == "__main__":
if args.series in mod.game_codes:
handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra)
handler.read()
logger.info("Done")

View File

@ -5,12 +5,13 @@ from titles.chuni.base import ChuniBase
from titles.chuni.const import ChuniConstants
from titles.chuni.config import ChuniConfig
class ChuniAir(ChuniBase):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_AIR
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)
ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["dataVersion"] = "1.10.00"
return ret
return ret

View File

@ -5,12 +5,13 @@ from titles.chuni.base import ChuniBase
from titles.chuni.const import ChuniConstants
from titles.chuni.config import ChuniConfig
class ChuniAirPlus(ChuniBase):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_AIR_PLUS
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)
ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["dataVersion"] = "1.15.00"
return ret
return ret

View File

@ -7,12 +7,13 @@ from titles.chuni.base import ChuniBase
from titles.chuni.const import ChuniConstants
from titles.chuni.config import ChuniConfig
class ChuniAmazon(ChuniBase):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_AMAZON
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)
ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["dataVersion"] = "1.30.00"
return ret

View File

@ -7,12 +7,13 @@ from titles.chuni.base import ChuniBase
from titles.chuni.const import ChuniConstants
from titles.chuni.config import ChuniConfig
class ChuniAmazonPlus(ChuniBase):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)
ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["dataVersion"] = "1.35.00"
return ret

View File

@ -11,7 +11,8 @@ from titles.chuni.const import ChuniConstants
from titles.chuni.database import ChuniData
from titles.chuni.config import ChuniConfig
class ChuniBase():
class ChuniBase:
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
self.core_cfg = core_cfg
self.game_cfg = game_cfg
@ -21,36 +22,32 @@ class ChuniBase():
self.game = ChuniConstants.GAME_CODE
self.version = ChuniConstants.VER_CHUNITHM
def handle_ping_request(self, data: Dict) -> Dict:
def handle_game_login_api_request(self, data: Dict) -> Dict:
# self.data.base.log_event("chuni", "login", logging.INFO, {"version": self.version, "user": data["userId"]})
return {"returnCode": 1}
def handle_game_login_api_request(self, data: Dict) -> Dict:
#self.data.base.log_event("chuni", "login", logging.INFO, {"version": self.version, "user": data["userId"]})
return { "returnCode": 1 }
def handle_game_logout_api_request(self, data: Dict) -> Dict:
#self.data.base.log_event("chuni", "logout", logging.INFO, {"version": self.version, "user": data["userId"]})
return { "returnCode": 1 }
# self.data.base.log_event("chuni", "logout", logging.INFO, {"version": self.version, "user": data["userId"]})
return {"returnCode": 1}
def handle_get_game_charge_api_request(self, data: Dict) -> Dict:
game_charge_list = self.data.static.get_enabled_charges(self.version)
charges = []
for x in range(len(game_charge_list)):
charges.append({
"orderId": x,
"chargeId": game_charge_list[x]["chargeId"],
"price": 1,
"startDate": "2017-12-05 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
"salePrice": 1,
"saleStartDate": "2017-12-05 07:00:00.0",
"saleEndDate": "2099-12-31 00:00:00.0"
})
return {
"length": len(charges),
"gameChargeList": charges
}
for x in range(len(game_charge_list)):
charges.append(
{
"orderId": x,
"chargeId": game_charge_list[x]["chargeId"],
"price": 1,
"startDate": "2017-12-05 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
"salePrice": 1,
"saleStartDate": "2017-12-05 07:00:00.0",
"saleEndDate": "2099-12-31 00:00:00.0",
}
)
return {"length": len(charges), "gameChargeList": charges}
def handle_get_game_event_api_request(self, data: Dict) -> Dict:
game_events = self.data.static.get_enabled_events(self.version)
@ -65,26 +62,30 @@ class ChuniBase():
event_list.append(tmp)
return {
"type": data["type"],
"length": len(event_list),
"gameEventList": event_list
"type": data["type"],
"length": len(event_list),
"gameEventList": event_list,
}
def handle_get_game_idlist_api_request(self, data: Dict) -> Dict:
return { "type": data["type"], "length": 0, "gameIdlistList": [] }
return {"type": data["type"], "length": 0, "gameIdlistList": []}
def handle_get_game_message_api_request(self, data: Dict) -> Dict:
return { "type": data["type"], "length": "0", "gameMessageList": [] }
return {"type": data["type"], "length": "0", "gameMessageList": []}
def handle_get_game_ranking_api_request(self, data: Dict) -> Dict:
return { "type": data["type"], "gameRankingList": [] }
return {"type": data["type"], "gameRankingList": []}
def handle_get_game_sale_api_request(self, data: Dict) -> Dict:
return { "type": data["type"], "length": 0, "gameSaleList": [] }
return {"type": data["type"], "length": 0, "gameSaleList": []}
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
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)
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
)
return {
"gameSetting": {
"dataVersion": "1.00.00",
@ -97,15 +98,17 @@ class ChuniBase():
"maxCountItem": 300,
"maxCountMusic": 300,
},
"isDumpUpload": "false",
"isAou": "false",
"isDumpUpload": "false",
"isAou": "false",
}
def handle_get_user_activity_api_request(self, data: Dict) -> Dict:
user_activity_list = self.data.profile.get_profile_activity(data["userId"], data["kind"])
user_activity_list = self.data.profile.get_profile_activity(
data["userId"], data["kind"]
)
activity_list = []
for activity in user_activity_list:
tmp = activity._asdict()
tmp.pop("user")
@ -114,15 +117,16 @@ class ChuniBase():
activity_list.append(tmp)
return {
"userId": data["userId"],
"length": len(activity_list),
"kind": data["kind"],
"userActivityList": activity_list
"userId": data["userId"],
"length": len(activity_list),
"kind": data["kind"],
"userActivityList": activity_list,
}
def handle_get_user_character_api_request(self, data: Dict) -> Dict:
characters = self.data.item.get_characters(data["userId"])
if characters is None: return {}
if characters is None:
return {}
next_idx = -1
characterList = []
@ -134,15 +138,17 @@ class ChuniBase():
if len(characterList) >= int(data["maxCount"]):
break
if len(characterList) >= int(data["maxCount"]) and len(characters) > int(data["maxCount"]) + int(data["nextIndex"]):
if len(characterList) >= int(data["maxCount"]) and len(characters) > int(
data["maxCount"]
) + int(data["nextIndex"]):
next_idx = int(data["maxCount"]) + int(data["nextIndex"]) + 1
return {
"userId": data["userId"],
"userId": data["userId"],
"length": len(characterList),
"nextIndex": next_idx,
"userCharacterList": characterList
"nextIndex": next_idx,
"userCharacterList": characterList,
}
def handle_get_user_charge_api_request(self, data: Dict) -> Dict:
@ -156,21 +162,21 @@ class ChuniBase():
charge_list.append(tmp)
return {
"userId": data["userId"],
"userId": data["userId"],
"length": len(charge_list),
"userChargeList": charge_list
"userChargeList": charge_list,
}
def handle_get_user_course_api_request(self, data: Dict) -> Dict:
user_course_list = self.data.score.get_courses(data["userId"])
if user_course_list is None:
if user_course_list is None:
return {
"userId": data["userId"],
"userId": data["userId"],
"length": 0,
"nextIndex": -1,
"userCourseList": []
"nextIndex": -1,
"userCourseList": [],
}
course_list = []
next_idx = int(data["nextIndex"])
max_ct = int(data["maxCount"])
@ -183,51 +189,48 @@ class ChuniBase():
if len(user_course_list) >= max_ct:
break
if len(user_course_list) >= max_ct:
next_idx = next_idx + max_ct
else:
next_idx = -1
return {
"userId": data["userId"],
"userId": data["userId"],
"length": len(course_list),
"nextIndex": next_idx,
"userCourseList": course_list
"nextIndex": next_idx,
"userCourseList": course_list,
}
def handle_get_user_data_api_request(self, data: Dict) -> Dict:
p = self.data.profile.get_profile_data(data["userId"], self.version)
if p is None: return {}
if p is None:
return {}
profile = p._asdict()
profile.pop("id")
profile.pop("user")
profile.pop("version")
return {
"userId": data["userId"],
"userData": profile
}
return {"userId": data["userId"], "userData": profile}
def handle_get_user_data_ex_api_request(self, data: Dict) -> Dict:
p = self.data.profile.get_profile_data_ex(data["userId"], self.version)
if p is None: return {}
if p is None:
return {}
profile = p._asdict()
profile.pop("id")
profile.pop("user")
profile.pop("version")
return {
"userId": data["userId"],
"userDataEx": profile
}
return {"userId": data["userId"], "userDataEx": profile}
def handle_get_user_duel_api_request(self, data: Dict) -> Dict:
user_duel_list = self.data.item.get_duels(data["userId"])
if user_duel_list is None: return {}
if user_duel_list is None:
return {}
duel_list = []
for duel in user_duel_list:
tmp = duel._asdict()
@ -236,18 +239,18 @@ class ChuniBase():
duel_list.append(tmp)
return {
"userId": data["userId"],
"userId": data["userId"],
"length": len(duel_list),
"userDuelList": duel_list
"userDuelList": duel_list,
}
def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
return {
"userId": data["userId"],
"userId": data["userId"],
"length": 0,
"kind": data["kind"],
"nextIndex": -1,
"userFavoriteItemList": []
"kind": data["kind"],
"nextIndex": -1,
"userFavoriteItemList": [],
}
def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict:
@ -255,22 +258,23 @@ class ChuniBase():
This is handled via the webui, which we don't have right now
"""
return {
"userId": data["userId"],
"length": 0,
"userFavoriteMusicList": []
}
return {"userId": data["userId"], "length": 0, "userFavoriteMusicList": []}
def handle_get_user_item_api_request(self, data: Dict) -> Dict:
kind = int(int(data["nextIndex"]) / 10000000000)
next_idx = int(int(data["nextIndex"]) % 10000000000)
user_item_list = self.data.item.get_items(data["userId"], kind)
if user_item_list is None or len(user_item_list) == 0:
return {"userId": data["userId"], "nextIndex": -1, "itemKind": kind, "userItemList": []}
if user_item_list is None or len(user_item_list) == 0:
return {
"userId": data["userId"],
"nextIndex": -1,
"itemKind": kind,
"userItemList": [],
}
items: list[Dict[str, Any]] = []
for i in range(next_idx, len(user_item_list)):
for i in range(next_idx, len(user_item_list)):
tmp = user_item_list[i]._asdict()
tmp.pop("user")
tmp.pop("id")
@ -280,38 +284,47 @@ class ChuniBase():
xout = kind * 10000000000 + next_idx + len(items)
if len(items) < int(data["maxCount"]): nextIndex = 0
else: nextIndex = xout
if len(items) < int(data["maxCount"]):
nextIndex = 0
else:
nextIndex = xout
return {"userId": data["userId"], "nextIndex": nextIndex, "itemKind": kind, "length": len(items), "userItemList": items}
return {
"userId": data["userId"],
"nextIndex": nextIndex,
"itemKind": kind,
"length": len(items),
"userItemList": items,
}
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
"""
Unsure how to get this to trigger...
"""
return {
"userId": data["userId"],
"userId": data["userId"],
"length": 2,
"userLoginBonusList": [
{
"presetId": '10',
"bonusCount": '0',
"lastUpdateDate": "1970-01-01 09:00:00",
"isWatched": "true"
"presetId": "10",
"bonusCount": "0",
"lastUpdateDate": "1970-01-01 09:00:00",
"isWatched": "true",
},
{
"presetId": '20',
"bonusCount": '0',
"lastUpdateDate": "1970-01-01 09:00:00",
"isWatched": "true"
"presetId": "20",
"bonusCount": "0",
"lastUpdateDate": "1970-01-01 09:00:00",
"isWatched": "true",
},
]
],
}
def handle_get_user_map_api_request(self, data: Dict) -> Dict:
user_map_list = self.data.item.get_maps(data["userId"])
if user_map_list is None: return {}
if user_map_list is None:
return {}
map_list = []
for map in user_map_list:
tmp = map._asdict()
@ -320,19 +333,19 @@ class ChuniBase():
map_list.append(tmp)
return {
"userId": data["userId"],
"userId": data["userId"],
"length": len(map_list),
"userMapList": map_list
"userMapList": map_list,
}
def handle_get_user_music_api_request(self, data: Dict) -> Dict:
music_detail = self.data.score.get_scores(data["userId"])
if music_detail is None:
if music_detail is None:
return {
"userId": data["userId"],
"length": 0,
"userId": data["userId"],
"length": 0,
"nextIndex": -1,
"userMusicList": [] #240
"userMusicList": [], # 240
}
song_list = []
next_idx = int(data["nextIndex"])
@ -343,66 +356,60 @@ class ChuniBase():
tmp = music_detail[x]._asdict()
tmp.pop("user")
tmp.pop("id")
for song in song_list:
if song["userMusicDetailList"][0]["musicId"] == tmp["musicId"]:
found = True
song["userMusicDetailList"].append(tmp)
song["length"] = len(song["userMusicDetailList"])
if not found:
song_list.append({
"length": 1,
"userMusicDetailList": [tmp]
})
song_list.append({"length": 1, "userMusicDetailList": [tmp]})
if len(song_list) >= max_ct:
break
if len(song_list) >= max_ct:
next_idx += max_ct
else:
next_idx = 0
return {
"userId": data["userId"],
"length": len(song_list),
"userId": data["userId"],
"length": len(song_list),
"nextIndex": next_idx,
"userMusicList": song_list #240
"userMusicList": song_list, # 240
}
def handle_get_user_option_api_request(self, data: Dict) -> Dict:
p = self.data.profile.get_profile_option(data["userId"])
option = p._asdict()
option.pop("id")
option.pop("user")
return {
"userId": data["userId"],
"userGameOption": option
}
return {"userId": data["userId"], "userGameOption": option}
def handle_get_user_option_ex_api_request(self, data: Dict) -> Dict:
p = self.data.profile.get_profile_option_ex(data["userId"])
option = p._asdict()
option.pop("id")
option.pop("user")
return {
"userId": data["userId"],
"userGameOptionEx": option
}
return {"userId": data["userId"], "userGameOptionEx": option}
def read_wtf8(self, src):
return bytes([ord(c) for c in src]).decode("utf-8")
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile_preview(data["userId"], self.version)
if profile is None: return None
profile_character = self.data.item.get_character(data["userId"], profile["characterId"])
if profile is None:
return None
profile_character = self.data.item.get_character(
data["userId"], profile["characterId"]
)
if profile_character is None:
chara = {}
else:
@ -411,8 +418,8 @@ class ChuniBase():
chara.pop("user")
return {
"userId": data["userId"],
# Current Login State
"userId": data["userId"],
# Current Login State
"isLogin": False,
"lastLoginDate": profile["lastPlayDate"],
# User Profile
@ -424,14 +431,14 @@ class ChuniBase():
"lastGameId": profile["lastGameId"],
"lastRomVersion": profile["lastRomVersion"],
"lastDataVersion": profile["lastDataVersion"],
"lastPlayDate": profile["lastPlayDate"],
"trophyId": profile["trophyId"],
"lastPlayDate": profile["lastPlayDate"],
"trophyId": profile["trophyId"],
"nameplateId": profile["nameplateId"],
# Current Selected Character
"userCharacter": chara,
# User Game Options
"playerLevel": profile["playerLevel"],
"rating": profile["rating"],
"playerLevel": profile["playerLevel"],
"rating": profile["rating"],
"headphone": profile["headphone"],
"chargeState": "1",
"userNameEx": profile["userName"],
@ -439,7 +446,7 @@ class ChuniBase():
def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict:
recet_rating_list = self.data.profile.get_profile_recent_rating(data["userId"])
if recet_rating_list is None:
if recet_rating_list is None:
return {
"userId": data["userId"],
"length": 0,
@ -462,11 +469,8 @@ class ChuniBase():
def handle_get_user_team_api_request(self, data: Dict) -> Dict:
# TODO: Team
return {
"userId": data["userId"],
"teamId": 0
}
return {"userId": data["userId"], "teamId": 0}
def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict:
return {
"userId": data["userId"],
@ -489,19 +493,30 @@ class ChuniBase():
if "userData" in upsert:
try:
upsert["userData"][0]["userName"] = self.read_wtf8(upsert["userData"][0]["userName"])
except: pass
upsert["userData"][0]["userName"] = self.read_wtf8(
upsert["userData"][0]["userName"]
)
except:
pass
self.data.profile.put_profile_data(user_id, self.version, upsert["userData"][0])
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
if "userDataEx" in upsert:
self.data.profile.put_profile_data_ex(user_id, self.version, upsert["userDataEx"][0])
self.data.profile.put_profile_data_ex(
user_id, self.version, upsert["userDataEx"][0]
)
if "userGameOption" in upsert:
self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0])
if "userGameOptionEx" in upsert:
self.data.profile.put_profile_option_ex(user_id, upsert["userGameOptionEx"][0])
self.data.profile.put_profile_option_ex(
user_id, upsert["userGameOptionEx"][0]
)
if "userRecentRatingList" in upsert:
self.data.profile.put_profile_recent_rating(user_id, upsert["userRecentRatingList"])
self.data.profile.put_profile_recent_rating(
user_id, upsert["userRecentRatingList"]
)
if "userCharacterList" in upsert:
for character in upsert["userCharacterList"]:
self.data.item.put_character(user_id, character)
@ -517,7 +532,7 @@ class ChuniBase():
if "userDuelList" in upsert:
for duel in upsert["userDuelList"]:
self.data.item.put_duel(user_id, duel)
if "userItemList" in upsert:
for item in upsert["userItemList"]:
self.data.item.put_item(user_id, item)
@ -525,23 +540,23 @@ class ChuniBase():
if "userActivityList" in upsert:
for activity in upsert["userActivityList"]:
self.data.profile.put_profile_activity(user_id, activity)
if "userChargeList" in upsert:
for charge in upsert["userChargeList"]:
self.data.profile.put_profile_charge(user_id, charge)
if "userMusicDetailList" in upsert:
for song in upsert["userMusicDetailList"]:
self.data.score.put_score(user_id, song)
if "userPlaylogList" in upsert:
for playlog in upsert["userPlaylogList"]:
self.data.score.put_playlog(user_id, playlog)
if "userTeamPoint" in upsert:
# TODO: team stuff
pass
if "userMapAreaList" in upsert:
for map_area in upsert["userMapAreaList"]:
self.data.item.put_map_area(user_id, map_area)
@ -554,22 +569,22 @@ class ChuniBase():
for emoney in upsert["userEmoneyList"]:
self.data.profile.put_profile_emoney(user_id, emoney)
return { "returnCode": "1" }
return {"returnCode": "1"}
def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:
return { "returnCode": "1" }
return {"returnCode": "1"}
def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict:
return { "returnCode": "1" }
return {"returnCode": "1"}
def handle_upsert_client_develop_api_request(self, data: Dict) -> Dict:
return { "returnCode": "1" }
return {"returnCode": "1"}
def handle_upsert_client_error_api_request(self, data: Dict) -> Dict:
return { "returnCode": "1" }
return {"returnCode": "1"}
def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict:
return { "returnCode": "1" }
return {"returnCode": "1"}
def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict:
return { "returnCode": "1" }
return {"returnCode": "1"}

View File

@ -1,36 +1,49 @@
from core.config import CoreConfig
from typing import Dict
class ChuniServerConfig():
class ChuniServerConfig:
def __init__(self, parent_config: "ChuniConfig") -> None:
self.__config = parent_config
@property
def enable(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'chuni', 'server', 'enable', default=True)
return CoreConfig.get_config_field(
self.__config, "chuni", "server", "enable", default=True
)
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'chuni', 'server', 'loglevel', default="info"))
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "chuni", "server", "loglevel", default="info"
)
)
class ChuniCryptoConfig():
class ChuniCryptoConfig:
def __init__(self, parent_config: "ChuniConfig") -> None:
self.__config = parent_config
@property
def keys(self) -> Dict:
"""
in the form of:
internal_version: [key, iv]
internal_version: [key, iv]
all values are hex strings
"""
return CoreConfig.get_config_field(self.__config, 'chuni', 'crypto', 'keys', default={})
return CoreConfig.get_config_field(
self.__config, "chuni", "crypto", "keys", default={}
)
@property
def encrypted_only(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'chuni', 'crypto', 'encrypted_only', default=False)
return CoreConfig.get_config_field(
self.__config, "chuni", "crypto", "encrypted_only", default=False
)
class ChuniConfig(dict):
def __init__(self) -> None:
self.server = ChuniServerConfig(self)
self.crypto = ChuniCryptoConfig(self)
self.crypto = ChuniCryptoConfig(self)

View File

@ -1,4 +1,4 @@
class ChuniConstants():
class ChuniConstants:
GAME_CODE = "SDBT"
GAME_CODE_NEW = "SDHD"
@ -8,7 +8,7 @@ class ChuniConstants():
VER_CHUNITHM_PLUS = 1
VER_CHUNITHM_AIR = 2
VER_CHUNITHM_AIR_PLUS = 3
VER_CHUNITHM_STAR = 4
VER_CHUNITHM_STAR = 4
VER_CHUNITHM_STAR_PLUS = 5
VER_CHUNITHM_AMAZON = 6
VER_CHUNITHM_AMAZON_PLUS = 7
@ -18,8 +18,21 @@ class ChuniConstants():
VER_CHUNITHM_NEW = 11
VER_CHUNITHM_NEW_PLUS = 12
VERSION_NAMES = ["Chunithm", "Chunithm+", "Chunithm Air", "Chunithm Air+", "Chunithm Star", "Chunithm Star+", "Chunithm Amazon",
"Chunithm Amazon+", "Chunithm Crystal", "Chunithm Crystal+", "Chunithm Paradise", "Chunithm New!!", "Chunithm New!!+"]
VERSION_NAMES = [
"Chunithm",
"Chunithm+",
"Chunithm Air",
"Chunithm Air+",
"Chunithm Star",
"Chunithm Star+",
"Chunithm Amazon",
"Chunithm Amazon+",
"Chunithm Crystal",
"Chunithm Crystal+",
"Chunithm Paradise",
"Chunithm New!!",
"Chunithm New!!+",
]
@classmethod
def game_ver_to_string(cls, ver: int):

View File

@ -7,12 +7,13 @@ from titles.chuni.base import ChuniBase
from titles.chuni.const import ChuniConstants
from titles.chuni.config import ChuniConfig
class ChuniCrystal(ChuniBase):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)
ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["dataVersion"] = "1.40.00"
return ret

View File

@ -7,12 +7,13 @@ from titles.chuni.base import ChuniBase
from titles.chuni.const import ChuniConstants
from titles.chuni.config import ChuniConfig
class ChuniCrystalPlus(ChuniBase):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)
ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["dataVersion"] = "1.45.00"
return ret

View File

@ -2,6 +2,7 @@ from core.data import Data
from core.config import CoreConfig
from titles.chuni.schema import *
class ChuniData(Data):
def __init__(self, cfg: CoreConfig) -> None:
super().__init__(cfg)
@ -9,4 +10,4 @@ class ChuniData(Data):
self.item = ChuniItemData(cfg, self.session)
self.profile = ChuniProfileData(cfg, self.session)
self.score = ChuniScoreData(cfg, self.session)
self.static = ChuniStaticData(cfg, self.session)
self.static = ChuniStaticData(cfg, self.session)

View File

@ -28,12 +28,15 @@ from titles.chuni.paradise import ChuniParadise
from titles.chuni.new import ChuniNew
from titles.chuni.newplus import ChuniNewPlus
class ChuniServlet():
class ChuniServlet:
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
self.core_cfg = core_cfg
self.game_cfg = ChuniConfig()
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}")))
self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"))
)
self.versions = [
ChuniBase(core_cfg, self.game_cfg),
@ -56,135 +59,161 @@ class ChuniServlet():
if not hasattr(self.logger, "inited"):
log_fmt_str = "[%(asctime)s] Chunithm | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "chuni"), encoding='utf8',
when="d", backupCount=10)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.core_cfg.server.log_dir, "chuni"),
encoding="utf8",
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler)
self.logger.setLevel(self.game_cfg.server.loglevel)
coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str)
coloredlogs.install(
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
)
self.logger.inited = True
@classmethod
def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]:
def get_allnet_info(
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
) -> Tuple[bool, str, str]:
game_cfg = ChuniConfig()
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}")))
game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"))
)
if not game_cfg.server.enable:
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,
f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/",
"",
)
return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "")
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
if url_path.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]
if version < 105: # 1.0
if version < 105: # 1.0
internal_ver = ChuniConstants.VER_CHUNITHM
elif version >= 105 and version < 110: # Plus
elif version >= 105 and version < 110: # Plus
internal_ver = ChuniConstants.VER_CHUNITHM_PLUS
elif version >= 110 and version < 115: # Air
elif version >= 110 and version < 115: # Air
internal_ver = ChuniConstants.VER_CHUNITHM_AIR
elif version >= 115 and version < 120: # Air Plus
elif version >= 115 and version < 120: # Air Plus
internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS
elif version >= 120 and version < 125: # Star
elif version >= 120 and version < 125: # Star
internal_ver = ChuniConstants.VER_CHUNITHM_STAR
elif version >= 125 and version < 130: # Star Plus
elif version >= 125 and version < 130: # Star Plus
internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS
elif version >= 130 and version < 135: # Amazon
elif version >= 130 and version < 135: # Amazon
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON
elif version >= 135 and version < 140: # Amazon Plus
elif version >= 135 and version < 140: # Amazon Plus
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
elif version >= 140 and version < 145: # Crystal
elif version >= 140 and version < 145: # Crystal
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL
elif version >= 145 and version < 150: # Crystal Plus
elif version >= 145 and version < 150: # Crystal Plus
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
elif version >= 150 and version < 200: # Paradise
elif version >= 150 and version < 200: # Paradise
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE
elif version >= 200 and version < 205: # New
elif version >= 200 and version < 205: # New
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
elif version >= 205 and version < 210: # New Plus
elif version >= 205 and version < 210: # New Plus
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
# If we get a 32 character long hex string, it's a hash and we're
# doing encrypted. The likelyhood of false positives is low but
# If we get a 32 character long hex string, it's a hash and we're
# doing encrypted. The likelyhood of false positives is low but
# technically not 0
endpoint = request.getHeader("User-Agent").split("#")[0]
try:
crypt = AES.new(
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][0]),
AES.MODE_CBC,
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][1])
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][0]),
AES.MODE_CBC,
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][1]),
)
req_raw = crypt.decrypt(req_raw)
except:
self.logger.error(f"Failed to decrypt v{version} request to {endpoint} -> {req_raw}")
return zlib.compress("{\"stat\": \"0\"}".encode("utf-8"))
self.logger.error(
f"Failed to decrypt v{version} request to {endpoint} -> {req_raw}"
)
return zlib.compress(b'{"stat": "0"}')
encrtped = True
if not encrtped and self.game_cfg.crypto.encrypted_only:
self.logger.error(f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}")
return zlib.compress("{\"stat\": \"0\"}".encode("utf-8"))
try:
if not encrtped and self.game_cfg.crypto.encrypted_only:
self.logger.error(
f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}"
)
return zlib.compress(b'{"stat": "0"}')
try:
unzip = zlib.decompress(req_raw)
except zlib.error as e:
self.logger.error(f"Failed to decompress v{version} {endpoint} request -> {e}")
self.logger.error(
f"Failed to decompress v{version} {endpoint} request -> {e}"
)
return b""
req_data = json.loads(unzip)
self.logger.info(f"v{version} {endpoint} request from {request.getClientAddress().host}")
self.logger.info(
f"v{version} {endpoint} request from {request.getClientAddress().host}"
)
self.logger.debug(req_data)
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
try:
handler = getattr(self.versions[internal_ver], func_to_find)
resp = handler(req_data)
except AttributeError as e:
self.logger.warning(f"Unhandled v{version} request {endpoint} - {e}")
return zlib.compress("{\"stat\": \"0\"}".encode("utf-8"))
if not hasattr(self.versions[internal_ver], func_to_find):
self.logger.warning(f"Unhandled v{version} request {endpoint}")
resp = {"returnCode": 1}
else:
try:
handler = getattr(self.versions[internal_ver], func_to_find)
resp = handler(req_data)
except Exception as e:
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
return zlib.compress(b'{"stat": "0"}')
except Exception as e:
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
return zlib.compress("{\"stat\": \"0\"}".encode("utf-8"))
if resp == None:
resp = {'returnCode': 1}
resp = {"returnCode": 1}
self.logger.debug(f"Response {resp}")
zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
if not encrtped:
return zipped
padded = pad(zipped, 16)
crypt = AES.new(
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][0]),
AES.MODE_CBC,
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][1])
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][0]),
AES.MODE_CBC,
bytes.fromhex(self.game_cfg.crypto.keys[str(internal_ver)][1]),
)
return crypt.encrypt(padded)

View File

@ -9,13 +9,9 @@ from titles.chuni.database import ChuniData
from titles.chuni.base import ChuniBase
from titles.chuni.config import ChuniConfig
class ChuniNew(ChuniBase):
ITEM_TYPE = {
"character": 20,
"story": 21,
"card": 22
}
class ChuniNew(ChuniBase):
ITEM_TYPE = {"character": 20, "story": 21, "card": 22}
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
self.core_cfg = core_cfg
@ -25,12 +21,20 @@ class ChuniNew(ChuniBase):
self.logger = logging.getLogger("chuni")
self.game = ChuniConstants.GAME_CODE
self.version = ChuniConstants.VER_CHUNITHM_NEW
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
match_start = datetime.strftime(datetime.now() - timedelta(hours=10), self.date_time_format)
match_end = datetime.strftime(datetime.now() + timedelta(hours=10), self.date_time_format)
reboot_start = datetime.strftime(datetime.now() - timedelta(hours=11), self.date_time_format)
reboot_end = datetime.strftime(datetime.now() - timedelta(hours=10), self.date_time_format)
match_start = datetime.strftime(
datetime.now() - timedelta(hours=10), self.date_time_format
)
match_end = datetime.strftime(
datetime.now() + timedelta(hours=10), self.date_time_format
)
reboot_start = datetime.strftime(
datetime.now() - timedelta(hours=11), self.date_time_format
)
reboot_end = datetime.strftime(
datetime.now() - timedelta(hours=10), self.date_time_format
)
return {
"gameSetting": {
"isMaintenance": "false",
@ -52,16 +56,19 @@ class ChuniNew(ChuniBase):
"udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
"reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
},
"isDumpUpload": "false",
"isAou": "false",
"isDumpUpload": "false",
"isAou": "false",
}
def handle_remove_token_api_request(self, data: Dict) -> Dict:
return { "returnCode": "1" }
def handle_delete_token_api_request(self, data: Dict) -> Dict:
return { "returnCode": "1" }
return {"returnCode": "1"}
def handle_create_token_api_request(self, data: Dict) -> Dict:
return { "returnCode": "1" }
return {"returnCode": "1"}
def handle_get_user_map_area_api_request(self, data: Dict) -> Dict:
user_map_areas = self.data.item.get_map_areas(data["userId"])
@ -72,32 +79,29 @@ class ChuniNew(ChuniBase):
tmp.pop("user")
map_areas.append(tmp)
return {
"userId": data["userId"],
"userMapAreaList": map_areas
}
return {"userId": data["userId"], "userMapAreaList": map_areas}
def handle_get_user_symbol_chat_setting_api_request(self, data: Dict) -> Dict:
return {
"userId": data["userId"],
"symbolCharInfoList": []
}
return {"userId": data["userId"], "symbolCharInfoList": []}
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile_preview(data["userId"], self.version)
if profile is None: return None
profile_character = self.data.item.get_character(data["userId"], profile["characterId"])
if profile is None:
return None
profile_character = self.data.item.get_character(
data["userId"], profile["characterId"]
)
if profile_character is None:
chara = {}
else:
chara = profile_character._asdict()
chara.pop("id")
chara.pop("user")
data1 = {
"userId": data["userId"],
# Current Login State
"userId": data["userId"],
# Current Login State
"isLogin": False,
"lastLoginDate": profile["lastPlayDate"],
# User Profile
@ -109,14 +113,14 @@ class ChuniNew(ChuniBase):
"lastGameId": profile["lastGameId"],
"lastRomVersion": profile["lastRomVersion"],
"lastDataVersion": profile["lastDataVersion"],
"lastPlayDate": profile["lastPlayDate"],
"lastPlayDate": profile["lastPlayDate"],
"emoneyBrandId": 0,
"trophyId": profile["trophyId"],
"trophyId": profile["trophyId"],
# Current Selected Character
"userCharacter": chara,
# User Game Options
"playerLevel": profile["playerLevel"],
"rating": profile["rating"],
"playerLevel": profile["playerLevel"],
"rating": profile["rating"],
"headphone": profile["headphone"],
"chargeState": 0,
"userNameEx": "0",

View File

@ -7,17 +7,26 @@ from titles.chuni.new import ChuniNew
from titles.chuni.const import ChuniConstants
from titles.chuni.config import ChuniConfig
class ChuniNewPlus(ChuniNew):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_NEW_PLUS
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["romVersion"] = "2.05.00"
ret["gameSetting"]["dataVersion"] = "2.05.00"
ret["gameSetting"]["matchingUri"] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
ret["gameSetting"]["matchingUriX"] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
ret["gameSetting"]["udpHolePunchUri"] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
ret["gameSetting"]["reflectorUri"] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
ret["gameSetting"][
"matchingUri"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
ret["gameSetting"][
"matchingUriX"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
ret["gameSetting"][
"udpHolePunchUri"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
ret["gameSetting"][
"reflectorUri"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
return ret

View File

@ -7,12 +7,13 @@ from titles.chuni.base import ChuniBase
from titles.chuni.const import ChuniConstants
from titles.chuni.config import ChuniConfig
class ChuniParadise(ChuniBase):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_PARADISE
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)
ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["dataVersion"] = "1.50.00"
return ret

View File

@ -5,12 +5,13 @@ from titles.chuni.base import ChuniBase
from titles.chuni.const import ChuniConstants
from titles.chuni.config import ChuniConfig
class ChuniPlus(ChuniBase):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_PLUS
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)
ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["dataVersion"] = "1.05.00"
return ret
return ret

View File

@ -7,48 +7,60 @@ from core.config import CoreConfig
from titles.chuni.database import ChuniData
from titles.chuni.const import ChuniConstants
class ChuniReader(BaseReader):
def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None:
def __init__(
self,
config: CoreConfig,
version: int,
bin_dir: Optional[str],
opt_dir: Optional[str],
extra: Optional[str],
) -> None:
super().__init__(config, version, bin_dir, opt_dir, extra)
self.data = ChuniData(config)
try:
self.logger.info(f"Start importer for {ChuniConstants.game_ver_to_string(version)}")
self.logger.info(
f"Start importer for {ChuniConstants.game_ver_to_string(version)}"
)
except IndexError:
self.logger.error(f"Invalid chunithm version {version}")
exit(1)
def read(self) -> None:
data_dirs = []
if self.bin_dir is not None:
data_dirs += self.get_data_directories(self.bin_dir)
if self.opt_dir is not None:
data_dirs += self.get_data_directories(self.opt_dir)
data_dirs += self.get_data_directories(self.opt_dir)
for dir in data_dirs:
self.logger.info(f"Read from {dir}")
self.read_events(f"{dir}/event")
self.read_music(f"{dir}/music")
self.read_charges(f"{dir}/chargeItem")
self.read_avatar(f"{dir}/avatarAccessory")
def read_events(self, evt_dir: str) -> None:
for root, dirs, files in walk(evt_dir):
for dir in dirs:
if path.exists(f"{root}/{dir}/Event.xml"):
with open(f"{root}/{dir}/Event.xml", 'rb') as fp:
with open(f"{root}/{dir}/Event.xml", "rb") as fp:
bytedata = fp.read()
strdata = bytedata.decode('UTF-8')
strdata = bytedata.decode("UTF-8")
xml_root = ET.fromstring(strdata)
for name in xml_root.findall('name'):
id = name.find('id').text
name = name.find('str').text
for substances in xml_root.findall('substances'):
event_type = substances.find('type').text
result = self.data.static.put_event(self.version, id, event_type, name)
for name in xml_root.findall("name"):
id = name.find("id").text
name = name.find("str").text
for substances in xml_root.findall("substances"):
event_type = substances.find("type").text
result = self.data.static.put_event(
self.version, id, event_type, name
)
if result is not None:
self.logger.info(f"Inserted event {id}")
else:
@ -58,73 +70,90 @@ class ChuniReader(BaseReader):
for root, dirs, files in walk(music_dir):
for dir in dirs:
if path.exists(f"{root}/{dir}/Music.xml"):
with open(f"{root}/{dir}/Music.xml", 'rb') as fp:
with open(f"{root}/{dir}/Music.xml", "rb") as fp:
bytedata = fp.read()
strdata = bytedata.decode('UTF-8')
strdata = bytedata.decode("UTF-8")
xml_root = ET.fromstring(strdata)
for name in xml_root.findall('name'):
song_id = name.find('id').text
title = name.find('str').text
for name in xml_root.findall("name"):
song_id = name.find("id").text
title = name.find("str").text
for artistName in xml_root.findall('artistName'):
artist = artistName.find('str').text
for artistName in xml_root.findall("artistName"):
artist = artistName.find("str").text
for genreNames in xml_root.findall('genreNames'):
for list_ in genreNames.findall('list'):
for StringID in list_.findall('StringID'):
genre = StringID.find('str').text
for genreNames in xml_root.findall("genreNames"):
for list_ in genreNames.findall("list"):
for StringID in list_.findall("StringID"):
genre = StringID.find("str").text
for jaketFile in xml_root.findall('jaketFile'): #nice typo, SEGA
jacket_path = jaketFile.find('path').text
for jaketFile in xml_root.findall("jaketFile"): # nice typo, SEGA
jacket_path = jaketFile.find("path").text
for fumens in xml_root.findall("fumens"):
for MusicFumenData in fumens.findall("MusicFumenData"):
fumen_path = MusicFumenData.find("file").find("path")
for fumens in xml_root.findall('fumens'):
for MusicFumenData in fumens.findall('MusicFumenData'):
fumen_path = MusicFumenData.find('file').find("path")
if fumen_path is not None:
chart_id = MusicFumenData.find('type').find('id').text
chart_id = MusicFumenData.find("type").find("id").text
if chart_id == "4":
level = float(xml_root.find("starDifType").text)
we_chara = xml_root.find("worldsEndTagName").find("str").text
we_chara = (
xml_root.find("worldsEndTagName")
.find("str")
.text
)
else:
level = float(f"{MusicFumenData.find('level').text}.{MusicFumenData.find('levelDecimal').text}")
level = float(
f"{MusicFumenData.find('level').text}.{MusicFumenData.find('levelDecimal').text}"
)
we_chara = None
result = self.data.static.put_music(
self.version,
song_id,
chart_id,
chart_id,
title,
artist,
level,
genre,
jacket_path,
we_chara
we_chara,
)
if result is not None:
self.logger.info(f"Inserted music {song_id} chart {chart_id}")
self.logger.info(
f"Inserted music {song_id} chart {chart_id}"
)
else:
self.logger.warn(f"Failed to insert music {song_id} chart {chart_id}")
self.logger.warn(
f"Failed to insert music {song_id} chart {chart_id}"
)
def read_charges(self, charge_dir: str) -> None:
for root, dirs, files in walk(charge_dir):
for dir in dirs:
if path.exists(f"{root}/{dir}/ChargeItem.xml"):
with open(f"{root}/{dir}/ChargeItem.xml", 'rb') as fp:
with open(f"{root}/{dir}/ChargeItem.xml", "rb") as fp:
bytedata = fp.read()
strdata = bytedata.decode('UTF-8')
strdata = bytedata.decode("UTF-8")
xml_root = ET.fromstring(strdata)
for name in xml_root.findall('name'):
id = name.find('id').text
name = name.find('str').text
expirationDays = xml_root.find('expirationDays').text
consumeType = xml_root.find('consumeType').text
sellingAppeal = bool(xml_root.find('sellingAppeal').text)
for name in xml_root.findall("name"):
id = name.find("id").text
name = name.find("str").text
expirationDays = xml_root.find("expirationDays").text
consumeType = xml_root.find("consumeType").text
sellingAppeal = bool(xml_root.find("sellingAppeal").text)
result = self.data.static.put_charge(self.version, id, name, expirationDays, consumeType, sellingAppeal)
result = self.data.static.put_charge(
self.version,
id,
name,
expirationDays,
consumeType,
sellingAppeal,
)
if result is not None:
self.logger.info(f"Inserted charge {id}")
@ -135,21 +164,23 @@ class ChuniReader(BaseReader):
for root, dirs, files in walk(avatar_dir):
for dir in dirs:
if path.exists(f"{root}/{dir}/AvatarAccessory.xml"):
with open(f"{root}/{dir}/AvatarAccessory.xml", 'rb') as fp:
with open(f"{root}/{dir}/AvatarAccessory.xml", "rb") as fp:
bytedata = fp.read()
strdata = bytedata.decode('UTF-8')
strdata = bytedata.decode("UTF-8")
xml_root = ET.fromstring(strdata)
for name in xml_root.findall('name'):
id = name.find('id').text
name = name.find('str').text
category = xml_root.find('category').text
for image in xml_root.findall('image'):
iconPath = image.find('path').text
for texture in xml_root.findall('texture'):
texturePath = texture.find('path').text
for name in xml_root.findall("name"):
id = name.find("id").text
name = name.find("str").text
category = xml_root.find("category").text
for image in xml_root.findall("image"):
iconPath = image.find("path").text
for texture in xml_root.findall("texture"):
texturePath = texture.find("path").text
result = self.data.static.put_avatar(self.version, id, name, category, iconPath, texturePath)
result = self.data.static.put_avatar(
self.version, id, name, category, iconPath, texturePath
)
if result is not None:
self.logger.info(f"Inserted avatarAccessory {id}")

View File

@ -3,4 +3,4 @@ from titles.chuni.schema.score import ChuniScoreData
from titles.chuni.schema.item import ChuniItemData
from titles.chuni.schema.static import ChuniStaticData
__all__ = ["ChuniProfileData", "ChuniScoreData", "ChuniItemData", "ChuniStaticData"]
__all__ = ["ChuniProfileData", "ChuniScoreData", "ChuniItemData", "ChuniStaticData"]

View File

@ -13,7 +13,11 @@ character = Table(
"chuni_item_character",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("characterId", Integer),
Column("level", Integer),
Column("param1", Integer),
@ -26,27 +30,35 @@ character = Table(
Column("assignIllust", Integer),
Column("exMaxLv", Integer),
UniqueConstraint("user", "characterId", name="chuni_item_character_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
item = Table(
"chuni_item_item",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("itemId", Integer),
Column("itemKind", Integer),
Column("stock", Integer),
Column("isValid", Boolean),
UniqueConstraint("user", "itemId", "itemKind", name="chuni_item_item_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
duel = Table(
"chuni_item_duel",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("duelId", Integer),
Column("progress", Integer),
Column("point", Integer),
@ -57,14 +69,18 @@ duel = Table(
Column("param3", Integer),
Column("param4", Integer),
UniqueConstraint("user", "duelId", name="chuni_item_duel_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
map = Table(
"chuni_item_map",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("mapId", Integer),
Column("position", Integer),
Column("isClear", Boolean),
@ -72,17 +88,21 @@ map = Table(
Column("routeNumber", Integer),
Column("eventId", Integer),
Column("rate", Integer),
Column("statusCount", Integer),
Column("statusCount", Integer),
Column("isValid", Boolean),
UniqueConstraint("user", "mapId", name="chuni_item_map_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
map_area = Table(
"chuni_item_map_area",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("mapAreaId", Integer),
Column("rate", Integer),
Column("isClear", Boolean),
@ -91,9 +111,10 @@ map_area = Table(
Column("statusCount", Integer),
Column("remainGridCount", Integer),
UniqueConstraint("user", "mapAreaId", name="chuni_item_map_area_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class ChuniItemData(BaseData):
def put_character(self, user_id: int, character_data: Dict) -> Optional[int]:
character_data["user"] = user_id
@ -104,24 +125,26 @@ class ChuniItemData(BaseData):
conflict = sql.on_duplicate_key_update(**character_data)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_character(self, user_id: int, character_id: int) -> Optional[Dict]:
sql = select(character).where(and_(
character.c.user == user_id,
character.c.characterId == character_id
))
sql = select(character).where(
and_(character.c.user == user_id, character.c.characterId == character_id)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def get_characters(self, user_id: int) -> Optional[List[Row]]:
sql = select(character).where(character.c.user == user_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def put_item(self, user_id: int, item_data: Dict) -> Optional[int]:
@ -133,22 +156,23 @@ class ChuniItemData(BaseData):
conflict = sql.on_duplicate_key_update(**item_data)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_items(self, user_id: int, kind: int = None) -> Optional[List[Row]]:
if kind is None:
sql = select(item).where(item.c.user == user_id)
else:
sql = select(item).where(and_(
item.c.user == user_id,
item.c.itemKind == kind
))
sql = select(item).where(
and_(item.c.user == user_id, item.c.itemKind == kind)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def put_duel(self, user_id: int, duel_data: Dict) -> Optional[int]:
duel_data["user"] = user_id
@ -158,14 +182,16 @@ class ChuniItemData(BaseData):
conflict = sql.on_duplicate_key_update(**duel_data)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_duels(self, user_id: int) -> Optional[List[Row]]:
sql = select(duel).where(duel.c.user == user_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def put_map(self, user_id: int, map_data: Dict) -> Optional[int]:
@ -177,16 +203,18 @@ class ChuniItemData(BaseData):
conflict = sql.on_duplicate_key_update(**map_data)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_maps(self, user_id: int) -> Optional[List[Row]]:
sql = select(map).where(map.c.user == user_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def put_map_area(self, user_id: int, map_area_data: Dict) -> Optional[int]:
map_area_data["user"] = user_id
@ -196,12 +224,14 @@ class ChuniItemData(BaseData):
conflict = sql.on_duplicate_key_update(**map_area_data)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_map_areas(self, user_id: int) -> Optional[List[Row]]:
sql = select(map_area).where(map_area.c.user == user_id)
result = self.execute(sql)
if result is None: return None
return result.fetchall()
if result is None:
return None
return result.fetchall()

View File

@ -13,7 +13,11 @@ profile = Table(
"chuni_profile_data",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("exp", Integer),
Column("level", Integer),
@ -62,7 +66,7 @@ profile = Table(
Column("firstTutorialCancelNum", Integer),
Column("totalAdvancedHighScore", Integer),
Column("masterTutorialCancelNum", Integer),
Column("ext1", Integer), # Added in chunew
Column("ext1", Integer), # Added in chunew
Column("ext2", Integer),
Column("ext3", Integer),
Column("ext4", Integer),
@ -71,16 +75,20 @@ profile = Table(
Column("ext7", Integer),
Column("ext8", Integer),
Column("ext9", Integer),
Column("ext10", Integer),
Column("ext10", Integer),
Column("extStr1", String(255)),
Column("extStr2", String(255)),
Column("extLong1", Integer),
Column("extLong2", Integer),
Column("mapIconId", Integer),
Column("compatibleCmVersion", String(25)),
Column("medal", Integer),
Column("medal", Integer),
Column("voiceId", Integer),
Column("teamId", Integer, ForeignKey("chuni_profile_team.id", ondelete="SET NULL", onupdate="SET NULL")),
Column(
"teamId",
Integer,
ForeignKey("chuni_profile_team.id", ondelete="SET NULL", onupdate="SET NULL"),
),
Column("avatarBack", Integer, server_default="0"),
Column("avatarFace", Integer, server_default="0"),
Column("eliteRankPoint", Integer, server_default="0"),
@ -121,14 +129,18 @@ profile = Table(
Column("netBattleEndState", Integer, server_default="0"),
Column("avatarHead", Integer, server_default="0"),
UniqueConstraint("user", "version", name="chuni_profile_profile_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
profile_ex = Table(
"chuni_profile_data_ex",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("ext1", Integer),
Column("ext2", Integer),
@ -165,14 +177,18 @@ profile_ex = Table(
Column("mapIconId", Integer),
Column("compatibleCmVersion", String(25)),
UniqueConstraint("user", "version", name="chuni_profile_data_ex_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
option = Table(
"chuni_profile_option",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("speed", Integer),
Column("bgInfo", Integer),
Column("rating", Integer),
@ -195,7 +211,7 @@ option = Table(
Column("successSkill", Integer),
Column("successSlideHold", Integer),
Column("successTapTimbre", Integer),
Column("ext1", Integer), # Added in chunew
Column("ext1", Integer), # Added in chunew
Column("ext2", Integer),
Column("ext3", Integer),
Column("ext4", Integer),
@ -224,14 +240,18 @@ option = Table(
Column("playTimingOffset", Integer, server_default="0"),
Column("fieldWallPosition_120", Integer, server_default="0"),
UniqueConstraint("user", name="chuni_profile_option_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
option_ex = Table(
"chuni_profile_option_ex",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("ext1", Integer),
Column("ext2", Integer),
Column("ext3", Integer),
@ -253,51 +273,69 @@ option_ex = Table(
Column("ext19", Integer),
Column("ext20", Integer),
UniqueConstraint("user", name="chuni_profile_option_ex_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
recent_rating = Table(
"chuni_profile_recent_rating",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("recentRating", JSON),
UniqueConstraint("user", name="chuni_profile_recent_rating_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
region = Table(
"chuni_profile_region",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("regionId", Integer),
Column("playCount", Integer),
UniqueConstraint("user", "regionId", name="chuni_profile_region_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
activity = Table(
"chuni_profile_activity",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("kind", Integer),
Column("activityId", Integer), # Reminder: Change this to ID in base.py or the game will be sad
Column(
"activityId", Integer
), # Reminder: Change this to ID in base.py or the game will be sad
Column("sortNumber", Integer),
Column("param1", Integer),
Column("param2", Integer),
Column("param3", Integer),
Column("param4", Integer),
UniqueConstraint("user", "kind", "activityId", name="chuni_profile_activity_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
charge = Table(
"chuni_profile_charge",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("chargeId", Integer),
Column("stock", Integer),
Column("purchaseDate", String(25)),
@ -306,14 +344,18 @@ charge = Table(
Column("param2", Integer),
Column("paramDate", String(25)),
UniqueConstraint("user", "chargeId", name="chuni_profile_charge_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
emoney = Table(
"chuni_profile_emoney",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("ext1", Integer),
Column("ext2", Integer),
Column("ext3", Integer),
@ -321,20 +363,24 @@ emoney = Table(
Column("emoneyBrand", Integer),
Column("emoneyCredit", Integer),
UniqueConstraint("user", "emoneyBrand", name="chuni_profile_emoney_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
overpower = Table(
"chuni_profile_overpower",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("genreId", Integer),
Column("difficulty", Integer),
Column("rate", Integer),
Column("point", Integer),
UniqueConstraint("user", "genreId", "difficulty", name="chuni_profile_emoney_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
team = Table(
@ -343,18 +389,21 @@ team = Table(
Column("id", Integer, primary_key=True, nullable=False),
Column("teamName", String(255)),
Column("teamPoint", Integer),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class ChuniProfileData(BaseData):
def put_profile_data(self, aime_id: int, version: int, profile_data: Dict) -> Optional[int]:
def put_profile_data(
self, aime_id: int, version: int, profile_data: Dict
) -> Optional[int]:
profile_data["user"] = aime_id
profile_data["version"] = version
if "accessCode" in profile_data:
profile_data.pop("accessCode")
profile_data = self.fix_bools(profile_data)
sql = insert(profile).values(**profile_data)
conflict = sql.on_duplicate_key_update(**profile_data)
result = self.execute(conflict)
@ -363,51 +412,64 @@ class ChuniProfileData(BaseData):
self.logger.warn(f"put_profile_data: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def get_profile_preview(self, aime_id: int, version: int) -> Optional[Row]:
sql = select([profile, option]).join(option, profile.c.user == option.c.user).filter(
and_(profile.c.user == aime_id, profile.c.version == version)
sql = (
select([profile, option])
.join(option, profile.c.user == option.c.user)
.filter(and_(profile.c.user == aime_id, profile.c.version == version))
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def get_profile_data(self, aime_id: int, version: int) -> Optional[Row]:
sql = select(profile).where(and_(
profile.c.user == aime_id,
profile.c.version == version,
))
sql = select(profile).where(
and_(
profile.c.user == aime_id,
profile.c.version == version,
)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def put_profile_data_ex(self, aime_id: int, version: int, profile_ex_data: Dict) -> Optional[int]:
def put_profile_data_ex(
self, aime_id: int, version: int, profile_ex_data: Dict
) -> Optional[int]:
profile_ex_data["user"] = aime_id
profile_ex_data["version"] = version
if "accessCode" in profile_ex_data:
profile_ex_data.pop("accessCode")
sql = insert(profile_ex).values(**profile_ex_data)
conflict = sql.on_duplicate_key_update(**profile_ex_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile_data_ex: Failed to update! aime_id: {aime_id}")
self.logger.warn(
f"put_profile_data_ex: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid
def get_profile_data_ex(self, aime_id: int, version: int) -> Optional[Row]:
sql = select(profile_ex).where(and_(
profile_ex.c.user == aime_id,
profile_ex.c.version == version,
))
sql = select(profile_ex).where(
and_(
profile_ex.c.user == aime_id,
profile_ex.c.version == version,
)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def put_profile_option(self, aime_id: int, option_data: Dict) -> Optional[int]:
option_data["user"] = aime_id
@ -416,7 +478,9 @@ class ChuniProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile_option: Failed to update! aime_id: {aime_id}")
self.logger.warn(
f"put_profile_option: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid
@ -424,18 +488,23 @@ class ChuniProfileData(BaseData):
sql = select(option).where(option.c.user == aime_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def put_profile_option_ex(self, aime_id: int, option_ex_data: Dict) -> Optional[int]:
def put_profile_option_ex(
self, aime_id: int, option_ex_data: Dict
) -> Optional[int]:
option_ex_data["user"] = aime_id
sql = insert(option_ex).values(**option_ex_data)
conflict = sql.on_duplicate_key_update(**option_ex_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile_option_ex: Failed to update! aime_id: {aime_id}")
self.logger.warn(
f"put_profile_option_ex: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid
@ -443,27 +512,32 @@ class ChuniProfileData(BaseData):
sql = select(option_ex).where(option_ex.c.user == aime_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def put_profile_recent_rating(self, aime_id: int, recent_rating_data: List[Dict]) -> Optional[int]:
def put_profile_recent_rating(
self, aime_id: int, recent_rating_data: List[Dict]
) -> Optional[int]:
sql = insert(recent_rating).values(
user = aime_id,
recentRating = recent_rating_data
user=aime_id, recentRating=recent_rating_data
)
conflict = sql.on_duplicate_key_update(recentRating = recent_rating_data)
conflict = sql.on_duplicate_key_update(recentRating=recent_rating_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile_recent_rating: Failed to update! aime_id: {aime_id}")
self.logger.warn(
f"put_profile_recent_rating: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid
def get_profile_recent_rating(self, aime_id: int) -> Optional[Row]:
sql = select(recent_rating).where(recent_rating.c.user == aime_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def put_profile_activity(self, aime_id: int, activity_data: Dict) -> Optional[int]:
@ -471,35 +545,39 @@ class ChuniProfileData(BaseData):
activity_data["user"] = aime_id
activity_data["activityId"] = activity_data["id"]
activity_data.pop("id")
sql = insert(activity).values(**activity_data)
conflict = sql.on_duplicate_key_update(**activity_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile_activity: Failed to update! aime_id: {aime_id}")
self.logger.warn(
f"put_profile_activity: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid
def get_profile_activity(self, aime_id: int, kind: int) -> Optional[List[Row]]:
sql = select(activity).where(and_(
activity.c.user == aime_id,
activity.c.kind == kind
))
sql = select(activity).where(
and_(activity.c.user == aime_id, activity.c.kind == kind)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def put_profile_charge(self, aime_id: int, charge_data: Dict) -> Optional[int]:
charge_data["user"] = aime_id
sql = insert(charge).values(**charge_data)
conflict = sql.on_duplicate_key_update(**charge_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile_charge: Failed to update! aime_id: {aime_id}")
self.logger.warn(
f"put_profile_charge: Failed to update! aime_id: {aime_id}"
)
return None
return result.lastrowid
@ -507,9 +585,10 @@ class ChuniProfileData(BaseData):
sql = select(charge).where(charge.c.user == aime_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def add_profile_region(self, aime_id: int, region_id: int) -> Optional[int]:
pass
@ -523,29 +602,35 @@ class ChuniProfileData(BaseData):
conflict = sql.on_duplicate_key_update(**emoney_data)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_profile_emoney(self, aime_id: int) -> Optional[List[Row]]:
sql = select(emoney).where(emoney.c.user == aime_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def put_profile_overpower(self, aime_id: int, overpower_data: Dict) -> Optional[int]:
def put_profile_overpower(
self, aime_id: int, overpower_data: Dict
) -> Optional[int]:
overpower_data["user"] = aime_id
sql = insert(overpower).values(**overpower_data)
conflict = sql.on_duplicate_key_update(**overpower_data)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_profile_overpower(self, aime_id: int) -> Optional[List[Row]]:
sql = select(overpower).where(overpower.c.user == aime_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()

View File

@ -13,7 +13,11 @@ course = Table(
"chuni_score_course",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("courseId", Integer),
Column("classId", Integer),
Column("playCount", Integer),
@ -33,14 +37,18 @@ course = Table(
Column("orderId", Integer),
Column("playerRating", Integer),
UniqueConstraint("user", "courseId", name="chuni_score_course_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
best_score = Table(
"chuni_score_best",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("musicId", Integer),
Column("level", Integer),
Column("playCount", Integer),
@ -60,14 +68,18 @@ best_score = Table(
Column("ext1", Integer),
Column("theoryCount", Integer),
UniqueConstraint("user", "musicId", "level", name="chuni_score_best_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
playlog = Table(
"chuni_score_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("orderId", Integer),
Column("sortNumber", Integer),
Column("placeId", Integer),
@ -122,15 +134,17 @@ playlog = Table(
Column("charaIllustId", Integer),
Column("romVersion", String(255)),
Column("judgeHeaven", Integer),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class ChuniScoreData(BaseData):
def get_courses(self, aime_id: int) -> Optional[Row]:
sql = select(course).where(course.c.user == aime_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def put_course(self, aime_id: int, course_data: Dict) -> Optional[int]:
@ -141,16 +155,18 @@ class ChuniScoreData(BaseData):
conflict = sql.on_duplicate_key_update(**course_data)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_scores(self, aime_id: int) -> Optional[Row]:
sql = select(best_score).where(best_score.c.user == aime_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def put_score(self, aime_id: int, score_data: Dict) -> Optional[int]:
score_data["user"] = aime_id
score_data = self.fix_bools(score_data)
@ -159,16 +175,18 @@ class ChuniScoreData(BaseData):
conflict = sql.on_duplicate_key_update(**score_data)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_playlogs(self, aime_id: int) -> Optional[Row]:
sql = select(playlog).where(playlog.c.user == aime_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def put_playlog(self, aime_id: int, playlog_data: Dict) -> Optional[int]:
playlog_data["user"] = aime_id
playlog_data = self.fix_bools(playlog_data)
@ -177,5 +195,6 @@ class ChuniScoreData(BaseData):
conflict = sql.on_duplicate_key_update(**playlog_data)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid

View File

@ -19,7 +19,7 @@ events = Table(
Column("name", String(255)),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "eventId", name="chuni_static_events_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
music = Table(
@ -30,13 +30,13 @@ music = Table(
Column("songId", Integer),
Column("chartId", Integer),
Column("title", String(255)),
Column("artist", String(255)),
Column("artist", String(255)),
Column("level", Float),
Column("genre", String(255)),
Column("jacketPath", String(255)),
Column("worldsEndTag", String(7)),
UniqueConstraint("version", "songId", "chartId", name="chuni_static_music_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
charge = Table(
@ -51,7 +51,7 @@ charge = Table(
Column("sellingAppeal", Boolean),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "chargeId", name="chuni_static_charge_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
avatar = Table(
@ -65,159 +65,203 @@ avatar = Table(
Column("iconPath", String(255)),
Column("texturePath", String(255)),
UniqueConstraint("version", "avatarAccessoryId", name="chuni_static_avatar_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class ChuniStaticData(BaseData):
def put_event(self, version: int, event_id: int, type: int, name: str) -> Optional[int]:
def put_event(
self, version: int, event_id: int, type: int, name: str
) -> Optional[int]:
sql = insert(events).values(
version = version,
eventId = event_id,
type = type,
name = name
version=version, eventId=event_id, type=type, name=name
)
conflict = sql.on_duplicate_key_update(
name = name
)
conflict = sql.on_duplicate_key_update(name=name)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def update_event(self, version: int, event_id: int, enabled: bool) -> Optional[bool]:
sql = events.update(and_(events.c.version == version, events.c.eventId == event_id)).values(
enabled = enabled
)
def update_event(
self, version: int, event_id: int, enabled: bool
) -> Optional[bool]:
sql = events.update(
and_(events.c.version == version, events.c.eventId == event_id)
).values(enabled=enabled)
result = self.execute(sql)
if result is None:
self.logger.warn(f"update_event: failed to update event! version: {version}, event_id: {event_id}, enabled: {enabled}")
if result is None:
self.logger.warn(
f"update_event: failed to update event! version: {version}, event_id: {event_id}, enabled: {enabled}"
)
return None
event = self.get_event(version, event_id)
if event is None:
self.logger.warn(f"update_event: failed to fetch event {event_id} after updating")
self.logger.warn(
f"update_event: failed to fetch event {event_id} after updating"
)
return None
return event["enabled"]
def get_event(self, version: int, event_id: int) -> Optional[Row]:
sql = select(events).where(and_(events.c.version == version, events.c.eventId == event_id))
sql = select(events).where(
and_(events.c.version == version, events.c.eventId == event_id)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def get_enabled_events(self, version: int) -> Optional[List[Row]]:
sql = select(events).where(and_(events.c.version == version, events.c.enabled == True))
sql = select(events).where(
and_(events.c.version == version, events.c.enabled == True)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def get_events(self, version: int) -> Optional[List[Row]]:
sql = select(events).where(events.c.version == version)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def put_music(self, version: int, song_id: int, chart_id: int, title: int, artist: str,
level: float, genre: str, jacketPath: str, we_tag: str) -> Optional[int]:
def put_music(
self,
version: int,
song_id: int,
chart_id: int,
title: int,
artist: str,
level: float,
genre: str,
jacketPath: str,
we_tag: str,
) -> Optional[int]:
sql = insert(music).values(
version = version,
songId = song_id,
chartId = chart_id,
title = title,
artist = artist,
level = level,
genre = genre,
jacketPath = jacketPath,
worldsEndTag = we_tag,
version=version,
songId=song_id,
chartId=chart_id,
title=title,
artist=artist,
level=level,
genre=genre,
jacketPath=jacketPath,
worldsEndTag=we_tag,
)
conflict = sql.on_duplicate_key_update(
title = title,
artist = artist,
level = level,
genre = genre,
jacketPath = jacketPath,
worldsEndTag = we_tag,
title=title,
artist=artist,
level=level,
genre=genre,
jacketPath=jacketPath,
worldsEndTag=we_tag,
)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def put_charge(self, version: int, charge_id: int, name: str, expiration_days: int,
consume_type: int, selling_appeal: bool) -> Optional[int]:
def put_charge(
self,
version: int,
charge_id: int,
name: str,
expiration_days: int,
consume_type: int,
selling_appeal: bool,
) -> Optional[int]:
sql = insert(charge).values(
version = version,
chargeId = charge_id,
name = name,
expirationDays = expiration_days,
consumeType = consume_type,
sellingAppeal = selling_appeal,
version=version,
chargeId=charge_id,
name=name,
expirationDays=expiration_days,
consumeType=consume_type,
sellingAppeal=selling_appeal,
)
conflict = sql.on_duplicate_key_update(
name = name,
expirationDays = expiration_days,
consumeType = consume_type,
sellingAppeal = selling_appeal,
name=name,
expirationDays=expiration_days,
consumeType=consume_type,
sellingAppeal=selling_appeal,
)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_enabled_charges(self, version: int) -> Optional[List[Row]]:
sql = select(charge).where(and_(
charge.c.version == version,
charge.c.enabled == True
))
sql = select(charge).where(
and_(charge.c.version == version, charge.c.enabled == True)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def get_charges(self, version: int) -> Optional[List[Row]]:
sql = select(charge).where(charge.c.version == version)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def get_music_chart(self, version: int, song_id: int, chart_id: int) -> Optional[List[Row]]:
sql = select(music).where(and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id
))
def get_music_chart(
self, version: int, song_id: int, chart_id: int
) -> Optional[List[Row]]:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id,
)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def put_avatar(self, version: int, avatarAccessoryId: int, name: str, category: int, iconPath: str, texturePath: str) -> Optional[int]:
def put_avatar(
self,
version: int,
avatarAccessoryId: int,
name: str,
category: int,
iconPath: str,
texturePath: str,
) -> Optional[int]:
sql = insert(avatar).values(
version = version,
avatarAccessoryId = avatarAccessoryId,
name = name,
category = category,
iconPath = iconPath,
texturePath = texturePath,
version=version,
avatarAccessoryId=avatarAccessoryId,
name=name,
category=category,
iconPath=iconPath,
texturePath=texturePath,
)
conflict = sql.on_duplicate_key_update(
name = name,
category = category,
iconPath = iconPath,
texturePath = texturePath,
name=name,
category=category,
iconPath=iconPath,
texturePath=texturePath,
)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid

View File

@ -5,12 +5,13 @@ from titles.chuni.base import ChuniBase
from titles.chuni.const import ChuniConstants
from titles.chuni.config import ChuniConfig
class ChuniStar(ChuniBase):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_STAR
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)
ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["dataVersion"] = "1.20.00"
return ret
return ret

View File

@ -5,12 +5,13 @@ from titles.chuni.base import ChuniBase
from titles.chuni.const import ChuniConstants
from titles.chuni.config import ChuniConfig
class ChuniStarPlus(ChuniBase):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_STAR_PLUS
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)
ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["dataVersion"] = "1.25.00"
return ret
return ret

View File

@ -10,45 +10,42 @@ from titles.cm.const import CardMakerConstants
from titles.cm.config import CardMakerConfig
class CardMakerBase():
class CardMakerBase:
def __init__(self, core_cfg: CoreConfig, game_cfg: CardMakerConfig) -> None:
self.core_cfg = core_cfg
self.game_cfg = game_cfg
self.date_time_format = "%Y-%m-%d %H:%M:%S"
self.date_time_format_ext = "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5]
self.date_time_format_ext = (
"%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5]
)
self.date_time_format_short = "%Y-%m-%d"
self.logger = logging.getLogger("cardmaker")
self.game = CardMakerConstants.GAME_CODE
self.version = CardMakerConstants.VER_CARD_MAKER
def handle_get_game_connect_api_request(self, data: Dict) -> Dict:
uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}"
if self.core_cfg.server.is_develop:
uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}"
else:
uri = f"http://{self.core_cfg.title.hostname}"
# CHUNITHM = 0, maimai = 1, ONGEKI = 2
return {
"length": 3,
"gameConnectList": [
{
"modelKind": 0,
"type": 1,
"titleUri": f"{uri}/SDHD/200/"
},
{
"modelKind": 1,
"type": 1,
"titleUri": f"{uri}/SDEZ/120/"
},
{
"modelKind": 2,
"type": 1,
"titleUri": f"{uri}/SDDT/130/"
}
]
{"modelKind": 0, "type": 1, "titleUri": f"{uri}/SDHD/200/"},
{"modelKind": 1, "type": 1, "titleUri": f"{uri}/SDEZ/120/"},
{"modelKind": 2, "type": 1, "titleUri": f"{uri}/SDDT/130/"},
],
}
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)
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
)
return {
"gameSetting": {
@ -64,18 +61,14 @@ class CardMakerBase():
"maxCountCard": 100,
"watermark": False,
"isMaintenance": False,
"isBackgroundDistribute": False
"isBackgroundDistribute": False,
},
"isDumpUpload": False,
"isAou": False
"isAou": False,
}
def handle_get_client_bookkeeping_api_request(self, data: Dict) -> Dict:
return {
"placeId": data["placeId"],
"length": 0,
"clientBookkeepingList": []
}
return {"placeId": data["placeId"], "length": 0, "clientBookkeepingList": []}
def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1, "apiName": "UpsertClientSettingApi"}

View File

@ -17,29 +17,17 @@ class CardMaker136(CardMakerBase):
self.version = CardMakerConstants.VER_CARD_MAKER_136
def handle_get_game_connect_api_request(self, data: Dict) -> Dict:
uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}"
ret = super().handle_get_game_connect_api_request(data)
if self.core_cfg.server.is_develop:
uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}"
else:
uri = f"http://{self.core_cfg.title.hostname}"
# CHUNITHM = 0, maimai = 1, ONGEKI = 2
return {
"length": 3,
"gameConnectList": [
{
"modelKind": 0,
"type": 1,
"titleUri": f"{uri}/SDHD/205/"
},
{
"modelKind": 1,
"type": 1,
"titleUri": f"{uri}/SDEZ/125/"
},
{
"modelKind": 2,
"type": 1,
"titleUri": f"{uri}/SDDT/135/"
}
]
}
ret["gameConnectList"][0]["titleUri"] = f"{uri}/SDHD/205/"
ret["gameConnectList"][1]["titleUri"] = f"{uri}/SDEZ/125/"
ret["gameConnectList"][2]["titleUri"] = f"{uri}/SDDT/135/"
return ret
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)

View File

@ -1,17 +1,23 @@
from core.config import CoreConfig
class CardMakerServerConfig():
class CardMakerServerConfig:
def __init__(self, parent_config: "CardMakerConfig") -> None:
self.__config = parent_config
@property
def enable(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'cardmaker', 'server', 'enable', default=True)
return CoreConfig.get_config_field(
self.__config, "cardmaker", "server", "enable", default=True
)
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'cardmaker', 'server', 'loglevel', default="info"))
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "cardmaker", "server", "loglevel", default="info"
)
)
class CardMakerConfig(dict):

View File

@ -1,4 +1,4 @@
class CardMakerConstants():
class CardMakerConstants:
GAME_CODE = "SDED"
CONFIG_NAME = "cardmaker.yaml"

View File

@ -18,23 +18,29 @@ from titles.cm.base import CardMakerBase
from titles.cm.cm136 import CardMaker136
class CardMakerServlet():
class CardMakerServlet:
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
self.core_cfg = core_cfg
self.game_cfg = CardMakerConfig()
if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"):
self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}")))
self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"))
)
self.versions = [
CardMakerBase(core_cfg, self.game_cfg),
CardMaker136(core_cfg, self.game_cfg)
CardMaker136(core_cfg, self.game_cfg),
]
self.logger = logging.getLogger("cardmaker")
log_fmt_str = "[%(asctime)s] Card Maker | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cardmaker"), encoding='utf8',
when="d", backupCount=10)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.core_cfg.server.log_dir, "cardmaker"),
encoding="utf8",
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
@ -45,20 +51,29 @@ class CardMakerServlet():
self.logger.addHandler(consoleHandler)
self.logger.setLevel(self.game_cfg.server.loglevel)
coloredlogs.install(level=self.game_cfg.server.loglevel,
logger=self.logger, fmt=log_fmt_str)
coloredlogs.install(
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
)
@classmethod
def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]:
def get_allnet_info(
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
) -> Tuple[bool, str, str]:
game_cfg = CardMakerConfig()
if path.exists(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"):
game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}")))
game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{CardMakerConstants.CONFIG_NAME}"))
)
if not game_cfg.server.enable:
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,
f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/",
"",
)
return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "")
@ -86,8 +101,9 @@ class CardMakerServlet():
except zlib.error as e:
self.logger.error(
f"Failed to decompress v{version} {endpoint} request -> {e}")
return zlib.compress("{\"stat\": \"0\"}".encode("utf-8"))
f"Failed to decompress v{version} {endpoint} request -> {e}"
)
return zlib.compress(b'{"stat": "0"}')
req_data = json.loads(unzip)
@ -95,22 +111,20 @@ class CardMakerServlet():
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
if not hasattr(self.versions[internal_ver], func_to_find):
self.logger.warning(f"Unhandled v{version} request {endpoint}")
return zlib.compress(b'{"returnCode": 1}')
try:
handler = getattr(self.versions[internal_ver], func_to_find)
resp = handler(req_data)
except AttributeError as e:
self.logger.warning(
f"Unhandled v{version} request {endpoint} - {e}")
return zlib.compress("{\"stat\": \"0\"}".encode("utf-8"))
except Exception as e:
self.logger.error(
f"Error handling v{version} method {endpoint} - {e}")
return zlib.compress("{\"stat\": \"0\"}".encode("utf-8"))
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
return zlib.compress(b'{"stat": "0"}')
if resp is None:
resp = {'returnCode': 1}
resp = {"returnCode": 1}
self.logger.info(f"Response {resp}")

View File

@ -15,14 +15,21 @@ from titles.ongeki.config import OngekiConfig
class CardMakerReader(BaseReader):
def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str],
opt_dir: Optional[str], extra: Optional[str]) -> None:
def __init__(
self,
config: CoreConfig,
version: int,
bin_dir: Optional[str],
opt_dir: Optional[str],
extra: Optional[str],
) -> None:
super().__init__(config, version, bin_dir, opt_dir, extra)
self.ongeki_data = OngekiData(config)
try:
self.logger.info(
f"Start importer for {CardMakerConstants.game_ver_to_string(version)}")
f"Start importer for {CardMakerConstants.game_ver_to_string(version)}"
)
except IndexError:
self.logger.error(f"Invalid Card Maker version {version}")
exit(1)
@ -30,7 +37,7 @@ class CardMakerReader(BaseReader):
def read(self) -> None:
static_datas = {
"static_gachas.csv": "read_ongeki_gacha_csv",
"static_gacha_cards.csv": "read_ongeki_gacha_card_csv"
"static_gacha_cards.csv": "read_ongeki_gacha_card_csv",
}
data_dirs = []
@ -41,7 +48,9 @@ class CardMakerReader(BaseReader):
read_csv = getattr(CardMakerReader, func)
read_csv(self, f"{self.bin_dir}/MU3/{file}")
else:
self.logger.warn(f"Couldn't find {file} file in {self.bin_dir}, skipping")
self.logger.warn(
f"Couldn't find {file} file in {self.bin_dir}, skipping"
)
if self.opt_dir is not None:
data_dirs += self.get_data_directories(self.opt_dir)
@ -64,7 +73,7 @@ class CardMakerReader(BaseReader):
row["kind"],
type=row["type"],
isCeiling=True if row["isCeiling"] == "1" else False,
maxSelectPoint=row["maxSelectPoint"]
maxSelectPoint=row["maxSelectPoint"],
)
self.logger.info(f"Added gacha {row['gachaId']}")
@ -81,7 +90,7 @@ class CardMakerReader(BaseReader):
rarity=row["rarity"],
weight=row["weight"],
isPickup=True if row["isPickup"] == "1" else False,
isSelect=True if row["isSelect"] == "1" else False
isSelect=True if row["isSelect"] == "1" else False,
)
self.logger.info(f"Added card {row['cardId']} to gacha")
@ -95,7 +104,7 @@ class CardMakerReader(BaseReader):
"Pickup": "Pickup",
"RecoverFiveShotFlag": "BonusRestored",
"Free": "Free",
"FreeSR": "Free"
"FreeSR": "Free",
}
for root, dirs, files in os.walk(base_dir):
@ -104,13 +113,19 @@ class CardMakerReader(BaseReader):
with open(f"{root}/{dir}/Gacha.xml", "r", encoding="utf-8") as f:
troot = ET.fromstring(f.read())
name = troot.find('Name').find('str').text
gacha_id = int(troot.find('Name').find('id').text)
name = troot.find("Name").find("str").text
gacha_id = int(troot.find("Name").find("id").text)
# skip already existing gachas
if self.ongeki_data.static.get_gacha(
OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, gacha_id) is not None:
self.logger.info(f"Gacha {gacha_id} already added, skipping")
if (
self.ongeki_data.static.get_gacha(
OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY, gacha_id
)
is not None
):
self.logger.info(
f"Gacha {gacha_id} already added, skipping"
)
continue
# 1140 is the first bright memory gacha
@ -120,7 +135,8 @@ class CardMakerReader(BaseReader):
version = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY
gacha_kind = OngekiConstants.CM_GACHA_KINDS[
type_to_kind[troot.find('Type').text]].value
type_to_kind[troot.find("Type").text]
].value
# hardcode which gachas get "Select Gacha" with 33 points
is_ceiling, max_select_point = 0, 0
@ -134,5 +150,6 @@ class CardMakerReader(BaseReader):
name,
gacha_kind,
isCeiling=is_ceiling,
maxSelectPoint=max_select_point)
maxSelectPoint=max_select_point,
)
self.logger.info(f"Added gacha {gacha_id}")

View File

@ -7,4 +7,4 @@ index = CxbServlet
database = CxbData
reader = CxbReader
game_codes = [CxbConstants.GAME_CODE]
current_schema_version = 1
current_schema_version = 1

View File

@ -11,82 +11,91 @@ from titles.cxb.config import CxbConfig
from titles.cxb.const import CxbConstants
from titles.cxb.database import CxbData
class CxbBase():
class CxbBase:
def __init__(self, cfg: CoreConfig, game_cfg: CxbConfig) -> None:
self.config = cfg # Config file
self.config = cfg # Config file
self.game_config = game_cfg
self.data = CxbData(cfg) # Database
self.data = CxbData(cfg) # Database
self.game = CxbConstants.GAME_CODE
self.logger = logging.getLogger("cxb")
self.version = CxbConstants.VER_CROSSBEATS_REV
def handle_action_rpreq_request(self, data: Dict) -> Dict:
return({})
return {}
def handle_action_hitreq_request(self, data: Dict) -> Dict:
return({"data":[]})
return {"data": []}
def handle_auth_usercheck_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile_index(0, data["usercheck"]["authid"], self.version)
profile = self.data.profile.get_profile_index(
0, data["usercheck"]["authid"], self.version
)
if profile is not None:
self.logger.info(f"User {data['usercheck']['authid']} has CXB profile")
return({"exist": "true", "logout": "true"})
return {"exist": "true", "logout": "true"}
self.logger.info(f"No profile for aime id {data['usercheck']['authid']}")
return({"exist": "false", "logout": "true"})
return {"exist": "false", "logout": "true"}
def handle_auth_entry_request(self, data: Dict) -> Dict:
self.logger.info(f"New profile for {data['entry']['authid']}")
return({"token": data["entry"]["authid"], "uid": data["entry"]["authid"]})
return {"token": data["entry"]["authid"], "uid": data["entry"]["authid"]}
def handle_auth_login_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile_index(0, data["login"]["authid"], self.version)
profile = self.data.profile.get_profile_index(
0, data["login"]["authid"], self.version
)
if profile is not None:
self.logger.info(f"Login user {data['login']['authid']}")
return({"token": data["login"]["authid"], "uid": data["login"]["authid"]})
return {"token": data["login"]["authid"], "uid": data["login"]["authid"]}
self.logger.warn(f"User {data['login']['authid']} does not have a profile")
return({})
return {}
def handle_action_loadrange_request(self, data: Dict) -> Dict:
range_start = data['loadrange']['range'][0]
range_end = data['loadrange']['range'][1]
uid = data['loadrange']['uid']
range_start = data["loadrange"]["range"][0]
range_end = data["loadrange"]["range"][1]
uid = data["loadrange"]["uid"]
self.logger.info(f"Load data for {uid}")
profile = self.data.profile.get_profile(uid, self.version)
songs = self.data.score.get_best_scores(uid)
songs = self.data.score.get_best_scores(uid)
data1 = []
index = []
versionindex = []
for profile_index in profile:
profile_data = profile_index["data"]
if int(range_start) == 800000:
return({"index":range_start, "data":[], "version":10400})
if not ( int(range_start) <= int(profile_index[3]) <= int(range_end) ):
return {"index": range_start, "data": [], "version": 10400}
if not (int(range_start) <= int(profile_index[3]) <= int(range_end)):
continue
#Prevent loading of the coupons within the profile to use the force unlock instead
# Prevent loading of the coupons within the profile to use the force unlock instead
elif 500 <= int(profile_index[3]) <= 510:
continue
#Prevent loading of songs saved in the profile
# Prevent loading of songs saved in the profile
elif 100000 <= int(profile_index[3]) <= 110000:
continue
#Prevent loading of the shop list / unlocked titles & icons saved in the profile
# Prevent loading of the shop list / unlocked titles & icons saved in the profile
elif 200000 <= int(profile_index[3]) <= 210000:
continue
#Prevent loading of stories in the profile
# Prevent loading of stories in the profile
elif 900000 <= int(profile_index[3]) <= 900200:
continue
else:
index.append(profile_index[3])
data1.append(b64encode(bytes(json.dumps(profile_data, separators=(',', ':')), 'utf-8')).decode('utf-8'))
data1.append(
b64encode(
bytes(json.dumps(profile_data, separators=(",", ":")), "utf-8")
).decode("utf-8")
)
'''
"""
100000 = Songs
200000 = Shop
300000 = Courses
@ -96,101 +105,140 @@ class CxbBase():
700000 = rcLog
800000 = Partners
900000 = Stories
'''
"""
# Coupons
for i in range(500,510):
for i in range(500, 510):
index.append(str(i))
couponid = int(i) - 500
dataValue = [{
"couponId":str(couponid),
"couponNum":"1",
"couponLog":[],
}]
data1.append(b64encode(bytes(json.dumps(dataValue[0], separators=(',', ':')), 'utf-8')).decode('utf-8'))
dataValue = [
{
"couponId": str(couponid),
"couponNum": "1",
"couponLog": [],
}
]
data1.append(
b64encode(
bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8")
).decode("utf-8")
)
# ShopList_Title
for i in range(200000,201451):
for i in range(200000, 201451):
index.append(str(i))
shopid = int(i) - 200000
dataValue = [{
"shopId":shopid,
"shopState":"2",
"isDisable":"t",
"isDeleted":"f",
"isSpecialFlag":"f"
}]
data1.append(b64encode(bytes(json.dumps(dataValue[0], separators=(',', ':')), 'utf-8')).decode('utf-8'))
dataValue = [
{
"shopId": shopid,
"shopState": "2",
"isDisable": "t",
"isDeleted": "f",
"isSpecialFlag": "f",
}
]
data1.append(
b64encode(
bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8")
).decode("utf-8")
)
#ShopList_Icon
for i in range(202000,202264):
# ShopList_Icon
for i in range(202000, 202264):
index.append(str(i))
shopid = int(i) - 200000
dataValue = [{
"shopId":shopid,
"shopState":"2",
"isDisable":"t",
"isDeleted":"f",
"isSpecialFlag":"f"
}]
data1.append(b64encode(bytes(json.dumps(dataValue[0], separators=(',', ':')), 'utf-8')).decode('utf-8'))
dataValue = [
{
"shopId": shopid,
"shopState": "2",
"isDisable": "t",
"isDeleted": "f",
"isSpecialFlag": "f",
}
]
data1.append(
b64encode(
bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8")
).decode("utf-8")
)
#Stories
for i in range(900000,900003):
# Stories
for i in range(900000, 900003):
index.append(str(i))
storyid = int(i) - 900000
dataValue = [{
"storyId":storyid,
"unlockState1":["t"] * 10,
"unlockState2":["t"] * 10,
"unlockState3":["t"] * 10,
"unlockState4":["t"] * 10,
"unlockState5":["t"] * 10,
"unlockState6":["t"] * 10,
"unlockState7":["t"] * 10,
"unlockState8":["t"] * 10,
"unlockState9":["t"] * 10,
"unlockState10":["t"] * 10,
"unlockState11":["t"] * 10,
"unlockState12":["t"] * 10,
"unlockState13":["t"] * 10,
"unlockState14":["t"] * 10,
"unlockState15":["t"] * 10,
"unlockState16":["t"] * 10
}]
data1.append(b64encode(bytes(json.dumps(dataValue[0], separators=(',', ':')), 'utf-8')).decode('utf-8'))
dataValue = [
{
"storyId": storyid,
"unlockState1": ["t"] * 10,
"unlockState2": ["t"] * 10,
"unlockState3": ["t"] * 10,
"unlockState4": ["t"] * 10,
"unlockState5": ["t"] * 10,
"unlockState6": ["t"] * 10,
"unlockState7": ["t"] * 10,
"unlockState8": ["t"] * 10,
"unlockState9": ["t"] * 10,
"unlockState10": ["t"] * 10,
"unlockState11": ["t"] * 10,
"unlockState12": ["t"] * 10,
"unlockState13": ["t"] * 10,
"unlockState14": ["t"] * 10,
"unlockState15": ["t"] * 10,
"unlockState16": ["t"] * 10,
}
]
data1.append(
b64encode(
bytes(json.dumps(dataValue[0], separators=(",", ":")), "utf-8")
).decode("utf-8")
)
for song in songs:
song_data = song["data"]
songCode = []
songCode.append({
"mcode": song_data['mcode'],
"musicState": song_data['musicState'],
"playCount": song_data['playCount'],
"totalScore": song_data['totalScore'],
"highScore": song_data['highScore'],
"everHighScore": song_data['everHighScore'] if 'everHighScore' in song_data else ["0","0","0","0","0"],
"clearRate": song_data['clearRate'],
"rankPoint": song_data['rankPoint'],
"normalCR": song_data['normalCR'] if 'normalCR' in song_data else ["0","0","0","0","0"],
"survivalCR": song_data['survivalCR'] if 'survivalCR' in song_data else ["0","0","0","0","0"],
"ultimateCR": song_data['ultimateCR'] if 'ultimateCR' in song_data else ["0","0","0","0","0"],
"nohopeCR": song_data['nohopeCR'] if 'nohopeCR' in song_data else ["0","0","0","0","0"],
"combo": song_data['combo'],
"coupleUserId": song_data['coupleUserId'],
"difficulty": song_data['difficulty'],
"isFullCombo": song_data['isFullCombo'],
"clearGaugeType": song_data['clearGaugeType'],
"fieldType": song_data['fieldType'],
"gameType": song_data['gameType'],
"grade": song_data['grade'],
"unlockState": song_data['unlockState'],
"extraState": song_data['extraState']
})
index.append(song_data['index'])
data1.append(b64encode(bytes(json.dumps(songCode[0], separators=(',', ':')), 'utf-8')).decode('utf-8'))
songCode.append(
{
"mcode": song_data["mcode"],
"musicState": song_data["musicState"],
"playCount": song_data["playCount"],
"totalScore": song_data["totalScore"],
"highScore": song_data["highScore"],
"everHighScore": song_data["everHighScore"]
if "everHighScore" in song_data
else ["0", "0", "0", "0", "0"],
"clearRate": song_data["clearRate"],
"rankPoint": song_data["rankPoint"],
"normalCR": song_data["normalCR"]
if "normalCR" in song_data
else ["0", "0", "0", "0", "0"],
"survivalCR": song_data["survivalCR"]
if "survivalCR" in song_data
else ["0", "0", "0", "0", "0"],
"ultimateCR": song_data["ultimateCR"]
if "ultimateCR" in song_data
else ["0", "0", "0", "0", "0"],
"nohopeCR": song_data["nohopeCR"]
if "nohopeCR" in song_data
else ["0", "0", "0", "0", "0"],
"combo": song_data["combo"],
"coupleUserId": song_data["coupleUserId"],
"difficulty": song_data["difficulty"],
"isFullCombo": song_data["isFullCombo"],
"clearGaugeType": song_data["clearGaugeType"],
"fieldType": song_data["fieldType"],
"gameType": song_data["gameType"],
"grade": song_data["grade"],
"unlockState": song_data["unlockState"],
"extraState": song_data["extraState"],
}
)
index.append(song_data["index"])
data1.append(
b64encode(
bytes(json.dumps(songCode[0], separators=(",", ":")), "utf-8")
).decode("utf-8")
)
for v in index:
try:
@ -198,66 +246,81 @@ class CxbBase():
v_profile_data = v_profile["data"]
versionindex.append(int(v_profile_data["appVersion"]))
except:
versionindex.append('10400')
versionindex.append("10400")
return({"index":index, "data":data1, "version":versionindex})
return {"index": index, "data": data1, "version": versionindex}
def handle_action_saveindex_request(self, data: Dict) -> Dict:
save_data = data['saveindex']
save_data = data["saveindex"]
try:
#REV Omnimix Version Fetcher
gameversion = data['saveindex']['data'][0][2]
# REV Omnimix Version Fetcher
gameversion = data["saveindex"]["data"][0][2]
self.logger.warning(f"Game Version is {gameversion}")
except:
pass
if "10205" in gameversion:
self.logger.info(f"Saving CrossBeats REV profile for {data['saveindex']['uid']}")
#Alright.... time to bring the jank code
for value in data['saveindex']['data']:
if 'playedUserId' in value[1]:
self.data.profile.put_profile(data['saveindex']['uid'], self.version, value[0], value[1])
if 'mcode' not in value[1]:
self.data.profile.put_profile(data['saveindex']['uid'], self.version, value[0], value[1])
if 'shopId' in value:
continue
if 'mcode' in value[1] and 'musicState' in value[1]:
song_json = json.loads(value[1])
songCode = []
songCode.append({
"mcode": song_json['mcode'],
"musicState": song_json['musicState'],
"playCount": song_json['playCount'],
"totalScore": song_json['totalScore'],
"highScore": song_json['highScore'],
"clearRate": song_json['clearRate'],
"rankPoint": song_json['rankPoint'],
"combo": song_json['combo'],
"coupleUserId": song_json['coupleUserId'],
"difficulty": song_json['difficulty'],
"isFullCombo": song_json['isFullCombo'],
"clearGaugeType": song_json['clearGaugeType'],
"fieldType": song_json['fieldType'],
"gameType": song_json['gameType'],
"grade": song_json['grade'],
"unlockState": song_json['unlockState'],
"extraState": song_json['extraState'],
"index": value[0]
})
self.data.score.put_best_score(data['saveindex']['uid'], song_json['mcode'], self.version, value[0], songCode[0])
return({})
else:
self.logger.info(f"Saving CrossBeats REV Sunrise profile for {data['saveindex']['uid']}")
#Sunrise
if "10205" in gameversion:
self.logger.info(
f"Saving CrossBeats REV profile for {data['saveindex']['uid']}"
)
# Alright.... time to bring the jank code
for value in data["saveindex"]["data"]:
if "playedUserId" in value[1]:
self.data.profile.put_profile(
data["saveindex"]["uid"], self.version, value[0], value[1]
)
if "mcode" not in value[1]:
self.data.profile.put_profile(
data["saveindex"]["uid"], self.version, value[0], value[1]
)
if "shopId" in value:
continue
if "mcode" in value[1] and "musicState" in value[1]:
song_json = json.loads(value[1])
songCode = []
songCode.append(
{
"mcode": song_json["mcode"],
"musicState": song_json["musicState"],
"playCount": song_json["playCount"],
"totalScore": song_json["totalScore"],
"highScore": song_json["highScore"],
"clearRate": song_json["clearRate"],
"rankPoint": song_json["rankPoint"],
"combo": song_json["combo"],
"coupleUserId": song_json["coupleUserId"],
"difficulty": song_json["difficulty"],
"isFullCombo": song_json["isFullCombo"],
"clearGaugeType": song_json["clearGaugeType"],
"fieldType": song_json["fieldType"],
"gameType": song_json["gameType"],
"grade": song_json["grade"],
"unlockState": song_json["unlockState"],
"extraState": song_json["extraState"],
"index": value[0],
}
)
self.data.score.put_best_score(
data["saveindex"]["uid"],
song_json["mcode"],
self.version,
value[0],
songCode[0],
)
return {}
else:
self.logger.info(
f"Saving CrossBeats REV Sunrise profile for {data['saveindex']['uid']}"
)
# Sunrise
try:
profileIndex = save_data['index'].index('0')
profileIndex = save_data["index"].index("0")
except:
return({"data":""}) #Maybe
return {"data": ""} # Maybe
profile = json.loads(save_data["data"][profileIndex])
aimeId = profile["aimeId"]
@ -265,65 +328,91 @@ class CxbBase():
for index, value in enumerate(data["saveindex"]["data"]):
if int(data["saveindex"]["index"][index]) == 101:
self.data.profile.put_profile(aimeId, self.version, data["saveindex"]["index"][index], value)
if int(data["saveindex"]["index"][index]) >= 700000 and int(data["saveindex"]["index"][index])<= 701000:
self.data.profile.put_profile(aimeId, self.version, data["saveindex"]["index"][index], value)
if int(data["saveindex"]["index"][index]) >= 500 and int(data["saveindex"]["index"][index]) <= 510:
self.data.profile.put_profile(aimeId, self.version, data["saveindex"]["index"][index], value)
if 'playedUserId' in value:
self.data.profile.put_profile(aimeId, self.version, data["saveindex"]["index"][index], json.loads(value))
if 'mcode' not in value and "normalCR" not in value:
self.data.profile.put_profile(aimeId, self.version, data["saveindex"]["index"][index], json.loads(value))
if 'shopId' in value:
self.data.profile.put_profile(
aimeId, self.version, data["saveindex"]["index"][index], value
)
if (
int(data["saveindex"]["index"][index]) >= 700000
and int(data["saveindex"]["index"][index]) <= 701000
):
self.data.profile.put_profile(
aimeId, self.version, data["saveindex"]["index"][index], value
)
if (
int(data["saveindex"]["index"][index]) >= 500
and int(data["saveindex"]["index"][index]) <= 510
):
self.data.profile.put_profile(
aimeId, self.version, data["saveindex"]["index"][index], value
)
if "playedUserId" in value:
self.data.profile.put_profile(
aimeId,
self.version,
data["saveindex"]["index"][index],
json.loads(value),
)
if "mcode" not in value and "normalCR" not in value:
self.data.profile.put_profile(
aimeId,
self.version,
data["saveindex"]["index"][index],
json.loads(value),
)
if "shopId" in value:
continue
# MusicList Index for the profile
indexSongList = []
for value in data["saveindex"]["index"]:
if int(value) in range(100000,110000):
if int(value) in range(100000, 110000):
indexSongList.append(value)
for index, value in enumerate(data["saveindex"]["data"]):
if 'mcode' not in value:
if "mcode" not in value:
continue
if 'playedUserId' in value:
if "playedUserId" in value:
continue
data1 = json.loads(value)
songCode = []
songCode.append({
"mcode": data1['mcode'],
"musicState": data1['musicState'],
"playCount": data1['playCount'],
"totalScore": data1['totalScore'],
"highScore": data1['highScore'],
"everHighScore": data1['everHighScore'],
"clearRate": data1['clearRate'],
"rankPoint": data1['rankPoint'],
"normalCR": data1['normalCR'],
"survivalCR": data1['survivalCR'],
"ultimateCR": data1['ultimateCR'],
"nohopeCR": data1['nohopeCR'],
"combo": data1['combo'],
"coupleUserId": data1['coupleUserId'],
"difficulty": data1['difficulty'],
"isFullCombo": data1['isFullCombo'],
"clearGaugeType": data1['clearGaugeType'],
"fieldType": data1['fieldType'],
"gameType": data1['gameType'],
"grade": data1['grade'],
"unlockState": data1['unlockState'],
"extraState": data1['extraState'],
"index": indexSongList[i]
})
songCode.append(
{
"mcode": data1["mcode"],
"musicState": data1["musicState"],
"playCount": data1["playCount"],
"totalScore": data1["totalScore"],
"highScore": data1["highScore"],
"everHighScore": data1["everHighScore"],
"clearRate": data1["clearRate"],
"rankPoint": data1["rankPoint"],
"normalCR": data1["normalCR"],
"survivalCR": data1["survivalCR"],
"ultimateCR": data1["ultimateCR"],
"nohopeCR": data1["nohopeCR"],
"combo": data1["combo"],
"coupleUserId": data1["coupleUserId"],
"difficulty": data1["difficulty"],
"isFullCombo": data1["isFullCombo"],
"clearGaugeType": data1["clearGaugeType"],
"fieldType": data1["fieldType"],
"gameType": data1["gameType"],
"grade": data1["grade"],
"unlockState": data1["unlockState"],
"extraState": data1["extraState"],
"index": indexSongList[i],
}
)
self.data.score.put_best_score(aimeId, data1['mcode'], self.version, indexSongList[i], songCode[0])
self.data.score.put_best_score(
aimeId, data1["mcode"], self.version, indexSongList[i], songCode[0]
)
i += 1
return({})
return {}
def handle_action_sprankreq_request(self, data: Dict) -> Dict:
uid = data['sprankreq']['uid']
uid = data["sprankreq"]["uid"]
self.logger.info(f"Get best rankings for {uid}")
p = self.data.score.get_best_rankings(uid)
@ -331,90 +420,122 @@ class CxbBase():
for rank in p:
if rank["song_id"] is not None:
rankList.append({
"sc": [rank["score"],rank["song_id"]],
"rid": rank["rev_id"],
"clear": rank["clear"]
})
rankList.append(
{
"sc": [rank["score"], rank["song_id"]],
"rid": rank["rev_id"],
"clear": rank["clear"],
}
)
else:
rankList.append({
"sc": [rank["score"]],
"rid": rank["rev_id"],
"clear": rank["clear"]
})
rankList.append(
{
"sc": [rank["score"]],
"rid": rank["rev_id"],
"clear": rank["clear"],
}
)
return({
return {
"uid": data["sprankreq"]["uid"],
"aid": data["sprankreq"]["aid"],
"rank": rankList,
"rankx":[1,1,1]
})
"rankx": [1, 1, 1],
}
def handle_action_getadv_request(self, data: Dict) -> Dict:
return({"data":[{"r":"1","i":"100300","c":"20"}]})
return {"data": [{"r": "1", "i": "100300", "c": "20"}]}
def handle_action_getmsg_request(self, data: Dict) -> Dict:
return({"msgs":[]})
return {"msgs": []}
def handle_auth_logout_request(self, data: Dict) -> Dict:
return({"auth":True})
def handle_action_rankreg_request(self, data: Dict) -> Dict:
uid = data['rankreg']['uid']
return {"auth": True}
def handle_action_rankreg_request(self, data: Dict) -> Dict:
uid = data["rankreg"]["uid"]
self.logger.info(f"Put {len(data['rankreg']['data'])} rankings for {uid}")
for rid in data['rankreg']['data']:
#REV S2
for rid in data["rankreg"]["data"]:
# REV S2
if "clear" in rid:
try:
self.data.score.put_ranking(user_id=uid, rev_id=int(rid["rid"]), song_id=int(rid["sc"][1]), score=int(rid["sc"][0]), clear=rid["clear"])
self.data.score.put_ranking(
user_id=uid,
rev_id=int(rid["rid"]),
song_id=int(rid["sc"][1]),
score=int(rid["sc"][0]),
clear=rid["clear"],
)
except:
self.data.score.put_ranking(user_id=uid, rev_id=int(rid["rid"]), song_id=0, score=int(rid["sc"][0]), clear=rid["clear"])
#REV
self.data.score.put_ranking(
user_id=uid,
rev_id=int(rid["rid"]),
song_id=0,
score=int(rid["sc"][0]),
clear=rid["clear"],
)
# REV
else:
try:
self.data.score.put_ranking(user_id=uid, rev_id=int(rid["rid"]), song_id=int(rid["sc"][1]), score=int(rid["sc"][0]), clear=0)
self.data.score.put_ranking(
user_id=uid,
rev_id=int(rid["rid"]),
song_id=int(rid["sc"][1]),
score=int(rid["sc"][0]),
clear=0,
)
except:
self.data.score.put_ranking(user_id=uid, rev_id=int(rid["rid"]), song_id=0, score=int(rid["sc"][0]), clear=0)
return({})
self.data.score.put_ranking(
user_id=uid,
rev_id=int(rid["rid"]),
song_id=0,
score=int(rid["sc"][0]),
clear=0,
)
return {}
def handle_action_addenergy_request(self, data: Dict) -> Dict:
uid = data['addenergy']['uid']
uid = data["addenergy"]["uid"]
self.logger.info(f"Add energy to user {uid}")
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:
if not p:
self.data.item.put_energy(uid, 5)
return({
return {
"class": data1["myClass"],
"granted": "5",
"total": "5",
"threshold": "1000"
})
"threshold": "1000",
}
array = []
newenergy = int(energy) + 5
self.data.item.put_energy(uid, newenergy)
if int(energy) <= 995:
array.append({
"class": data1["myClass"],
"granted": "5",
"total": str(energy),
"threshold": "1000"
})
array.append(
{
"class": data1["myClass"],
"granted": "5",
"total": str(energy),
"threshold": "1000",
}
)
else:
array.append({
"class": data1["myClass"],
"granted": "0",
"total": str(energy),
"threshold": "1000"
})
array.append(
{
"class": data1["myClass"],
"granted": "0",
"total": str(energy),
"threshold": "1000",
}
)
return array[0]
def handle_action_eventreq_request(self, data: Dict) -> Dict:

View File

@ -1,40 +1,60 @@
from core.config import CoreConfig
class CxbServerConfig():
class CxbServerConfig:
def __init__(self, parent_config: "CxbConfig"):
self.__config = parent_config
@property
def enable(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'enable', default=True)
return CoreConfig.get_config_field(
self.__config, "cxb", "server", "enable", default=True
)
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'loglevel', default="info"))
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "cxb", "server", "loglevel", default="info"
)
)
@property
def hostname(self) -> str:
return CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'hostname', default="localhost")
return CoreConfig.get_config_field(
self.__config, "cxb", "server", "hostname", default="localhost"
)
@property
def ssl_enable(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'ssl_enable', default=False)
return CoreConfig.get_config_field(
self.__config, "cxb", "server", "ssl_enable", default=False
)
@property
def port(self) -> int:
return CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'port', default=8082)
return CoreConfig.get_config_field(
self.__config, "cxb", "server", "port", default=8082
)
@property
def port_secure(self) -> int:
return CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'port_secure', default=443)
return CoreConfig.get_config_field(
self.__config, "cxb", "server", "port_secure", default=443
)
@property
def ssl_cert(self) -> str:
return CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'ssl_cert', default="cert/title.crt")
return CoreConfig.get_config_field(
self.__config, "cxb", "server", "ssl_cert", default="cert/title.crt"
)
@property
def ssl_key(self) -> str:
return CoreConfig.get_config_field(self.__config, 'cxb', 'server', 'ssl_key', default="cert/title.key")
return CoreConfig.get_config_field(
self.__config, "cxb", "server", "ssl_key", default="cert/title.key"
)
class CxbConfig(dict):
def __init__(self) -> None:

View File

@ -1,4 +1,4 @@
class CxbConstants():
class CxbConstants:
GAME_CODE = "SDCA"
CONFIG_NAME = "cxb.yaml"
@ -8,8 +8,13 @@ class CxbConstants():
VER_CROSSBEATS_REV_SUNRISE_S2 = 2
VER_CROSSBEATS_REV_SUNRISE_S2_OMNI = 3
VERSION_NAMES = ("crossbeats REV.", "crossbeats REV. SUNRISE", "crossbeats REV. SUNRISE S2", "crossbeats REV. SUNRISE S2 Omnimix")
VERSION_NAMES = (
"crossbeats REV.",
"crossbeats REV. SUNRISE",
"crossbeats REV. SUNRISE S2",
"crossbeats REV. SUNRISE S2 Omnimix",
)
@classmethod
def game_ver_to_string(cls, ver: int):
return cls.VERSION_NAMES[ver]
return cls.VERSION_NAMES[ver]

474
titles/cxb/data/Export.csv Normal file
View File

@ -0,0 +1,474 @@
index,mcode,name,artist,category,easy,standard,hard,master,unlimited,
100000,tutori2,Tutorial,Tutorial,Unknown,Easy N/A,Standard N/A,Hard N/A,Master N/A,Unlimited N/A,
100000,tutori3,Tutorial,Tutorial,Unknown,Easy N/A,Standard N/A,Hard N/A,Master N/A,Unlimited N/A,
100000,tutori4,Tutorial,Tutorial,Pick-Up J-Pop (New),Easy N/A,Standard N/A,Hard N/A,Master N/A,Unlimited N/A,
100000,tutori6,Tutorial,Tutorial,Pick-Up J-Pop (New),Easy N/A,Standard N/A,Hard N/A,Master N/A,Unlimited N/A,
100000,tutori8,白鳥の湖 (Short Remix),,Original,Easy N/A,Standard 3,Hard 15,Master 35,Unlimited N/A,
100300,sateli,Satellite System ft.Diana Chiaki,GRATEC MOUR,Original,Easy 17,Standard 28,Hard 49,Master 77,Unlimited 82,
100301,nature,Human Nature,Z pinkpong,Original,Easy 5,Standard 14,Hard 24,Master 53,Unlimited 75,
100307,purple,DEEP PURPLE,NAOKI,Original,Easy 14,Standard 22,Hard 54,Master 64,Unlimited 73,
100308,hearts,Heartstrings,Nhato,Original,Easy 8,Standard 18,Hard 38,Master 68,Unlimited 77,
100310,phasea,Phase Angel,OCOT,Original,Easy 9,Standard 16,Hard 38,Master 65,Unlimited 75,
100311,planet,Planet Calling,Nyolfen,Original,Easy 10,Standard 17,Hard 36,Master 49,Unlimited 71,
100314,firefo,Firefox,Go-qualia,Original,Easy 7,Standard 13,Hard 36,Master 57,Unlimited 83,
100315,kounen,光年konen,小野秀幸,Original,Easy 10,Standard 21,Hard 40,Master 66,Unlimited 78,
100316,essenc,Another Essence,RAM,Original,Easy 11,Standard 25,Hard 50,Master 70,Unlimited 76,
100317,summer,Summer End Anthem,Personative,Original,Easy 13,Standard 23,Hard 57,Master 79,Unlimited 89,
100319,tanosi,たのしいことだけ,Yamajet,Original,Easy 16,Standard 25,Hard 45,Master 70,Unlimited 80,
100320,picora,ピコラセテ,TORIENA,Original,Easy 8,Standard 15,Hard 38,Master 66,Unlimited 75,
100323,devils,Devil's Classic,Tatsh,Original,Easy 15,Standard 27,Hard 40,Master 80,Unlimited N/A,
100328,techno,Techno Highway,SIMON,Original,Easy 9,Standard 16,Hard 38,Master 51,Unlimited 74,
100335,glowww,GLOW,Shoichiro Hirata feat. Ellie,Original,Easy 8,Standard 17,Hard 28,Master 42,Unlimited 60,
100336,powerr,Power,Dubscribe,Original,Easy 12,Standard 19,Hard 38,Master 69,Unlimited 79,
100340,amater,Amateras,Sakuzyo,Original,Easy 13,Standard 21,Hard 48,Master 65,Unlimited 79,
100349,advers,Adverse Effect,Rin,Original,Easy 9,Standard 15,Hard 48,Master 71,Unlimited 83,
100353,venera,Venerated,Tosh,Original,Easy 8,Standard 15,Hard 43,Master 68,Unlimited 75,
100357,dazaii,堕罪,HAKKYOU-KUN feat.せつな,Original,Easy 12,Standard 21,Hard 43,Master 73,Unlimited 77,
100365,thesig,The Signs Of The Last Day,SLAKE,Original,Easy 10,Standard 21,Hard 38,Master 56,Unlimited 73,
100344,hosita,星達のメロディ,ゆいこんぬ,Original,Easy 10,Standard 16,Hard 36,Master 48,Unlimited 65,
100372,bluede,Blue Destiny Blue,NAOKI feat. Florence McNair,Original,Easy 12,Standard 22,Hard 41,Master 58,Unlimited 70,
100373,emerao,EMERALD♡KISS Original Side,jun with Aimee,Original,Easy 19,Standard 30,Hard 53,Master 85,Unlimited N/A,
100129,megaro,MEGALOMAN[i]A,TITANZ,Original,Easy 0,Standard 55,Hard 80,Master 93,Unlimited 98,
100330,angeli,angelik-vice,void,Original,Easy 22,Standard 33,Hard 56,Master 82,Unlimited 90,
100342,moonli,月鳴 -moonlit urge-,AZURE FACTORY,Original,Easy 8,Standard 14,Hard 43,Master 61,Unlimited 73,
100369,yumemi,ユメミル船,yozuca*,Original,Easy 6,Standard 12,Hard 35,Master 59,Unlimited 69,
100348,pinkym,Pinky Magic,Junk,Original,Easy 16,Standard 24,Hard 44,Master 74,Unlimited 81,
100370,dynami2,DYNAMITE SENSATION REV.,NAOKI feat. Hyphen,Original,Easy 8,Standard 18,Hard 51,Master 78,Unlimited 80,
100306,reseed3,Reseed (Another Edit),quick master,Original,Easy 10,Standard 20,Hard 55,Master 76,Unlimited 80,
100002,toucho,Touch Of Gold,Togo Project feat. Frances Maya,Original,Easy 5,Standard 9,Hard 28,Master 44,Unlimited 65,
100003,ameoto,雨の音が虹を呼ぶ,Barbarian On The Groove feat.霜月はるか,Original,Easy 6,Standard 12,Hard 26,Master 47,Unlimited 63,
100004,kimito,キミとMUSIC,CooRie,Original,Easy 7,Standard 10,Hard 26,Master 49,Unlimited 66,
100021,giantk,Giant Killing,R-Lab,Original,Easy 11,Standard 25,Hard 53,Master 71,Unlimited 78,
100015,breakd,Break down,GARNiDELiA,Original,Easy 11,Standard 23,Hard 34,Master 57,Unlimited 74,
100028,dazzlj,DAZZLING♡SEASON (Japanese Side),jun,Original,Easy 16,Standard 35,Hard 60,Master 80,Unlimited 90,
100093,ididid,I.D.,Tatsh feat. 彩音,Original,Easy 16,Standard 29,Hard 46,Master 72,Unlimited 81,
100042,sundro,Sundrop,Yamajet,Original,Easy 14,Standard 24,Hard 47,Master 75,Unlimited 83,
100063,auflcb,some day (instrumental),NAOKI,Original,Easy 8,Standard 13,Hard 43,Master 81,Unlimited N/A,
100045,dennou,電脳少女は歌姫の夢を見るか?,デスおはぎ feat.蛮,Original,Easy 15,Standard 29,Hard 60,Master 76,Unlimited 87,
100068,hokoro,ホコロビシロガールズ,むかしばなし,Original,Easy 14,Standard 29,Hard 57,Master 71,Unlimited 81,
100005,landin,Landing on the moon,SIMON,Original,Easy 13,Standard 26,Hard 33,Master 49,Unlimited 67,
100362,tomorr,Tomorrow,桜井零士,Original,Easy 8,Standard 15,Hard 24,Master 44,Unlimited 62,
100363,daybyd,day by day,海辺,Original,Easy 6,Standard 13,Hard 26,Master 38,Unlimited 59,
100309,syoujo,生々世々,SADA,Original,Easy 8,Standard 19,Hard 35,Master 53,Unlimited 78,
100352,destru,Destrudo,D-Fener,Original,Easy 10,Standard 19,Hard 41,Master 62,Unlimited 72,
100041,gingat,Re:Milky way,イトヲカシ,Original,Easy 5,Standard 13,Hard 29,Master 46,Unlimited 61,
100066,daisak,大殺界がらくたシンパシー,まふまふ,Original,Easy 12,Standard 28,Hard 36,Master 60,Unlimited 75,
100376,paradi,Paradise Regained,LC:AZE feat.chakk,Original,Easy 10,Standard 16,Hard 28,Master 53,Unlimited 64,
100377,pigooo,PIG-O,NNNNNNNNNN,Original,Easy 13,Standard 19,Hard 34,Master 59,Unlimited 84,
100386,season,The Four Seasons -SPRING- (Remix Ver.),,Variety,Easy 8,Standard 15,Hard 28,Master 44,Unlimited 65,
100387,canonn,カノン (Remix Ver.),,Variety,Easy 7,Standard 17,Hard 28,Master 50,Unlimited 83,
100388,rhapso,Rhapsody in Blue (Remix Ver.),,Variety,Easy 6,Standard 18,Hard 34,Master 62,Unlimited 74,
100389,turkis,トルコ行進曲 (Remix Ver.),,Variety,Easy 11,Standard 19,Hard 39,Master 64,Unlimited 84,
100390,biohaz,code_,umbrella Cores,Variety,Easy 6,Standard 15,Hard 30,Master 51,Unlimited 64,
100391,monhan,英雄の証 4Version,カプコンサウンドチーム,Variety,Easy 5,Standard 10,Hard 26,Master 36,Unlimited 54,
100392,gyakut2,追求 ~最終プロモーションバージョン (crossbeats REV.アレンジ),岩垂 徳行,Variety,Easy 5,Standard 13,Hard 35,Master 43,Unlimited 56,
100393,street,Theme of Ryu -SFIV Arrange-,Capcom Sound Team / Hideyuki Fukasawa,Variety,Easy 7,Standard 13,Hard 34,Master 47,Unlimited 66,
100394,rockma2,Dr. WILY STAGE 1 -OMEGAMAN MIX-,ROCK-MEN,Variety,Easy 14,Standard 21,Hard 34,Master 49,Unlimited 76,
100374,auflcb3,SOMEDAY -00.prologue-,TЁЯRA,Original,Easy 6,Standard 16,Hard 36,Master 66,Unlimited 86,
100325,irohaa,Iroha,Ryunosuke Kudo,Original,Easy 12,Standard 19,Hard 41,Master 55,Unlimited 76,
100326,ibelie,I Believe Someday,SPARKER,Original,Easy 14,Standard 27,Hard 47,Master 78,Unlimited 82,
100409,monhan2,灼熱の刃 ディノバルド,カプコンサウンドチーム,Variety,Easy 6,Standard 12,Hard 24,Master 43,Unlimited 68,
100410,monhan3,古代の息吹き,カプコンサウンドチーム,Variety,Easy 8,Standard 18,Hard 28,Master 45,Unlimited 73,
100418,yejiii,YEJI,ginkiha,Original,Easy 10,Standard 22,Hard 36,Master 63,Unlimited 79,
100419,histor,HISTORIA,Cranky,Original,Easy 11,Standard 20,Hard 36,Master 56,Unlimited 82,
100338,chaset,Chase the WAVE,Tatsh feat. AKINO with bless4,Original,Easy 8,Standard 15,Hard 31,Master 58,Unlimited 76,
100412,metall,Metallical parade,Vice Principal,Original,Easy 8,Standard 16,Hard 28,Master 57,Unlimited 77,
100327,letmeg,Let Me Give You My Heart,brinq,Original,Easy 12,Standard 18,Hard 32,Master 50,Unlimited 72,
100010,hontno,ホントのワタシ,mao,Original,Easy 9,Standard 12,Hard 26,Master 53,Unlimited 66,
100024,azitat,Azitate,void,Original,Easy 14,Standard 24,Hard 55,Master 70,Unlimited 83,
100360,hellom,Hello Mr.crosbie,民安★ROCK,Original,Easy 7,Standard 18,Hard 37,Master 58,Unlimited 72,
100337,laught,Perfect laughter,ぽんず loved by yksb,Original,Easy 7,Standard 20,Hard 35,Master 51,Unlimited 71,
100426,bluede2,Blue Destiny Blue ETERNAL,NAOKI feat. Florence McNair,Original,Easy 9,Standard 16,Hard 36,Master 56,Unlimited 81,
100423,street2,Ultra Street Fighter IV,Hideyuki Fukasawa,Variety,Easy 14,Standard 21,Hard 37,Master 55,Unlimited 73,
100424,street3,Theme of Chun-Li -SFIV Arrange-,Capcom Sound Team / Hideyuki Fukasawa,Variety,Easy 13,Standard 24,Hard 38,Master 57,Unlimited 74,
100425,street4,Street Fighter V,Masahiro Aoki,Variety,Easy 11,Standard 17,Hard 32,Master 51,Unlimited 78,
100421,silbur,Silbury Sign,カヒーナムジカ,Original,Easy 9,Standard 20,Hard 35,Master 54,Unlimited 75,
100422,spicaa,Spica,Endorfin.,Original,Easy 10,Standard 23,Hard 36,Master 56,Unlimited 78,
100438,tricko,Trick Or Treat,SLAKE,Original,Easy 11,Standard 23,Hard 34,Master 56,Unlimited 75,
100435,thisis,THIS IS HDM,Relect,Original,Easy 12,Standard 23,Hard 37,Master 60,Unlimited 74,
100436,rising,Rising Day ft. Satan,GRATEC MOUR,Original,Easy 14,Standard 23,Hard 38,Master 66,Unlimited 85,
100411,orbita,Orbital velocity,Vice Principal,Original,Easy 12,Standard 20,Hard 38,Master 62,Unlimited 74,
100433,dddddd,D,六弦アリス,Original,Easy 9,Standard 13,Hard 33,Master 53,Unlimited 69,
100427,pyroma,Pyromania,KO3,Original,Easy 8,Standard 22,Hard 42,Master 70,Unlimited 84,
100312,touchn,Touch n Go,Paisley Parks,Original,Easy 15,Standard 27,Hard 46,Master 68,Unlimited 86,
100359,onlyll,only L,emon,Original,Easy 13,Standard 21,Hard 32,Master 56,Unlimited 69,
100313,upside,Upside Down,Nave ft.Mayu Wakisaka,Original,Easy 8,Standard 18,Hard 27,Master 48,Unlimited 67,
100322,istanb,İstanbul,REVen-G,Original,Easy 23,Standard 41,Hard 49,Master 90,Unlimited 98,
100371,memori,Memoria ~終焉を司る荊姫の静粛なる宴~,Astilbe × arendsii,Original,Easy 13,Standard 26,Hard 38,Master 65,Unlimited 84,
100350,straye,Strayer,Taishi,Original,Easy 14,Standard 25,Hard 36,Master 61,Unlimited 73,
100358,rearhy,Rearhythm,CooRie,Original,Easy 7,Standard 17,Hard 32,Master 52,Unlimited 69,
100432,hereco,Here comes the sun ~For you~,Z pinkpong,Original,Easy 11,Standard 16,Hard 34,Master 51,Unlimited 68,
100441,thesun,THE SUN,Tatsh,Original,Easy 13,Standard 25,Hard 40,Master 72,Unlimited 87,
100343,sayona,さよなら最終列車,むかしばなし,Original,Easy 10,Standard 20,Hard 34,Master 53,Unlimited 71,
100380,flameu,Flame Up,Inu Machine,Original,Easy 10,Standard 18,Hard 36,Master 57,Unlimited 65,
100434,raidon,RAiD on Mars,sky_delta,Original,Easy 13,Standard 30,Hard 38,Master 58,Unlimited 87,
100437,riseup,Rise Up,Dubscribe,Original,Easy 7,Standard 18,Hard 41,Master 67,Unlimited 77,
100431,sunglo,Sunglow,Yamajet feat. ひうらまさこ,Original,Easy 10,Standard 18,Hard 39,Master 59,Unlimited 72,
100439,kinbos,金星kinboshi,Hideyuki Ono,Original,Easy 12,Standard 22,Hard 38,Master 64,Unlimited 77,
100430,densho,電脳少女と機械仕掛けの神,Chimera music.,Original,Easy 17,Standard 28,Hard 42,Master 74,Unlimited 90,
100471,aiohoo,愛をほおばりたいッLike a Monkey!,新堂敦士,J-Pop,Easy 10,Standard 18,Hard 29,Master 42,Unlimited 66,
100472,entert,エンターテイナー (Remix ver.),,Variety,Easy 9,Standard 15,Hard 33,Master 54,Unlimited 87,
100457,takeit,Take It Back,Daniel Seven,Original,Easy 12,Standard 20,Hard 38,Master 65,Unlimited 83,
100449,harmon,Harmony,ピクセルビー,Original,Easy 12,Standard 20,Hard 36,Master 49,Unlimited 65,
100428,avemar,アヴェ・マリア (Remix ver.),,Variety,Easy 8,Standard 16,Hard 31,Master 53,Unlimited 75,
100429,mateki,復讐の炎は地獄のように我が心に燃え (Remix ver.),,Variety,Easy 10,Standard 18,Hard 35,Master 54,Unlimited 79,
100445,lovech,LOVE CHASE,大島はるな,Original,Easy 8,Standard 16,Hard 30,Master 46,Unlimited 68,
100473,akaihe,赤いヘッドホン,新堂敦士,J-Pop,Easy 8,Standard 21,Hard 32,Master 50,Unlimited 69,
100474,juicys,Juicy! ~幸せスパイラル~,新堂敦士,J-Pop,Easy 10,Standard 18,Hard 26,Master 45,Unlimited 83,
100468,codena,CODENAMEはEQ,TORIENA,Original,Easy 9,Standard 18,Hard 35,Master 48,Unlimited 68,
100475,groove,LINK LINK FEVER!!!(グルーヴコースター 3 リンクフィーバーより),リンカ CV:豊田萌絵),Variety,Easy 10,Standard 22,Hard 40,Master 52,Unlimited 73,
100450,kansho,観賞用マーメイド,ヤマイ,Original,Easy 7,Standard 13,Hard 27,Master 55,Unlimited 74,
100486,overcl2,Over Clock ~前兆~,NAOKI feat. un∞limited,Original,Easy 12,Standard 23,Hard 42,Master 58,Unlimited 74,
100483,taikoo,SAKURA EXHAUST,RIO HAMAMOTO(BNSI)「太鼓の達人」より,Variety,Easy 6,Standard 13,Hard 39,Master 50,Unlimited 75,
100480,groove2,QLWAグルーヴコースター 3 リンクフィーバーより),t+pazolite,Variety,Easy 9,Standard 15,Hard 40,Master 58,Unlimited 85,
100487,overcl,Over Clock ~開放~,NAOKI feat. un∞limited,Original,Easy 8,Standard 12,Hard 35,Master 57,Unlimited 86,
100466,notoss,Notos,ginkiha,Original,Easy 8,Standard 12,Hard 42,Master 65,Unlimited 91,
100447,machup,マチュ☆ピチュ,コツキミヤ,Original,Easy 9,Standard 17,Hard 32,Master 41,Unlimited 71,
100488,groove3,カリソメ(グルーヴコースター 3 リンクフィーバーより),コンプ(豚乙女) × ichigo(岸田教団 THE明星ロケッツ),Touhou + Variety,Easy 10,Standard 18,Hard 34,Master 64,Unlimited 85,
100489,groove4,そして誰もいなくなった(グルーヴコースター 3 リンクフィーバーより),コバヤシユウヤ(IOSYS) × あにー(TaNaBaTa),Touhou + Variety,Easy 12,Standard 22,Hard 35,Master 50,Unlimited 75,
100482,everyt,EVERYTHING,Tatsh feat.小田ユウ,Original,Easy 13,Standard 22,Hard 30,Master 74,Unlimited N/A,
100465,lespri,L'esprit,Cosine,Original,Easy 13,Standard 25,Hard 57,Master 80,Unlimited N/A,
100491,groove5,グルーヴ・ザ・ハート(グルーヴコースター 3 リンクフィーバーより),ビートまりお+あまね,Variety,Easy 14,Standard 24,Hard 37,Master 67,Unlimited N/A,
100490,honeyo,HONEY♡SUNRiSE Original Side,jun with Aimee,Original,Easy 24,Standard 32,Hard 63,Master 88,Unlimited 93,
100494,groove6,Got hive of Raグルーヴコースター 3 リンクフィーバーより),E.G.G.,Variety,Easy 22,Standard 30,Hard 64,Master 79,Unlimited N/A,
100495,sunglo2,Sunglow (Happy Hardcore Style),Yamajet feat. ひうらまさこ,Original,Easy 11,Standard 21,Hard 36,Master 67,Unlimited 81,
100498,fourte,14th Clock,INNOCENT NOIZE,Original,Easy 14,Standard 24,Hard 50,Master 74,Unlimited 80,
100496,monhan4,英雄の証MHF- 2015 Version,若林タカツグ,Variety,Easy 5,Standard 12,Hard 40,Master 51,Unlimited 62,
100497,monhan5,異ヲ辿リシモノ -対峙-,若林タカツグ,Variety,Easy 10,Standard 12,Hard 35,Master 42,Unlimited 65,
100504,darkpa,Dark Parashu,INNOCENT NOIZE,Original,Easy 16,Standard 26,Hard 39,Master 70,Unlimited 84,
100505,hervor,Hervor,INNOCENT NOIZE,Original,Easy 18,Standard 28,Hard 39,Master 73,Unlimited 81,
100499,cirnon,チルノのパーフェクトさんすう教室,ARM夕野ヨシミ (IOSYS) feat. miko,Touhou,Easy 17,Standard 24,Hard 40,Master 60,Unlimited 79,
100500,marisa,魔理沙は大変なものを盗んでいきました,ARM夕野ヨシミ (IOSYS) feat. 藤咲かりん,Touhou,Easy 18,Standard 25,Hard 41,Master 62,Unlimited 85,
100501,yakini,究極焼肉レストラン!お燐の地獄亭!,ARM夕野ヨシミ (IOSYS) feat. 藤枝あかね,Touhou,Easy 13,Standard 25,Hard 34,Master 58,Unlimited 82,
100502,justic,ジャスティス・オブ・ザ・界隈 ~ALL IS FAIR IN LOVE AND ALIMARI~,void (IOSYS) feat.山本椛,Touhou,Easy 14,Standard 19,Hard 36,Master 57,Unlimited 83,
100503,sintyo,進捗どうですか?,sumijun feat.ななひら,Touhou,Easy 16,Standard 25,Hard 46,Master 70,Unlimited 83,
100347,ascand,Ascendanz,void,Original,Easy 18,Standard 32,Hard 54,Master 80,Unlimited 90,
100506,blackl,Black Lotus,Maozon,Original,Easy 12,Standard 19,Hard 41,Master 73,Unlimited 84,
100043,childr,チルドレン・オートマトン~ある歌声の亡霊~,あさまっく,Original,Easy 14,Standard 24,Hard 39,Master 56,Unlimited 62,
100044,tsukai,ツカイステ・デッドワールド,コゲ犬×ゆちゃ,Original,Easy 13,Standard 19,Hard 44,Master 72,Unlimited 76,
100067,rideon,RIDE ON NOW,さつき が てんこもり feat.un:c,Original,Easy 16,Standard 29,Hard 41,Master 60,Unlimited 80,
100507,minest,Minestrone,orangentle,Original,Easy 13,Standard 21,Hard 39,Master 62,Unlimited 76,
100508,ordine,Ordine,orangentle,Original,Easy 19,Standard 25,Hard 43,Master 73,Unlimited 82,
100509,dreamw,DReamWorKer,LC:AZE,Original,Easy 16,Standard 26,Hard 37,Master 62,Unlimited 75,
100510,minerv,Minerva,xi,Original,Easy 25,Standard 32,Hard 61,Master 90,Unlimited N/A,
100001,wannab,Wanna Be Your Special,Shoichiro Hirata feat. SUIMI,Original,Easy 5,Standard 9,Hard 23,Master 40,Unlimited 65,
100511,sekain,世界の果て,Yamajet,Original,Easy 16,Standard 26,Hard 39,Master 69,Unlimited 78,
100512,farawa,Faraway,ミフメイ,Original,Easy 18,Standard 23,Hard 36,Master 60,Unlimited 76,
100100,crissc,Crisscrosser,void,Original,Easy 17,Standard 37,Hard 63,Master 86,Unlimited 91,
100324,speedy,Awake Speedy,DJ MURASAME,Original,Easy 11,Standard 22,Hard 55,Master 77,Unlimited N/A,
100513,xxxrev,XXX-revolt,void feat. KOTOKO,Original,Easy 15,Standard 21,Hard 34,Master 56,Unlimited 73,
100016,higame,Hi,Go-qualia,Original,Easy 13,Standard 20,Hard 30,Master 58,Unlimited 71,
100022,theepi,The Epic,Cranky,Original,Easy 14,Standard 19,Hard 40,Master 61,Unlimited 75,
100023,anomie,Anomie,D-Fener,Original,Easy 15,Standard 22,Hard 38,Master 61,Unlimited 77,
100524,crocus,Crocus,村瀬悠太,Original,Easy 15,Standard 26,Hard 37,Master 60,Unlimited 72,
100546,lavien,La vie en Fleurs,VILA,Original,Easy 18,Standard 27,Hard 41,Master 71,Unlimited 80,
100361,megaro2,MEGALOMAN[i]A -2nd IMPACT-,NEO-G,Original,Easy N/A,Standard N/A,Hard N/A,Master 99,Unlimited 100,
100541,chipnn,Chip Notch Educ@tion,yaseta feat. chip_Notch,Original,Easy 16,Standard 27,Hard 34,Master 61,Unlimited 79,
100007,yiyoyi,Wanyo Wanyo,MC Natsack,Original,Easy 7,Standard 14,Hard 33,Master 56,Unlimited 70,
100014,binary,Binary Overdrive,フラット3rd,Original,Easy 14,Standard 17,Hard 35,Master 64,Unlimited 89,
100054,makaim,魔界村 (平地BGM),Remixed by ARM (IOSYS),Original + Variety,Easy 23,Standard 30,Hard 50,Master 77,Unlimited N/A,
100055,gyakut,逆転裁判 (綾里真宵 ~逆転姉妹のテーマ),Remixed by OSTER project,Original + Variety,Easy 6,Standard 15,Hard 21,Master 46,Unlimited 64,
100056,basara,戦国BASARA (SENGOKU BASARA),Remixed by SOUND HOLIC,Original + Variety,Easy 14,Standard 19,Hard 37,Master 64,Unlimited 73,
100514,daybre,DAYBREAK FRONTLINE,Orangestar,Vocaloid,Easy 9,Standard 16,Hard 32,Master 53,Unlimited 72,
100515,umiyur,ウミユリ海底譚,n-buna,Vocaloid,Easy 8,Standard 14,Hard 28,Master 46,Unlimited 64,
100516,chalur,シャルル,バルーン,Vocaloid,Easy 14,Standard 18,Hard 40,Master 60,Unlimited 72,
100517,melanc,メランコリック,Junky,Vocaloid,Easy 10,Standard 15,Hard 30,Master 50,Unlimited 63,
100518,konofu,このふざけた素晴らしき世界は、僕の為にある,n.k,Vocaloid,Easy 11,Standard 23,Hard 35,Master 62,Unlimited 81,
100526,bladem,The Blade Master,mikashu,Original,Easy 17,Standard 28,Hard 38,Master 63,Unlimited 75,
100536,southw,South wind,moimoi,Original,Easy 12,Standard 18,Hard 27,Master 57,Unlimited 68,
100537,ryuuse,流星デモクラシー,kamejack,Original,Easy 13,Standard 21,Hard 32,Master 59,Unlimited N/A,
100519,redhea,ROCK'N'ROLL☆FLYING REDHEAD,暁Records,Touhou,Easy 10,Standard 27,Hard 39,Master 59,Unlimited 72,
100520,warnin,WARNING×WARNING×WARNING,暁Records,Touhou,Easy 12,Standard 25,Hard 36,Master 61,Unlimited 74,
100521,topsec,TOP SECRET -My Red World-,暁Records,Touhou,Easy 13,Standard 24,Hard 34,Master 51,Unlimited 64,
100522,dddoll,DOWN DOWN DOLL,暁Records,Touhou,Easy 14,Standard 26,Hard 38,Master 55,Unlimited 63,
100548,tracee,トレイス・エゴイズム,暁Records,Touhou,Easy 9,Standard 19,Hard 31,Master 49,Unlimited 65,
100111,drivin,Driving story,Duca,Original,Easy 8,Standard 23,Hard 40,Master 66,Unlimited 76,
100118,genzit,現実幻覚スピードスター,yozuca*,Original,Easy 12,Standard 18,Hard 46,Master 73,Unlimited 82,
100039,aerial,エアリアル,カヒーナムジカ,Original,Easy 5,Standard 11,Hard 28,Master 56,Unlimited 71,
100532,einher,Einherjar,閣下,Original,Easy 16,Standard 29,Hard 40,Master 74,Unlimited 80,
100540,ariell,Ariel,nanobii,Original,Easy 15,Standard 19,Hard 32,Master 64,Unlimited 73,
100542,firstl,First Love,UFO,Original,Easy 17,Standard 25,Hard 36,Master 65,Unlimited 77,
100550,heartl,Heartland,Bernis,Original,Easy 11,Standard 23,Hard 30,Master 64,Unlimited N/A,
100551,erasee,ERASE,MozSound,Original,Easy 12,Standard 22,Hard 35,Master 58,Unlimited 68,
100530,regene,Regeneration ray,Tsukasa,Original,Easy 13,Standard 20,Hard 30,Master 56,Unlimited 70,
100549,allelu,アレルヤ,HAKKYOU-KUN feat.玉置成実,Original,Easy 16,Standard 28,Hard 35,Master 64,Unlimited 75,
100543,lighto,Light of my Life,S3RL,Original,Easy 12,Standard 25,Hard 33,Master 60,Unlimited 74,
100552,termin,Terminus a quo,ginkiha,Original,Easy 13,Standard 24,Hard 34,Master 63,Unlimited 79,
100556,ryuuse2,流星でもくらちー☆,kamejack,Original,Easy 13,Standard 20,Hard 36,Master 62,Unlimited 75,
100547,prizmm,PRIZM,ミフメイ,Original,Easy 12,Standard 21,Hard 30,Master 54,Unlimited N/A,
100098,samalv,サマ★ラブ,コツキミヤ,Original,Easy 13,Standard 19,Hard 39,Master 58,Unlimited 77,
100544,palpit,Palpitation,Zekk,Original,Easy 18,Standard 29,Hard 55,Master 84,Unlimited 92,
100558,gainen,Break the Wall!! ~ロンリガイネン,暁Records,Original,Easy 15,Standard 26,Hard 37,Master 63,Unlimited N/A,
100525,moonsh,Moon Shard,satella,Original,Easy 10,Standard 23,Hard 36,Master 62,Unlimited N/A,
100559,moonki,MoonLightKiss,effe,Original,Easy 13,Standard 25,Hard 39,Master 64,Unlimited N/A,
100560,moonri,Moonrise,Relect,Original,Easy 14,Standard 21,Hard 38,Master 58,Unlimited 85,
100561,goaway,Go Away,Cranky,Original,Easy 10,Standard 23,Hard 45,Master 59,Unlimited 70,
100567,itback,Bring it back now,siromaru,Original,Easy 12,Standard 23,Hard 38,Master 71,Unlimited N/A,
100569,redhhh,Red Heart,Yooh vs. siromaru,Original,Easy 13,Standard 24,Hard 39,Master 77,Unlimited N/A,
100568,actual,Actual Reverse,siromaru,Original,Easy 14,Standard 25,Hard 38,Master 80,Unlimited N/A,
100367,zonzon,Bi-Zon Zon Zombi,MC Natsack,Original,Easy 5,Standard 16,Hard 33,Master 63,Unlimited 67,
100565,memorm,Memorim,Avans,Original,Easy 15,Standard 26,Hard 37,Master 73,Unlimited N/A,
100554,kokoro,ココロメソッド,Endorfin.,Original,Easy 12,Standard 20,Hard 43,Master 65,Unlimited 69,
100563,poweri,Power is Power,KO3,Original,Easy 13,Standard 26,Hard 49,Master 75,Unlimited 91,
100555,nisenn,,Ω,Original,Easy N/A,Standard N/A,Hard N/A,Master 76,Unlimited N/A,
100096,yukiya,Vespero,Monotone Rhythm feat.綾川雪弥,Original,Easy 11,Standard 19,Hard 40,Master 61,Unlimited N/A,
100124,zankyo,残響のアカーシャ,Astilbe × arendsii,Original,Easy 10,Standard 18,Hard 38,Master 57,Unlimited 74,
100119,overlp,オーバーラップ,millie loved by yksb,Original,Easy 9,Standard 17,Hard 30,Master 51,Unlimited N/A,
100529,fracta,Fractalize,Sakuzyo,Original,Easy 19,Standard 31,Hard 52,Master 83,Unlimited N/A,
100455,cantst,Can't Stop,KaSa,Original,Easy 11,Standard 23,Hard 42,Master 65,Unlimited N/A,
100527,primaa,Prima,Kiryu(桐生),Original,Easy 12,Standard 18,Hard 35,Master 54,Unlimited 75,
100448,cyberg,CYBER GANG,ヒゲドライVAN,Original,Easy 12,Standard 23,Hard 35,Master 60,Unlimited N/A,
100018,freakw,Freak With Me,SLAKE,Original,Easy 13,Standard 22,Hard 42,Master 65,Unlimited 66,
100006,aquali,Aqualight,MAYA AKAI,Original,Easy 11,Standard 16,Hard 34,Master 58,Unlimited N/A,
100572,takesc,Music Takes Control,Fierce Chain,Original,Easy 10,Standard 27,Hard 37,Master 69,Unlimited N/A,
100531,cthugh,Cthugha,MozSound,Original,Easy 14,Standard 25,Hard 48,Master 73,Unlimited N/A,
100571,thetaa,θ (theta) ,effe,Original,Easy 11,Standard 21,Hard 34,Master 62,Unlimited N/A,
100493,nekofu,ネコふんじゃった☆ (クローニャSTYLE),,Variety,Easy 10,Standard 22,Hard 34,Master 57,Unlimited 80,
100057,howtru,How True Is Your Love,brinq,Original,Easy 8,Standard 12,Hard 25,Master 53,Unlimited 74,
100047,romanc,ロマンシングゲーム,まふ×ティン,Original,Easy 10,Standard 28,Hard 55,Master 78,Unlimited N/A,
100573,kotobu,KOTOBUKI,REVen-G,Original,Easy 25,Standard 32,Hard 71,Master 90,Unlimited N/A,
100417,xmasss,ジングルベル (NM REMIX),,Variety,Easy 8,Standard 18,Hard 38,Master 56,Unlimited 77,
100600,galaxy,GALAXY,キュウソネコカミ,J-Pop,Easy 10,Standard 16,Hard 32,Master 43,Unlimited 67,
100601,rebell,Rebellion,NAOKI underground,Original,Easy N/A,Standard 49,Hard 63,Master 91,Unlimited N/A,
100602,anothe,Another Chance,Luci,Original,Easy N/A,Standard 27,Hard 37,Master 73,Unlimited 76,
100603,addict,Addicted,luz×アリエP,Original,Easy N/A,Standard 20,Hard 34,Master 52,Unlimited 62,
100604,dirtyy,Dirty Mouth,Asletics,Original,Easy N/A,Standard 15,Hard 28,Master 59,Unlimited 74,
100605,levelf,LEVEL5-Judgelight-,fripSide,J-Pop,Easy 5,Standard 11,Hard 28,Master 45,Unlimited 63,
100606,omnive,Omniverse,Atomic,Original,Easy N/A,Standard 34,Hard 52,Master 83,Unlimited 86,
100607,kakuse,覚醒 ∞ awake,PwD,Original,Easy N/A,Standard 17,Hard 55,Master 75,Unlimited N/A,
100608,unbeli,アンビリーバーズ,米津玄師,J-Pop,Easy 7,Standard 13,Hard 26,Master 38,Unlimited 62,
100609,sonzai,ソンザイキョウドウタイ,ジギル,Original,Easy N/A,Standard 26,Hard 40,Master 59,Unlimited 66,
100610,okonik,OKONIKUKOD,SHU OKUYAMA,Original,Easy N/A,Standard 26,Hard 45,Master 67,Unlimited N/A,
100611,crssho,CrossShooter,Tatsh,Original,Easy 10,Standard 35,Hard 60,Master 85,Unlimited N/A,
100612,reanim,Reanimation,DC feat.S!N,Original,Easy N/A,Standard 28,Hard 44,Master 70,Unlimited 80,
100613,kamino,kaminoko,HAKKYOU-KUN,Original,Easy 15,Standard 40,Hard 62,Master 78,Unlimited N/A,
100614,fiveee,Five,ANOTHER STORY,J-Pop,Easy 10,Standard 18,Hard 37,Master 61,Unlimited 71,
100615,granda,Grand Arc,Tosh,Original,Easy N/A,Standard 21,Hard 38,Master 79,Unlimited N/A,
100616,fronti2,NEXT FRONTIER -TRUE RISE-,NAOKI,Original,Easy 9,Standard 46,Hard 69,Master 89,Unlimited N/A,
100617,saigon,最後の1ページ,桜井零士,Original,Easy N/A,Standard 19,Hard 31,Master 57,Unlimited N/A,
100618,replay,REPLAY,VAMPS,J-Pop,Easy 8,Standard 18,Hard 44,Master 63,Unlimited 70,
100619,mousou,妄想全開,志麻×ふぉP,Original,Easy N/A,Standard 16,Hard 26,Master 54,Unlimited N/A,
100620,aheadd,AHEAD,VAMPS,J-Pop,Easy 7,Standard 13,Hard 25,Master 35,Unlimited 58,
100621,musicr1,All You Need Is Beat(s) -musicるTV・ミリオン連発音楽作家塾第7弾-,CLONE,Original,Easy 12,Standard 22,Hard 33,Master 58,Unlimited 74,
100622,getthe,Get the glory,中ノ森文子,J-Pop,Easy 6,Standard 17,Hard 37,Master 49,Unlimited 66,
100623,design,Designed World,Alinut,Original,Easy N/A,Standard 15,Hard 39,Master 68,Unlimited 69,
100624,garnet,GARNET HOWL,フラット3rd,Original,Easy N/A,Standard 26,Hard 46,Master 70,Unlimited 94,
100625,hopesb,Hopes Bright,WHITE ASH,J-Pop,Easy 7,Standard 10,Hard 25,Master 44,Unlimited 61,
100626,shooti,Shooting Star feat.HISASHI (GLAY),96猫,J-Pop,Easy 7,Standard 15,Hard 37,Master 49,Unlimited 69,
100627,dangan,弾丸と星空,HAKKYOU-KUN,Original,Easy N/A,Standard 28,Hard 58,Master 81,Unlimited N/A,
100628,impact,Impact,Tatsh,Original,Easy 20,Standard 24,Hard 60,Master 72,Unlimited 90,
100629,lightm,Light My Fire,KOTOKO,J-Pop,Easy 11,Standard 26,Hard 33,Master 54,Unlimited 71,
100630,miiroo,海色,AKINO from bless4,J-Pop,Easy 11,Standard 22,Hard 39,Master 58,Unlimited 68,
100631,voiceo,Voice Of House,DOT96,Original,Easy N/A,Standard 18,Hard 34,Master 58,Unlimited 59,
100632,cosmol,Cosmology,RIC,Original,Easy 25,Standard 36,Hard 64,Master 87,Unlimited N/A,
100633,vividd,ViViD,May'n,J-Pop,Easy 9,Standard 16,Hard 35,Master 55,Unlimited 65,
100634,splash,SPLASH,MAYA AKAI,Original,Easy N/A,Standard 26,Hard 50,Master 71,Unlimited N/A,
100635,donuth,ドーナツホール,ハチ,Vocaloid,Easy 11,Standard 22,Hard 40,Master 54,Unlimited 80,
100636,senbon,千本桜,和楽器バンド,Vocaloid,Easy 12,Standard 20,Hard 28,Master 54,Unlimited 74,
100637,kmtyju,君と野獣,バンドハラスメント,J-Pop,Easy 12,Standard 24,Hard 31,Master 57,Unlimited 74,
100638,fronti,NEXT FRONTIER,NAOKI,Original,Easy 13,Standard 48,Hard 65,Master 82,Unlimited N/A,
100639,nueraa,Nu Era,SPARKER,Original,Easy N/A,Standard 22,Hard 43,Master 75,Unlimited 53,
100640,childe,CHiLD -error-,MY FIRST STORY,J-Pop,Easy 4,Standard 9,Hard 24,Master 34,Unlimited 56,
100641,dazzli2,DAZZLING♡SEASON (Darwin Remix),jun,Original,Easy 19,Standard 35,Hard 60,Master 82,Unlimited N/A,
100642,perfec,Perfectionism,高橋渉 feat.2d6,Original,Easy N/A,Standard 39,Hard 64,Master 78,Unlimited N/A,
100643,flower,Flowerwall,米津玄師,J-Pop,Easy 6,Standard 7,Hard 20,Master 40,Unlimited 65,
100644,frgmnt,Frgmnts,Nyolfen,Original,Easy 10,Standard 33,Hard 63,Master 74,Unlimited 65,
100645,headph,HEADPHONE PARTY,A-One,Original,Easy N/A,Standard 24,Hard 32,Master 52,Unlimited N/A,
100646,crsang,CrossAngel,Tatsh feat. 彩音,Original,Easy 13,Standard 27,Hard 53,Master 67,Unlimited N/A,
100647,musicr4,Accept,sushi feat.とよだま,Original,Easy 12,Standard 19,Hard 32,Master 58,Unlimited N/A,
100648,imaxim,A×E×U×G -act.1-,190Cb,Original,Easy N/A,Standard 44,Hard 69,Master 90,Unlimited 87,
100649,azitat2,Azitate (Prologue Edition),void,Original,Easy 8,Standard 23,Hard 52,Master 66,Unlimited N/A,
100650,dynami,DYNAMITE SENSATION,NAOKI,Original,Easy 11,Standard 26,Hard 54,Master 68,Unlimited N/A,
100651,incave,Into the Cave,Jerico,Original,Easy N/A,Standard 22,Hard 44,Master 76,Unlimited 78,
100652,aktuki,AKATSUKI,NAOKI underground,Original,Easy 10,Standard 26,Hard 58,Master 84,Unlimited N/A,
100653,kindof,Wonderful,Fraz,Original,Easy N/A,Standard 14,Hard 29,Master 48,Unlimited N/A,
100654,mikaku,未確認XX生命体,民安★ROCK,Original,Easy N/A,Standard 19,Hard 31,Master 54,Unlimited N/A,
100655,strang,ストレンジ・ディーヴァ,麹町養蚕館,Original,Easy N/A,Standard 12,Hard 28,Master 55,Unlimited N/A,
100656,hesper,Hesperides,xi,Original,Easy N/A,Standard 36,Hard 61,Master 92,Unlimited 93,
100657,breaka,Break a spell,川田まみ,J-Pop,Easy 7,Standard 15,Hard 31,Master 45,Unlimited 68,
100658,myname,When You Call My Name,Beat Envy,Original,Easy N/A,Standard 6,Hard 14,Master 30,Unlimited 57,
100659,amaiko,甘い言葉,Kenichi Chiba feat. EVO+,Original,Easy N/A,Standard 15,Hard 37,Master 60,Unlimited N/A,
100660,reseed2,Reseed,quick master,Original,Easy N/A,Standard 22,Hard 47,Master 63,Unlimited N/A,
100661,kingst,KING STUN,JUPITRIS,Original,Easy 12,Standard 38,Hard 63,Master 74,Unlimited N/A,
100662,ramram,Break Your World,RAM,Original,Easy N/A,Standard 23,Hard 34,Master 67,Unlimited N/A,
100663,murasa,Murasame,Ryunosuke Kudo,Original,Easy N/A,Standard 28,Hard 41,Master 76,Unlimited N/A,
100664,happyd,Happy Deathday,ANOTHER STORY,Original,Easy 18,Standard 22,Hard 41,Master 73,Unlimited 79,
100665,izimed,イジメ、ダメ、ゼッタイ,BABYMETAL,J-Pop,Easy 9,Standard 19,Hard 39,Master 69,Unlimited 77,
100666,wastel,Wasteland,James Taplin,Original,Easy N/A,Standard 4,Hard 12,Master 23,Unlimited 40,
100667,assign,Assign,MASAYASU,Original,Easy N/A,Standard 26,Hard 43,Master 61,Unlimited 62,
100668,jahaci,Jahacid,DJ SODEYAMA,Original,Easy N/A,Standard 17,Hard 29,Master 59,Unlimited N/A,
100669,hisuii,Hisui,stereoberry,Original,Easy N/A,Standard 22,Hard 47,Master 70,Unlimited N/A,
100670,godkno,God knows...,涼宮ハルヒ(C.V.平野綾),J-Pop,Easy 6,Standard 10,Hard 26,Master 45,Unlimited 64,
100671,roadof,Road of Resistance,BABYMETAL,J-Pop,Easy 7,Standard 15,Hard 36,Master 50,Unlimited 75,
100672,rokuch,六兆年と一夜物語,和楽器バンド,J-Pop + Vocaloid,Easy 11,Standard 21,Hard 35,Master 62,Unlimited 81,
100673,valent,いつか王子様が (Remix Ver.),,Original,Easy 10,Standard 27,Hard 33,Master 59,Unlimited 77,
100674,unfini,→unfinished→,KOTOKO,J-Pop,Easy 8,Standard 16,Hard 32,Master 50,Unlimited 71,
100675,auflcb2,some day -see you again-,NAOKI,Original,Easy 10,Standard 22,Hard 37,Master 75,Unlimited N/A,
100676,burnin,Burning Inside,Nhato,Original,Easy 15,Standard 18,Hard 28,Master 60,Unlimited 85,
100677,sphere,Hypersphere,Dubscribe,Original,Easy N/A,Standard 20,Hard 38,Master 73,Unlimited N/A,
100678,dropou,D.O.B.,野水いおり,J-Pop,Easy 14,Standard 17,Hard 31,Master 46,Unlimited 69,
100679,xencou,X-encounter,黒崎真音,J-Pop,Easy 8,Standard 20,Hard 32,Master 52,Unlimited 60,
100680,killyk,killy killy JOKER,分島花音,J-Pop,Easy 6,Standard 13,Hard 42,Master 63,Unlimited 76,
100681,missil,the Last Missile Man,adHoc World,Original,Easy N/A,Standard 16,Hard 38,Master 59,Unlimited N/A,
100682,burstt,Burst The Gravity,ALTIMA,J-Pop,Easy 7,Standard 12,Hard 25,Master 46,Unlimited 63,
100683,musicr2,My Recklessness,Kagerou,Original,Easy 12,Standard 22,Hard 33,Master 58,Unlimited N/A,
100684,isingl,Isinglass,Voltex,Original,Easy 12,Standard 25,Hard 44,Master 80,Unlimited N/A,
100685,lvless,Loveless,YOSA,Original,Easy N/A,Standard 23,Hard 38,Master 60,Unlimited N/A,
100686,sapphi,Sapphire,voltex,Original,Easy N/A,Standard 29,Hard 44,Master 81,Unlimited N/A,
100687,musicr3,Climaxxx Party -musicるTV・ミリオン連発音楽作家塾第7弾-,Kyota. feat.とよだま&れい,Original,Easy 12,Standard 19,Hard 32,Master 58,Unlimited 72,
100688,deeout,Deep Outside,Seiho,Original,Easy N/A,Standard 18,Hard 34,Master 63,Unlimited 81,
100689,sugars,シュガーソングとビターステップ,UNISON SQUARE GARDEN,J-Pop,Easy 6,Standard 17,Hard 30,Master 42,Unlimited 66,
100690,mercur,MERCURY ,E.Z.M,Original,Easy N/A,Standard 14,Hard 35,Master 66,Unlimited N/A,
100691,zizizi,Z[i],Cybermiso,Original,Easy N/A,Standard 30,Hard 57,Master 88,Unlimited 96,
100692,wegooo,WE GO,BREAKERZ,J-Pop,Easy 10,Standard 18,Hard 34,Master 54,Unlimited 68,
100693,alonee,ALONE,MY FIRST STORY,J-Pop,Easy 5,Standard 11,Hard 21,Master 36,Unlimited 48,
100694,nuheat,Nu Heat,Paisley Parks,Original,Easy N/A,Standard 29,Hard 44,Master 65,Unlimited 85,
100695,granro,メモリーズ,GRANRODEO,J-Pop,Easy 8,Standard 15,Hard 28,Master 43,Unlimited 60,
100696,sister,sister's noise,fripSide,J-Pop,Easy 7,Standard 10,Hard 27,Master 46,Unlimited 63,
100697,lotusl,Lotus Love,Maozon,Original,Easy N/A,Standard 20,Hard 36,Master 64,Unlimited N/A,
100698,yukari,YUKARI,Ocelot,Original,Easy N/A,Standard 31,Hard 50,Master 76,Unlimited 84,
100699,flawli,フローライト,米津玄師,J-Pop,Easy 8,Standard 17,Hard 30,Master 40,Unlimited 59,
100700,nightf,NIGHT FEELIN',マセラティ渚,Original,Easy N/A,Standard 15,Hard 28,Master 46,Unlimited 71,
100701,random,シャッフルセレクト,シャッフルセレクト,Original,Easy N/A,Standard N/A,Hard N/A,Master N/A,Unlimited N/A,
100702,wiwwtw,What Is Wrong With The World,SADA,Original,Easy N/A,Standard 26,Hard 38,Master 62,Unlimited N/A,
100703,inneru,Inner Urge,上坂すみれ,Original,Easy 9,Standard 22,Hard 36,Master 48,Unlimited 67,
100704,taishi,Otherside,Taishi,Original,Easy N/A,Standard 19,Hard 35,Master 58,Unlimited N/A,
100705,daysss,Days,Kent Alexander,Original,Easy N/A,Standard 38,Hard 59,Master 81,Unlimited 81,
100706,bokuwa,僕は君のアジテーターじゃない feat.Neru,焚吐,J-Pop,Easy 16,Standard 23,Hard 34,Master 55,Unlimited 69,
100707,showww,掌 -show-,喜多村英梨,Original,Easy 15,Standard 18,Hard 35,Master 51,Unlimited 79,
100708,nevers,Never Sleep Again,PassCode,J-Pop,Easy 15,Standard 26,Hard 32,Master 65,Unlimited 75,
100709,bleeze,BLEEZE,GLAY,J-Pop,Easy 9,Standard 16,Hard 31,Master 47,Unlimited 62,
100710,dreami,DREAMIN' OF YOU feat.コッテル,Arts Of Collective,Original,Easy N/A,Standard 14,Hard 37,Master 65,Unlimited N/A,
100711,allune,All U Need,MesoPhunk,Pick-Up (New + Revival),Easy N/A,Standard 14,Hard 35,Master 71,Unlimited N/A,
100712,always,Always Thinking Of You,Sketchout,Pick-Up (New + Revival),Easy N/A,Standard 13,Hard 27,Master 49,Unlimited N/A,
100713,anomie2,Anomie (Axiom Style),D-Fener,Pick-Up (New + Revival),Easy N/A,Standard 16,Hard 43,Master 84,Unlimited N/A,
100714,aquali2,Aqualight (Remix Ver.),MAYA AKAI,Pick-Up (New + Revival),Easy N/A,Standard 22,Hard 43,Master 60,Unlimited 81,
100715,astaro,ASTAROTH,JUPITRIS,Pick-Up (New + Revival),Easy N/A,Standard 23,Hard 40,Master 74,Unlimited N/A,
100716,bassan,BASS ANTICS,Mitomoro,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 32,Master 66,Unlimited N/A,
100717,zonzon2,Bi-Zon Zon Zombi (More Zombies Ver.),MC Natsack,Pick-Up (New + Revival),Easy N/A,Standard 13,Hard 27,Master 68,Unlimited 75,
100718,bouled,boule de berlin,JTTR,Pick-Up (New + Revival),Easy N/A,Standard 19,Hard 30,Master 57,Unlimited N/A,
100719,brandn,BRAND NEW,Headphone-Tokyo(star)(star) feat.カヒーナ,Pick-Up (New + Revival),Easy N/A,Standard 9,Hard 39,Master 66,Unlimited 72,
100720,bravee,BRAVE,Ryuno,Pick-Up (New + Revival),Easy N/A,Standard 35,Hard 60,Master 82,Unlimited N/A,
100721,breakd2,Break down (2nd Edition),GARNiDELiA,Pick-Up (New + Revival),Easy N/A,Standard 34,Hard 64,Master 74,Unlimited N/A,
100722,buffet,Buffet survivor,Yamajet feat. Cathy & TEA,Pick-Up (New + Revival),Easy N/A,Standard 38,Hard 55,Master 68,Unlimited N/A,
100723,buzzke,BUZZ Ketos,フラット3rd,Pick-Up (New + Revival),Easy N/A,Standard 18,Hard 33,Master 58,Unlimited 77,
100724,cashhh,Cash!,Nor,Pick-Up (New + Revival),Easy N/A,Standard 19,Hard 25,Master 64,Unlimited N/A,
100725,cloudb,Cloudburst,Relect,Pick-Up (New + Revival),Easy N/A,Standard 37,Hard 66,Master 74,Unlimited N/A,
100726,clouds,cloudstepping,Ryuno,Pick-Up (New + Revival),Easy N/A,Standard 13,Hard 25,Master 47,Unlimited N/A,
100727,codepa,Code Paradiso,Himmel,Pick-Up (New + Revival),Easy N/A,Standard 29,Hard 55,Master 70,Unlimited N/A,
100728,comear,Come Around,MesoPhunk,Pick-Up (New + Revival),Easy N/A,Standard 38,Hard 56,Master 83,Unlimited N/A,
100729,crysta,Crystal Ribbon,Cosine,Pick-Up (New + Revival),Easy N/A,Standard 37,Hard 56,Master 81,Unlimited N/A,
100730,curseo,Curse of Doll,KO3,Pick-Up (New + Revival),Easy N/A,Standard 22,Hard 36,Master 74,Unlimited N/A,
100731,datami,data mining,voia,Pick-Up (New + Revival),Easy N/A,Standard 18,Hard 36,Master 66,Unlimited N/A,
100732,defaul,default affinity,JTTR,Pick-Up (New + Revival),Easy N/A,Standard 21,Hard 33,Master 48,Unlimited N/A,
100733,design2,Designed World (Remix ver.),Alinut,Pick-Up (New + Revival),Easy N/A,Standard 25,Hard 43,Master 68,Unlimited N/A,
100734,diamon,DIAMOND SKIN,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 10,Hard 26,Master 33,Unlimited N/A,
100735,dispel,dispel,Endorfin.,Pick-Up (New + Revival),Easy N/A,Standard 28,Hard 48,Master 80,Unlimited N/A,
100736,distan,Distantmemory,村瀬悠太,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 30,Master 68,Unlimited N/A,
100737,dokibl,Doki Blaster,VOIA,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 23,Master 67,Unlimited N/A,
100738,dontwa,Don't Walk Away,Sarah-Jane,Pick-Up (New + Revival),Easy N/A,Standard 13,Hard 34,Master 69,Unlimited N/A,
100739,drgirl,Dreaming Girl,Nor,Pick-Up (New + Revival),Easy N/A,Standard 19,Hard 35,Master 54,Unlimited 73,
100740,eterna,Eternally,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 12,Hard 21,Master 39,Unlimited N/A,
100741,everkr,everKrack,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 18,Hard 29,Master 41,Unlimited N/A,
100742,everwh,EverWhite,satella,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 31,Master 58,Unlimited N/A,
100743,farthe,FarthestEnd,Sakuzyo,Pick-Up (New + Revival),Easy N/A,Standard 30,Hard 56,Master 78,Unlimited 87,
100744,filame,Filament Flow,Endorfin.,Pick-Up (New + Revival),Easy N/A,Standard 23,Hard 38,Master 63,Unlimited N/A,
100745,flameu2,Flame Up (Remix Ver.),Inu Machine,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 24,Master 59,Unlimited N/A,
100746,freeee,Free,千π,Pick-Up (New + Revival),Easy N/A,Standard 19,Hard 39,Master 69,Unlimited N/A,
100747,funkyb2,FUNKYBABY EVOLUTION,Yamajet,Pick-Up (New + Revival),Easy N/A,Standard 21,Hard 34,Master 56,Unlimited N/A,
100748,granda2,Grand Arc (Club Remix),Tosh,Pick-Up (New + Revival),Easy N/A,Standard 24,Hard 41,Master 73,Unlimited 83,
100749,hsphsp,H.S.P (Hard Style Party),Ravine & Tom Budin,Pick-Up (New + Revival),Easy N/A,Standard 12,Hard 25,Master 69,Unlimited N/A,
100750,halluc,Hallucination XXX,t+pazolite,Pick-Up (New + Revival),Easy N/A,Standard 40,Hard 52,Master 87,Unlimited N/A,
100751,indigo,Indigo Isle,Syntax Error,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 33,Master 50,Unlimited 75,
100752,inters,Interstellar Plazma,KO3,Pick-Up (New + Revival),Easy N/A,Standard 25,Hard 42,Master 77,Unlimited N/A,
100753,incave2,Into the Cave (Another Edit),Jerico,Pick-Up (New + Revival),Easy N/A,Standard 31,Hard 57,Master 88,Unlimited N/A,
100754,ioniza,IONIZATION,llliiillliiilll,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 34,Master 70,Unlimited 85,
100755,guilty,JUSTICE [from] GUILTY,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 28,Master 50,Unlimited N/A,
100756,keraun,Keraunos,Xiphoid Sphere (xi + siromaru),Pick-Up (New + Revival),Easy N/A,Standard 25,Hard 52,Master 79,Unlimited N/A,
100757,landin2,Landing on the moon (Instrumental Version),SIMON,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 34,Master 59,Unlimited 66,
100758,videog,Life In A Video Game,Bentobox,Pick-Up (New + Revival),Easy N/A,Standard 21,Hard 37,Master 62,Unlimited N/A,
100759,loseyo,Lose Your Mind,Vau Boy,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 30,Master 71,Unlimited N/A,
100760,machin,Machine,Sparky,Pick-Up (New + Revival),Easy N/A,Standard 12,Hard 28,Master 72,Unlimited N/A,
100761,makeit,Make It Fresh EDM ver.,HighLux,Pick-Up (New + Revival),Easy N/A,Standard 11,Hard 24,Master 48,Unlimited N/A,
100762,daydre,Mechanized Daydream,s-don,Pick-Up (New + Revival),Easy N/A,Standard 19,Hard 36,Master 80,Unlimited N/A,
100763,metron,Metro Night,ginkiha,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 44,Master 71,Unlimited N/A,
100764,milkyw,Milky Way Trip,Nor,Pick-Up (New + Revival),Easy N/A,Standard 22,Hard 31,Master 60,Unlimited N/A,
100766,nayuta,nayuta,happy machine,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 37,Master 68,Unlimited N/A,
100767,nightm,nightmares,Seeds of the Upcoming Infection,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 49,Master 73,Unlimited N/A,
100768,otherw,Other World,XIO,Pick-Up (New + Revival),Easy N/A,Standard 23,Hard 41,Master 76,Unlimited N/A,
100769,overth,Over The Blue (Breaking Through),Fracus & Darwin Feat. Jenna,Pick-Up (New + Revival),Easy N/A,Standard 33,Hard 57,Master 82,Unlimited N/A,
100770,uuuuuu,Phoenix,U,Pick-Up (New + Revival),Easy N/A,Standard 23,Hard 37,Master 74,Unlimited N/A,
100771,rainin,Raining Again feat. Bea Aria,Sanxion,Pick-Up (New + Revival),Easy N/A,Standard 16,Hard 41,Master 69,Unlimited N/A,
100772,raisey,Raise Your Handz!,KO3 & Relect,Pick-Up (New + Revival),Easy N/A,Standard 23,Hard 55,Master 75,Unlimited N/A,
100773,resona,Resonance,RAM,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 32,Master 64,Unlimited N/A,
100774,reuniv,Reuniverse,Headphone-Tokyo(star)(star) feat.カヒーナ,Pick-Up (New + Revival),Easy N/A,Standard 14,Hard 23,Master 41,Unlimited N/A,
100775,rhythm,RHYTHM GAME MACHINE,ginkiha,Pick-Up (New + Revival),Easy N/A,Standard 37,Hard 56,Master 78,Unlimited N/A,
100776,rushhh,Rush,TANUKI,Pick-Up (New + Revival),Easy N/A,Standard 25,Hard 37,Master 75,Unlimited N/A,
100777,steeee,S.T.E,Tatsh,Pick-Up (New + Revival),Easy N/A,Standard 30,Hard 58,Master 87,Unlimited N/A,
100778,sangey,Sangeyasya,NNNNNNNNNN,Pick-Up (New + Revival),Easy N/A,Standard 27,Hard 47,Master 85,Unlimited N/A,
100779,senpai,Senpai Slam,千π,Pick-Up (New + Revival),Easy N/A,Standard 38,Hard 54,Master 77,Unlimited N/A,
100780,sestea,Sestea,Feryquitous,Pick-Up (New + Revival),Easy N/A,Standard 27,Hard 47,Master 76,Unlimited N/A,
100781,silver,Silverd,Feryquitous,Pick-Up (New + Revival),Easy N/A,Standard 28,Hard 40,Master 69,Unlimited N/A,
100782,sodama,Soda Machine,Syntax Error,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 40,Master 65,Unlimited N/A,
100783,stardu,STARDUST (game edit),MINIKOMA★,Pick-Up (New + Revival),Easy N/A,Standard 19,Hard 33,Master 64,Unlimited N/A,
100784,starti,starting station,happy machine,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 31,Master 54,Unlimited 70,
100785,sunday,SUNDAY リベンジ,HAPPY SUNDAY,Pick-Up (New + Revival),Easy N/A,Standard 18,Hard 29,Master 46,Unlimited 67,
100786,sundro2,Sundrop (Remix ver.),Yamajet,Pick-Up (New + Revival),Easy N/A,Standard 30,Hard 48,Master 79,Unlimited 82,
100787,sunnyd,Sunny day,センラ×蒼炎P,Pick-Up (New + Revival),Easy N/A,Standard 23,Hard 38,Master 59,Unlimited N/A,
100788,superl,SuperLuminalGirl Rebirth,Yamajet feat. 小宮真央,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 32,Master 59,Unlimited N/A,
100789,switch,SW!TCH,千π MesoPhunk,Pick-Up (New + Revival),Easy N/A,Standard 16,Hard 35,Master 69,Unlimited N/A,
100790,theepi2,The Epic -Introduction-,Cranky,Pick-Up (New + Revival),Easy N/A,Standard 22,Hard 37,Master 65,Unlimited N/A,
100791,epipha,The Epiphany of Hardcore,SOTUI,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 30,Master 70,Unlimited N/A,
100792,thekin,The King of Pirates,RiraN,Pick-Up (New + Revival),Easy N/A,Standard 22,Hard 52,Master 75,Unlimited N/A,
100793,timele,Timeless encode,Vice Principal,Pick-Up (New + Revival),Easy N/A,Standard 16,Hard 33,Master 72,Unlimited N/A,
100794,tokyoo,tokyo,Headphone-Tokyo(star)(star) feat.nayuta,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 33,Master 71,Unlimited N/A,
100795,toooma,Tooo Many,S3RL,Pick-Up (New + Revival),Easy N/A,Standard 30,Hard 51,Master 77,Unlimited N/A,
100796,toucho2,Touch Of Gold (Bongo Mango Remix),Togo Project feat. Frances Maya,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 32,Master 52,Unlimited 78,
100797,tayuta,tΔyutΔi,ミフメイ,Pick-Up (New + Revival),Easy N/A,Standard 26,Hard 35,Master 72,Unlimited N/A,
100798,ultrix,ULTRiX,sky_delta,Pick-Up (New + Revival),Easy N/A,Standard 27,Hard 45,Master 76,Unlimited N/A,
100799,underw,Underworld,ANOTHER STORY,Pick-Up (New + Revival),Easy N/A,Standard 29,Hard 46,Master 69,Unlimited 86,
100800,virtua,Virtual Reality Controller,フラット3rd,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 35,Master 63,Unlimited N/A,
100801,voiceo2,VOICE OF HOUSE (96TH RETROMAN REMIX),DOT96,Pick-Up (New + Revival),Easy N/A,Standard 14,Hard 38,Master 67,Unlimited N/A,
100802,wannab2,Wanna Be Your Special (Remix ver.),Shoichiro Hirata feat. SUIMI,Pick-Up (New + Revival),Easy N/A,Standard 26,Hard 41,Master 69,Unlimited N/A,
100803,wiwwtw2,What Is Wrong With The World (Cross Edit),SADA,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 43,Master 67,Unlimited 72,
100804,wingso,Wings of Twilight,sky_delta,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 53,Master 71,Unlimited N/A,
100805,winter,Winter again,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 14,Hard 24,Master 41,Unlimited N/A,
100806,iineee,いいね!,BABYMETAL,Pick-Up (New + Revival),Easy N/A,Standard 21,Hard 40,Master 81,Unlimited N/A,
100807,illumi,イルミナレガロ,Headphone-Tokyo(star)(star) feat.MiLO,Pick-Up (New + Revival),Easy N/A,Standard 10,Hard 25,Master 46,Unlimited 63,
100808,yellll,エール,FullMooN,Pick-Up (New + Revival),Easy N/A,Standard 8,Hard 17,Master 52,Unlimited N/A,
100809,eschat,エスカトロジィ,MozSound,Pick-Up (New + Revival),Easy N/A,Standard 36,Hard 57,Master 77,Unlimited N/A,
100810,counte,カウンターストップ,フラット3rd,Pick-Up (New + Revival),Easy N/A,Standard 29,Hard 34,Master 71,Unlimited N/A,
100811,gimcho,ギミチョコ!!,BABYMETAL,Pick-Up (New + Revival),Easy N/A,Standard 18,Hard 39,Master 70,Unlimited N/A,
100812,surviv,サバイバル,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 24,Hard 40,Master 65,Unlimited N/A,
100814,turkis3,トルコ行進曲 (Short Remix),,Pick-Up (New + Revival),Easy N/A,Standard 6,Hard 20,Master 48,Unlimited N/A,
100815,picora2,ピコラセテ (Instrumental Ver.),TORIENA,Pick-Up (New + Revival),Easy N/A,Standard 28,Hard 53,Master 80,Unlimited N/A,
100816,fortis,フォルテシモ,らむだーじゃん,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 37,Master 53,Unlimited N/A,
100817,hedban,ヘドバンギャー!!,BABYMETAL,Pick-Up (New + Revival),Easy N/A,Standard 16,Hard 43,Master 66,Unlimited N/A,
100818,megitu,メギツネ,BABYMETAL,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 30,Master 49,Unlimited N/A,
100819,rockma,ロックマン (CUTMAN STAGE),Remixed by てつ×ねこ,Pick-Up (New + Revival),Easy N/A,Standard 27,Hard 48,Master 73,Unlimited N/A,
100820,kounen2,光年konen-Remix Ver.-,小野秀幸,Pick-Up (New + Revival),Easy N/A,Standard 21,Hard 43,Master 73,Unlimited N/A,
100821,saisyu,最終回STORY,MY FIRST STORY,Pick-Up (New + Revival),Easy N/A,Standard 18,Hard 36,Master 56,Unlimited N/A,
100822,yuukan,勇敢 i tout,kamejack,Pick-Up (New + Revival),Easy N/A,Standard 22,Hard 33,Master 78,Unlimited N/A,
100823,modern,彼女の“Modern…” CROSS×BEATS Remix,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 32,Master 56,Unlimited N/A,
100824,miraie,未来へのプレリュード,カヒーナムジカ,Pick-Up (New + Revival),Easy N/A,Standard 21,Hard 35,Master 66,Unlimited N/A,
100825,ranfes,狂乱セレブレーション,Yamajet,Pick-Up (New + Revival),Easy N/A,Standard 20,Hard 42,Master 65,Unlimited N/A,
100826,nemure,眠れない歌,iru,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 38,Master 67,Unlimited 76,
100827,yuwaku,誘惑,GLAY,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 26,Master 43,Unlimited N/A,
100828,dontst,Don't Stop The Music feat.森高千里,tofubeats,Pick-Up (New + Revival),Easy N/A,Standard 15,Hard 32,Master 56,Unlimited 70,
100829,mottai,もったいないとらんど,きゃりーぱみゅぱみゅ,Pick-Up (New + Revival),Easy N/A,Standard 10,Hard 26,Master 36,Unlimited N/A,
100830,slysly,SLY,RIP SLYME,Pick-Up (New + Revival),Easy N/A,Standard 10,Hard 33,Master 58,Unlimited N/A,
100831,lookam,(Where's)THE SILENT MAJORITY?,高橋優,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 34,Master 67,Unlimited N/A,
100832,feverr,フィーバー,パスピエ,Pick-Up (New + Revival),Easy N/A,Standard 28,Hard 48,Master 68,Unlimited N/A,
100833,fashio,ファッションモンスター,きゃりーぱみゅぱみゅ,Pick-Up (New + Revival),Easy N/A,Standard 8,Hard 24,Master 39,Unlimited N/A,
100834,hagito,「ハギとこ!」のテーマ,ハギー,Pick-Up (New + Revival),Easy N/A,Standard 12,Hard 26,Master 50,Unlimited N/A,
100835,invade,インベーダーインベーダー,きゃりーぱみゅぱみゅ,Pick-Up (New + Revival),Easy N/A,Standard 10,Hard 28,Master 47,Unlimited N/A,
100836,ainoch,愛の地球祭,チームしゃちほこ,Pick-Up (New + Revival),Easy N/A,Standard 17,Hard 40,Master 59,Unlimited N/A,
100837,nakama,仲間を探したい,神聖かまってちゃん,Pick-Up (New + Revival),Easy N/A,Standard 14,Hard 32,Master 53,Unlimited N/A,
100838,ninjar,にんじゃりばんばん,きゃりーぱみゅぱみゅ,Pick-Up (New + Revival),Easy N/A,Standard 8,Hard 23,Master 41,Unlimited 65,
100839,parall,パラレルスペック,ゲスの極み乙女。,Pick-Up (New + Revival),Easy N/A,Standard 14,Hard 35,Master 61,Unlimited N/A,
100840,yukifu,雪降る夜にキスして,バンドじゃないもん!,Pick-Up (New + Revival),Easy N/A,Standard 13,Hard 29,Master 51,Unlimited N/A,
100841,furiso,ふりそでーしょん,きゃりーぱみゅぱみゅ,Pick-Up (New + Revival),Easy N/A,Standard 12,Hard 24,Master 44,Unlimited 74,
100842,honeyj,HONEY♡SUNRiSE jun Side,jun with Aimee,Original,Easy 24,Standard 32,Hard 63,Master 88,Unlimited 93,
100843,emeraj,EMERALD♡KISS jun Side,jun with Aimee,Original,Easy 19,Standard 30,Hard 53,Master 85,Unlimited N/A,
100844,dazzlo,DAZZLING♡SEASON (Original Side),jun,Original,Easy 16,Standard 35,Hard 60,Master 80,Unlimited 90,
100844,shares,SHARE SONG,SHARE SONG,Original,Easy N/A,Standard N/A,Hard N/A,Master N/A,Unlimited N/A,
1 index mcode name artist category easy standard hard master unlimited
2 100000 tutori2 Tutorial Tutorial Unknown Easy N/A Standard N/A Hard N/A Master N/A Unlimited N/A
3 100000 tutori3 Tutorial Tutorial Unknown Easy N/A Standard N/A Hard N/A Master N/A Unlimited N/A
4 100000 tutori4 Tutorial Tutorial Pick-Up J-Pop (New) Easy N/A Standard N/A Hard N/A Master N/A Unlimited N/A
5 100000 tutori6 Tutorial Tutorial Pick-Up J-Pop (New) Easy N/A Standard N/A Hard N/A Master N/A Unlimited N/A
6 100000 tutori8 白鳥の湖 (Short Remix) Original Easy N/A Standard 3 Hard 15 Master 35 Unlimited N/A
7 100300 sateli Satellite System ft.Diana Chiaki GRATEC MOUR Original Easy 17 Standard 28 Hard 49 Master 77 Unlimited 82
8 100301 nature Human Nature Z pinkpong Original Easy 5 Standard 14 Hard 24 Master 53 Unlimited 75
9 100307 purple DEEP PURPLE NAOKI Original Easy 14 Standard 22 Hard 54 Master 64 Unlimited 73
10 100308 hearts Heartstrings Nhato Original Easy 8 Standard 18 Hard 38 Master 68 Unlimited 77
11 100310 phasea Phase Angel OCOT Original Easy 9 Standard 16 Hard 38 Master 65 Unlimited 75
12 100311 planet Planet Calling Nyolfen Original Easy 10 Standard 17 Hard 36 Master 49 Unlimited 71
13 100314 firefo Firefox Go-qualia Original Easy 7 Standard 13 Hard 36 Master 57 Unlimited 83
14 100315 kounen 光年(konen) 小野秀幸 Original Easy 10 Standard 21 Hard 40 Master 66 Unlimited 78
15 100316 essenc Another Essence RAM Original Easy 11 Standard 25 Hard 50 Master 70 Unlimited 76
16 100317 summer Summer End Anthem Personative Original Easy 13 Standard 23 Hard 57 Master 79 Unlimited 89
17 100319 tanosi たのしいことだけ Yamajet Original Easy 16 Standard 25 Hard 45 Master 70 Unlimited 80
18 100320 picora ピコラセテ TORIENA Original Easy 8 Standard 15 Hard 38 Master 66 Unlimited 75
19 100323 devils Devil's Classic Tatsh Original Easy 15 Standard 27 Hard 40 Master 80 Unlimited N/A
20 100328 techno Techno Highway SIMON Original Easy 9 Standard 16 Hard 38 Master 51 Unlimited 74
21 100335 glowww GLOW Shoichiro Hirata feat. Ellie Original Easy 8 Standard 17 Hard 28 Master 42 Unlimited 60
22 100336 powerr Power Dubscribe Original Easy 12 Standard 19 Hard 38 Master 69 Unlimited 79
23 100340 amater Amateras Sakuzyo Original Easy 13 Standard 21 Hard 48 Master 65 Unlimited 79
24 100349 advers Adverse Effect Rin Original Easy 9 Standard 15 Hard 48 Master 71 Unlimited 83
25 100353 venera Venerated Tosh Original Easy 8 Standard 15 Hard 43 Master 68 Unlimited 75
26 100357 dazaii 堕罪 HAKKYOU-KUN feat.せつな Original Easy 12 Standard 21 Hard 43 Master 73 Unlimited 77
27 100365 thesig The Signs Of The Last Day SLAKE Original Easy 10 Standard 21 Hard 38 Master 56 Unlimited 73
28 100344 hosita 星達のメロディ ゆいこんぬ Original Easy 10 Standard 16 Hard 36 Master 48 Unlimited 65
29 100372 bluede Blue Destiny Blue NAOKI feat. Florence McNair Original Easy 12 Standard 22 Hard 41 Master 58 Unlimited 70
30 100373 emerao EMERALD♡KISS ~Original Side~ jun with Aimee Original Easy 19 Standard 30 Hard 53 Master 85 Unlimited N/A
31 100129 megaro MEGALOMAN[i]A TITANZ Original Easy 0 Standard 55 Hard 80 Master 93 Unlimited 98
32 100330 angeli angelik-vice void Original Easy 22 Standard 33 Hard 56 Master 82 Unlimited 90
33 100342 moonli 月鳴 -moonlit urge- AZURE FACTORY Original Easy 8 Standard 14 Hard 43 Master 61 Unlimited 73
34 100369 yumemi ユメミル船 yozuca* Original Easy 6 Standard 12 Hard 35 Master 59 Unlimited 69
35 100348 pinkym Pinky Magic Junk Original Easy 16 Standard 24 Hard 44 Master 74 Unlimited 81
36 100370 dynami2 DYNAMITE SENSATION REV. NAOKI feat. Hyphen Original Easy 8 Standard 18 Hard 51 Master 78 Unlimited 80
37 100306 reseed3 Reseed (Another Edit) quick master Original Easy 10 Standard 20 Hard 55 Master 76 Unlimited 80
38 100002 toucho Touch Of Gold Togo Project feat. Frances Maya Original Easy 5 Standard 9 Hard 28 Master 44 Unlimited 65
39 100003 ameoto 雨の音が虹を呼ぶ Barbarian On The Groove feat.霜月はるか Original Easy 6 Standard 12 Hard 26 Master 47 Unlimited 63
40 100004 kimito キミとMUSIC CooRie Original Easy 7 Standard 10 Hard 26 Master 49 Unlimited 66
41 100021 giantk Giant Killing R-Lab Original Easy 11 Standard 25 Hard 53 Master 71 Unlimited 78
42 100015 breakd Break down GARNiDELiA Original Easy 11 Standard 23 Hard 34 Master 57 Unlimited 74
43 100028 dazzlj DAZZLING♡SEASON (Japanese Side) jun Original Easy 16 Standard 35 Hard 60 Master 80 Unlimited 90
44 100093 ididid I.D. Tatsh feat. 彩音 Original Easy 16 Standard 29 Hard 46 Master 72 Unlimited 81
45 100042 sundro Sundrop Yamajet Original Easy 14 Standard 24 Hard 47 Master 75 Unlimited 83
46 100063 auflcb some day (instrumental) NAOKI Original Easy 8 Standard 13 Hard 43 Master 81 Unlimited N/A
47 100045 dennou 電脳少女は歌姫の夢を見るか? デスおはぎ feat.蛮 Original Easy 15 Standard 29 Hard 60 Master 76 Unlimited 87
48 100068 hokoro ホコロビシロガールズ むかしばなし Original Easy 14 Standard 29 Hard 57 Master 71 Unlimited 81
49 100005 landin Landing on the moon SIMON Original Easy 13 Standard 26 Hard 33 Master 49 Unlimited 67
50 100362 tomorr Tomorrow 桜井零士 Original Easy 8 Standard 15 Hard 24 Master 44 Unlimited 62
51 100363 daybyd day by day 海辺 Original Easy 6 Standard 13 Hard 26 Master 38 Unlimited 59
52 100309 syoujo 生々世々 SADA Original Easy 8 Standard 19 Hard 35 Master 53 Unlimited 78
53 100352 destru Destrudo D-Fener Original Easy 10 Standard 19 Hard 41 Master 62 Unlimited 72
54 100041 gingat Re:Milky way イトヲカシ Original Easy 5 Standard 13 Hard 29 Master 46 Unlimited 61
55 100066 daisak 大殺界がらくたシンパシー まふまふ Original Easy 12 Standard 28 Hard 36 Master 60 Unlimited 75
56 100376 paradi Paradise Regained LC:AZE feat.chakk Original Easy 10 Standard 16 Hard 28 Master 53 Unlimited 64
57 100377 pigooo PIG-O NNNNNNNNNN Original Easy 13 Standard 19 Hard 34 Master 59 Unlimited 84
58 100386 season The Four Seasons -SPRING- (Remix Ver.) Variety Easy 8 Standard 15 Hard 28 Master 44 Unlimited 65
59 100387 canonn カノン (Remix Ver.) Variety Easy 7 Standard 17 Hard 28 Master 50 Unlimited 83
60 100388 rhapso Rhapsody in Blue (Remix Ver.) Variety Easy 6 Standard 18 Hard 34 Master 62 Unlimited 74
61 100389 turkis トルコ行進曲 (Remix Ver.) Variety Easy 11 Standard 19 Hard 39 Master 64 Unlimited 84
62 100390 biohaz code_ umbrella Cores Variety Easy 6 Standard 15 Hard 30 Master 51 Unlimited 64
63 100391 monhan 英雄の証 ~ 4Version カプコンサウンドチーム Variety Easy 5 Standard 10 Hard 26 Master 36 Unlimited 54
64 100392 gyakut2 追求 ~最終プロモーションバージョン (crossbeats REV.アレンジ) 岩垂 徳行 Variety Easy 5 Standard 13 Hard 35 Master 43 Unlimited 56
65 100393 street Theme of Ryu -SFIV Arrange- Capcom Sound Team / Hideyuki Fukasawa Variety Easy 7 Standard 13 Hard 34 Master 47 Unlimited 66
66 100394 rockma2 Dr. WILY STAGE 1 -OMEGAMAN MIX- ROCK-MEN Variety Easy 14 Standard 21 Hard 34 Master 49 Unlimited 76
67 100374 auflcb3 SOMEDAY -00.prologue- TЁЯRA Original Easy 6 Standard 16 Hard 36 Master 66 Unlimited 86
68 100325 irohaa Iroha Ryunosuke Kudo Original Easy 12 Standard 19 Hard 41 Master 55 Unlimited 76
69 100326 ibelie I Believe Someday SPARKER Original Easy 14 Standard 27 Hard 47 Master 78 Unlimited 82
70 100409 monhan2 灼熱の刃 ~ ディノバルド カプコンサウンドチーム Variety Easy 6 Standard 12 Hard 24 Master 43 Unlimited 68
71 100410 monhan3 古代の息吹き カプコンサウンドチーム Variety Easy 8 Standard 18 Hard 28 Master 45 Unlimited 73
72 100418 yejiii YEJI ginkiha Original Easy 10 Standard 22 Hard 36 Master 63 Unlimited 79
73 100419 histor HISTORIA Cranky Original Easy 11 Standard 20 Hard 36 Master 56 Unlimited 82
74 100338 chaset Chase the WAVE Tatsh feat. AKINO with bless4 Original Easy 8 Standard 15 Hard 31 Master 58 Unlimited 76
75 100412 metall Metallical parade Vice Principal Original Easy 8 Standard 16 Hard 28 Master 57 Unlimited 77
76 100327 letmeg Let Me Give You My Heart brinq Original Easy 12 Standard 18 Hard 32 Master 50 Unlimited 72
77 100010 hontno ホントのワタシ mao Original Easy 9 Standard 12 Hard 26 Master 53 Unlimited 66
78 100024 azitat Azitate void Original Easy 14 Standard 24 Hard 55 Master 70 Unlimited 83
79 100360 hellom Hello Mr.crosbie 民安★ROCK Original Easy 7 Standard 18 Hard 37 Master 58 Unlimited 72
80 100337 laught Perfect laughter ぽんず loved by yksb Original Easy 7 Standard 20 Hard 35 Master 51 Unlimited 71
81 100426 bluede2 Blue Destiny Blue ETERNAL NAOKI feat. Florence McNair Original Easy 9 Standard 16 Hard 36 Master 56 Unlimited 81
82 100423 street2 Ultra Street Fighter IV Hideyuki Fukasawa Variety Easy 14 Standard 21 Hard 37 Master 55 Unlimited 73
83 100424 street3 Theme of Chun-Li -SFIV Arrange- Capcom Sound Team / Hideyuki Fukasawa Variety Easy 13 Standard 24 Hard 38 Master 57 Unlimited 74
84 100425 street4 Street Fighter V Masahiro Aoki Variety Easy 11 Standard 17 Hard 32 Master 51 Unlimited 78
85 100421 silbur Silbury Sign カヒーナムジカ Original Easy 9 Standard 20 Hard 35 Master 54 Unlimited 75
86 100422 spicaa Spica Endorfin. Original Easy 10 Standard 23 Hard 36 Master 56 Unlimited 78
87 100438 tricko Trick Or Treat SLAKE Original Easy 11 Standard 23 Hard 34 Master 56 Unlimited 75
88 100435 thisis THIS IS HDM Relect Original Easy 12 Standard 23 Hard 37 Master 60 Unlimited 74
89 100436 rising Rising Day ft. Satan GRATEC MOUR Original Easy 14 Standard 23 Hard 38 Master 66 Unlimited 85
90 100411 orbita Orbital velocity Vice Principal Original Easy 12 Standard 20 Hard 38 Master 62 Unlimited 74
91 100433 dddddd D 六弦アリス Original Easy 9 Standard 13 Hard 33 Master 53 Unlimited 69
92 100427 pyroma Pyromania KO3 Original Easy 8 Standard 22 Hard 42 Master 70 Unlimited 84
93 100312 touchn Touch n Go Paisley Parks Original Easy 15 Standard 27 Hard 46 Master 68 Unlimited 86
94 100359 onlyll only L emon Original Easy 13 Standard 21 Hard 32 Master 56 Unlimited 69
95 100313 upside Upside Down Nave ft.Mayu Wakisaka Original Easy 8 Standard 18 Hard 27 Master 48 Unlimited 67
96 100322 istanb İstanbul REVen-G Original Easy 23 Standard 41 Hard 49 Master 90 Unlimited 98
97 100371 memori Memoria ~終焉を司る荊姫の静粛なる宴~ Astilbe × arendsii Original Easy 13 Standard 26 Hard 38 Master 65 Unlimited 84
98 100350 straye Strayer Taishi Original Easy 14 Standard 25 Hard 36 Master 61 Unlimited 73
99 100358 rearhy Rearhythm CooRie Original Easy 7 Standard 17 Hard 32 Master 52 Unlimited 69
100 100432 hereco Here comes the sun ~For you~ Z pinkpong Original Easy 11 Standard 16 Hard 34 Master 51 Unlimited 68
101 100441 thesun THE SUN Tatsh Original Easy 13 Standard 25 Hard 40 Master 72 Unlimited 87
102 100343 sayona さよなら最終列車 むかしばなし Original Easy 10 Standard 20 Hard 34 Master 53 Unlimited 71
103 100380 flameu Flame Up Inu Machine Original Easy 10 Standard 18 Hard 36 Master 57 Unlimited 65
104 100434 raidon RAiD on Mars sky_delta Original Easy 13 Standard 30 Hard 38 Master 58 Unlimited 87
105 100437 riseup Rise Up Dubscribe Original Easy 7 Standard 18 Hard 41 Master 67 Unlimited 77
106 100431 sunglo Sunglow Yamajet feat. ひうらまさこ Original Easy 10 Standard 18 Hard 39 Master 59 Unlimited 72
107 100439 kinbos 金星(kinboshi) Hideyuki Ono Original Easy 12 Standard 22 Hard 38 Master 64 Unlimited 77
108 100430 densho 電脳少女と機械仕掛けの神 Chimera music. Original Easy 17 Standard 28 Hard 42 Master 74 Unlimited 90
109 100471 aiohoo 愛をほおばりたいッ!~Like a Monkey!~ 新堂敦士 J-Pop Easy 10 Standard 18 Hard 29 Master 42 Unlimited 66
110 100472 entert エンターテイナー (Remix ver.) Variety Easy 9 Standard 15 Hard 33 Master 54 Unlimited 87
111 100457 takeit Take It Back Daniel Seven Original Easy 12 Standard 20 Hard 38 Master 65 Unlimited 83
112 100449 harmon Harmony ピクセルビー Original Easy 12 Standard 20 Hard 36 Master 49 Unlimited 65
113 100428 avemar アヴェ・マリア (Remix ver.) Variety Easy 8 Standard 16 Hard 31 Master 53 Unlimited 75
114 100429 mateki 復讐の炎は地獄のように我が心に燃え (Remix ver.) Variety Easy 10 Standard 18 Hard 35 Master 54 Unlimited 79
115 100445 lovech LOVE CHASE 大島はるな Original Easy 8 Standard 16 Hard 30 Master 46 Unlimited 68
116 100473 akaihe 赤いヘッドホン 新堂敦士 J-Pop Easy 8 Standard 21 Hard 32 Master 50 Unlimited 69
117 100474 juicys Juicy! ~幸せスパイラル~ 新堂敦士 J-Pop Easy 10 Standard 18 Hard 26 Master 45 Unlimited 83
118 100468 codena CODENAMEはEQ TORIENA Original Easy 9 Standard 18 Hard 35 Master 48 Unlimited 68
119 100475 groove LINK LINK FEVER!!!(グルーヴコースター 3 リンクフィーバーより) リンカ (CV:豊田萌絵) Variety Easy 10 Standard 22 Hard 40 Master 52 Unlimited 73
120 100450 kansho 観賞用マーメイド ヤマイ Original Easy 7 Standard 13 Hard 27 Master 55 Unlimited 74
121 100486 overcl2 Over Clock ~前兆~ NAOKI feat. un∞limited Original Easy 12 Standard 23 Hard 42 Master 58 Unlimited 74
122 100483 taikoo SAKURA EXHAUST RIO HAMAMOTO(BNSI)「太鼓の達人」より Variety Easy 6 Standard 13 Hard 39 Master 50 Unlimited 75
123 100480 groove2 QLWA(グルーヴコースター 3 リンクフィーバーより) t+pazolite Variety Easy 9 Standard 15 Hard 40 Master 58 Unlimited 85
124 100487 overcl Over Clock ~開放~ NAOKI feat. un∞limited Original Easy 8 Standard 12 Hard 35 Master 57 Unlimited 86
125 100466 notoss Notos ginkiha Original Easy 8 Standard 12 Hard 42 Master 65 Unlimited 91
126 100447 machup マチュ☆ピチュ コツキミヤ Original Easy 9 Standard 17 Hard 32 Master 41 Unlimited 71
127 100488 groove3 カリソメ(グルーヴコースター 3 リンクフィーバーより) コンプ(豚乙女) × ichigo(岸田教団 & THE明星ロケッツ) Touhou + Variety Easy 10 Standard 18 Hard 34 Master 64 Unlimited 85
128 100489 groove4 そして誰もいなくなった(グルーヴコースター 3 リンクフィーバーより) コバヤシユウヤ(IOSYS) × あにー(TaNaBaTa) Touhou + Variety Easy 12 Standard 22 Hard 35 Master 50 Unlimited 75
129 100482 everyt EVERYTHING Tatsh feat.小田ユウ Original Easy 13 Standard 22 Hard 30 Master 74 Unlimited N/A
130 100465 lespri L'esprit Cosine Original Easy 13 Standard 25 Hard 57 Master 80 Unlimited N/A
131 100491 groove5 グルーヴ・ザ・ハート(グルーヴコースター 3 リンクフィーバーより) ビートまりお+あまね Variety Easy 14 Standard 24 Hard 37 Master 67 Unlimited N/A
132 100490 honeyo HONEY♡SUNRiSE ~Original Side~ jun with Aimee Original Easy 24 Standard 32 Hard 63 Master 88 Unlimited 93
133 100494 groove6 Got hive of Ra(グルーヴコースター 3 リンクフィーバーより) E.G.G. Variety Easy 22 Standard 30 Hard 64 Master 79 Unlimited N/A
134 100495 sunglo2 Sunglow (Happy Hardcore Style) Yamajet feat. ひうらまさこ Original Easy 11 Standard 21 Hard 36 Master 67 Unlimited 81
135 100498 fourte 14th Clock INNOCENT NOIZE Original Easy 14 Standard 24 Hard 50 Master 74 Unlimited 80
136 100496 monhan4 英雄の証/MHF-G 2015 Version 若林タカツグ Variety Easy 5 Standard 12 Hard 40 Master 51 Unlimited 62
137 100497 monhan5 異ヲ辿リシモノ -対峙- 若林タカツグ Variety Easy 10 Standard 12 Hard 35 Master 42 Unlimited 65
138 100504 darkpa Dark Parashu INNOCENT NOIZE Original Easy 16 Standard 26 Hard 39 Master 70 Unlimited 84
139 100505 hervor Hervor INNOCENT NOIZE Original Easy 18 Standard 28 Hard 39 Master 73 Unlimited 81
140 100499 cirnon チルノのパーフェクトさんすう教室 ARM+夕野ヨシミ (IOSYS) feat. miko Touhou Easy 17 Standard 24 Hard 40 Master 60 Unlimited 79
141 100500 marisa 魔理沙は大変なものを盗んでいきました ARM+夕野ヨシミ (IOSYS) feat. 藤咲かりん Touhou Easy 18 Standard 25 Hard 41 Master 62 Unlimited 85
142 100501 yakini 究極焼肉レストラン!お燐の地獄亭! ARM+夕野ヨシミ (IOSYS) feat. 藤枝あかね Touhou Easy 13 Standard 25 Hard 34 Master 58 Unlimited 82
143 100502 justic ジャスティス・オブ・ザ・界隈 ~ALL IS FAIR IN LOVE AND ALIMARI~ void (IOSYS) feat.山本椛 Touhou Easy 14 Standard 19 Hard 36 Master 57 Unlimited 83
144 100503 sintyo 進捗どうですか? sumijun feat.ななひら Touhou Easy 16 Standard 25 Hard 46 Master 70 Unlimited 83
145 100347 ascand Ascendanz void Original Easy 18 Standard 32 Hard 54 Master 80 Unlimited 90
146 100506 blackl Black Lotus Maozon Original Easy 12 Standard 19 Hard 41 Master 73 Unlimited 84
147 100043 childr チルドレン・オートマトン~ある歌声の亡霊~ あさまっく Original Easy 14 Standard 24 Hard 39 Master 56 Unlimited 62
148 100044 tsukai ツカイステ・デッドワールド コゲ犬×ゆちゃ Original Easy 13 Standard 19 Hard 44 Master 72 Unlimited 76
149 100067 rideon RIDE ON NOW! さつき が てんこもり feat.un:c Original Easy 16 Standard 29 Hard 41 Master 60 Unlimited 80
150 100507 minest Minestrone orangentle Original Easy 13 Standard 21 Hard 39 Master 62 Unlimited 76
151 100508 ordine Ordine orangentle Original Easy 19 Standard 25 Hard 43 Master 73 Unlimited 82
152 100509 dreamw DReamWorKer LC:AZE Original Easy 16 Standard 26 Hard 37 Master 62 Unlimited 75
153 100510 minerv Minerva xi Original Easy 25 Standard 32 Hard 61 Master 90 Unlimited N/A
154 100001 wannab Wanna Be Your Special Shoichiro Hirata feat. SUIMI Original Easy 5 Standard 9 Hard 23 Master 40 Unlimited 65
155 100511 sekain 世界の果て Yamajet Original Easy 16 Standard 26 Hard 39 Master 69 Unlimited 78
156 100512 farawa Faraway ミフメイ Original Easy 18 Standard 23 Hard 36 Master 60 Unlimited 76
157 100100 crissc Crisscrosser void Original Easy 17 Standard 37 Hard 63 Master 86 Unlimited 91
158 100324 speedy Awake Speedy DJ MURASAME Original Easy 11 Standard 22 Hard 55 Master 77 Unlimited N/A
159 100513 xxxrev XXX-revolt void feat. KOTOKO Original Easy 15 Standard 21 Hard 34 Master 56 Unlimited 73
160 100016 higame Hi Go-qualia Original Easy 13 Standard 20 Hard 30 Master 58 Unlimited 71
161 100022 theepi The Epic Cranky Original Easy 14 Standard 19 Hard 40 Master 61 Unlimited 75
162 100023 anomie Anomie D-Fener Original Easy 15 Standard 22 Hard 38 Master 61 Unlimited 77
163 100524 crocus Crocus 村瀬悠太 Original Easy 15 Standard 26 Hard 37 Master 60 Unlimited 72
164 100546 lavien La vie en Fleurs VILA Original Easy 18 Standard 27 Hard 41 Master 71 Unlimited 80
165 100361 megaro2 MEGALOMAN[i]A -2nd IMPACT- NEO-G Original Easy N/A Standard N/A Hard N/A Master 99 Unlimited 100
166 100541 chipnn Chip Notch Educ@tion yaseta feat. chip_Notch Original Easy 16 Standard 27 Hard 34 Master 61 Unlimited 79
167 100007 yiyoyi Wanyo Wanyo MC Natsack Original Easy 7 Standard 14 Hard 33 Master 56 Unlimited 70
168 100014 binary Binary Overdrive フラット3rd Original Easy 14 Standard 17 Hard 35 Master 64 Unlimited 89
169 100054 makaim 魔界村 (平地BGM) Remixed by ARM (IOSYS) Original + Variety Easy 23 Standard 30 Hard 50 Master 77 Unlimited N/A
170 100055 gyakut 逆転裁判 (綾里真宵 ~逆転姉妹のテーマ) Remixed by OSTER project Original + Variety Easy 6 Standard 15 Hard 21 Master 46 Unlimited 64
171 100056 basara 戦国BASARA (SENGOKU BASARA) Remixed by SOUND HOLIC Original + Variety Easy 14 Standard 19 Hard 37 Master 64 Unlimited 73
172 100514 daybre DAYBREAK FRONTLINE Orangestar Vocaloid Easy 9 Standard 16 Hard 32 Master 53 Unlimited 72
173 100515 umiyur ウミユリ海底譚 n-buna Vocaloid Easy 8 Standard 14 Hard 28 Master 46 Unlimited 64
174 100516 chalur シャルル バルーン Vocaloid Easy 14 Standard 18 Hard 40 Master 60 Unlimited 72
175 100517 melanc メランコリック Junky Vocaloid Easy 10 Standard 15 Hard 30 Master 50 Unlimited 63
176 100518 konofu このふざけた素晴らしき世界は、僕の為にある n.k Vocaloid Easy 11 Standard 23 Hard 35 Master 62 Unlimited 81
177 100526 bladem The Blade Master mikashu Original Easy 17 Standard 28 Hard 38 Master 63 Unlimited 75
178 100536 southw South wind moimoi Original Easy 12 Standard 18 Hard 27 Master 57 Unlimited 68
179 100537 ryuuse 流星デモクラシー kamejack Original Easy 13 Standard 21 Hard 32 Master 59 Unlimited N/A
180 100519 redhea ROCK'N'ROLL☆FLYING REDHEAD 暁Records Touhou Easy 10 Standard 27 Hard 39 Master 59 Unlimited 72
181 100520 warnin WARNING×WARNING×WARNING 暁Records Touhou Easy 12 Standard 25 Hard 36 Master 61 Unlimited 74
182 100521 topsec TOP SECRET -My Red World- 暁Records Touhou Easy 13 Standard 24 Hard 34 Master 51 Unlimited 64
183 100522 dddoll DOWN DOWN DOLL 暁Records Touhou Easy 14 Standard 26 Hard 38 Master 55 Unlimited 63
184 100548 tracee トレイス・エゴイズム 暁Records Touhou Easy 9 Standard 19 Hard 31 Master 49 Unlimited 65
185 100111 drivin Driving story Duca Original Easy 8 Standard 23 Hard 40 Master 66 Unlimited 76
186 100118 genzit 現実幻覚スピードスター yozuca* Original Easy 12 Standard 18 Hard 46 Master 73 Unlimited 82
187 100039 aerial エアリアル カヒーナムジカ Original Easy 5 Standard 11 Hard 28 Master 56 Unlimited 71
188 100532 einher Einherjar 閣下 Original Easy 16 Standard 29 Hard 40 Master 74 Unlimited 80
189 100540 ariell Ariel nanobii Original Easy 15 Standard 19 Hard 32 Master 64 Unlimited 73
190 100542 firstl First Love UFO Original Easy 17 Standard 25 Hard 36 Master 65 Unlimited 77
191 100550 heartl Heartland Bernis Original Easy 11 Standard 23 Hard 30 Master 64 Unlimited N/A
192 100551 erasee ERASE MozSound Original Easy 12 Standard 22 Hard 35 Master 58 Unlimited 68
193 100530 regene Regeneration ray Tsukasa Original Easy 13 Standard 20 Hard 30 Master 56 Unlimited 70
194 100549 allelu アレルヤ HAKKYOU-KUN feat.玉置成実 Original Easy 16 Standard 28 Hard 35 Master 64 Unlimited 75
195 100543 lighto Light of my Life S3RL Original Easy 12 Standard 25 Hard 33 Master 60 Unlimited 74
196 100552 termin Terminus a quo ginkiha Original Easy 13 Standard 24 Hard 34 Master 63 Unlimited 79
197 100556 ryuuse2 流星でもくらちー☆ kamejack Original Easy 13 Standard 20 Hard 36 Master 62 Unlimited 75
198 100547 prizmm PRIZM ミフメイ Original Easy 12 Standard 21 Hard 30 Master 54 Unlimited N/A
199 100098 samalv サマ★ラブ コツキミヤ Original Easy 13 Standard 19 Hard 39 Master 58 Unlimited 77
200 100544 palpit Palpitation Zekk Original Easy 18 Standard 29 Hard 55 Master 84 Unlimited 92
201 100558 gainen Break the Wall!! ~ロンリガイネン 暁Records Original Easy 15 Standard 26 Hard 37 Master 63 Unlimited N/A
202 100525 moonsh Moon Shard satella Original Easy 10 Standard 23 Hard 36 Master 62 Unlimited N/A
203 100559 moonki MoonLightKiss effe Original Easy 13 Standard 25 Hard 39 Master 64 Unlimited N/A
204 100560 moonri Moonrise Relect Original Easy 14 Standard 21 Hard 38 Master 58 Unlimited 85
205 100561 goaway Go Away Cranky Original Easy 10 Standard 23 Hard 45 Master 59 Unlimited 70
206 100567 itback Bring it back now siromaru Original Easy 12 Standard 23 Hard 38 Master 71 Unlimited N/A
207 100569 redhhh Red Heart Yooh vs. siromaru Original Easy 13 Standard 24 Hard 39 Master 77 Unlimited N/A
208 100568 actual Actual Reverse siromaru Original Easy 14 Standard 25 Hard 38 Master 80 Unlimited N/A
209 100367 zonzon Bi-Zon Zon Zombi MC Natsack Original Easy 5 Standard 16 Hard 33 Master 63 Unlimited 67
210 100565 memorm Memorim Avans Original Easy 15 Standard 26 Hard 37 Master 73 Unlimited N/A
211 100554 kokoro ココロメソッド Endorfin. Original Easy 12 Standard 20 Hard 43 Master 65 Unlimited 69
212 100563 poweri Power is Power KO3 Original Easy 13 Standard 26 Hard 49 Master 75 Unlimited 91
213 100555 nisenn 2020 Ω Original Easy N/A Standard N/A Hard N/A Master 76 Unlimited N/A
214 100096 yukiya Vespero Monotone Rhythm feat.綾川雪弥 Original Easy 11 Standard 19 Hard 40 Master 61 Unlimited N/A
215 100124 zankyo 残響のアカーシャ Astilbe × arendsii Original Easy 10 Standard 18 Hard 38 Master 57 Unlimited 74
216 100119 overlp オーバーラップ millie loved by yksb Original Easy 9 Standard 17 Hard 30 Master 51 Unlimited N/A
217 100529 fracta Fractalize Sakuzyo Original Easy 19 Standard 31 Hard 52 Master 83 Unlimited N/A
218 100455 cantst Can't Stop KaSa Original Easy 11 Standard 23 Hard 42 Master 65 Unlimited N/A
219 100527 primaa Prima Kiryu(桐生) Original Easy 12 Standard 18 Hard 35 Master 54 Unlimited 75
220 100448 cyberg CYBER GANG ヒゲドライVAN Original Easy 12 Standard 23 Hard 35 Master 60 Unlimited N/A
221 100018 freakw Freak With Me SLAKE Original Easy 13 Standard 22 Hard 42 Master 65 Unlimited 66
222 100006 aquali Aqualight MAYA AKAI Original Easy 11 Standard 16 Hard 34 Master 58 Unlimited N/A
223 100572 takesc Music Takes Control Fierce Chain Original Easy 10 Standard 27 Hard 37 Master 69 Unlimited N/A
224 100531 cthugh Cthugha MozSound Original Easy 14 Standard 25 Hard 48 Master 73 Unlimited N/A
225 100571 thetaa θ (theta) effe Original Easy 11 Standard 21 Hard 34 Master 62 Unlimited N/A
226 100493 nekofu ネコふんじゃった☆ (クローニャSTYLE) Variety Easy 10 Standard 22 Hard 34 Master 57 Unlimited 80
227 100057 howtru How True Is Your Love brinq Original Easy 8 Standard 12 Hard 25 Master 53 Unlimited 74
228 100047 romanc ロマンシングゲーム まふ×ティン Original Easy 10 Standard 28 Hard 55 Master 78 Unlimited N/A
229 100573 kotobu KOTOBUKI REVen-G Original Easy 25 Standard 32 Hard 71 Master 90 Unlimited N/A
230 100417 xmasss ジングルベル (NM REMIX) Variety Easy 8 Standard 18 Hard 38 Master 56 Unlimited 77
231 100600 galaxy GALAXY キュウソネコカミ J-Pop Easy 10 Standard 16 Hard 32 Master 43 Unlimited 67
232 100601 rebell Rebellion NAOKI underground Original Easy N/A Standard 49 Hard 63 Master 91 Unlimited N/A
233 100602 anothe Another Chance Luci Original Easy N/A Standard 27 Hard 37 Master 73 Unlimited 76
234 100603 addict Addicted luz×アリエP Original Easy N/A Standard 20 Hard 34 Master 52 Unlimited 62
235 100604 dirtyy Dirty Mouth Asletics Original Easy N/A Standard 15 Hard 28 Master 59 Unlimited 74
236 100605 levelf LEVEL5-Judgelight- fripSide J-Pop Easy 5 Standard 11 Hard 28 Master 45 Unlimited 63
237 100606 omnive Omniverse Atomic Original Easy N/A Standard 34 Hard 52 Master 83 Unlimited 86
238 100607 kakuse 覚醒 ∞ awake! PwD Original Easy N/A Standard 17 Hard 55 Master 75 Unlimited N/A
239 100608 unbeli アンビリーバーズ 米津玄師 J-Pop Easy 7 Standard 13 Hard 26 Master 38 Unlimited 62
240 100609 sonzai ソンザイキョウドウタイ ジギル Original Easy N/A Standard 26 Hard 40 Master 59 Unlimited 66
241 100610 okonik OKONIKUKOD SHU OKUYAMA Original Easy N/A Standard 26 Hard 45 Master 67 Unlimited N/A
242 100611 crssho CrossShooter Tatsh Original Easy 10 Standard 35 Hard 60 Master 85 Unlimited N/A
243 100612 reanim Reanimation DC feat.S!N Original Easy N/A Standard 28 Hard 44 Master 70 Unlimited 80
244 100613 kamino kaminoko HAKKYOU-KUN Original Easy 15 Standard 40 Hard 62 Master 78 Unlimited N/A
245 100614 fiveee Five ANOTHER STORY J-Pop Easy 10 Standard 18 Hard 37 Master 61 Unlimited 71
246 100615 granda Grand Arc Tosh Original Easy N/A Standard 21 Hard 38 Master 79 Unlimited N/A
247 100616 fronti2 NEXT FRONTIER -TRUE RISE- NAOKI Original Easy 9 Standard 46 Hard 69 Master 89 Unlimited N/A
248 100617 saigon 最後の1ページ 桜井零士 Original Easy N/A Standard 19 Hard 31 Master 57 Unlimited N/A
249 100618 replay REPLAY VAMPS J-Pop Easy 8 Standard 18 Hard 44 Master 63 Unlimited 70
250 100619 mousou 妄想全開 志麻×ふぉP Original Easy N/A Standard 16 Hard 26 Master 54 Unlimited N/A
251 100620 aheadd AHEAD VAMPS J-Pop Easy 7 Standard 13 Hard 25 Master 35 Unlimited 58
252 100621 musicr1 All You Need Is Beat(s) -musicるTV・ミリオン連発音楽作家塾第7弾- CLONE Original Easy 12 Standard 22 Hard 33 Master 58 Unlimited 74
253 100622 getthe Get the glory 中ノ森文子 J-Pop Easy 6 Standard 17 Hard 37 Master 49 Unlimited 66
254 100623 design Designed World Alinut Original Easy N/A Standard 15 Hard 39 Master 68 Unlimited 69
255 100624 garnet GARNET HOWL フラット3rd Original Easy N/A Standard 26 Hard 46 Master 70 Unlimited 94
256 100625 hopesb Hopes Bright WHITE ASH J-Pop Easy 7 Standard 10 Hard 25 Master 44 Unlimited 61
257 100626 shooti Shooting Star feat.HISASHI (GLAY) 96猫 J-Pop Easy 7 Standard 15 Hard 37 Master 49 Unlimited 69
258 100627 dangan 弾丸と星空 HAKKYOU-KUN Original Easy N/A Standard 28 Hard 58 Master 81 Unlimited N/A
259 100628 impact Impact Tatsh Original Easy 20 Standard 24 Hard 60 Master 72 Unlimited 90
260 100629 lightm Light My Fire KOTOKO J-Pop Easy 11 Standard 26 Hard 33 Master 54 Unlimited 71
261 100630 miiroo 海色 AKINO from bless4 J-Pop Easy 11 Standard 22 Hard 39 Master 58 Unlimited 68
262 100631 voiceo Voice Of House DOT96 Original Easy N/A Standard 18 Hard 34 Master 58 Unlimited 59
263 100632 cosmol Cosmology RIC Original Easy 25 Standard 36 Hard 64 Master 87 Unlimited N/A
264 100633 vividd ViViD May'n J-Pop Easy 9 Standard 16 Hard 35 Master 55 Unlimited 65
265 100634 splash SPLASH MAYA AKAI Original Easy N/A Standard 26 Hard 50 Master 71 Unlimited N/A
266 100635 donuth ドーナツホール ハチ Vocaloid Easy 11 Standard 22 Hard 40 Master 54 Unlimited 80
267 100636 senbon 千本桜 和楽器バンド Vocaloid Easy 12 Standard 20 Hard 28 Master 54 Unlimited 74
268 100637 kmtyju 君と野獣 バンドハラスメント J-Pop Easy 12 Standard 24 Hard 31 Master 57 Unlimited 74
269 100638 fronti NEXT FRONTIER NAOKI Original Easy 13 Standard 48 Hard 65 Master 82 Unlimited N/A
270 100639 nueraa Nu Era SPARKER Original Easy N/A Standard 22 Hard 43 Master 75 Unlimited 53
271 100640 childe CHiLD -error- MY FIRST STORY J-Pop Easy 4 Standard 9 Hard 24 Master 34 Unlimited 56
272 100641 dazzli2 DAZZLING♡SEASON (Darwin Remix) jun Original Easy 19 Standard 35 Hard 60 Master 82 Unlimited N/A
273 100642 perfec Perfectionism 高橋渉 feat.2d6 Original Easy N/A Standard 39 Hard 64 Master 78 Unlimited N/A
274 100643 flower Flowerwall 米津玄師 J-Pop Easy 6 Standard 7 Hard 20 Master 40 Unlimited 65
275 100644 frgmnt Frgmnts Nyolfen Original Easy 10 Standard 33 Hard 63 Master 74 Unlimited 65
276 100645 headph HEADPHONE PARTY A-One Original Easy N/A Standard 24 Hard 32 Master 52 Unlimited N/A
277 100646 crsang Cross+Angel Tatsh feat. 彩音 Original Easy 13 Standard 27 Hard 53 Master 67 Unlimited N/A
278 100647 musicr4 Accept sushi feat.とよだま Original Easy 12 Standard 19 Hard 32 Master 58 Unlimited N/A
279 100648 imaxim A×E×U×G -act.1- 190Cb Original Easy N/A Standard 44 Hard 69 Master 90 Unlimited 87
280 100649 azitat2 Azitate (Prologue Edition) void Original Easy 8 Standard 23 Hard 52 Master 66 Unlimited N/A
281 100650 dynami DYNAMITE SENSATION NAOKI Original Easy 11 Standard 26 Hard 54 Master 68 Unlimited N/A
282 100651 incave Into the Cave Jerico Original Easy N/A Standard 22 Hard 44 Master 76 Unlimited 78
283 100652 aktuki AKATSUKI NAOKI underground Original Easy 10 Standard 26 Hard 58 Master 84 Unlimited N/A
284 100653 kindof Wonderful Fraz Original Easy N/A Standard 14 Hard 29 Master 48 Unlimited N/A
285 100654 mikaku 未確認XX生命体 民安★ROCK Original Easy N/A Standard 19 Hard 31 Master 54 Unlimited N/A
286 100655 strang ストレンジ・ディーヴァ 麹町養蚕館 Original Easy N/A Standard 12 Hard 28 Master 55 Unlimited N/A
287 100656 hesper Hesperides xi Original Easy N/A Standard 36 Hard 61 Master 92 Unlimited 93
288 100657 breaka Break a spell 川田まみ J-Pop Easy 7 Standard 15 Hard 31 Master 45 Unlimited 68
289 100658 myname When You Call My Name Beat Envy Original Easy N/A Standard 6 Hard 14 Master 30 Unlimited 57
290 100659 amaiko 甘い言葉 Kenichi Chiba feat. EVO+ Original Easy N/A Standard 15 Hard 37 Master 60 Unlimited N/A
291 100660 reseed2 Reseed quick master Original Easy N/A Standard 22 Hard 47 Master 63 Unlimited N/A
292 100661 kingst KING STUN JUPITRIS Original Easy 12 Standard 38 Hard 63 Master 74 Unlimited N/A
293 100662 ramram Break Your World RAM Original Easy N/A Standard 23 Hard 34 Master 67 Unlimited N/A
294 100663 murasa Murasame Ryunosuke Kudo Original Easy N/A Standard 28 Hard 41 Master 76 Unlimited N/A
295 100664 happyd Happy Deathday ANOTHER STORY Original Easy 18 Standard 22 Hard 41 Master 73 Unlimited 79
296 100665 izimed イジメ、ダメ、ゼッタイ BABYMETAL J-Pop Easy 9 Standard 19 Hard 39 Master 69 Unlimited 77
297 100666 wastel Wasteland James Taplin Original Easy N/A Standard 4 Hard 12 Master 23 Unlimited 40
298 100667 assign Assign MASAYASU Original Easy N/A Standard 26 Hard 43 Master 61 Unlimited 62
299 100668 jahaci Jahacid DJ SODEYAMA Original Easy N/A Standard 17 Hard 29 Master 59 Unlimited N/A
300 100669 hisuii Hisui stereoberry Original Easy N/A Standard 22 Hard 47 Master 70 Unlimited N/A
301 100670 godkno God knows... 涼宮ハルヒ(C.V.平野綾) J-Pop Easy 6 Standard 10 Hard 26 Master 45 Unlimited 64
302 100671 roadof Road of Resistance BABYMETAL J-Pop Easy 7 Standard 15 Hard 36 Master 50 Unlimited 75
303 100672 rokuch 六兆年と一夜物語 和楽器バンド J-Pop + Vocaloid Easy 11 Standard 21 Hard 35 Master 62 Unlimited 81
304 100673 valent いつか王子様が (Remix Ver.) Original Easy 10 Standard 27 Hard 33 Master 59 Unlimited 77
305 100674 unfini →unfinished→ KOTOKO J-Pop Easy 8 Standard 16 Hard 32 Master 50 Unlimited 71
306 100675 auflcb2 some day -see you again- NAOKI Original Easy 10 Standard 22 Hard 37 Master 75 Unlimited N/A
307 100676 burnin Burning Inside Nhato Original Easy 15 Standard 18 Hard 28 Master 60 Unlimited 85
308 100677 sphere Hypersphere Dubscribe Original Easy N/A Standard 20 Hard 38 Master 73 Unlimited N/A
309 100678 dropou D.O.B. 野水いおり J-Pop Easy 14 Standard 17 Hard 31 Master 46 Unlimited 69
310 100679 xencou X-encounter 黒崎真音 J-Pop Easy 8 Standard 20 Hard 32 Master 52 Unlimited 60
311 100680 killyk killy killy JOKER 分島花音 J-Pop Easy 6 Standard 13 Hard 42 Master 63 Unlimited 76
312 100681 missil the Last Missile Man adHoc World Original Easy N/A Standard 16 Hard 38 Master 59 Unlimited N/A
313 100682 burstt Burst The Gravity ALTIMA J-Pop Easy 7 Standard 12 Hard 25 Master 46 Unlimited 63
314 100683 musicr2 My Recklessness Kagerou Original Easy 12 Standard 22 Hard 33 Master 58 Unlimited N/A
315 100684 isingl Isinglass Voltex Original Easy 12 Standard 25 Hard 44 Master 80 Unlimited N/A
316 100685 lvless Loveless YOSA Original Easy N/A Standard 23 Hard 38 Master 60 Unlimited N/A
317 100686 sapphi Sapphire voltex Original Easy N/A Standard 29 Hard 44 Master 81 Unlimited N/A
318 100687 musicr3 Climaxxx Party -musicるTV・ミリオン連発音楽作家塾第7弾- Kyota. feat.とよだま&れい Original Easy 12 Standard 19 Hard 32 Master 58 Unlimited 72
319 100688 deeout Deep Outside Seiho Original Easy N/A Standard 18 Hard 34 Master 63 Unlimited 81
320 100689 sugars シュガーソングとビターステップ UNISON SQUARE GARDEN J-Pop Easy 6 Standard 17 Hard 30 Master 42 Unlimited 66
321 100690 mercur MERCURY E.Z.M Original Easy N/A Standard 14 Hard 35 Master 66 Unlimited N/A
322 100691 zizizi Z[i] Cybermiso Original Easy N/A Standard 30 Hard 57 Master 88 Unlimited 96
323 100692 wegooo WE GO BREAKERZ J-Pop Easy 10 Standard 18 Hard 34 Master 54 Unlimited 68
324 100693 alonee ALONE MY FIRST STORY J-Pop Easy 5 Standard 11 Hard 21 Master 36 Unlimited 48
325 100694 nuheat Nu Heat Paisley Parks Original Easy N/A Standard 29 Hard 44 Master 65 Unlimited 85
326 100695 granro メモリーズ GRANRODEO J-Pop Easy 8 Standard 15 Hard 28 Master 43 Unlimited 60
327 100696 sister sister's noise fripSide J-Pop Easy 7 Standard 10 Hard 27 Master 46 Unlimited 63
328 100697 lotusl Lotus Love Maozon Original Easy N/A Standard 20 Hard 36 Master 64 Unlimited N/A
329 100698 yukari YUKARI Ocelot Original Easy N/A Standard 31 Hard 50 Master 76 Unlimited 84
330 100699 flawli フローライト 米津玄師 J-Pop Easy 8 Standard 17 Hard 30 Master 40 Unlimited 59
331 100700 nightf NIGHT FEELIN' マセラティ渚 Original Easy N/A Standard 15 Hard 28 Master 46 Unlimited 71
332 100701 random シャッフルセレクト シャッフルセレクト Original Easy N/A Standard N/A Hard N/A Master N/A Unlimited N/A
333 100702 wiwwtw What Is Wrong With The World SADA Original Easy N/A Standard 26 Hard 38 Master 62 Unlimited N/A
334 100703 inneru Inner Urge 上坂すみれ Original Easy 9 Standard 22 Hard 36 Master 48 Unlimited 67
335 100704 taishi Otherside Taishi Original Easy N/A Standard 19 Hard 35 Master 58 Unlimited N/A
336 100705 daysss Days Kent Alexander Original Easy N/A Standard 38 Hard 59 Master 81 Unlimited 81
337 100706 bokuwa 僕は君のアジテーターじゃない feat.Neru 焚吐 J-Pop Easy 16 Standard 23 Hard 34 Master 55 Unlimited 69
338 100707 showww 掌 -show- 喜多村英梨 Original Easy 15 Standard 18 Hard 35 Master 51 Unlimited 79
339 100708 nevers Never Sleep Again PassCode J-Pop Easy 15 Standard 26 Hard 32 Master 65 Unlimited 75
340 100709 bleeze BLEEZE GLAY J-Pop Easy 9 Standard 16 Hard 31 Master 47 Unlimited 62
341 100710 dreami DREAMIN' OF YOU feat.コッテル Arts Of Collective Original Easy N/A Standard 14 Hard 37 Master 65 Unlimited N/A
342 100711 allune All U Need MesoPhunk Pick-Up (New + Revival) Easy N/A Standard 14 Hard 35 Master 71 Unlimited N/A
343 100712 always Always Thinking Of You Sketchout Pick-Up (New + Revival) Easy N/A Standard 13 Hard 27 Master 49 Unlimited N/A
344 100713 anomie2 Anomie (Axiom Style) D-Fener Pick-Up (New + Revival) Easy N/A Standard 16 Hard 43 Master 84 Unlimited N/A
345 100714 aquali2 Aqualight (Remix Ver.) MAYA AKAI Pick-Up (New + Revival) Easy N/A Standard 22 Hard 43 Master 60 Unlimited 81
346 100715 astaro ASTAROTH JUPITRIS Pick-Up (New + Revival) Easy N/A Standard 23 Hard 40 Master 74 Unlimited N/A
347 100716 bassan BASS ANTICS Mitomoro Pick-Up (New + Revival) Easy N/A Standard 20 Hard 32 Master 66 Unlimited N/A
348 100717 zonzon2 Bi-Zon Zon Zombi (More Zombies Ver.) MC Natsack Pick-Up (New + Revival) Easy N/A Standard 13 Hard 27 Master 68 Unlimited 75
349 100718 bouled boule de berlin JTTR Pick-Up (New + Revival) Easy N/A Standard 19 Hard 30 Master 57 Unlimited N/A
350 100719 brandn BRAND NEW Headphone-Tokyo(star)(star) feat.カヒーナ Pick-Up (New + Revival) Easy N/A Standard 9 Hard 39 Master 66 Unlimited 72
351 100720 bravee BRAVE Ryuno Pick-Up (New + Revival) Easy N/A Standard 35 Hard 60 Master 82 Unlimited N/A
352 100721 breakd2 Break down (2nd Edition) GARNiDELiA Pick-Up (New + Revival) Easy N/A Standard 34 Hard 64 Master 74 Unlimited N/A
353 100722 buffet Buffet survivor Yamajet feat. Cathy & TEA Pick-Up (New + Revival) Easy N/A Standard 38 Hard 55 Master 68 Unlimited N/A
354 100723 buzzke BUZZ Ketos フラット3rd Pick-Up (New + Revival) Easy N/A Standard 18 Hard 33 Master 58 Unlimited 77
355 100724 cashhh Cash! Nor Pick-Up (New + Revival) Easy N/A Standard 19 Hard 25 Master 64 Unlimited N/A
356 100725 cloudb Cloudburst Relect Pick-Up (New + Revival) Easy N/A Standard 37 Hard 66 Master 74 Unlimited N/A
357 100726 clouds cloudstepping Ryuno Pick-Up (New + Revival) Easy N/A Standard 13 Hard 25 Master 47 Unlimited N/A
358 100727 codepa Code Paradiso Himmel Pick-Up (New + Revival) Easy N/A Standard 29 Hard 55 Master 70 Unlimited N/A
359 100728 comear Come Around MesoPhunk Pick-Up (New + Revival) Easy N/A Standard 38 Hard 56 Master 83 Unlimited N/A
360 100729 crysta Crystal Ribbon Cosine Pick-Up (New + Revival) Easy N/A Standard 37 Hard 56 Master 81 Unlimited N/A
361 100730 curseo Curse of Doll KO3 Pick-Up (New + Revival) Easy N/A Standard 22 Hard 36 Master 74 Unlimited N/A
362 100731 datami data mining voia Pick-Up (New + Revival) Easy N/A Standard 18 Hard 36 Master 66 Unlimited N/A
363 100732 defaul default affinity JTTR Pick-Up (New + Revival) Easy N/A Standard 21 Hard 33 Master 48 Unlimited N/A
364 100733 design2 Designed World (Remix ver.) Alinut Pick-Up (New + Revival) Easy N/A Standard 25 Hard 43 Master 68 Unlimited N/A
365 100734 diamon DIAMOND SKIN GLAY Pick-Up (New + Revival) Easy N/A Standard 10 Hard 26 Master 33 Unlimited N/A
366 100735 dispel dispel Endorfin. Pick-Up (New + Revival) Easy N/A Standard 28 Hard 48 Master 80 Unlimited N/A
367 100736 distan Distantmemory 村瀬悠太 Pick-Up (New + Revival) Easy N/A Standard 20 Hard 30 Master 68 Unlimited N/A
368 100737 dokibl Doki Blaster VOIA Pick-Up (New + Revival) Easy N/A Standard 15 Hard 23 Master 67 Unlimited N/A
369 100738 dontwa Don't Walk Away Sarah-Jane Pick-Up (New + Revival) Easy N/A Standard 13 Hard 34 Master 69 Unlimited N/A
370 100739 drgirl Dreaming Girl Nor Pick-Up (New + Revival) Easy N/A Standard 19 Hard 35 Master 54 Unlimited 73
371 100740 eterna Eternally GLAY Pick-Up (New + Revival) Easy N/A Standard 12 Hard 21 Master 39 Unlimited N/A
372 100741 everkr everKrack GLAY Pick-Up (New + Revival) Easy N/A Standard 18 Hard 29 Master 41 Unlimited N/A
373 100742 everwh EverWhite satella Pick-Up (New + Revival) Easy N/A Standard 20 Hard 31 Master 58 Unlimited N/A
374 100743 farthe FarthestEnd Sakuzyo Pick-Up (New + Revival) Easy N/A Standard 30 Hard 56 Master 78 Unlimited 87
375 100744 filame Filament Flow Endorfin. Pick-Up (New + Revival) Easy N/A Standard 23 Hard 38 Master 63 Unlimited N/A
376 100745 flameu2 Flame Up (Remix Ver.) Inu Machine Pick-Up (New + Revival) Easy N/A Standard 17 Hard 24 Master 59 Unlimited N/A
377 100746 freeee Free 千π Pick-Up (New + Revival) Easy N/A Standard 19 Hard 39 Master 69 Unlimited N/A
378 100747 funkyb2 FUNKYBABY EVOLUTION Yamajet Pick-Up (New + Revival) Easy N/A Standard 21 Hard 34 Master 56 Unlimited N/A
379 100748 granda2 Grand Arc (Club Remix) Tosh Pick-Up (New + Revival) Easy N/A Standard 24 Hard 41 Master 73 Unlimited 83
380 100749 hsphsp H.S.P (Hard Style Party) Ravine & Tom Budin Pick-Up (New + Revival) Easy N/A Standard 12 Hard 25 Master 69 Unlimited N/A
381 100750 halluc Hallucination XXX t+pazolite Pick-Up (New + Revival) Easy N/A Standard 40 Hard 52 Master 87 Unlimited N/A
382 100751 indigo Indigo Isle Syntax Error Pick-Up (New + Revival) Easy N/A Standard 17 Hard 33 Master 50 Unlimited 75
383 100752 inters Interstellar Plazma KO3 Pick-Up (New + Revival) Easy N/A Standard 25 Hard 42 Master 77 Unlimited N/A
384 100753 incave2 Into the Cave (Another Edit) Jerico Pick-Up (New + Revival) Easy N/A Standard 31 Hard 57 Master 88 Unlimited N/A
385 100754 ioniza IONIZATION llliiillliiilll Pick-Up (New + Revival) Easy N/A Standard 17 Hard 34 Master 70 Unlimited 85
386 100755 guilty JUSTICE [from] GUILTY GLAY Pick-Up (New + Revival) Easy N/A Standard 15 Hard 28 Master 50 Unlimited N/A
387 100756 keraun Keraunos Xiphoid Sphere (xi + siromaru) Pick-Up (New + Revival) Easy N/A Standard 25 Hard 52 Master 79 Unlimited N/A
388 100757 landin2 Landing on the moon (Instrumental Version) SIMON Pick-Up (New + Revival) Easy N/A Standard 20 Hard 34 Master 59 Unlimited 66
389 100758 videog Life In A Video Game Bentobox Pick-Up (New + Revival) Easy N/A Standard 21 Hard 37 Master 62 Unlimited N/A
390 100759 loseyo Lose Your Mind Vau Boy Pick-Up (New + Revival) Easy N/A Standard 20 Hard 30 Master 71 Unlimited N/A
391 100760 machin Machine Sparky Pick-Up (New + Revival) Easy N/A Standard 12 Hard 28 Master 72 Unlimited N/A
392 100761 makeit Make It Fresh EDM ver. HighLux Pick-Up (New + Revival) Easy N/A Standard 11 Hard 24 Master 48 Unlimited N/A
393 100762 daydre Mechanized Daydream s-don Pick-Up (New + Revival) Easy N/A Standard 19 Hard 36 Master 80 Unlimited N/A
394 100763 metron Metro Night ginkiha Pick-Up (New + Revival) Easy N/A Standard 20 Hard 44 Master 71 Unlimited N/A
395 100764 milkyw Milky Way Trip Nor Pick-Up (New + Revival) Easy N/A Standard 22 Hard 31 Master 60 Unlimited N/A
396 100766 nayuta nayuta happy machine Pick-Up (New + Revival) Easy N/A Standard 17 Hard 37 Master 68 Unlimited N/A
397 100767 nightm nightmares Seeds of the Upcoming Infection Pick-Up (New + Revival) Easy N/A Standard 20 Hard 49 Master 73 Unlimited N/A
398 100768 otherw Other World XIO Pick-Up (New + Revival) Easy N/A Standard 23 Hard 41 Master 76 Unlimited N/A
399 100769 overth Over The Blue (Breaking Through) Fracus & Darwin Feat. Jenna Pick-Up (New + Revival) Easy N/A Standard 33 Hard 57 Master 82 Unlimited N/A
400 100770 uuuuuu Phoenix U Pick-Up (New + Revival) Easy N/A Standard 23 Hard 37 Master 74 Unlimited N/A
401 100771 rainin Raining Again feat. Bea Aria Sanxion Pick-Up (New + Revival) Easy N/A Standard 16 Hard 41 Master 69 Unlimited N/A
402 100772 raisey Raise Your Handz! KO3 & Relect Pick-Up (New + Revival) Easy N/A Standard 23 Hard 55 Master 75 Unlimited N/A
403 100773 resona Resonance RAM Pick-Up (New + Revival) Easy N/A Standard 17 Hard 32 Master 64 Unlimited N/A
404 100774 reuniv Reuniverse Headphone-Tokyo(star)(star) feat.カヒーナ Pick-Up (New + Revival) Easy N/A Standard 14 Hard 23 Master 41 Unlimited N/A
405 100775 rhythm RHYTHM GAME MACHINE ginkiha Pick-Up (New + Revival) Easy N/A Standard 37 Hard 56 Master 78 Unlimited N/A
406 100776 rushhh Rush TANUKI Pick-Up (New + Revival) Easy N/A Standard 25 Hard 37 Master 75 Unlimited N/A
407 100777 steeee S.T.E Tatsh Pick-Up (New + Revival) Easy N/A Standard 30 Hard 58 Master 87 Unlimited N/A
408 100778 sangey Sangeyasya NNNNNNNNNN Pick-Up (New + Revival) Easy N/A Standard 27 Hard 47 Master 85 Unlimited N/A
409 100779 senpai Senpai Slam 千π Pick-Up (New + Revival) Easy N/A Standard 38 Hard 54 Master 77 Unlimited N/A
410 100780 sestea Sestea Feryquitous Pick-Up (New + Revival) Easy N/A Standard 27 Hard 47 Master 76 Unlimited N/A
411 100781 silver Silverd Feryquitous Pick-Up (New + Revival) Easy N/A Standard 28 Hard 40 Master 69 Unlimited N/A
412 100782 sodama Soda Machine Syntax Error Pick-Up (New + Revival) Easy N/A Standard 20 Hard 40 Master 65 Unlimited N/A
413 100783 stardu STARDUST (game edit) MINIKOMA★ Pick-Up (New + Revival) Easy N/A Standard 19 Hard 33 Master 64 Unlimited N/A
414 100784 starti starting station happy machine Pick-Up (New + Revival) Easy N/A Standard 17 Hard 31 Master 54 Unlimited 70
415 100785 sunday SUNDAY リベンジ HAPPY SUNDAY Pick-Up (New + Revival) Easy N/A Standard 18 Hard 29 Master 46 Unlimited 67
416 100786 sundro2 Sundrop (Remix ver.) Yamajet Pick-Up (New + Revival) Easy N/A Standard 30 Hard 48 Master 79 Unlimited 82
417 100787 sunnyd Sunny day センラ×蒼炎P Pick-Up (New + Revival) Easy N/A Standard 23 Hard 38 Master 59 Unlimited N/A
418 100788 superl SuperLuminalGirl Rebirth Yamajet feat. 小宮真央 Pick-Up (New + Revival) Easy N/A Standard 15 Hard 32 Master 59 Unlimited N/A
419 100789 switch SW!TCH 千π & MesoPhunk Pick-Up (New + Revival) Easy N/A Standard 16 Hard 35 Master 69 Unlimited N/A
420 100790 theepi2 The Epic -Introduction- Cranky Pick-Up (New + Revival) Easy N/A Standard 22 Hard 37 Master 65 Unlimited N/A
421 100791 epipha The Epiphany of Hardcore SOTUI Pick-Up (New + Revival) Easy N/A Standard 15 Hard 30 Master 70 Unlimited N/A
422 100792 thekin The King of Pirates RiraN Pick-Up (New + Revival) Easy N/A Standard 22 Hard 52 Master 75 Unlimited N/A
423 100793 timele Timeless encode Vice Principal Pick-Up (New + Revival) Easy N/A Standard 16 Hard 33 Master 72 Unlimited N/A
424 100794 tokyoo tokyo Headphone-Tokyo(star)(star) feat.nayuta Pick-Up (New + Revival) Easy N/A Standard 15 Hard 33 Master 71 Unlimited N/A
425 100795 toooma Tooo Many S3RL Pick-Up (New + Revival) Easy N/A Standard 30 Hard 51 Master 77 Unlimited N/A
426 100796 toucho2 Touch Of Gold (Bongo Mango Remix) Togo Project feat. Frances Maya Pick-Up (New + Revival) Easy N/A Standard 17 Hard 32 Master 52 Unlimited 78
427 100797 tayuta tΔyutΔi ミフメイ Pick-Up (New + Revival) Easy N/A Standard 26 Hard 35 Master 72 Unlimited N/A
428 100798 ultrix ULTRiX sky_delta Pick-Up (New + Revival) Easy N/A Standard 27 Hard 45 Master 76 Unlimited N/A
429 100799 underw Underworld ANOTHER STORY Pick-Up (New + Revival) Easy N/A Standard 29 Hard 46 Master 69 Unlimited 86
430 100800 virtua Virtual Reality Controller フラット3rd Pick-Up (New + Revival) Easy N/A Standard 15 Hard 35 Master 63 Unlimited N/A
431 100801 voiceo2 VOICE OF HOUSE (96TH RETROMAN REMIX) DOT96 Pick-Up (New + Revival) Easy N/A Standard 14 Hard 38 Master 67 Unlimited N/A
432 100802 wannab2 Wanna Be Your Special (Remix ver.) Shoichiro Hirata feat. SUIMI Pick-Up (New + Revival) Easy N/A Standard 26 Hard 41 Master 69 Unlimited N/A
433 100803 wiwwtw2 What Is Wrong With The World (Cross Edit) SADA Pick-Up (New + Revival) Easy N/A Standard 20 Hard 43 Master 67 Unlimited 72
434 100804 wingso Wings of Twilight sky_delta Pick-Up (New + Revival) Easy N/A Standard 20 Hard 53 Master 71 Unlimited N/A
435 100805 winter Winter again GLAY Pick-Up (New + Revival) Easy N/A Standard 14 Hard 24 Master 41 Unlimited N/A
436 100806 iineee いいね! BABYMETAL Pick-Up (New + Revival) Easy N/A Standard 21 Hard 40 Master 81 Unlimited N/A
437 100807 illumi イルミナレガロ Headphone-Tokyo(star)(star) feat.MiLO Pick-Up (New + Revival) Easy N/A Standard 10 Hard 25 Master 46 Unlimited 63
438 100808 yellll エール FullMooN Pick-Up (New + Revival) Easy N/A Standard 8 Hard 17 Master 52 Unlimited N/A
439 100809 eschat エスカトロジィ MozSound Pick-Up (New + Revival) Easy N/A Standard 36 Hard 57 Master 77 Unlimited N/A
440 100810 counte カウンターストップ フラット3rd Pick-Up (New + Revival) Easy N/A Standard 29 Hard 34 Master 71 Unlimited N/A
441 100811 gimcho ギミチョコ!! BABYMETAL Pick-Up (New + Revival) Easy N/A Standard 18 Hard 39 Master 70 Unlimited N/A
442 100812 surviv サバイバル GLAY Pick-Up (New + Revival) Easy N/A Standard 24 Hard 40 Master 65 Unlimited N/A
443 100814 turkis3 トルコ行進曲 (Short Remix) Pick-Up (New + Revival) Easy N/A Standard 6 Hard 20 Master 48 Unlimited N/A
444 100815 picora2 ピコラセテ (Instrumental Ver.) TORIENA Pick-Up (New + Revival) Easy N/A Standard 28 Hard 53 Master 80 Unlimited N/A
445 100816 fortis フォルテシモ らむだーじゃん Pick-Up (New + Revival) Easy N/A Standard 20 Hard 37 Master 53 Unlimited N/A
446 100817 hedban ヘドバンギャー!! BABYMETAL Pick-Up (New + Revival) Easy N/A Standard 16 Hard 43 Master 66 Unlimited N/A
447 100818 megitu メギツネ BABYMETAL Pick-Up (New + Revival) Easy N/A Standard 15 Hard 30 Master 49 Unlimited N/A
448 100819 rockma ロックマン (CUTMAN STAGE) Remixed by てつ×ねこ Pick-Up (New + Revival) Easy N/A Standard 27 Hard 48 Master 73 Unlimited N/A
449 100820 kounen2 光年(konen)-Remix Ver.- 小野秀幸 Pick-Up (New + Revival) Easy N/A Standard 21 Hard 43 Master 73 Unlimited N/A
450 100821 saisyu 最終回STORY MY FIRST STORY Pick-Up (New + Revival) Easy N/A Standard 18 Hard 36 Master 56 Unlimited N/A
451 100822 yuukan 勇敢 i tout kamejack Pick-Up (New + Revival) Easy N/A Standard 22 Hard 33 Master 78 Unlimited N/A
452 100823 modern 彼女の“Modern…” CROSS×BEATS Remix GLAY Pick-Up (New + Revival) Easy N/A Standard 20 Hard 32 Master 56 Unlimited N/A
453 100824 miraie 未来へのプレリュード カヒーナムジカ Pick-Up (New + Revival) Easy N/A Standard 21 Hard 35 Master 66 Unlimited N/A
454 100825 ranfes 狂乱セレブレーション Yamajet Pick-Up (New + Revival) Easy N/A Standard 20 Hard 42 Master 65 Unlimited N/A
455 100826 nemure 眠れない歌 iru Pick-Up (New + Revival) Easy N/A Standard 15 Hard 38 Master 67 Unlimited 76
456 100827 yuwaku 誘惑 GLAY Pick-Up (New + Revival) Easy N/A Standard 15 Hard 26 Master 43 Unlimited N/A
457 100828 dontst Don't Stop The Music feat.森高千里 tofubeats Pick-Up (New + Revival) Easy N/A Standard 15 Hard 32 Master 56 Unlimited 70
458 100829 mottai もったいないとらんど きゃりーぱみゅぱみゅ Pick-Up (New + Revival) Easy N/A Standard 10 Hard 26 Master 36 Unlimited N/A
459 100830 slysly SLY RIP SLYME Pick-Up (New + Revival) Easy N/A Standard 10 Hard 33 Master 58 Unlimited N/A
460 100831 lookam (Where's)THE SILENT MAJORITY? 高橋優 Pick-Up (New + Revival) Easy N/A Standard 17 Hard 34 Master 67 Unlimited N/A
461 100832 feverr フィーバー パスピエ Pick-Up (New + Revival) Easy N/A Standard 28 Hard 48 Master 68 Unlimited N/A
462 100833 fashio ファッションモンスター きゃりーぱみゅぱみゅ Pick-Up (New + Revival) Easy N/A Standard 8 Hard 24 Master 39 Unlimited N/A
463 100834 hagito 「ハギとこ!」のテーマ ハギー Pick-Up (New + Revival) Easy N/A Standard 12 Hard 26 Master 50 Unlimited N/A
464 100835 invade インベーダーインベーダー きゃりーぱみゅぱみゅ Pick-Up (New + Revival) Easy N/A Standard 10 Hard 28 Master 47 Unlimited N/A
465 100836 ainoch 愛の地球祭 チームしゃちほこ Pick-Up (New + Revival) Easy N/A Standard 17 Hard 40 Master 59 Unlimited N/A
466 100837 nakama 仲間を探したい 神聖かまってちゃん Pick-Up (New + Revival) Easy N/A Standard 14 Hard 32 Master 53 Unlimited N/A
467 100838 ninjar にんじゃりばんばん きゃりーぱみゅぱみゅ Pick-Up (New + Revival) Easy N/A Standard 8 Hard 23 Master 41 Unlimited 65
468 100839 parall パラレルスペック ゲスの極み乙女。 Pick-Up (New + Revival) Easy N/A Standard 14 Hard 35 Master 61 Unlimited N/A
469 100840 yukifu 雪降る夜にキスして バンドじゃないもん! Pick-Up (New + Revival) Easy N/A Standard 13 Hard 29 Master 51 Unlimited N/A
470 100841 furiso ふりそでーしょん きゃりーぱみゅぱみゅ Pick-Up (New + Revival) Easy N/A Standard 12 Hard 24 Master 44 Unlimited 74
471 100842 honeyj HONEY♡SUNRiSE ~jun Side~ jun with Aimee Original Easy 24 Standard 32 Hard 63 Master 88 Unlimited 93
472 100843 emeraj EMERALD♡KISS ~jun Side~ jun with Aimee Original Easy 19 Standard 30 Hard 53 Master 85 Unlimited N/A
473 100844 dazzlo DAZZLING♡SEASON (Original Side) jun Original Easy 16 Standard 35 Hard 60 Master 80 Unlimited 90
474 100844 shares SHARE SONG SHARE SONG Original Easy N/A Standard N/A Hard N/A Master N/A Unlimited N/A

View File

@ -1,8 +1,8 @@
from core.data import Data
from core.config import CoreConfig
from titles.cxb.schema import CxbProfileData, CxbScoreData, CxbItemData, CxbStaticData
class CxbData(Data):
def __init__(self, cfg: CoreConfig) -> None:
super().__init__(cfg)

View File

@ -17,6 +17,7 @@ from titles.cxb.rev import CxbRev
from titles.cxb.rss1 import CxbRevSunriseS1
from titles.cxb.rss2 import CxbRevSunriseS2
class CxbServlet(resource.Resource):
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
self.isLeaf = True
@ -24,62 +25,85 @@ class CxbServlet(resource.Resource):
self.core_cfg = core_cfg
self.game_cfg = CxbConfig()
if path.exists(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}"):
self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}")))
self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}"))
)
self.logger = logging.getLogger("cxb")
if not hasattr(self.logger, "inited"):
log_fmt_str = "[%(asctime)s] CXB | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"), encoding='utf8',
when="d", backupCount=10)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.core_cfg.server.log_dir, "cxb"),
encoding="utf8",
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler)
self.logger.setLevel(self.game_cfg.server.loglevel)
coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str)
coloredlogs.install(
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
)
self.logger.inited = True
self.versions = [
CxbRev(core_cfg, self.game_cfg),
CxbRevSunriseS1(core_cfg, self.game_cfg),
CxbRevSunriseS2(core_cfg, self.game_cfg),
]
@classmethod
def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]:
def get_allnet_info(
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
) -> Tuple[bool, str, str]:
game_cfg = CxbConfig()
if path.exists(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}"):
game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}")))
game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{CxbConstants.CONFIG_NAME}"))
)
if not game_cfg.server.enable:
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,
f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/",
"",
)
return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "")
def setup(self):
if self.game_cfg.server.enable:
endpoints.serverFromString(reactor, f"tcp:{self.game_cfg.server.port}:interface={self.core_cfg.server.listen_address}")\
.listen(server.Site(CxbServlet(self.core_cfg, self.cfg_dir)))
if self.core_cfg.server.is_develop and self.game_cfg.server.ssl_enable:
endpoints.serverFromString(reactor, f"ssl:{self.game_cfg.server.port_secure}"\
f":interface={self.core_cfg.server.listen_address}:privateKey={self.game_cfg.server.ssl_key}:"\
f"certKey={self.game_cfg.server.ssl_cert}")\
.listen(server.Site(CxbServlet(self.core_cfg, self.cfg_dir)))
endpoints.serverFromString(
reactor,
f"tcp:{self.game_cfg.server.port}:interface={self.core_cfg.server.listen_address}",
).listen(server.Site(CxbServlet(self.core_cfg, self.cfg_dir)))
self.logger.info(f"Crossbeats title server ready on port {self.game_cfg.server.port} & {self.game_cfg.server.port_secure}")
if self.core_cfg.server.is_develop and self.game_cfg.server.ssl_enable:
endpoints.serverFromString(
reactor,
f"ssl:{self.game_cfg.server.port_secure}"
f":interface={self.core_cfg.server.listen_address}:privateKey={self.game_cfg.server.ssl_key}:"
f"certKey={self.game_cfg.server.ssl_cert}",
).listen(server.Site(CxbServlet(self.core_cfg, self.cfg_dir)))
self.logger.info(
f"Crossbeats title server ready on port {self.game_cfg.server.port} & {self.game_cfg.server.port_secure}"
)
else:
self.logger.info(f"Crossbeats title server ready on port {self.game_cfg.server.port}")
self.logger.info(
f"Crossbeats title server ready on port {self.game_cfg.server.port}"
)
def render_POST(self, request: Request):
version = 0
@ -96,21 +120,28 @@ class CxbServlet(resource.Resource):
except Exception as e:
try:
req_json: Dict = json.loads(req_bytes.decode().replace('"', '\\"').replace("'", '"'))
req_json: Dict = json.loads(
req_bytes.decode().replace('"', '\\"').replace("'", '"')
)
except Exception as f:
self.logger.warn(f"Error decoding json: {e} / {f} - {req_url} - {req_bytes}")
self.logger.warn(
f"Error decoding json: {e} / {f} - {req_url} - {req_bytes}"
)
return b""
if req_json == {}:
self.logger.warn(f"Empty json request to {req_url}")
return b""
cmd = url_split[len(url_split) - 1]
subcmd = list(req_json.keys())[0]
if subcmd == "dldate":
if not type(req_json["dldate"]) is dict or "filetype" not in req_json["dldate"]:
if (
not type(req_json["dldate"]) is dict
or "filetype" not in req_json["dldate"]
):
self.logger.warn(f"Malformed dldate request: {req_url} {req_json}")
return b""
@ -119,7 +150,9 @@ class CxbServlet(resource.Resource):
version = int(filetype_split[0])
filetype_inflect_split = inflection.underscore(filetype).split("/")
match = re.match("^([A-Za-z]*)(\d\d\d\d)$", filetype_split[len(filetype_split) - 1])
match = re.match(
"^([A-Za-z]*)(\d\d\d\d)$", filetype_split[len(filetype_split) - 1]
)
if match:
subcmd = f"{inflection.underscore(match.group(1))}xxxx"
else:
@ -128,7 +161,7 @@ class CxbServlet(resource.Resource):
filetype = subcmd
func_to_find = f"handle_{cmd}_{subcmd}_request"
if version <= 10102:
version_string = "Rev"
internal_ver = CxbConstants.VER_CROSSBEATS_REV
@ -136,28 +169,28 @@ class CxbServlet(resource.Resource):
elif version == 10113 or version == 10103:
version_string = "Rev SunriseS1"
internal_ver = CxbConstants.VER_CROSSBEATS_REV_SUNRISE_S1
elif version >= 10114 or version == 10104:
version_string = "Rev SunriseS2"
internal_ver = CxbConstants.VER_CROSSBEATS_REV_SUNRISE_S2
else:
version_string = "Base"
self.logger.info(f"{version_string} Request {req_url} -> {filetype}")
self.logger.debug(req_json)
try:
handler = getattr(self.versions[internal_ver], func_to_find)
resp = handler(req_json)
except AttributeError as e:
except AttributeError as e:
self.logger.warning(f"Unhandled {version_string} request {req_url} - {e}")
resp = {}
except Exception as e:
self.logger.error(f"Error handling {version_string} method {req_url} - {e}")
raise
self.logger.debug(f"{version_string} Response {resp}")
self.logger.debug(f"{version_string} Response {resp}")
return json.dumps(resp, ensure_ascii=False).encode("utf-8")

View File

@ -8,13 +8,23 @@ from core.config import CoreConfig
from titles.cxb.database import CxbData
from titles.cxb.const import CxbConstants
class CxbReader(BaseReader):
def __init__(self, config: CoreConfig, version: int, bin_arg: Optional[str], opt_arg: Optional[str], extra: Optional[str]) -> None:
def __init__(
self,
config: CoreConfig,
version: int,
bin_arg: Optional[str],
opt_arg: Optional[str],
extra: Optional[str],
) -> None:
super().__init__(config, version, bin_arg, opt_arg, extra)
self.data = CxbData(config)
try:
self.logger.info(f"Start importer for {CxbConstants.game_ver_to_string(version)}")
self.logger.info(
f"Start importer for {CxbConstants.game_ver_to_string(version)}"
)
except IndexError:
self.logger.error(f"Invalid project cxb version {version}")
exit(1)
@ -28,7 +38,7 @@ class CxbReader(BaseReader):
if pull_bin_ram:
self.read_csv(f"{self.bin_dir}")
def read_csv(self, bin_dir: str) -> None:
self.logger.info(f"Read csv from {bin_dir}")
@ -45,18 +55,73 @@ class CxbReader(BaseReader):
if not "N/A" in row["standard"]:
self.logger.info(f"Added song {song_id} chart 0")
self.data.static.put_music(self.version, song_id, index, 0, title, artist, genre, int(row["standard"].replace("Standard ","").replace("N/A","0")))
self.data.static.put_music(
self.version,
song_id,
index,
0,
title,
artist,
genre,
int(
row["standard"]
.replace("Standard ", "")
.replace("N/A", "0")
),
)
if not "N/A" in row["hard"]:
self.logger.info(f"Added song {song_id} chart 1")
self.data.static.put_music(self.version, song_id, index, 1, title, artist, genre, int(row["hard"].replace("Hard ","").replace("N/A","0")))
self.data.static.put_music(
self.version,
song_id,
index,
1,
title,
artist,
genre,
int(row["hard"].replace("Hard ", "").replace("N/A", "0")),
)
if not "N/A" in row["master"]:
self.logger.info(f"Added song {song_id} chart 2")
self.data.static.put_music(self.version, song_id, index, 2, title, artist, genre, int(row["master"].replace("Master ","").replace("N/A","0")))
self.data.static.put_music(
self.version,
song_id,
index,
2,
title,
artist,
genre,
int(
row["master"].replace("Master ", "").replace("N/A", "0")
),
)
if not "N/A" in row["unlimited"]:
self.logger.info(f"Added song {song_id} chart 3")
self.data.static.put_music(self.version, song_id, index, 3, title, artist, genre, int(row["unlimited"].replace("Unlimited ","").replace("N/A","0")))
self.data.static.put_music(
self.version,
song_id,
index,
3,
title,
artist,
genre,
int(
row["unlimited"]
.replace("Unlimited ", "")
.replace("N/A", "0")
),
)
if not "N/A" in row["easy"]:
self.logger.info(f"Added song {song_id} chart 4")
self.data.static.put_music(self.version, song_id, index, 4, title, artist, genre, int(row["easy"].replace("Easy ","").replace("N/A","0")))
self.data.static.put_music(
self.version,
song_id,
index,
4,
title,
artist,
genre,
int(row["easy"].replace("Easy ", "").replace("N/A", "0")),
)
except:
self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping")

View File

@ -11,155 +11,191 @@ from titles.cxb.config import CxbConfig
from titles.cxb.base import CxbBase
from titles.cxb.const import CxbConstants
class CxbRev(CxbBase):
def __init__(self, cfg: CoreConfig, game_cfg: CxbConfig) -> None:
super().__init__(cfg, game_cfg)
self.version = CxbConstants.VER_CROSSBEATS_REV
def handle_data_path_list_request(self, data: Dict) -> Dict:
return { "data": "" }
return {"data": ""}
def handle_data_putlog_request(self, data: Dict) -> Dict:
if data["putlog"]["type"] == "ResultLog":
score_data = json.loads(data["putlog"]["data"])
userid = score_data['usid']
userid = score_data["usid"]
self.data.score.put_playlog(userid, score_data['mcode'], score_data['difficulty'], score_data["score"], int(Decimal(score_data["clearrate"]) * 100), score_data["flawless"], score_data["super"], score_data["cool"], score_data["fast"], score_data["fast2"], score_data["slow"], score_data["slow2"], score_data["fail"], score_data["combo"])
return({"data":True})
return {"data": True }
self.data.score.put_playlog(
userid,
score_data["mcode"],
score_data["difficulty"],
score_data["score"],
int(Decimal(score_data["clearrate"]) * 100),
score_data["flawless"],
score_data["super"],
score_data["cool"],
score_data["fast"],
score_data["fast2"],
score_data["slow"],
score_data["slow2"],
score_data["fail"],
score_data["combo"],
)
return {"data": True}
return {"data": True}
@cached(lifetime=86400)
@cached(lifetime=86400)
def handle_data_music_list_request(self, data: Dict) -> Dict:
ret_str = ""
with open(r"titles/cxb/rev_data/MusicArchiveList.csv") as music:
lines = music.readlines()
for line in lines:
line_split = line.split(',')
line_split = line.split(",")
ret_str += f"{line_split[0]},{line_split[1]},{line_split[2]},{line_split[3]},{line_split[4]},{line_split[5]},{line_split[6]},{line_split[7]},{line_split[8]},{line_split[9]},{line_split[10]},{line_split[11]},{line_split[12]},{line_split[13]},{line_split[14]},\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
def handle_data_item_list_icon_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ItemListIcon\r\n"
with open(r"titles/cxb/rev_data/Item/ItemArchiveList_Icon.csv", encoding="utf-8") as item:
with open(
r"titles/cxb/rev_data/Item/ItemArchiveList_Icon.csv", encoding="utf-8"
) as item:
lines = item.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
@cached(lifetime=86400)
def handle_data_item_list_skin_notes_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ItemListSkinNotes\r\n"
with open(r"titles/cxb/rev_data/Item/ItemArchiveList_SkinNotes.csv", encoding="utf-8") as item:
with open(
r"titles/cxb/rev_data/Item/ItemArchiveList_SkinNotes.csv", encoding="utf-8"
) as item:
lines = item.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
@cached(lifetime=86400)
def handle_data_item_list_skin_effect_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ItemListSkinEffect\r\n"
with open(r"titles/cxb/rev_data/Item/ItemArchiveList_SkinEffect.csv", encoding="utf-8") as item:
with open(
r"titles/cxb/rev_data/Item/ItemArchiveList_SkinEffect.csv", encoding="utf-8"
) as item:
lines = item.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
@cached(lifetime=86400)
def handle_data_item_list_skin_bg_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ItemListSkinBg\r\n"
with open(r"titles/cxb/rev_data/Item/ItemArchiveList_SkinBg.csv", encoding="utf-8") as item:
with open(
r"titles/cxb/rev_data/Item/ItemArchiveList_SkinBg.csv", encoding="utf-8"
) as item:
lines = item.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
@cached(lifetime=86400)
def handle_data_item_list_title_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ItemListTitle\r\n"
with open(r"titles/cxb/rev_data/Item/ItemList_Title.csv", encoding="shift-jis") as item:
with open(
r"titles/cxb/rev_data/Item/ItemList_Title.csv", encoding="shift-jis"
) as item:
lines = item.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
@cached(lifetime=86400)
def handle_data_shop_list_music_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ShopListMusic\r\n"
with open(r"titles/cxb/rev_data/Shop/ShopList_Music.csv", encoding="shift-jis") as shop:
with open(
r"titles/cxb/rev_data/Shop/ShopList_Music.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
@cached(lifetime=86400)
def handle_data_shop_list_icon_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ShopListIcon\r\n"
with open(r"titles/cxb/rev_data/Shop/ShopList_Icon.csv", encoding="shift-jis") as shop:
with open(
r"titles/cxb/rev_data/Shop/ShopList_Icon.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
@cached(lifetime=86400)
def handle_data_shop_list_title_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ShopListTitle\r\n"
with open(r"titles/cxb/rev_data/Shop/ShopList_Title.csv", encoding="shift-jis") as shop:
with open(
r"titles/cxb/rev_data/Shop/ShopList_Title.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
def handle_data_shop_list_skin_hud_request(self, data: Dict) -> Dict:
return({"data":""})
def handle_data_shop_list_skin_arrow_request(self, data: Dict) -> Dict:
return({"data":""})
def handle_data_shop_list_skin_hit_request(self, data: Dict) -> Dict:
return({"data":""})
return {"data": ret_str}
@cached(lifetime=86400)
def handle_data_shop_list_skin_hud_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_shop_list_skin_arrow_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_shop_list_skin_hit_request(self, data: Dict) -> Dict:
return {"data": ""}
@cached(lifetime=86400)
def handle_data_shop_list_sale_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ShopListSale\r\n"
with open(r"titles/cxb/rev_data/Shop/ShopList_Sale.csv", encoding="shift-jis") as shop:
with open(
r"titles/cxb/rev_data/Shop/ShopList_Sale.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
def handle_data_extra_stage_list_request(self, data: Dict) -> Dict:
return({"data":""})
return {"data": ""}
@cached(lifetime=86400)
@cached(lifetime=86400)
def handle_data_exxxxx_request(self, data: Dict) -> Dict:
extra_num = int(data["dldate"]["filetype"][-4:])
ret_str = ""
with open(fr"titles/cxb/rev_data/Ex000{extra_num}.csv", encoding="shift-jis") as stage:
with open(
rf"titles/cxb/rev_data/Ex000{extra_num}.csv", encoding="shift-jis"
) as stage:
lines = stage.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
def handle_data_bonus_list10100_request(self, data: Dict) -> Dict:
return({"data": ""})
def handle_data_free_coupon_request(self, data: Dict) -> Dict:
return({"data": ""})
return {"data": ret_str}
@cached(lifetime=86400)
def handle_data_bonus_list10100_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_free_coupon_request(self, data: Dict) -> Dict:
return {"data": ""}
@cached(lifetime=86400)
def handle_data_news_list_request(self, data: Dict) -> Dict:
ret_str = ""
with open(r"titles/cxb/rev_data/NewsList.csv", encoding="UTF-8") as news:
lines = news.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
def handle_data_tips_request(self, data: Dict) -> Dict:
return({"data":""})
return {"data": ""}
@cached(lifetime=86400)
def handle_data_license_request(self, data: Dict) -> Dict:
ret_str = ""
@ -167,90 +203,104 @@ class CxbRev(CxbBase):
lines = lic.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
@cached(lifetime=86400)
def handle_data_course_list_request(self, data: Dict) -> Dict:
ret_str = ""
with open(r"titles/cxb/rev_data/Course/CourseList.csv", encoding="UTF-8") as course:
with open(
r"titles/cxb/rev_data/Course/CourseList.csv", encoding="UTF-8"
) as course:
lines = course.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
@cached(lifetime=86400)
def handle_data_csxxxx_request(self, data: Dict) -> Dict:
# Removed the CSVs since the format isnt quite right
extra_num = int(data["dldate"]["filetype"][-4:])
ret_str = ""
with open(fr"titles/cxb/rev_data/Course/Cs000{extra_num}.csv", encoding="shift-jis") as course:
with open(
rf"titles/cxb/rev_data/Course/Cs000{extra_num}.csv", encoding="shift-jis"
) as course:
lines = course.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
@cached(lifetime=86400)
def handle_data_mission_list_request(self, data: Dict) -> Dict:
ret_str = ""
with open(r"titles/cxb/rev_data/MissionList.csv", encoding="shift-jis") as mission:
with open(
r"titles/cxb/rev_data/MissionList.csv", encoding="shift-jis"
) as mission:
lines = mission.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
def handle_data_mission_bonus_request(self, data: Dict) -> Dict:
return({"data": ""})
def handle_data_unlimited_mission_request(self, data: Dict) -> Dict:
return({"data": ""})
return {"data": ret_str}
@cached(lifetime=86400)
def handle_data_mission_bonus_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_unlimited_mission_request(self, data: Dict) -> Dict:
return {"data": ""}
@cached(lifetime=86400)
def handle_data_event_list_request(self, data: Dict) -> Dict:
ret_str = ""
with open(r"titles/cxb/rev_data/Event/EventArchiveList.csv", encoding="shift-jis") as mission:
with open(
r"titles/cxb/rev_data/Event/EventArchiveList.csv", encoding="shift-jis"
) as mission:
lines = mission.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
def handle_data_event_music_list_request(self, data: Dict) -> Dict:
return({"data": ""})
def handle_data_event_mission_list_request(self, data: Dict) -> Dict:
return({"data": ""})
def handle_data_event_achievement_single_high_score_list_request(self, data: Dict) -> Dict:
return({"data": ""})
def handle_data_event_achievement_single_accumulation_request(self, data: Dict) -> Dict:
return({"data": ""})
def handle_data_event_ranking_high_score_list_request(self, data: Dict) -> Dict:
return({"data": ""})
def handle_data_event_ranking_accumulation_list_request(self, data: Dict) -> Dict:
return({"data": ""})
def handle_data_event_ranking_stamp_list_request(self, data: Dict) -> Dict:
return({"data": ""})
def handle_data_event_ranking_store_list_request(self, data: Dict) -> Dict:
return({"data": ""})
def handle_data_event_ranking_area_list_request(self, data: Dict) -> Dict:
return({"data": ""})
return {"data": ""}
@cached(lifetime=86400)
def handle_data_event_mission_list_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_event_achievement_single_high_score_list_request(
self, data: Dict
) -> Dict:
return {"data": ""}
def handle_data_event_achievement_single_accumulation_request(
self, data: Dict
) -> Dict:
return {"data": ""}
def handle_data_event_ranking_high_score_list_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_event_ranking_accumulation_list_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_event_ranking_stamp_list_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_event_ranking_store_list_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_event_ranking_area_list_request(self, data: Dict) -> Dict:
return {"data": ""}
@cached(lifetime=86400)
def handle_data_event_stamp_list_request(self, data: Dict) -> Dict:
ret_str = ""
with open(r"titles/cxb/rev_data/Event/EventStampList.csv", encoding="shift-jis") as event:
with open(
r"titles/cxb/rev_data/Event/EventStampList.csv", encoding="shift-jis"
) as event:
lines = event.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict:
return({"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"})
return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"}
def handle_data_server_state_request(self, data: Dict) -> Dict:
return({"data": True})
return {"data": True}

View File

@ -11,128 +11,147 @@ from titles.cxb.config import CxbConfig
from titles.cxb.base import CxbBase
from titles.cxb.const import CxbConstants
class CxbRevSunriseS1(CxbBase):
def __init__(self, cfg: CoreConfig, game_cfg: CxbConfig) -> None:
super().__init__(cfg, game_cfg)
self.version = CxbConstants.VER_CROSSBEATS_REV_SUNRISE_S1
def handle_data_path_list_request(self, data: Dict) -> Dict:
return { "data": "" }
@cached(lifetime=86400)
def handle_data_path_list_request(self, data: Dict) -> Dict:
return {"data": ""}
@cached(lifetime=86400)
def handle_data_music_list_request(self, data: Dict) -> Dict:
ret_str = ""
with open(r"titles/cxb/rss1_data/MusicArchiveList.csv") as music:
lines = music.readlines()
for line in lines:
line_split = line.split(',')
line_split = line.split(",")
ret_str += f"{line_split[0]},{line_split[1]},{line_split[2]},{line_split[3]},{line_split[4]},{line_split[5]},{line_split[6]},{line_split[7]},{line_split[8]},{line_split[9]},{line_split[10]},{line_split[11]},{line_split[12]},{line_split[13]},{line_split[14]},\r\n"
return({"data":ret_str})
@cached(lifetime=86400)
return {"data": ret_str}
@cached(lifetime=86400)
def handle_data_item_list_detail_request(self, data: Dict) -> Dict:
#ItemListIcon load
# ItemListIcon load
ret_str = "#ItemListIcon\r\n"
with open(r"titles/cxb/rss1_data/Item/ItemList_Icon.csv", encoding="shift-jis") as item:
with open(
r"titles/cxb/rss1_data/Item/ItemList_Icon.csv", encoding="shift-jis"
) as item:
lines = item.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
#ItemListTitle load
# ItemListTitle load
ret_str += "\r\n#ItemListTitle\r\n"
with open(r"titles/cxb/rss1_data/Item/ItemList_Title.csv", encoding="shift-jis") as item:
with open(
r"titles/cxb/rss1_data/Item/ItemList_Title.csv", encoding="shift-jis"
) as item:
lines = item.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
@cached(lifetime=86400)
def handle_data_shop_list_detail_request(self, data: Dict) -> Dict:
#ShopListIcon load
# ShopListIcon load
ret_str = "#ShopListIcon\r\n"
with open(r"titles/cxb/rss1_data/Shop/ShopList_Icon.csv", encoding="utf-8") as shop:
with open(
r"titles/cxb/rss1_data/Shop/ShopList_Icon.csv", encoding="utf-8"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
#ShopListMusic load
ret_str += "\r\n#ShopListMusic\r\n"
with open(r"titles/cxb/rss1_data/Shop/ShopList_Music.csv", encoding="utf-8") as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
#ShopListSale load
ret_str += "\r\n#ShopListSale\r\n"
with open(r"titles/cxb/rss1_data/Shop/ShopList_Sale.csv", encoding="shift-jis") as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
#ShopListSkinBg load
ret_str += "\r\n#ShopListSkinBg\r\n"
with open(r"titles/cxb/rss1_data/Shop/ShopList_SkinBg.csv", encoding="shift-jis") as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
#ShopListSkinEffect load
ret_str += "\r\n#ShopListSkinEffect\r\n"
with open(r"titles/cxb/rss1_data/Shop/ShopList_SkinEffect.csv", encoding="shift-jis") as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
#ShopListSkinNotes load
ret_str += "\r\n#ShopListSkinNotes\r\n"
with open(r"titles/cxb/rss1_data/Shop/ShopList_SkinNotes.csv", encoding="shift-jis") as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
#ShopListTitle load
ret_str += "\r\n#ShopListTitle\r\n"
with open(r"titles/cxb/rss1_data/Shop/ShopList_Title.csv", encoding="utf-8") as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
def handle_data_extra_stage_list_request(self, data: Dict) -> Dict:
return({"data":""})
def handle_data_ex0001_request(self, data: Dict) -> Dict:
return({"data":""})
def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict:
return({"data":""})
def handle_data_bonus_list10100_request(self, data: Dict) -> Dict:
return({"data":""})
def handle_data_oe0001_request(self, data: Dict) -> Dict:
return({"data":""})
def handle_data_free_coupon_request(self, data: Dict) -> Dict:
return({"data":""})
@cached(lifetime=86400)
# ShopListMusic load
ret_str += "\r\n#ShopListMusic\r\n"
with open(
r"titles/cxb/rss1_data/Shop/ShopList_Music.csv", encoding="utf-8"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
# ShopListSale load
ret_str += "\r\n#ShopListSale\r\n"
with open(
r"titles/cxb/rss1_data/Shop/ShopList_Sale.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
# ShopListSkinBg load
ret_str += "\r\n#ShopListSkinBg\r\n"
with open(
r"titles/cxb/rss1_data/Shop/ShopList_SkinBg.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
# ShopListSkinEffect load
ret_str += "\r\n#ShopListSkinEffect\r\n"
with open(
r"titles/cxb/rss1_data/Shop/ShopList_SkinEffect.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
# ShopListSkinNotes load
ret_str += "\r\n#ShopListSkinNotes\r\n"
with open(
r"titles/cxb/rss1_data/Shop/ShopList_SkinNotes.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
# ShopListTitle load
ret_str += "\r\n#ShopListTitle\r\n"
with open(
r"titles/cxb/rss1_data/Shop/ShopList_Title.csv", encoding="utf-8"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return {"data": ret_str}
def handle_data_extra_stage_list_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_ex0001_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_bonus_list10100_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_oe0001_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_free_coupon_request(self, data: Dict) -> Dict:
return {"data": ""}
@cached(lifetime=86400)
def handle_data_news_list_request(self, data: Dict) -> Dict:
ret_str = ""
with open(r"titles/cxb/rss1_data/NewsList.csv", encoding="UTF-8") as news:
lines = news.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
def handle_data_tips_request(self, data: Dict) -> Dict:
return({"data":""})
return {"data": ""}
def handle_data_release_info_list_request(self, data: Dict) -> Dict:
return({"data":""})
return {"data": ""}
@cached(lifetime=86400)
def handle_data_random_music_list_request(self, data: Dict) -> Dict:
ret_str = ""
@ -141,10 +160,12 @@ class CxbRevSunriseS1(CxbBase):
count = 0
for line in lines:
line_split = line.split(",")
ret_str += str(count) + "," + line_split[0] + "," + line_split[0] + ",\r\n"
ret_str += (
str(count) + "," + line_split[0] + "," + line_split[0] + ",\r\n"
)
return {"data": ret_str}
return({"data":ret_str})
@cached(lifetime=86400)
def handle_data_license_request(self, data: Dict) -> Dict:
ret_str = ""
@ -152,54 +173,58 @@ class CxbRevSunriseS1(CxbBase):
lines = licenses.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
def handle_data_course_list_request(self, data: Dict) -> Dict:
ret_str = ""
with open(r"titles/cxb/rss1_data/Course/CourseList.csv", encoding="UTF-8") as course:
with open(
r"titles/cxb/rss1_data/Course/CourseList.csv", encoding="UTF-8"
) as course:
lines = course.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
def handle_data_csxxxx_request(self, data: Dict) -> Dict:
extra_num = int(data["dldate"]["filetype"][-4:])
ret_str = ""
with open(fr"titles/cxb/rss1_data/Course/Cs{extra_num}.csv", encoding="shift-jis") as course:
with open(
rf"titles/cxb/rss1_data/Course/Cs{extra_num}.csv", encoding="shift-jis"
) as course:
lines = course.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
def handle_data_mission_list_request(self, data: Dict) -> Dict:
return({"data":""})
return {"data": ""}
def handle_data_mission_bonus_request(self, data: Dict) -> Dict:
return({"data":""})
return {"data": ""}
def handle_data_unlimited_mission_request(self, data: Dict) -> Dict:
return({"data":""})
return {"data": ""}
def handle_data_partner_list_request(self, data: Dict) -> Dict:
ret_str = ""
# Lord forgive me for the sins I am about to commit
for i in range(0,10):
for i in range(0, 10):
ret_str += f"80000{i},{i},{i},0,10000,,\r\n"
ret_str += f"80000{i},{i},{i},1,10500,,\r\n"
ret_str += f"80000{i},{i},{i},2,10500,,\r\n"
for i in range(10,13):
for i in range(10, 13):
ret_str += f"8000{i},{i},{i},0,10000,,\r\n"
ret_str += f"8000{i},{i},{i},1,10500,,\r\n"
ret_str += f"8000{i},{i},{i},2,10500,,\r\n"
ret_str +="\r\n---\r\n0,150,100,100,100,100,\r\n"
for i in range(1,130):
ret_str +=f"{i},100,100,100,100,100,\r\n"
ret_str += "\r\n---\r\n0,150,100,100,100,100,\r\n"
for i in range(1, 130):
ret_str += f"{i},100,100,100,100,100,\r\n"
ret_str += "---\r\n"
return({"data": ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
def handle_data_partnerxxxx_request(self, data: Dict) -> Dict:
partner_num = int(data["dldate"]["filetype"][-4:])
@ -208,50 +233,54 @@ class CxbRevSunriseS1(CxbBase):
lines = partner.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data": ret_str})
return {"data": ret_str}
def handle_data_server_state_request(self, data: Dict) -> Dict:
return({"data": True})
return {"data": True}
def handle_data_settings_request(self, data: Dict) -> Dict:
return({"data": "2,\r\n"})
return {"data": "2,\r\n"}
def handle_data_story_list_request(self, data: Dict) -> Dict:
#story id, story name, game version, start time, end time, course arc, unlock flag, song mcode for menu
# story id, story name, game version, start time, end time, course arc, unlock flag, song mcode for menu
ret_str = "\r\n"
ret_str += f"st0000,RISING PURPLE,10104,1464370990,4096483201,Cs1000,-1,purple,\r\n"
ret_str += f"st0001,REBEL YELL,10104,1467999790,4096483201,Cs1000,-1,chaset,\r\n"
ret_str += (
f"st0000,RISING PURPLE,10104,1464370990,4096483201,Cs1000,-1,purple,\r\n"
)
ret_str += (
f"st0001,REBEL YELL,10104,1467999790,4096483201,Cs1000,-1,chaset,\r\n"
)
ret_str += f"st0002,REMNANT,10104,1502127790,4096483201,Cs1000,-1,overcl,\r\n"
return({"data": ret_str})
return {"data": ret_str}
def handle_data_stxxxx_request(self, data: Dict) -> Dict:
story_num = int(data["dldate"]["filetype"][-4:])
ret_str = ""
for i in range(1,11):
ret_str +=f"{i},st000{story_num}_{i-1},,,,,,,,,,,,,,,,1,,-1,1,\r\n"
return({"data": ret_str})
for i in range(1, 11):
ret_str += f"{i},st000{story_num}_{i-1},,,,,,,,,,,,,,,,1,,-1,1,\r\n"
return {"data": ret_str}
def handle_data_event_stamp_list_request(self, data: Dict) -> Dict:
return({"data":"Cs1032,1,1,1,1,1,1,1,1,1,1,\r\n"})
return {"data": "Cs1032,1,1,1,1,1,1,1,1,1,1,\r\n"}
def handle_data_premium_list_request(self, data: Dict) -> Dict:
return({"data": "1,,,,10,,,,,99,,,,,,,,,100,,\r\n"})
return {"data": "1,,,,10,,,,,99,,,,,,,,,100,,\r\n"}
def handle_data_event_list_request(self, data: Dict) -> Dict:
return({"data":""})
return {"data": ""}
def handle_data_event_detail_list_request(self, data: Dict) -> Dict:
event_id = data["dldate"]["filetype"].split("/")[2]
if "EventStampMapListCs1002" in event_id:
return({"data":"1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"})
return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"}
elif "EventStampList" in event_id:
return({"data":"Cs1002,1,1,1,1,1,1,1,1,1,1,\r\n"})
return {"data": "Cs1002,1,1,1,1,1,1,1,1,1,1,\r\n"}
else:
return({"data":""})
return {"data": ""}
def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict:
event_id = data["dldate"]["filetype"].split("/")[2]
if "EventStampMapListCs1002" in event_id:
return({"data":"1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"})
return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"}
else:
return({"data":""})
return {"data": ""}

View File

@ -11,128 +11,147 @@ from titles.cxb.config import CxbConfig
from titles.cxb.base import CxbBase
from titles.cxb.const import CxbConstants
class CxbRevSunriseS2(CxbBase):
def __init__(self, cfg: CoreConfig, game_cfg: CxbConfig) -> None:
super().__init__(cfg, game_cfg)
self.version = CxbConstants.VER_CROSSBEATS_REV_SUNRISE_S2_OMNI
def handle_data_path_list_request(self, data: Dict) -> Dict:
return { "data": "" }
@cached(lifetime=86400)
def handle_data_path_list_request(self, data: Dict) -> Dict:
return {"data": ""}
@cached(lifetime=86400)
def handle_data_music_list_request(self, data: Dict) -> Dict:
ret_str = ""
with open(r"titles/cxb/rss2_data/MusicArchiveList.csv") as music:
lines = music.readlines()
for line in lines:
line_split = line.split(',')
line_split = line.split(",")
ret_str += f"{line_split[0]},{line_split[1]},{line_split[2]},{line_split[3]},{line_split[4]},{line_split[5]},{line_split[6]},{line_split[7]},{line_split[8]},{line_split[9]},{line_split[10]},{line_split[11]},{line_split[12]},{line_split[13]},{line_split[14]},\r\n"
return({"data":ret_str})
@cached(lifetime=86400)
return {"data": ret_str}
@cached(lifetime=86400)
def handle_data_item_list_detail_request(self, data: Dict) -> Dict:
#ItemListIcon load
# ItemListIcon load
ret_str = "#ItemListIcon\r\n"
with open(r"titles/cxb/rss2_data/Item/ItemList_Icon.csv", encoding="utf-8") as item:
with open(
r"titles/cxb/rss2_data/Item/ItemList_Icon.csv", encoding="utf-8"
) as item:
lines = item.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
#ItemListTitle load
# ItemListTitle load
ret_str += "\r\n#ItemListTitle\r\n"
with open(r"titles/cxb/rss2_data/Item/ItemList_Title.csv", encoding="utf-8") as item:
with open(
r"titles/cxb/rss2_data/Item/ItemList_Title.csv", encoding="utf-8"
) as item:
lines = item.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
@cached(lifetime=86400)
def handle_data_shop_list_detail_request(self, data: Dict) -> Dict:
#ShopListIcon load
# ShopListIcon load
ret_str = "#ShopListIcon\r\n"
with open(r"titles/cxb/rss2_data/Shop/ShopList_Icon.csv", encoding="utf-8") as shop:
with open(
r"titles/cxb/rss2_data/Shop/ShopList_Icon.csv", encoding="utf-8"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
#ShopListMusic load
ret_str += "\r\n#ShopListMusic\r\n"
with open(r"titles/cxb/rss2_data/Shop/ShopList_Music.csv", encoding="utf-8") as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
#ShopListSale load
ret_str += "\r\n#ShopListSale\r\n"
with open(r"titles/cxb/rss2_data/Shop/ShopList_Sale.csv", encoding="shift-jis") as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
#ShopListSkinBg load
ret_str += "\r\n#ShopListSkinBg\r\n"
with open(r"titles/cxb/rss2_data/Shop/ShopList_SkinBg.csv", encoding="shift-jis") as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
#ShopListSkinEffect load
ret_str += "\r\n#ShopListSkinEffect\r\n"
with open(r"titles/cxb/rss2_data/Shop/ShopList_SkinEffect.csv", encoding="shift-jis") as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
#ShopListSkinNotes load
ret_str += "\r\n#ShopListSkinNotes\r\n"
with open(r"titles/cxb/rss2_data/Shop/ShopList_SkinNotes.csv", encoding="shift-jis") as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
#ShopListTitle load
ret_str += "\r\n#ShopListTitle\r\n"
with open(r"titles/cxb/rss2_data/Shop/ShopList_Title.csv", encoding="utf-8") as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
def handle_data_extra_stage_list_request(self, data: Dict) -> Dict:
return({"data":""})
def handle_data_ex0001_request(self, data: Dict) -> Dict:
return({"data":""})
def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict:
return({"data":""})
def handle_data_bonus_list10100_request(self, data: Dict) -> Dict:
return({"data":""})
def handle_data_oe0001_request(self, data: Dict) -> Dict:
return({"data":""})
def handle_data_free_coupon_request(self, data: Dict) -> Dict:
return({"data":""})
@cached(lifetime=86400)
# ShopListMusic load
ret_str += "\r\n#ShopListMusic\r\n"
with open(
r"titles/cxb/rss2_data/Shop/ShopList_Music.csv", encoding="utf-8"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
# ShopListSale load
ret_str += "\r\n#ShopListSale\r\n"
with open(
r"titles/cxb/rss2_data/Shop/ShopList_Sale.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
# ShopListSkinBg load
ret_str += "\r\n#ShopListSkinBg\r\n"
with open(
r"titles/cxb/rss2_data/Shop/ShopList_SkinBg.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
# ShopListSkinEffect load
ret_str += "\r\n#ShopListSkinEffect\r\n"
with open(
r"titles/cxb/rss2_data/Shop/ShopList_SkinEffect.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
# ShopListSkinNotes load
ret_str += "\r\n#ShopListSkinNotes\r\n"
with open(
r"titles/cxb/rss2_data/Shop/ShopList_SkinNotes.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
# ShopListTitle load
ret_str += "\r\n#ShopListTitle\r\n"
with open(
r"titles/cxb/rss2_data/Shop/ShopList_Title.csv", encoding="utf-8"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return {"data": ret_str}
def handle_data_extra_stage_list_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_ex0001_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_one_more_extra_list_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_bonus_list10100_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_oe0001_request(self, data: Dict) -> Dict:
return {"data": ""}
def handle_data_free_coupon_request(self, data: Dict) -> Dict:
return {"data": ""}
@cached(lifetime=86400)
def handle_data_news_list_request(self, data: Dict) -> Dict:
ret_str = ""
with open(r"titles/cxb/rss2_data/NewsList.csv", encoding="UTF-8") as news:
lines = news.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
def handle_data_tips_request(self, data: Dict) -> Dict:
return({"data":""})
return {"data": ""}
def handle_data_release_info_list_request(self, data: Dict) -> Dict:
return({"data":""})
return {"data": ""}
@cached(lifetime=86400)
def handle_data_random_music_list_request(self, data: Dict) -> Dict:
ret_str = ""
@ -141,10 +160,12 @@ class CxbRevSunriseS2(CxbBase):
count = 0
for line in lines:
line_split = line.split(",")
ret_str += str(count) + "," + line_split[0] + "," + line_split[0] + ",\r\n"
ret_str += (
str(count) + "," + line_split[0] + "," + line_split[0] + ",\r\n"
)
return {"data": ret_str}
return({"data":ret_str})
@cached(lifetime=86400)
def handle_data_license_request(self, data: Dict) -> Dict:
ret_str = ""
@ -152,54 +173,58 @@ class CxbRevSunriseS2(CxbBase):
lines = licenses.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
def handle_data_course_list_request(self, data: Dict) -> Dict:
ret_str = ""
with open(r"titles/cxb/rss2_data/Course/CourseList.csv", encoding="UTF-8") as course:
with open(
r"titles/cxb/rss2_data/Course/CourseList.csv", encoding="UTF-8"
) as course:
lines = course.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
def handle_data_csxxxx_request(self, data: Dict) -> Dict:
extra_num = int(data["dldate"]["filetype"][-4:])
ret_str = ""
with open(fr"titles/cxb/rss2_data/Course/Cs{extra_num}.csv", encoding="shift-jis") as course:
with open(
rf"titles/cxb/rss2_data/Course/Cs{extra_num}.csv", encoding="shift-jis"
) as course:
lines = course.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data":ret_str})
return {"data": ret_str}
def handle_data_mission_list_request(self, data: Dict) -> Dict:
return({"data":""})
return {"data": ""}
def handle_data_mission_bonus_request(self, data: Dict) -> Dict:
return({"data":""})
return {"data": ""}
def handle_data_unlimited_mission_request(self, data: Dict) -> Dict:
return({"data":""})
return {"data": ""}
def handle_data_partner_list_request(self, data: Dict) -> Dict:
ret_str = ""
# Lord forgive me for the sins I am about to commit
for i in range(0,10):
for i in range(0, 10):
ret_str += f"80000{i},{i},{i},0,10000,,\r\n"
ret_str += f"80000{i},{i},{i},1,10500,,\r\n"
ret_str += f"80000{i},{i},{i},2,10500,,\r\n"
for i in range(10,13):
for i in range(10, 13):
ret_str += f"8000{i},{i},{i},0,10000,,\r\n"
ret_str += f"8000{i},{i},{i},1,10500,,\r\n"
ret_str += f"8000{i},{i},{i},2,10500,,\r\n"
ret_str +="\r\n---\r\n0,150,100,100,100,100,\r\n"
for i in range(1,130):
ret_str +=f"{i},100,100,100,100,100,\r\n"
ret_str += "\r\n---\r\n0,150,100,100,100,100,\r\n"
for i in range(1, 130):
ret_str += f"{i},100,100,100,100,100,\r\n"
ret_str += "---\r\n"
return({"data": ret_str})
return {"data": ret_str}
@cached(lifetime=86400)
def handle_data_partnerxxxx_request(self, data: Dict) -> Dict:
partner_num = int(data["dldate"]["filetype"][-4:])
@ -208,55 +233,65 @@ class CxbRevSunriseS2(CxbBase):
lines = partner.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return({"data": ret_str})
return {"data": ret_str}
def handle_data_server_state_request(self, data: Dict) -> Dict:
return({"data": True})
return {"data": True}
def handle_data_settings_request(self, data: Dict) -> Dict:
return({"data": "2,\r\n"})
return {"data": "2,\r\n"}
def handle_data_story_list_request(self, data: Dict) -> Dict:
#story id, story name, game version, start time, end time, course arc, unlock flag, song mcode for menu
# story id, story name, game version, start time, end time, course arc, unlock flag, song mcode for menu
ret_str = "\r\n"
ret_str += f"st0000,RISING PURPLE,10104,1464370990,4096483201,Cs1000,-1,purple,\r\n"
ret_str += f"st0001,REBEL YELL,10104,1467999790,4096483201,Cs1000,-1,chaset,\r\n"
ret_str += (
f"st0000,RISING PURPLE,10104,1464370990,4096483201,Cs1000,-1,purple,\r\n"
)
ret_str += (
f"st0001,REBEL YELL,10104,1467999790,4096483201,Cs1000,-1,chaset,\r\n"
)
ret_str += f"st0002,REMNANT,10104,1502127790,4096483201,Cs1000,-1,overcl,\r\n"
return({"data": ret_str})
return {"data": ret_str}
def handle_data_stxxxx_request(self, data: Dict) -> Dict:
story_num = int(data["dldate"]["filetype"][-4:])
ret_str = ""
# Each stories appears to have 10 pieces based on the wiki but as on how they are set.... no clue
for i in range(1,11):
ret_str +=f"{i},st000{story_num}_{i-1},,,,,,,,,,,,,,,,1,,-1,1,\r\n"
return({"data": ret_str})
for i in range(1, 11):
ret_str += f"{i},st000{story_num}_{i-1},,,,,,,,,,,,,,,,1,,-1,1,\r\n"
return {"data": ret_str}
def handle_data_event_stamp_list_request(self, data: Dict) -> Dict:
return({"data":"Cs1002,1,1,1,1,1,1,1,1,1,1,\r\n"})
return {"data": "Cs1002,1,1,1,1,1,1,1,1,1,1,\r\n"}
def handle_data_premium_list_request(self, data: Dict) -> Dict:
return({"data": "1,,,,10,,,,,99,,,,,,,,,100,,\r\n"})
return {"data": "1,,,,10,,,,,99,,,,,,,,,100,,\r\n"}
def handle_data_event_list_request(self, data: Dict) -> Dict:
return({"data":"Cs4001,0,10000,1601510400,1604188799,1,nv2006,1,\r\nCs4005,0,10000,1609459200,1615766399,1,nv2006,1,\r\n"})
return {
"data": "Cs4001,0,10000,1601510400,1604188799,1,nv2006,1,\r\nCs4005,0,10000,1609459200,1615766399,1,nv2006,1,\r\n"
}
def handle_data_event_detail_list_request(self, data: Dict) -> Dict:
event_id = data["dldate"]["filetype"].split("/")[2]
if "Cs4001" in event_id:
return({"data":"#EventMusicList\r\n1,zonzon2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n2,moonki,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n3,tricko,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n"})
return {
"data": "#EventMusicList\r\n1,zonzon2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n2,moonki,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n3,tricko,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n"
}
elif "Cs4005" in event_id:
return({"data":"#EventMusicList\r\n2,firstl,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n2,valent,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n2,dazzli2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n"})
return {
"data": "#EventMusicList\r\n2,firstl,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n2,valent,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n2,dazzli2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,\r\n"
}
elif "EventStampMapListCs1002" in event_id:
return({"data":"1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"})
return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"}
elif "EventStampList" in event_id:
return({"data":"Cs1002,1,1,1,1,1,1,1,1,1,1,\r\n"})
return {"data": "Cs1002,1,1,1,1,1,1,1,1,1,1,\r\n"}
else:
return({"data":""})
return {"data": ""}
def handle_data_event_stamp_map_list_csxxxx_request(self, data: Dict) -> Dict:
event_id = data["dldate"]["filetype"].split("/")[2]
if "EventStampMapListCs1002" in event_id:
return({"data":"1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"})
return {"data": "1,2,1,1,2,3,9,5,6,7,8,9,10,\r\n"}
else:
return({"data":""})
return {"data": ""}

View File

@ -14,32 +14,29 @@ energy = Table(
Column("user", ForeignKey("aime_user.id", ondelete="cascade"), nullable=False),
Column("energy", Integer, nullable=False, server_default="0"),
UniqueConstraint("user", name="cxb_rev_energy_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class CxbItemData(BaseData):
def put_energy(self, user_id: int, rev_energy: int) -> Optional[int]:
sql = insert(energy).values(
user = user_id,
energy = rev_energy
)
conflict = sql.on_duplicate_key_update(
energy = rev_energy
)
class CxbItemData(BaseData):
def put_energy(self, user_id: int, rev_energy: int) -> Optional[int]:
sql = insert(energy).values(user=user_id, energy=rev_energy)
conflict = sql.on_duplicate_key_update(energy=rev_energy)
result = self.execute(conflict)
if result is None:
self.logger.error(f"{__name__} failed to insert item! user: {user_id}, energy: {rev_energy}")
self.logger.error(
f"{__name__} failed to insert item! user: {user_id}, energy: {rev_energy}"
)
return None
return result.lastrowid
def get_energy(self, user_id: int) -> Optional[Dict]:
sql = energy.select(
and_(energy.c.user == user_id)
)
sql = energy.select(and_(energy.c.user == user_id))
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()

View File

@ -16,57 +16,63 @@ profile = Table(
Column("index", Integer, nullable=False),
Column("data", JSON, nullable=False),
UniqueConstraint("user", "index", name="cxb_profile_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class CxbProfileData(BaseData):
def put_profile(self, user_id: int, version: int, index: int, data: JSON) -> Optional[int]:
def put_profile(
self, user_id: int, version: int, index: int, data: JSON
) -> Optional[int]:
sql = insert(profile).values(
user = user_id,
version = version,
index = index,
data = data
user=user_id, version=version, index=index, data=data
)
conflict = sql.on_duplicate_key_update(
index = index,
data = data
)
conflict = sql.on_duplicate_key_update(index=index, data=data)
result = self.execute(conflict)
if result is None:
self.logger.error(f"{__name__} failed to update! user: {user_id}, index: {index}, data: {data}")
self.logger.error(
f"{__name__} failed to update! user: {user_id}, index: {index}, data: {data}"
)
return None
return result.lastrowid
def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]:
"""
Given a game version and either a profile or aime id, return the profile
"""
sql = profile.select(and_(
profile.c.version == version,
profile.c.user == aime_id
))
sql = profile.select(
and_(profile.c.version == version, profile.c.user == aime_id)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def get_profile_index(self, index: int, aime_id: int = None, version: int = None) -> Optional[Dict]:
def get_profile_index(
self, index: int, aime_id: int = None, version: int = None
) -> Optional[Dict]:
"""
Given a game version and either a profile or aime id, return the profile
"""
if aime_id is not None and version is not None and index is not None:
sql = profile.select(and_(
sql = profile.select(
and_(
profile.c.version == version,
profile.c.user == aime_id,
profile.c.index == index
))
profile.c.index == index,
)
)
else:
self.logger.error(f"get_profile: Bad arguments!! aime_id {aime_id} version {version}")
self.logger.error(
f"get_profile: Bad arguments!! aime_id {aime_id} version {version}"
)
return None
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()

View File

@ -18,7 +18,7 @@ score = Table(
Column("song_index", Integer),
Column("data", JSON),
UniqueConstraint("user", "song_mcode", "song_index", name="cxb_score_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
playlog = Table(
@ -40,7 +40,7 @@ playlog = Table(
Column("fail", Integer),
Column("combo", Integer),
Column("date_scored", TIMESTAMP, server_default=func.now()),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
ranking = Table(
@ -53,11 +53,19 @@ ranking = Table(
Column("score", Integer),
Column("clear", Integer),
UniqueConstraint("user", "rev_id", name="cxb_ranking_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class CxbScoreData(BaseData):
def put_best_score(self, user_id: int, song_mcode: str, game_version: int, song_index: int, data: JSON) -> Optional[int]:
def put_best_score(
self,
user_id: int,
song_mcode: str,
game_version: int,
song_index: int,
data: JSON,
) -> Optional[int]:
"""
Update the user's best score for a chart
"""
@ -66,22 +74,37 @@ class CxbScoreData(BaseData):
song_mcode=song_mcode,
game_version=game_version,
song_index=song_index,
data=data
data=data,
)
conflict = sql.on_duplicate_key_update(
data = sql.inserted.data
)
conflict = sql.on_duplicate_key_update(data=sql.inserted.data)
result = self.execute(conflict)
if result is None:
self.logger.error(f"{__name__} failed to insert best score! profile: {user_id}, song: {song_mcode}, data: {data}")
self.logger.error(
f"{__name__} failed to insert best score! profile: {user_id}, song: {song_mcode}, data: {data}"
)
return None
return result.lastrowid
def put_playlog(self, user_id: int, song_mcode: str, chart_id: int, score: int, clear: int, flawless: int, this_super: int,
cool: int, this_fast: int, this_fast2: int, this_slow: int, this_slow2: int, fail: int, combo: int) -> Optional[int]:
def put_playlog(
self,
user_id: int,
song_mcode: str,
chart_id: int,
score: int,
clear: int,
flawless: int,
this_super: int,
cool: int,
this_fast: int,
this_fast2: int,
this_slow: int,
this_slow2: int,
fail: int,
combo: int,
) -> Optional[int]:
"""
Add an entry to the user's play log
"""
@ -99,45 +122,42 @@ class CxbScoreData(BaseData):
slow=this_slow,
slow2=this_slow2,
fail=fail,
combo=combo
combo=combo,
)
result = self.execute(sql)
if result is None:
self.logger.error(f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_mcode}, chart: {chart_id}")
self.logger.error(
f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_mcode}, chart: {chart_id}"
)
return None
return result.lastrowid
def put_ranking(self, user_id: int, rev_id: int, song_id: int, score: int, clear: int) -> Optional[int]:
def put_ranking(
self, user_id: int, rev_id: int, song_id: int, score: int, clear: int
) -> Optional[int]:
"""
Add an entry to the user's ranking logs
"""
if song_id == 0:
sql = insert(ranking).values(
user=user_id,
rev_id=rev_id,
score=score,
clear=clear
user=user_id, rev_id=rev_id, score=score, clear=clear
)
else:
sql = insert(ranking).values(
user=user_id,
rev_id=rev_id,
song_id=song_id,
score=score,
clear=clear
user=user_id, rev_id=rev_id, song_id=song_id, score=score, clear=clear
)
conflict = sql.on_duplicate_key_update(
score = score
)
conflict = sql.on_duplicate_key_update(score=score)
result = self.execute(conflict)
if result is None:
self.logger.error(f"{__name__} failed to insert ranking log! profile: {user_id}, score: {score}, clear: {clear}")
self.logger.error(
f"{__name__} failed to insert ranking log! profile: {user_id}, score: {score}, clear: {clear}"
)
return None
return result.lastrowid
def get_best_score(self, user_id: int, song_mcode: int) -> Optional[Dict]:
@ -146,21 +166,22 @@ class CxbScoreData(BaseData):
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def get_best_scores(self, user_id: int) -> Optional[Dict]:
sql = score.select(score.c.user == user_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def get_best_rankings(self, user_id: int) -> Optional[List[Dict]]:
sql = ranking.select(
ranking.c.user == user_id
)
sql = ranking.select(ranking.c.user == user_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()

View File

@ -21,54 +21,75 @@ music = Table(
Column("artist", String(255)),
Column("category", String(255)),
Column("level", Float),
UniqueConstraint("version", "songId", "chartId", "index", name="cxb_static_music_uk"),
mysql_charset='utf8mb4'
UniqueConstraint(
"version", "songId", "chartId", "index", name="cxb_static_music_uk"
),
mysql_charset="utf8mb4",
)
class CxbStaticData(BaseData):
def put_music(self, version: int, mcode: str, index: int, chart: int, title: str, artist: str, category: str, level: float ) -> Optional[int]:
def put_music(
self,
version: int,
mcode: str,
index: int,
chart: int,
title: str,
artist: str,
category: str,
level: float,
) -> Optional[int]:
sql = insert(music).values(
version = version,
songId = mcode,
index = index,
chartId = chart,
title = title,
artist = artist,
category = category,
level = level
version=version,
songId=mcode,
index=index,
chartId=chart,
title=title,
artist=artist,
category=category,
level=level,
)
conflict = sql.on_duplicate_key_update(
title = title,
artist = artist,
category = category,
level = level
title=title, artist=artist, category=category, level=level
)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_music(self, version: int, song_id: Optional[int] = None) -> Optional[List[Row]]:
def get_music(
self, version: int, song_id: Optional[int] = None
) -> Optional[List[Row]]:
if song_id is None:
sql = select(music).where(music.c.version == version)
else:
sql = select(music).where(and_(
music.c.version == version,
music.c.songId == song_id,
))
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def get_music_chart(self, version: int, song_id: int, chart_id: int) -> Optional[List[Row]]:
sql = select(music).where(and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id
))
def get_music_chart(
self, version: int, song_id: int, chart_id: int
) -> Optional[List[Row]]:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id,
)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()

View File

@ -7,4 +7,4 @@ index = DivaServlet
database = DivaData
reader = DivaReader
game_codes = [DivaConstants.GAME_CODE]
current_schema_version = 1
current_schema_version = 1

View File

@ -1,6 +1,6 @@
import datetime
from typing import Any, List, Dict
import logging
import logging
import json
import urllib
@ -9,34 +9,35 @@ from titles.diva.config import DivaConfig
from titles.diva.const import DivaConstants
from titles.diva.database import DivaData
class DivaBase():
class DivaBase:
def __init__(self, cfg: CoreConfig, game_cfg: DivaConfig) -> None:
self.core_cfg = cfg # Config file
self.core_cfg = cfg # Config file
self.game_config = game_cfg
self.data = DivaData(cfg) # Database
self.data = DivaData(cfg) # Database
self.date_time_format = "%Y-%m-%d %H:%M:%S"
self.logger = logging.getLogger("diva")
self.game = DivaConstants.GAME_CODE
self.version = DivaConstants.VER_PROJECT_DIVA_ARCADE_FUTURE_TONE
dt = datetime.datetime.now()
self.time_lut=urllib.parse.quote(dt.strftime("%Y-%m-%d %H:%M:%S:16.0"))
self.time_lut = urllib.parse.quote(dt.strftime("%Y-%m-%d %H:%M:%S:16.0"))
def handle_test_request(self, data: Dict) -> Dict:
return ""
def handle_game_init_request(self, data: Dict) -> Dict:
return ( f'' )
return f""
def handle_attend_request(self, data: Dict) -> Dict:
encoded = "&"
params = {
'atnd_prm1': '0,1,1,0,0,0,1,0,100,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1',
'atnd_prm2': '30,10,100,4,1,50,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1',
'atnd_prm3': '100,0,1,1,1,1,1,1,1,1,2,3,4,1,1,1,3,4,5,1,1,1,4,5,6,1,1,1,5,6,7,4,4,4,9,10,14,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,10,30,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0',
'atnd_lut': f'{self.time_lut}',
"atnd_prm1": "0,1,1,0,0,0,1,0,100,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1",
"atnd_prm2": "30,10,100,4,1,50,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1",
"atnd_prm3": "100,0,1,1,1,1,1,1,1,1,2,3,4,1,1,1,3,4,5,1,1,1,4,5,6,1,1,1,5,6,7,4,4,4,9,10,14,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,10,30,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0",
"atnd_lut": f"{self.time_lut}",
}
encoded += urllib.parse.urlencode(params)
encoded = encoded.replace("%2C", ",")
@ -45,42 +46,42 @@ class DivaBase():
def handle_ping_request(self, data: Dict) -> Dict:
encoded = "&"
params = {
'ping_b_msg': f'Welcome to {self.core_cfg.server.name} network!',
'ping_m_msg': 'xxx',
'atnd_lut': f'{self.time_lut}',
'fi_lut': f'{self.time_lut}',
'ci_lut': f'{self.time_lut}',
'qi_lut': f'{self.time_lut}',
'pvl_lut': '2021-05-22 12:08:16.0',
'shp_ctlg_lut': '2020-06-10 19:44:16.0',
'cstmz_itm_ctlg_lut': '2019-10-08 20:23:12.0',
'ngwl_lut': '2019-10-08 20:23:12.0',
'rnk_nv_lut': '2020-06-10 19:51:30.0',
'rnk_ps_lut': f'{self.time_lut}',
'bi_lut': '2020-09-18 10:00:00.0',
'cpi_lut': '2020-10-25 09:25:10.0',
'bdlol_lut': '2020-09-18 10:00:00.0',
'p_std_hc_lut': '2019-08-01 04:00:36.0',
'p_std_i_n_lut': '2019-08-01 04:00:36.0',
'pdcl_lut': '2019-08-01 04:00:36.0',
'pnml_lut': '2019-08-01 04:00:36.0',
'cinml_lut': '2019-08-01 04:00:36.0',
'rwl_lut': '2019-08-01 04:00:36.0',
'req_inv_cmd_num': '-1,-1,-1,-1,-1,-1,-1,-1,-1,-1',
'req_inv_cmd_prm1': '-1,-1,-1,-1,-1,-1,-1,-1,-1,-1',
'req_inv_cmd_prm2': '-1,-1,-1,-1,-1,-1,-1,-1,-1,-1',
'req_inv_cmd_prm3': '-1,-1,-1,-1,-1,-1,-1,-1,-1,-1',
'req_inv_cmd_prm4': '-1,-1,-1,-1,-1,-1,-1,-1,-1,-1',
'pow_save_flg': 0,
'nblss_dnt_p': 100,
'nblss_ltt_rl_vp': 1500,
'nblss_ex_ltt_flg': 1,
'nblss_dnt_st_tm': "2019-07-15 12:00:00.0",
'nblss_dnt_ed_tm': "2019-09-17 12:00:00.0",
'nblss_ltt_st_tm': "2019-09-18 12:00:00.0",
'nblss_ltt_ed_tm': "2019-09-22 12:00:00.0",
"ping_b_msg": f"Welcome to {self.core_cfg.server.name} network!",
"ping_m_msg": "xxx",
"atnd_lut": f"{self.time_lut}",
"fi_lut": f"{self.time_lut}",
"ci_lut": f"{self.time_lut}",
"qi_lut": f"{self.time_lut}",
"pvl_lut": "2021-05-22 12:08:16.0",
"shp_ctlg_lut": "2020-06-10 19:44:16.0",
"cstmz_itm_ctlg_lut": "2019-10-08 20:23:12.0",
"ngwl_lut": "2019-10-08 20:23:12.0",
"rnk_nv_lut": "2020-06-10 19:51:30.0",
"rnk_ps_lut": f"{self.time_lut}",
"bi_lut": "2020-09-18 10:00:00.0",
"cpi_lut": "2020-10-25 09:25:10.0",
"bdlol_lut": "2020-09-18 10:00:00.0",
"p_std_hc_lut": "2019-08-01 04:00:36.0",
"p_std_i_n_lut": "2019-08-01 04:00:36.0",
"pdcl_lut": "2019-08-01 04:00:36.0",
"pnml_lut": "2019-08-01 04:00:36.0",
"cinml_lut": "2019-08-01 04:00:36.0",
"rwl_lut": "2019-08-01 04:00:36.0",
"req_inv_cmd_num": "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1",
"req_inv_cmd_prm1": "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1",
"req_inv_cmd_prm2": "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1",
"req_inv_cmd_prm3": "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1",
"req_inv_cmd_prm4": "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1",
"pow_save_flg": 0,
"nblss_dnt_p": 100,
"nblss_ltt_rl_vp": 1500,
"nblss_ex_ltt_flg": 1,
"nblss_dnt_st_tm": "2019-07-15 12:00:00.0",
"nblss_dnt_ed_tm": "2019-09-17 12:00:00.0",
"nblss_ltt_st_tm": "2019-09-18 12:00:00.0",
"nblss_ltt_ed_tm": "2019-09-22 12:00:00.0",
}
encoded += urllib.parse.urlencode(params)
encoded = encoded.replace("+", "%20")
encoded = encoded.replace("%2C", ",")
@ -122,7 +123,7 @@ class DivaBase():
response += f"&pvl_lut={self.time_lut}"
response += f"&pv_lst={pvlist}"
return ( response )
return response
def handle_shop_catalog_request(self, data: Dict) -> Dict:
catalog = ""
@ -137,7 +138,21 @@ class DivaBase():
else:
for shop in shopList:
line = str(shop["shopId"]) + "," + str(shop['unknown_0']) + "," + shop['name'] + "," + str(shop['points']) + "," + shop['start_date'] + "," + shop['end_date'] + "," + str(shop["type"])
line = (
str(shop["shopId"])
+ ","
+ str(shop["unknown_0"])
+ ","
+ shop["name"]
+ ","
+ str(shop["points"])
+ ","
+ shop["start_date"]
+ ","
+ shop["end_date"]
+ ","
+ str(shop["type"])
)
line = urllib.parse.quote(line) + ","
catalog += f"{urllib.parse.quote(line)}"
@ -146,7 +161,7 @@ class DivaBase():
response = f"&shp_ctlg_lut={self.time_lut}"
response += f"&shp_ctlg={catalog[:-3]}"
return ( response )
return response
def handle_buy_module_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile(data["pd_id"], self.version)
@ -162,10 +177,7 @@ class DivaBase():
new_vcld_pts = profile["vcld_pts"] - int(data["mdl_price"])
self.data.profile.update_profile(
profile["user"],
vcld_pts=new_vcld_pts
)
self.data.profile.update_profile(profile["user"], vcld_pts=new_vcld_pts)
self.data.module.put_module(data["pd_id"], self.version, data["mdl_id"])
# generate the mdl_have string
@ -191,7 +203,21 @@ class DivaBase():
else:
for item in itemList:
line = str(item["itemId"]) + "," + str(item['unknown_0']) + "," + item['name'] + "," + str(item['points']) + "," + item['start_date'] + "," + item['end_date'] + "," + str(item["type"])
line = (
str(item["itemId"])
+ ","
+ str(item["unknown_0"])
+ ","
+ item["name"]
+ ","
+ str(item["points"])
+ ","
+ item["start_date"]
+ ","
+ item["end_date"]
+ ","
+ str(item["type"])
)
line = urllib.parse.quote(line) + ","
catalog += f"{urllib.parse.quote(line)}"
@ -200,11 +226,13 @@ class DivaBase():
response = f"&cstmz_itm_ctlg_lut={self.time_lut}"
response += f"&cstmz_itm_ctlg={catalog[:-3]}"
return ( response )
return response
def handle_buy_cstmz_itm_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile(data["pd_id"], self.version)
item = self.data.static.get_enabled_item(self.version, int(data["cstmz_itm_id"]))
item = self.data.static.get_enabled_item(
self.version, int(data["cstmz_itm_id"])
)
# make sure module is available to purchase
if not item:
@ -217,15 +245,16 @@ class DivaBase():
new_vcld_pts = profile["vcld_pts"] - int(data["cstmz_itm_price"])
# save new Vocaloid Points balance
self.data.profile.update_profile(
profile["user"],
vcld_pts=new_vcld_pts
self.data.profile.update_profile(profile["user"], vcld_pts=new_vcld_pts)
self.data.customize.put_customize_item(
data["pd_id"], self.version, data["cstmz_itm_id"]
)
self.data.customize.put_customize_item(data["pd_id"], self.version, data["cstmz_itm_id"])
# generate the cstmz_itm_have string
cstmz_itm_have = self.data.customize.get_customize_items_have_string(data["pd_id"], self.version)
cstmz_itm_have = self.data.customize.get_customize_items_have_string(
data["pd_id"], self.version
)
response = "&shp_rslt=1"
response += f"&cstmz_itm_id={data['cstmz_itm_id']}"
@ -237,33 +266,33 @@ class DivaBase():
def handle_festa_info_request(self, data: Dict) -> Dict:
encoded = "&"
params = {
'fi_id': '1,-1',
'fi_name': f'{self.core_cfg.server.name} Opening,xxx',
'fi_kind': '0,0',
'fi_difficulty': '-1,-1',
'fi_pv_id_lst': 'ALL,ALL',
'fi_attr': '7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
'fi_add_vp': '20,0',
'fi_mul_vp': '1,1',
'fi_st': '2022-06-17 17:00:00.0,2014-07-08 18:10:11.0',
'fi_et': '2029-01-01 10:00:00.0,2014-07-08 18:10:11.0',
'fi_lut': '{self.time_lut}',
"fi_id": "1,-1",
"fi_name": f"{self.core_cfg.server.name} Opening,xxx",
"fi_kind": "0,0",
"fi_difficulty": "-1,-1",
"fi_pv_id_lst": "ALL,ALL",
"fi_attr": "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
"fi_add_vp": "20,0",
"fi_mul_vp": "1,1",
"fi_st": "2022-06-17 17:00:00.0,2014-07-08 18:10:11.0",
"fi_et": "2029-01-01 10:00:00.0,2014-07-08 18:10:11.0",
"fi_lut": "{self.time_lut}",
}
encoded += urllib.parse.urlencode(params)
encoded = encoded.replace("+", "%20")
encoded = encoded.replace("%2C", ",")
return encoded
def handle_contest_info_request(self, data: Dict) -> Dict:
response = ""
response += f"&ci_lut={self.time_lut}"
response += "&ci_str=%2A%2A%2A,%2A%2A%2A,%2A%2A%2A,%2A%2A%2A,%2A%2A%2A,%2A%2A%2A,%2A%2A%2A,%2A%2A%2A"
return ( response )
return response
def handle_qst_inf_request(self, data: Dict) -> Dict:
quest = ""
@ -279,11 +308,31 @@ class DivaBase():
response += f"&qhi_str={quest[:-1]}"
else:
for quests in questList:
line = str(quests["questId"]) + "," + str(quests['quest_order']) + "," + str(quests['kind']) + "," + str(quests['unknown_0']) + "," + quests['start_datetime'] + "," + quests['end_datetime'] + "," + quests["name"] + "," + str(quests["unknown_1"]) + "," + str(quests["unknown_2"]) + "," + str(quests["quest_enable"])
line = (
str(quests["questId"])
+ ","
+ str(quests["quest_order"])
+ ","
+ str(quests["kind"])
+ ","
+ str(quests["unknown_0"])
+ ","
+ quests["start_datetime"]
+ ","
+ quests["end_datetime"]
+ ","
+ quests["name"]
+ ","
+ str(quests["unknown_1"])
+ ","
+ str(quests["unknown_2"])
+ ","
+ str(quests["quest_enable"])
)
quest += f"{urllib.parse.quote(line)}%0A,"
responseline = f"{quest[:-1]},"
for i in range(len(questList),59):
for i in range(len(questList), 59):
responseline += "%2A%2A%2A%0A,"
response = ""
@ -292,44 +341,44 @@ class DivaBase():
response += "&qrai_str=%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1,%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1,%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1%2C%2D1"
return ( response )
return response
def handle_nv_ranking_request(self, data: Dict) -> Dict:
return ( f'' )
return f""
def handle_ps_ranking_request(self, data: Dict) -> Dict:
return ( f'' )
return f""
def handle_ng_word_request(self, data: Dict) -> Dict:
return ( f'' )
return f""
def handle_rmt_wp_list_request(self, data: Dict) -> Dict:
return ( f'' )
return f""
def handle_pv_def_chr_list_request(self, data: Dict) -> Dict:
return ( f'' )
return f""
def handle_pv_ng_mdl_list_request(self, data: Dict) -> Dict:
return ( f'' )
return f""
def handle_cstmz_itm_ng_mdl_lst_request(self, data: Dict) -> Dict:
return ( f'' )
return f""
def handle_banner_info_request(self, data: Dict) -> Dict:
return ( f'' )
return f""
def handle_banner_data_request(self, data: Dict) -> Dict:
return ( f'' )
return f""
def handle_cm_ply_info_request(self, data: Dict) -> Dict:
return ( f'' )
return f""
def handle_pstd_h_ctrl_request(self, data: Dict) -> Dict:
return ( f'' )
return f""
def handle_pstd_item_ng_lst_request(self, data: Dict) -> Dict:
return ( f'' )
return f""
def handle_pre_start_request(self, data: Dict) -> str:
profile = self.data.profile.get_profile(data["aime_id"], self.version)
profile_shop = self.data.item.get_shop(data["aime_id"], self.version)
@ -372,8 +421,10 @@ class DivaBase():
return response
def handle_registration_request(self, data: Dict) -> Dict:
self.data.profile.create_profile(self.version, data["aime_id"], data["player_name"])
return (f"&cd_adm_result=1&pd_id={data['aime_id']}")
self.data.profile.create_profile(
self.version, data["aime_id"], data["player_name"]
)
return f"&cd_adm_result=1&pd_id={data['aime_id']}"
def handle_start_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile(data["pd_id"], self.version)
@ -384,12 +435,16 @@ class DivaBase():
mdl_have = "F" * 250
# generate the mdl_have string if "unlock_all_modules" is disabled
if not self.game_config.mods.unlock_all_modules:
mdl_have = self.data.module.get_modules_have_string(data["pd_id"], self.version)
mdl_have = self.data.module.get_modules_have_string(
data["pd_id"], self.version
)
cstmz_itm_have = "F" * 250
# generate the cstmz_itm_have string if "unlock_all_items" is disabled
if not self.game_config.mods.unlock_all_items:
cstmz_itm_have = self.data.customize.get_customize_items_have_string(data["pd_id"], self.version)
cstmz_itm_have = self.data.customize.get_customize_items_have_string(
data["pd_id"], self.version
)
response = f"&pd_id={data['pd_id']}"
response += "&start_result=1"
@ -452,15 +507,16 @@ class DivaBase():
response += f"&mdl_eqp_ary={mdl_eqp_ary}"
response += f"&c_itm_eqp_ary={c_itm_eqp_ary}"
response += f"&ms_itm_flg_ary={ms_itm_flg_ary}"
return ( response )
return response
def handle_pd_unlock_request(self, data: Dict) -> Dict:
return ( f'' )
return f""
def handle_spend_credit_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile(data["pd_id"], self.version)
if profile is None: return
if profile is None:
return
response = ""
@ -471,10 +527,16 @@ class DivaBase():
response += f"&lv_efct_id={profile['lv_efct_id']}"
response += f"&lv_plt_id={profile['lv_plt_id']}"
return ( response )
return response
def _get_pv_pd_result(self, song: int, pd_db_song: Dict, pd_db_ranking: Dict,
pd_db_customize: Dict, edition: int) -> str:
def _get_pv_pd_result(
self,
song: int,
pd_db_song: Dict,
pd_db_ranking: Dict,
pd_db_customize: Dict,
edition: int,
) -> str:
"""
Helper function to generate the pv_result string for every song, ranking and edition
"""
@ -483,7 +545,7 @@ class DivaBase():
# make sure there are enough max scores to calculate a ranking
if pd_db_ranking["ranking"] != 0:
global_ranking = pd_db_ranking["ranking"]
# pv_no
pv_result = f"{song},"
# edition
@ -513,7 +575,7 @@ class DivaBase():
f"{pd_db_customize['chsld_se']},"
f"{pd_db_customize['sldtch_se']}"
)
pv_result += f"{module_eqp},"
pv_result += f"{customize_eqp},"
pv_result += f"{customize_flag},"
@ -537,21 +599,35 @@ class DivaBase():
if int(song) > 0:
# the request do not send a edition so just perform a query best score and ranking for each edition.
# 0=ORIGINAL, 1=EXTRA
pd_db_song_0 = self.data.score.get_best_user_score(data["pd_id"], int(song), data["difficulty"], edition=0)
pd_db_song_1 = self.data.score.get_best_user_score(data["pd_id"], int(song), data["difficulty"], edition=1)
pd_db_song_0 = self.data.score.get_best_user_score(
data["pd_id"], int(song), data["difficulty"], edition=0
)
pd_db_song_1 = self.data.score.get_best_user_score(
data["pd_id"], int(song), data["difficulty"], edition=1
)
pd_db_ranking_0, pd_db_ranking_1 = None, None
if pd_db_song_0:
pd_db_ranking_0 = self.data.score.get_global_ranking(data["pd_id"], int(song), data["difficulty"], edition=0)
pd_db_ranking_0 = self.data.score.get_global_ranking(
data["pd_id"], int(song), data["difficulty"], edition=0
)
if pd_db_song_1:
pd_db_ranking_1 = self.data.score.get_global_ranking(data["pd_id"], int(song), data["difficulty"], edition=1)
pd_db_ranking_1 = self.data.score.get_global_ranking(
data["pd_id"], int(song), data["difficulty"], edition=1
)
pd_db_customize = self.data.pv_customize.get_pv_customize(
data["pd_id"], int(song)
)
pd_db_customize = self.data.pv_customize.get_pv_customize(data["pd_id"], int(song))
# generate the pv_result string with the ORIGINAL edition and the EXTRA edition appended
pv_result = self._get_pv_pd_result(int(song), pd_db_song_0, pd_db_ranking_0, pd_db_customize, edition=0)
pv_result += "," + self._get_pv_pd_result(int(song), pd_db_song_1, pd_db_ranking_1, pd_db_customize, edition=1)
pv_result = self._get_pv_pd_result(
int(song), pd_db_song_0, pd_db_ranking_0, pd_db_customize, edition=0
)
pv_result += "," + self._get_pv_pd_result(
int(song), pd_db_song_1, pd_db_ranking_1, pd_db_customize, edition=1
)
self.logger.debug(f"pv_result = {pv_result}")
@ -565,13 +641,12 @@ class DivaBase():
response += "&pdddt_flg=0"
response += f"&pdddt_tm={self.time_lut}"
return ( response )
return response
def handle_stage_start_request(self, data: Dict) -> Dict:
return ( f'' )
return f""
def handle_stage_result_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile(data["pd_id"], self.version)
pd_song_list = data["stg_ply_pv_id"].split(",")
@ -590,15 +665,100 @@ class DivaBase():
for index, value in enumerate(pd_song_list):
if "-1" not in pd_song_list[index]:
profile_pd_db_song = self.data.score.get_best_user_score(data["pd_id"], pd_song_list[index], pd_song_difficulty[index], pd_song_edition[index])
profile_pd_db_song = self.data.score.get_best_user_score(
data["pd_id"],
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
)
if profile_pd_db_song is None:
self.data.score.put_best_score(data["pd_id"], self.version, pd_song_list[index], pd_song_difficulty[index], pd_song_edition[index], pd_song_max_score[index], pd_song_max_atn_pnt[index], pd_song_ranking[index], pd_song_sort_kind, pd_song_cool_cnt[index], pd_song_fine_cnt[index], pd_song_safe_cnt[index], pd_song_sad_cnt[index], pd_song_worst_cnt[index], pd_song_max_combo[index])
self.data.score.put_playlog(data["pd_id"], self.version, pd_song_list[index], pd_song_difficulty[index], pd_song_edition[index], pd_song_max_score[index], pd_song_max_atn_pnt[index], pd_song_ranking[index], pd_song_sort_kind, pd_song_cool_cnt[index], pd_song_fine_cnt[index], pd_song_safe_cnt[index], pd_song_sad_cnt[index], pd_song_worst_cnt[index], pd_song_max_combo[index])
self.data.score.put_best_score(
data["pd_id"],
self.version,
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
pd_song_max_score[index],
pd_song_max_atn_pnt[index],
pd_song_ranking[index],
pd_song_sort_kind,
pd_song_cool_cnt[index],
pd_song_fine_cnt[index],
pd_song_safe_cnt[index],
pd_song_sad_cnt[index],
pd_song_worst_cnt[index],
pd_song_max_combo[index],
)
self.data.score.put_playlog(
data["pd_id"],
self.version,
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
pd_song_max_score[index],
pd_song_max_atn_pnt[index],
pd_song_ranking[index],
pd_song_sort_kind,
pd_song_cool_cnt[index],
pd_song_fine_cnt[index],
pd_song_safe_cnt[index],
pd_song_sad_cnt[index],
pd_song_worst_cnt[index],
pd_song_max_combo[index],
)
elif int(pd_song_max_score[index]) >= int(profile_pd_db_song["score"]):
self.data.score.put_best_score(data["pd_id"], self.version, pd_song_list[index], pd_song_difficulty[index], pd_song_edition[index], pd_song_max_score[index], pd_song_max_atn_pnt[index], pd_song_ranking[index], pd_song_sort_kind, pd_song_cool_cnt[index], pd_song_fine_cnt[index], pd_song_safe_cnt[index], pd_song_sad_cnt[index], pd_song_worst_cnt[index], pd_song_max_combo[index])
self.data.score.put_playlog(data["pd_id"], self.version, pd_song_list[index], pd_song_difficulty[index], pd_song_edition[index], pd_song_max_score[index], pd_song_max_atn_pnt[index], pd_song_ranking[index], pd_song_sort_kind, pd_song_cool_cnt[index], pd_song_fine_cnt[index], pd_song_safe_cnt[index], pd_song_sad_cnt[index], pd_song_worst_cnt[index], pd_song_max_combo[index])
self.data.score.put_best_score(
data["pd_id"],
self.version,
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
pd_song_max_score[index],
pd_song_max_atn_pnt[index],
pd_song_ranking[index],
pd_song_sort_kind,
pd_song_cool_cnt[index],
pd_song_fine_cnt[index],
pd_song_safe_cnt[index],
pd_song_sad_cnt[index],
pd_song_worst_cnt[index],
pd_song_max_combo[index],
)
self.data.score.put_playlog(
data["pd_id"],
self.version,
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
pd_song_max_score[index],
pd_song_max_atn_pnt[index],
pd_song_ranking[index],
pd_song_sort_kind,
pd_song_cool_cnt[index],
pd_song_fine_cnt[index],
pd_song_safe_cnt[index],
pd_song_sad_cnt[index],
pd_song_worst_cnt[index],
pd_song_max_combo[index],
)
elif int(pd_song_max_score[index]) != int(profile_pd_db_song["score"]):
self.data.score.put_playlog(data["pd_id"], self.version, pd_song_list[index], pd_song_difficulty[index], pd_song_edition[index], pd_song_max_score[index], pd_song_max_atn_pnt[index], pd_song_ranking[index], pd_song_sort_kind, pd_song_cool_cnt[index], pd_song_fine_cnt[index], pd_song_safe_cnt[index], pd_song_sad_cnt[index], pd_song_worst_cnt[index], pd_song_max_combo[index])
self.data.score.put_playlog(
data["pd_id"],
self.version,
pd_song_list[index],
pd_song_difficulty[index],
pd_song_edition[index],
pd_song_max_score[index],
pd_song_max_atn_pnt[index],
pd_song_ranking[index],
pd_song_sort_kind,
pd_song_cool_cnt[index],
pd_song_fine_cnt[index],
pd_song_safe_cnt[index],
pd_song_sad_cnt[index],
pd_song_worst_cnt[index],
pd_song_max_combo[index],
)
# Profile saving based on registration list
@ -608,7 +768,7 @@ class DivaBase():
total_atn_pnt = 0
for best_score in best_scores:
total_atn_pnt += best_score["atn_pnt"]
new_level = (total_atn_pnt // 13979) + 1
new_level_pnt = round((total_atn_pnt % 13979) / 13979 * 100)
@ -630,7 +790,7 @@ class DivaBase():
nxt_dffclty=int(data["nxt_dffclty"]),
nxt_edtn=int(data["nxt_edtn"]),
my_qst_id=data["my_qst_id"],
my_qst_sts=data["my_qst_sts"]
my_qst_sts=data["my_qst_sts"],
)
response += f"&lv_num={new_level}"
@ -663,35 +823,51 @@ class DivaBase():
response += "&my_ccd_r_hnd=-1,-1,-1,-1,-1"
response += "&my_ccd_r_vp=-1,-1,-1,-1,-1"
return ( response )
return response
def handle_end_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile(data["pd_id"], self.version)
self.data.profile.update_profile(
profile["user"],
my_qst_id=data["my_qst_id"],
my_qst_sts=data["my_qst_sts"]
profile["user"], my_qst_id=data["my_qst_id"], my_qst_sts=data["my_qst_sts"]
)
return (f'')
return f""
def handle_shop_exit_request(self, data: Dict) -> Dict:
self.data.item.put_shop(data["pd_id"], self.version, data["mdl_eqp_cmn_ary"], data["c_itm_eqp_cmn_ary"], data["ms_itm_flg_cmn_ary"])
self.data.item.put_shop(
data["pd_id"],
self.version,
data["mdl_eqp_cmn_ary"],
data["c_itm_eqp_cmn_ary"],
data["ms_itm_flg_cmn_ary"],
)
if int(data["use_pv_mdl_eqp"]) == 1:
self.data.pv_customize.put_pv_customize(data["pd_id"], self.version, data["ply_pv_id"],
data["mdl_eqp_pv_ary"], data["c_itm_eqp_pv_ary"], data["ms_itm_flg_pv_ary"])
self.data.pv_customize.put_pv_customize(
data["pd_id"],
self.version,
data["ply_pv_id"],
data["mdl_eqp_pv_ary"],
data["c_itm_eqp_pv_ary"],
data["ms_itm_flg_pv_ary"],
)
else:
self.data.pv_customize.put_pv_customize(data["pd_id"], self.version, data["ply_pv_id"],
"-1,-1,-1", "-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1", "1,1,1,1,1,1,1,1,1,1,1,1")
self.data.pv_customize.put_pv_customize(
data["pd_id"],
self.version,
data["ply_pv_id"],
"-1,-1,-1",
"-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1",
"1,1,1,1,1,1,1,1,1,1,1,1",
)
response = "&shp_rslt=1"
return ( response )
return response
def handle_card_procedure_request(self, data: Dict) -> str:
profile = self.data.profile.get_profile(data["aime_id"], self.version)
if profile is None:
return "&cd_adm_result=0"
response = "&cd_adm_result=1"
response += "&chg_name_price=100"
response += "&accept_idx=100"
@ -706,20 +882,18 @@ class DivaBase():
response += f"&passwd_stat={profile['passwd_stat']}"
return response
def handle_change_name_request(self, data: Dict) -> str:
profile = self.data.profile.get_profile(data["pd_id"], self.version)
# make sure user has enough Vocaloid Points
if profile["vcld_pts"] < int(data["chg_name_price"]):
return "&cd_adm_result=0"
# update the vocaloid points and player name
new_vcld_pts = profile["vcld_pts"] - int(data["chg_name_price"])
self.data.profile.update_profile(
profile["user"],
player_name=data["player_name"],
vcld_pts=new_vcld_pts
profile["user"], player_name=data["player_name"], vcld_pts=new_vcld_pts
)
response = "&cd_adm_result=1"
@ -728,19 +902,17 @@ class DivaBase():
response += f"&player_name={data['player_name']}"
return response
def handle_change_passwd_request(self, data: Dict) -> str:
profile = self.data.profile.get_profile(data["pd_id"], self.version)
# TODO: return correct error number instead of 0
if (data["passwd"] != profile["passwd"]):
if data["passwd"] != profile["passwd"]:
return "&cd_adm_result=0"
# set password to true and update the saved password
self.data.profile.update_profile(
profile["user"],
passwd_stat=1,
passwd=data["new_passwd"]
profile["user"], passwd_stat=1, passwd=data["new_passwd"]
)
response = "&cd_adm_result=1"

View File

@ -1,30 +1,40 @@
from core.config import CoreConfig
class DivaServerConfig():
class DivaServerConfig:
def __init__(self, parent_config: "DivaConfig") -> None:
self.__config = parent_config
@property
def enable(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'diva', 'server', 'enable', default=True)
return CoreConfig.get_config_field(
self.__config, "diva", "server", "enable", default=True
)
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'diva', 'server', 'loglevel', default="info"))
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "diva", "server", "loglevel", default="info"
)
)
class DivaModsConfig():
class DivaModsConfig:
def __init__(self, parent_config: "DivaConfig") -> None:
self.__config = parent_config
@property
def unlock_all_modules(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'diva', 'mods', 'unlock_all_modules', default=True)
return CoreConfig.get_config_field(
self.__config, "diva", "mods", "unlock_all_modules", default=True
)
@property
def unlock_all_items(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'diva', 'mods', 'unlock_all_items', default=True)
return CoreConfig.get_config_field(
self.__config, "diva", "mods", "unlock_all_items", default=True
)
class DivaConfig(dict):

View File

@ -1,4 +1,4 @@
class DivaConstants():
class DivaConstants:
GAME_CODE = "SBZV"
CONFIG_NAME = "diva.yaml"
@ -10,4 +10,4 @@ class DivaConstants():
@classmethod
def game_ver_to_string(cls, ver: int):
return cls.VERSION_NAMES[ver]
return cls.VERSION_NAMES[ver]

View File

@ -1,6 +1,14 @@
from core.data import Data
from core.config import CoreConfig
from titles.diva.schema import DivaProfileData, DivaScoreData, DivaModuleData, DivaCustomizeItemData, DivaPvCustomizeData, DivaItemData, DivaStaticData
from titles.diva.schema import (
DivaProfileData,
DivaScoreData,
DivaModuleData,
DivaCustomizeItemData,
DivaPvCustomizeData,
DivaItemData,
DivaStaticData,
)
class DivaData(Data):

View File

@ -14,85 +14,112 @@ from titles.diva.config import DivaConfig
from titles.diva.const import DivaConstants
from titles.diva.base import DivaBase
class DivaServlet():
class DivaServlet:
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
self.core_cfg = core_cfg
self.game_cfg = DivaConfig()
if path.exists(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"):
self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}")))
self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"))
)
self.base = DivaBase(core_cfg, self.game_cfg)
self.logger = logging.getLogger("diva")
log_fmt_str = "[%(asctime)s] Diva | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "diva"), encoding='utf8',
when="d", backupCount=10)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.core_cfg.server.log_dir, "diva"),
encoding="utf8",
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler)
self.logger.setLevel(self.game_cfg.server.loglevel)
coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str)
coloredlogs.install(
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
)
@classmethod
def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]:
def get_allnet_info(
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
) -> Tuple[bool, str, str]:
game_cfg = DivaConfig()
if path.exists(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"):
game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}")))
game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"))
)
if not game_cfg.server.enable:
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,
f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/",
"",
)
return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", "")
def render_POST(self, req: Request, version: int, url_path: str) -> bytes:
req_raw = req.content.getvalue()
url_header = req.getAllHeaders()
#Ping Dispatch
if "THIS_STRING_SEPARATES"in str(url_header):
# Ping Dispatch
if "THIS_STRING_SEPARATES" in str(url_header):
binary_request = req_raw.splitlines()
binary_cmd_decoded = binary_request[3].decode("utf-8")
binary_array = binary_cmd_decoded.split('&')
binary_array = binary_cmd_decoded.split("&")
bin_req_data = {}
for kvp in binary_array:
split_bin = kvp.split("=")
bin_req_data[split_bin[0]] = split_bin[1]
self.logger.info(f"Binary {bin_req_data['cmd']} Request")
self.logger.debug(bin_req_data)
handler = getattr(self.base, f"handle_{bin_req_data['cmd']}_request")
resp = handler(bin_req_data)
self.logger.debug(f"Response cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}")
return f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}".encode('utf-8')
self.logger.debug(
f"Response cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}"
)
return f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}".encode(
"utf-8"
)
# Main Dispatch
json_string = json.dumps(
req_raw.decode("utf-8")
) # Take the response and decode as UTF-8 and dump
b64string = json_string.replace(
r"\n", "\n"
) # Remove all \n and separate them as new lines
gz_string = base64.b64decode(b64string) # Decompressing the base64 string
#Main Dispatch
json_string = json.dumps(req_raw.decode("utf-8")) #Take the response and decode as UTF-8 and dump
b64string = json_string.replace(r'\n', '\n') # Remove all \n and separate them as new lines
gz_string = base64.b64decode(b64string) # Decompressing the base64 string
try:
url_data = zlib.decompress( gz_string ).decode("utf-8") # Decompressing the gzip
url_data = zlib.decompress(gz_string).decode(
"utf-8"
) # Decompressing the gzip
except zlib.error as e:
self.logger.error(f"Failed to defalte! {e} -> {gz_string}")
return "stat=0"
req_kvp = urllib.parse.unquote(url_data)
req_data = {}
# We then need to split each parts with & so we can reuse them to fill out the requests
splitted_request = str.split(req_kvp, "&")
for kvp in splitted_request:
@ -109,15 +136,25 @@ class DivaServlet():
handler = getattr(self.base, func_to_find)
resp = handler(req_data)
except AttributeError as e:
except AttributeError as e:
self.logger.warning(f"Unhandled {req_data['cmd']} request {e}")
return f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok".encode('utf-8')
return f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok".encode(
"utf-8"
)
except Exception as e:
self.logger.error(f"Error handling method {func_to_find} {e}")
return f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok".encode('utf-8')
return f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok".encode(
"utf-8"
)
req.responseHeaders.addRawHeader(b"content-type", b"text/plain")
self.logger.debug(f"Response cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok{resp}")
self.logger.debug(
f"Response cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok{resp}"
)
return f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok{resp}".encode('utf-8')
return (
f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok{resp}".encode(
"utf-8"
)
)

View File

@ -7,13 +7,23 @@ from core.config import CoreConfig
from titles.diva.database import DivaData
from titles.diva.const import DivaConstants
class DivaReader(BaseReader):
def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None:
def __init__(
self,
config: CoreConfig,
version: int,
bin_dir: Optional[str],
opt_dir: Optional[str],
extra: Optional[str],
) -> None:
super().__init__(config, version, bin_dir, opt_dir, extra)
self.data = DivaData(config)
try:
self.logger.info(f"Start importer for {DivaConstants.game_ver_to_string(version)}")
self.logger.info(
f"Start importer for {DivaConstants.game_ver_to_string(version)}"
)
except IndexError:
self.logger.error(f"Invalid project diva version {version}")
exit(1)
@ -30,7 +40,7 @@ class DivaReader(BaseReader):
if not path.exists(f"{self.bin_dir}/rom"):
self.logger.warn(f"Couldn't find rom folder in {self.bin_dir}, skipping")
pull_bin_rom = False
if self.opt_dir is not None:
opt_dirs = self.get_data_directories(self.opt_dir)
else:
@ -44,18 +54,25 @@ class DivaReader(BaseReader):
if pull_opt_rom:
for dir in opt_dirs:
self.read_rom(f"{dir}/rom")
def read_ram(self, ram_root_dir: str) -> None:
self.logger.info(f"Read RAM from {ram_root_dir}")
if path.exists(f"{ram_root_dir}/databank"):
for root, dirs, files in walk(f"{ram_root_dir}/databank"):
for file in files:
if file.startswith("ShopCatalog_") or file.startswith("CustomizeItemCatalog_") or \
(file.startswith("QuestInfo") and not file.startswith("QuestInfoTm")):
if (
file.startswith("ShopCatalog_")
or file.startswith("CustomizeItemCatalog_")
or (
file.startswith("QuestInfo")
and not file.startswith("QuestInfoTm")
)
):
with open(f"{root}/{file}", "r") as f:
file_data: str = urllib.parse.unquote(urllib.parse.unquote(f.read()))
file_data: str = urllib.parse.unquote(
urllib.parse.unquote(f.read())
)
if file_data == "***":
self.logger.info(f"{file} is empty, skipping")
continue
@ -70,23 +87,54 @@ class DivaReader(BaseReader):
if file.startswith("ShopCatalog_"):
for x in range(0, len(split), 7):
self.logger.info(f"Added shop item {split[x+0]}")
self.logger.info(
f"Added shop item {split[x+0]}"
)
self.data.static.put_shop(self.version, split[x+0], split[x+2], split[x+6], split[x+3],
split[x+1], split[x+4], split[x+5])
self.data.static.put_shop(
self.version,
split[x + 0],
split[x + 2],
split[x + 6],
split[x + 3],
split[x + 1],
split[x + 4],
split[x + 5],
)
elif file.startswith("CustomizeItemCatalog_") and len(split) >= 7:
elif (
file.startswith("CustomizeItemCatalog_")
and len(split) >= 7
):
for x in range(0, len(split), 7):
self.logger.info(f"Added item {split[x+0]}")
self.data.static.put_items(self.version, split[x+0], split[x+2], split[x+6], split[x+3],
split[x+1], split[x+4], split[x+5])
self.data.static.put_items(
self.version,
split[x + 0],
split[x + 2],
split[x + 6],
split[x + 3],
split[x + 1],
split[x + 4],
split[x + 5],
)
elif file.startswith("QuestInfo") and len(split) >= 9:
self.logger.info(f"Added quest {split[0]}")
self.data.static.put_quests(self.version, split[0], split[6], split[2], split[3],
split[7], split[8], split[1], split[4], split[5])
self.data.static.put_quests(
self.version,
split[0],
split[6],
split[2],
split[3],
split[7],
split[8],
split[1],
split[4],
split[5],
)
else:
continue
@ -102,13 +150,13 @@ class DivaReader(BaseReader):
elif path.exists(f"{rom_root_dir}/pv_db.txt"):
file_path = f"{rom_root_dir}/pv_db.txt"
else:
self.logger.warn(f"Cannot find pv_db.txt or mdata_pv_db.txt in {rom_root_dir}, skipping")
self.logger.warn(
f"Cannot find pv_db.txt or mdata_pv_db.txt in {rom_root_dir}, skipping"
)
return
with open(file_path, "r", encoding="utf-8") as f:
for line in f.readlines():
if line.startswith("#") or not line:
continue
@ -127,14 +175,13 @@ class DivaReader(BaseReader):
for x in range(1, len(key_split)):
key_args.append(key_split[x])
try:
pv_list[pv_id] = self.add_branch(pv_list[pv_id], key_args, val)
except KeyError:
pv_list[pv_id] = {}
pv_list[pv_id] = self.add_branch(pv_list[pv_id], key_args, val)
for pv_id, pv_data in pv_list.items():
song_id = int(pv_id.split("_")[1])
if "songinfo" not in pv_data:
@ -148,46 +195,99 @@ class DivaReader(BaseReader):
if "music" not in pv_data["songinfo"]:
pv_data["songinfo"]["music"] = "-"
if "easy" in pv_data['difficulty'] and '0' in pv_data['difficulty']['easy']:
diff = pv_data['difficulty']['easy']['0']['level'].split('_')
if "easy" in pv_data["difficulty"] and "0" in pv_data["difficulty"]["easy"]:
diff = pv_data["difficulty"]["easy"]["0"]["level"].split("_")
self.logger.info(f"Added song {song_id} chart 0")
self.data.static.put_music(self.version, song_id, 0, pv_data["song_name"], pv_data["songinfo"]["arranger"],
pv_data["songinfo"]["illustrator"], pv_data["songinfo"]["lyrics"], pv_data["songinfo"]["music"],
float(f"{diff[2]}.{diff[3]}"), pv_data["bpm"], pv_data["date"])
if "normal" in pv_data['difficulty'] and '0' in pv_data['difficulty']['normal']:
diff = pv_data['difficulty']['normal']['0']['level'].split('_')
self.data.static.put_music(
self.version,
song_id,
0,
pv_data["song_name"],
pv_data["songinfo"]["arranger"],
pv_data["songinfo"]["illustrator"],
pv_data["songinfo"]["lyrics"],
pv_data["songinfo"]["music"],
float(f"{diff[2]}.{diff[3]}"),
pv_data["bpm"],
pv_data["date"],
)
if (
"normal" in pv_data["difficulty"]
and "0" in pv_data["difficulty"]["normal"]
):
diff = pv_data["difficulty"]["normal"]["0"]["level"].split("_")
self.logger.info(f"Added song {song_id} chart 1")
self.data.static.put_music(self.version, song_id, 1, pv_data["song_name"], pv_data["songinfo"]["arranger"],
pv_data["songinfo"]["illustrator"], pv_data["songinfo"]["lyrics"], pv_data["songinfo"]["music"],
float(f"{diff[2]}.{diff[3]}"), pv_data["bpm"], pv_data["date"])
if "hard" in pv_data['difficulty'] and '0' in pv_data['difficulty']['hard']:
diff = pv_data['difficulty']['hard']['0']['level'].split('_')
self.data.static.put_music(
self.version,
song_id,
1,
pv_data["song_name"],
pv_data["songinfo"]["arranger"],
pv_data["songinfo"]["illustrator"],
pv_data["songinfo"]["lyrics"],
pv_data["songinfo"]["music"],
float(f"{diff[2]}.{diff[3]}"),
pv_data["bpm"],
pv_data["date"],
)
if "hard" in pv_data["difficulty"] and "0" in pv_data["difficulty"]["hard"]:
diff = pv_data["difficulty"]["hard"]["0"]["level"].split("_")
self.logger.info(f"Added song {song_id} chart 2")
self.data.static.put_music(self.version, song_id, 2, pv_data["song_name"], pv_data["songinfo"]["arranger"],
pv_data["songinfo"]["illustrator"], pv_data["songinfo"]["lyrics"], pv_data["songinfo"]["music"],
float(f"{diff[2]}.{diff[3]}"), pv_data["bpm"], pv_data["date"])
if "extreme" in pv_data['difficulty']:
if "0" in pv_data['difficulty']['extreme']:
diff = pv_data['difficulty']['extreme']['0']['level'].split('_')
self.data.static.put_music(
self.version,
song_id,
2,
pv_data["song_name"],
pv_data["songinfo"]["arranger"],
pv_data["songinfo"]["illustrator"],
pv_data["songinfo"]["lyrics"],
pv_data["songinfo"]["music"],
float(f"{diff[2]}.{diff[3]}"),
pv_data["bpm"],
pv_data["date"],
)
if "extreme" in pv_data["difficulty"]:
if "0" in pv_data["difficulty"]["extreme"]:
diff = pv_data["difficulty"]["extreme"]["0"]["level"].split("_")
self.logger.info(f"Added song {song_id} chart 3")
self.data.static.put_music(self.version, song_id, 3, pv_data["song_name"], pv_data["songinfo"]["arranger"],
pv_data["songinfo"]["illustrator"], pv_data["songinfo"]["lyrics"], pv_data["songinfo"]["music"],
float(f"{diff[2]}.{diff[3]}"), pv_data["bpm"], pv_data["date"])
self.data.static.put_music(
self.version,
song_id,
3,
pv_data["song_name"],
pv_data["songinfo"]["arranger"],
pv_data["songinfo"]["illustrator"],
pv_data["songinfo"]["lyrics"],
pv_data["songinfo"]["music"],
float(f"{diff[2]}.{diff[3]}"),
pv_data["bpm"],
pv_data["date"],
)
if "1" in pv_data['difficulty']['extreme']:
diff = pv_data['difficulty']['extreme']['1']['level'].split('_')
if "1" in pv_data["difficulty"]["extreme"]:
diff = pv_data["difficulty"]["extreme"]["1"]["level"].split("_")
self.logger.info(f"Added song {song_id} chart 4")
self.data.static.put_music(self.version, song_id, 4, pv_data["song_name"], pv_data["songinfo"]["arranger"],
pv_data["songinfo"]["illustrator"], pv_data["songinfo"]["lyrics"], pv_data["songinfo"]["music"],
float(f"{diff[2]}.{diff[3]}"), pv_data["bpm"], pv_data["date"])
self.data.static.put_music(
self.version,
song_id,
4,
pv_data["song_name"],
pv_data["songinfo"]["arranger"],
pv_data["songinfo"]["illustrator"],
pv_data["songinfo"]["lyrics"],
pv_data["songinfo"]["music"],
float(f"{diff[2]}.{diff[3]}"),
pv_data["bpm"],
pv_data["date"],
)
def add_branch(self, tree: Dict, vector: List, value: str):
"""
@ -195,9 +295,9 @@ class DivaReader(BaseReader):
Author: iJames on StackOverflow
"""
key = vector[0]
tree[key] = value \
if len(vector) == 1 \
else self.add_branch(tree[key] if key in tree else {},
vector[1:],
value)
return tree
tree[key] = (
value
if len(vector) == 1
else self.add_branch(tree.get(key, {}), vector[1:], value)
)
return tree

View File

@ -6,6 +6,12 @@ from titles.diva.schema.pv_customize import DivaPvCustomizeData
from titles.diva.schema.item import DivaItemData
from titles.diva.schema.static import DivaStaticData
__all__ = [DivaProfileData, DivaScoreData, DivaModuleData,
DivaCustomizeItemData, DivaPvCustomizeData, DivaItemData,
DivaStaticData]
__all__ = [
DivaProfileData,
DivaScoreData,
DivaModuleData,
DivaCustomizeItemData,
DivaPvCustomizeData,
DivaItemData,
DivaStaticData,
]

View File

@ -10,25 +10,29 @@ customize = Table(
"diva_profile_customize_item",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("item_id", Integer, nullable=False),
UniqueConstraint("user", "version", "item_id", name="diva_profile_customize_item_uk"),
mysql_charset='utf8mb4'
UniqueConstraint(
"user", "version", "item_id", name="diva_profile_customize_item_uk"
),
mysql_charset="utf8mb4",
)
class DivaCustomizeItemData(BaseData):
def put_customize_item(self, aime_id: int, version: int, item_id: int) -> None:
sql = insert(customize).values(
version=version,
user=aime_id,
item_id=item_id
)
sql = insert(customize).values(version=version, user=aime_id, item_id=item_id)
result = self.execute(sql)
if result is None:
self.logger.error(f"{__name__} Failed to insert diva profile customize item! aime id: {aime_id} item: {item_id}")
self.logger.error(
f"{__name__} Failed to insert diva profile customize item! aime id: {aime_id} item: {item_id}"
)
return None
return result.lastrowid
@ -36,10 +40,9 @@ class DivaCustomizeItemData(BaseData):
"""
Given a game version and an aime id, return all the customize items, not used directly
"""
sql = customize.select(and_(
customize.c.version == version,
customize.c.user == aime_id
))
sql = customize.select(
and_(customize.c.version == version, customize.c.user == aime_id)
)
result = self.execute(sql)
if result is None:

View File

@ -11,37 +11,48 @@ shop = Table(
"diva_profile_shop",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("mdl_eqp_ary", String(32)),
Column("c_itm_eqp_ary", String(59)),
Column("ms_itm_flg_ary", String(59)),
UniqueConstraint("user", "version", name="diva_profile_shop_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class DivaItemData(BaseData):
def put_shop(self, aime_id: int, version: int, mdl_eqp_ary: str,
c_itm_eqp_ary: str, ms_itm_flg_ary: str) -> None:
def put_shop(
self,
aime_id: int,
version: int,
mdl_eqp_ary: str,
c_itm_eqp_ary: str,
ms_itm_flg_ary: str,
) -> None:
sql = insert(shop).values(
version=version,
user=aime_id,
mdl_eqp_ary=mdl_eqp_ary,
c_itm_eqp_ary=c_itm_eqp_ary,
ms_itm_flg_ary=ms_itm_flg_ary
ms_itm_flg_ary=ms_itm_flg_ary,
)
conflict = sql.on_duplicate_key_update(
mdl_eqp_ary=mdl_eqp_ary,
c_itm_eqp_ary=c_itm_eqp_ary,
ms_itm_flg_ary=ms_itm_flg_ary
ms_itm_flg_ary=ms_itm_flg_ary,
)
result = self.execute(conflict)
if result is None:
self.logger.error(f"{__name__} Failed to insert diva profile! aime id: {aime_id} array: {mdl_eqp_ary}")
self.logger.error(
f"{__name__} Failed to insert diva profile! aime id: {aime_id} array: {mdl_eqp_ary}"
)
return None
return result.lastrowid
@ -49,10 +60,7 @@ class DivaItemData(BaseData):
"""
Given a game version and either a profile or aime id, return the profile
"""
sql = shop.select(and_(
shop.c.version == version,
shop.c.user == aime_id
))
sql = shop.select(and_(shop.c.version == version, shop.c.user == aime_id))
result = self.execute(sql)
if result is None:

View File

@ -10,25 +10,27 @@ module = Table(
"diva_profile_module",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("module_id", Integer, nullable=False),
UniqueConstraint("user", "version", "module_id", name="diva_profile_module_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class DivaModuleData(BaseData):
def put_module(self, aime_id: int, version: int, module_id: int) -> None:
sql = insert(module).values(
version=version,
user=aime_id,
module_id=module_id
)
sql = insert(module).values(version=version, user=aime_id, module_id=module_id)
result = self.execute(sql)
if result is None:
self.logger.error(f"{__name__} Failed to insert diva profile module! aime id: {aime_id} module: {module_id}")
self.logger.error(
f"{__name__} Failed to insert diva profile module! aime id: {aime_id} module: {module_id}"
)
return None
return result.lastrowid
@ -36,10 +38,7 @@ class DivaModuleData(BaseData):
"""
Given a game version and an aime id, return all the modules, not used directly
"""
sql = module.select(and_(
module.c.version == version,
module.c.user == aime_id
))
sql = module.select(and_(module.c.version == version, module.c.user == aime_id))
result = self.execute(sql)
if result is None:

View File

@ -11,8 +11,11 @@ profile = Table(
"diva_profile",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade",
onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("player_name", String(10), nullable=False),
Column("lv_str", String(24), nullable=False, server_default="Dab on 'em"),
@ -29,10 +32,8 @@ profile = Table(
Column("use_pv_skn_eqp", Boolean, nullable=False, server_default="0"),
Column("use_pv_btn_se_eqp", Boolean, nullable=False, server_default="1"),
Column("use_pv_sld_se_eqp", Boolean, nullable=False, server_default="0"),
Column("use_pv_chn_sld_se_eqp", Boolean,
nullable=False, server_default="0"),
Column("use_pv_sldr_tch_se_eqp", Boolean,
nullable=False, server_default="0"),
Column("use_pv_chn_sld_se_eqp", Boolean, nullable=False, server_default="0"),
Column("use_pv_sldr_tch_se_eqp", Boolean, nullable=False, server_default="0"),
Column("nxt_pv_id", Integer, nullable=False, server_default="708"),
Column("nxt_dffclty", Integer, nullable=False, server_default="2"),
Column("nxt_edtn", Integer, nullable=False, server_default="0"),
@ -44,35 +45,39 @@ profile = Table(
Column("lv_plt_id", Integer, nullable=False, server_default="1"),
Column("passwd_stat", Integer, nullable=False, server_default="0"),
Column("passwd", String(12), nullable=False, server_default="**********"),
Column("my_qst_id", String(
128), server_default="-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1"),
Column("my_qst_sts", String(
128), server_default="-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1"),
Column(
"my_qst_id",
String(128),
server_default="-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1",
),
Column(
"my_qst_sts",
String(128),
server_default="-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1",
),
UniqueConstraint("user", "version", name="diva_profile_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class DivaProfileData(BaseData):
def create_profile(self, version: int, aime_id: int,
player_name: str) -> Optional[int]:
def create_profile(
self, version: int, aime_id: int, player_name: str
) -> Optional[int]:
"""
Given a game version, aime id, and player_name, create a profile and return it's ID
"""
sql = insert(profile).values(
version=version,
user=aime_id,
player_name=player_name
version=version, user=aime_id, player_name=player_name
)
conflict = sql.on_duplicate_key_update(
player_name=sql.inserted.player_name
)
conflict = sql.on_duplicate_key_update(player_name=sql.inserted.player_name)
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} Failed to insert diva profile! aime id: {aime_id} username: {player_name}")
f"{__name__} Failed to insert diva profile! aime id: {aime_id} username: {player_name}"
)
return None
return result.lastrowid
@ -86,17 +91,17 @@ class DivaProfileData(BaseData):
result = self.execute(sql)
if result is None:
self.logger.error(
f"update_profile: failed to update profile! profile: {aime_id}")
f"update_profile: failed to update profile! profile: {aime_id}"
)
return None
def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]:
"""
Given a game version and either a profile or aime id, return the profile
"""
sql = profile.select(and_(
profile.c.version == version,
profile.c.user == aime_id
))
sql = profile.select(
and_(profile.c.version == version, profile.c.user == aime_id)
)
result = self.execute(sql)
if result is None:

View File

@ -10,27 +10,44 @@ pv_customize = Table(
"diva_profile_pv_customize",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("pv_id", Integer, nullable=False),
Column("mdl_eqp_ary", String(14), server_default="-999,-999,-999"),
Column("c_itm_eqp_ary", String(59), server_default="-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999"),
Column("ms_itm_flg_ary", String(59), server_default="-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1"),
Column(
"c_itm_eqp_ary",
String(59),
server_default="-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999",
),
Column(
"ms_itm_flg_ary",
String(59),
server_default="-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1",
),
Column("skin", Integer, server_default="-1"),
Column("btn_se", Integer, server_default="-1"),
Column("sld_se", Integer, server_default="-1"),
Column("chsld_se", Integer, server_default="-1"),
Column("sldtch_se", Integer, server_default="-1"),
UniqueConstraint("user", "version", "pv_id", name="diva_profile_pv_customize_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class DivaPvCustomizeData(BaseData):
def put_pv_customize(self, aime_id: int, version: int, pv_id: int,
mdl_eqp_ary: str, c_itm_eqp_ary: str,
ms_itm_flg_ary: str) -> Optional[int]:
def put_pv_customize(
self,
aime_id: int,
version: int,
pv_id: int,
mdl_eqp_ary: str,
c_itm_eqp_ary: str,
ms_itm_flg_ary: str,
) -> Optional[int]:
sql = insert(pv_customize).values(
version=version,
user=aime_id,
@ -49,19 +66,19 @@ class DivaPvCustomizeData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.error(f"{__name__} Failed to insert diva pv customize! aime id: {aime_id}")
self.logger.error(
f"{__name__} Failed to insert diva pv customize! aime id: {aime_id}"
)
return None
return result.lastrowid
def get_pv_customize(self, aime_id: int,
pv_id: int) -> Optional[List[Dict]]:
def get_pv_customize(self, aime_id: int, pv_id: int) -> Optional[List[Dict]]:
"""
Given either a profile or aime id, return a Pv Customize row
"""
sql = pv_customize.select(and_(
pv_customize.c.user == aime_id,
pv_customize.c.pv_id == pv_id
))
sql = pv_customize.select(
and_(pv_customize.c.user == aime_id, pv_customize.c.pv_id == pv_id)
)
result = self.execute(sql)
if result is None:

View File

@ -28,7 +28,7 @@ score = Table(
Column("worst", Integer),
Column("max_combo", Integer),
UniqueConstraint("user", "pv_id", "difficulty", "edition", name="diva_score_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
playlog = Table(
@ -51,16 +51,29 @@ playlog = Table(
Column("worst", Integer),
Column("max_combo", Integer),
Column("date_scored", TIMESTAMP, server_default=func.now()),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class DivaScoreData(BaseData):
def put_best_score(self, user_id: int, game_version: int, song_id: int,
difficulty: int, edition: int, song_score: int,
atn_pnt: int, clr_kind: int, sort_kind: int,
cool: int, fine: int, safe: int, sad: int,
worst: int, max_combo: int) -> Optional[int]:
def put_best_score(
self,
user_id: int,
game_version: int,
song_id: int,
difficulty: int,
edition: int,
song_score: int,
atn_pnt: int,
clr_kind: int,
sort_kind: int,
cool: int,
fine: int,
safe: int,
sad: int,
worst: int,
max_combo: int,
) -> Optional[int]:
"""
Update the user's best score for a chart
"""
@ -98,16 +111,30 @@ class DivaScoreData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.error(
f"{__name__} failed to insert best score! profile: {user_id}, song: {song_id}")
f"{__name__} failed to insert best score! profile: {user_id}, song: {song_id}"
)
return None
return result.lastrowid
def put_playlog(self, user_id: int, game_version: int, song_id: int,
difficulty: int, edition: int, song_score: int,
atn_pnt: int, clr_kind: int, sort_kind: int,
cool: int, fine: int, safe: int, sad: int,
worst: int, max_combo: int) -> Optional[int]:
def put_playlog(
self,
user_id: int,
game_version: int,
song_id: int,
difficulty: int,
edition: int,
song_score: int,
atn_pnt: int,
clr_kind: int,
sort_kind: int,
cool: int,
fine: int,
safe: int,
sad: int,
worst: int,
max_combo: int,
) -> Optional[int]:
"""
Add an entry to the user's play log
"""
@ -126,24 +153,28 @@ class DivaScoreData(BaseData):
safe=safe,
sad=sad,
worst=worst,
max_combo=max_combo
max_combo=max_combo,
)
result = self.execute(sql)
if result is None:
self.logger.error(
f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_id}, chart: {difficulty}")
f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_id}, chart: {difficulty}"
)
return None
return result.lastrowid
def get_best_user_score(self, user_id: int, pv_id: int, difficulty: int,
edition: int) -> Optional[Dict]:
def get_best_user_score(
self, user_id: int, pv_id: int, difficulty: int, edition: int
) -> Optional[Dict]:
sql = score.select(
and_(score.c.user == user_id,
score.c.pv_id == pv_id,
score.c.difficulty == difficulty,
score.c.edition == edition)
and_(
score.c.user == user_id,
score.c.pv_id == pv_id,
score.c.difficulty == difficulty,
score.c.edition == edition,
)
)
result = self.execute(sql)
@ -151,36 +182,48 @@ class DivaScoreData(BaseData):
return None
return result.fetchone()
def get_top3_scores(self, pv_id: int, difficulty: int,
edition: int) -> Optional[List[Dict]]:
sql = score.select(
and_(score.c.pv_id == pv_id,
score.c.difficulty == difficulty,
score.c.edition == edition)
).order_by(score.c.score.desc()).limit(3)
def get_top3_scores(
self, pv_id: int, difficulty: int, edition: int
) -> Optional[List[Dict]]:
sql = (
score.select(
and_(
score.c.pv_id == pv_id,
score.c.difficulty == difficulty,
score.c.edition == edition,
)
)
.order_by(score.c.score.desc())
.limit(3)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_global_ranking(self, user_id: int, pv_id: int, difficulty: int,
edition: int) -> Optional[List]:
def get_global_ranking(
self, user_id: int, pv_id: int, difficulty: int, edition: int
) -> Optional[List]:
# get the subquery max score of a user with pv_id, difficulty and
# edition
sql_sub = select([score.c.score]).filter(
score.c.user == user_id,
score.c.pv_id == pv_id,
score.c.difficulty == difficulty,
score.c.edition == edition
).scalar_subquery()
sql_sub = (
select([score.c.score])
.filter(
score.c.user == user_id,
score.c.pv_id == pv_id,
score.c.difficulty == difficulty,
score.c.edition == edition,
)
.scalar_subquery()
)
# Perform the main query, also rename the resulting column to ranking
sql = select(func.count(score.c.id).label("ranking")).filter(
score.c.score >= sql_sub,
score.c.pv_id == pv_id,
score.c.difficulty == difficulty,
score.c.edition == edition
score.c.edition == edition,
)
result = self.execute(sql)

View File

@ -25,7 +25,7 @@ music = Table(
Column("bpm", Integer),
Column("date", String(255)),
UniqueConstraint("version", "songId", "chartId", name="diva_static_music_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
quests = Table(
@ -43,9 +43,8 @@ quests = Table(
Column("quest_order", Integer),
Column("start_datetime", String(255)),
Column("end_datetime", String(255)),
UniqueConstraint("version", "questId", name="diva_static_quests_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
shop = Table(
@ -62,7 +61,7 @@ shop = Table(
Column("end_date", String(255)),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "shopId", name="diva_static_shop_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
items = Table(
@ -79,64 +78,91 @@ items = Table(
Column("end_date", String(255)),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "itemId", name="diva_static_items_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class DivaStaticData(BaseData):
def put_quests(self, version: int, questId: int, name: str, kind: int, unknown_0: int, unknown_1: int, unknown_2: int, quest_order: int, start_datetime: str, end_datetime: str) -> Optional[int]:
def put_quests(
self,
version: int,
questId: int,
name: str,
kind: int,
unknown_0: int,
unknown_1: int,
unknown_2: int,
quest_order: int,
start_datetime: str,
end_datetime: str,
) -> Optional[int]:
sql = insert(quests).values(
version = version,
questId = questId,
name = name,
kind = kind,
unknown_0 = unknown_0,
unknown_1 = unknown_1,
unknown_2 = unknown_2,
quest_order = quest_order,
start_datetime = start_datetime,
end_datetime = end_datetime
version=version,
questId=questId,
name=name,
kind=kind,
unknown_0=unknown_0,
unknown_1=unknown_1,
unknown_2=unknown_2,
quest_order=quest_order,
start_datetime=start_datetime,
end_datetime=end_datetime,
)
conflict = sql.on_duplicate_key_update(
name = name
)
conflict = sql.on_duplicate_key_update(name=name)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_enabled_quests(self, version: int) -> Optional[List[Row]]:
sql = select(quests).where(and_(quests.c.version == version, quests.c.quest_enable == True))
sql = select(quests).where(
and_(quests.c.version == version, quests.c.quest_enable == True)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def put_shop(self, version: int, shopId: int, name: str, type: int, points: int, unknown_0: int, start_date: str, end_date: str) -> Optional[int]:
def put_shop(
self,
version: int,
shopId: int,
name: str,
type: int,
points: int,
unknown_0: int,
start_date: str,
end_date: str,
) -> Optional[int]:
sql = insert(shop).values(
version = version,
shopId = shopId,
name = name,
type = type,
points = points,
unknown_0 = unknown_0,
start_date = start_date,
end_date = end_date
version=version,
shopId=shopId,
name=name,
type=type,
points=points,
unknown_0=unknown_0,
start_date=start_date,
end_date=end_date,
)
conflict = sql.on_duplicate_key_update(
name = name
)
conflict = sql.on_duplicate_key_update(name=name)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_enabled_shop(self, version: int, shopId: int) -> Optional[Row]:
sql = select(shop).where(and_(
shop.c.version == version,
shop.c.shopId == shopId,
shop.c.enabled == True))
sql = select(shop).where(
and_(
shop.c.version == version,
shop.c.shopId == shopId,
shop.c.enabled == True,
)
)
result = self.execute(sql)
if result is None:
@ -144,40 +170,52 @@ class DivaStaticData(BaseData):
return result.fetchone()
def get_enabled_shops(self, version: int) -> Optional[List[Row]]:
sql = select(shop).where(and_(
shop.c.version == version,
shop.c.enabled == True))
sql = select(shop).where(
and_(shop.c.version == version, shop.c.enabled == True)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_items(self, version: int, itemId: int, name: str, type: int, points: int, unknown_0: int, start_date: str, end_date: str) -> Optional[int]:
def put_items(
self,
version: int,
itemId: int,
name: str,
type: int,
points: int,
unknown_0: int,
start_date: str,
end_date: str,
) -> Optional[int]:
sql = insert(items).values(
version = version,
itemId = itemId,
name = name,
type = type,
points = points,
unknown_0 = unknown_0,
start_date = start_date,
end_date = end_date
version=version,
itemId=itemId,
name=name,
type=type,
points=points,
unknown_0=unknown_0,
start_date=start_date,
end_date=end_date,
)
conflict = sql.on_duplicate_key_update(
name = name
)
conflict = sql.on_duplicate_key_update(name=name)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_enabled_item(self, version: int, itemId: int) -> Optional[Row]:
sql = select(items).where(and_(
items.c.version == version,
items.c.itemId == itemId,
items.c.enabled == True))
sql = select(items).where(
and_(
items.c.version == version,
items.c.itemId == itemId,
items.c.enabled == True,
)
)
result = self.execute(sql)
if result is None:
@ -185,66 +223,89 @@ class DivaStaticData(BaseData):
return result.fetchone()
def get_enabled_items(self, version: int) -> Optional[List[Row]]:
sql = select(items).where(and_(
items.c.version == version,
items.c.enabled == True))
sql = select(items).where(
and_(items.c.version == version, items.c.enabled == True)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_music(self, version: int, song: int, chart: int, title: str, arranger: str, illustrator: str,
lyrics: str, music_comp: str, level: float, bpm: int, date: str) -> Optional[int]:
def put_music(
self,
version: int,
song: int,
chart: int,
title: str,
arranger: str,
illustrator: str,
lyrics: str,
music_comp: str,
level: float,
bpm: int,
date: str,
) -> Optional[int]:
sql = insert(music).values(
version = version,
songId = song,
chartId = chart,
title = title,
vocaloid_arranger = arranger,
pv_illustrator = illustrator,
lyrics = lyrics,
bg_music = music_comp,
level = level,
bpm = bpm,
date = date
version=version,
songId=song,
chartId=chart,
title=title,
vocaloid_arranger=arranger,
pv_illustrator=illustrator,
lyrics=lyrics,
bg_music=music_comp,
level=level,
bpm=bpm,
date=date,
)
conflict = sql.on_duplicate_key_update(
title = title,
vocaloid_arranger = arranger,
pv_illustrator = illustrator,
lyrics = lyrics,
bg_music = music_comp,
level = level,
bpm = bpm,
date = date
title=title,
vocaloid_arranger=arranger,
pv_illustrator=illustrator,
lyrics=lyrics,
bg_music=music_comp,
level=level,
bpm=bpm,
date=date,
)
result = self.execute(conflict)
if result is None: return None
if result is None:
return None
return result.lastrowid
def get_music(self, version: int, song_id: Optional[int] = None) -> Optional[List[Row]]:
def get_music(
self, version: int, song_id: Optional[int] = None
) -> Optional[List[Row]]:
if song_id is None:
sql = select(music).where(music.c.version == version)
else:
sql = select(music).where(and_(
music.c.version == version,
music.c.songId == song_id,
))
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def get_music_chart(self, version: int, song_id: int, chart_id: int) -> Optional[List[Row]]:
sql = select(music).where(and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id
))
def get_music_chart(
self, version: int, song_id: int, chart_id: int
) -> Optional[List[Row]]:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id,
)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()

View File

@ -7,4 +7,4 @@ index = Mai2Servlet
database = Mai2Data
reader = Mai2Reader
game_codes = [Mai2Constants.GAME_CODE]
current_schema_version = 2
current_schema_version = 2

View File

@ -7,7 +7,8 @@ from titles.mai2.const import Mai2Constants
from titles.mai2.config import Mai2Config
from titles.mai2.database import Mai2Data
class Mai2Base():
class Mai2Base:
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
self.core_config = cfg
self.game_config = game_cfg
@ -16,27 +17,28 @@ class Mai2Base():
self.data = Mai2Data(cfg)
self.logger = logging.getLogger("mai2")
def handle_ping_request(self, data: Dict) -> Dict:
return {"returnCode": 1}
def handle_get_game_setting_api_request(self, data: Dict):
reboot_start = date.strftime(datetime.now() + timedelta(hours=3), Mai2Constants.DATE_TIME_FORMAT)
reboot_end = date.strftime(datetime.now() + timedelta(hours=4), Mai2Constants.DATE_TIME_FORMAT)
reboot_start = date.strftime(
datetime.now() + timedelta(hours=3), Mai2Constants.DATE_TIME_FORMAT
)
reboot_end = date.strftime(
datetime.now() + timedelta(hours=4), Mai2Constants.DATE_TIME_FORMAT
)
return {
"gameSetting": {
"isMaintenance": "false",
"requestInterval": 10,
"rebootStartTime": reboot_start,
"rebootEndTime": reboot_end,
"movieUploadLimit": 10000,
"movieStatus": 0,
"movieServerUri": "",
"deliverServerUri": "",
"oldServerUri": "",
"usbDlServerUri": "",
"rebootInterval": 0
"gameSetting": {
"isMaintenance": "false",
"requestInterval": 10,
"rebootStartTime": reboot_start,
"rebootEndTime": reboot_end,
"movieUploadLimit": 10000,
"movieStatus": 0,
"movieServerUri": "",
"deliverServerUri": "",
"oldServerUri": "",
"usbDlServerUri": "",
"rebootInterval": 0,
},
"isAouAccession": "true",
"isAouAccession": "true",
}
def handle_get_game_ranking_api_request(self, data: Dict) -> Dict:
@ -49,34 +51,44 @@ class Mai2Base():
def handle_get_game_event_api_request(self, data: Dict) -> Dict:
events = self.data.static.get_enabled_events(self.version)
events_lst = []
if events is None: return {"type": data["type"], "length": 0, "gameEventList": []}
if events is None:
return {"type": data["type"], "length": 0, "gameEventList": []}
for event in events:
events_lst.append({
"type": event["type"],
"id": event["eventId"],
"startDate": "2017-12-05 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0"
})
events_lst.append(
{
"type": event["type"],
"id": event["eventId"],
"startDate": "2017-12-05 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
}
)
return {"type": data["type"], "length": len(events_lst), "gameEventList": events_lst}
return {
"type": data["type"],
"length": len(events_lst),
"gameEventList": events_lst,
}
def handle_get_game_ng_music_id_api_request(self, data: Dict) -> Dict:
return {"length": 0, "musicIdList": []}
def handle_get_game_charge_api_request(self, data: Dict) -> Dict:
game_charge_list = self.data.static.get_enabled_tickets(self.version, 1)
if game_charge_list is None: return {"length": 0, "gameChargeList": []}
if game_charge_list is None:
return {"length": 0, "gameChargeList": []}
charge_list = []
for x in range(len(game_charge_list)):
charge_list.append({
"orderId": x,
"chargeId": game_charge_list[x]["ticketId"],
"price": game_charge_list[x]["price"],
"startDate": "2017-12-05 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0"
})
charge_list.append(
{
"orderId": x,
"chargeId": game_charge_list[x]["ticketId"],
"price": game_charge_list[x]["price"],
"startDate": "2017-12-05 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
}
)
return {"length": len(charge_list), "gameChargeList": charge_list}
@ -95,7 +107,8 @@ class Mai2Base():
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
p = self.data.profile.get_profile_detail(data["userId"], self.version)
o = self.data.profile.get_profile_option(data["userId"], self.version)
if p is None or o is None: return {} # Register
if p is None or o is None:
return {} # Register
profile = p._asdict()
option = o._asdict()
@ -109,20 +122,24 @@ class Mai2Base():
"lastLoginDate": profile["lastLoginDate"],
"lastPlayDate": profile["lastPlayDate"],
"playerRating": profile["playerRating"],
"nameplateId": 0, # Unused
"nameplateId": 0, # Unused
"iconId": profile["iconId"],
"trophyId": 0, # Unused
"trophyId": 0, # Unused
"partnerId": profile["partnerId"],
"frameId": profile["frameId"],
"dispRate": option["dispRate"], # 0: all/begin, 1: disprate, 2: dispDan, 3: hide, 4: end
"dispRate": option[
"dispRate"
], # 0: all/begin, 1: disprate, 2: dispDan, 3: hide, 4: end
"totalAwake": profile["totalAwake"],
"isNetMember": profile["isNetMember"],
"dailyBonusDate": profile["dailyBonusDate"],
"headPhoneVolume": option["headPhoneVolume"],
"isInherit": False, # Not sure what this is or does??
"banState": profile["banState"] if profile["banState"] is not None else 0 # New with uni+
"isInherit": False, # Not sure what this is or does??
"banState": profile["banState"]
if profile["banState"] is not None
else 0, # New with uni+
}
def handle_user_login_api_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile_detail(data["userId"], self.version)
@ -140,10 +157,10 @@ class Mai2Base():
"returnCode": 1,
"lastLoginDate": lastLoginDate,
"loginCount": loginCt,
"consecutiveLoginCount": 0, # We don't really have a way to track this...
"loginId": loginCt # Used with the playlog!
"consecutiveLoginCount": 0, # We don't really have a way to track this...
"loginId": loginCt, # Used with the playlog!
}
def handle_upload_user_playlog_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
playlog = data["userPlaylog"]
@ -157,50 +174,83 @@ class Mai2Base():
if "userData" in upsert and len(upsert["userData"]) > 0:
upsert["userData"][0]["isNetMember"] = 1
upsert["userData"][0].pop("accessCode")
self.data.profile.put_profile_detail(user_id, self.version, upsert["userData"][0])
self.data.profile.put_profile_detail(
user_id, self.version, upsert["userData"][0]
)
if "userExtend" in upsert and len(upsert["userExtend"]) > 0:
self.data.profile.put_profile_extend(user_id, self.version, upsert["userExtend"][0])
self.data.profile.put_profile_extend(
user_id, self.version, upsert["userExtend"][0]
)
if "userGhost" in upsert:
for ghost in upsert["userGhost"]:
self.data.profile.put_profile_extend(user_id, self.version, ghost)
if "userOption" in upsert and len(upsert["userOption"]) > 0:
self.data.profile.put_profile_option(user_id, self.version, upsert["userOption"][0])
self.data.profile.put_profile_option(
user_id, self.version, upsert["userOption"][0]
)
if "userRatingList" in upsert and len(upsert["userRatingList"]) > 0:
self.data.profile.put_profile_rating(user_id, self.version, upsert["userRatingList"][0])
self.data.profile.put_profile_rating(
user_id, self.version, upsert["userRatingList"][0]
)
if "userActivityList" in upsert and len(upsert["userActivityList"]) > 0:
for k,v in upsert["userActivityList"][0].items():
for k, v in upsert["userActivityList"][0].items():
for act in v:
self.data.profile.put_profile_activity(user_id, act)
if upsert["isNewCharacterList"] and int(upsert["isNewCharacterList"]) > 0:
for char in upsert["userCharacterList"]:
self.data.item.put_character(user_id, char["characterId"], char["level"], char["awakening"], char["useCount"])
self.data.item.put_character(
user_id,
char["characterId"],
char["level"],
char["awakening"],
char["useCount"],
)
if upsert["isNewItemList"] and int(upsert["isNewItemList"]) > 0:
for item in upsert["userItemList"]:
self.data.item.put_item(user_id, int(item["itemKind"]), item["itemId"], item["stock"], item["isValid"])
self.data.item.put_item(
user_id,
int(item["itemKind"]),
item["itemId"],
item["stock"],
item["isValid"],
)
if upsert["isNewLoginBonusList"] and int(upsert["isNewLoginBonusList"]) > 0:
for login_bonus in upsert["userLoginBonusList"]:
self.data.item.put_login_bonus(user_id, login_bonus["bonusId"], login_bonus["point"], login_bonus["isCurrent"], login_bonus["isComplete"])
self.data.item.put_login_bonus(
user_id,
login_bonus["bonusId"],
login_bonus["point"],
login_bonus["isCurrent"],
login_bonus["isComplete"],
)
if upsert["isNewMapList"] and int(upsert["isNewMapList"]) > 0:
for map in upsert["userMapList"]:
self.data.item.put_map(user_id, map["mapId"], map["distance"], map["isLock"], map["isClear"], map["isComplete"])
self.data.item.put_map(
user_id,
map["mapId"],
map["distance"],
map["isLock"],
map["isClear"],
map["isComplete"],
)
if upsert["isNewMusicDetailList"] and int(upsert["isNewMusicDetailList"]) > 0:
for music in upsert["userMusicDetailList"]:
self.data.score.put_best_score(user_id, music)
if upsert["isNewCourseList"] and int(upsert["isNewCourseList"]) > 0:
for course in upsert["userCourseList"]:
self.data.score.put_course(user_id, course)
if upsert["isNewFavoriteList"] and int(upsert["isNewFavoriteList"]) > 0:
for fav in upsert["userFavoriteList"]:
self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"])
@ -214,45 +264,39 @@ class Mai2Base():
def handle_get_user_data_api_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile_detail(data["userId"], self.version)
if profile is None: return
if profile is None:
return
profile_dict = profile._asdict()
profile_dict.pop("id")
profile_dict.pop("user")
profile_dict.pop("version")
return {
"userId": data["userId"],
"userData": profile_dict
}
return {"userId": data["userId"], "userData": profile_dict}
def handle_get_user_extend_api_request(self, data: Dict) -> Dict:
extend = self.data.profile.get_profile_extend(data["userId"], self.version)
if extend is None: return
if extend is None:
return
extend_dict = extend._asdict()
extend_dict.pop("id")
extend_dict.pop("user")
extend_dict.pop("version")
return {
"userId": data["userId"],
"userExtend": extend_dict
}
return {"userId": data["userId"], "userExtend": extend_dict}
def handle_get_user_option_api_request(self, data: Dict) -> Dict:
options = self.data.profile.get_profile_option(data["userId"], self.version)
if options is None: return
if options is None:
return
options_dict = options._asdict()
options_dict.pop("id")
options_dict.pop("user")
options_dict.pop("version")
return {
"userId": data["userId"],
"userOption": options_dict
}
return {"userId": data["userId"], "userOption": options_dict}
def handle_get_user_card_api_request(self, data: Dict) -> Dict:
return {"userId": data["userId"], "nextIndex": 0, "userCardList": []}
@ -269,73 +313,83 @@ class Mai2Base():
for x in range(next_idx, data["maxCount"]):
try:
user_item_list.append({"item_kind": user_items[x]["item_kind"], "item_id": user_items[x]["item_id"],
"stock": user_items[x]["stock"], "isValid": user_items[x]["is_valid"]})
except: break
user_item_list.append(
{
"item_kind": user_items[x]["item_kind"],
"item_id": user_items[x]["item_id"],
"stock": user_items[x]["stock"],
"isValid": user_items[x]["is_valid"],
}
)
except:
break
if len(user_item_list) == data["maxCount"]:
next_idx = data["nextIndex"] + data["maxCount"] + 1
break
return {"userId": data["userId"], "nextIndex": next_idx, "itemKind": kind, "userItemList": user_item_list}
return {
"userId": data["userId"],
"nextIndex": next_idx,
"itemKind": kind,
"userItemList": user_item_list,
}
def handle_get_user_character_api_request(self, data: Dict) -> Dict:
characters = self.data.item.get_characters(data["userId"])
chara_list = []
for chara in characters:
chara_list.append({
"characterId": chara["character_id"],
"level": chara["level"],
"awakening": chara["awakening"],
"useCount": chara["use_count"],
})
chara_list.append(
{
"characterId": chara["character_id"],
"level": chara["level"],
"awakening": chara["awakening"],
"useCount": chara["use_count"],
}
)
return {"userId": data["userId"], "userCharacterList": chara_list}
def handle_get_user_favorite_api_request(self, data: Dict) -> Dict:
favorites = self.data.item.get_favorites(data["userId"], data["itemKind"])
if favorites is None: return
if favorites is None:
return
userFavs = []
for fav in favorites:
userFavs.append({
"userId": data["userId"],
"itemKind": fav["itemKind"],
"itemIdList": fav["itemIdList"]
})
userFavs.append(
{
"userId": data["userId"],
"itemKind": fav["itemKind"],
"itemIdList": fav["itemIdList"],
}
)
return {
"userId": data["userId"],
"userFavoriteData": userFavs
}
return {"userId": data["userId"], "userFavoriteData": userFavs}
def handle_get_user_ghost_api_request(self, data: Dict) -> Dict:
ghost = self.data.profile.get_profile_ghost(data["userId"], self.version)
if ghost is None: return
if ghost is None:
return
ghost_dict = ghost._asdict()
ghost_dict.pop("user")
ghost_dict.pop("id")
ghost_dict.pop("version_int")
return {
"userId": data["userId"],
"userGhost": ghost_dict
}
return {"userId": data["userId"], "userGhost": ghost_dict}
def handle_get_user_rating_api_request(self, data: Dict) -> Dict:
rating = self.data.profile.get_profile_rating(data["userId"], self.version)
if rating is None: return
if rating is None:
return
rating_dict = rating._asdict()
rating_dict.pop("user")
rating_dict.pop("id")
rating_dict.pop("version")
return {
"userId": data["userId"],
"userRating": rating_dict
}
return {"userId": data["userId"], "userRating": rating_dict}
def handle_get_user_activity_api_request(self, data: Dict) -> Dict:
"""
@ -343,31 +397,27 @@ class Mai2Base():
"""
playlist = self.data.profile.get_profile_activity(data["userId"], 1)
musiclist = self.data.profile.get_profile_activity(data["userId"], 2)
if playlist is None or musiclist is None: return
if playlist is None or musiclist is None:
return
plst = []
mlst = []
for play in playlist:
tmp = play._asdict()
tmp = play._asdict()
tmp["id"] = tmp["activityId"]
tmp.pop("activityId")
tmp.pop("user")
plst.append(tmp)
for music in musiclist:
tmp = music._asdict()
tmp = music._asdict()
tmp["id"] = tmp["activityId"]
tmp.pop("activityId")
tmp.pop("user")
mlst.append(tmp)
return {
"userActivity": {
"playList": plst,
"musicList": mlst
}
}
return {"userActivity": {"playList": plst, "musicList": mlst}}
def handle_get_user_course_api_request(self, data: Dict) -> Dict:
user_courses = self.data.score.get_courses(data["userId"])
@ -392,21 +442,30 @@ class Mai2Base():
for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]):
try:
friend_season_ranking_list.append({
"mapId": friend_season_ranking_list[x]["map_id"],
"distance": friend_season_ranking_list[x]["distance"],
"isLock": friend_season_ranking_list[x]["is_lock"],
"isClear": friend_season_ranking_list[x]["is_clear"],
"isComplete": friend_season_ranking_list[x]["is_complete"],
})
friend_season_ranking_list.append(
{
"mapId": friend_season_ranking_list[x]["map_id"],
"distance": friend_season_ranking_list[x]["distance"],
"isLock": friend_season_ranking_list[x]["is_lock"],
"isClear": friend_season_ranking_list[x]["is_clear"],
"isComplete": friend_season_ranking_list[x]["is_complete"],
}
)
except:
break
# We're capped and still have some left to go
if len(friend_season_ranking_list) == data["maxCount"] and len(friend_season_ranking) > data["maxCount"] + data["nextIndex"]:
if (
len(friend_season_ranking_list) == data["maxCount"]
and len(friend_season_ranking) > data["maxCount"] + data["nextIndex"]
):
next_index = data["maxCount"] + data["nextIndex"]
return {"userId": data["userId"], "nextIndex": next_index, "userFriendSeasonRankingList": friend_season_ranking_list}
return {
"userId": data["userId"],
"nextIndex": next_index,
"userFriendSeasonRankingList": friend_season_ranking_list,
}
def handle_get_user_map_api_request(self, data: Dict) -> Dict:
maps = self.data.item.get_maps(data["userId"])
@ -415,21 +474,30 @@ class Mai2Base():
for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]):
try:
map_list.append({
"mapId": maps[x]["map_id"],
"distance": maps[x]["distance"],
"isLock": maps[x]["is_lock"],
"isClear": maps[x]["is_clear"],
"isComplete": maps[x]["is_complete"],
})
map_list.append(
{
"mapId": maps[x]["map_id"],
"distance": maps[x]["distance"],
"isLock": maps[x]["is_lock"],
"isClear": maps[x]["is_clear"],
"isComplete": maps[x]["is_complete"],
}
)
except:
break
# We're capped and still have some left to go
if len(map_list) == data["maxCount"] and len(maps) > data["maxCount"] + data["nextIndex"]:
if (
len(map_list) == data["maxCount"]
and len(maps) > data["maxCount"] + data["nextIndex"]
):
next_index = data["maxCount"] + data["nextIndex"]
return {"userId": data["userId"], "nextIndex": next_index, "userMapList": map_list}
return {
"userId": data["userId"],
"nextIndex": next_index,
"userMapList": map_list,
}
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
login_bonuses = self.data.item.get_login_bonuses(data["userId"])
@ -438,20 +506,29 @@ class Mai2Base():
for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]):
try:
login_bonus_list.append({
"bonusId": login_bonuses[x]["bonus_id"],
"point": login_bonuses[x]["point"],
"isCurrent": login_bonuses[x]["is_current"],
"isComplete": login_bonuses[x]["is_complete"],
})
login_bonus_list.append(
{
"bonusId": login_bonuses[x]["bonus_id"],
"point": login_bonuses[x]["point"],
"isCurrent": login_bonuses[x]["is_current"],
"isComplete": login_bonuses[x]["is_complete"],
}
)
except:
break
# We're capped and still have some left to go
if len(login_bonus_list) == data["maxCount"] and len(login_bonuses) > data["maxCount"] + data["nextIndex"]:
if (
len(login_bonus_list) == data["maxCount"]
and len(login_bonuses) > data["maxCount"] + data["nextIndex"]
):
next_index = data["maxCount"] + data["nextIndex"]
return {"userId": data["userId"], "nextIndex": next_index, "userLoginBonusList": login_bonus_list}
return {
"userId": data["userId"],
"nextIndex": next_index,
"userLoginBonusList": login_bonus_list,
}
def handle_get_user_region_api_request(self, data: Dict) -> Dict:
return {"userId": data["userId"], "length": 0, "userRegionList": []}
@ -463,18 +540,24 @@ class Mai2Base():
if songs is not None:
for song in songs:
music_detail_list.append({
"musicId": song["song_id"],
"level": song["chart_id"],
"playCount": song["play_count"],
"achievement": song["achievement"],
"comboStatus": song["combo_status"],
"syncStatus": song["sync_status"],
"deluxscoreMax": song["dx_score"],
"scoreRank": song["score_rank"],
})
music_detail_list.append(
{
"musicId": song["song_id"],
"level": song["chart_id"],
"playCount": song["play_count"],
"achievement": song["achievement"],
"comboStatus": song["combo_status"],
"syncStatus": song["sync_status"],
"deluxscoreMax": song["dx_score"],
"scoreRank": song["score_rank"],
}
)
if len(music_detail_list) == data["maxCount"]:
next_index = data["maxCount"] + data["nextIndex"]
break
return {"userId": data["userId"], "nextIndex": next_index, "userMusicList": [{"userMusicDetailList": music_detail_list}]}
return {
"userId": data["userId"],
"nextIndex": next_index,
"userMusicList": [{"userMusicDetailList": music_detail_list}],
}

View File

@ -1,17 +1,25 @@
from core.config import CoreConfig
class Mai2ServerConfig():
class Mai2ServerConfig:
def __init__(self, parent: "Mai2Config") -> None:
self.__config = parent
@property
def enable(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'mai2', 'server', 'enable', default=True)
return CoreConfig.get_config_field(
self.__config, "mai2", "server", "enable", default=True
)
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'mai2', 'server', 'loglevel', default="info"))
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "mai2", "server", "loglevel", default="info"
)
)
class Mai2Config(dict):
def __init__(self) -> None:
self.server = Mai2ServerConfig(self)
self.server = Mai2ServerConfig(self)

View File

@ -1,4 +1,4 @@
class Mai2Constants():
class Mai2Constants:
GRADE = {
"D": 0,
"C": 1,
@ -13,22 +13,10 @@ class Mai2Constants():
"SS": 10,
"SS+": 11,
"SSS": 12,
"SSS+": 13
}
FC = {
"None": 0,
"FC": 1,
"FC+": 2,
"AP": 3,
"AP+": 4
}
SYNC = {
"None": 0,
"FS": 1,
"FS+": 2,
"FDX": 3,
"FDX+": 4
"SSS+": 13,
}
FC = {"None": 0, "FC": 1, "FC+": 2, "AP": 3, "AP+": 4}
SYNC = {"None": 0, "FS": 1, "FS+": 2, "FDX": 3, "FDX+": 4}
DATE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
@ -43,9 +31,15 @@ class Mai2Constants():
VER_MAIMAI_DX_UNIVERSE = 4
VER_MAIMAI_DX_UNIVERSE_PLUS = 5
VERSION_STRING = ("maimai Delux", "maimai Delux PLUS", "maimai Delux Splash", "maimai Delux Splash PLUS", "maimai Delux Universe",
"maimai Delux Universe PLUS")
VERSION_STRING = (
"maimai Delux",
"maimai Delux PLUS",
"maimai Delux Splash",
"maimai Delux Splash PLUS",
"maimai Delux Universe",
"maimai Delux Universe PLUS",
)
@classmethod
def game_ver_to_string(cls, ver: int):
return cls.VERSION_STRING[ver]
return cls.VERSION_STRING[ver]

View File

@ -1,6 +1,12 @@
from core.data import Data
from core.config import CoreConfig
from titles.mai2.schema import Mai2ItemData, Mai2ProfileData, Mai2StaticData, Mai2ScoreData
from titles.mai2.schema import (
Mai2ItemData,
Mai2ProfileData,
Mai2StaticData,
Mai2ScoreData,
)
class Mai2Data(Data):
def __init__(self, cfg: CoreConfig) -> None:
@ -9,4 +15,4 @@ class Mai2Data(Data):
self.profile = Mai2ProfileData(self.config, self.session)
self.item = Mai2ItemData(self.config, self.session)
self.static = Mai2StaticData(self.config, self.session)
self.score = Mai2ScoreData(self.config, self.session)
self.score = Mai2ScoreData(self.config, self.session)

View File

@ -20,12 +20,14 @@ from titles.mai2.universe import Mai2Universe
from titles.mai2.universeplus import Mai2UniversePlus
class Mai2Servlet():
class Mai2Servlet:
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
self.core_cfg = core_cfg
self.game_cfg = Mai2Config()
if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"):
self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}")))
self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"))
)
self.versions = [
Mai2Base(core_cfg, self.game_cfg),
@ -39,89 +41,112 @@ class Mai2Servlet():
self.logger = logging.getLogger("mai2")
log_fmt_str = "[%(asctime)s] Mai2 | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "mai2"), encoding='utf8',
when="d", backupCount=10)
fileHandler = TimedRotatingFileHandler(
"{0}/{1}.log".format(self.core_cfg.server.log_dir, "mai2"),
encoding="utf8",
when="d",
backupCount=10,
)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler)
self.logger.setLevel(self.game_cfg.server.loglevel)
coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str)
coloredlogs.install(
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
)
@classmethod
def get_allnet_info(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str, str]:
def get_allnet_info(
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
) -> Tuple[bool, str, str]:
game_cfg = Mai2Config()
if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"):
game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}")))
game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"))
)
if not game_cfg.server.enable:
return (False, "", "")
if core_cfg.server.is_develop:
return (True, f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/", f"{core_cfg.title.hostname}:{core_cfg.title.port}/")
return (True, f"http://{core_cfg.title.hostname}/{game_code}/$v/", f"{core_cfg.title.hostname}/")
return (
True,
f"http://{core_cfg.title.hostname}:{core_cfg.title.port}/{game_code}/$v/",
f"{core_cfg.title.hostname}:{core_cfg.title.port}/",
)
return (
True,
f"http://{core_cfg.title.hostname}/{game_code}/$v/",
f"{core_cfg.title.hostname}/",
)
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
if url_path.lower() == "/ping":
return zlib.compress(b'{"returnCode": "1"}')
req_raw = request.content.getvalue()
url = request.uri.decode()
url_split = url_path.split("/")
internal_ver = 0
endpoint = url_split[len(url_split) - 1]
if version < 105: # 1.0
if version < 105: # 1.0
internal_ver = Mai2Constants.VER_MAIMAI_DX
elif version >= 105 and version < 110: # Plus
elif version >= 105 and version < 110: # Plus
internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS
elif version >= 110 and version < 115: # Splash
elif version >= 110 and version < 115: # Splash
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH
elif version >= 115 and version < 120: # Splash Plus
elif version >= 115 and version < 120: # Splash Plus
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS
elif version >= 120 and version < 125: # Universe
elif version >= 120 and version < 125: # Universe
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE
elif version >= 125: # Universe Plus
elif version >= 125: # Universe Plus
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
# If we get a 32 character long hex string, it's a hash and we're
# doing encrypted. The likelyhood of false positives is low but
# If we get a 32 character long hex string, it's a hash and we're
# doing encrypted. The likelyhood of false positives is low but
# technically not 0
self.logger.error("Encryption not supported at this time")
try:
try:
unzip = zlib.decompress(req_raw)
except zlib.error as e:
self.logger.error(f"Failed to decompress v{version} {endpoint} request -> {e}")
return zlib.compress("{\"stat\": \"0\"}".encode("utf-8"))
self.logger.error(
f"Failed to decompress v{version} {endpoint} request -> {e}"
)
return zlib.compress(b'{"stat": "0"}')
req_data = json.loads(unzip)
self.logger.info(f"v{version} {endpoint} request - {req_data}")
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
if not hasattr(self.versions[internal_ver], func_to_find):
self.logger.warning(f"Unhandled v{version} request {endpoint}")
return zlib.compress(b'{"returnCode": 1}')
try:
handler = getattr(self.versions[internal_ver], func_to_find)
resp = handler(req_data)
except AttributeError as e:
self.logger.warning(f"Unhandled v{version} request {endpoint} - {e}")
return zlib.compress("{\"stat\": \"0\"}".encode("utf-8"))
except Exception as e:
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
return zlib.compress("{\"stat\": \"0\"}".encode("utf-8"))
return zlib.compress(b'{"stat": "0"}')
if resp == None:
resp = {'returnCode': 1}
resp = {"returnCode": 1}
self.logger.info(f"Response {resp}")
return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))

View File

@ -8,7 +8,8 @@ from titles.mai2.base import Mai2Base
from titles.mai2.config import Mai2Config
from titles.mai2.const import Mai2Constants
class Mai2Plus(Mai2Base):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_PLUS
self.version = Mai2Constants.VER_MAIMAI_DX_PLUS

View File

@ -11,25 +11,35 @@ from read import BaseReader
from titles.mai2.const import Mai2Constants
from titles.mai2.database import Mai2Data
class Mai2Reader(BaseReader):
def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None:
def __init__(
self,
config: CoreConfig,
version: int,
bin_dir: Optional[str],
opt_dir: Optional[str],
extra: Optional[str],
) -> None:
super().__init__(config, version, bin_dir, opt_dir, extra)
self.data = Mai2Data(config)
try:
self.logger.info(f"Start importer for {Mai2Constants.game_ver_to_string(version)}")
self.logger.info(
f"Start importer for {Mai2Constants.game_ver_to_string(version)}"
)
except IndexError:
self.logger.error(f"Invalid maidx version {version}")
exit(1)
def read(self) -> None:
data_dirs = []
if self.bin_dir is not None:
data_dirs += self.get_data_directories(self.bin_dir)
if self.opt_dir is not None:
data_dirs += self.get_data_directories(self.opt_dir)
data_dirs += self.get_data_directories(self.opt_dir)
for dir in data_dirs:
self.logger.info(f"Read from {dir}")
self.get_events(f"{dir}/event")
@ -43,47 +53,64 @@ class Mai2Reader(BaseReader):
for dir in dirs:
if os.path.exists(f"{root}/{dir}/Event.xml"):
with open(f"{root}/{dir}/Event.xml", encoding="utf-8") as f:
troot = ET.fromstring(f.read())
name = troot.find('name').find('str').text
id = int(troot.find('name').find('id').text)
event_type = int(troot.find('infoType').text)
name = troot.find("name").find("str").text
id = int(troot.find("name").find("id").text)
event_type = int(troot.find("infoType").text)
self.data.static.put_game_event(self.version, event_type, id, name)
self.data.static.put_game_event(
self.version, event_type, id, name
)
self.logger.info(f"Added event {id}...")
def read_music(self, base_dir: str) -> None:
self.logger.info(f"Reading music from {base_dir}...")
for root, dirs, files in os.walk(base_dir):
for dir in dirs:
for dir in dirs:
if os.path.exists(f"{root}/{dir}/Music.xml"):
with open(f"{root}/{dir}/Music.xml", encoding="utf-8") as f:
troot = ET.fromstring(f.read())
song_id = int(troot.find('name').find('id').text)
title = troot.find('name').find('str').text
artist = troot.find('artistName').find('str').text
genre = troot.find('genreName').find('str').text
bpm = int(troot.find('bpm').text)
added_ver = troot.find('AddVersion').find('str').text
song_id = int(troot.find("name").find("id").text)
title = troot.find("name").find("str").text
artist = troot.find("artistName").find("str").text
genre = troot.find("genreName").find("str").text
bpm = int(troot.find("bpm").text)
added_ver = troot.find("AddVersion").find("str").text
note_data = troot.find('notesData').findall('Notes')
note_data = troot.find("notesData").findall("Notes")
for dif in note_data:
path = dif.find('file').find('path').text
path = dif.find("file").find("path").text
if path is not None:
if os.path.exists(f"{root}/{dir}/{path}"):
chart_id = int(path.split(".")[0].split('_')[1])
diff_num = float(f"{dif.find('level').text}.{dif.find('levelDecimal').text}")
note_designer = dif.find('notesDesigner').find('str').text
chart_id = int(path.split(".")[0].split("_")[1])
diff_num = float(
f"{dif.find('level').text}.{dif.find('levelDecimal').text}"
)
note_designer = (
dif.find("notesDesigner").find("str").text
)
self.data.static.put_game_music(
self.version,
song_id,
chart_id,
title,
artist,
genre,
bpm,
added_ver,
diff_num,
note_designer,
)
self.logger.info(
f"Added music id {song_id} chart {chart_id}"
)
self.data.static.put_game_music(self.version, song_id, chart_id, title, artist,
genre, bpm, added_ver, diff_num, note_designer)
self.logger.info(f"Added music id {song_id} chart {chart_id}")
def read_tickets(self, base_dir: str) -> None:
self.logger.info(f"Reading tickets from {base_dir}...")
@ -91,13 +118,14 @@ class Mai2Reader(BaseReader):
for dir in dirs:
if os.path.exists(f"{root}/{dir}/Ticket.xml"):
with open(f"{root}/{dir}/Ticket.xml", encoding="utf-8") as f:
troot = ET.fromstring(f.read())
name = troot.find('name').find('str').text
id = int(troot.find('name').find('id').text)
ticket_type = int(troot.find('ticketKind').find('id').text)
price = int(troot.find('creditNum').text)
name = troot.find("name").find("str").text
id = int(troot.find("name").find("id").text)
ticket_type = int(troot.find("ticketKind").find("id").text)
price = int(troot.find("creditNum").text)
self.data.static.put_game_ticket(self.version, id, ticket_type, price, name)
self.data.static.put_game_ticket(
self.version, id, ticket_type, price, name
)
self.logger.info(f"Added ticket {id}...")

View File

@ -3,4 +3,4 @@ from titles.mai2.schema.item import Mai2ItemData
from titles.mai2.schema.static import Mai2StaticData
from titles.mai2.schema.score import Mai2ScoreData
__all__ = [Mai2ProfileData, Mai2ItemData, Mai2StaticData, Mai2ScoreData]
__all__ = [Mai2ProfileData, Mai2ItemData, Mai2StaticData, Mai2ScoreData]

View File

@ -12,20 +12,28 @@ character = Table(
"mai2_item_character",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("character_id", Integer, nullable=False),
Column("level", Integer, nullable=False, server_default="1"),
Column("awakening", Integer, nullable=False, server_default="0"),
Column("use_count", Integer, nullable=False, server_default="0"),
UniqueConstraint("user", "character_id", name="mai2_item_character_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
card = Table(
"mai2_item_card",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("card_kind", Integer, nullable=False),
Column("card_id", Integer, nullable=False),
Column("chara_id", Integer, nullable=False),
@ -33,54 +41,70 @@ card = Table(
Column("start_date", String(255), nullable=False),
Column("end_date", String(255), nullable=False),
UniqueConstraint("user", "card_kind", "card_id", name="mai2_item_card_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
item = Table(
"mai2_item_item",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("item_kind", Integer, nullable=False),
Column("item_id", Integer, nullable=False),
Column("stock", Integer, nullable=False, server_default="1"),
Column("is_valid", Boolean, nullable=False, server_default="1"),
UniqueConstraint("user", "item_kind", "item_id", name="mai2_item_item_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
map = Table(
"mai2_item_map",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("map_id", Integer, nullable=False),
Column("distance", Integer, nullable=False),
Column("is_lock", Boolean, nullable=False, server_default="0"),
Column("is_clear", Boolean, nullable=False, server_default="0"),
Column("is_complete", Boolean, nullable=False, server_default="0"),
UniqueConstraint("user", "map_id", name="mai2_item_map_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
login_bonus = Table(
"mai2_item_login_bonus",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("bonus_id", Integer, nullable=False),
Column("point", Integer, nullable=False),
Column("is_current", Boolean, nullable=False, server_default="0"),
Column("is_complete", Boolean, nullable=False, server_default="0"),
UniqueConstraint("user", "bonus_id", name="mai2_item_login_bonus_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
friend_season_ranking = Table(
"mai2_item_friend_season_ranking",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("season_id", Integer, nullable=False),
Column("point", Integer, nullable=False),
Column("rank", Integer, nullable=False),
@ -88,35 +112,46 @@ friend_season_ranking = Table(
Column("user_name", String(8), nullable=False),
Column("record_date", String(255), nullable=False),
UniqueConstraint("user", "season_id", "user_name", name="mai2_item_login_bonus_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
favorite = Table(
"mai2_item_favorite",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("itemKind", Integer, nullable=False),
Column("itemIdList", JSON),
UniqueConstraint("user", "itemKind", name="mai2_item_favorite_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
charge = Table(
"mai2_item_charge",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("charge_id", Integer, nullable=False),
Column("stock", Integer, nullable=False),
Column("purchase_date", String(255), nullable=False),
Column("valid_date", String(255), nullable=False),
UniqueConstraint("user", "charge_id", name="mai2_item_charge_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class Mai2ItemData(BaseData):
def put_item(self, user_id: int, item_kind: int, item_id: int, stock: int, is_valid: bool) -> None:
def put_item(
self, user_id: int, item_kind: int, item_id: int, stock: int, is_valid: bool
) -> None:
sql = insert(item).values(
user=user_id,
item_kind=item_kind,
@ -132,28 +167,47 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_item: failed to insert item! user_id: {user_id}, item_kind: {item_kind}, item_id: {item_id}")
self.logger.warn(
f"put_item: failed to insert item! user_id: {user_id}, item_kind: {item_kind}, item_id: {item_id}"
)
return None
return result.lastrowid
def get_items(self, user_id: int, item_kind: int = None) -> Optional[List[Row]]:
if item_kind is None:
sql = item.select(item.c.user == user_id)
else:
sql = item.select(and_(item.c.user == user_id, item.c.item_kind == item_kind))
sql = item.select(
and_(item.c.user == user_id, item.c.item_kind == item_kind)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def get_item(self, user_id: int, item_kind: int, item_id: int) -> Optional[Row]:
sql = item.select(and_(item.c.user == user_id, item.c.item_kind == item_kind, item.c.item_id == item_id))
sql = item.select(
and_(
item.c.user == user_id,
item.c.item_kind == item_kind,
item.c.item_id == item_id,
)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def put_login_bonus(self, user_id: int, bonus_id: int, point: int, is_current: bool, is_complete: bool) -> None:
def put_login_bonus(
self,
user_id: int,
bonus_id: int,
point: int,
is_current: bool,
is_complete: bool,
) -> None:
sql = insert(login_bonus).values(
user=user_id,
bonus_id=bonus_id,
@ -170,25 +224,39 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_login_bonus: failed to insert item! user_id: {user_id}, bonus_id: {bonus_id}, point: {point}")
self.logger.warn(
f"put_login_bonus: failed to insert item! user_id: {user_id}, bonus_id: {bonus_id}, point: {point}"
)
return None
return result.lastrowid
def get_login_bonuses(self, user_id: int) -> Optional[List[Row]]:
sql = login_bonus.select(login_bonus.c.user == user_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def get_login_bonus(self, user_id: int, bonus_id: int) -> Optional[Row]:
sql = login_bonus.select(and_(login_bonus.c.user == user_id, login_bonus.c.bonus_id == bonus_id))
sql = login_bonus.select(
and_(login_bonus.c.user == user_id, login_bonus.c.bonus_id == bonus_id)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def put_map(self, user_id: int, map_id: int, distance: int, is_lock: bool, is_clear: bool, is_complete: bool) -> None:
def put_map(
self,
user_id: int,
map_id: int,
distance: int,
is_lock: bool,
is_clear: bool,
is_complete: bool,
) -> None:
sql = insert(map).values(
user=user_id,
map_id=map_id,
@ -207,25 +275,36 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_map: failed to insert item! user_id: {user_id}, map_id: {map_id}, distance: {distance}")
self.logger.warn(
f"put_map: failed to insert item! user_id: {user_id}, map_id: {map_id}, distance: {distance}"
)
return None
return result.lastrowid
def get_maps(self, user_id: int) -> Optional[List[Row]]:
sql = map.select(map.c.user == user_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def get_map(self, user_id: int, map_id: int) -> Optional[Row]:
sql = map.select(and_(map.c.user == user_id, map.c.map_id == map_id))
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def put_character(self, user_id: int, character_id: int, level: int, awakening: int, use_count: int) -> None:
def put_character(
self,
user_id: int,
character_id: int,
level: int,
awakening: int,
use_count: int,
) -> None:
sql = insert(character).values(
user=user_id,
character_id=character_id,
@ -242,57 +321,64 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_character: failed to insert item! user_id: {user_id}, character_id: {character_id}, level: {level}")
self.logger.warn(
f"put_character: failed to insert item! user_id: {user_id}, character_id: {character_id}, level: {level}"
)
return None
return result.lastrowid
def get_characters(self, user_id: int) -> Optional[List[Row]]:
sql = character.select(character.c.user == user_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def get_character(self, user_id: int, character_id: int) -> Optional[Row]:
sql = character.select(and_(character.c.user == user_id, character.c.character_id == character_id))
sql = character.select(
and_(character.c.user == user_id, character.c.character_id == character_id)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def get_friend_season_ranking(self, user_id: int) -> Optional[Row]:
sql = friend_season_ranking.select(friend_season_ranking.c.user == user_id)
result = self.execute(sql)
if result is None:return None
if result is None:
return None
return result.fetchone()
def put_favorite(self, user_id: int, kind: int, item_id_list: List[int]) -> Optional[int]:
def put_favorite(
self, user_id: int, kind: int, item_id_list: List[int]
) -> Optional[int]:
sql = insert(favorite).values(
user=user_id,
kind=kind,
item_id_list=item_id_list
user=user_id, kind=kind, item_id_list=item_id_list
)
conflict = sql.on_duplicate_key_update(
item_id_list=item_id_list
)
conflict = sql.on_duplicate_key_update(item_id_list=item_id_list)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_favorite: failed to insert item! user_id: {user_id}, kind: {kind}")
self.logger.warn(
f"put_favorite: failed to insert item! user_id: {user_id}, kind: {kind}"
)
return None
return result.lastrowid
def get_favorites(self, user_id: int, kind: int = None) -> Optional[Row]:
if kind is None:
sql = favorite.select(favorite.c.user == user_id)
else:
sql = favorite.select(and_(
favorite.c.user == user_id,
favorite.c.itemKind == kind
))
sql = favorite.select(
and_(favorite.c.user == user_id, favorite.c.itemKind == kind)
)
result = self.execute(sql)
if result is None:return None
return result.fetchall()
if result is None:
return None
return result.fetchall()

View File

@ -14,7 +14,11 @@ detail = Table(
"mai2_profile_detail",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("userName", String(25)),
Column("isNetMember", Integer),
@ -41,9 +45,9 @@ detail = Table(
Column("lastRomVersion", String(25)),
Column("lastDataVersion", String(25)),
Column("lastLoginDate", String(25)),
Column("lastPairLoginDate", String(25)), # new with uni+
Column("lastPairLoginDate", String(25)), # new with uni+
Column("lastPlayDate", String(25)),
Column("lastTrialPlayDate", String(25)), # new with uni+
Column("lastTrialPlayDate", String(25)), # new with uni+
Column("lastPlayCredit", Integer),
Column("lastPlayMode", Integer),
Column("lastPlaceId", Integer),
@ -90,16 +94,20 @@ detail = Table(
Column("playerOldRating", BigInteger),
Column("playerNewRating", BigInteger),
Column("dateTime", BigInteger),
Column("banState", Integer), # new with uni+
Column("banState", Integer), # new with uni+
UniqueConstraint("user", "version", name="mai2_profile_detail_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
ghost = Table(
"mai2_profile_ghost",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version_int", Integer, nullable=False),
Column("name", String(25)),
Column("iconId", Integer),
@ -120,15 +128,21 @@ ghost = Table(
Column("resultBitList", JSON),
Column("resultNum", Integer),
Column("achievement", Integer),
UniqueConstraint("user", "version", "musicId", "difficulty", name="mai2_profile_ghost_uk"),
mysql_charset='utf8mb4'
UniqueConstraint(
"user", "version", "musicId", "difficulty", name="mai2_profile_ghost_uk"
),
mysql_charset="utf8mb4",
)
extend = Table(
"mai2_profile_extend",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("selectMusicId", Integer),
Column("selectDifficultyId", Integer),
@ -145,14 +159,18 @@ extend = Table(
Column("selectedCardList", JSON),
Column("encountMapNpcList", JSON),
UniqueConstraint("user", "version", name="mai2_profile_extend_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
option = Table(
"mai2_profile_option",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("selectMusicId", Integer),
Column("optionKind", Integer),
@ -200,14 +218,18 @@ option = Table(
Column("sortTab", Integer),
Column("sortMusic", Integer),
UniqueConstraint("user", "version", name="mai2_profile_option_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
rating = Table(
"mai2_profile_rating",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("rating", Integer),
Column("ratingList", JSON),
@ -216,26 +238,34 @@ rating = Table(
Column("nextNewRatingList", JSON),
Column("udemae", JSON),
UniqueConstraint("user", "version", name="mai2_profile_rating_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
region = Table(
"mai2_profile_region",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("regionId", Integer),
Column("playCount", Integer, server_default="1"),
Column("created", String(25)),
UniqueConstraint("user", "regionId", name="mai2_profile_region_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
activity = Table(
"mai2_profile_activity",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("kind", Integer, nullable=False),
Column("activityId", Integer, nullable=False),
Column("param1", Integer, nullable=False),
@ -244,11 +274,14 @@ activity = Table(
Column("param4", Integer, nullable=False),
Column("sortNumber", Integer, nullable=False),
UniqueConstraint("user", "kind", "activityId", name="mai2_profile_activity_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class Mai2ProfileData(BaseData):
def put_profile_detail(self, user_id: int, version: int, detail_data: Dict) -> Optional[Row]:
def put_profile_detail(
self, user_id: int, version: int, detail_data: Dict
) -> Optional[Row]:
detail_data["user"] = user_id
detail_data["version"] = version
sql = insert(detail).values(**detail_data)
@ -257,18 +290,25 @@ class Mai2ProfileData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile: Failed to create profile! user_id {user_id}")
self.logger.warn(
f"put_profile: Failed to create profile! user_id {user_id}"
)
return None
return result.lastrowid
def get_profile_detail(self, user_id: int, version: int) -> Optional[Row]:
sql = select(detail).where(and_(detail.c.user == user_id, detail.c.version == version))
sql = select(detail).where(
and_(detail.c.user == user_id, detail.c.version == version)
)
result = self.execute(sql)
if result is None:return None
if result is None:
return None
return result.fetchone()
def put_profile_ghost(self, user_id: int, version: int, ghost_data: Dict) -> Optional[int]:
def put_profile_ghost(
self, user_id: int, version: int, ghost_data: Dict
) -> Optional[int]:
ghost_data["user"] = user_id
ghost_data["version_int"] = version
@ -282,13 +322,18 @@ class Mai2ProfileData(BaseData):
return result.lastrowid
def get_profile_ghost(self, user_id: int, version: int) -> Optional[Row]:
sql = select(ghost).where(and_(ghost.c.user == user_id, ghost.c.version_int == version))
sql = select(ghost).where(
and_(ghost.c.user == user_id, ghost.c.version_int == version)
)
result = self.execute(sql)
if result is None:return None
if result is None:
return None
return result.fetchone()
def put_profile_extend(self, user_id: int, version: int, extend_data: Dict) -> Optional[int]:
def put_profile_extend(
self, user_id: int, version: int, extend_data: Dict
) -> Optional[int]:
extend_data["user"] = user_id
extend_data["version"] = version
@ -302,13 +347,18 @@ class Mai2ProfileData(BaseData):
return result.lastrowid
def get_profile_extend(self, user_id: int, version: int) -> Optional[Row]:
sql = select(extend).where(and_(extend.c.user == user_id, extend.c.version == version))
sql = select(extend).where(
and_(extend.c.user == user_id, extend.c.version == version)
)
result = self.execute(sql)
if result is None:return None
if result is None:
return None
return result.fetchone()
def put_profile_option(self, user_id: int, version: int, option_data: Dict) -> Optional[int]:
def put_profile_option(
self, user_id: int, version: int, option_data: Dict
) -> Optional[int]:
option_data["user"] = user_id
option_data["version"] = version
@ -322,13 +372,18 @@ class Mai2ProfileData(BaseData):
return result.lastrowid
def get_profile_option(self, user_id: int, version: int) -> Optional[Row]:
sql = select(option).where(and_(option.c.user == user_id, option.c.version == version))
sql = select(option).where(
and_(option.c.user == user_id, option.c.version == version)
)
result = self.execute(sql)
if result is None:return None
if result is None:
return None
return result.fetchone()
def put_profile_rating(self, user_id: int, version: int, rating_data: Dict) -> Optional[int]:
def put_profile_rating(
self, user_id: int, version: int, rating_data: Dict
) -> Optional[int]:
rating_data["user"] = user_id
rating_data["version"] = version
@ -342,23 +397,24 @@ class Mai2ProfileData(BaseData):
return result.lastrowid
def get_profile_rating(self, user_id: int, version: int) -> Optional[Row]:
sql = select(rating).where(and_(rating.c.user == user_id, rating.c.version == version))
sql = select(rating).where(
and_(rating.c.user == user_id, rating.c.version == version)
)
result = self.execute(sql)
if result is None:return None
if result is None:
return None
return result.fetchone()
def put_profile_region(self, user_id: int, region_id: int) -> Optional[int]:
sql = insert(region).values(
user = user_id,
regionId = region_id,
created = datetime.strftime(datetime.now(), Mai2Constants.DATE_TIME_FORMAT)
)
conflict = sql.on_duplicate_key_update(
playCount = region.c.playCount + 1
user=user_id,
regionId=region_id,
created=datetime.strftime(datetime.now(), Mai2Constants.DATE_TIME_FORMAT),
)
conflict = sql.on_duplicate_key_update(playCount=region.c.playCount + 1)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_region: failed to update! {user_id}")
@ -369,34 +425,38 @@ class Mai2ProfileData(BaseData):
sql = select(region).where(region.c.user == user_id)
result = self.execute(sql)
if result is None:return None
if result is None:
return None
return result.fetchall()
def put_profile_activity(self, user_id: int, activity_data: Dict) -> Optional[int]:
if "id" in activity_data:
activity_data["activityId"] = activity_data["id"]
activity_data.pop("id")
activity_data["user"] = user_id
sql = insert(activity).values(**activity_data)
conflict = sql.on_duplicate_key_update(**activity_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_profile_activity: failed to update! user_id: {user_id}")
self.logger.warn(
f"put_profile_activity: failed to update! user_id: {user_id}"
)
return None
return result.lastrowid
def get_profile_activity(self, user_id: int, kind: int = None) -> Optional[Row]:
sql = activity.select(
and_(
activity.c.user == user_id,
activity.c.user == user_id,
(activity.c.kind == kind) if kind is not None else True,
)
)
result = self.execute(sql)
if result is None:return None
if result is None:
return None
return result.fetchone()

View File

@ -12,7 +12,11 @@ best_score = Table(
"mai2_score_best",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("musicId", Integer),
Column("level", Integer),
Column("playCount", Integer),
@ -22,14 +26,18 @@ best_score = Table(
Column("deluxscoreMax", Integer),
Column("scoreRank", Integer),
UniqueConstraint("user", "musicId", "level", name="mai2_score_best_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
playlog = Table(
"mai2_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("userId", BigInteger),
Column("orderId", Integer),
Column("playlogId", BigInteger),
@ -136,14 +144,18 @@ playlog = Table(
Column("extNum1", Integer),
Column("extNum2", Integer),
Column("trialPlayAchievement", Integer),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
course = Table(
"mai2_score_course",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("courseId", Integer),
Column("isLastClear", Boolean),
Column("totalRestlife", Integer),
@ -157,9 +169,10 @@ course = Table(
Column("bestDeluxscore", Integer),
Column("bestDeluxscoreDate", String(25)),
UniqueConstraint("user", "courseId", name="mai2_score_best_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
class Mai2ScoreData(BaseData):
def put_best_score(self, user_id: int, score_data: Dict) -> Optional[int]:
score_data["user"] = user_id
@ -169,33 +182,39 @@ class Mai2ScoreData(BaseData):
result = self.execute(conflict)
if result is None:
self.logger.error(f"put_best_score: Failed to insert best score! user_id {user_id}")
self.logger.error(
f"put_best_score: Failed to insert best score! user_id {user_id}"
)
return None
return result.lastrowid
def get_best_scores(self, user_id: int, song_id: int = None) -> Optional[List[Row]]:
sql = best_score.select(
and_(
best_score.c.user == user_id,
(best_score.c.song_id == song_id) if song_id is not None else True
)
best_score.c.user == user_id,
(best_score.c.song_id == song_id) if song_id is not None else True,
)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchall()
def get_best_score(self, user_id: int, song_id: int, chart_id: int) -> Optional[Row]:
def get_best_score(
self, user_id: int, song_id: int, chart_id: int
) -> Optional[Row]:
sql = best_score.select(
and_(
best_score.c.user == user_id,
best_score.c.song_id == song_id,
best_score.c.chart_id == chart_id
)
best_score.c.user == user_id,
best_score.c.song_id == song_id,
best_score.c.chart_id == chart_id,
)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def put_playlog(self, user_id: int, playlog_data: Dict) -> Optional[int]:
@ -209,7 +228,7 @@ class Mai2ScoreData(BaseData):
self.logger.error(f"put_playlog: Failed to insert! user_id {user_id}")
return None
return result.lastrowid
def put_course(self, user_id: int, course_data: Dict) -> Optional[int]:
course_data["user"] = user_id
sql = insert(course).values(**course_data)
@ -221,10 +240,11 @@ class Mai2ScoreData(BaseData):
self.logger.error(f"put_course: Failed to insert! user_id {user_id}")
return None
return result.lastrowid
def get_courses(self, user_id: int) -> Optional[List[Row]]:
sql = course.select(best_score.c.user == user_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()

View File

@ -12,20 +12,20 @@ event = Table(
"mai2_static_event",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer,nullable=False),
Column("version", Integer, nullable=False),
Column("eventId", Integer),
Column("type", Integer),
Column("name", String(255)),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "eventId", "type", name="mai2_static_event_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
music = Table(
"mai2_static_music",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer,nullable=False),
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("songId", Integer),
Column("chartId", Integer),
Column("title", String(255)),
@ -36,39 +36,42 @@ music = Table(
Column("difficulty", Float),
Column("noteDesigner", String(255)),
UniqueConstraint("songId", "chartId", "version", name="mai2_static_music_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
ticket = Table(
"mai2_static_ticket",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer,nullable=False),
Column("version", Integer, nullable=False),
Column("ticketId", Integer),
Column("kind", Integer),
Column("name", String(255)),
Column("price", Integer, server_default="1"),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version","ticketId", name="mai2_static_ticket_uk"),
mysql_charset='utf8mb4'
UniqueConstraint("version", "ticketId", name="mai2_static_ticket_uk"),
mysql_charset="utf8mb4",
)
class Mai2StaticData(BaseData):
def put_game_event(self, version: int, type: int, event_id: int, name: str) -> Optional[int]:
def put_game_event(
self, version: int, type: int, event_id: int, name: str
) -> Optional[int]:
sql = insert(event).values(
version = version,
type = type,
eventId = event_id,
name = name,
version=version,
type=type,
eventId=event_id,
name=name,
)
conflict = sql.on_duplicate_key_update(
eventId = event_id
)
conflict = sql.on_duplicate_key_update(eventId=event_id)
result = self.execute(conflict)
if result is None:
self.logger.warning(f"put_game_event: Failed to insert event! event_id {event_id} type {type} name {name}")
self.logger.warning(
f"put_game_event: Failed to insert event! event_id {event_id} type {type} name {name}"
)
return result.lastrowid
def get_game_events(self, version: int) -> Optional[List[Row]]:
@ -78,50 +81,65 @@ class Mai2StaticData(BaseData):
if result is None:
return None
return result.fetchall()
def get_enabled_events(self, version: int) -> Optional[List[Row]]:
sql = select(event).where(and_(
event.c.version == version,
event.c.enabled == True
))
result = self.execute(sql)
if result is None: return None
return result.fetchall()
def toggle_game_events(self, version: int, event_id: int, toggle: bool) -> Optional[List]:
sql = event.update(and_(event.c.version == version, event.c.event_id == event_id)).values(
enabled = int(toggle)
sql = select(event).where(
and_(event.c.version == version, event.c.enabled == True)
)
result = self.execute(sql)
if result is None:
self.logger.warning(f"toggle_game_events: Failed to update event! event_id {event_id} toggle {toggle}")
return None
return result.fetchall()
def toggle_game_events(
self, version: int, event_id: int, toggle: bool
) -> Optional[List]:
sql = event.update(
and_(event.c.version == version, event.c.event_id == event_id)
).values(enabled=int(toggle))
result = self.execute(sql)
if result is None:
self.logger.warning(
f"toggle_game_events: Failed to update event! event_id {event_id} toggle {toggle}"
)
return result.last_updated_params()
def put_game_music(self, version: int, song_id: int, chart_id: int, title: str, artist: str,
genre: str, bpm: str, added_version: str, difficulty: float, note_designer: str) -> None:
def put_game_music(
self,
version: int,
song_id: int,
chart_id: int,
title: str,
artist: str,
genre: str,
bpm: str,
added_version: str,
difficulty: float,
note_designer: str,
) -> None:
sql = insert(music).values(
version = version,
songId = song_id,
chartId = chart_id,
title = title,
artist = artist,
genre = genre,
bpm = bpm,
addedVersion = added_version,
difficulty = difficulty,
noteDesigner = note_designer,
version=version,
songId=song_id,
chartId=chart_id,
title=title,
artist=artist,
genre=genre,
bpm=bpm,
addedVersion=added_version,
difficulty=difficulty,
noteDesigner=note_designer,
)
conflict = sql.on_duplicate_key_update(
title = title,
artist = artist,
genre = genre,
bpm = bpm,
addedVersion = added_version,
difficulty = difficulty,
noteDesigner = note_designer,
title=title,
artist=artist,
genre=genre,
bpm=bpm,
addedVersion=added_version,
difficulty=difficulty,
noteDesigner=note_designer,
)
result = self.execute(conflict)
@ -129,50 +147,64 @@ class Mai2StaticData(BaseData):
self.logger.warn(f"Failed to insert song {song_id} chart {chart_id}")
return None
return result.lastrowid
def put_game_ticket(self, version: int, ticket_id: int, ticket_type: int, ticket_price: int, name: str) -> Optional[int]:
def put_game_ticket(
self,
version: int,
ticket_id: int,
ticket_type: int,
ticket_price: int,
name: str,
) -> Optional[int]:
sql = insert(ticket).values(
version = version,
ticketId = ticket_id,
kind = ticket_type,
price = ticket_price,
name = name
version=version,
ticketId=ticket_id,
kind=ticket_type,
price=ticket_price,
name=name,
)
conflict = sql.on_duplicate_key_update(
price = ticket_price
)
conflict = sql.on_duplicate_key_update(price=ticket_price)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert charge {ticket_id} type {ticket_type}")
return None
return result.lastrowid
def get_enabled_tickets(self, version: int, kind: int = None) -> Optional[List[Row]]:
def get_enabled_tickets(
self, version: int, kind: int = None
) -> Optional[List[Row]]:
if kind is not None:
sql = select(ticket).where(and_(
ticket.c.version == version,
ticket.c.enabled == True,
ticket.c.kind == kind
))
sql = select(ticket).where(
and_(
ticket.c.version == version,
ticket.c.enabled == True,
ticket.c.kind == kind,
)
)
else:
sql = select(ticket).where(and_(
ticket.c.version == version,
ticket.c.enabled == True
))
sql = select(ticket).where(
and_(ticket.c.version == version, ticket.c.enabled == True)
)
result = self.execute(sql)
if result is None:return None
if result is None:
return None
return result.fetchall()
def get_music_chart(self, version: int, song_id: int, chart_id: int) -> Optional[List[Row]]:
sql = select(music).where(and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id
))
def get_music_chart(
self, version: int, song_id: int, chart_id: int
) -> Optional[List[Row]]:
sql = select(music).where(
and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id,
)
)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()

View File

@ -8,7 +8,8 @@ from titles.mai2.base import Mai2Base
from titles.mai2.config import Mai2Config
from titles.mai2.const import Mai2Constants
class Mai2Splash(Mai2Base):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH
self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH

View File

@ -8,7 +8,8 @@ from titles.mai2.base import Mai2Base
from titles.mai2.config import Mai2Config
from titles.mai2.const import Mai2Constants
class Mai2SplashPlus(Mai2Base):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS
self.version = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS

View File

@ -8,7 +8,8 @@ from titles.mai2.base import Mai2Base
from titles.mai2.const import Mai2Constants
from titles.mai2.config import Mai2Config
class Mai2Universe(Mai2Base):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE
self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE

View File

@ -8,7 +8,8 @@ from titles.mai2.base import Mai2Base
from titles.mai2.const import Mai2Constants
from titles.mai2.config import Mai2Config
class Mai2UniversePlus(Mai2Base):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS
self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS

View File

@ -7,4 +7,4 @@ index = OngekiServlet
database = OngekiData
reader = OngekiReader
game_codes = [OngekiConstants.GAME_CODE]
current_schema_version = 2
current_schema_version = 2

View File

@ -11,6 +11,7 @@ from titles.ongeki.config import OngekiConfig
from titles.ongeki.database import OngekiData
from titles.ongeki.config import OngekiConfig
class OngekiBattleGrade(Enum):
FAILED = 0
DRAW = 1
@ -21,6 +22,7 @@ class OngekiBattleGrade(Enum):
UNBELIEVABLE_GOLD = 6
UNBELIEVABLE_RAINBOW = 7
class OngekiBattlePointGrade(Enum):
FRESHMAN = 0
KYU10 = 1
@ -45,20 +47,22 @@ class OngekiBattlePointGrade(Enum):
DAN10 = 20
SODEN = 21
class OngekiTechnicalGrade(Enum):
D = 0
C = 1
B = 2
BB = 3
BBB = 4
A = 5
AA = 6
AAA = 7
S = 8
SS = 9
SSS = 10
D = 0
C = 1
B = 2
BB = 3
BBB = 4
A = 5
AA = 6
AAA = 7
S = 8
SS = 9
SSS = 10
SSSp = 11
class OngekiDifficulty(Enum):
BASIC = 0
ADVANCED = 1
@ -66,6 +70,7 @@ class OngekiDifficulty(Enum):
MASTER = 3
LUNATIC = 10
class OngekiGPLogKind(Enum):
NONE = 0
BUY1_START = 1
@ -82,22 +87,28 @@ class OngekiGPLogKind(Enum):
PAY_MAS_UNLOCK = 13
PAY_MONEY = 14
class OngekiBase():
class OngekiBase:
def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None:
self.core_cfg = core_cfg
self.game_cfg = game_cfg
self.data = OngekiData(core_cfg)
self.date_time_format = "%Y-%m-%d %H:%M:%S"
self.date_time_format_ext = "%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5]
self.date_time_format_ext = (
"%Y-%m-%d %H:%M:%S.%f" # needs to be lopped off at [:-5]
)
self.date_time_format_short = "%Y-%m-%d"
self.logger = logging.getLogger("ongeki")
self.game = OngekiConstants.GAME_CODE
self.version = OngekiConstants.VER_ONGEKI
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)
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
)
return {
"gameSetting": {
"dataVersion": "1.00.00",
@ -128,20 +139,53 @@ class OngekiBase():
def handle_get_game_ranking_api_request(self, data: Dict) -> Dict:
return {"length": 0, "gameRankingList": []}
def handle_get_game_point_api_request(self, data: Dict) -> Dict:
"""
Sets the GP ammount for A and B sets for 1 - 3 crdits
"""
return {"length":6,"gamePointList":[
{"type":0,"cost":100,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"},
{"type":1,"cost":200,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"},
{"type":2,"cost":300,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"},
{"type":3,"cost":120,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"},
{"type":4,"cost":240,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"},
{"type":5,"cost":360,"startDate":"2000-01-01 05:00:00.0","endDate":"2099-01-01 05:00:00.0"}
]}
return {
"length": 6,
"gamePointList": [
{
"type": 0,
"cost": 100,
"startDate": "2000-01-01 05:00:00.0",
"endDate": "2099-01-01 05:00:00.0",
},
{
"type": 1,
"cost": 200,
"startDate": "2000-01-01 05:00:00.0",
"endDate": "2099-01-01 05:00:00.0",
},
{
"type": 2,
"cost": 300,
"startDate": "2000-01-01 05:00:00.0",
"endDate": "2099-01-01 05:00:00.0",
},
{
"type": 3,
"cost": 120,
"startDate": "2000-01-01 05:00:00.0",
"endDate": "2099-01-01 05:00:00.0",
},
{
"type": 4,
"cost": 240,
"startDate": "2000-01-01 05:00:00.0",
"endDate": "2099-01-01 05:00:00.0",
},
{
"type": 5,
"cost": 360,
"startDate": "2000-01-01 05:00:00.0",
"endDate": "2099-01-01 05:00:00.0",
},
],
}
def handle_game_login_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1, "apiName": "gameLogin"}
@ -184,11 +228,19 @@ class OngekiBase():
def handle_upsert_user_gplog_api_request(self, data: Dict) -> Dict:
user = data["userId"]
if user >= 200000000000000: # Account for guest play
if user >= 200000000000000: # Account for guest play
user = None
self.data.log.put_gp_log(user, data["usedCredit"], data["placeName"], data["userGplog"]["trxnDate"],
data["userGplog"]["placeId"], data["userGplog"]["kind"], data["userGplog"]["pattern"], data["userGplog"]["currentGP"])
self.data.log.put_gp_log(
user,
data["usedCredit"],
data["placeName"],
data["userGplog"]["trxnDate"],
data["userGplog"]["placeId"],
data["userGplog"]["kind"],
data["userGplog"]["pattern"],
data["userGplog"]["currentGP"],
)
return {"returnCode": 1, "apiName": "UpsertUserGplogApi"}
@ -197,39 +249,53 @@ class OngekiBase():
def handle_get_game_event_api_request(self, data: Dict) -> Dict:
evts = self.data.static.get_enabled_events(self.version)
evt_list = []
for event in evts:
evt_list.append({
"type": event["type"],
"id": event["eventId"],
"startDate": "2017-12-05 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0"
})
return {"type": data["type"], "length": len(evt_list), "gameEventList": evt_list}
evt_list.append(
{
"type": event["type"],
"id": event["eventId"],
"startDate": "2017-12-05 07:00:00.0",
"endDate": "2099-12-31 00:00:00.0",
}
)
return {
"type": data["type"],
"length": len(evt_list),
"gameEventList": evt_list,
}
def handle_get_game_id_list_api_request(self, data: Dict) -> Dict:
game_idlist: list[str, Any] = [] #1 to 230 & 8000 to 8050
game_idlist: list[str, Any] = [] # 1 to 230 & 8000 to 8050
if data["type"] == 1:
for i in range(1,231):
for i in range(1, 231):
game_idlist.append({"type": 1, "id": i})
return {"type": data["type"], "length": len(game_idlist), "gameIdlistList": game_idlist}
return {
"type": data["type"],
"length": len(game_idlist),
"gameIdlistList": game_idlist,
}
elif data["type"] == 2:
for i in range(8000,8051):
for i in range(8000, 8051):
game_idlist.append({"type": 2, "id": i})
return {"type": data["type"], "length": len(game_idlist), "gameIdlistList": game_idlist}
return {
"type": data["type"],
"length": len(game_idlist),
"gameIdlistList": game_idlist,
}
def handle_get_user_region_api_request(self, data: Dict) -> Dict:
return {"userId": data["userId"], "length": 0, "userRegionList": []}
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
profile = self.data.profile.get_profile_preview(data["userId"], self.version)
if profile is None:
if profile is None:
return {
"userId": data["userId"],
"userId": data["userId"],
"isLogin": False,
"lastLoginDate": "0000-00-00 00:00:00",
"userName": "",
@ -240,12 +306,12 @@ class OngekiBase():
"lastGameId": "",
"lastRomVersion": "",
"lastDataVersion": "",
"lastPlayDate": "",
"lastPlayDate": "",
"nameplateId": 0,
"trophyId": 0,
"cardId": 0,
"dispPlayerLv": 0,
"dispRating": 0,
"trophyId": 0,
"cardId": 0,
"dispPlayerLv": 0,
"dispRating": 0,
"dispBP": 0,
"headphone": 0,
"banStatus": 0,
@ -253,7 +319,7 @@ class OngekiBase():
}
return {
"userId": data["userId"],
"userId": data["userId"],
"isLogin": False,
"lastLoginDate": profile["lastPlayDate"],
"userName": profile["userName"],
@ -264,12 +330,12 @@ class OngekiBase():
"lastGameId": profile["lastGameId"],
"lastRomVersion": profile["lastRomVersion"],
"lastDataVersion": profile["lastDataVersion"],
"lastPlayDate": profile["lastPlayDate"],
"lastPlayDate": profile["lastPlayDate"],
"nameplateId": profile["nameplateId"],
"trophyId": profile["trophyId"],
"cardId": profile["cardId"],
"dispPlayerLv": profile["dispPlayerLv"],
"dispRating": profile["dispRating"],
"trophyId": profile["trophyId"],
"cardId": profile["cardId"],
"dispPlayerLv": profile["dispPlayerLv"],
"dispRating": profile["dispRating"],
"dispBP": profile["dispBP"],
"headphone": profile["headphone"],
"banStatus": profile["banStatus"],
@ -297,7 +363,8 @@ class OngekiBase():
def handle_get_user_tech_event_api_request(self, data: Dict) -> Dict:
user_tech_event_list = self.data.item.get_tech_event(data["userId"])
if user_tech_event_list is None: return {}
if user_tech_event_list is None:
return {}
tech_evt = []
for evt in user_tech_event_list:
@ -313,11 +380,11 @@ class OngekiBase():
}
def handle_get_user_tech_event_ranking_api_request(self, data: Dict) -> Dict:
#user_event_ranking_list = self.data.item.get_tech_event_ranking(data["userId"])
#if user_event_ranking_list is None: return {}
# user_event_ranking_list = self.data.item.get_tech_event_ranking(data["userId"])
# if user_event_ranking_list is None: return {}
evt_ranking = []
#for evt in user_event_ranking_list:
# for evt in user_event_ranking_list:
# tmp = evt._asdict()
# tmp.pop("id")
# tmp.pop("user")
@ -331,7 +398,8 @@ class OngekiBase():
def handle_get_user_kop_api_request(self, data: Dict) -> Dict:
kop_list = self.data.profile.get_kop(data["userId"])
if kop_list is None: return {}
if kop_list is None:
return {}
for kop in kop_list:
kop.pop("user")
@ -349,10 +417,10 @@ class OngekiBase():
next_idx = data["nextIndex"]
start_idx = next_idx
end_idx = max_ct + start_idx
if len(song_list[start_idx:]) > max_ct:
next_idx += max_ct
else:
next_idx = -1
@ -360,15 +428,20 @@ class OngekiBase():
"userId": data["userId"],
"length": len(song_list[start_idx:end_idx]),
"nextIndex": next_idx,
"userMusicList": song_list[start_idx:end_idx]
"userMusicList": song_list[start_idx:end_idx],
}
def handle_get_user_item_api_request(self, data: Dict) -> Dict:
kind = data["nextIndex"] / 10000000000
p = self.data.item.get_items(data["userId"], kind)
if p is None:
return {"userId": data["userId"], "nextIndex": -1, "itemKind": kind, "userItemList": []}
if p is None:
return {
"userId": data["userId"],
"nextIndex": -1,
"itemKind": kind,
"userItemList": [],
}
items: list[Dict[str, Any]] = []
for i in range(data["nextIndex"] % 10000000000, len(p)):
@ -381,14 +454,23 @@ class OngekiBase():
xout = kind * 10000000000 + (data["nextIndex"] % 10000000000) + len(items)
if len(items) < data["maxCount"] or data["maxCount"] == 0: nextIndex = 0
else: nextIndex = xout
if len(items) < data["maxCount"] or data["maxCount"] == 0:
nextIndex = 0
else:
nextIndex = xout
return {"userId": data["userId"], "nextIndex": int(nextIndex), "itemKind": int(kind), "length": len(items), "userItemList": items}
return {
"userId": data["userId"],
"nextIndex": int(nextIndex),
"itemKind": int(kind),
"length": len(items),
"userItemList": items,
}
def handle_get_user_option_api_request(self, data: Dict) -> Dict:
o = self.data.profile.get_profile_options(data["userId"])
if o is None: return {}
if o is None:
return {}
# get the dict representation of the row so we can modify values
user_opts = o._asdict()
@ -401,14 +483,17 @@ class OngekiBase():
def handle_get_user_data_api_request(self, data: Dict) -> Dict:
p = self.data.profile.get_profile_data(data["userId"], self.version)
if p is None: return {}
if p is None:
return {}
cards = self.data.card.get_user_cards(data["userId"])
if cards is None or len(cards) == 0:
# This should never happen
self.logger.error(f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}")
self.logger.error(
f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}"
)
return {}
# get the dict representation of the row so we can modify values
user_data = p._asdict()
@ -422,14 +507,14 @@ class OngekiBase():
# add access code that we don't store
user_data["accessCode"] = cards[0]["access_code"]
return {"userId": data["userId"], "userData":user_data}
return {"userId": data["userId"], "userData": user_data}
def handle_get_user_event_ranking_api_request(self, data: Dict) -> Dict:
#user_event_ranking_list = self.data.item.get_event_ranking(data["userId"])
#if user_event_ranking_list is None: return {}
# user_event_ranking_list = self.data.item.get_event_ranking(data["userId"])
# if user_event_ranking_list is None: return {}
evt_ranking = []
#for evt in user_event_ranking_list:
# for evt in user_event_ranking_list:
# tmp = evt._asdict()
# tmp.pop("id")
# tmp.pop("user")
@ -443,7 +528,8 @@ class OngekiBase():
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
user_login_bonus_list = self.data.item.get_login_bonuses(data["userId"])
if user_login_bonus_list is None: return {}
if user_login_bonus_list is None:
return {}
login_bonuses = []
for scenerio in user_login_bonus_list:
@ -451,16 +537,19 @@ class OngekiBase():
tmp.pop("id")
tmp.pop("user")
login_bonuses.append(tmp)
return {
"userId": data["userId"],
"length": len(login_bonuses),
"userLoginBonusList": login_bonuses
"userId": data["userId"],
"length": len(login_bonuses),
"userLoginBonusList": login_bonuses,
}
def handle_get_user_bp_base_request(self, data: Dict) -> Dict:
p = self.data.profile.get_profile(self.game, self.version, user_id = data["userId"])
if p is None: return {}
p = self.data.profile.get_profile(
self.game, self.version, user_id=data["userId"]
)
if p is None:
return {}
profile = json.loads(p["data"])
return {
"userId": data["userId"],
@ -470,7 +559,8 @@ class OngekiBase():
def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict:
recent_rating = self.data.profile.get_profile_recent_rating(data["userId"])
if recent_rating is None: return {}
if recent_rating is None:
return {}
userRecentRatingList = recent_rating["recentRating"]
@ -482,31 +572,35 @@ class OngekiBase():
def handle_get_user_activity_api_request(self, data: Dict) -> Dict:
activity = self.data.profile.get_profile_activity(data["userId"], data["kind"])
if activity is None: return {}
if activity is None:
return {}
user_activity = []
for act in activity:
user_activity.append({
"kind": act["kind"],
"id": act["activityId"],
"sortNumber": act["sortNumber"],
"param1": act["param1"],
"param2": act["param2"],
"param3": act["param3"],
"param4": act["param4"],
})
user_activity.append(
{
"kind": act["kind"],
"id": act["activityId"],
"sortNumber": act["sortNumber"],
"param1": act["param1"],
"param2": act["param2"],
"param3": act["param3"],
"param4": act["param4"],
}
)
return {
"userId": data["userId"],
"userId": data["userId"],
"length": len(user_activity),
"kind": data["kind"],
"userActivityList": user_activity
"userActivityList": user_activity,
}
def handle_get_user_story_api_request(self, data: Dict) -> Dict:
user_stories = self.data.item.get_stories(data["userId"])
if user_stories is None: return {}
if user_stories is None:
return {}
story_list = []
for story in user_stories:
@ -516,14 +610,15 @@ class OngekiBase():
story_list.append(tmp)
return {
"userId": data["userId"],
"length": len(story_list),
"userStoryList": story_list
"userId": data["userId"],
"length": len(story_list),
"userStoryList": story_list,
}
def handle_get_user_chapter_api_request(self, data: Dict) -> Dict:
user_chapters = self.data.item.get_chapters(data["userId"])
if user_chapters is None: return {}
if user_chapters is None:
return {}
chapter_list = []
for chapter in user_chapters:
@ -531,11 +626,11 @@ class OngekiBase():
tmp.pop("id")
tmp.pop("user")
chapter_list.append(tmp)
return {
"userId": data["userId"],
"length": len(chapter_list),
"userChapterList": chapter_list
"userId": data["userId"],
"length": len(chapter_list),
"userChapterList": chapter_list,
}
def handle_get_user_training_room_by_key_api_request(self, data: Dict) -> Dict:
@ -547,7 +642,8 @@ class OngekiBase():
def handle_get_user_character_api_request(self, data: Dict) -> Dict:
user_characters = self.data.item.get_characters(data["userId"])
if user_characters is None: return {}
if user_characters is None:
return {}
character_list = []
for character in user_characters:
@ -555,16 +651,17 @@ class OngekiBase():
tmp.pop("id")
tmp.pop("user")
character_list.append(tmp)
return {
"userId": data["userId"],
"length": len(character_list),
"userCharacterList": character_list
"userId": data["userId"],
"length": len(character_list),
"userCharacterList": character_list,
}
def handle_get_user_card_api_request(self, data: Dict) -> Dict:
user_cards = self.data.item.get_cards(data["userId"])
if user_cards is None: return {}
if user_cards is None:
return {}
card_list = []
for card in user_cards:
@ -572,17 +669,18 @@ class OngekiBase():
tmp.pop("id")
tmp.pop("user")
card_list.append(tmp)
return {
"userId": data["userId"],
"length": len(card_list),
"userCardList": card_list
"userId": data["userId"],
"length": len(card_list),
"userCardList": card_list,
}
def handle_get_user_deck_by_key_api_request(self, data: Dict) -> Dict:
# Auth key doesn't matter, it just wants all the decks
decks = self.data.item.get_decks(data["userId"])
if decks is None: return {}
if decks is None:
return {}
deck_list = []
for deck in decks:
@ -599,7 +697,8 @@ class OngekiBase():
def handle_get_user_trade_item_api_request(self, data: Dict) -> Dict:
user_trade_items = self.data.item.get_trade_items(data["userId"])
if user_trade_items is None: return {}
if user_trade_items is None:
return {}
trade_item_list = []
for trade_item in user_trade_items:
@ -616,7 +715,8 @@ class OngekiBase():
def handle_get_user_scenario_api_request(self, data: Dict) -> Dict:
user_scenerio = self.data.item.get_scenerios(data["userId"])
if user_scenerio is None: return {}
if user_scenerio is None:
return {}
scenerio_list = []
for scenerio in user_scenerio:
@ -624,7 +724,7 @@ class OngekiBase():
tmp.pop("id")
tmp.pop("user")
scenerio_list.append(tmp)
return {
"userId": data["userId"],
"length": len(scenerio_list),
@ -633,7 +733,8 @@ class OngekiBase():
def handle_get_user_ratinglog_api_request(self, data: Dict) -> Dict:
rating_log = self.data.profile.get_profile_rating_log(data["userId"])
if rating_log is None: return {}
if rating_log is None:
return {}
userRatinglogList = []
for rating in rating_log:
@ -650,7 +751,8 @@ class OngekiBase():
def handle_get_user_mission_point_api_request(self, data: Dict) -> Dict:
user_mission_point_list = self.data.item.get_mission_points(data["userId"])
if user_mission_point_list is None: return {}
if user_mission_point_list is None:
return {}
mission_point_list = []
for evt_music in user_mission_point_list:
@ -667,7 +769,8 @@ class OngekiBase():
def handle_get_user_event_point_api_request(self, data: Dict) -> Dict:
user_event_point_list = self.data.item.get_event_points(data["userId"])
if user_event_point_list is None: return {}
if user_event_point_list is None:
return {}
event_point_list = []
for evt_music in user_event_point_list:
@ -684,7 +787,8 @@ class OngekiBase():
def handle_get_user_music_item_api_request(self, data: Dict) -> Dict:
user_music_item_list = self.data.item.get_music_items(data["userId"])
if user_music_item_list is None: return {}
if user_music_item_list is None:
return {}
music_item_list = []
for evt_music in user_music_item_list:
@ -701,7 +805,8 @@ class OngekiBase():
def handle_get_user_event_music_api_request(self, data: Dict) -> Dict:
user_evt_music_list = self.data.item.get_event_music(data["userId"])
if user_evt_music_list is None: return {}
if user_evt_music_list is None:
return {}
evt_music_list = []
for evt_music in user_evt_music_list:
@ -718,7 +823,8 @@ class OngekiBase():
def handle_get_user_boss_api_request(self, data: Dict) -> Dict:
p = self.data.item.get_bosses(data["userId"])
if p is None: return {}
if p is None:
return {}
boss_list = []
for boss in p:
@ -740,7 +846,9 @@ class OngekiBase():
# The isNew fields are new as of Red and up. We just won't use them for now.
if "userData" in upsert and len(upsert["userData"]) > 0:
self.data.profile.put_profile_data(user_id, self.version, upsert["userData"][0])
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0]
)
if "userOption" in upsert and len(upsert["userOption"]) > 0:
self.data.profile.put_profile_options(user_id, upsert["userOption"][0])
@ -751,27 +859,37 @@ class OngekiBase():
if "userActivityList" in upsert:
for act in upsert["userActivityList"]:
self.data.profile.put_profile_activity(user_id, act["kind"], act["id"], act["sortNumber"], act["param1"],
act["param2"], act["param3"], act["param4"])
self.data.profile.put_profile_activity(
user_id,
act["kind"],
act["id"],
act["sortNumber"],
act["param1"],
act["param2"],
act["param3"],
act["param4"],
)
if "userRecentRatingList" in upsert:
self.data.profile.put_profile_recent_rating(user_id, upsert["userRecentRatingList"])
self.data.profile.put_profile_recent_rating(
user_id, upsert["userRecentRatingList"]
)
if "userBpBaseList" in upsert:
self.data.profile.put_profile_bp_list(user_id, upsert["userBpBaseList"])
if "userMusicDetailList" in upsert:
for x in upsert["userMusicDetailList"]:
self.data.score.put_best_score(user_id, x)
if "userCharacterList" in upsert:
for x in upsert["userCharacterList"]:
self.data.item.put_character(user_id, x)
if "userCardList" in upsert:
for x in upsert["userCardList"]:
self.data.item.put_card(user_id, x)
if "userDeckList" in upsert:
for x in upsert["userDeckList"]:
self.data.item.put_deck(user_id, x)
@ -779,43 +897,45 @@ class OngekiBase():
if "userTrainingRoomList" in upsert:
for x in upsert["userTrainingRoomList"]:
self.data.profile.put_training_room(user_id, x)
if "userStoryList" in upsert:
for x in upsert["userStoryList"]:
self.data.item.put_story(user_id, x)
if "userChapterList" in upsert:
for x in upsert["userChapterList"]:
self.data.item.put_chapter(user_id, x)
if "userMemoryChapterList" in upsert:
for x in upsert["userMemoryChapterList"]:
self.data.item.put_memorychapter(user_id, x)
if "userItemList" in upsert:
for x in upsert["userItemList"]:
self.data.item.put_item(user_id, x)
if "userMusicItemList" in upsert:
for x in upsert["userMusicItemList"]:
self.data.item.put_music_item(user_id, x)
if "userLoginBonusList" in upsert:
for x in upsert["userLoginBonusList"]:
self.data.item.put_login_bonus(user_id, x)
if "userEventPointList" in upsert:
for x in upsert["userEventPointList"]:
self.data.item.put_event_point(user_id, x)
if "userMissionPointList" in upsert:
for x in upsert["userMissionPointList"]:
self.data.item.put_mission_point(user_id, x)
if "userRatinglogList" in upsert:
for x in upsert["userRatinglogList"]:
self.data.profile.put_profile_rating_log(user_id, x["dataVersion"], x["highestRating"])
self.data.profile.put_profile_rating_log(
user_id, x["dataVersion"], x["highestRating"]
)
if "userBossList" in upsert:
for x in upsert["userBossList"]:
self.data.item.put_boss(user_id, x)
@ -844,7 +964,7 @@ class OngekiBase():
for x in upsert["userKopList"]:
self.data.profile.put_kop(user_id, x)
return {'returnCode': 1, 'apiName': 'upsertUserAll'}
return {"returnCode": 1, "apiName": "upsertUserAll"}
def handle_get_user_rival_api_request(self, data: Dict) -> Dict:
"""
@ -857,29 +977,28 @@ class OngekiBase():
"length": 0,
"userRivalList": [],
}
return {
"userId": data["userId"],
"length": len(rival_list),
"userRivalList": rival_list._asdict(),
}
def handle_get_user_rival_data_api_reqiest(self, data:Dict) -> Dict:
def handle_get_user_rival_data_api_reqiest(self, data: Dict) -> Dict:
"""
Added in Bright
"""
rivals = []
for rival in data["userRivalList"]:
name = self.data.profile.get_profile_name(rival["rivalUserId"], self.version)
name = self.data.profile.get_profile_name(
rival["rivalUserId"], self.version
)
if name is None:
continue
rivals.append({
"rivalUserId": rival["rival"],
"rivalUserName": name
})
rivals.append({"rivalUserId": rival["rival"], "rivalUserName": name})
return {
"userId": data["userId"],
"length": len(rivals),
@ -893,11 +1012,9 @@ class OngekiBase():
rival_id = data["rivalUserId"]
next_idx = data["nextIndex"]
max_ct = data["maxCount"]
music = self.handle_get_user_music_api_request({
"userId": rival_id,
"nextIndex": next_idx,
"maxCount": max_ct
})
music = self.handle_get_user_music_api_request(
{"userId": rival_id, "nextIndex": next_idx, "maxCount": max_ct}
)
for song in music["userMusicList"]:
song["userRivalMusicDetailList"] = song["userMusicDetailList"]
@ -921,18 +1038,15 @@ class OngekiBase():
tmp = md._asdict()
tmp.pop("user")
tmp.pop("id")
for song in song_list:
if song["userMusicDetailList"][0]["musicId"] == tmp["musicId"]:
found = True
song["userMusicDetailList"].append(tmp)
song["length"] = len(song["userMusicDetailList"])
break
if not found:
song_list.append({
"length": 1,
"userMusicDetailList": [tmp]
})
song_list.append({"length": 1, "userMusicDetailList": [tmp]})
return song_list

View File

@ -31,7 +31,8 @@ class OngekiBright(OngekiBase):
if cards is None or len(cards) == 0:
# This should never happen
self.logger.error(
f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}")
f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}"
)
return {}
# get the dict representation of the row so we can modify values
@ -86,7 +87,7 @@ class OngekiBright(OngekiBase):
"userId": data["userId"],
"length": len(card_list[start_idx:end_idx]),
"nextIndex": next_idx,
"userCardList": card_list[start_idx:end_idx]
"userCardList": card_list[start_idx:end_idx],
}
def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
@ -115,31 +116,26 @@ class OngekiBright(OngekiBase):
"userId": data["userId"],
"length": len(character_list[start_idx:end_idx]),
"nextIndex": next_idx,
"userCharacterList": character_list[start_idx:end_idx]
"userCharacterList": character_list[start_idx:end_idx],
}
def handle_get_user_gacha_api_request(self, data: Dict) -> Dict:
user_gachas = self.data.item.get_user_gachas(data["userId"])
if user_gachas is None:
return {
"userId": data["userId"],
"length": 0,
"userGachaList": []
}
return {"userId": data["userId"], "length": 0, "userGachaList": []}
user_gacha_list = []
for gacha in user_gachas:
tmp = gacha._asdict()
tmp.pop("id")
tmp.pop("user")
tmp["dailyGachaDate"] = datetime.strftime(
tmp["dailyGachaDate"], "%Y-%m-%d")
tmp["dailyGachaDate"] = datetime.strftime(tmp["dailyGachaDate"], "%Y-%m-%d")
user_gacha_list.append(tmp)
return {
"userId": data["userId"],
"length": len(user_gacha_list),
"userGachaList": user_gacha_list
"userGachaList": user_gacha_list,
}
def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
@ -147,21 +143,16 @@ class OngekiBright(OngekiBase):
def handle_cm_get_user_gacha_supply_api_request(self, data: Dict) -> Dict:
# not used for now? not sure what it even does
user_gacha_supplies = self.data.item.get_user_gacha_supplies(
data["userId"])
user_gacha_supplies = self.data.item.get_user_gacha_supplies(data["userId"])
if user_gacha_supplies is None:
return {
"supplyId": 1,
"length": 0,
"supplyCardList": []
}
return {"supplyId": 1, "length": 0, "supplyCardList": []}
supply_list = [gacha["cardId"] for gacha in user_gacha_supplies]
return {
"supplyId": 1,
"length": len(supply_list),
"supplyCardList": supply_list
"supplyCardList": supply_list,
}
def handle_get_game_gacha_api_request(self, data: Dict) -> Dict:
@ -182,29 +173,33 @@ class OngekiBright(OngekiBase):
tmp = gacha._asdict()
tmp.pop("id")
tmp.pop("version")
tmp["startDate"] = datetime.strftime(
tmp["startDate"], "%Y-%m-%d %H:%M:%S")
tmp["endDate"] = datetime.strftime(
tmp["endDate"], "%Y-%m-%d %H:%M:%S")
tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S")
tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S")
tmp["noticeStartDate"] = datetime.strftime(
tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S")
tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S"
)
tmp["noticeEndDate"] = datetime.strftime(
tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S")
tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S"
)
tmp["convertEndDate"] = datetime.strftime(
tmp["convertEndDate"], "%Y-%m-%d %H:%M:%S")
tmp["convertEndDate"], "%Y-%m-%d %H:%M:%S"
)
# make sure to only show gachas for the current version
# so only up to bright, 1140 is the first bright memory gacha
if self.version == OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY:
game_gacha_list.append(tmp)
elif self.version == OngekiConstants.VER_ONGEKI_BRIGHT and tmp["gachaId"] < 1140:
elif (
self.version == OngekiConstants.VER_ONGEKI_BRIGHT
and tmp["gachaId"] < 1140
):
game_gacha_list.append(tmp)
return {
"length": len(game_gacha_list),
"gameGachaList": game_gacha_list,
# no clue
"registIdList": []
"registIdList": [],
}
def handle_roll_gacha_api_request(self, data: Dict) -> Dict:
@ -251,7 +246,7 @@ class OngekiBright(OngekiBase):
assert len(rarity) == 100
# uniform distribution to get the rarity of the card
rolls = [rarity[randint(0, len(rarity)-1)] for _ in range(num_rolls)]
rolls = [rarity[randint(0, len(rarity) - 1)] for _ in range(num_rolls)]
# if SSR book used, make sure you always get one SSR
if book_used == 1:
@ -273,15 +268,9 @@ class OngekiBright(OngekiBase):
gacha_cards = self.data.static.get_gacha_cards(gacha_id)
for card in gacha_cards:
if card["rarity"] == 3:
cards_sr.append({
"cardId": card["cardId"],
"rarity": 2
})
cards_sr.append({"cardId": card["cardId"], "rarity": 2})
elif card["rarity"] == 4:
cards_ssr.append({
"cardId": card["cardId"],
"rarity": 3
})
cards_ssr.append({"cardId": card["cardId"], "rarity": 3})
else:
cards_sr = self.data.static.get_cards_by_rarity(self.version, 2)
cards_ssr = self.data.static.get_cards_by_rarity(self.version, 3)
@ -294,46 +283,39 @@ class OngekiBright(OngekiBase):
for card in gacha_cards:
# make sure to add the cards to the corresponding rarity
if card["rarity"] == 2:
cards_r += [{
"cardId": card["cardId"],
"rarity": 1
}] * chances
cards_r += [{"cardId": card["cardId"], "rarity": 1}] * chances
if card["rarity"] == 3:
cards_sr += [{
"cardId": card["cardId"],
"rarity": 2
}] * chances
cards_sr += [{"cardId": card["cardId"], "rarity": 2}] * chances
elif card["rarity"] == 4:
cards_ssr += [{
"cardId": card["cardId"],
"rarity": 3
}] * chances
cards_ssr += [{"cardId": card["cardId"], "rarity": 3}] * chances
# get the card id for each roll
rolled_cards = []
for i in range(len(rolls)):
if rolls[i] == 1:
rolled_cards.append(cards_r[randint(0, len(cards_r)-1)])
rolled_cards.append(cards_r[randint(0, len(cards_r) - 1)])
elif rolls[i] == 2:
rolled_cards.append(cards_sr[randint(0, len(cards_sr)-1)])
rolled_cards.append(cards_sr[randint(0, len(cards_sr) - 1)])
elif rolls[i] == 3:
rolled_cards.append(cards_ssr[randint(0, len(cards_ssr)-1)])
rolled_cards.append(cards_ssr[randint(0, len(cards_ssr) - 1)])
game_gacha_card_list = []
for card in rolled_cards:
game_gacha_card_list.append({
"gachaId": data["gachaId"],
"cardId": card["cardId"],
# +1 because Card Maker is weird
"rarity": card["rarity"] + 1,
"weight": 1,
"isPickup": False,
"isSelect": False
})
game_gacha_card_list.append(
{
"gachaId": data["gachaId"],
"cardId": card["cardId"],
# +1 because Card Maker is weird
"rarity": card["rarity"] + 1,
"weight": 1,
"isPickup": False,
"isSelect": False,
}
)
return {
"length": len(game_gacha_card_list),
"gameGachaCardList": game_gacha_card_list
"gameGachaCardList": game_gacha_card_list,
}
def handle_cm_upsert_user_gacha_api_request(self, data: Dict):
@ -342,12 +324,12 @@ class OngekiBright(OngekiBase):
gacha_id = data["gachaId"]
gacha_count = data["gachaCnt"]
play_date = datetime.strptime(data["playDate"][:10], '%Y-%m-%d')
play_date = datetime.strptime(data["playDate"][:10], "%Y-%m-%d")
select_point = data["selectPoint"]
total_gacha_count, ceiling_gacha_count = 0, 0
daily_gacha_cnt, five_gacha_cnt, eleven_gacha_cnt = 0, 0, 0
daily_gacha_date = datetime.strptime('2000-01-01', '%Y-%m-%d')
daily_gacha_date = datetime.strptime("2000-01-01", "%Y-%m-%d")
# check if the user previously rolled the exact same gacha
user_gacha = self.data.item.get_user_gacha(user_id, gacha_id)
@ -374,9 +356,11 @@ class OngekiBright(OngekiBase):
selectPoint=select_point,
useSelectPoint=0,
dailyGachaCnt=daily_gacha_cnt + gacha_count,
fiveGachaCnt=five_gacha_cnt+1 if gacha_count == 5 else five_gacha_cnt,
elevenGachaCnt=eleven_gacha_cnt+1 if gacha_count == 11 else eleven_gacha_cnt,
dailyGachaDate=daily_gacha_date
fiveGachaCnt=five_gacha_cnt + 1 if gacha_count == 5 else five_gacha_cnt,
elevenGachaCnt=eleven_gacha_cnt + 1
if gacha_count == 11
else eleven_gacha_cnt,
dailyGachaDate=daily_gacha_date,
)
if "userData" in upsert and len(upsert["userData"]) > 0:
@ -385,11 +369,13 @@ class OngekiBright(OngekiBase):
if p is not None:
# save the bright memory profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0])
user_id, self.version, upsert["userData"][0]
)
else:
# save the bright profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0])
user_id, self.version, upsert["userData"][0]
)
if "userCharacterList" in upsert:
for x in upsert["userCharacterList"]:
@ -407,7 +393,7 @@ class OngekiBright(OngekiBase):
# if "gameGachaCardList" in upsert:
# for x in upsert["gameGachaCardList"]:
return {'returnCode': 1, 'apiName': 'CMUpsertUserGachaApi'}
return {"returnCode": 1, "apiName": "CMUpsertUserGachaApi"}
def handle_cm_upsert_user_select_gacha_api_request(self, data: Dict) -> Dict:
upsert = data["cmUpsertUserSelectGacha"]
@ -419,11 +405,13 @@ class OngekiBright(OngekiBase):
if p is not None:
# save the bright memory profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0])
user_id, self.version, upsert["userData"][0]
)
else:
# save the bright profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0])
user_id, self.version, upsert["userData"][0]
)
if "userCharacterList" in upsert:
for x in upsert["userCharacterList"]:
@ -439,10 +427,10 @@ class OngekiBright(OngekiBase):
user_id,
x["gachaId"],
selectPoint=0,
useSelectPoint=x["useSelectPoint"]
useSelectPoint=x["useSelectPoint"],
)
return {'returnCode': 1, 'apiName': 'cmUpsertUserSelectGacha'}
return {"returnCode": 1, "apiName": "cmUpsertUserSelectGacha"}
def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict:
game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"])
@ -459,7 +447,7 @@ class OngekiBright(OngekiBase):
"rarity": 4,
"weight": 1,
"isPickup": False,
"isSelect": True
"isSelect": True,
},
{
"gachaId": data["gachaId"],
@ -467,7 +455,7 @@ class OngekiBright(OngekiBase):
"rarity": 3,
"weight": 2,
"isPickup": False,
"isSelect": True
"isSelect": True,
},
{
"gachaId": data["gachaId"],
@ -475,7 +463,7 @@ class OngekiBright(OngekiBase):
"rarity": 3,
"weight": 2,
"isPickup": False,
"isSelect": True
"isSelect": True,
},
{
"gachaId": data["gachaId"],
@ -483,7 +471,7 @@ class OngekiBright(OngekiBase):
"rarity": 2,
"weight": 3,
"isPickup": False,
"isSelect": True
"isSelect": True,
},
{
"gachaId": data["gachaId"],
@ -491,7 +479,7 @@ class OngekiBright(OngekiBase):
"rarity": 2,
"weight": 3,
"isPickup": False,
"isSelect": True
"isSelect": True,
},
{
"gachaId": data["gachaId"],
@ -499,12 +487,12 @@ class OngekiBright(OngekiBase):
"rarity": 2,
"weight": 3,
"isPickup": False,
"isSelect": True
}
"isSelect": True,
},
],
"emissionList": [],
"afterCalcList": [],
"ssrBookCalcList": []
"ssrBookCalcList": [],
}
game_gacha_card_list = []
@ -521,7 +509,7 @@ class OngekiBright(OngekiBase):
# again no clue
"emissionList": [],
"afterCalcList": [],
"ssrBookCalcList": []
"ssrBookCalcList": [],
}
def handle_get_game_theater_api_request(self, data: Dict) -> Dict:
@ -548,18 +536,14 @@ class OngekiBright(OngekiBase):
}
"""
return {
"length": 0,
"gameTheaterList": [],
"registIdList": []
}
return {"length": 0, "gameTheaterList": [], "registIdList": []}
def handle_cm_upsert_user_print_playlog_api_request(self, data: Dict) -> Dict:
return {
"returnCode": 1,
"orderId": 0,
"serialId": "11111111111111111111",
"apiName": "CMUpsertUserPrintPlaylogApi"
"apiName": "CMUpsertUserPrintPlaylogApi",
}
def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
@ -567,34 +551,32 @@ class OngekiBright(OngekiBase):
"returnCode": 1,
"orderId": 0,
"serialId": "11111111111111111111",
"apiName": "CMUpsertUserPrintlogApi"
"apiName": "CMUpsertUserPrintlogApi",
}
def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
user_print_detail = data["userPrintDetail"]
# generate random serial id
serial_id = ''.join([str(randint(0, 9)) for _ in range(20)])
serial_id = "".join([str(randint(0, 9)) for _ in range(20)])
# not needed because are either zero or unset
user_print_detail.pop("orderId")
user_print_detail.pop("printNumber")
user_print_detail.pop("serialId")
user_print_detail["printDate"] = datetime.strptime(
user_print_detail["printDate"], '%Y-%m-%d'
user_print_detail["printDate"], "%Y-%m-%d"
)
# add the entry to the user print table with the random serialId
self.data.item.put_user_print_detail(
data["userId"],
serial_id,
user_print_detail
data["userId"], serial_id, user_print_detail
)
return {
"returnCode": 1,
"serialId": serial_id,
"apiName": "CMUpsertUserPrintApi"
"apiName": "CMUpsertUserPrintApi",
}
def handle_cm_upsert_user_all_api_request(self, data: Dict) -> Dict:
@ -607,17 +589,26 @@ class OngekiBright(OngekiBase):
if p is not None:
# save the bright memory profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0])
user_id, self.version, upsert["userData"][0]
)
else:
# save the bright profile
self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0])
user_id, self.version, upsert["userData"][0]
)
if "userActivityList" in upsert:
for act in upsert["userActivityList"]:
self.data.profile.put_profile_activity(
user_id, act["kind"], act["id"], act["sortNumber"],
act["param1"], act["param2"], act["param3"], act["param4"])
user_id,
act["kind"],
act["id"],
act["sortNumber"],
act["param1"],
act["param2"],
act["param3"],
act["param4"],
)
if "userItemList" in upsert:
for x in upsert["userItemList"]:
@ -627,4 +618,4 @@ class OngekiBright(OngekiBase):
for x in upsert["userCardList"]:
self.data.item.put_card(user_id, x)
return {'returnCode': 1, 'apiName': 'cmUpsertUserAll'}
return {"returnCode": 1, "apiName": "cmUpsertUserAll"}

View File

@ -30,14 +30,96 @@ class OngekiBrightMemory(OngekiBright):
def handle_get_user_memory_chapter_api_request(self, data: Dict) -> Dict:
memories = self.data.item.get_memorychapters(data["userId"])
if not memories:
return {"userId": data["userId"], "length":6, "userMemoryChapterList":[
{"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70001, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0},
{"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70002, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0},
{"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70003, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0},
{"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70004, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0},
{"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70005, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0},
{"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70099, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}
]}
return {
"userId": data["userId"],
"length": 6,
"userMemoryChapterList": [
{
"gaugeId": 0,
"isClear": False,
"gaugeNum": 0,
"chapterId": 70001,
"jewelCount": 0,
"isBossWatched": False,
"isStoryWatched": False,
"isDialogWatched": False,
"isEndingWatched": False,
"lastPlayMusicId": 0,
"lastPlayMusicLevel": 0,
"lastPlayMusicCategory": 0,
},
{
"gaugeId": 0,
"isClear": False,
"gaugeNum": 0,
"chapterId": 70002,
"jewelCount": 0,
"isBossWatched": False,
"isStoryWatched": False,
"isDialogWatched": False,
"isEndingWatched": False,
"lastPlayMusicId": 0,
"lastPlayMusicLevel": 0,
"lastPlayMusicCategory": 0,
},
{
"gaugeId": 0,
"isClear": False,
"gaugeNum": 0,
"chapterId": 70003,
"jewelCount": 0,
"isBossWatched": False,
"isStoryWatched": False,
"isDialogWatched": False,
"isEndingWatched": False,
"lastPlayMusicId": 0,
"lastPlayMusicLevel": 0,
"lastPlayMusicCategory": 0,
},
{
"gaugeId": 0,
"isClear": False,
"gaugeNum": 0,
"chapterId": 70004,
"jewelCount": 0,
"isBossWatched": False,
"isStoryWatched": False,
"isDialogWatched": False,
"isEndingWatched": False,
"lastPlayMusicId": 0,
"lastPlayMusicLevel": 0,
"lastPlayMusicCategory": 0,
},
{
"gaugeId": 0,
"isClear": False,
"gaugeNum": 0,
"chapterId": 70005,
"jewelCount": 0,
"isBossWatched": False,
"isStoryWatched": False,
"isDialogWatched": False,
"isEndingWatched": False,
"lastPlayMusicId": 0,
"lastPlayMusicLevel": 0,
"lastPlayMusicCategory": 0,
},
{
"gaugeId": 0,
"isClear": False,
"gaugeNum": 0,
"chapterId": 70099,
"jewelCount": 0,
"isBossWatched": False,
"isStoryWatched": False,
"isDialogWatched": False,
"isEndingWatched": False,
"lastPlayMusicId": 0,
"lastPlayMusicLevel": 0,
"lastPlayMusicCategory": 0,
},
],
}
memory_chp = []
for chp in memories:
@ -49,14 +131,11 @@ class OngekiBrightMemory(OngekiBright):
return {
"userId": data["userId"],
"length": len(memory_chp),
"userMemoryChapterList": memory_chp
"userMemoryChapterList": memory_chp,
}
def handle_get_game_music_release_state_api_request(self, data: Dict) -> Dict:
return {
"techScore": 0,
"cardNum": 0
}
return {"techScore": 0, "cardNum": 0}
def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
# check for a bright memory profile

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